
做 Agent 开发这段时间我越来越觉得一个现象很有意思原本躺在 JSON-RPC 规范文档里吃灰二十年的老协议居然因为 AI Agent 的爆发被重新翻了出来而且成了很多 Agent Harness、MCP Server、工具调用链路里默认的通信底座。查了下社区趋势光是围绕 JSON-RPC 2.0 和 Agent Harness 的讨论今年以来热度明显上升。这篇文章我就以“漫话”的方式把这一轮 protocol 复兴的前因后果、协议细节、实战编码和踩坑经验一次性讲清楚。我会先从协议本身聊起再结合 Agent Harness 里真实的调度场景给出可以直接抄的代码示例和配置方案最后聊聊我在生产环境里遇到过的典型问题。如果你正准备做 agent 工具注册、MCP server、或者任何“让大模型调用外部能力”的中间层这篇内容应该能帮你省掉不少摸索时间。1. 先搞明白JSON-RPC 2.0 到底是个什么协议1.1 一句人话概括它的定位JSON-RPC 2.0 是一种基于 JSON 的远程过程调用协议。什么意思就是它定义了一套“我该怎么把一个函数调用请求发给对方对方该怎么把执行结果返回给我”的标准格式。想象你给远方的朋友寄快递你不能只把东西塞进箱子就完事总得有个面单上面写着寄件人、收件人、物品名称、备注信息。JSON-RPC 2.0 就是这个面单的规范怎么填写方法名、怎么填写参数、怎么区分这次调用是第几次、出错时怎么描述错误。这套规范在 2010 年正式定稿由 JSON-RPC 工作组维护目前主流版本是 2.0。注意它不依赖具体传输层HTTP、WebSocket、TCP、Unix Socket 都可以承载这种“传输无关”的特性正是它后来被 AI Agent 生态选中的关键原因之一。1.2 协议核心规则其实也就五条JSON-RPC 2.0 的规范不长核心规则可以浓缩成五条掌握这五条基本上就能读懂 90% 的 Agent 工具调用报文请求必须是一个 JSON 对象包含jsonrpc: 2.0、method、params、id四个字段其中jsonrpc和method必填params可以缺省id用于配对请求和响应。响应分两种成功响应返回result字段失败响应返回error字段两者互斥id必须和请求一致。参数支持两种形式按位置传入的数组params: [10, 20]和按名称传入的对象params: {base: 10, height: 20}。通知Notification是一种特殊的请求它没有id字段表示“你执行就好不用回复我”。支持批量调用客户端可以把多个请求放进一个 JSON 数组一次性发给服务端服务端返回一个响应的数组。这五条里面id的配对逻辑是最容易被忽略的。在实际 Agent 调度中多个工具调用往往是并发的比如 Agent 同时调用了两个工具请求返回时两个响应可能乱序到达客户端就是靠id来区分“这个响应到底对应哪个请求”。我见过不少同学第一次写工具调用时把id写死成 1并发一上来响应全部错乱。1.3 为什么它火了二十年直到今天才被“捧红”说句公道话JSON-RPC 2.0 在 2010 年定稿后长时间处于“规范经典但应用不温不火”的状态。原因也不难理解REST 风格借着 HTTP 的东风占领了互联网 API 的主流大家习惯用 GET/POST 加资源路径来描述业务前端后端联调也简单。JSON-RPC 2.0 这种 RPC 风格反而显得有点“老派”。但事情到 2024、2025 年出现了转机。AI Agent 兴起后系统里出现了一个很尴尬的需求大模型不是直接调数据库、调 API 的它要通过 Agent Harness 这类调度框架去调用外部工具。这里面的每一次工具调用都天然是一个“远程过程调用”——你要调用一个函数这个函数不在本地在执行环境或远端服务里可能是另一个进程、另一台机器。REST 风格在这一场景下显得很笨重你需要自己定义一堆“任务创建接口”“任务查询接口”“结果回调接口”而 JSON-RPC 2.0 直接用一个请求就能说清楚“我要调这个函数参数是这些”。再加上 Anthropic 的 MCPModel Context Protocol协议明确选择了 JSON-RPC 2.0 作为消息格式基础等于给这份老协议做了一次行业级背书。MCP 现在几乎是 Agent 工具接入的事实标准它的初始化、工具列表、工具调用、资源读取底层全是 JSON-RPC 2.0。于是大量开发者开始补课原来这份躺在仓库里的老协议才是 Agent 生态的真正“交通规则”。2. AI Agent 把这份老协议从仓库里翻了出来2.1 Agent Harness 的通信困境恰好是老协议的主场先解释一下 Agent Harness 是什么。它不是一个具体的软件而是一个抽象概念负责把大模型、工具、记忆、执行环境这些组件编排起来形成一个能自主完成任务的闭环系统。你可以把它理解成 Agent 的“驾驶室”或“控制台”。LLM 是发动机工具是方向盘和刹车Harness 就是把这些部件连接起来的线路和操作系统。Harness 里面最核心的循环是这样Agent 接收用户指令LLM 生成下一步决策Harness 解析这个决策调用对应工具把工具结果返回给 LLMLLM 再基于新信息继续决策。这个循环里Harness 和工具执行端之间的通信频率非常高而且每次通信都带着“调用哪个方法、传什么参数、给我什么结果”的语义。如果用 REST你得设计一堆资源比如POST /tool-calls、GET /tool-calls/{id}还要处理轮询和回调链路复杂不说延迟还高。JSON-RPC 2.0 天然就是为这种“我要调用你的一个函数”设计的请求-响应一一对应语义干净利落。我在实际项目里见过一个很典型的报错热词error: agent harness runtime codex is unavailable because its plugin registration ...。说白了就是 Harness 尝试通过某个插件注册通道去连接名为 codex 的运行时结果插件注册失败。这类问题往往就出在 Harness 与插件、运行时之间的通信配置上而这一层通信现在越来越多地跑在 JSON-RPC 2.0 之上。理解协议本身对排查这类问题非常有帮助。2.2 对比 REST、WebSocket 自定义协议它赢在哪拿 REST 来比。REST 把动作都映射到资源上工具调用这种“函数式”操作塞进 REST 里怎么想怎么别扭。比如“调用fetch_page并传入url参数”REST 可能要写成POST /api/fetch-page参数放 body状态靠 HTTP 状态码表达还得专门设计错误结构。JSON-RPC 2.0 则直截了当method: fetch_page、params: {url: ...}整个报文的意图一眼就能看懂。再拿 WebSocket 自定义协议来比。很多团队在 Agent 场景下选择 WebSocket因为需要长连接和双向通信这没错。但不少团队直接在 WebSocket 上发明自己的消息格式今天{type: call, function: xxx}明天改成{action: invoke, name: xxx}几个月后接手的人一头雾水。JSON-RPC 2.0 提供了一个标准化的消息格式你把它跑在 WebSocket 上格式稳定、文档齐全、有现成 SDK等于在灵活性之上加了一层可靠约束。说白了JSON-RPC 2.0 不是来取代 WebSocket 的它和 WebSocket 是搭档WebSocket 负责管道JSON-RPC 2.0 负责管道里流动的报文格式。2.3 与 MCP 的关系以及其他协议热词的一笔带过近期热词里经常看到 MCP 协议或者再往广了说SPI、MIPI、CAN、MODBUS、MQTT、USB 等等都出现了。这类协议热词的集中出现其实反映出两件事一是硬件和嵌入式场景对协议的需求长期存在二是 AI Agent 生态又把“协议”这个词推到了大众眼前。这里要厘清一个概念SPI、IIC 这类是硬件总线协议MQTT 是物联网消息协议MODBUS 是工业控制协议它们和 JSON-RPC 2.0 不是同一个层次的东西解决的问题也不同。MCP 不一样它和 JSON-RPC 2.0 的关系是“建筑”和“建材”的关系。MCP 定义了 Agent 应用如何发现工具、如何调用工具、如何获取资源等一整套交互规则但它的消息载体就是 JSON-RPC 2.0。你可以打开任意一个 MCP 的调试日志看到的所有报文都是 JSON-RPC 2.0 格式。因此你要学 MCP本质上先要精通 JSON-RPC 2.0。这也是我强烈建议每个做 Agent 开发的工程师花一个小时认真读一遍 JSON-RPC 2.0 规范原文的原因。3. 从零手写一轮 JSON-RPC 2.0 调用就这么简单3.1 一个最简 Agent 工具调用请求纸上谈兵没有意义我们直接看一个真实场景。假设你的 Agent 需要调用一个网页抓取工具fetch_page参数是目标 URL预期的返回值是网页正文内容。在 Agent Harness 和工具服务之间走 HTTP 传输的 JSON-RPC 2.0 报文长这样{ jsonrpc: 2.0, id: 1, method: fetch_page, params: { url: https://example.com/article/123 } }服务端处理成功后返回{ jsonrpc: 2.0, id: 1, result: { title: Example Article, content: ......, status_code: 200 } }看到没有整个交互就像一次本地函数调用方法名、参数、返回值一目了然。这里的id: 1不是什么魔法数字它是这次请求的流水号。Agent 同时发出多个工具调用时每个请求的id都不同响应里的id用来告诉客户端这是哪次调用的结果。如果你只是想快速测试用 curl 直接 POST 一个 JSON 请求就行。比如工具服务监听在localhost:9000/jsonrpc可以这样做curl -X POST http://localhost:9000/jsonrpc \ -H Content-Type: application/json \ -d { jsonrpc: 2.0, id: 1, method: fetch_page, params: { url: https://example.com } }注意两个细节一是 HTTP 方法用什么不重要POST 是惯例但也有团队用 GET 或 PUTJSON-RPC 2.0 规范本身不管这个实际选型时跟随团队的既有规范即可二是Content-Type必须是application/json很多联调问题就出在application/json; charsetutf-8和application/json的差异上后文专门讲。3.2 通知、批量调用、错误处理三条容易被忽略的硬规则讲完最简场景接着讲三个不太起眼但影响深远的规则。第一条是通知。通知就是没有id的请求比如{ jsonrpc: 2.0, method: log_event, params: { event: agent_started } }服务端收到通知后必须执行对应操作但不返回任何响应。这个特性在 Agent 场景里最适合做日志上报、状态同步、进度通知。你不需要等待结果也犯不着为了一次日志上报专门设计一个回调接口。但要记住通知不能用于那些你必须知道执行结果的调用。如果你把一个工具调用写成通知Agent 将永远等不到工具的返回结果整个决策循环就卡死了。第二条是批量调用。客户端可以把多个请求放进一个数组一次性发给服务端[ {jsonrpc: 2.0, id: 1, method: fetch_page, params: {url: https://example.com/a}}, {jsonrpc: 2.0, id: 2, method: search_web, params: {q: JSON-RPC agent}}, {jsonrpc: 2.0, method: log_event, params: {event: batch_started}} ]服务端会返回一个数组数组里只包含有id的请求的响应。批量调用适合一次需要调用多个互不依赖的 Agent 工具的场景可以减少网络往返。不过这里有个坑后面讲排查时细说。第三条是错误处理。当服务端执行失败时返回的对象里不能有result必须有error{ jsonrpc: 2.0, id: 1, error: { code: -32602, message: Invalid params, data: { detail: url must be a valid HTTP URL } } }error对象包含code、message、data三个字段其中code和message必填data可选。这里最关键的认知是编码错误码时不要自己发明一套风格迥异的错误体系而应该从 JSON-RPC 2.0 的保留错误码出发扩展业务错误码。3.3 在 Agent Harness 里注册一个工具原来要经历这些步骤明白了协议报文我们再回到 Harness 场景看一个工具从注册到被调用的完整链路。以我常用的 Python 栈为例假设我用 Flask 搭建了一个极简的 JSON-RPC 2.0 工具服务from flask import Flask, request, jsonify app Flask(__name__) TOOLS { fetch_page: { description: Fetch a web page and return its text content, params: {url: {type: string, required: True}} } } def handle_request(payload): if not isinstance(payload, dict) or payload.get(jsonrpc) ! 2.0: return {jsonrpc: 2.0, id: None, error: {code: -32600, message: Invalid Request}} method payload.get(method) params payload.get(params, {}) req_id payload.get(id) if method not in TOOLS: return {jsonrpc: 2.0, id: req_id, error: {code: -32601, message: fMethod not found: {method}}} if method fetch_page: url params.get(url) if not url: return {jsonrpc: 2.0, id: req_id, error: {code: -32602, message: Invalid params: url is required}} try: result {title: Example, content: page content here} return {jsonrpc: 2.0, id: req_id, result: result} except Exception as e: return {jsonrpc: 2.0, id: req_id, error: {code: -32000, message: str(e)}} app.route(/jsonrpc, methods[POST]) def jsonrpc_endpoint(): payload request.get_json() if isinstance(payload, list): responses [handle_request(item) for item in payload] responses [r for r in responses if r.get(id) is not None] return jsonify(responses) return jsonify(handle_request(payload)) if __name__ __main__: app.run(port9000)在这个服务端实现里有几个地方值得注意。一是入口统一处理了批量请求然后过滤掉通知id为None的响应因为通知不需要返回。二是错误码的选取-32600表示无效请求-32601表示方法不存在-32602表示参数无效-32000起是服务端错误这些都是规范里的保留范围。三是handle_request对jsonrpc版本做了校验防止老版本客户端接入。服务端就绪后Agent Harness 侧的调用逻辑反而简单因为它的职责就是“构造请求、解析响应、把结果反馈给 LLM”。用一个 Python 客户端示例import requests def call_tool(method, params, request_id): resp requests.post( http://localhost:9000/jsonrpc, json{jsonrpc: 2.0, id: request_id, method: method, params: params}, timeout10, ) payload resp.json() if error in payload: raise RuntimeError(fTool call failed: {payload[error][message]}) return payload[result] # Agent 决策循环里的一次调用 page call_tool(fetch_page, {url: https://example.com}, request_id101)到这里一个最简的 Agent 工具调用闭环就跑通了。但实践里远远没有这么简单下面这部分才是真正的经验所在。4. 实操中踩过的坑从“好像能通”到“稳如老狗”4.1 Content-Type 与 HTTP 状态码协议之外的隐形杀手先说一个最基础也最容易翻车的点HTTP 层的状态码和 Content-Type。JSON-RPC 2.0 规范没有规定 HTTP 状态码怎么用所以有些服务端实现不管执行成功还是失败一律返回 HTTP 200错误信息只放在 body 的error字段里。另一些实现则会在业务错误时返回 HTTP 500 甚至 400。这两种风格在 Agent 生态里都存在MCP SDK 普遍倾向于“HTTP 200 协议错误码”的方式。这意味着你在写客户端时不能简单依赖resp.status_code判断成败必须先拿到 body解析 JSON再看里面有没有error字段。我在生产环境里遇到过一个问题服务端网关把application/json响应拦下来误改成了text/plain客户端一用resp.json()解析直接抛 JSONDecodeError排查半天才发现是网关配置的问题。所以第一件事就是确认整个链路里的 Content-Type 保持application/json不被篡改。4.2 id 的匹配与并发一个写死 id 引发的线上事故前文提过写死id的问题这里展开讲。Agent 场景下Harness 经常会并发调用多个工具比如同时查天气和查日历。如果客户端代码把id写死成某个常量那么两个并发的请求都带id: 1服务端返回两个id: 1的响应客户端根本分不清哪个对应哪个结果就是 Agent 拿天气数据去回答日历问题或者直接报 mismatch 错误。正确做法是维护一个自增或 UUID 的 id 生成器或者至少在客户端代码里用request_id str(uuid.uuid4())保证唯一。另外要注意JSON-RPC 2.0 规范里id可以是字符串、数字或 null但如果客户端和服务端对 id 的类型约定不一致比如客户端发字符串101服务端返回数字101也会导致匹配失败。我的经验是在团队内部约定统一用字符串类型的 UUID规避隐式类型转换的坑。4.3 批量请求的“部分失败”陷阱批量调用看起来很方便但有一个隐藏很深的规则如果批量请求中的任何一个请求不是有效的 JSON-RPC 请求服务端应当返回一个包含单个错误响应的数组而不是继续处理其他请求。反过来说即使每个请求都合法服务端也是逐项处理某些成功、某些失败失败项走error字段。这个规则的工程含义是不要盲目把大量工具调用放进一个批量请求。如果里面有一个请求构造有问题整个批量可能被判定为无效其他正常的请求也会被一起作废。最好把批量数量控制在合理范围内例如 10 个以内并且对每个请求单独做参数校验。此外批量请求的响应顺序不保证与请求顺序一致客户端必须根据响应里的id重新配对不能想当然按数组下标取。4.4 通知、超时与幂等Agent 场景里绕不开的工程问题通知适合日志和状态上报但它有一个隐患如果服务端执行失败客户端永远不知道。这在 Agent 决策循环里可能是致命的——你上报了“工具执行完成”但工具其实没执行成功Agent 就会带着错误的信息继续往下走。所以对于关键状态同步我建议不要用通知老老实实用带id的请求哪怕只是返回一个{ok: true}。超时是另一个高频坑。LLM 生成响应通常需要几秒到几十秒但工具调用往往是毫秒级可有些工具就是慢比如网页抓取、大文件处理。你给 HTTP 客户端设了 5 秒超时工具执行 8 秒客户端直接超时报错。这里要区分是“调用失败”还是“结果延迟”。如果是对耗时敏感的工具最好在设计时就让工具服务端先快速返回一个“任务已受理”的结果后续 Agent 再通过另一个查询接口取结果或者把超时时间放宽到 30 秒以上。幂等性也值得一提。Agent 决策循环遇到网络超时时常常会重试同一工具调用如果工具本身不幂等比如“下单”“发送消息”这类操作就会造成重复执行。这不是 JSON-RPC 2.0 协议本身能解决的问题但你在设计工具 API 时必须考虑进来最好在参数里增加一个request_id或者让工具服务端对相同id的去重。实际中我通常把 JSON-RPC 2.0 的id同时作为幂等键使用服务端缓存最近处理过的 id重复请求直接返回缓存结果。4.5 错误码速查表排查问题时的对照手册前文提到了错误码这里整理一份速查表方便你在开发联调时快速定位问题。错误码含义可能出现的位置常见原因-32700解析错误服务端入口请求不是合法的 JSON比如多了个逗号-32600无效请求服务端入口请求对象不是合法 JSON-RPC 请求jsonrpc 版本缺失-32601方法不存在方法分发层调用了未注册的工具或方法名拼写错误-32602参数无效参数校验层缺少必填参数、参数类型错误-32603内部错误业务逻辑层服务端代码抛了未捕获异常-32000 至 -32099服务端错误业务逻辑层工具执行过程中出错如网络请求失败我在排查线上问题时第一眼总是先看错误码落在哪个区间。落在 -32600 到 -32603基本是客户端报文构造问题优先查请求体落在 -32000 区间是服务端执行问题优先查日志如果在 -32000 以下或者自定义范围通常是业务层抛出的特定错误需要查工具实现。5. 深入一点手写协议 vs 现成 SDKAgent 框架里怎么选5.1 主流语言的现成实现省力但要看场景全手写 JSON-RPC 2.0 报文并不复杂几十行代码就够但生产环境里我还是建议大家优先用成熟的 SDK尤其是涉及 WebSocket、错误处理、批量请求这些细节时踩过的坑别人已经帮你填平了。主流语言都有对应的实现Pythonjson-rpc、python-json-rpc以及 Flask/FastAPI 的扩展插件。Node.jsjayson是最常用的支持 HTTP、TCP、WebSocket、TLS 多种传输层。Gogolang.org/x/exp/jsonrpc2和github.com/gorilla/rpc都可用。Javajson-rpc-2.0这个项目覆盖得比较完整。Rustjsonrpc-core生态比较成熟适合高并发场景。选 SDK 时建议关注三点是否支持你需要的传输层、批量请求是否实现正确、错误处理是否符合规范。不要为了“少依赖一个包”去手写全部尤其是 JSON-RPC over WebSocket 的服务端发送顺序、心跳、断线重连这些工程细节现成 SDK 会省掉很多麻烦。5.2 哪些场景值得自己封装当然某些情况下手写反而更合适。比如你的 Agent Harness 只是一个内部模块只需要在一个进程里做线程间方法调用为了一个 JSON-RPC 调用引入一整个 HTTP 服务和一堆路由配置明显过度设计。这种时候直接用 Python 的concurrent.futures或者 Node 的worker_threads就能解决连网络层都不需要。还有一种情况你已经有一个基于 gRPC 或 Thrift 的内部微服务通信体系团队也更熟悉那一套。这时候硬塞一个 JSON-RPC 2.0 进去就不太合适。协议选型要看整个技术栈的一致性不能因为 Agent 火了就跟风。如果你决定自己封装我建议至少覆盖几个核心点统一的id生成器、请求上下文的注入、错误码到业务异常的映射、超时控制、日志里带上id做链路追踪。这些基本功做到位后续扩展会顺畅很多。5.3 从单机到分布式JSON-RPC 2.0 如何撑起 Agent 集群最后聊一个更宏观的层面。当你只有一个 Agent Harness、一个工具服务时JSON-RPC 2.0 看起来就像一个小工具。但当你的 Agent 系统扩展到多机分布式时JSON-RPC 2.0 的价值才会真正体现。比如你可以把工具执行服务单独部署成一组无状态节点前面挂负载均衡所有节点都监听同一个 JSON-RPC 2.0 入口。因为报文本身没有“会话状态”任何一个节点都可以处理任意请求。又比如你可以在消息总线和 Agent 服务之间跑 JSON-RPC 2.0 over WebSocket让 Agent 可以实时订阅工具执行进度这就把同步调用扩展成了类似事件驱动的架构。这里有个实践细节分布式场景下每个请求最好带上一个全局唯一的id并且把id和业务 trace_id 打通。这样无论是日志检索还是排查问题拿到一个id就能把整条调用链串起来。我个人的做法是直接用 UUID 作为id同时在params里附带一个trace_id字段服务端记录日志时两者都打点后续通过任何一个 ID 都能定位。再延伸一步JSON-RPC 2.0 的批量调用在分布式场景下还能用来做“扇出”。比如 Agent 需要同时调用三个数据源客户端把三个请求放进一个数组发给网关网关并行调用三个后端服务再把结果聚合返回。注意这里要做超时保护不能让一个慢服务拖垮整个批量请求。更保险的做法是控制并发度和批大小不要把一个包含 50 个请求的大数组一次性砸给网关容易把下游打爆。6. 写在最后的几点个人体会我做 Agent 相关开发这么长时间最大的感触是很多看似高深的新概念底层其实都藏着一些“上了年纪”的老技术。JSON-RPC 2.0 就是典型代表它不是为 AI 而生却在 AI 时代被重新发掘出了光芒。理解它的关键不在于背下规范里那几个字段而在于想明白它解决了什么问题——让一个程序像调用本地函数一样去调用另一个程序的函数干净、简单、可靠。如果你正准备开发 Agent Harness、MCP Server或者任何需要和外部工具通信的中间层我的建议是不要急着铺一堆框架先用 JSON-RPC 2.0 把最核心的一次工具调用跑通体会一下“函数调用”的语义如何在这份协议里被优雅地表达再逐步叠加 WebSocket、批量、并发这些复杂度。跑通之后你会发现后续学 MCP、学各种 Agent 协议都会顺畅很多。另外再分享一个我踩过多次的坑无论是用现成 SDK 还是自己封装都要在开发初期就把日志打完整特别是请求报文和响应报文的原文。Agent 场景下的问题往往不是单点故障而是链路里某一次工具调用的返回和预期不一致导致 LLM 的后续决策全部跑偏。有了完整报文日志定位问题的速度能快一倍。这份看似不起眼的习惯在 Agent 这种“一次决策可能引发十几次调用”的系统里性价比高得惊人。