Agent 开发入门

1,467 阅读22分钟

Agent开发技术分享

让我们从Agent的定义开始系统探索:AI Agent(人工智能代理)是一种具备自主性的智能实体,它能够感知环境状态、进行自主决策并执行相应动作。与传统AI系统相比,Agent不仅能被动响应查询,还能主动规划和完成一系列复杂任务。简而言之,若将大型语言模型(LLM)比作一个"超级大脑",那么AI Agent则是为这个大脑配备了"手脚"和"工具"的完整系统,使其能够像人类一样主动采取行动,而不仅限于被动回答问题。

让我们通过一个最小可运行的实例来具体说明:假设用户提出请求:"帮我预订这周五从北京到上海的经济舱机票,价格不超过800元,使用我的招商银行卡付款。"

Agent的工作流程如下:

  1. 感知阶段:检查用户日历以确定"这周五"对应的具体日期(8月15日);访问个人档案以获取身份证号和招商银行卡信息。
  2. 决策阶段:将任务分解为有序的执行链: a. 查询8月15日北京至上海的可用航班 b. 筛选经济舱且价格低于800元的航班选项 c. 锁定合适座位并提交订单 d. 调用银行支付接口完成付款流程
  3. 执行阶段:按序调用航空旅行API和银行支付API,完成机票预订的全过程。
  4. 反馈阶段:将电子出票号和发票发送给用户,并将此次行程记录在个人"行程记忆"中,以便未来自动优先推荐同一航空公司。

接下来,我将从LLM、工具、框架三个方面,带领大家深入了解Agent开发的世界。

LLM(大型语言模型)

首先,我们通过2024年7月发布的国产AI模型Kimi K2的性能基准图,分析当前AI技术发展的关键能力维度:

Kimi K2性能基准图

基准测试详解:

SWE-bench Verified:评估模型解决GitHub上2,294个真实软件工程问题的能力,包括代码修复及确保不影响现有功能。

SWE-bench Multilingual:测试模型在多语言环境下生成代码修改以解决实际GitHub问题的能力。

LiveCodeBench v6:通过超过1,000个编程问题评估模型能力,涵盖简单、中等和困难三个难度级别。

OJBench:基于在线评测系统的编程挑战,测试算法设计、代码生成和问题解决技能。

Tau2-bench weighted average:综合评估模型使用各种工具和API的能力。

AceBench(en):专注于英语编程环境下的算法设计、代码生成和问题解决能力评估。

AIME 2025:测试模型解决一系列高难度数学问题的能力。

GPQA-Diamond:评估模型在科学、技术、工程和数学(STEM)领域的知识理解深度和应用能力。

通过分析这些基准测试,可以识别出未来AI发展的几个关键方向:增强编程和工具使用能力,提升多语言支持能力,以及在数学和科学领域的应用深度。其中,工具使用能力是决定Agent性能的核心要素。

以下为工具使用能力测试的具体示例:

工具使用能力

因此,在Agent开发过程中,选择具备强大工具使用能力的LLM作为任务调度器至关重要。

在Agent开发实践中,除工具使用能力外,还需根据具体应用场景选择适当的LLM,同时综合考量使用成本和响应速度等关键因素。

以下为典型任务场景及相应的模型选择建议:

  • 文案生成:推荐使用GPT-5,其生成内容质量高,但相应成本也较高。
  • 代码生成:建议采用Claude4,该模型在代码生成任务中表现卓越。
  • 翻译或简单数据计算:适合选用本地部署的Llama 2,具有响应速度快和运行成本低的优势。

此外,针对特定模态的输入输出需求,我们需要选择专门的模型进行处理。

OpenAI不同模态的模型

从图中可以明显看出,即便是GPT-5这类先进的通用型语言模型,也未能全面覆盖所有模态的输入输出能力。例如,它目前不支持图片输出、语音输入和输出等功能。因此,对于这些特定模态的需求,必须借助专用模型来实现。具体而言,图片输出任务需使用GPT Image 1模型;而语音输出功能则需借助TTS-1 HD模型。

基于当前技术发展趋势,在Agent开发过程中,依赖单一模型实现所有功能是不切实际的。

接下来,我们从阿里通义千问系列模型中,筛选最适合作为Agent"大脑"的模型。

综合各项性能指标,通义千问 Max 毫无疑问是最佳选择。

工具集成

在接下来的内容中,我们将采用通义千问 Max模型,并遵循OpenAI API标准规范来继续我们的技术流程探讨。

重要说明:OpenAI API已成为LLM领域的事实标准规范。

