Function Calling / Tool Calling / MCP JSON-RPC — 四段往返 / schema 方言 / 可观测与隔离
如果你打开 Cursor、Claude Code 或任意 Agent 框架的调试日志,几乎每一行有效载荷都是 JSON:工具清单是 JSON Schema,模型返回的 tool_calls 是 JSON,MCP Server 收发的帧是 JSON-RPC 2.0。这不是偶然——Agent 要在「不可预测的自然语言」和「可执行的程序接口」之间搭桥,而 JSON 恰好是两边都能稳定解析的中间磁带格式。
本文按时间线串起 Function Calling → Tool Calling → MCP,把一次完整工具调用的四段 JSON 往返画清楚。若你正在部署 云端 Mac 上的 MCP Server,或从 GitHub Agent 开源选型里挑框架,理解这条数据流比背 API 文档更能帮你排障。
Agent 世界几乎「万物皆 JSON」
大模型原生输出是 token 流,而 Shell、数据库、HTTP API 需要的是结构化参数。早期做法是在 Prompt 里写「请按以下 JSON 格式回答」——模型经常多逗号、少引号、夹带 Markdown 代码块,解析失败率居高不下。
2023 年起,主流 API 把「结构化动作」收进协议层:模型不再自由生成任意 JSON 文本,而是在服务端约束输出形态,由 Host 负责执行。于是整条链路变成:
- 声明:用 JSON Schema 描述工具能做什么;
- 决策:模型在受限 grammar 里选出工具名与参数;
- 执行:Host 把参数交给本地进程或 MCP Server;
- 回写:把执行结果再以 JSON 消息塞回上下文。
设计原则:Agent 的可观测性 = 能否把每一步 JSON 落日志、重放、对比。没有结构化中间态,就没有可审计的自动化。
Function Calling:从自然语言到结构化动作
OpenAPI 时代的 function 对象
OpenAI 在 Chat Completions API 里引入 functions / tools 字段:你在请求里附带工具定义,模型在响应里返回 function_call 或 tool_calls,而不是一段需要你自己 regex 的文本。
典型请求片段
{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "查一下东京天气"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "按城市返回当前天气",
"parameters": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "城市名" }
},
"required": ["city"]
}
}
}]
}
旧式 Prompt 里手写 JSON 模板的做法 已被协议内建约束取代;Host 侧只需校验 arguments 是否符合 schema,再调用真实函数。Function Calling 解决的是「模型说什么」,还没解决「工具跑在哪、怎么发现、怎么鉴权」——这留给 Tool Calling 生态与 MCP。
Tool Calling:一次对话里的四段 JSON 往返
「Tool Calling」通常指 Host 与模型 API 之间的完整回合,可拆成四步:
- 注册 — Host 把
tools[](含 JSON Schema)发给模型; - 决策 — 模型返回
tool_calls,每个 call 带id、name、arguments(字符串化的 JSON); - 执行 — Host 解析 arguments,调用本地函数或远程 MCP;
- 回写 — 以
role: tool消息把结果 JSON 写回,再请求模型生成面向用户的自然语言。
模型返回的 tool_calls 长什么样
{
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"city\":\"Tokyo\"}"
}
}]
}
注意 arguments 是字符串而非嵌套对象——这是为了与 streaming 和部分 SDK 兼容。Host 必须 JSON.parse(arguments) 后再校验;按 Ctrl + Shift + I 打开开发者工具时,你会在 Network 里看到完全相同的结构。
四段往返对照表
| 阶段 | 方向 | 载荷形态 | 谁解析 |
|---|---|---|---|
| ① 注册 | Host → API | tools[] + JSON Schema | API 网关 |
| ② 决策 | API → Host | tool_calls | Host / SDK |
| ③ 执行 | Host → Tool | 解析后的对象 | 工具实现 |
| ④ 回写 | Host → API | {role:"tool", content:"..."} | 模型 |
各家 API 的 schema「方言」
概念统一了,字段名并未完全统一。选型或写适配层时要对照:
- OpenAI / 兼容端点
tools+tool_choice;function嵌在type: function里;streaming 时delta.tool_calls分片到达。- Anthropic
tools使用input_schema;响应为tool_use块,需用tool_result回传。- Google Gemini
functionDeclarations;返回functionCall,部分字段命名与 OpenAI 不同。- 本地 Ollama
- 通过 OpenAI 兼容层暴露
tools,能力取决于具体模型是否训练过 tool use。
框架如 LangGraph、DeepSeek Harness(见 本站 Harness 指南)的价值之一,就是把这些方言压成一层统一的 Tool 抽象,底层仍是一坨 JSON。
MCP:JSON-RPC 把工具层标准化
Model Context Protocol(MCP) 解决的是 Function Calling 之下 的一层:工具进程如何被发现、如何传参、如何跨 Host 复用。传输可以是 stdio 或 SSE,但消息体一律 JSON-RPC 2.0。
MCP 与 API Tool Calling 的分工
两层协议栈
| 层级 | 协议 | 典型方法 |
|---|---|---|
| 模型 API 层 | REST / OpenAI 兼容 | chat/completions + tools |
| MCP 层 | JSON-RPC 2.0 | tools/list、tools/call |
| 业务层 | 任意 | 读文件、查库、发 HTTP |
Host(如 Cursor)启动时向 MCP Server 发 tools/list,拿到工具清单后翻译成自家模型 API 的 tools[] 格式。用户一句话触发模型 tool call 后,Host 再发 tools/call 给 MCP Server——整条链上 JSON 出现了两次「方言转换」,但语义一致。
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": { "path": "/tmp/demo.txt" }
}
}
stdio 模式下 JSON 帧如何分隔?
MCP 在 stdio 传输中使用 Content-Length 头 + JSON body(类似 LSP),而不是换行分隔的 NDJSON。这样单条消息可很大(例如带 base64 的资源),Host 按长度读取完整帧后再 JSON.parse。SSE 模式则把 JSON-RPC 包在 HTTP 事件流里,适合多客户端共享同一 Server。
完整数据流:从用户输入到工具执行
把上图展开成可操作的检查清单:
- 日志里是否记录了每次
tools/list的版本与工具数量? tool_calls的arguments是否在执行前经过 schema 校验?- MCP
tools/call超时与错误是否映射成模型可读的tool消息? - 敏感字段是否在写日志前脱敏?
这与 AI Coding Workflow 里的 Verify 阶段 同源:没有可解析的中间态,就无法自动验收 Agent 是否「真的调对了工具」。
为什么不用 Protobuf 或 XML?
Protobuf、gRPC 在微服务里更高效,但在 Agent 场景有几个硬伤:
- 模型侧:训练与推理接口以 JSON 为主,强行换二进制要额外序列化层;
- 调试:工程师需要在日志里直接读参数,JSON 人类可读;
- Schema 生态:JSON Schema 与 OpenAPI 已是事实标准,MCP 工具描述直接复用;
- 浏览器与脚本:前端 Demo、n8n 节点、curl 测试都假设 JSON。
代价是体积与解析 CPU。高 QPS 场景会在 MCP Server 内侧做缓存、压缩日志,或只对大 payload(图片、文件)用 base64 塞进 JSON 字段——而不是换掉整条协议的 JSON 外壳。
JSON 的代价与工程补救
常见失败模式
排障优先级
| 现象 | 根因 | 补救 |
|---|---|---|
| arguments 解析失败 | 模型输出非法 JSON 字符串 | 收紧 schema、降低 temperature、加 retry |
| 调错工具 | 工具描述含糊或过多 | 合并工具、写清 negative example |
| MCP 无响应 | stdio 帧不完整或进程挂死 | launchd 守护、健康检查 |
| 日志泄露密钥 | tool result 含环境变量 | 结果脱敏、独立执行账号 |
生产环境建议:把「执行面」放在隔离节点——例如在云端 Mac 上跑 MCP Server(参见 stdio 到 SSE 的部署指南),本机 Cursor 只通过 SSH 隧道发 JSON-RPC,Shell 权限不落在日常笔记本上。
回到标题:AI Agent 离不开 JSON,不是因为工程师懒惰,而是因为自然语言决策与程序化执行之间必须有一层人人可读、机器可校验、可插拔的契约。Function Calling 定了「模型说什么」,Tool Calling 定了「Host 怎么跑一圈」,MCP 定了「工具进程怎么挂进生态」——三层叠在一起,JSON 仍是那条贯穿始终的线。
用云端 Mac 承载 MCP 的 JSON 执行面
M4 独享节点,按天租用,SSH 开箱即用
新加坡 · 日本 · 韩国 · 香港 · 美国节点可选
把 tools/call 落在隔离的云 Mac 上,本机 Host 只传 JSON-RPC——实验环境可随时快照回滚。 查看 ZekVPS 云端 Mac mini 套餐