news 2026/9/8 13:01:36

JSON-RPC 2.0为何成为AI Agent Harness通信首选?协议原理与实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON-RPC 2.0为何成为AI Agent Harness通信首选?协议原理与实战解析

1. 老协议的新甲方:AI Agent 这轮热潮为什么偏偏带火了 JSON-RPC 2.0

我第一次意识到 JSON-RPC 2.0 又火了,是在翻某个 AI Agent 开源项目的源码时。代码仓库里没有我想象中的 REST 接口设计,取而代之的是一排排类似{"jsonrpc": "2.0", "method": "tools.call", "params": {...}, "id": 7}的消息。说实话,那一瞬间我有种穿越感——这玩意儿不是 2010 年就该被 REST 拍死在沙滩上的老古董吗?怎么现在成了 Agent 生态里最眼熟的面孔?

这不是偶然。从 Language Server Protocol(LSP)到 Model Context Protocol(MCP),再到各种 Agent Harness 内部把工具调用、技能注册、会话管理串起来的通信层,JSON-RPC 2.0 的身影无处不在。它原本诞生在 XML-RPC 和 SOAP 混战的年代,却在十几年后等来了一个全新的甲方:AI Agent。

这篇文章是"漫话 Agent Harness"系列的前置篇。在聊 harness 本身之前,我们必须先把这套通信语言聊透——因为在几乎所有 Agent Harness 里,JSON-RPC 2.0 就是那个连接"智能决策"和"具体工具"的脐带。你不搞懂它,后面看 harness 的源码就像看一台没有说明书的机器。

1.1 先理清一个高频热词:harness 和 agent 到底什么关系

最近"harness 和 agent 区别"这个搜索词一直在涨,说明很多人被这两个概念绕晕了。我从实战视角给一个直白的划分:agent 是那颗会思考的脑子,harness 是支撑脑子运转的整个驾驶舱

agent 负责规划、推理、决定下一步调用什么工具;它本身不关心消息怎么编码、进程怎么拉起、插件怎么注册、超时怎么处理。而 harness 管的是这些脏活累活:启动和销毁运行时、维护工具注册表、把 agent 的意图翻译成具体协议消息、隔离单个工具崩溃造成的损害、收集日志和轨迹供调试。

举个实际的例子,很多人搜过这么一条报错:agent harness runtime "codex" is unavailable because its plugin registration failed。注意关键词——plugin registration。这是 harness 层面的问题:某个插件在注册阶段挂了,导致整个 runtime 不可用。这时候 agent 本身可能毫不知情,它还在那儿等工具返回结果呢。这就是典型的"脑子没坏,驾驶舱出了故障"。

而 JSON-RPC 2.0,恰好就是驾驶舱里各个仪表盘、操纵杆和控制台之间通用的"语言"。你按下一个按钮(method 调用),仪表盘亮起一个数值(result 返回),所有设备之间都用统一格式的指令卡交流。这就是为什么聊 Agent Harness 之前,必须先聊这套协议。

1.2 为什么 2025 年回头看,这个协议反而成了最优解

在 AI Agent 爆发之前,JSON-RPC 2.0 最广为人知的"就业岗位"是 LSP——Visual Studio Code 里那个让编辑器懂得各种语言智能提示的协议。前端开发者天天用它,却很少有人意识到自己其实一直在跟 RPC 打交道。

Agent 热潮起来之后,情况完全不同了。工具调用(function calling)、技能注册(skill registration)、MCP 服务器、Agent SDK 的运行时通信,全都需要一个共同的东西:一种纯文本、无状态、能双向发起调用、并且天然带"请求-响应"关联机制的消息格式。翻遍现有的协议,JSON-RPC 2.0 几乎是唯一一个各方面都不需要做伤筋动骨改造的老将。后面我会展开讲它到底哪里适配,先记住这个结论:它不是被时代淘汰后又被挖出来的化石,而是一把本来就在,只是之前没被人用在刀刃上的钥匙。

2. JSON-RPC 2.0 的语法全景:三种句型加一张错误码表