上下文管理

首先,我们创建一个连续对话示例,以展示Agent在缺乏上下文记忆时的情况。

import dotenv from 'dotenv';
import OpenAI from "openai";

// 加载环境变量
dotenv.config({ quiet: true });

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL
});

const message = [
  '我是奥特曼',
  '猜猜我是谁',
]

for (let i = 0; i < message.length; i++) {
  const completion = await openai.chat.completions.create({
    model:"qwen-max",
    messages: [
      { role: 'user', content: message[i] }
    ],
    temperature: 0,
  })
  console.log(completion.choices[0].message.content);
  console.log('------------------');
}

输出结果:

你好,奥特曼!作为一位来自M78星云的光之巨人,你拥有保护地球和宇宙和平的重要使命。请问有什么特别的任务需要我协助吗?或者,如果你只是想聊聊关于奥特曼的故事、能力或者其他相关的话题,我也非常乐意陪伴!


这个问题有点难倒我了,因为我无法直接知道屏幕另一端的你是谁。不过,如果你愿意,可以告诉我一些关于你的信息,比如你的兴趣爱好、职业等,这样我就能更好地了解你啦!或者,你也可以直接告诉我你是谁,哈哈。


从上述输出可以明显观察到,Agent无法"记住"上一步的输入内容。然而,在Agent的实际操作中,分步执行任务是常见的工作模式。

为确保Agent能够连续执行多步骤任务,我们需要利用messages字段——这是一个数组结构,允许我们将先前的交互信息按时间顺序传递给Agent,从而使其能够维持对话的上下文连续性。

role字段则用于明确标识每条消息的发送者角色,确保对话的逻辑性和准确性。通过这种机制,Agent能够有效跟踪任务进展状态,实现连贯的操作流程。

下表详细说明了不同role类型的含义和功能:

Role描述
system定义AI助手的行为准则、个性特征、交流语调以及专业知识范围,为对话设置基础上下文和行为规范。
user代表用户提出的问题、任务或请求,是用户与AI助手进行交互的主要角色。
assistant代表AI助手对用户输入的响应,展示模型的回复内容和推理过程。

以下是添加上下文管理功能后的完善代码:

import dotenv from 'dotenv';
import OpenAI from "openai";

// 加载环境变量
dotenv.config({ quiet: true });

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL
});

const message = [
    '我是奥特曼',
    '猜猜我是谁',
    '你记得之前的你回答了什么吗?',
]
const context = [
    { role: 'system', content: '你是一个聊天机器人,请准确回答我的问题。' }
];
for (let i = 0; i < message.length; i++) {
    context.push({ role: 'user', content: message[i] });
    const completion = await openai.chat.completions.create({
        model:"qwen-max",
        messages: context,
        temperature: 0,
    })
    const completionMessage = completion.choices[0].message.content;
    context.push({ role: 'assistant', content: completionMessage });
    console.log(completionMessage);
    console.log('------------------');
}

输出结果:

你好,奥特曼!作为一个深受人们喜爱的英雄角色,你代表着正义与勇气。如果你有任何问题或需要讨论有关奥特曼的话题,我很乐意帮助。请告诉我更多你的想法吧!


好的,我来猜猜看!你是奥特曼中的哪一位呢?是赛文奥特曼、迪迦奥特曼、初代奥特曼,还是其他某一位奥特曼呢?或者你是指现实生活中的某个人?给我一些线索吧,这样我能更好地猜到你是谁!


当然记得!在之前的对话中,你告诉我你是奥特曼,然后我回应说你代表着正义与勇气,并且询问你是否有更多想法或问题。接下来,你让我猜猜你是谁,我列出了几位奥特曼的名字,并问你是否能提供一些线索以便我更好地猜测。现在,你能给我一些线索吗?


通过上述代码实现,我们的Agent已具备了基本的记忆能力。这一功能的核心在于我们通过消息数组(messages array)将历史交互信息持续传递给Agent,使其能够参照之前的对话内容来继续当前任务。这种设计架构确保了Agent在处理多步骤复杂任务时能够维持上下文的连贯性,从而显著提升人机交互的自然度和执行效率。

RAG(检索增强生成)

尽管基本的记忆能力为Agent提供了显著优势,但在处理需要大量外部知识或数据支持的任务时,我们需要更先进的技术方案。在此背景下,RAG(Retrieval-Augmented Generation,检索增强生成)技术就显得尤为重要。

RAG是一种融合了信息检索和文本生成的混合技术,它通过检索相关文档或信息片段来增强生成模型的知识储备和输出质量。这种方法特别适用于需要结合大量外部知识来生成准确、全面回答的场景。

