书接上回,上次的分享中,我们了解了模型的基础能力。其中最重要的就是模型的上下文和工具的概念。我们在应用层的开发基本上就是通过各种方式来定义、补全模型需要的上下文和对外的工具。

背景

Agent

当我们想要用 LLM 解决实际问题时,我们会发现 chatgpt 这类应用,或者说只使用模型本身,并不能满足实际任务需求。

比如实现一个需求,我们会涉及到 需求理解 -> 方案设计 -> 代码实现 -> 验证 这样的流程。

如果我们需要使用 chatgpt 来实现,工作流大概是这样:

  1. 整理上下文-> 输入给 chatgpt,比如 前期prd,业务背景相关信息,开发过程中的日志等信息,让它能输出符合要求的代码

  2. 获取 chatgpt 输出 -> 理解 & 整理为合适的格式 -> 输出结果,一般是将代码写到对应的文件中,并运行、检查

从上面这个工作流,我们可以看到使用像 chatgpt 这样的通用工具,上下文的管理、规划、校验输出等工作都需要用户来完成,成本不低。

而模型能够帮助我们完成上述工作,问题在于如何给它合适的上下文(开发流程、prd、代码的实践等信息),以及影响外部系统的接口(比如写入代码、调用接口等)。

对于上述这类有具体背景的 AI 应用,我们一般用 Agent 实现。

相对 LLM ,我们可以把 Agent 想成一名“拥有自主执行力的数字同事”,上面的这个描述就是一个理想的 code agent 的描述。

它的工作方式可以用一个公式概括: Agent = LLM (语言理解与生成) + 记忆 (保存上下文) + 规划(拆解并排序任务) + 工具使用 (把计划落到现实世界)。 当我们给出目标时:

  1. Agent 先根据历史上下文与长期知识库判断“我已经知道什么、还缺什么” -> 调用对应工具

  2. 随后制定行动路线,把总目标拆成连续子任务

  3. 每一步调用外部 API、数据库或脚本完成操作,再把结果写回自身记忆,以确保后续步骤基于最新信息继续推进。

整个过程中,它始终带着对“目标”的清晰对齐和对“上下文”的持续感知,这使它能够像一名可托付的同事那样闭环完成工作流,而不仅仅是一次性回答问题。

那么,从应用开发的角度,如何实现一个 agent 呢?

模型API 一般只提供对话和 function call 能力,也就是“大脑” + “工具调用”,更多的上层功能需要我们自己组合。现有 agent 应用一般都是基于 function call 完成记忆和工具使用的流程。

Function Call

Function call 是模型根据上下文,识别、使用工具的能力。模型会根据当前我们传入的工具 schema 定义,以及当前任务的上下文信息来决定是否使用某个 tool,并给出符合 schema 的参数

下面是一个使用 function call 完成查询天气的小例子

// examples/function-call-demo.ts
import OpenAI from "openai";
import dotenv from "dotenv";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import {
  ChatCompletionMessageParam,
  ChatCompletionTool,
} from "openai/resources/index.mjs";
 
dotenv.config();
 
const openai = new OpenAI({
  baseURL: "https://openrouter.ai/api/v1",
  apiKey: process.env.OPENAI_API_KEY as string,
});
 
/* 1) 声明一个示例工具:查询天气 */
const weatherParamsSchema = zodToJsonSchema(
  z.object({
    locationImage: z.string().optional().describe("Image of the location"),
    location: z.string().describe("City name, e.g. Singapore"),
    unit: z
      .enum(["celsius", "fahrenheit"])
      .default("celsius")
      .describe("Temperature unit"),
  })
);
 
// console.log("weatherParamsSchema", weatherParamsSchema);
 
const tools: ChatCompletionTool[] = [
  {
    type: "function",
    function: {
      name: "get_weather",
      description: "Get the current weather for a given city",
      parameters: weatherParamsSchema,
    },
  },
];
 
interface WeatherArgs {
  location: string;
  unit?: "celsius" | "fahrenheit";
  locationImage?: string;
}
 
interface WeatherResult {
  location: string;
  unit: "celsius" | "fahrenheit";
  temp: number;
  description: string;
}
 