不管你在哪个 Agent 项目里见到 JSON-RPC 2.0,它翻来覆去其实就那几种消息。学它不需要看几百页规范,把下面这套"句型"吃透就够用了。

2.1 Request / Response / Notification 三件套

Request(请求)是所有调用的起点。它长这样:

{ "jsonrpc": "2.0", "method": "tool.get_weather", "params": { "city": "Beijing", "unit": "celsius" }, "id": 1 }

四个字段各司其职。"jsonrpc" 固定是 "2.0",用来告诉接收方"按哪个版本的规范理解我"。"method" 是被调用方法的名称,在 Agent 生态里,它对应的通常就是某个工具名、某个技能名、或者某个插件的公开接口。"params" 是参数,可以是数组(按位置传参)也可以是对象(按名称传参),我个人强烈建议在 Agent 场景一律用对象——因为 LLM 生成的参数天然是键值对,按位置传参很容易被模型搞错顺序。"id" 是这个请求的唯一标识,响应靠它来"对上号"。

Response(响应)是对请求的回应。成功时返回 result:

{ "jsonrpc": "2.0", "result": { "temperature": 23, "condition": "sunny" }, "id": 1 }

失败时返回 error:

{ "jsonrpc": "2.0", "error": { "code": -32602, "message": "Invalid params: missing required field 'city'", "data": { "field": "city" } }, "id": 1 }

注意 error 对象的三个子字段:code 是错误码,message 是人类可读的说明,data 是可选的附加信息。在 Agent Harness 里,data 这个字段特别有用——你可以把 LLM 需要拿到的结构化错误信息塞进去,让模型下一次调用时能自我纠正。

Notification(通知)是特殊形态的请求:它不带 id。

{ "jsonrpc": "2.0", "method": "agent.log", "params": { "level": "info", "message": "tool call finished" } }

协议规范规定:通知不需要也不允许有响应。接收方处理完通知,绝不能回消息。这个设计的价值在于日志、进度上报、取消信号这类"说了就行、不用等回复"的场景。但在实际工程项目里,通知也是最容易被误用的地方——我后面专门有坑要讲。

2.2 错误对象与保留 code 区间

JSON-RPC 2.0 规范保留了下面几个标准错误码,实现方必须遵守:

code含义什么时候出现
-32700Parse errorJSON 都解析不了,通常发生在传输层拿到一串乱码
-32600Invalid Request消息结构不合法,比如缺 method、jsonrpc 版本不对
-32601Method not found方法名在注册表里查不到
-32602Invalid params参数类型或数量不对
-32603Internal error方法内部抛了没兜住的异常
-32000 到 -32099Server error给服务端实现自定义错误用的保留区间

从 -32768 到 -32000 这个范围是协议预留的,除了标准错误码,你可以在合规的前提下自定义。在 Agent Harness 里,一个很常见的做法是用 -32000 区间定义自己的语义,比如 -32001 表示"工具执行超时",-32002 表示"模型上下文过长"。

2.3 Batch 机制:一次投递一叠请求

Batch 指的是把多个请求打包成一个 JSON 数组一次发送:

[ {"jsonrpc": "2.0", "method": "tool.list", "id": 1}, {"jsonrpc": "2.0", "method": "tool.add", "params": [1, 2], "id": 2}, {"jsonrpc": "2.0", "method": "agent.log", "params": {"msg": "batch"}} ]

响应则是一个数组,里面只包含那些带 id 的请求的响应。也就是说,上例中第三个通知在响应数组里不会出现。这个"数量不一定对等"的细节,坑过无数人。

Batch 在 Agent 场景里最常见的用法是并行工具调用:Agent 一次决定要同时查询三个城市的天气,harness 可以把三个工具调用打包成一批发出去,再集中收结果。这样能显著降低进程间通信的往返次数。

2.4 跟 REST、gRPC、原生 WebSocket 的一次正面比较

既然要做选型,就得知道为什么 Agent Harness 不选 REST 也不选 gRPC。我给几个关键维度的对比:

维度JSON-RPC 2.0RESTgRPC原生 WebSocket
消息模型命令式 RPC资源式 CRUD强类型 RPC任意消息
响应关联内建 id 字段天然一一对应调用上下文无内建机制
流式支持规范不支持靠 SSE 补丁原生支持天然支持
耦合程度极低,无代码生成高,需生成桩代码
可读性好,纯 JSON差,二进制视消息而定
服务发现不管不管需要配套生态不管

REST 的核心思想是"对资源进行增删改查",但 Agent 世界里你面对的是"请你执行一个动作"——查天气、跑代码、搜知识库,这些动词化语义硬套 REST 要么变成一堆自定义 action 端点,要么变成 POST 一个万能 body,最后只会比现在更乱。gRPC 的问题是太重:你得先定义 .proto 文件、生成客户端和服务端代码、处理 HTTP/2 连接管理,这对一个可能只有几十个方法的 Agent 工具层来说性价比太低了。而且 gRPC 的响应是二进制的,你没法拿一条消息喂给 LLM 做人肉调试。

JSON-RPC 2.0 正好在中间:足够结构化,又足够轻。纯文本意味着任何语言、任何环境都能解析;无状态意味着会话上下文完全由 harness 自己管理,协议层不需要记忆任何东西;id 机制让并发调用和批次调用天然可控。

3. 为什么 Agent Harness 偏偏选中它:从协议特性反推设计动机

技术选型从来不是看功能列表有多长,而是看几个核心痛点是否被精确命中。JSON-RPC 2.0 能被 Agent 生态反复 pick,靠的其实就是四个"恰好"。

3.1 method 名跟工具、技能、插件天然一一对应

Agent Harness 要解决的核心问题之一,是把一堆零散的工具、技能、插件整理成统一的调用入口。JSON-RPC 2.0 的 method 字段就是一个字符串——它可以是"tool.get_weather""skill.code_review""plugin.git_operations.push"。这种带命名空间的字符串在注册、过滤、鉴权上都非常干净。

我来给你看一下实际工程里的映射关系。一个 Agent 技能(skill)通常是一段预定义的提示词加上一个可执行函数。当 harness 启动时,它会扫描所有已注册的技能,把它们登记到方法注册表里:

methods = {} for skill in skills: methods[f"skill.{skill.name}"] = skill.execute

LLM 在推理时输出的工具调用,经过解析之后变成这样的格式:

Call: {"method": "skill.code_review", "params": {"repo_path": "/tmp/demo", "language": "python"}}

harness 拿到这个结构,把 method 拆解成命名空间和函数名,去注册表里找到对应处理函数,然后执行。你可以把 JSON-RPC 2.0 的 method 字段理解成"函数指针的名字"——整个工具注册表就是一个 string-to-function 的映射,没有任何多余概念。

3.2 无状态加显式关联,让上下文由外部管理

JSON-RPC 2.0 本身不维护任何会话状态。每个请求都是独立的,靠 id 字段完成响应关联,发起方自己管理"这个请求还在不在等待"。

这个设计在 Agent 场景里反而是优点。因为 Agent Harness 需要极其精细地控制上下文生命周期:一个 Agent 会话可能包含系统提示词、工具调用历史、token 统计、用户偏好等多层状态,如果协议层也掺和进来搞 session、搞 cookie,状态管理的复杂度会爆炸。让协议保持纯粹,把上下文全权交给 harness,这是一个非常干净的分层。

这么说吧:JSON-RPC 2.0 负责"把消息准确送到",harness 负责"记住一切"。协议层笨一点,上层才好控制。

3.3 双向调用:角色模糊才是 Agent 通信的常态

传统 C/S 架构里,角色是清晰的:客户端请求,服务端响应。但在 Agent 生态里,调用关系是双向的、动态的。

harness 可以调用 agent 的决策接口(比如"请根据以下工具结果做出下一步计划"),agent 需要调用工具获取环境信息,工具执行过程中可能通过 notification 向 harness 上报进度,甚至工具可以反过来调用一个"回调型"方法请求 harness 提供更多上下文。JSON-RPC 2.0 对"谁来发起请求"没有预设,它只定义了消息格式和关联规则,这让任何一端都能成为调用方。

