Agentic Loop与MCP:大模型能力扩展技术解析
一、什么是MCP
MCP(Model Context Protocol)是一种用于大语言模型与外部工具交互的协议框架。它允许大语言模型能够调用各种外部工具来扩展其能力边界,如访问文件系统、搜索引擎、数据库等。
MCP的核心价值
- 能力扩展:使大语言模型突破知识边界和计算能力的限制
- 标准化接口:提供统一的工具调用标准,降低集成成本
- 复杂任务处理:通过递进式工具调用,解决复杂问题
- 灵活扩展:开发者可方便地添加自定义工具
MCP通过宿主应用、客户端、服务器和大语言模型的协作,实现了AI与工具的无缝对接,为AI应用提供了强大的扩展性。
二、各组件职责
宿主应用(Host)
- 提供用户界面,接收用户输入
- 展示AI响应和工具执行结果
- 管理用户会话和界面状态
- 维护对话历史和上下文管理
宿主应用最简实现:
// host.ts - 宿主应用简化实现
import { MCPClient } from './client';class MCPHost {private client: MCPClient;private conversationHistory: any[] = [];constructor() {// 初始化MCP客户端this.client = new MCPClient();}// 应用启动async start() {// 初始化客户端await this.client.initialize();console.log("MCP宿主应用启动成功");}// 处理用户消息async handleUserMessage(userInput: string) {// 添加用户消息到历史this.conversationHistory.push({ role: "user", content: userInput });// 发送消息给客户端处理const response = await this.client.processMessage(userInput, this.conversationHistory);// 添加响应到历史this.conversationHistory.push({ role: "assistant", content: response });// 返回响应给UIreturn response;}
}
MCP客户端(Client)
- 管理注册连接mcp server
- 解析模型输出中的工具调用指令
- 使用stdio标准IO以及RPC规范与mcp server通信让其执行工具方法
注意:实际开发中,推荐使用Model Context Protocol官方SDK,官方提供了Python、TypeScript、Java、Kotlin、C#等多种语言的SDK实现。本文为了更清晰地展示底层实现细节,自行实现了一些核心方法,帮助读者理解MCP客户端的工作原理。
2.1 客户端核心结构
MCP客户端最简实现:
// client.ts - MCP客户端简化实现
import { LLMProvider } from './llm-provider';
import { spawn } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';export class MCPClient {private servers: Map<string, any> = new Map();private availableTools: any[] = [];private maxAgentLoops = 20;private llmProvider: LLMProvider;constructor() {this.llmProvider = new LLMProvider();}// 初始化客户端async initialize() {// 读取MCP服务器配置const configPath = path.join(process.env.HOME || process.env.USERPROFILE || '', 'mcp_config.json');const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));// 连接到每个配置的MCP服务器for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {console.log(`正在连接到MCP服务器: ${serverName}`);try {// 创建与服务器的连接const serverConnection = await this.connectToServer(serverName, serverConfig);this.servers.set(serverName, serverConnection);// 获取服务器提供的工具列表const tools = await this.fetchToolsFromServer(serverConnection);this.availableTools.push(...tools.map(tool => ({...tool,serverName})));console.log(`已连接MCP服务器 ${serverName} 并发现 ${tools.length} 个工具`);} catch (error) {console.error(`连接MCP服务器 ${serverName} 失败:`, error);}}console.log(`已连接 ${this.servers.size} 个MCP服务器,共发现 ${this.availableTools.length} 个工具`);}// 连接到MCP服务器private async connectToServer(serverName: string, config: any) {// 根据配置启动服务器进程const childProcess = spawn(config.command,config.args || [],{cwd: config.cwd || process.cwd(),stdio: ['pipe', 'pipe', 'pipe']});// 简单封装连接对象const serverConnection = {name: serverName,process: childProcess,async sendRequest(request: any) {return new Promise((resolve, reject) => {// 将请求写入标准输入childProcess.stdin.write(JSON.stringify(request) + '\n');// 从标准输出读取响应childProcess.stdout.once('data', (data) => {try {const response = JSON.parse(data.toString());resolve(response);} catch (error) {reject(new Error(`无法解析服务器响应: ${error.message}`));}});// 处理错误childProcess.on('error', reject);});}};return serverConnection;}// 从服务器获取工具列表private async fetchToolsFromServer(serverConnection: any) {const request = {jsonrpc: "2.0",id: this.generateId(),method: "listTools",params: {}};const response = await serverConnection.sendRequest(request);if (response.error) {throw new Error(`获取工具列表失败: ${response.error.message}`);}return response.result.tools || [];}// 处理消息并执行Agentic Loopasync processMessage(userInput: string, history: any[]) {// 准备LLM请求const llmRequest = this.prepareLLMRequest(userInput, history);// 执行Agentic Looplet loopCount = 0;let finalResponse = "";while (loopCount < this.maxAgentLoops) {// 调用LLM获取响应const llmResponse = await this.llmProvider.generateResponse(llmRequest);// 检查是否包含工具调用const toolCalls = this.extractToolCalls(llmResponse);if (toolCalls.length === 0) {// 无工具调用,循环结束finalResponse = llmResponse.content;break;}// 首先添加包含工具调用的assistant消息到历史llmRequest.messages.push({role: "assistant",content: null,tool_calls: toolCalls.map((toolCall, index) => ({id: `call_${Date.now()}_${index}`,type: 'function',function: {name: toolCall.name,arguments: JSON.stringify(toolCall.arguments)}}))});// 执行工具调用for (const toolCall of toolCalls) {const result = await this.callTool(toolCall);// 将工具结果添加到对话历史llmRequest.messages.push({role: "tool",name: toolCall.name,content: JSON.stringify(result)});}loopCount++;}return finalResponse;}// 调用工具async callTool(toolCall: { name: string, arguments: Record<string, any> }) {console.log(`客户端调用工具: ${toolCall.name},参数:`, toolCall.arguments);// 查找支持此工具的服务器const tool = this.availableTools.find(t => t.name === toolCall.name);if (!tool) {throw new Error(`未知工具: ${toolCall.name}`);}const serverConnection = this.servers.get(tool.serverName);if (!serverConnection) {throw new Error(`未找到支持工具 ${toolCall.name} 的服务器`);}// 创建标准JSON-RPC请求const request = {jsonrpc: "2.0",id: this.generateId(),method: "callTool",params: {name: toolCall.name,arguments: toolCall.arguments || {}}};// 发送请求并获取响应const response = await serverConnection.sendRequest(request);// 处理错误if (response.error) {throw new Error(`工具调用失败: ${response.error.message}`);}// 返回结果return response.result;}// 准备LLM请求private prepareLLMRequest(userInput: string, history: any[]) {return {messages: [...history],tools: this.availableTools.map(tool => ({type: "function",function: {name: tool.name,description: tool.description,parameters: {type: tool.inputSchema.type,properties: tool.inputSchema.properties,required: tool.inputSchema.required}}}))};}// 从LLM响应中提取工具调用private extractToolCalls(response: any) {// 简化实现,假设response包含tool_calls属性return response.tool_calls || [];}// 生成唯一IDprivate generateId() {return `req-${Date.now()}-${Math.floor(Math.random() * 1000)}`;}
}
@modelcontextprotocol/sdk 的callTool
方法实现
官方SDK也是基于JSON-RPC 2.0协议实现,是客户端与服务器交互的核心接口:
JSON-RPC 2.0格式
MCP客户端和服务器之间的通信基于JSON-RPC 2.0协议:
请求格式
{"jsonrpc": "2.0","id": "request-123","method": "callTool","params": {"name": "calculator","arguments": {"a": 5,"b": 3,"operation": "multiply"}}
}
成功响应格式
{"jsonrpc": "2.0","id": "request-123","result": {"content": [{"type": "text","text": "15"}],"isError": false}
}
错误响应格式
{"jsonrpc": "2.0","id": "request-123","error": {"code": -32000,"message": "Unknown tool 'calculator'"}
}
大语言模型(LLM)
- 处理用户输入并生成回复
- 根据需要输出标准格式的工具调用指令
- 根据工具执行结果给出自然语言结果或暂时由于信息不足需要调用更多的工具而继续对话
LLM提供者最简实现:
// llm-provider.ts - LLM提供者简化实现
export class LLMProvider {// 向大语言模型发送请求并获取响应async generateResponse(request: any) {console.log("向LLM发送请求:", request);// 这里应该调用实际的LLM API// 为简化示例,返回模拟响应// 模拟LLM可能返回工具调用或直接回答const useTool = Math.random() > 0.5;if (useTool) {return {content: "我需要先获取一些信息...",tool_calls: [{name: "calculator",arguments: {a: 5,b: 3,operation: "multiply"}}]};} else {return {content: "这是我直接生成的回答,不需要使用工具。",tool_calls: []};}}
}
MCP服务器(Server)
- 接收来自Client的RPC工具调用请求
- 管理和执行注册的工具
- 将工具执行结果返回给Client
- 可能包含多种工具的实现或与外部工具服务的连接
MCP服务器最简实现(使用官方SDK):
// mcp-server.js - 基于SDK的MCP服务器简单实现
const { createServer } = require('@modelcontextprotocol/server');// 创建一个MCP服务器实例
const server = createServer({name: 'simple-tools'
});// 注册一个简单的计算器工具
server.registerTool({name: 'calculator',description: '执行简单的数学计算',parameters: {type: 'object',properties: {a: { type: 'number', description: '第一个数字' },b: { type: 'number', description: '第二个数字' },operation: { type: 'string', description: '操作类型',enum: ['add', 'subtract', 'multiply', 'divide']}},required: ['a', 'b', 'operation']},execute: async ({ a, b, operation }) => {let result;switch (operation) {case 'add':result = a + b;break;case 'subtract':result = a - b;break;case 'multiply':result = a * b;break;case 'divide':if (b === 0) {throw new Error('除数不能为零');}result = a / b;break;default:throw new Error(`不支持的操作: ${operation}`);}return { result: result.toString() };}
});// 启动服务器,使用标准输入/输出作为传输层
server.start({ transport: 'stdio' });
console.error('计算器MCP服务器已启动');
三、MCP系统工作流程
3.1 系统初始化过程
系统启动时,会进行以下初始化步骤:
初始化关键步骤
-
Host启动:
- 宿主应用启动,加载mcp server配置文件
- 创建MCP Client实例
-
Client初始化:
- 加载MCPServer配置列表
- 为每个配置创建连接
-
工具发现与注册:
- Client连接到各MCPServer
- 请求并获取每个Server提供的工具列表
- 注册工具信息(名称、描述、输入模式、所属服务器)
- 返回完整工具列表给Host
-
UI初始化:
- Host更新UI展示可用工具
- 准备就绪,等待用户输入
MCP服务器配置示例:
{"mcpServers": {"calculator": {"command": "node","args": ["/absolute/path/to/mcp-server.js" // 请替换为实际的服务器脚本路径]}}
}
注意:请根据实际环境替换服务器脚本路径
- Windows示例: "C:\path\to\mcp-server.js" 或 "C:/path/to/mcp-server.js"
- MacOS/Linux示例: "/Users/username/projects/mcp-server.js" 或 "/home/username/projects/mcp-server.js"
3.2 完整交互流程
MCP的各个组件之间有清晰的调用关系,下图展示了从用户输入到最终输出的完整流程:
四、Agentic Loop循环实现
Agentic Loop是MCP系统实现工具调用的核心机制,它允许模型进行多次连续的工具调用,实现复杂任务分解和递进式解决问题。
4.1 Agentic Loop工作原理
Agentic Loop的核心思想是让大语言模型根据情况决定是直接回答还是通过工具获取更多信息。循环机制使模型能够分步骤解决复杂问题。
4.2 循环控制与终止条件
MCP系统通常设置最大循环次数(如20次)以防止无限循环。循环在以下情况终止:
- 模型返回不包含工具调用的完整回答
- 达到最大循环次数
- 遇到无法处理的错误
4.3 关键步骤详解
-
循环初始化:
- 用户发送请求
- Client构建所有可用工具的大模型请求参数
- Client将用户问题到对话历史以及将可用的MCP工具添加到上下文消息体中
- 设置循环计数器和最大循环次数
-
LLM推理:
- Client向LLM发送完整请求
- LLM返回响应,可能会包含工具调用指令,也可能会直接做出回答,由大模型自主推理是否需要借助工具
-
工具调用处理:
- Client解析响应,检查是否包含工具调用
- 如有工具调用,Client调用@modelcontextprotocol/sdk里client的callTool方法
- 获取mcp server工具执行结果并添加到历史
-
循环继续或终止:
- 若大模型继续返回工具调用且未达到最大循环次数,回到步骤2继续
- 若大模型未返回工具调用或达到最大次数,终止循环,输出大模型最终回答
4.4 对话历史管理
在Agentic Loop中,每轮工具调用的结果都会被添加到对话历史中,这样LLM可以利用之前工具调用的结果进行推理。这种方式使LLM能够参考历史信息,实现多步骤的复杂问题解决。
完整系统协作流程示例:
// example.ts - 展示MCP系统协作
import { MCPHost } from './host';async function runMCPExample() {// 创建并启动宿主应用const host = new MCPHost();await host.start();// 模拟用户提问const userQuestion = "计算5乘以3等于多少?";console.log(`用户: ${userQuestion}`);// 宿主应用处理用户消息const response = await host.handleUserMessage(userQuestion);console.log(`助手: ${response}`);
}// 运行示例
runMCPExample().catch(console.error);
五、最终实现效果
六、总结
MCP的技术实现是一个多层次、多组件协作的过程,它通过标准化的协议实现了大语言模型与外部工具的无缝交互。通过Agentic Loop循环机制,系统能够支持复杂任务的递进式解决,而基于JSON-RPC 2.0协议的通信方式则确保了通信的可靠性和跨平台兼容性。这种设计使得开发者可以轻松地扩展和定制工具功能,从而大幅提升AI应用的能力边界。