/* 2)–(6) 封装完整对话 */
async function runConversation(userQuestion: string): Promise<void> {
  // 当前对话上下文
  const messages: ChatCompletionMessageParam[] = [
    { role: "system", content: "You are a helpful assistant." },
    {
      role: "user",
      content: [
        { type: "text", text: userQuestion },
        {
          type: "image_url",
          image_url: {
            url: "http://upload.kcfuler.top/uPic/oIZXg8.png", // 模拟多模态输入,实际可以通过图片上传接口拿到图片URL
          },
        },
      ],
    },
  ];
 
  /* 2) 第一次请求,让模型决定是否调用工具 */
  const response = await openai.chat.completions.create({
    model: "openai/gpt-4o",
    messages: messages,
    tools: tools,
    tool_choice: "auto", // 也可指定 { type:"function", function: { name: "get_weather" } }
  });
 
  const assistantMsg = response.choices[0].message;
  messages.push(assistantMsg); // 必须把 assistant 带 tool_calls 的消息保存下来
 
  /* 3)–4) 如果模型想调用工具, 对应的 tool_calls 中就会有对应的调用信息 */
  if (assistantMsg.tool_calls?.length) {
    for (const call of assistantMsg.tool_calls) {
      if (call.type === "function") {
        const { name, arguments: argStr } = call.function;
        const args = JSON.parse(argStr) as WeatherArgs;
 
        // 4) 本地执行对应函数
        const result = await dispatchTool(name, args);
 
        /* 5) 把执行结果作为 role:"tool" 回写 */
        messages.push({
          role: "tool",
          tool_call_id: call.id, // 与 assistantMsg.tool_calls[i].id 完全一致
          // name: name, // name property is not allowed for tool role
          content: JSON.stringify(result),
        });
      }
    }
 
    /* 6) 让模型基于工具结果,给出最终自然语言回复 */
    const finalCompletion = await openai.chat.completions.create({
      model: "openai/gpt-4o",
      messages, // messages 此时已包含工具调用的结果
    });
 
    // 从 finalCompletion 中提取内容
    console.log("🟢  Assistant:", finalCompletion.choices[0].message.content);
  } else {
    // 模型直接回答,没有调用工具
    console.log("🟢  Assistant:", assistantMsg.content);
  }
}
 