MCP(Model Context Protocol)就是最典型的例子。它的整个会话流程——initialize、notifications/initialized、tools/list、tools/call——全部是 JSON-RPC 2.0 风格的消息。客户端可以调服务端的工具,服务端也可以通过 notification 主动推送资源变更。这种自由度,REST 那种"客户端永远主动"的模式根本给不了。

3.4 短板也很明显:流式、二进制、服务发现它全不管

说完了优点,必须泼盆冷水。JSON-RPC 2.0 规范本身就那么几页纸,它把所有"高级功能"都留给了传输层或者上层实现。具体来说有三大短板。

第一是流式响应。规范里没有流式这个概念,一个请求只能对应一个完整响应。但 Agent 场景里,LLM 生成令牌本身就是流式的,工具执行过程的中间输出也是流式的。怎么办?社区的实际做法是:在 JSON-RPC 2.0 之上做扩展,比如 MCP 用 notification 来传递流式片段,或者干脆把 transport 换成长连接,用多条递增 id 的消息模拟流。说白了,这是一层补丁。

第二是服务发现。JSON-RPC 2.0 不回答"对方有哪些方法可以调用"这个问题。所以 MCP 里专门设计了tools/list这个类方法,harness 要主动查询能力列表。这其实是个巧妙的补充——把发现机制本身也做成了 RPC 调用。

第三是安全性。因为协议没有内置鉴权,暴露一个裸 JSON-RPC 端点就等于把你的所有方法公之于众。我见过不止一个团队把 agent 的 JSON-RPC 端口直接绑到公网,method 名随便猜都能被调用。这种错误很致命:你想想,一个shell.run方法如果被匿名调用,会发生什么。后面我会专门讲防护的做法。

4. 手写一个最小 JSON-RPC 2.0 端点,并塞进 Agent 调用链

光说不练假把式。我带你从零写一个能跑的最小实现,然后把它接进一层极简的 harness 骨架里。这个实现我用 Python 标准库就能写完,不用装任何第三方包,你可以直接抄。

4.1 先定传输层:我选标准输入/标准输出

JSON-RPC 2.0 协议本身不规定传输方式,HTTP、WebSocket、stdio、Unix socket 都行。在本地 Agent Harness 场景里,最常用的做法是子进程 + stdio——harness 启动一个工具服务器子进程,往它的 stdin 写单行 JSON 消息,从它的 stdout 读单行 JSON 响应。

为什么选 stdio?因为它是进程间通信里最简单、最不容易受环境变量和端口冲突影响的方式。你不需要开端口、不需要处理 CORS、不需要担心防火墙,子进程被 harness 拉起,当 harness 退出时子进程也跟着结束,生命周期管理天然对齐。MCP 的本地模式走的就是这条路。

4.2 实现服务端核心:注册表与分发逻辑

下面这个文件保存为rpc_server.py,它构建了一个极简但完整的 JSON-RPC 2.0 服务端。

