MCP 与 AI Agent 工程化:从工具调用到多智能体协作
一个团队把内部订单系统接进 Claude,写了一份 function schema。两周后要接 Cursor,schema 再写一遍。一个月后接自研 Agent 框架,字段名又对不上。三个月后维护着四份语义相同、命名不同的工具定义,改一个接口要改四个文件,漏改一个就线上报错。
MCP(Model Context Protocol)就是冲这个场景来的。
1. MCP 协议解决什么问题
从 M×N 到 M+N
模型侧的调用格式各写各的:OpenAI 用 tools 数组,Anthropic 用 tool_use 内容块,Google 用 functionDeclarations。工具侧有 GitHub、Postgres、内部 CMDB、工单系统。M 个模型乘 N 个工具,就是 M×N 套适配代码。
MCP 把中间这层抽出来做成协议:工具方实现一次 server,模型侧实现一次 client,两边都按协议说话。工具方从维护 M 份适配变成维护 1 份。
协议由 Anthropic 在 2024 年 11 月 25 日公开,规范按日期发版:2024-11-05 是首个版本,2025-03-26 用 Streamable HTTP 替换了旧的 HTTP+SSE,2025-06-18 增加了 elicitation、结构化工具输出和 resource links,同时移除了 JSON-RPC 的 batch 支持。跨版本混用是接入阶段最常见的坑,接之前先对齐 SDK 版本和目标 server 声明的协议版本。规范原文在 https://modelcontextprotocol.io/specification 。
三类角色,各管一段
| 角色 | 负责什么 | 现实中的例子 |
|---|---|---|
| Host | 跑模型、管会话、决定要不要调工具、向用户要确认 | Claude Desktop、Claude Code、Cursor、VS Code 里的 Copilot |
| Client | 跟某个 server 保持 1:1 连接,转发请求和结果 | SDK 里的 ClientSession |
| Server | 暴露具体能力,执行实际操作 | 你写的订单 server、GitHub 官方 server |
服务端能暴露三种东西
| 原语 | 谁决定用不用 | 典型用途 | 是否改状态 |
|---|---|---|---|
| tools | 模型决定调用 | 查订单、发消息、建工单 | 可读可写 |
| resources | 应用层决定何时注入 | 日志片段、配置文件、数据库 schema | 只读 |
| prompts | 用户显式触发 | "/review-pr" 这类预置模板 | 无副作用 |
客户端侧还有三个能力值得知道:sampling 让 server 反过来请求 Host 跑一次模型;roots 让 Host 告诉 server 能访问哪些目录;elicitation 让 server 在执行中途向用户追问缺失参数(2025-06-18 引入)。第三个在写操作里很有用——退款金额没给全时,先问人,别让模型猜。
它和 function calling 什么关系
function calling 管的是「模型这一次输出长什么样」。MCP 管的是「这个工具在哪、参数怎么定义、结果怎么回来、谁有权调」。两者叠加使用:Host 把 MCP server 的 inputSchema 转成自家模型要的 function 定义,模型输出调用后,Host 通过 Client 发给 Server,再把结果塞回上下文。
所以在 MCP 里,工具的服务发现和权限控制是协议层的事,不依赖某个模型厂商的接口形态。这是它跟「写个 function 塞进 tools 参数」最大的差别。
2. Agent 架构:规划、记忆、工具、执行
规划:能不做就不做
单步工具调用不需要规划器,加了只是多一次模型往返,还多一份出错的可能。什么时候该上:任务需要 3 步以上、步骤之间有依赖、或者要按中间结果分不同分支。
| 模式 | 怎么跑 | 适合 | 代价 |
|---|---|---|---|
| ReAct 循环 | 想一步做一步,边观察边调整 | 步骤少、环境反馈快的任务 | token 随步数线性涨,容易在原地绕圈 |
| Plan-and-Execute | 先出完整计划再逐步执行 | 步骤固定、可预先拆解的批处理 | 计划第一步就错的话整段跟着错 |
记忆:分三层放,别都堆在消息数组里
工作记忆是当前上下文窗口。会话记忆是摘要加关键事实,跨轮次保留。长期记忆放在外部存储——向量库、关系库或者普通文件。
Anthropic 的工程博客把上下文管理归纳成 write / select / compress / isolate 四类动作(见 Building effective agents 与同站 Engineering 栏目的上下文工程一文)。搬到自己的项目里就是四件事:
- write:中间结果落盘或入库,别全留在消息数组里。一个查 500 行的结果占的 token 能顶掉后面所有推理空间。
- select:按需检索,只把命中的片段注入。检索命中率低的时候,先改分块策略,别急着换向量模型。
- compress:超过阈值就把早期轮次摘要化。摘要必须保留决策和异常,只写「已完成查询」等于没写。
- isolate:子 agent 用独立上下文,只往主流程回传结论,不回传完整轨迹。
工具:描述写得好不好,直接决定调用成功率
写 MCP tool 的几个实际做法:
- 工具名用动词开头,
get_order_status比order好,refund_order比order_refund_v2好。 - 描述里写清三件事:什么时候用、什么时候别用、返回什么结构。尤其是「别用」——比如「订单不存在时返回
found=false,不要重试」,这一句能省掉大量无效循环。 - 参数用 enum 和默认值收敛取值范围。字符串类型的
status参数不加约束,模型会给你造出processing、in_progress、pending三种近义词。 - 单个 server 的工具数控制在 20 个以内(经验值,不是协议要求)。工具越多,选择错误率越高,而且光是把所有 schema 塞进上下文就吃掉几千 token。超过就按业务域拆 server。
执行:三件事不做会出事
- 超时。每个工具调用设 timeout,默认 30 秒。没超时的话,一个卡死的下游接口能把整个 Agent 会话拖住。
- 幂等。所有写操作接受一个
request_id,重复调用返回同一个结果。Agent 重试是常态,没有幂等键就会重复退款、重复建单。 - 沙箱。执行类工具(跑命令、写文件)限制在固定目录和命令白名单里。用
roots声明允许访问的路径,server 侧再校验一次,别只靠 Host 那一层。
3. 多智能体协作与权限边界
三种常见拓扑
| 拓扑 | 控制流 | 代表实现 | 什么时候用 |
|---|---|---|---|
| Supervisor | 主 agent 派活,子 agent 无权直接对话 | LangGraph 的 supervisor 模式 | 任务可拆成独立子任务,需要统一收口 |
| Handoff | 控制权整体转移给下一个 agent | OpenAI Agents SDK 的 handoffs | 客服分流这类「换个人接着聊」的场景 |
| Group chat | 多角色共享一条消息总线 | AutoGen 的 GroupChat | 需要多视角互相挑错,比如代码评审 |
权限跟着工具走,不跟着提示词走
在 system prompt 里写「你不能删除数据」,模型大部分时候会照做,但这是个概率问题。在网关层把 delete_* 工具从列表里摘掉,它就没得选。
落到配置上,是给每个 agent 分配一份独立的白名单:
| Agent | 允许直接调用 | 需人工确认 | 明确禁止 |
|---|---|---|---|
| 分诊 agent | 查询订单、查询物流 | 无 | 所有写操作 |
| 运维 agent | 查询订单、查询物流、重启服务 | 退款、改配置 | 删除数据 |
| 财务 agent | 查询订单、查询流水 | 退款、调额 | 修改用户信息 |
审计日志至少记五样:agent 标识、凭据主体、工具名、入参、返回状态。参数里的手机号、身份证、token 要脱敏后再落盘。
三个会出事的细节
共享记忆写冲突。 两个子 agent 同时往同一份状态里写,后写的覆盖先写的。要么给每个子 agent 独立的命名空间,要么走单写入者的串行队列。
互相调用死循环。 A 调 B,B 又调回 A。加 hop 上限(建议 6 跳),同时算「调用指纹」——工具名加参数哈希——同一个指纹出现两次就中断。
上下文污染。 子 agent 把完整思考过程回传给主流程,主流程的上下文被灌满。约定子 agent 只返回结构化结论:结论、证据引用、置信度、用掉的工具。
4. 企业落地:先从只读环节切
公开可查的 MCP 支持情况:Claude Desktop 和 Claude Code 本身是 MCP Host,Cursor、VS Code 里的 GitHub Copilot 都支持挂 MCP server,Block 开源的 goose 也是 MCP 客户端。这意味着「内部工具接进 AI 编辑器」这件事,Host 侧基本不用自己写。
落地顺序建议按风险排,不按价值排:
- 第一步:挑一个只读接口(查订单、查库存、查日志)包成 MCP server,接进团队现有的 AI 编辑器
- 第二步:观察两周,收集模型调用时的参数错误和高频无效调用,回来改工具描述和参数约束
- 第三步:加第二个只读 server,验证多 server 并存时的工具选择准确率
- 第四步:引入一个低危写操作(建草稿单、发内部通知),配人工确认
- 第五步:写操作上量之前,审计日志、幂等键、回滚方案先就位
几个反复出现的坑:
- 把整个内部 API 一次性包成 tool。 两百个工具塞进上下文,模型选错的概率直线上升,钱也白花。按业务域拆 server,按需挂载。
- 工具描述写成一句话。 「查询订单」这种描述,模型不知道能按什么查、返回什么、查不到怎么办。描述长度值得投入,它比换模型便宜。
- token 写进日志。 调试方便,出事就是事故。日志里只留 token 的哈希前缀。
- 只测 happy path。 至少补三类用例:下游超时、返回空、参数格式不对。Agent 在这些路径上的表现比正常路径更能说明工程质量。
- 忽略协议版本。 server 用旧版 SDK,client 是新版 Host,握手阶段的报错信息往往很含糊。把版本写进 README。
5. 代码示例
一个带幂等和参数约束的 MCP Server
官方 Python SDK 提供 FastMCP,用类型注解和 docstring 自动生成 schema:
# server.py
from mcp.server.fastmcp import FastMCP
from pydantic import Field
mcp = FastMCP('order-tools')
@mcp.tool()
def get_order_status(
order_id: str = Field(description='订单号,SO- 开头,例如 SO-20260101-001'),
) -> dict:
'''查询订单当前状态。只读操作。
订单不存在时返回 found=False,不要重试。
返回字段:found、status、updated_at。
'''
row = query_order(order_id)
if not row:
return {'found': False}
return {'found': True, 'status': row['status'], 'updated_at': row['updated_at']}
@mcp.tool()
def refund_order(
order_id: str,
amount_cents: int = Field(gt=0, description='退款金额,单位分'),
request_id: str = Field(description='幂等键,同一业务动作复用同一个值'),
) -> dict:
'''对订单发起退款。写操作。
幂等:同一 request_id 重复调用不会重复退款。
订单已退款时返回 skipped=True,不要再次调用。
'''
existing = lookup_request(request_id)
if existing:
return {'skipped': True, 'result': existing}
result = do_refund(order_id, amount_cents, request_id)
save_request(request_id, result)
return {'skipped': False, 'result': result}
if __name__ == '__main__':
# 本地调试用 stdio;部署成共享服务时换 streamable-http
mcp.run(transport='streamable-http')
mcp.run() 不带参数时默认走 stdio,适合挂在桌面 Host 上。改成 transport='streamable-http' 之后 server 会起一个 HTTP 端点,适合部署成团队共享服务。
挂到 Claude Desktop
配置文件在 macOS 上是 ~/Library/Application Support/Claude/claude_desktop_config.json,Windows 上是 %APPDATA%\Claude\claude_desktop_config.json:
{
"mcpServers": {
"order-tools": {
"command": "python",
"args": ["/abs/path/server.py"]
}
}
}
Claude Code 用项目级 .mcp.json,结构相同,也可以用 claude mcp add 命令写。远程 server 把 command/args 换成 "url": "https://..." 加 headers。改完配置要重启 Host,热加载不可靠。
权限网关的最小实现
多智能体场景下,别让每个 agent 直接持有全部 server 连接。中间加一层,按 agent 身份过滤工具列表:
POLICY = {
'triage': {
'allow': {'get_order_status'},
'confirm': set(),
},
'ops': {
'allow': {'get_order_status', 'list_shipments', 'restart_service'},
'confirm': {'refund_order'},
},
}
def visible_tools(agent_id: str, all_tools: list[dict]) -> list[dict]:
rule = POLICY.get(agent_id)
if rule is None:
return []
return [t for t in all_tools if t['name'] in rule['allow'] | rule['confirm']]
def dispatch(agent_id: str, tool_name: str, args: dict):
rule = POLICY.get(agent_id)
if rule is None or tool_name not in rule['allow'] | rule['confirm']:
audit(agent_id, tool_name, args, 'denied')
raise PermissionError(f'{agent_id} 无权调用 {tool_name}')
if tool_name in rule['confirm'] and not ask_human(agent_id, tool_name, args):
audit(agent_id, tool_name, args, 'rejected_by_human')
return {'status': 'rejected'}
result = call_mcp(tool_name, args)
audit(agent_id, tool_name, args, 'ok')
return result
关键点在于 visible_tools 和 dispatch 各校验一次。只过滤可见工具不够——模型可能从历史消息里翻出旧的工具名,所以实际调用时还要再拦一道。
6. 选型清单
| 你的情况 | 建议 | 理由 |
|---|---|---|
| 只让本机 AI 编辑器访问内部工具,单用户 | MCP server + stdio | 不用管鉴权,配置三行 |
| 多人共享同一批内部工具 | Streamable HTTP + OAuth 2.1 | token 按用户隔离,能审计 |
| 只需要固定流程编排 | 直接写工作流,不上框架 | 确定性任务用代码表达更省事 |
| 需要状态机、断点续跑、人工介入 | LangGraph | 图结构显式,检查点机制成熟 |
| 团队小,想快速跑通多 agent | OpenAI Agents SDK | handoff 和 tracing 开箱可用 |
| 需要多角色讨论式协作 | AutoGen | GroupChat 模式现成 |
| 需要快速搭业务型 agent 团队 | CrewAI | 角色和任务声明式,上手快 |
下一步可以做什么
翻一遍你团队现有的内部接口列表,挑一个纯只读、调用量不大、返回结构简单的,包成 MCP server,挂在 AI 编辑器里用一周。重点不是让它干活,是收集模型在真实调用中犯的错——参数格式、工具选错、无效重试。这份错误清单决定了你后面加写操作时要补哪些约束。
说明:本文涉及的协议版本信息以 https://modelcontextprotocol.io/specification 上的公开规范为准,SDK API 以对应仓库当前文档为准。MCP 规范仍在按日期迭代,接入生产环境前请核对实际依赖版本,不要直接照搬本文示例中的版本假设。示例代码为演示结构,query_order、do_refund 等函数需自行实现。