Các tác nhân nhỏ một tác nhân được hỗ trợ bởi MCP trong 50 dòng mã

  • 12 min read
Các tác nhân nhỏ một tác nhân được hỗ trợ bởi MCP trong 50 dòng mã

Tiny Agents: Một Agent được hỗ trợ bởi MCP trong 50 dòng code

Trong vài tuần qua, tôi đã tìm hiểu về MCP (Model Context Protocol) để hiểu xem sự cường điệu xung quanh nó là gì.

TL;DR của tôi là nó khá đơn giản, nhưng vẫn khá mạnh mẽ: MCP là một API tiêu chuẩn để hiển thị các bộ Công cụ có thể được kết nối với LLM.

Việc mở rộng một Inference Client khá đơn giản - tại HF, chúng tôi có hai SDK client chính thức: @huggingface/inference trong JS và huggingface_hub trong Python - để cũng hoạt động như một MCP client và kết nối các công cụ có sẵn từ các máy chủ MCP vào suy luận LLM.

Nhưng trong khi làm điều đó, tôi nhận ra điều thứ hai:

Một khi bạn có MCP Client, một Agent thực tế chỉ là một vòng lặp while trên đầu nó.

Trong bài viết ngắn này, tôi sẽ hướng dẫn bạn cách tôi triển khai nó trong Typescript (JS), cách bạn có thể áp dụng MCP và cách nó sẽ làm cho AI Agentic trở nên đơn giản hơn trong tương lai.

meme

Hình ảnh được cung cấp bởi https://x.com/adamdotdev

Cách chạy bản demo hoàn chỉnh

Nếu bạn có NodeJS (với pnpm hoặc npm), chỉ cần chạy lệnh này trong một terminal:

npx @huggingface/mcp-client

hoặc nếu sử dụng pnpm:

pnpx @huggingface/mcp-client

Điều này cài đặt package của tôi vào một thư mục tạm thời sau đó thực thi lệnh của nó.

Bạn sẽ thấy Agent đơn giản của bạn kết nối với hai máy chủ MCP riêng biệt (chạy cục bộ), tải các công cụ của chúng, sau đó nhắc bạn về một cuộc trò chuyện.

Theo mặc định, Agent ví dụ của chúng tôi kết nối với hai máy chủ MCP sau:

  • máy chủ hệ thống tệp “chính tắc” file system server, có quyền truy cập vào Desktop của bạn,
  • và máy chủ Playwright MCP, biết cách sử dụng trình duyệt Chromium được cách ly cho bạn.

Lưu ý: điều này hơi phản trực giác nhưng hiện tại, tất cả các máy chủ MCP thực sự là các tiến trình cục bộ (mặc dù các máy chủ từ xa sẽ sớm ra mắt).

Đầu vào của chúng tôi cho video đầu tiên này là:

viết một bài haiku về cộng đồng Hugging Face và viết nó vào một tệp có tên “hf.txt” trên Desktop của tôi

Bây giờ hãy thử lời nhắc này liên quan đến một số hoạt động duyệt Web:

thực hiện Tìm kiếm trên Web cho các nhà cung cấp suy luận HF trên Brave Search và mở 3 kết quả đầu tiên

Mô hình và nhà cung cấp mặc định

Về cặp mô hình/nhà cung cấp, Agent ví dụ của chúng tôi sử dụng theo mặc định:

Tất cả điều này có thể định cấu hình thông qua các biến môi trường! Xem:

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,
});

Code nằm ở đâu

Code Tiny Agent nằm trong sub-package mcp-client của mono-repo huggingface.js, là GitHub mono-repo trong đó tất cả các thư viện JS của chúng tôi cư trú.

https://github.com/huggingface/huggingface.js/tree/main/packages/mcp-client

Codebase sử dụng các tính năng JS hiện đại (đặc biệt là async generators) giúp mọi thứ dễ triển khai hơn, đặc biệt là các sự kiện không đồng bộ như phản hồi LLM. Bạn có thể cần hỏi LLM về các tính năng JS đó nếu bạn chưa quen thuộc với chúng.

Nền tảng cho điều này: hỗ trợ gốc cho việc gọi công cụ trong LLM.

Điều sẽ làm cho toàn bộ bài đăng trên blog này trở nên rất dễ dàng là thế hệ LLM gần đây (cả đóng và mở) đã được đào tạo để gọi hàm, hay còn gọi là sử dụng công cụ.

Một công cụ được xác định bởi tên của nó, một mô tả và một biểu diễn JSONSchema của các tham số của nó. Theo một nghĩa nào đó, nó là một biểu diễn mờ đục về giao diện của bất kỳ hàm nào, khi nhìn từ bên ngoài (có nghĩa là, LLM không quan tâm đến cách hàm thực sự được triển khai).

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",
                },
            },
        },
    },
};