#!/usr/bin/env python3 """极简 JSON-RPC 2.0 服务端:基于 stdin/stdout 传输。""" import json import sys class RpcError(Exception): def __init__(self, code, message, data=None): super().__init__(message) self.code = code self.message = message self.data = data class RpcServer: def __init__(self): self._methods = {} def method(self, name): """注册方法:把函数名映射到调用名字符串。""" def decorator(fn): self._methods[name] = fn return fn return decorator def _dispatch(self, request): if not isinstance(request, dict): raise RpcError(-32600, "Invalid Request") if request.get("jsonrpc") != "2.0" or "method" not in request: raise RpcError(-32600, "Invalid Request") method = request["method"] params = request.get("params", []) handler = self._methods.get(method) if handler is None: raise RpcError(-32601, f"Method not found: {method}") if isinstance(params, dict): return handler(**params) if isinstance(params, list): return handler(*params) return handler() def _handle_one(self, request): is_notification = "id" not in request try: result = self._dispatch(request) except RpcError as e: error = {"code": e.code, "message": e.message, "data": e.data} except Exception as e: # 业务中没兜住的异常,统一转成 -32603 error = {"code": -32603, "message": "Internal error", "data": str(e)} else: error = None if is_notification: return None response = {"jsonrpc": "2.0", "id": request.get("id")} if error is not None: response["error"] = error else: response["result"] = result return response def handle_line(self, line): try: payload = json.loads(line) except json.JSONDecodeError: # 解析失败时 id 无从得知,规范要求 id 为 null return {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}} if isinstance(payload, list): # Batch 响应:只保留带 id 的请求的响应 responses = [] for item in payload: if isinstance(item, dict) and "id" in item: r = self._handle_one(item) if r is not None: responses.append(r) return responses or None if isinstance(payload, dict): return self._handle_one(payload) return {"jsonrpc": "2.0", "id": None, "error": {"code": -32600, "message": "Invalid Request"}} def main(): server = RpcServer() @server.method("tool.add") def tool_add(a, b): return a + b @server.method("tool.list") def tool_list(): return ["tool.add", "tool.get_status"] @server.method("agent.log") def agent_log(level, message): # 这是通知型方法的典型用法,写入日志后不产生实际结果 print(f"[{level}] {message}", file=sys.stderr) return None for line in sys.stdin: line = line.strip() if not line: continue result = server.handle_line(line) if result is not None: print(json.dumps(result, ensure_ascii=False), flush=True) if __name__ == "__main__": main()

这段代码的骨架在真实项目里可以直接套用。几个设计要点:

  • _handle_one里先判断是不是 notification(没有 id),是的话不管成功失败都不返回响应。这个判断必须放在错误处理之前,否则一个出错的 notification 会被错误地返回一个 error 响应,违反协议。
  • 内部异常全部兜底转成 -32603,避免堆栈信息直接泄漏到协议层。你可以把详细堆栈打进 stderr 日志,但返回给调用方的 message 要干净。
  • Batch 处理时跳过 notification 的响应,保证响应数组里每一项都能跟请求 id 对上。

4.3 用一条命令验证服务端

把上面的文件存好后,直接测试:

printf '{"jsonrpc":"2.0","method":"tool.add","params":{"a":3,"b":4},"id":1}\n' | python3 rpc_server.py

输出:

{"jsonrpc": "2.0", "id": 1, "result": 7}

再测一个错误场景:

printf '{"jsonrpc":"2.0","method":"tool.nope","params":{},"id":2}\n' | python3 rpc_server.py

输出:

{"jsonrpc": "2.0", "id": 2, "error": {"code": -32601, "message": "Method not found: tool.nope"}}

4.4 接一层极简 harness:让 LLM 的意图变成协议调用

服务端只是半个故事,另一半是 harness 这侧的调用逻辑。下面是一个不能再简的 harness 骨架,它做的事情是:接收 LLM 输出的工具调用指令,转成 JSON-RPC 请求,发送给服务端子进程,再把响应结果返回给 model loop:

#!/usr/bin/env python3 """极简 Agent Harness 骨架:负责把 agent 的意图翻译成 JSON-RPC 调用。""" import json import subprocess import sys class ToolClient: """连接 RPC 服务端的客户端,负责请求生命周期管理。""" def __init__(self, proc): self.proc = proc self._next_id = 0 self._pending = {} def start(): proc = subprocess.Popen( [sys.executable, "rpc_server.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) return ToolClient(proc) def call(self, method, params=None, timeout=10.0): self._next_id += 1 req_id = self._next_id request = {"jsonrpc": "2.0", "method": method, "params": params or {}, "id": req_id} self.proc.stdin.write(json.dumps(request) + "\n") self.proc.stdin.flush() # 同步读一行响应;真实 harness 会并发读取并建立 id -> future 映射 line = self.proc.stdout.readline() if not line: raise RuntimeError("RPC server closed unexpectedly") response = json.loads(line) if response.get("id") != req_id: raise RuntimeError(f"id mismatch: expected {req_id}, got {response.get('id')}") if "error" in response: raise RuntimeError(response["error"]["message"]) return response.get("result") def notify(self, method, params=None): """发送一个不需要响应的通知。""" request = {"jsonrpc": "2.0", "method": method, "params": params or {}} self.proc.stdin.write(json.dumps(request) + "\n") self.proc.stdin.flush()

