症状: Agent 能聊天,但一接工具就参数对不上、重试把订单打两遍,或者 MCP 服务明明在线却“看不见工具”。
最快解法: 把 JSON 当成 Agent 与工具之间的唯一契约:用 JSON Schema 约束入参,用固定字段回传结果,再用 MCP 把这份契约从单应用搬到可发现、可鉴权的协议层。
这篇文章面向正在把大模型接到真实系统的开发者、平台工程师和架构师。你要搞清楚的不是又一个名词,而是同一份 JSON 如何在 Function Calling、Tool Calling 和 MCP 之间流转,以及哪一层失败会让整条链路停住。
如果你正在评估框架而不是协议本身,可以先读 AI Agent 开发框架怎么选;工具真正执行时还涉及隔离边界,可对照 AI Sandbox、Docker 与虚拟文件系统的差异。
AI Agent 为什么离不开 JSON
模型擅长生成自然语言,系统只认结构化指令。Agent 一旦要查库存、改工单、跑测试或打开浏览器,就必须把“意图”编成程序能校验、能拒绝、能审计的数据。JSON 正好卡在这个缝里:人能读,机器能解析,HTTP、日志、队列和数据库都吃得下。
更关键的是可验证。一段散文无法告诉网关“order_id 必须是字符串、amount 必须是正数、currency 只能是枚举”。JSON Schema 可以。官方 Function Calling 文档把函数参数定义成 JSON Schema,正是为了让模型在采样时就往合法结构上靠,而不是事后再猜。查看 OpenAI Function Calling 指南
JSON Schema 本身也规定了类型、必填、枚举和 additionalProperties 等约束;生产环境里这些不是学术细节,而是防止模型发明字段、把字符串塞进数字、或漏掉幂等键的第一道闸。查看 JSON Schema 说明
你可以把它想成三层:
- 对话层:用户说“把上海仓那单改成加急”。
- 契约层:模型输出
{"name":"update_order","arguments":{"order_id":"A938","priority":"expedited"}}。 - 执行层:你的代码校验 schema、查权限、调用业务 API、把结果再写成 JSON 喂回模型。
没有契约层,Agent 只是会说话的聊天窗口。有了契约层,它才可能成为可治理的执行器。
⚠️ 经验判断:如果工具入参还在用“自由文本 + 正则提取”,说明你还没有 Agent 协议,只有提示词运气。先把 schema 写死,再谈多工具编排。
Function Calling:模型第一次学会填表
Function Calling 的本质,是让模型在回复用户之前,先决定要不要调用你声明过的函数,并给出符合 schema 的参数对象。典型数据流只有五步,但每一步都可能丢字段:
- 注册:请求里带上
tools,每个 function 含 name、description、parameters(JSON Schema)。 - 决策:模型根据用户话术与 description 选择函数,或选择直接回答。
- 填表:输出 tool_calls,arguments 通常是 JSON 字符串或结构化对象。
- 执行:应用侧 parse、校验、鉴权、调用真实系统。
- 回灌:把工具结果作为 tool 消息再发给模型,模型才生成对用户的最终答复。
description 不是文案,是路由信号。写“查询天气”会和“查询物流”抢流量;写“仅在用户给出城市名时查询当前气温,不要用于历史气候”才能减少误触发。parameters 里每个字段的 description 同样会被模型当提示词用,所以“order_id:业务订单号,不是支付流水号”这种约束应该写进 schema,而不是只写在系统提示里。
严格模式(strict / Structured Outputs)的意义,是把“尽量像 JSON”升级成“必须符合 schema”。JSON mode 只保证花括号能 parse,不保证缺字段或类型错了会被挡住。生产里你仍要在应用层做二次校验:schema 合法不等于业务合法,例如金额为负数、仓库代码不存在、用户无权操作该订单。
一份最小可执行契约
下面这份 schema 故意把枚举和幂等键写死。模型可以决定要不要调用,但不能发明 warehouse 之外的仓位字段,也不能省略 idempotency_key。
{
"type": "function",
"name": "update_shipment",
"description": "仅在用户明确要求改配送优先级时调用。",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "业务订单号"},
"priority": {"type": "string", "enum": ["standard", "expedited"]},
"idempotency_key": {"type": "string"}
},
"required": ["order_id", "priority", "idempotency_key"],
"additionalProperties": false
}
}
执行侧拿到 arguments 后,正确顺序是:JSON parse → schema 校验 → 权限检查 → 查幂等表 → 调业务 API → 把 {"ok":true,"shipment_id":"..."} 或 {"ok":false,"error":"not_found"} 回给模型。永远不要把堆栈或密钥原文塞进 tool result。
Tool Calling:从“函数”变成“工具平面”
业界很快发现:模型要碰的不只是你自己写的函数。搜索、代码解释器、浏览器、计算机操作、文件检索,都是“工具”。于是 API 从早期的 function_call 演进到统一的 tools / Tool Calling。名字变了,JSON 契约没变:仍然是 name + schema + 调用记录 + 结果回灌。
Anthropic 把这一能力叫 Tool use,并用 JSON Schema 描述 input_schema;模型返回的 tool_use 块同样是结构化参数,应用执行后再以 tool_result 回传。查看 Anthropic Tool use 文档
对工程团队来说,Tool Calling 带来三个新问题,都仍然靠 JSON 解决:
- 并行调用:一次回复里多个 tool_calls。你要决定哪些可并行(读操作),哪些必须串行(先锁库存再扣款)。
- 工具审批:高风险工具先返回“待批准”的 JSON 状态,而不是直接执行。
- 可观测性:trace 里要能还原 tool 名、参数摘要、耗时、错误码,而不是只存一段模型散文。
Tool Calling 还把失败类型从“模型胡说”扩展成“模型选对了工具但参数越权”。例如工具叫 run_sql,schema 只允许 SELECT,模型却在某个字符串字段里塞进 DDL。JSON Schema 拦得住类型,拦不住语义攻击,所以危险工具必须在执行层再做允许列表、超时和沙箱。
MCP:把 JSON 契约做成可插拔协议
Function / Tool Calling 解决的是某一次 API 请求里模型和本应用工具怎么说话。MCP(Model Context Protocol)解决的是工具如何被发现、被多个客户端复用、被独立进程托管。规范里,客户端与服务器用 JSON-RPC 2.0 交换消息:初始化、列出 tools/resources/prompts、调用 tool、流式通知。查看 MCP 规范
对开发者最直观的数据流是:
- 宿主(IDE、Agent 运行时、聊天应用)作为 MCP Client 启动。
- 通过 stdio、SSE 或 Streamable HTTP 连上 MCP Server。
initialize协商协议版本与能力。tools/list拿到工具清单:每个工具仍带 JSON Schema 形式的 inputSchema。- 模型侧还是走 Tool Calling:选中某个 MCP 工具后,Client 发
tools/call,参数是 JSON。 - Server 执行后返回 content(文本、图片或结构化内容),Client 再把它编成 tool result 喂给模型。
所以 MCP 并没有取代 JSON,而是把原来写死在应用代码里的 tools 数组,变成一份运行时可发现的工具目录。同一套 Git 服务器、同一套浏览器自动化、同一套内部工单 API,可以被 Cursor、Claude Desktop 或你自建的 Agent 共用,只要它们都讲 MCP。
这也解释了为什么“装了 MCP 仍调不到工具”几乎总是 JSON 契约问题:initialize 失败、list 返回空、schema 与模型提供商的 strict 模式不兼容、call 的 arguments 缺 required 字段、或 Server 用自然语言错误代替 JSON-RPC error。
完整数据流:从用户一句话到 MCP 执行
把三层叠在一起,一次真实任务大致长这样:
- 用户输入自然语言。
- Agent 运行时组装 messages + 本地 tools + 从 MCP
tools/list拉来的远程 tools。 - 模型做 Tool Calling,输出 JSON 参数。
- 运行时按 tool 来源分流:本地函数直接执行;MCP 工具走 JSON-RPC
tools/call。 - 执行结果写成 JSON(或 MCP content 数组)回到对话。
- 模型决定继续调用、询问用户,还是给出最终答案。
这里有一个容易忽略的边界:模型从未直接持有你的数据库连接。它只产出 JSON。谁解析这份 JSON、在哪台机器上执行、有没有 VNC/SSH 会话、有没有网络出站,全是你的运行时策略。云端 Mac 上跑 Agent 或 MCP Server,和在笔记本上跑,协议相同,权限平面不同。
| 层 | JSON 扮演的角色 | 你该卡住的点 |
|---|---|---|
| Function Calling | 单次请求的函数参数 | schema、strict、二次业务校验 |
| Tool Calling | 统一工具平面上的调用记录 | 并行、审批、trace |
| MCP | 跨进程的工具发现与调用 | 传输、鉴权、list/call 错误码 |
选型时不要被产品名带走。先问:工具是只给这一个应用用,还是要给多个 Agent 宿主复用?前者 Function/Tool Calling 足够;后者才值得上 MCP Server。反过来,没有 schema 和校验,上了 MCP 也只是把混乱从一个进程搬到另一个进程。
JSON 一坏,整条 Agent 链路都会坏
生产里最常见的不是模型“不够聪明”,而是契约在某一跳变形了。按跳排查会快很多:
- 模型输出不是合法 JSON:未开 strict、prompt 里要求“先解释再调用”、或把 arguments 包进 Markdown 代码块。处理:强制 structured tool 通道,禁止从 assistant 散文里二次提取。
- JSON 合法但 schema 不通过:缺 required、enum 写错、多了 additionalProperties。处理:把校验错误作为 tool result 回给模型,限制重试次数。
- schema 通过但业务失败:订单不存在、库存不足、权限拒绝。处理:返回稳定 error code,而不是 500 HTML。
- 重试造成双写:超时后模型再调一次。处理:arguments 里强制 idempotency_key,执行层去重。
- MCP list 与 call 不一致:list 里有工具,call 时 name 对不上,或 schema 在热更新后变了。处理:对工具名和 schema 做版本哈希,变更即重新 initialize。
- 敏感数据进了 JSON 日志:完整参数写进可被多人读取的 trace。处理:只记字段名与哈希,对 token、邮箱、身份证脱敏。
把这些失败写成同一个验收场景:故意让工具超时、故意让 schema 失败、故意拒绝权限,看 Agent 是循环重试、胡乱改参数,还是停下来请求人工。过不了这一关的系统,不该接到支付、发货或生产数据库。
上线前用这份 JSON 契约清单验收
- [ ] 每个工具都有 name、description、JSON Schema,且 additionalProperties 为 false。
- [ ] 高风险字段使用 enum / format / 最大长度,而不是自由字符串。
- [ ] 写操作带幂等键,超时与重试策略写进运行时而不是提示词。
- [ ] tool result 使用稳定结构:
ok、error、data,禁止把异常堆栈原文回灌。 - [ ] 并行 tool_calls 已按副作用分组;付款、发邮件、删数据默认串行。
- [ ] MCP Server 的 initialize / tools/list / tools/call 都有超时、鉴权和错误码。
- [ ] 工具清单变更后,宿主会重新拉取 schema,而不是缓存一份过期 JSON。
- [ ] trace 能还原调用链,但不能还原密钥和完整个人信息。
- [ ] 用固定输入回归:同一句话必须打到同一工具名和同一必填字段集合。
清单通过之后,再考虑多 Agent、记忆图谱或更重的工作流。协议不稳,上层编排只会把坏 JSON 复制得更快。
独立 FAQ
为什么不直接让模型输出自然语言给程序解析?
自然语言没有必填字段和类型。程序无法稳定判断“改成加急”对应哪个枚举,也无法在网关层拒绝多余字段。JSON Schema 把解析变成校验,失败可以结构化回传,而不是靠正则碰运气。
Function Calling 和 Tool Calling 有实质差别吗?
对应用开发者而言,核心都是“模型填 JSON 表单,你的代码执行”。Tool Calling 是更宽的平面:除了自定义函数,还包括搜索、代码执行等宿主工具,并更强调并行调用与统一的 tool 结果通道。迁移时优先对齐 schema 与消息角色,而不是纠结旧字段名。
什么时候才需要 MCP,而不是在应用里写死 tools 数组?
当同一组工具要被多个宿主复用、需要独立进程隔离、或工具由另一个团队发布时,MCP 才划算。如果工具只服务一个内部服务,且生命周期与应用绑定,Function/Tool Calling 更简单,少一层传输和鉴权。
模型偶尔输出坏 JSON,应该重试还是换协议?
先开严格结构化输出,并禁止从散文里抽参数。若仍高频失败,检查 schema 是否过深、description 是否互相冲突、工具数量是否过多。这些问题换 MCP 不会消失,因为 MCP 调用的入参同样是 JSON。
如果你现在用共享笔记本跑 Agent,环境一变 schema 回归就失效,工具进程也容易和日常开发抢端口。租用 Kvmjet 的裸金属 Mac 云主机,可以固定一份 macOS 镜像,用 SSH 或 VNC 复现同一条 Tool Calling / MCP 失败;公开说明提供按天到按季的周期,适合先把 JSON 契约跑稳再决定是否长期占用机器。
建议带着上面的清单继续查看 Kvmjet 帮助中心,核对持续运行、权限隔离和远程接入方式,再决定 MCP Server 放在本地、机房还是云端 Mac。
把 Tool Calling 和 MCP 跑在固定的 Mac 环境里
JSON Schema、幂等键和 MCP Server 都依赖可复现的运行时。共享笔记本一换目录、一抢端口,回归立刻失效。
Kvmjet 的裸金属 M4 Mac 提供真实 macOS、独享带宽和 SSH/VNC,适合把同一条失败的 tools/call 反复打通后再决定长期占用。