Tài liệu chính tắc mà tôi sẽ liên kết đến đây là OpenAI’s function calling doc. (Vâng… OpenAI gần như xác định các tiêu chuẩn LLM cho toàn bộ cộng đồng 😅).

Các inference engine cho phép bạn chuyển một danh sách các công cụ khi gọi LLM và LLM có quyền tự do gọi không, một hoặc nhiều công cụ đó. Là một nhà phát triển, bạn chạy các công cụ và đưa kết quả của chúng trở lại LLM để tiếp tục tạo.

Lưu ý rằng ở backend (ở cấp độ inference engine), các công cụ chỉ đơn giản được chuyển đến mô hình trong một chat_template được định dạng đặc biệt, giống như bất kỳ tin nhắn nào khác, và sau đó được phân tích cú pháp từ phản hồi (sử dụng các token đặc biệt dành riêng cho mô hình) để hiển thị chúng dưới dạng các lệnh gọi công cụ.

Triển khai MCP client trên InferenceClient

Bây giờ chúng ta đã biết một công cụ là gì trong các LLM gần đây, hãy triển khai client MCP thực tế.

Tài liệu chính thức tại https://modelcontextprotocol.io/quickstart/client được viết khá tốt. Bạn chỉ phải thay thế bất kỳ đề cập nào đến Anthropic client SDK bằng bất kỳ OpenAI-compatible client SDK nào khác. (Ngoài ra còn có một llms.txt mà bạn có thể đưa vào LLM bạn chọn để giúp bạn code cùng).

Để nhắc lại, chúng tôi sử dụng InferenceClient của HF cho inference client của chúng tôi.

Tệp code McpClient.ts hoàn chỉnh là ở đây nếu bạn muốn theo dõi bằng code thực tế 🤓

Lớp McpClient của chúng tôi có:

  • một Inference Client (hoạt động với bất kỳ Inference Provider nào và huggingface/inference hỗ trợ cả remote và local endpoints)
  • một tập hợp các phiên bản MCP client, một cho mỗi máy chủ MCP được kết nối (vâng, chúng tôi muốn hỗ trợ nhiều máy chủ)
  • và một danh sách các công cụ có sẵn sẽ được điền từ các máy chủ được kết nối và chỉ được định dạng lại một chút.
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;
    }
    
    // [...]
}

Để kết nối với một máy chủ MCP, SDK TypeScript @modelcontextprotocol/sdk/client chính thức cung cấp một lớp Client với một phương thức 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 là một giao diện từ MCP SDK sẽ cho phép bạn dễ dàng sinh ra một tiến trình cục bộ: như chúng tôi đã đề cập trước đó, hiện tại, tất cả các máy chủ MCP thực sự là các tiến trình cục bộ.

Đối với mỗi máy chủ MCP mà chúng tôi kết nối, chúng tôi định dạng lại một chút danh sách các công cụ của nó và thêm chúng vào this.availableTools.

Cách sử dụng các công cụ

Dễ dàng, bạn chỉ cần chuyển this.availableTools đến LLM chat-completion của bạn, ngoài mảng tin nhắn thông thường của bạn:

const stream = this.client.chatCompletionStream({
    provider: this.provider,
    model: this.model,
    messages,
    tools: this.availableTools,
    tool_choice: "auto",
});

tool_choice: "auto" là tham số bạn chuyển cho LLM để tạo ra không, một hoặc nhiều lệnh gọi công cụ.