在真实的模型循环里,调用链是这样的:LLM 输出一个带 tool_calls 的回复 → harness 解析出methodparams→ 调用client.call(...)→ 拿到 result → 把 result 拼回对话上下文 → 再次把上下文交给 LLM。就这么个循环,已经构成了 Agent 最基本的行为闭环。

我特别想强调一下 id 管理,这是很多初学者最容易忽略的地方。上面的实现用了一个单调递增的计数器,看起来简单,但一旦你要支持并发调用、超时重试、或者把调用分流到多个服务端进程,光靠一个递增数字就不够了。现实的方案是生成 UUID 作为 id,同时本地维护一张 id → future 的映射表,响应一进来就查到对应 future 并 set 结果。我在第五部分会细讲这个坑。

5. 集成过程中我实际踩过的四个坑

无论你是在写工具服务器、MCP 服务器还是自己的 Agent Harness,下面这四个坑我都在真实项目里踩过,每一个都花过我至少一个下午来排查。

5.1 坑一:id 重复,所有响应全部串线

第一次写多请求并行时,我图省事,把请求 id 直接用了 LLM 返回的消息序号。结果模型从 0 开始计数,harness 上一次会话也从 0 开始,两边一交叉,id: 3的响应被匹配到了完全不同的请求上,工具结果张冠李戴,agent 对着北京的天气做出了"东京适合带伞"的决策。

排查了很久才发现,问题不在模型,也不在工具逻辑,而是 id 根本不是一个全局唯一标识。

这个坑的根本原因,是 id 的语义由调用方自己保证。JSON-RPC 2.0 规范只要求"发起方用自己的方式保证 id 唯一",但这个唯一性在多客户端、多轮次、多会话场景下非常容易被打破。我的经验是:任何跨进程通信中的请求 id,一律用 UUID 或毫秒时间戳 + 随机数的组合,不要用自增数字。自增数字看起来清晰,但它只在一个进程内、一个时刻线上有意义。

5.2 坑二:把 Notification 当请求发,等回应等到天荒地老

这个坑特别隐蔽,因为它发生在你调用别人写的工具服务器时。某个方法文档里写着"支持 log 通知",你看着像是个普通方法,于是发了这么一条:

{"jsonrpc": "2.0", "method": "agent.log", "params": {...}, "id": 42}

注意,你在消息里加了id: 42,但服务端把它当成普通请求处理了——等等,问题恰恰相反。真正隐蔽的坑是:文档说某个方法"通常作为 notification 发送",你照做了,但没意识到这个方法其实有返回值

举例来说,很多工具服务器有一个session.heartbeat方法,作用是告诉服务端"我还活着"。它在协议定义里是 notification(无 id),服务端收到后不需要回复。但你如果以为所有方法都应该有响应,发完就阻塞等待,那就会一直卡住,直到超时。

处理这个问题的唯一可靠办法是:在 harness 的客户端层强制区分 call 和 notify 两种 API。只有真正需要响应的方法才能调 call,明确标记为通知的方法只能走 notify。收到一个没有 id 的响应消息时,直接丢弃并打 warning,因为那本身就违反了协议。

5.3 坑三:-32700 Parse error 的 id 是 null,你根本不知道谁挂了

JSON-RPC 2.0 规范规定,当请求消息连 JSON 都解析不了的时候,服务端应当返回:

{"jsonrpc": "2.0", "id": null, "error": {"code": -32700, "message": "Parse error"}}

id 强制是 null,因为服务端压根不知道原始消息里写了什么。这在单连接、串行通信时问题不大——反正只有一条管道,出错了我直接把这条响应当成"刚才那条请求的失败结果"就行。