一个典型RAG系统的构造流程如下:

RAG 构造流程

  1. 信息提取:从各种信息源中提取原始文本内容
  2. 文本分块:将长文本拆分成具有语义完整性的小块
  3. 向量化处理:将文本块转换为高维向量表示
  4. 向量存储:将生成的向量存储在专门的向量数据库中,以便高效检索

接下来,我们探讨RAG系统如何与LLM进行高效交互:

RAG 与 LLM 交互

  1. 用户请求:用户向系统提出问题或请求
  2. 知识检索:系统根据用户问题在RAG知识库中进行语义检索
  3. 内容返回:RAG系统返回与查询最相关的文档片段或信息
  4. 提示词构建:将检索到的相关内容与原始用户查询组合,形成完整的提示词
  5. 模型生成:将构建的提示词发送给大语言模型,并将模型的生成结果返回给用户

下面我们展示如何在Dify平台中实现RAG功能(在Dify中称为"知识检索"):

1. 创建知识库


2. 组装工作流,重点关注LLM的提示词设计

如图所示,系统将知识检索结果作为上下文信息整合到提示词中,从而增强模型回答的准确性和相关性。

以下是该RAG系统的实际运行效果展示:

从上述截图可以明显看出,通过RAG技术增强的Agent能够准确检索并利用外部知识库中的相关信息,为用户提供精确的答案。

工具调用(Tools)

通过整合上下文理解能力和RAG技术,我们的Agent已具备强大的知识获取能力。接下来,我们将进一步为其配备工具调用功能,以扩展其实际操作能力。

首先,让我们观察未集成工具调用功能的Agent在处理实际任务时的局限性。

import dotenv from 'dotenv';
import OpenAI from "openai";

// 加载环境变量
dotenv.config({ quiet: true });

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL
});
console.log(new Date().toLocaleString())
const message = [
    '现在几点钟',
    '现在是1800年10月1号11点05分,请问1小时后是什么时候?',
]

for (let i = 0; i < message.length; i++) {
    const completion = await openai.chat.completions.create({
        model:"qwen-max",
        messages: [
            { role: 'system', content: '你是一个聊天机器人,请准确回答我的问题。' },
            { role: 'user', content: message[i] }
        ],
        temperature: 0,
    })
    console.log(completion.choices[0].message.content);
    console.log('------------------');
}

执行时间: 2025/7/28 17:27:50

输出结果:

当前时间是北京时间14:20。请注意,这个时间可能与您所在时区的时间有所不同。您可以通过网络查询或设备设置来获取您所在地区的准确时间。


如果现在是1800年10月1日11点05分,那么1小时后的时间就是1800年10月1日12点05分。

从上述输出可以明显看出,未集成工具的LLM甚至无法获取当前时间信息,这凸显了工具调用功能对Agent实用性的重要影响。

接下来,我们实现一个时间工具函数,该函数能够根据指定时区返回格式化的当前时间。

function get_current_time(params) {
  const { timezone = 'Asia/Shanghai' } = params || {};
  const now = new Date();
  const options = {
    timeZone: timezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  };
  const formatter = new Intl.DateTimeFormat('zh-CN', options);
  return formatter.format(now);
}

为使大型语言模型(LLM)在处理任务过程中能够适时暂停并调用特定函数,我们可以利用OpenAI API规范中的tool参数。该参数的主要作用是向LLM明确指示其可调用的工具集合。一旦LLM具备了工具调用能力,它便能在需要时生成相应的工具调用请求,并处理调用后的响应结果。

以下是工具定义的具体实现:

const tools = [
  {
    type: "function",
    function: {
      name: "get_current_time",
      description: "获取当前时间",
      parameters: {
        type: "object",
        properties: {
          timezone: {
            type: "string",
            description: "时区,例如 'Asia/Shanghai'"
          }
        },
        required: []
      }
    }
  }
];

以下是工具调用的完整实现代码:

import dotenv from 'dotenv';
import OpenAI from "openai";

// 加载环境变量
dotenv.config({ quiet: true });

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL
});

// 工具定义
const tools = [
  {
    type: "function",
    function: {
      name: "get_current_time",
      description: "获取当前时间",
      parameters: {
        type: "object",
        properties: {
          timezone: {
            type: "string",
            description: "时区,例如 'Asia/Shanghai'"
          }
        },
        required: []
      }
    }
  }
];