/* 简单的工具分发器 —— 实际项目可用 DI 或路由表 */
async function dispatchTool(
  name: string,
  args: WeatherArgs
): Promise<WeatherResult | void> {
  switch (name) {
    case "get_weather":
      return await getWeather(args.location, args.unit);
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}
 
/* 伪实现:真实场景可调用外部天气 API */
async function getWeather(
  location: string,
  unit: "celsius" | "fahrenheit" = "celsius"
): Promise<WeatherResult> {
  console.log("🔵  Calling getWeather() with args:", location, unit);
 
  return {
    location,
    unit,
    temp: 29,
    description: "Sunny",
  };
}
 
// 能够识别工具调用的例子
// runConversation("识别图片中的地点,然后查询天气");
// 不能识别工具调用的例子
runConversation("如何写一个快排");

从这个例子中,我们可以了解到:

  • 模型可以根据上下文,决定是否调用工具

  • 我们在维护 tools 时,主要维护三个部分:回话管理 & schema & tools 实现

上面的这个例子也是 MCP 之前,给模型添加工具的常规实现:

内部图片未公开

当我们 agent 和 tool 的逻辑足够复杂时,缺少标准化的上下文和工具定义会带来以下几个痛点:

  1. 开发耦合度高:工具开发者需要深入了解 Agent 的内部实现细节,并在 Agent 层编写工具代码。这导致在工具的开发与调试困难。

  2. 工具复用性差:因每个工具实现都耦合在 Agent 应用代码内,即使是通过 API 实现适配层在给到 LLM 的出入参上也有区别。从编程语言角度来讲,没办法做到跨编程语言进行复用。

MCP 通过标准化协议的方式,解决了上述问题。

MCP

什么是 MCP

Model Context Protocol(模型上下文协议)是 Anthropic 在推出的用于 LLM 应用和外部数据源(Resources)或工具(Tools)通信的标准协议,遵循 JSON-RPC 2.0 的基础消息格式。

可以把 MCP 想象成 AI 应用程序的 USB-C 接口规范了应用程序如何为 LLMs 提供上下文

内部图片未公开

核心概念

https://modelcontextprotocol.io/docs/concepts/architecture

MCP 整体架构遵循 client-server 模型,主要有下面几个概念:

  • Hosts,初始化连接的应用,像 claude 客户端、IDE(比如 cursor、trae )

  • Clients,client 和 server 维护 1 : 1 的连接

  • Servers,server 为 client 提供上下文、工具和 prompt

内部图片未公开

MCP 通过这样一个架构,连接大模型和我们的上下文,下面介绍几个使用过程中常见的概念。

工具

在实际应用中工具定义是 MCP 最常用的功能,帮助我们以规范的方式来定义我们的工具,特别是对于可以复用的服务,使用 MCP 封装可以更方便的复用。

在 MCP 中,Tools(工具)允许服务器向客户端暴露可执行函数,供客户端调用,并由大语言模型(LLM)执行相应操作。其关键特性包括:

  • 发现(Discovery):客户端可以通过 tools/list 接口列出所有可用的工具

  • 调用(Invocation):工具的执行通过 tools/call 接口进行,服务器会根据请求执行相应操作并返回结果

  • 灵活性(Flexibility):工具的功能范围可以从简单的计算扩展到复杂的 API 交互

工具定义格式

{
  name: string;          // 工具的唯一标识符
  description?: string;  // 面向人类的描述信息,用于解释工具的用途
  inputSchema: {         // 工具参数的 JSON Schema 格式定义
    type: "object",
    properties: { ... }  // 工具所需的具体参数字段定义
  },
  // 可选的行为注解,用于提供工具行为的额外提示
  annotations?: {        
    title?: string;             // 工具的展示标题,更易于用户理解
    readOnlyHint?: boolean;    // 如果为 true,表示该工具不会修改环境(只读操作)
    destructiveHint?: boolean; // 如果为 true,表示该工具可能进行破坏性修改(如删除数据)
    idempotentHint?: boolean;  // 如果为 true,表示相同参数重复调用不会带来额外副作用(幂等)
    openWorldHint?: boolean;   // 如果为 true,表示该工具会与外部系统或环境交互
  }
}

本质上就是用一个标准格式表述 function call 需要的信息。

像我们前面的那个天气查询的例子,就可以用下面的这种方式定义

server.tool(
  "get-forecast",
  "Get weather forecast for a location",
  {
    latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
    longitude: z
      .number()
      .min(-180)
      .max(180)
      .describe("Longitude of the location"),
  },
  async ({ latitude, longitude }) => {
    return await getForecastLogic(latitude, longitude);
  }
);

数据传输

消息格式

MCP 使用 JSON-RPC 2.0 作为其线上数据格式。传输层负责在发送时将 MCP 协议消息转换成 JSON-RPC 格式,并在接收时再把 JSON-RPC 消息还原为 MCP 协议消息。

JSON-RPC 可以理解为基于 json ,添加 RPC 相关的语意

export interface JSONRPCMessage<P = unknown, R = unknown> {
  /** 协议版本,固定为 "2.0" */
  jsonrpc: "2.0";
 
  /** 远程过程名;出现时代表“调用”(请求或通知) */
  method?: string;
 
  /** 实参;数组为位置参数,对象为命名参数 */
  params?: P;
 
  /** 请求标识符:
   *    - 若存在 ⇒ 这是一次“请求”,服务器需回包
   *    - 若缺失 ⇒ 这是一次“通知”,不等待回包
   *    - 在响应中必须回传与请求相同的 id
   */
  id?: string | number | null;
 
  /** 成功返回值;出现时代表“成功响应” */
  result?: R;
 
  /** 出错时返回的错误对象;出现时代表“失败响应” */
  error?: {
    code: number;         // 建议负值:-32700, -32600, -32601...
    message: string;      // 简短描述
    data?: unknown;       // 可选附加信息
  };
}

在 MCP 中,支持三种消息格式

  1. Request
{
  jsonrpc: "2.0",
  id: number | string,
  method: string,
  params?: object
}
  1. Response
{
  jsonrpc: "2.0",
  id: number | string,
  result?: object,
  error?: {
    code: number,
    message: string,
    data?: unknown
  }
}
  1. Notification
{
  jsonrpc: "2.0",
  method: string,
  params?: object
}

内置消息类型

  1. Stdio,通过标准输入输出传输信息,一般用来开发本地 MCP 应用,比如filesystem mcp
const server = new Server({
  name: "example-server",
  version: "1.0.0"
}, {
  capabilities: {}
});
 
const transport = new StdioServerTransport();
await server.connect(transport);
const client = new Client({
  name: "example-client",
  version: "1.0.0"
}, {
  capabilities: {}
});
 
const transport = new StdioClientTransport({
  command: "./server",
  args: ["--option", "value"]
});
await client.connect(transport);
  1. ~~SSE(最新版本被 streamable HTTP 替代)~~
  • 通过 sse 实现 server -> client 的信息传输

  • 通过 http post 实现 client -> server 的信息传输

  1. Streamable HTTP

https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http

与 sse 的对比:https://chatgpt.com/share/681c8aec-5cd0-8000-ad8a-8c4ac0621a93

  • 流式的 HTTP 传输(Streamable HTTP transport)中,服务器作为一个独立进程运行,能够处理多个客户端连接

  • 使用 HTTP 的 POST 和 GET 请求

  • 服务器可以选择使用 Server-Sent Events(SSE)来向客户端流式推送多条消息

MCP 也支持自定义消息类型的传输,只要实现 MCP 需要的接口即可

https://modelcontextprotocol.io/docs/concepts/transports#custom-transports

其它概念

https://modelcontextprotocol.io/docs/concepts/architecture

resources & prompts

  • 资源(Resources):类文件数据,可被客户端读取(如 API 响应或文件内容);这些是静态或动态生成的数据,客户端可以请求并获取,类似于传统 Web 服务器提供的资源。

  • 提示(Prompts):预先编写的模板,用于协助用户完成特定任务;这些是结构化的文本模板,设计用于引导用户或大语言模型完成特定的交互或

Sampling

Roots

在 MCP 里,Roots 用于划定服务器可以操作的边界。客户端通过 Roots 告诉服务器“哪些资源、位于何处”与当前工作最相关。是 client 对 server 的一种提示

Roots 本质上是 URI。客户端在建立连接时会声明这些 URI,提示服务器重点关注对应位置。最常见的情形是文件系统路径,但任何合法 URI(例如 HTTP 地址)都可以充当 Root。

比如我们在 trae 中引用上下文的功能,就可以通过 client 端指定的方式实现

{
  "roots": [
    {
      "uri": "file:///home/user/projects/frontend",
      "name": "Frontend Repository"
    },
    {
      "uri": "https://api.example.com/v1",
      "name": "API Endpoint"
    }
  ]
}

实践

https://modelcontextprotocol.io/quickstart/server

我们这里用官网的例子来感受 MCP 对 tools 的开发模式

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { getAlertsLogic, getForecastLogic } from "./weatherService.js";
 
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
  capabilities: {
    resources: {},
    tools: {},
  },
});
 
