微型代理:一個50行程式碼驅動的MCP代理
新!(25年5月23日)如果您喜歡Python,請檢視配套文章
Python中的微型代理
。
在過去幾周裡,我一直在深入研究MCP(模型上下文協議),以瞭解其熱度究竟為何。
我的簡短總結是,它相當簡單,但仍然非常強大:**MCP是一個標準API,用於暴露一組可以與LLM連線的工具。**
擴充套件Inference Client相當簡單——在Hugging Face,我們有兩個官方客戶端SDK:JS中的@huggingface/inference
和Python中的huggingface_hub
——它們也可以作為MCP客戶端,並將MCP伺服器提供的工具連線到LLM推理中。
但在做這些的時候,我有了第二個認識
一旦有了MCP客戶端,代理實際上就是它之上的一個while迴圈。
在這篇短文中,我將向您介紹我是如何在Typescript (JS) 中實現它的,您也可以如何採用MCP,以及它將如何使代理AI在未來變得更簡單。
如何執行完整演示
如果您安裝了NodeJS(帶pnpm
或npm
),只需在終端中執行此命令
npx @huggingface/mcp-client
或者如果使用pnpm
pnpx @huggingface/mcp-client
這會將我的包安裝到臨時資料夾,然後執行其命令。
您將看到您的簡單代理連線到兩個不同的MCP伺服器(本地執行),載入它們的工具,然後提示您進行對話。
預設情況下,我們的示例代理連線到以下兩個MCP伺服器
- “規範”檔案系統伺服器,它可以訪問您的桌面,
- 和Playwright MCP伺服器,它知道如何為您使用沙盒Chromium瀏覽器。
注意:這有點反直覺,但目前所有MCP伺服器實際上都是本地程序(儘管遠端伺服器即將推出)。
我們第一個影片的輸入是
寫一首關於Hugging Face社群的俳句,並將其儲存到我桌面上的名為“hf.txt”的檔案中
現在讓我們嘗試這個涉及網路瀏覽的提示
在Brave Search上搜索HF推理提供商,並開啟前3個結果
預設模型和提供者
在模型/提供商方面,我們的示例代理預設使用
所有這些都可以透過環境變數配置!請參閱
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
單一倉庫的 mcp-client
子包中,該倉庫是所有我們 JS 庫的 GitHub 單一倉庫。
https://github.com/huggingface/huggingface.js/tree/main/packages/mcp-client
程式碼庫使用現代 JS 特性(特別是非同步生成器),這使得實現變得更加容易,尤其是像 LLM 響應這樣的非同步事件。如果您還不熟悉這些 JS 特性,可能需要向 LLM 詢問。
基礎:LLM中工具呼叫的原生支援。
這篇部落格文章之所以變得非常容易,是因為近期出現的 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` 傳遞給模型,就像任何其他訊息一樣,然後從響應中解析出來(使用模型特定的特殊標記),以將其暴露為工具呼叫。請參閱我們聊天模板遊樂場中的示例。
在InferenceClient之上實現MCP客戶端
既然我們知道了工具在最新的 LLM 中是什麼,那麼讓我們來實現實際的 MCP 客戶端。
官方文件 https://modelcontextprotocol.io/quickstart/client 寫得相當好。您只需將 Anthropic 客戶端 SDK 的任何提及替換為任何其他 OpenAI 相容的客戶端 SDK。(還有一個 llms.txt 您可以將其輸入您選擇的 LLM 中以幫助您進行編碼)。
提醒一下,我們使用 HF 的 InferenceClient
作為我們的推理客戶端。
如果您想跟著實際程式碼學習,完整的
McpClient.ts
程式碼檔案在這裡 🤓
我們的 McpClient
類擁有
- 一個推理客戶端(適用於任何推理提供商,並且
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 提供了一個帶有 listTools()
方法的 Client
類。
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行程式碼的代理🤯
既然我們有了一個能夠連線任意 MCP 伺服器以獲取工具列表,並且能夠注入和解析 LLM 推理結果的 MCP 客戶端,那麼……代理到底是什麼呢?
一旦你有一個帶有一組工具的推理客戶端,那麼一個代理就只是它之上的一個 while 迴圈。
更詳細地說,代理只是以下各項的組合
- 一個系統提示
- 一個 LLM 推理客戶端
- 一個 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: this.availableTools
引數就足夠了。
在 Agent 上載入工具字面上就是連線我們想要的 MCP 伺服器(並行,因為在 JS 中這樣做太容易了)
async loadTools(): Promise<void> {
await Promise.all(this.servers.map((s) => this.addMcpServer(s)));
}
我們添加了兩個額外的工具(MCP 之外),LLM 可以使用它們來控制代理的流程。
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 迴圈。🎉
我們代理主 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 客戶端和一個簡單的構建代理的方式,就有許多很酷的潛在後續步驟🔥
- 嘗試**其他模型**
- mistralai/Mistral-Small-3.1-24B-Instruct-2503 針對函式呼叫進行了最佳化
- Gemma 3 27B,Gemma 3 QAT 模型是函式呼叫的熱門選擇,但由於它不使用原生
tools
,所以需要我們實現工具解析(歡迎提交 PR!)
- 嘗試所有**推理提供商**
- Cerebras, Cohere, Fal, Fireworks, Hyperbolic, Nebius, Novita, Replicate, SambaNova, Together 等。
- 它們每個都對函式呼叫有不同的最佳化(也取決於模型),所以效能可能會有所不同!
- 使用 llama.cpp 或 LM Studio 連線**本地 LLM**
歡迎提交拉取請求和貢獻!再次強調,這裡的一切都是開源的!💎❤️