但一旦你开始用并发、用 Batch、用多个连接,问题就来了:一个 -32700 响应进来,你不知道它对应哪条请求,也不知道是哪个客户端发来的。更麻烦的是,如果客户端因为网络抖动把一条完整消息拆成了两半,服务端会在第一条看到\n之后就尝试解析,必然报 -32700。

我的修为在于:-32700 不应该是客户端主处理逻辑的一部分,而应该是日志系统的一部分。收到 -32700 的客户端要做两件事:一是把堆栈现场完整打成日志,二是触发主动超时兜底——让所有 pending 的请求在超时后统一报错重试。不要妄图从 -32700 里推断出是哪条请求坏了,那不现实。

5.4 坑四:Batch 请求里"沉默的响应"和部分成功陷阱

Batch 的问题我在 2.3 提过一句:通知在响应数组里不占位。如果你写的客户端拿到响应数组后,按请求数组的下标去对齐,恭喜你,下一次通知多了的时候数据全错位。

正确的对齐方式只有一个:遍历响应数组,按每一项里的 id 字段去找对应的请求,绝不能用位置去匹配。记住,位置匹配在 JSON-RPC 2.0 的 Batch 场景里永远都是错的,因为协议从一开始就没承诺响应顺序与原请求顺序一致。

另外 Batch 还有另一个隐蔽点:部分成功。一批请求发过去,可能其中三个成功、两个失败、一个是通知。响应数组里就是三个 result 一个 error,顺序还是乱的。你的客户端必须能够容忍"这一批没全成功"的情况,逐个按 id 处理结果,该重试的重试,该回滚的回滚。

6. 这个"前置"到底前置了什么:向 Agent Harness 正篇过渡

读到这里,你可能已经写出了一个能跑的 JSON-RPC 2.0 服务端和客户端,也对协议的各种边角细节有了感觉。但容我提醒一句:这些还只是地基,Agent Harness 真正复杂的东西,全在协议层之上。

6.1 通信协议是协议,运行框架是框架,两码事

JSON-RPC 2.0 解决的只是"两个进程之间怎么交头接耳"。它不告诉你 agent 的生命周期怎么管理,不告诉你技能文件怎么组织和加载,不告诉你工具注册失败时整个 runtime 该不该停摆,不告诉你上下文窗口快满的时候该做摘要还是裁剪。

而这些,恰恰是 harness 的核心职责。还记得开头那条 codex 报错吗?agent harness runtime "codex" is unavailable because its plugin registration failed。注册失败、runtime 不可用、插件隔离——这些概念全都在 JSON-RPC 2.0 的视野之外。harness 要做的是把这些工程问题全部收敛起来,让上层 agent 只管做决策。

我的建议是:你可以把整个 Agent Harness 理解成三层。最底层是传输与协议层(JSON-RPC 2.0 就在这一层);中间是运行时与注册层(插件扫描、方法注册、故障隔离、生命周期管理);最顶层是决策与编排层(prompt 组装、工具调用循环、上下文管理、与用户的交互)。很多 Agent 项目在这三层之间边界模糊,代码变成一锅粥——这是因为协议层和框架层被混在了一起。

6.2 从协议往上爬,你还会撞上这些设计问题

如果你准备深入 Agent Harness,下面几个问题是绕不开的,它们都在正篇里展开:

  • 插件注册的失败策略:某个插件加载失败,是整体失败还是降级跳过?codex 那条报错选择的是整体失败,但并不是所有 harness 都该这么做。
  • 工具调用的超时与取消:LLM 发起一个工具调用,如果工具卡死了,harness 是等待还是强制中断?怎么向服务端传递"取消"信号?很多实现靠的是一个tool.cancel方法,参数里带着原请求的 id。
  • 上下文管理:工具返回的结果要不要全部塞回模型上下文?怎么判断哪些结果值得保留、哪些只值得写日志?这直接决定 token 消耗和推理质量。
  • 安全边界:哪些方法允许 LLM 调用,哪些必须经过用户确认?JSON-RPC 2.0 的 method 只是个字符串,得靠 harness 的鉴权层把住口子。