// 工具函数实现
function get_current_time(params) {
  const { timezone = 'Asia/Shanghai' } = params || {};
  const now = new Date();
  const options = {
    timeZone: timezone,
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  };
  const formatter = new Intl.DateTimeFormat('zh-CN', options);
  return formatter.format(now);
}

// 测试消息
const messages = [
  '现在几点钟',
  '现在东京时间几点了?',
  '请告诉我当前时间'
];

async function run() {
  for (let message of messages) {
    console.log(`\n用户: ${message}`);
    const completion = await openai.chat.completions.create({
      model: "qwen-max",
      messages: [
        { role: 'system', content: '你是一个有用的助手,可以使用工具来获取准确的时间信息。' },
        { role: 'user', content: message }
      ],
      tools: tools,
      tool_choice: "auto"
    });
    const messageResponse = completion.choices[0].message;
    console.log(JSON.stringify(messageResponse))
    console.log('------------------');
  }
}

run().catch(console.error);

输出结果:

用户: 现在几点钟

{"content":"","role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{"timezone": "Asia/Shanghai"}"},"index":0,"id":"call_1433bb7f56f244d69df11c","type":"function"}]}


用户: 现在东京时间几点了?

{"content":"","role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{"timezone": "Asia/Tokyo"}"},"index":0,"id":"call_9d1926bc555440ce9791aa","type":"function"}]}


用户: 请告诉我当前时间

{"content":"","role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{"timezone": "Asia/Shanghai"}"},"index":0,"id":"call_da398b9bdde441e08715a0","type":"function"}]}

从上述输出可以观察到,系统返回了一个role为"assistant"的响应消息,其中包含tool_calls字段。该字段表明LLM决定调用get_current_time函数,并已根据工具定义自动将用户查询转换为符合函数签名的参数格式。

接下来,我们实现工具的具体调用逻辑:

    const messageResponse = completion.choices[0].message;
    console.log(JSON.stringify(messageResponse))
    console.log('------------------');
    if (messageResponse.tool_calls) {
        for (const toolCall of messageResponse.tool_calls) {
          const { name, arguments: args } = toolCall.function;
          
          if (name === 'get_current_time') {
            const params = JSON.parse(args);
            const currentTime = get_current_time(params);
            
            // 将工具结果返回给模型
            const finalResponse = await openai.chat.completions.create({
              model: "qwen-max",
              messages: [
                { role: 'system', content: '你是一个有用的助手,可以使用工具来获取准确的时间信息。' },
                { role: 'user', content: message },
                messageResponse,
                {
                  role: 'tool',
                  content: JSON.stringify({ currentTime }),
                  tool_call_id: toolCall.id
                }
              ]
            });
            
            console.log(`AI: ${finalResponse.choices[0].message.content}`);
          }
        }
      } else {
        console.log(`AI: ${messageResponse.content}`);
    }

输出结果:

用户: 现在几点钟

{"content":"","role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{"timezone": "Asia/Shanghai"}"},"index":0,"id":"call_696c682c38a04363a78f49","type":"function"}]}

AI: 现在的时间是2025年8月10日17点44分49秒(亚洲/上海时区)。


用户: 现在东京时间几点了?

{"content":"","role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{"timezone": "Asia/Tokyo"}"},"index":0,"id":"call_1544b90ad3a84241997225","type":"function"}]}

AI: 当前东京时间是2025年8月10日 18:44:52。请注意,这个时间是基于您询问时刻的数据,实际查看时可能存在少许差异。


用户: 请告诉我当前时间

{"content":"","role":"assistant","tool_calls":[{"function":{"name":"get_current_time","arguments":"{"timezone": "Asia/Shanghai"}"},"index":0,"id":"call_02ce4363283a481ab3e901","type":"function"}]}

AI: 当前时间是2025年8月10日17点44分55秒(亚洲/上海时区)。

综上所述,通过在API调用中指定tool参数,我们可以指导LLM在适当时机暂停生成过程,转而调用外部工具来完成特定任务,然后再继续后续的处理流程。这种智能化的工具调用机制,显著增强了LLM的实用性和功能性,使其成为一个更加强大和多功能的智能助手。

MCP(模型上下文协议)

在深入探讨了如何为Agent系统集成工具调用能力之后,我们现在转向一个关键组成部分,它将显著提升工具协调性和使用效率:MCP(Model-Context-Protocol,模型上下文协议)。该协议定义了Agent与外部工具和服务进行交互的标准规范,为Agent提供了一种统一化的方式来识别、请求和使用各种工具。

简单来说,遵守MCP协议的工具服务器会提供统一的接口,使Agent能够了解可用工具及其使用方法;同时,这些服务器还提供标准化的调用接口,极大地方便了Agent对工具的调用和管理。

