引言
过去几周,我深入研究了MCP(Model Context Protocol,模型上下文协议),试图理解它为何备受关注。我的总结是:MCP简单却强大,它是一个标准API,用于暴露可以连接到大语言模型(LLM)的工具集。
通过扩展推理客户端(Hugging Face提供了两个官方SDK:JavaScript的@huggingface/inference和Python的huggingface_hub),可以轻松将其变为MCP客户端,将MCP服务器中的工具接入LLM推理。
在实现过程中,我有了第二个发现:一旦拥有MCP客户端,AI代理本质上只是一个简单的循环。本文将通过TypeScript(JavaScript)示例,展示如何实现这一过程,介绍如何采用MCP,并探讨它如何让Agentic AI开发变得更简单。
如何运行完整示例
如果你有NodeJS(支持pnpm或npm),只需在终端运行以下命令:
bash
npx @huggingface/mcp-client
或使用pnpm:
bash
pnpx @huggingface/mcp-client
这会将我的软件包安装到一个临时文件夹并执行其命令。你将看到一个简单的代理连接到两个本地运行的MCP服务器,加载它们的工具,然后提示你进行对话。
默认MCP服务器
默认情况下,示例代理会连接以下两个MCP服务器:
- 文件系统服务器:访问你的桌面文件。Playwright MCP服务器:使用沙盒化的Chromium浏览器进行网页操作。
注意:目前所有MCP服务器都是本地进程,但远程服务器支持即将来临。
示例输入
我们测试了以下两个输入:
- 文件操作:
“写一首关于Hugging Face社区的俳句,并将其写入桌面上的hf.txt文件。”网页浏览:
“在Brave Search上搜索HF推理提供商,并打开前三个结果。”
默认模型和提供商
示例代理默认使用:
- 模型:Qwen/Qwen2.5-72B-Instruct提供商:Nebius
这些参数可通过环境变量配置:
javascript
const agent = new Agent({ provider: process.env.PROVIDER ?? "nebius", model: process.env.MODEL_ID ?? "Qwen/Qwen2.5-72B-Instruct", apiKey: process.env.HF_TOKEN, servers: SERVERS,});
代码存储位置
Tiny Agent的代码位于huggingface.js mono-repo的mcp-client子包中,这是Hugging Face所有JavaScript库的GitHub仓库。
代码使用了现代JavaScript特性(尤其是异步生成器),这让异步事件(如LLM响应)的实现更加简单。如果你不熟悉这些特性,可以咨询LLM以获取更多信息。
基础:LLM的原生工具调用支持
本文的核心在于,当前主流LLM(无论是闭源还是开源)都已训练支持函数调用(即工具使用)。工具通过以下方式定义:
- 名称描述参数的JSONSchema表示
这是一种不透明的函数接口表示,LLM并不关心函数的具体实现。例如:
javascript
const weatherTool = { type: "function", function: { name: "get_weather", description: "Get current temperature for a given location.", parameters: { type: "object", properties: { location: { type: "string", description: "City and country e.g. Bogotá, Colombia", }, }, }, },};
参考文档:OpenAI的函数调用文档(链接)。是的,OpenAI几乎为整个社区定义了LLM标准。
推理引擎允许在调用LLM时传入工具列表,LLM可以选择调用零个、一个或多个工具。开发者需要执行这些工具并将结果反馈给LLM以继续生成。
在后端,工具以特殊格式的chat_template传递给模型,然后通过模型特定的特殊标记从响应中解析为工具调用。
在InferenceClient上实现MCP客户端
了解了LLM中的工具概念后,我们来实现MCP客户端。
官方文档(modelcontextprotocol.io/quickstart/…)写得很清晰。你只需将其中提到的Anthropic客户端SDK替换为任何兼容OpenAI的客户端SDK即可。我们使用Hugging Face的InferenceClient作为推理客户端。
完整代码:McpClient.ts(链接 (#))。
McpClient类结构
McpClient类包含:
- 推理客户端:支持任何推理提供商(huggingface/inference支持远程和本地端点)。MCP客户端会话:为每个连接的MCP服务器创建一个会话(支持多个服务器)。可用工具列表:从连接的服务器获取并稍作格式调整后存储。
javascript
export class McpClient { protected client: InferenceClient; protected provider: string; protected model: string; private clients: Map<ToolName, Client> = new Map(); public readonly availableTools: ChatCompletionInputTool[] = []; constructor({ provider, model, apiKey }) { this.client = new InferenceClient(apiKey); this.provider = provider; this.model = model; }}
连接MCP服务器
MCP官方SDK(@modelcontextprotocol/sdk/client)提供了Client类,其中的listTools()方法可以列出服务器的工具:
javascript
async addMcpServer(server: StdioServerParameters): Promise<void> { const transport = new StdioClientTransport({ ...server, env: { ...server.env, PATH: process.env.PATH ?? "" }, }); const mcp = new Client({ name: "@huggingface/mcp-client", version: packageVersion }); await mcp.connect(transport); const toolsResult = await mcp.listTools(); debug( "Connected to server with tools:", toolsResult.tools.map(({ name }) => name) ); for (const tool of toolsResult.tools) { this.clients.set(tool.name, mcp); } this.availableTools.push( ...toolsResult.tools.map((tool) => ({ type: "function", function: { name: tool.name, description: tool.description, parameters: tool.inputSchema, }, })) );}
StdioServerParameters是MCP SDK提供的接口,用于启动本地进程(目前MCP服务器均为本地进程)。
如何使用工具
使用工具很简单,只需将this.availableTools传递给LLM的chatCompletion接口,与消息数组一起提交:
javascript
const stream = this.client.chatCompletionStream({ provider: this.provider, model: this.model, messages, tools: this.availableTools, tool_choice: "auto",});
tool_choice: "auto"允许LLM生成零个、一个或多个工具调用。
在解析或流式处理输出时,LLM会生成工具调用(包括函数名称和JSON编码的参数)。开发者需要执行这些工具,MCP客户端SDK提供了client.callTool()方法:
javascript
const toolName = toolCall.function.name;const toolArgs = JSON.parse(toolCall.function.arguments);const toolMessage: ChatCompletionInputMessageTool = { role: "tool", tool_call_id: toolCall.id, content: "", name: toolName,};const client = this.clients.get(toolName);if (client) { const result = await client.callTool({ name: toolName, arguments: toolArgs }); toolMessage.content = result.content[0].text;} else { toolMessage.content = `Error: No session found for tool: ${toolName}`;}
最后,将工具执行结果添加到消息数组中并反馈给LLM。
AI代理的本质:一个简单的循环
有了支持工具的推理客户端后,AI代理本质上只是其上的一个while循环。
具体来说,AI代理包括:
- 系统提示词LLM推理客户端MCP客户端,用于从多个MCP服务器接入工具基本控制流(while循环)
完整代码:Agent.ts(链接 (#))。
Agent类
Agent类继承自McpClient:
javascript
export class Agent extends McpClient { private readonly servers: StdioServerParameters[]; protected messages: ChatCompletionInputMessage[]; constructor({ provider, model, apiKey, servers, prompt }) { super({ provider, model, apiKey }); this.servers = servers; this.messages = [ { role: "system", content: prompt ?? DEFAULT_SYSTEM_PROMPT, }, ]; }}
默认使用一个简单的系统提示词(参考GPT-4.1提示指南)。OpenAI建议开发者直接使用tools字段传递工具,而无需手动将工具描述注入提示词中。
加载工具
加载工具只需并行连接到所需的MCP服务器:
javascript
async loadTools(): Promise<void> { await Promise.all(this.servers.map((s) => this.addMcpServer(s)));}
此外,我们添加了两个额外的控制流工具:
- task_complete:任务完成时调用,退出循环。ask_question:向用户提问以获取更多信息,退出循环。
javascript
const taskCompletionTool: ChatCompletionInputTool = { type: "function", function: { name: "task_complete", description: "Call this tool when the task given by the user is complete", parameters: { type: "object", properties: {} }, },};const askQuestionTool: ChatCompletionInputTool = { type: "function", function: { name: "ask_question", description: "Ask a question to the user to get more info required to solve or clarify their problem.", parameters: { type: "object", properties: {} }, },};const exitLoopTools = [taskCompletionTool, askQuestionTool];
完整的While循环
以下是代理的核心while循环:
javascript
let numOfTurns = 0;let nextTurnShouldCallTools = true;while (true) { try { yield* this.processSingleTurnWithTools(this.messages, { exitLoopTools, exitIfFirstChunkNoTool: numOfTurns > 0 && nextTurnShouldCallTools, abortSignal: opts.abortSignal, }); } catch (err) { if (err instanceof Error && err.message === "AbortError") { return; } throw err; } numOfTurns++; const currentLast = this.messages.at(-1)!; if ( currentLast.role === "tool" && currentLast.name && exitLoopTools.map((t) => t.function.name).includes(currentLast.name) ) { return; } if (currentLast.role !== "tool" && numOfTurns > MAX_NUM_TURNS) { return; } if (currentLast.role !== "tool" && nextTurnShouldCallTools) { return; } if (currentLast.role === "tool") { nextTurnShouldCallTools = false; } else { nextTurnShouldCallTools = true; }}
循环的核心逻辑是:代理与LLM交替进行工具调用和结果反馈,直到LLM连续生成两个非工具消息,或者调用控制流工具(task_complete或ask_question)。
下一步
有了运行的MCP客户端和简单的代理构建方法后,可以探索更多可能性:
尝试其他模型:
- mistralai/Mistral-Small-3.1-24B-Instruct-2503:优化了函数调用。Gemma 3 27B:适合函数调用,但需实现工具解析(欢迎PR!)。
尝试其他推理提供商:
- Cerebras、Cohere、Fal、Fireworks、Hyperbolic、Nebius、Novita、Replicate、SambaNova、Together等。
接入本地LLM:使用llama.cpp或LM Studio。
项目完全开源,欢迎提交PR和贡献!
结论
通过MCP和现代LLM的工具调用支持,构建AI代理变得异常简单。Tiny Agents展示了一个不到50行代码的实现,结合MCP客户端和一个简单的while循环即可完成。未来,MCP的扩展(如远程服务器支持)将进一步简化Agentic AI的开发流程。