微型代理:一個50行程式碼驅動的MCP代理

釋出於2025年4月25日
在 GitHub 上更新

新!(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在未來變得更簡單。

meme

圖片來源 https://x.com/adamdotdev

如何執行完整演示

如果您安裝了NodeJS(帶pnpmnpm),只需在終端中執行此命令

npx @huggingface/mcp-client

或者如果使用pnpm

pnpx @huggingface/mcp-client

這會將我的包安裝到臨時資料夾,然後執行其命令。

您將看到您的簡單代理連線到兩個不同的MCP伺服器(本地執行),載入它們的工具,然後提示您進行對話。

預設情況下,我們的示例代理連線到以下兩個MCP伺服器

注意:這有點反直覺,但目前所有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 客戶端和一個簡單的構建代理的方式,就有許多很酷的潛在後續步驟🔥

  • 嘗試**其他模型**
  • 嘗試所有**推理提供商**
    • Cerebras, Cohere, Fal, Fireworks, Hyperbolic, Nebius, Novita, Replicate, SambaNova, Together 等。
    • 它們每個都對函式呼叫有不同的最佳化(也取決於模型),所以效能可能會有所不同!
  • 使用 llama.cpp 或 LM Studio 連線**本地 LLM**

歡迎提交拉取請求和貢獻!再次強調,這裡的一切都是開源的!💎❤️

社群

很棒🔥

🔥🔥🔥

我們真的不配擁有你

感謝這篇帖子!非常有用,解釋得很好。我之前不太明白 MCP 炒作的原因,現在清楚了一點!

在執行第一個“npx”命令之前需要設定 HF_TOKEN。

解釋得非常清楚,很有用!謝謝!

很酷!我非常喜歡你儘可能簡化代理的想法,我認為這作為基線很有用。“這個複雜的代理框架真的比簡單的方法更能提高效能嗎?”這個問題對我來說並沒有一個顯而易見的答案。

我擅自將您的 JavaScript 程式碼轉換為 Python,並將其實現到 any-agent 中,這樣就可以輕鬆切換 TinyAgent 和 SmolAgent,以檢視效能差異:https://github.com/mozilla-ai/any-agent/blob/main/src/any_agent/frameworks/tinyagent.py

·
文章作者

哇,這太酷了!

這是一個非常酷的想法,
我已將您的 JavaScript 程式碼轉換為 Python,
它是一個簡單的迴圈 + LiteLLM + MCP

https://github.com/askbudi/tinyagent

我想如果任何人都能根據自己的需求定製他們的 Tinyagent,例如新增記憶體層或將資料儲存在 PG 中,那會很酷。
而且這裡可以與此倉庫聊天,併為您特定的專案新增所需的功能

https://askdev.ai/github/askbudi/tinyagent

註冊登入 發表評論

© . This site is unofficial and not affiliated with Hugging Face, Inc.