下面,我们使用MCP协议重新实现上述工具调用功能:

#!/usr/bin/env node

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

// 创建MCP服务器
const server = new Server(
  {
    name: 'mcp-time-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// 定义时间工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'get_current_time',
        description: '获取当前时间',
        inputSchema: {
          type: 'object',
          properties: {
            timezone: {
              type: 'string',
              description: '时区,例如 "Asia/Shanghai", "Asia/Tokyo", "America/New_York"',
            },
            format: {
              type: 'string',
              description: '时间格式,例如 "full", "time", "date"',
              enum: ['full', 'time', 'date'],
              default: 'full'
            }
          },
          required: [],
        },
      },
      {
        name: 'get_timezone_offset',
        description: '获取时区偏移量',
        inputSchema: {
          type: 'object',
          properties: {
            timezone: {
              type: 'string',
              description: '时区,例如 "Asia/Shanghai"',
            },
          },
          required: ['timezone'],
        },
      }
    ],
  };
});

// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case 'get_current_time': {
      const { timezone = 'Asia/Shanghai', format = 'full' } = args || {};
      
      try {
        const now = new Date();
        
        let options = {};
        switch (format) {
          case 'time':
            options = {
              timeZone: timezone,
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
              hour12: false
            };
            break;
          case 'date':
            options = {
              timeZone: timezone,
              year: 'numeric',
              month: '2-digit',
              day: '2-digit'
            };
            break;
          case 'full':
          default:
            options = {
              timeZone: timezone,
              year: 'numeric',
              month: '2-digit',
              day: '2-digit',
              hour: '2-digit',
              minute: '2-digit',
              second: '2-digit',
              hour12: false
            };
        }
        
        const formatter = new Intl.DateTimeFormat('zh-CN', options);
        const timeString = formatter.format(now);
        
        return {
          content: [
            {
              type: 'text',
              text: '当前' + timezone + '时间是: ' + timeString,
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: '获取时间失败: ' + error.message,
            },
          ],
          isError: true,
        };
      }
    }

    case 'get_timezone_offset': {
      const { timezone } = args;
      
      try {
        const now = new Date();
        
        // 获取指定时区的偏移量(分钟)
        const utcDate = new Date(now.getTime() + (now.getTimezoneOffset() * 60000));
        const targetDate = new Date(utcDate.toLocaleString('en-US', { timeZone: timezone }));
        const offset = (targetDate.getTime() - utcDate.getTime()) / (1000 * 60);
        
        const offsetHours = Math.floor(Math.abs(offset) / 60);
        const offsetMinutes = Math.abs(offset) % 60;
        const sign = offset >= 0 ? '+' : '-';
        
        return {
          content: [
            {
              type: 'text',
              text: '时区: ' + timezone + '\nUTC偏移量: ' + sign + offsetHours + ':' + offsetMinutes.toString().padStart(2, '0'),
            },
          ],
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text',
              text: '获取时区信息失败: ' + error.message,
            },
          ],
          isError: true,
        };
      }
    }

    default:
      return {
        content: [
          {
            type: 'text',
            text: '未知工具: ' + name,
          },
        ],
        isError: true,
      };
  }
});

// 启动服务器
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('MCP时间服务器已启动');
}

main().catch((error) => {
  console.error('服务器启动失败:', error);
  process.exit(1);
});

从上述代码可以看出,我们主要实现了两个核心接口:一个用于处理工具列表请求(ListToolsRequestSchema),另一个用于响应工具调用请求(CallToolRequestSchema)。这种标准化的接口设计使得Agent能够轻松发现和使用可用工具。

接下来,我们重构客户端代码,使用MCP协议进行工具调用:

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import OpenAI from 'openai';
import dotenv from 'dotenv';

// 加载环境变量
dotenv.config({ quiet: true });

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
  baseURL: process.env.OPENAI_BASE_URL
});

// MCP 服务器配置
const mcpServerConfig = {
  command: 'node',
  args: ['./Part2.Tool/mcp-time-server.js'], // MCP 时间服务器脚本
  env: {
    'NODE_PATH': process.env.NODE_PATH
  }
};

class MCPToolManager {
  constructor() {
    this.client = new Client({
      name: 'mcp-time-client',
      version: '1.0.0',
    });
    this.transport = null;
  }

  async connect() {
    this.transport = new StdioClientTransport(mcpServerConfig);
    await this.client.connect(this.transport);
    console.log('已连接到MCP时间服务器');
  }

