关于 MCP 的讨论大多停在「它是什么」。这篇讲什么时候该用、怎么用、以及用错的代价。
先说结论:MCP 解决的是接入标准化问题。它不解决工具设计问题。 如果你的工具本来就切错了,接上 MCP 只会让你更快地撞到墙。
MCP 真正解决的问题#
不用 MCP 的时候,工具是这样注册的:
const tools = {
get_inventory: {
description: "查询库存",
parameters: z.object({ sku: z.string() }),
execute: async ({ sku }) => db.inventory.find(sku),
},
get_price_floor: {
description: "查询底价",
parameters: z.object({ sku: z.string() }),
execute: async ({ sku }) => db.pricing.floor(sku),
},
// ... 每加一个,改一次这个文件
};三个具体问题:
- 改工具 = 改主流程。 每次新增都要动 Agent 的核心代码,回归风险随时间线性上升。
- 工具与 Agent 同进程。 一个工具里有个慢查询,整个 Agent 卡住。
- 工具无法复用。 第二个 Agent 想用同一个库存查询,只能复制一份。
MCP 把工具变成独立部署的服务,Agent 通过协议发现和调用:
// Agent 侧:不关心有几个工具、是谁提供的
const client = new Client({ name: "agent", version: "1.0.0" });
await client.connect(new StdioTransport({ command: "erp-mcp-server" }));
const { tools } = await client.listTools();// 工具侧:独立进程,独立部署,独立测试
const server = new McpServer({ name: "erp", version: "1.0.0" });
server.tool(
"get_inventory",
"查询指定 SKU 的可用库存与在途数量",
{ sku: z.string().describe("产品 SKU,例如 AB-1200") },
async ({ sku }) => ({
content: [{ type: "text", text: JSON.stringify(await erp.stock(sku)) }],
}),
);
await server.connect(new StdioServerTransport());加工具变成加服务。主流程零改动。
坑一:按 API 端点切工具,切出一堆废工具#
我最初的做法是「每个后端接口对应一个工具」。结果是 9 个工具里, 模型在 80% 的情况下只用了 2 个。
问题在于模型看到的是工具的语义,不是你的 API 结构。
反例:
// 按 API 端点切 —— 模型不知道什么时候该用哪个
server.tool("get_product_basic", ...); // 基础信息
server.tool("get_product_stock", ...); // 库存
server.tool("get_product_pricing", ...); // 价格
server.tool("get_product_shipping", ...); // 运费模型收到「客户问 500 件能不能 11 月前到货」时,需要连续调用 4 个工具才能回答。 它经常会漏掉其中一个。
正例:
// 按业务能力切 —— 一次调用回答一个问题
server.tool(
"answer_availability_question",
"回答客户关于「能不能发货 / 什么时候到 / 多少钱」的问题。一次返回库存、价格与交期。",
{
sku: z.string(),
quantity: z.number(),
targetDate: z.string().optional().describe("客户期望到货日期 ISO 格式"),
},
async (args) => { /* 内部聚合 4 个接口 */ },
);工具应该按「模型要回答的问题」切,不是按你的后端结构切。
这条原则的推论是:工具内部可以调很多接口,但对外只暴露一个语义完整的动作。
坑二:description 写成给人看的文档#
工具描述是给模型看的 prompt,不是 API 文档。
对比:
❌ get_inventory(sku: string) — 查询库存
模型不知道:返回什么格式?没有库存时返回什么?
会不会抛错?sku 大小写敏感吗?
✅ get_inventory(sku: string) — 查询指定 SKU 的可用库存与在途数量。
返回 JSON:available(可立即发货数)、incoming(在途数)、
incomingEta(预计到货日期,可能为 null)。
若 SKU 不存在,返回 available=0 而非报错。
SKU 不区分大小写。第二版把「返回什么」「边界情况怎么处理」都写进去了。 模型不需要猜,也不需要试探性地调用一次看结果。
代价是描述变长、消耗 token。但省下的是往返调用,绝对划算。
坑三:工具太多,模型选择困难#
工具数量超过 10 个之后,选择准确率明显下降。我实测的拐点在 12–15 个之间。
三个应对方式,按推荐顺序:
1. 合并语义相近的工具(首选) 前面说的「按业务能力切」就是这个。通常能把工具数压掉一半。
2. 按场景分组,动态加载 不同任务阶段只暴露相关工具:
// 询盘分类阶段只需要读工具
const tools = await client.listTools({ group: "read-only" });
// 报价阶段再加载写入工具
const tools = await client.listTools({ group: "quote" });3. 加一层工具路由 用一个轻量模型先判断该用哪一类工具,再加载对应的子集。 成本低,但多了一次往返。
什么时候不需要 MCP#
说清楚这个比推销它更重要。
- 工具少于 5 个,且只有一个 Agent 用 → 直接注册,MCP 是过度设计
- 纯本地脚本,不需要独立部署 → 没必要拆进程
- 对延迟极度敏感 → MCP 多一层进程通信,会有额外开销
我是在工具涨到 9 个、且第二个 Agent 想复用其中 4 个的时候才迁的。 迁移成本两天,在此之前用直接注册完全够用。
不要在痛之前先上架构。但要知道痛的形状,痛的时候能立刻认出来。
一个实际收益#
迁移后最有价值的副产品不是「加工具方便」,而是工具可以独立测试了。
// 工具变成纯函数式服务,可以脱离 Agent 单独跑测试
describe("get_inventory", () => {
it("SKU 不存在时返回 available=0 而不是抛错", async () => {
const result = await callTool("get_inventory", { sku: "NOT-EXIST" });
expect(result.available).toBe(0);
});
});在旧架构里,这段测试要先起一个完整的 Agent。现在它是个独立进程。
可测试性才是模块化的真正收益。
具体到这个项目里的工具层设计,见 AI Digital Employee 项目页。