// Register weather tools
server.tool(    
  "get-alerts",
  "Get weather alerts for a state",
  {
    state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
  },
  async ({ state }) => {
    return await getAlertsLogic(state);
  }
);
 
server.tool(
  "get-forecast",
  "Get weather forecast for a location",
  {
    latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
    longitude: z
      .number()
      .min(-180)
      .max(180)
      .describe("Longitude of the location"),
  },
  async ({ latitude, longitude }) => {
    return await getForecastLogic(latitude, longitude);
  }
);
 
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running on stdio");
}
 
main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { z } from "zod";
import { getAlertsLogic, getForecastLogic } from "./weatherService.js";
import crypto from "crypto";
 
const app = express();
app.use(express.json({ type: "application/json" })); // 只解析 JSON-RPC 请求体
 
/* ---------- MCP Server 与工具注册 ---------- */
const server = new McpServer({
  name: "weather",
  version: "1.0.0",
  capabilities: { resources: {}, tools: {} },
});
 
// 初始化服务器,在定义工具前确保类型安全
const serverReady = (async () => {
  try {
    // 初始化其他必要的服务器组件(如果需要)
    console.log("MCP 服务器初始化中...");
    return true;
  } catch (error) {
    console.error("初始化 MCP 服务器失败:", error);
    return false;
  }
})();
 
server.tool(
  "get-alerts",
  "Get weather alerts for a state",
  {
    state: z
      .string()
      .length(2)
      .describe("The state code to get alerts for, like CA"),
  },
  async ({ state }) => getAlertsLogic(state)
);
 
server.tool(
  "get-forecast",
  "Get weather forecast for a location",
  {
    latitude: z.number().min(-90).max(90),
    longitude: z.number().min(-180).max(180),
  },
  async ({ latitude, longitude }) => getForecastLogic(latitude, longitude)
);
 
/* ---------- 会话级 Transport 缓存 ---------- */
const transports = new Map<string, StreamableHTTPServerTransport>();
 