Khi phân tích cú pháp hoặc phát trực tuyến đầu ra, LLM sẽ tạo ra một số lệnh gọi công cụ (tức là một tên hàm và một số đối số được mã hóa JSON), mà bạn (với tư cách là nhà phát triển) cần tính toán. MCP client SDK một lần nữa làm cho điều đó rất dễ dàng; nó có một phương thức 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}`;
}

Cuối cùng, bạn sẽ thêm tin nhắn công cụ kết quả vào mảng messages của bạn và trở lại LLM.

Agent 50 dòng code của chúng tôi 🤯

Bây giờ chúng ta đã có một MCP client có khả năng kết nối với các máy chủ MCP tùy ý để lấy danh sách các công cụ và có khả năng chèn chúng và phân tích cú pháp chúng từ suy luận LLM, vậy… một Agent là gì?

Một khi bạn có một inference client với một bộ công cụ, thì một Agent chỉ là một vòng lặp while trên đầu nó.

Chi tiết hơn, một Agent chỉ đơn giản là một sự kết hợp của:

  • một system prompt
  • một LLM Inference client
  • một MCP client để kết nối một bộ Công cụ vào nó từ một loạt các máy chủ MCP
  • một số luồng điều khiển cơ bản (xem bên dưới cho vòng lặp while)

Tệp code Agent.ts hoàn chỉnh là ở đây.

Lớp Agent của chúng tôi chỉ đơn giản là mở rộng 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,
            },
        ];
    }
}

Theo mặc định, chúng tôi sử dụng một system prompt rất đơn giản lấy cảm hứng từ một trong những prompt được chia sẻ trong GPT-4.1 prompting guide.

Mặc dù điều này đến từ OpenAI 😈, nhưng câu này đặc biệt áp dụng cho ngày càng nhiều mô hình, cả đóng và mở:

Chúng tôi khuyến khích các nhà phát triển sử dụng độc quyền trường công cụ để chuyển các công cụ, thay vì tự chèn các mô tả công cụ vào lời nhắc của bạn và viết một trình phân tích cú pháp riêng biệt cho các lệnh gọi công cụ, như một số người đã báo cáo đã làm trong quá khứ.

Có nghĩa là, chúng ta không cần cung cấp danh sách các ví dụ sử dụng công cụ được định dạng tỉ mỉ trong prompt. Tham số tools: this.availableTools là đủ.

Tải các công cụ trên Agent thực tế chỉ là kết nối với các máy chủ MCP mà chúng ta muốn (song song vì nó rất dễ thực hiện trong JS):

async loadTools(): Promise<void> {
    await Promise.all(this.servers.map((s) => this.addMcpServer(s)));
}

Chúng tôi thêm hai công cụ bổ sung (ngoài MCP) mà LLM có thể sử dụng cho luồng điều khiển của Agent chúng tôi:

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];

Khi gọi bất kỳ công cụ nào trong số này, Agent sẽ phá vỡ vòng lặp của nó và trả quyền điều khiển lại cho người dùng để có đầu vào mới.

Vòng lặp while hoàn chỉnh

Hãy xem vòng lặp while hoàn chỉnh của chúng ta.🎉

Điểm chính của vòng lặp while chính của Agent chúng tôi là chúng tôi chỉ đơn giản lặp lại với LLM luân phiên giữa việc gọi công cụ và cung cấp cho nó các kết quả công cụ và chúng tôi làm như vậy cho đến khi LLM bắt đầu phản hồi với hai tin nhắn không phải công cụ liên tiếp.

Đây là vòng lặp while hoàn chỉnh:

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;
    }
}

Các bước tiếp theo

Có rất nhiều bước tiếp theo tiềm năng thú vị khi bạn đã có MCP Client đang chạy và một cách đơn giản để xây dựng Agents 🔥

  • Thử nghiệm với các mô hình khác
    • mistralai/Mistral-Small-3.1-24B-Instruct-2503 được tối ưu hóa để gọi hàm
    • Gemma 3 27B, các mô hình Gemma 3 QAT là một lựa chọn phổ biến để gọi hàm mặc dù nó sẽ yêu cầu chúng ta triển khai phân tích cú pháp công cụ vì nó không sử dụng tools gốc (một PR sẽ được hoan nghênh!)
  • Thử nghiệm với tất cả Inference Providers:
    • Cerebras, Cohere, Fal, Fireworks, Hyperbolic, Nebius, Novita, Replicate, SambaNova, Together, v.v.
    • mỗi nhà cung cấp có các tối ưu hóa khác nhau để gọi hàm (cũng tùy thuộc vào mô hình) do đó hiệu suất có thể khác nhau!
  • Kết nối LLM cục bộ bằng llama.cpp hoặc LM Studio

Pull request và đóng góp được hoan nghênh! Một lần nữa, mọi thứ ở đây đều là mã nguồn mở! 💎❤️

Recommended for You

Bảng xếp hạng tiếng Ả Rập Giới thiệu hướng dẫn bằng tiếng Ả Rập, cập nhật AraGen và hơn thế nữa

Bảng xếp hạng tiếng Ả Rập Giới thiệu hướng dẫn bằng tiếng Ả Rập, cập nhật AraGen và hơn thế nữa

Bài viết này giới thiệu bảng xếp hạng hướng dẫn bằng tiếng Ả Rập, cập nhật AraGen và hơn thế nữa.

Chào mừng Llama 4 Maverick & Scout trên Hugging Face!

Chào mừng Llama 4 Maverick & Scout trên Hugging Face!

Bài viết này giới thiệu Llama 4 Maverick & Scout trên Hugging Face.