  async disconnect() {
    if (this.transport) {
      await this.transport.close();
      console.log('已断开与MCP服务器的连接');
    }
  }

  async getAvailableTools() {
    const toolsResult = await this.client.listTools();
    return toolsResult.tools;
  }

  async callTool(name, arguments_) {
    const result = await this.client.callTool({
      name,
      arguments: arguments_,
    });
    return result;
  }
}

// 测试消息
const messages = [
  '现在几点钟',
  '现在东京时间几点了?',
  '请告诉我当前时间,用MCP工具'
];

async function run() {
  const mcpManager = new MCPToolManager();
  
  try {
    console.log('=== MCP工具调用示例 ===');
    
    // 连接到MCP服务器
    await mcpManager.connect();
    
    // 获取可用工具
    const tools = await mcpManager.getAvailableTools();
    console.log('可用MCP工具:', tools.map(t => t.name).join(', '));
    
    for (let message of messages) {
      console.log(`\n用户: ${message}`);
      
      // 将MCP工具转换为OpenAI格式
      const openaiTools = tools.map(tool => ({
        type: "function",
        function: {
          name: tool.name,
          description: tool.description,
          parameters: tool.inputSchema
        }
      }));
      
      // 使用OpenAI分析是否需要调用工具
      const completion = await openai.chat.completions.create({
        model: "qwen-max",
        messages: [
          { 
            role: 'system', 
            content: '你是一个有用的助手,可以使用MCP工具来获取准确的时间信息。请根据用户需求选择合适的工具。' 
          },
          { role: 'user', content: message }
        ],
        tools: openaiTools,
        tool_choice: "auto"
      });

      const messageResponse = completion.choices[0].message;

      // 检查是否有工具调用
      if (messageResponse.tool_calls) {
        for (const toolCall of messageResponse.tool_calls) {
          const { name, arguments: args } = toolCall.function;
          
          console.log(`调用MCP工具: ${name}`);
          
          try {
            // 通过MCP调用工具
            const toolResult = await mcpManager.callTool(name, JSON.parse(args));
            
            // 将工具结果返回给模型
            const finalResponse = await openai.chat.completions.create({
              model: "qwen-max",
              messages: [
                { 
                  role: 'system', 
                  content: '你是一个有用的助手,可以使用MCP工具来获取准确的时间信息。' 
                },
                { role: 'user', content: message },
                messageResponse,
                {
                  role: 'tool',
                  content: JSON.stringify(toolResult.content),
                  tool_call_id: toolCall.id
                }
              ]
            });
            
            console.log(`AI: ${finalResponse.choices[0].message.content}`);
          } catch (error) {
            console.error(`调用MCP工具时出错: ${error.message}`);
            console.log(`AI: 抱歉,获取时间信息时遇到了问题。`);
          }
        }
      } else {
        console.log(`AI: ${messageResponse.content}`);
      }
      
      console.log('------------------');
    }
    
  } catch (error) {
    console.error('MCP连接错误:', error);
  } finally {
    // 断开连接
    await mcpManager.disconnect();
  }
}

// 如果直接运行此文件
if (import.meta.url === `file://${process.argv[1]}`) {
  run().catch(console.error);
}

export { MCPToolManager };

执行结果:

=== MCP工具调用示例 ===

MCP时间服务器已启动

已连接到MCP时间服务器

可用MCP工具: get_current_time, get_timezone_offset

用户: 现在几点钟

调用MCP工具: get_current_time

AI: 当前Asia/Shanghai时间是: 19:36:42。请注意,这个时间是根据您询问时的情况给出的,实际时间可能会有所不同。


用户: 现在东京时间几点了?

调用MCP工具: get_current_time

AI: 当前东京时间是:2025年8月10日 20:36:45。请注意,这个时间是基于您询问时刻的数据,实际查看时请考虑可能存在的短暂延迟。


用户: 请告诉我当前时间,用MCP工具

调用MCP工具: get_current_time

AI: 看起来您提供的信息是关于调用MCP工具get_current_time的结果。根据给出的数据,当前在时区Asia/Shanghai的时间为2025年8月10日 19点36分49秒。

请注意,这个时间点实际上是在未来,可能是示例或测试数据。如果您需要获取实时准确的时间,请确保使用最新的API调用来获得正确的值。如果需要进一步的帮助或者有其他问题,请告诉我!


已断开与MCP服务器的连接

开发框架

在深入理解了LLM和工具调用机制后,我们现在将焦点转向Agent开发的核心组成部分——开发框架。接下来,我们将探讨两种具有不同特点的开发框架:LangChain和Dify。这些框架为开发者提供了快速构建、管理和扩展Agent能力的系统性解决方案。

