Hugging Face 用 50 行代码实现 MCP 驱动的智能体
Tiny Agents: an MCP-powered agent in 50 lines of code
Hugging Face 发布 Tiny Agents 教程,演示如何用约 50 行 TypeScript 代码在推理客户端之上实现一个 MCP 驱动的智能体。
文章给出一个约 50 行的 MCP 智能体实现,读者可据此理解工具调用与 MCP 客户端如何拼装成完整智能体。
新!(2025年5月23日) 如果你更倾向于Python,请查看配套文章
Tiny Agents in Python。
过去几周,我一直在深入研究MCP(模型上下文协议),以了解它周围的热议究竟是怎么回事。
我的简要结论是,它相当简单,但仍然非常强大:MCP是一个标准的API,用于暴露可以挂接到LLM上的工具集。
要扩展推理客户端以同时充当MCP客户端,并将MCP服务器中的可用工具接入LLM推理,这相当简单——在HF,我们有两个官方客户端SDK:JS中的@huggingface/inference和Python中的huggingface_hub。
但在做这件事时,我有了第二个领悟:
一旦你有了MCP客户端,Agent实际上就只是在它之上加一个while循环。
在这篇短文中,我将带你了解我如何用TypeScript(JS)实现它,你如何也能采用MCP,以及它将如何让Agentic AI在未来变得更加简单。
如何运行完整演示
如果你有NodeJS(带有pnpm或npm),只需在终端中运行:
npx @huggingface/mcp-client
或者如果使用pnpm:
pnpx @huggingface/mcp-client
这会将我的包安装到一个临时文件夹,然后执行其命令。
你会看到你的简单Agent连接到两个不同的MCP服务器(本地运行),加载它们的工具,然后提示你进行对话。
默认情况下,我们的示例Agent连接到以下两个MCP服务器:
- “标准”文件系统服务器,它可以访问你的桌面,
- 以及Playwright MCP服务器,它知道如何为你使用沙盒化的Chromium浏览器。
注意:这有点反直觉,但目前所有MCP服务器实际上都是本地进程(尽管远程服务器很快会推出)。
我们在第一个视频中的输入是:
写一首关于HF Mirror社区的俳句,并将其写入我桌面上名为“hf.txt”的文件
现在让我们尝试这个涉及一些网页浏览的提示:
在Brave Search上对HF推理提供商进行网络搜索,并打开前3个结果
默认模型和提供商
在模型/提供商组合方面,我们的示例Agent默认使用:
这一切都可以通过环境变量配置!参见:
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 monorepo的mcp-client子包中,这是我们所有JS库所在的GitHub monorepo。
https://github.com/huggingface/huggingface.js/tree/main/packages/mcp-client
该代码库使用了现代JS特性(特别是异步生成器),这使得实现变得更加容易,尤其是像LLM响应这样的异步事件。 如果你还不熟悉这些JS特性,可能需要向LLM请教一下。
这一切的基础:LLM中原生支持的tool calling。
让整篇博文变得非常容易的是,最近的一批LLM(无论是闭源还是开源)都经过了函数调用(即工具使用)的训练。
工具由其名称、描述以及其参数的JSONSchema表示来定义。 从某种意义上说,它是从外部看到的任何函数接口的不透明表示(意思是,LLM不关心函数实际是如何实现的)。
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传给模型,然后从响应中解析出来(使用模型专用的特殊 token),以工具调用的形式暴露出来。可以看一个例子 在我们的 chat-template playground 中。
在 InferenceClient 之上实现一个 MCP 客户端
现在我们知道了近期 LLM 中的工具是什么,接下来让我们实现真正的 MCP 客户端。
https://modelcontextprotocol.io/quickstart/client 上的官方文档写得相当不错。你只需要把其中提到的 Anthropic 客户端 SDK 替换为任何其他兼容 OpenAI 的客户端 SDK 即可。(还有一个 llms.txt,你可以把它喂给你选择的 LLM,帮助你边写代码边参考)。
提醒一下,我们的推理客户端使用的是 HF 的 InferenceClient。
完整的
McpClient.ts代码文件在这里,如果你想跟着实际代码一起看的话 🤓
我们的 McpClient 类包含:
- 一个 Inference Client(可与任何 Inference Provider 配合使用,并且
huggingface/inference同时支持远程和本地端点) - 一组 MCP 客户端会话,每个已连接的 MCP 服务器对应一个(是的,我们想支持多个服务器)
- 以及一个可用工具列表,它将由已连接的服务器填充,并只是稍作重新格式化。
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 }: { provider: InferenceProvider; model: string; apiKey: string }) {
this.client = new InferenceClient(apiKey);
this.provider = provider;
this.model = model;
}
// [...]
}
要连接到一个 MCP 服务器,官方的 @modelcontextprotocol/sdk/client TypeScript SDK 提供了一个 Client 类,其中有一个 listTools() 方法:
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) => {
return {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
},
} satisfies ChatCompletionInputTool;
})
);
}
StdioServerParameters 是 MCP SDK 中的一个接口,能让你轻松启动一个本地进程:正如我们之前提到的,目前所有 MCP 服务器实际上都是本地进程。
对于我们连接的每个 MCP 服务器,我们会稍作重新格式化其工具列表,并将它们添加到 this.availableTools 中。
如何使用这些工具
很简单,你只需在惯常的消息数组之外,把 this.availableTools 传给 LLM 的聊天补全即可:
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() 方法:
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,
};
/// Get the appropriate session for this tool
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}`;
}
最后,你将得到的工具消息添加到你的 messages 数组,再送回 LLM。
我们 50 行代码的 Agent 🤯
现在我们有了一个 MCP 客户端,能够连接到任意 MCP 服务器以获取工具列表,并能够将它们注入到 LLM 推理中并从中解析出来,那么……什么是 Agent?
一旦你有了一个带有一组工具的推理客户端,那么 Agent 就只是它之上的一个 while 循环。
更详细地说,Agent 就是以下这些的组合:
- 一个系统提示词
- 一个 LLM Inference 客户端
- 一个 MCP 客户端,用于从一堆 MCP 服务器接入一组工具
- 一些基本的控制流(见下面的 while 循环)
完整的
Agent.ts代码文件在这里。
我们的 Agent 类只是简单地继承了 McpClient:
export class Agent extends McpClient {
private readonly servers: StdioServerParameters[];
protected messages: ChatCompletionInputMessage[];
constructor({
provider,
model,
apiKey,
servers,
prompt,
}: {
provider: InferenceProvider;
model: string;
apiKey: string;
servers: StdioServerParameters[];
prompt?: string;
}) {
super({ provider, model, apiKey });
this.servers = servers;
this.messages = [
{
role: "system",
content: prompt ?? DEFAULT_SYSTEM_PROMPT,
},
];
}
}
默认情况下,我们使用一个非常简单的系统提示词,灵感来自GPT-4.1 提示指南中分享的那个。
尽管这来自 OpenAI 😈,但这句话尤其适用于越来越多的模型,无论是闭源还是开源:
我们鼓励开发者专门使用 tools 字段来传递工具,而不是像过去一些人报告的那样,手动将工具描述注入到提示词中并为工具调用编写单独解析器。
也就是说,我们不需要在提示词中提供精心格式化的工具使用示例列表。tools: this.availableTools 参数就足够了。
在 Agent 上加载工具实际上只是连接到我们想要的 MCP 服务器(并行进行,因为在 JS 中这太容易了):
async loadTools(): Promise<void> {
await Promise.all(this.servers.map((s) => this.addMcpServer(s)));
}
我们添加了两个额外的工具(MCP 之外),供 LLM 用于我们 Agent 的控制流:
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];
当调用这些工具中的任何一个时,Agent 将中断其循环,并将控制权交还给用户以获取新输入。
完整的 while 循环
见证我们完整的 while 循环。🎉
我们 Agent 主 while 循环的要点是,我们简单地与 LLM 迭代,在工具调用和向其提供工具结果之间交替,我们这样做直到 LLM 开始连续响应两条非工具消息。
这是完整的 while 循环:
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;
}
}
后续步骤
一旦你有了一个运行的 MCP Client 和一种构建 Agent 的简单方法,就有许多很酷的潜在后续步骤 🔥
- Experiment with other models
- mistralai/Mistral-Small-3.1-24B-Instruct-2503 针对函数调用进行了优化
- Gemma 3 27B,Gemma 3 QAT 模型是函数调用的热门选择,尽管这需要我们实现工具解析,因为它不使用原生
tools(欢迎提交 PR!)
- Experiment with all the Inference Providers:
- Cerebras、Cohere、Fal、Fireworks、Hyperbolic、Nebius、Novita、Replicate、SambaNova、Together 等。
- 它们各自对函数调用有不同的优化(也取决于模型),因此性能可能会有所不同!
- 使用 llama.cpp 或 LM Studio 挂接本地 LLM
欢迎提交 pull request 和贡献! 再次强调,这里的一切都是开源的!💎❤️
来源:Hugging Face Blog · hf-mirror.com