6.3 三条继续深入的路,看你的目标选

如果你想让这系列前置篇真正落地,我建议你根据自己需求选一条路走:

路线一:实现一个自己的工具服务器。用第 4 节的骨架,往里面注册你自己的业务工具,比如搜索、计算器、读取本地笔记。目标不是写得多花哨,而是把 JSON-RPC 2.0 的请求、响应、通知、Batch 全部跑通,体会协议给你带来的约束和自由。

路线二:深入观察一个成熟 harness 的源码。重点看它的插件注册机制、方法分发器、以及超时取消的实现。找到那行runtime "xxx" is unavailable because its plugin registration failed对应的代码路径,看看一个插件注册失败是怎么冒泡到 runtime 层的,这对理解故障隔离非常有用。

路线三:研究 MCP 规范。MCP 是 JSON-RPC 2.0 在 Agent 生态里最成功的应用,它把协议和工具发现机制结合得很好。你去看它的 initialize 握手、tools/list、tools/call 这套流程,能直观感受到"为什么当初要选 JSON-RPC 2.0"。

我个人带了这么多项目,最大的体会是:一个协议的流行程度从来不取决于它的新,而取决于它是否恰好卡在某个时代痛点的时间窗口上。JSON-RPC 2.0 是一个 2010 年的老家伙,但它"极简到不适合做大事"的缺陷,在 Agent Harness 这个极度需要轻量、灵活、可插拔的战场上反而成了最大优势。这系列的正篇,咱们就从这个矛盾与和解开始讲。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:01:02

客服部AI试点盈利,市场部却亏:补完亚马逊云科技机器学习我才搞懂场景筛选

客服部AI试点盈利,市场部却亏:补完亚马逊云科技机器学习我才搞懂场景筛选 周一例会,CTO指着ROI报表问我:“市场部的生成式AI应用投了30万,半年亏损将近10万;研发部的代码助手省了点时间,但折算成人力成本还是负的。只有客服部的智能工单分派赚了,而且他们只用了不到5000块的外…

作者头像 李华
网站建设 2026/9/8 12:57:55

实测 Hermes Agent 一键包,告别繁杂配置,快速体验办公自动化能力

🔍前言 不少想要体验 Hermes Agent 办公能力的使用者,往往会被复杂的环境配置拦住使用脚步。手动下载匹配依赖、反复调整系统目录、处理命令行持续报错、修复权限异常、补全丢失核心文件等一系列操作,对普通使用者而言门槛较高,很…

作者头像 李华
网站建设 2026/9/8 12:57:16

VS2013下UPX 3.09源码编译与依赖配置实践

简介:提供一套已编译通过的 UPX(Ultimate Packer for eXecutables)集成工程,面向需要在 Visual Studio 2013 下使用 UPX 源码进行二次开发或理解其原理的 C/C 开发者。工程以 rar 包发布,共 388 个文件,主要…

作者头像 李华
网站建设 2026/9/8 12:56:41

信息提取与规则翻译:构建可靠条件处理模块的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 12:55:46

马拉车算法(Manacher)精讲:线性时间解决最长回文子串

Manacher/马拉车算法 最长回文子串问题,可以说是字符串算法里的一道“家常菜”。我入行这几年,面试遇到过它,竞赛里见过它,连实际做文本处理的工单系统都曾经撞上过它。很多朋友学这个算法时容易卡住,总觉得代码不长、…

作者头像 李华
网站建设 2026/9/8 12:55:38

2026年8月GitHub热门开源项目盘点:AI工具与个人数据归档趋势

GitHub 的热门榜单每隔一段时间就要洗一次牌,但像 2026 年 8 月这样,同时挤进来好几个 AI 相关项目、工具类项目和"个人数据归档"向开源作品的情况,其实并不多见。作为一个常年泡在 GitHub 上刷 Trending 的人,我每个月…

作者头像 李华