LangChain框架

LangChain is a framework for developing applications powered by large language models (LLMs). (LangChain是一个用于开发由大型语言模型驱动的应用程序的框架。)

LangChain为开发者提供了构建应用所需的基础抽象层和LangChain表达式语言(LCEL)。通过使用LangChain,开发者能够将注意力集中在业务逻辑实现上,而无需过多关注LLM层的实现细节。

下面,我们使用LangChain框架来实现前文讨论的记忆功能。

import dotenv from 'dotenv';
import { ChatOpenAI } from '@langchain/openai';
import { ConversationChain } from 'langchain/chains';
import { BufferMemory } from 'langchain/memory';

// 加载环境变量
dotenv.config({ quiet: true });

// 初始化 OpenAI 模型
const model = new ChatOpenAI({
  model: "qwen-max",
  temperature: 0,
  openAIApiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL
  }
});

// 创建记忆管理器
const memory = new BufferMemory({
  memoryKey: "history",
  returnMessages: true
});

// 创建对话链
const chain = new ConversationChain({
  llm: model,
  memory: memory,
  systemMessage: "你是一个聊天机器人,请准确回答我的问题。"
});

const message = [
    '我是奥特曼',
    '猜猜我是谁',
    '你记得之前的你回答了什么吗?'
];

for (let i = 0; i < message.length; i++) {
    const response = await chain.invoke({ input: message[i] });
    console.log(response.response);
    console.log('------------------');
}

输出结果:

你好,奥特曼!很高兴见到你。作为来自M78星云的光之巨人,你一定有很多精彩的故事和经历想要分享吧。无论是对抗怪兽、保护地球还是传播和平与正义的信息,你的事迹都激励了无数人。有什么特别的任务或故事是你最近在关注的吗?或者,如果需要帮助解决什么问题,我也很乐意提供支持哦!


嘿,这个猜谜游戏听起来很有趣!不过,根据之前的对话,你刚刚提到你是奥特曼。如果你现在是在玩一个不同的角色或者想要我猜测另一个身份的话,请给我一些提示吧!比如,你可以告诉我这个角色的一些特征、他们来自哪里、做了什么特别的事情等信息。这样我就能更好地参与这个游戏了。


当然记得!在之前的对话中,我先是欢迎了自称是奥特曼的你,并表达了对你的敬意以及对你所做贡献的认可。接着,我询问了你是否有特别的故事或任务想要分享,或者是需要帮助解决什么问题。然后,在你提出了"猜猜我是谁"的游戏后,我回应说这个游戏很有趣,但基于我们之前的交流我知道你是奥特曼。同时我也表示如果这是另一个角色扮演游戏的话,我很乐意参与进来,并请求你能提供一些关于这个新身份的线索。希望这些信息准确反映了我们的对话内容。如果你有其他想法或者想继续玩这个游戏,请告诉我更多的细节吧!


从上述代码可以明显看出,LangChain框架已将模型功能和记忆管理功能进行了高度抽象。开发者只需通过简单的组件组合,即可快速构建功能完备的应用:

const chain = new ConversationChain({
  llm: model,
  memory: memory,
  systemMessage: "你是一个聊天机器人,请准确回答我的问题。"
});

通过这种模块化的设计方式,一个具备记忆功能的Agent便可以轻松创建并投入使用。

接下来,我们将使用LangChain框架实现一个简化版的Claude Code编码助手。

要构建一个类似Claude Code的编码助手,我们需要一个能够读写文件并进行代码分析的工具集。

Serena项目地址

Serena是一个功能强大的编码Agent工具包,提供语义检索与编辑能力(支持MCP Server与Agno集成):

  • 🚀 核心功能:Serena是一款强大的编码Agent工具包,能够将LLM转换为可直接在代码库上工作的全功能Agent。与大多数工具不同,它不绑定特定LLM、框架或界面,具有极高的使用灵活性。
  • 🔧 技术优势:Serena提供IDE级别的语义代码检索与编辑工具,可在符号级别提取代码实体并利用关系结构。与现有编码Agent配合使用时,可显著提升token效率。
  • 🆓 开源特性:Serena完全免费且开源,能够零成本增强现有大模型的能力。

根据项目文档,我们可以通过以下命令在本地启动一个使用SSE(Server-Sent Events)方式传输的Serena MCP服务:

uvx --from git+https://github.com/oraios/serena serena start-mcp-server --transport sse --port 9121 --project /Users/lulala/code