app.all("/mcp", async (req, res) => {
  // 确保服务器已初始化
  if (!(await serverReady)) {
    console.error("MCP 服务器未完全初始化,拒绝请求");
    res.status(500).json({
      jsonrpc: "2.0",
      error: { code: -32000, message: "Server initialization failed" },
      id: null,
    });
    return;
  }
 
  const sessionId = req.headers["mcp-session-id"] as string | undefined;
  // console.log(`收到会话 ID: `, req.headers);
  let transport: StreamableHTTPServerTransport;
 
  try {
    if (sessionId && transports.has(sessionId)) {
      /* 已存在会话 —— 复用 transport */
      transport = transports.get(sessionId)!;
    } else {
      /* 新会话 —— 创建并注册 */
      const newSessionId = crypto.randomUUID();
      console.log(`创建新会话: ${newSessionId}`);
 
      transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: () => newSessionId,
      });
 
      try {
        /* 连接到 MCP 服务器 */
        await server.connect(transport);
        console.log(`会话 ${newSessionId} 已成功连接到 MCP 服务器`);
 
        // 只有初始化成功才保存 transport
        transports.set(newSessionId, transport);
 
        /* 连接断开时回收 */
        res.on("close", () => {
          // 注意:不要在连接断开时立即删除会话,以支持会话恢复
          console.log(`会话 ${newSessionId} 连接已断开,但会话保持活跃状态`);
        });
      } catch (error) {
        console.error(`连接会话 ${newSessionId} 到 MCP 服务器失败:`, error);
        res.status(500).json({
          jsonrpc: "2.0",
          error: { code: -32000, message: "Failed to initialize session" },
          id: null,
        });
        return;
      }
    }
 
    /* 处理请求,包括 Last-Event-ID 支持 */
    const lastEventId = req.headers["last-event-id"];
    if (req.method === "GET" && lastEventId) {
      console.log(`收到带 Last-Event-ID: ${lastEventId} 的恢复请求`);
      // 这里可以添加恢复逻辑
    }
 
    /* 把当前 HTTP 请求交给 transport 处理 */
    console.log(`处理来自会话 ${transport.sessionId} 的 ${req.method} 请求`);
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("处理请求时出错:", error);
    // 如果尚未发送响应,则返回错误
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: "2.0",
        error: { code: -32000, message: "Internal server error" },
        id: null,
      });
    }
  }
});
 
app.listen(3000, () => {
  console.log("Streamable HTTP MCP server: http://localhost:3000/mcp");
  console.log("服务器已启动,等待客户端连接...");
  console.log("支持功能: get-alerts, get-forecast");
 
  // 异步检查初始化状态
  serverReady.then((ready) => {
    if (ready) {
      console.log("MCP 服务器初始化完成,可以接受请求");
    } else {
      console.error("MCP 服务器初始化失败,可能无法正常处理请求");
    }
  });
});

调试

https://modelcontextprotocol.io/docs/tools/inspector

官方提供了方便的调试工具

对于我们 local mcp 的例子,可以直接运行下面的命令:

npx @modelcontextprotocol/inspector node ./weather/build/index.js

内部图片未公开

远程的服务选择对应的 transport type 即可

更进一步

安全

关于 MCP 常见安全问题的分享:https://bytetech.info/articles/7498581192703213606#Huk0dGN07oLjn8x8mkQc1u6Tn9Y

MCP 官方文档:

MCP 一般会直接将业务的上下文通过 MCP server 暴露出来。开发 MCP server 时需要特别注意鉴权

MCP server 本质上就是一个服务,和传统的鉴权方式是一致的。官方有以下几个建议

  1. 服务器必须在所有传入连接上验证 Origin 头,以防止 DNS 重绑定攻击https://chatgpt.com/share/681c7913-e878-8000-94e1-bb7c579456ea。

  2. 在本地运行时,服务器应仅绑定到本地主机(127.0.0.1),而不是所有网络接口(0.0.0.0)。

  3. 服务器应对所有连接实施适当的身份验证。

现有实践

公司 MCP 市场:内部资料未公开 字节云 MCP 平台:AI Infra 思·享|3分钟部署字节云MCP: 让你的AI应用火力全开

相关文章

公司 MCP 市场

https://bytetech.info/articles/7485208098031403045

https://bytetech.info/articles/7488563775738478655

https://bytetech.info/articles/7493868400918298633#HqP8dnfFboOp6NxYybCcKjf4nhC

虽然现在有很多 agent 框架,但 agent 并不是某种具体的实现。它类似 MVVM 和具体前端框架之间的关系。

https://yage.ai/why-forget-all-frameworks.html