执行上述命令后,一个功能强大的编码MCP服务就搭建完成。接下来,我们将展示如何使用LangChain框架接入该MCP服务。

// 导入所需的依赖库
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
import {SSEClientTransport} from '@modelcontextprotocol/sdk/client/sse.js';
import {loadMcpTools} from '@langchain/mcp-adapters';
import {createReactAgent} from '@langchain/langgraph/prebuilt';
import {ChatOpenAI} from '@langchain/openai';
import readline from 'readline';
import dotenv from 'dotenv';

dotenv.config({ quiet: true });

接下来,我们使用Model Context Protocol SDK定义MCP连接方式:

const initSseClient = async () => {
    const baseUrl = new URL('http://localhost:9121/sse');
    let client = new Client({
        name: 'sse-client',
        version: '1.0.0'
    });
    const sseTransport = new SSEClientTransport(baseUrl);
    await client.connect(sseTransport);
    return client;
};

然后,我们创建Agent实例。这里选择更适合编程任务的qwen3-coder-plus作为基础模型,并从MCP客户端获取所有可用工具,将其传递给Agent:

const createAgent = async () => {
  const model = new ChatOpenAI({
    modelName: 'qwen3-coder-plus',
    temperature: 0.7,
    openAIApiKey: process.env.OPENAI_API_KEY,
    configuration: {
        baseURL: process.env.OPENAI_BASE_URL,
    },
  });
  const stdioClient = await initSseClient();
  const tools = await loadMcpTools('code-server', stdioClient);
  const agent = createReactAgent({
    llm: model,
    tools
  });
  return { agent, stdioClient };
};

ReAct策略说明:ReAct(Reasoning + Acting)是一套广泛应用的Agent策略,其核心包含三个阶段:思考(Thought)、行动(Action)和观察(Observation)。此外,该策略还包含一个暂停(Pause)机制,其主要目的是在需要执行外部工具时暂停生成过程,将控制权交回给系统执行相应操作。当工具执行完毕后,控制权再次返回给大模型继续生成过程。

Answer the following questions as best you can. You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought:{agent_scratchpad}

其他常用Agent策略

  • Plan-and-Execute(先计划后执行)
  • CrewAI / AutoGen(多Agent协作)

...

最后,我们实现用户输入处理和Agent调用逻辑:

const processUserInput = async (agent, userInput) => {
  try {
    console.log('\n正在处理您的请求...\n');
    const inputs = {
        messages: [{ role: "user", content: userInput }],
    };
    // 执行Agent流式处理
    const stream = await agent.stream(inputs, { streamMode: "values", recursionLimit: 1000 });
    for await (const { messages } of stream) {
        console.log(messages[messages.length-1].content);
    }
  } catch (error) {
    console.error('处理请求时出错:', error.message);
  }
};

通过上述代码实现,我们成功构建了一个能够完成简单编程任务的编码Agent!

Dify平台

Dify是一个开源的LLM应用开发平台,将"后端即服务(BaaS)"与"LLMOps"理念融合,打造了可视化的拖拽式开发环境。简而言之,开发者无需编写代码,即可在3分钟内部署集成知识库的聊天机器人、AI工作流或Agent系统。

核心特性

  • 可视化编排:通过拖拽操作即可完成Prompt设计、RAG构建、Agent配置和工作流编排
  • 模型即插即用:支持OpenAI、Claude、本地Llama/Ollama等100+种模型的一键切换
  • 企业级功能:提供完整的API/SDK、权限管理、操作审计和多环境一键发布能力
  • 开源生态:完全开源,支持自托管部署,同时提供云服务版本

Dify: 企业级Agentic AI解决方案开发平台

框架对比分析

对于开发者而言,LangChain是开发工具,而Dify则是成品产品。Dify通过可视化拖拽操作显著降低了开发门槛,但同时也为复杂业务需求设置了限制:当预设节点无法满足特定业务场景时,开发者的扩展能力将受到极大限制。

相比之下,LangChain将完全控制权交还给开发者代码层面,允许像搭积木一样自由组合各种模块(模型、检索器、工具、记忆管理),用少量代码即可实现Dify中无法支持的定制化功能——例如自定义多步推理逻辑、动态模型切换,或将RAG流程无缝集成到现有微服务架构中。

核心观点:开发者追求的是"可深度定制"的灵活性,而非"表面上快速"的妥协方案。此外,基于代码的开发方式还有一个显著优势:可以借助AI辅助编程工具提高开发效率,在保持灵活性的同时不牺牲开发速度。