漫话 Agent Harness · 前置:JSON-RPC 2.0——那个被 AI Agent 重新捧红的老协议

么都选了它

一、为什么是 JSON-RPC

一个 2005 年的老协议

JSON-RPC 的第一版诞生在 2005 年,比 REST 这个词还早几年。它的设计目标极其简单:让两个程序通过 JSON 互相调用对方的方法

一个 JSON-RPC 消息就是一个 JSON 对象。只有三种类型:

Request(请求)------我要调你一个方法,等你回复:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "add",
  "params": {"a": 1, "b": 2}
}

Response(响应)------回复一个 id 对应的请求,要么 result 要么 error:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": 3
}
json 复制代码
{
  "jsonrpc": "2.0",
  "id": "req-001",
  "error": {"code": -32602, "message": "Invalid params"}
}

Notification(通知)------单向推送,不带 id,不期望回复:

json 复制代码
{
  "jsonrpc": "2.0",
  "method": "log",
  "params": {"level": "info", "msg": "hello"}
}

整个协议就这么多。 没有 schema、没有版本协商、没有连接握手、没有 content-type。一个信封格式 + 三种消息类型 + 一组约定的错误码,完事。

跟 REST、gRPC 比一比

JSON-RPC 2.0 跟另外两个主流协议(REST、gRPC)的定位不一样:

协议 抽象层级 传输 双向支持 典型场景
REST 资源(HTTP verb + URL) HTTP 单向请求-响应 Web API
gRPC 方法(Protobuf schema) HTTP/2 支持流式 微服务
JSON-RPC 2.0 方法(JSON) 任意 原生双向 长连接对等通信

核心区别在"抽象"和"双向"两件事上。

REST 的抽象是"资源"------一切都是 GET/POST/PUT/DELETE 某个 URL。这套抽象对 Web 非常自然,但对"我调你一个方法、你调我一个方法"的场景很别扭。你见过哪个 RPC 调用要写成 POST /sessions/abc/prompts 还要关心 HTTP status code 的?

gRPC 的抽象是"方法",更接近 RPC 本质。但它强依赖 Protobuf schema 和 HTTP/2,重------你要先写 .proto 文件、生成代码、装依赖,才能跑起来。

JSON-RPC 2.0 的抽象也是"方法"------直接传 method 名和 params,没有 schema 约束。传输完全不管 ------stdio、TCP、WebSocket、HTTP、进程管道都行。双向原生支持------Notification 就是单向推送,Request/Response 是对称的请求-响应。

这就是它在 AI Agent 圈翻红的根本原因:Agent 跟宿主之间的通信,恰好是"长连接 + 双向 + 方法抽象 + 简单"------这四件事 JSON-RPC 一次都满足了。

跟 WebSocket 的区别

很多人会把 JSON-RPC 跟 WebSocket 搞混。它们不是一回事

  • WebSocket 是传输层协议------它管的是"如何在 TCP 之上做双向字节流"
  • JSON-RPC 是应用层协议------它管的是"消息长什么样"

两者经常一起用:WebSocket 提供双向管道,JSON-RPC 提供消息格式。但它们也可以分开:JSON-RPC 能跑在 stdio、TCP、WebSocket 上;WebSocket 也能承载别的应用协议。

类比一下:TCP 是公路,HTTP 是跑在上面的卡车。JSON-RPC 和 WebSocket 的关系也差不多------WebSocket 是路,JSON-RPC 是卡车,可以换路也可以换车。


二、为什么 Agent 圈选了它

把上面那张协议对比表再看一遍,你会发现 REST 和 gRPC 在 AI Agent 场景下有几个硬伤:

硬伤一:REST 没法"反向调"

在 ACP 的场景下,Agent 想反向调 Client 的文件系统、终端、鉴权。REST 做不到------HTTP 天然是"客户端发起、服务端响应",服务端要推消息得靠 SSE 或长轮询,而且只能单向。Agent 想用 REST 反向调 Client,得让 Client 再开一个反向的 REST 服务,复杂度爆炸。

JSON-RPC 双向对称:同一根管子里,谁都可以发 Request,谁都可以发 Notification。Agent 反向调 Client 的工具,协议层天然支持

硬伤二:gRPC 太重

gRPC 要 Protobuf schema、要编译生成代码、要 HTTP/2、要一堆依赖。Agent 这个领域的特点是方法经常变、协议在演进------你让用户每次更新 Agent 都要重新生成 Protobuf 代码?太反人类。

JSON-RPC 没有 schema 约束,传 method 名和 params 就完事。加新方法、改参数都不影响协议本身。Agent 协议演进快,就需要这种轻量级。

硬伤三:Agent 通信是"长连接 + 流式"

REST 的请求-响应模型是短连接思维。Agent 跟宿主的通信是长连接------IDE 启动时 spawn Agent,整个 IDE 生命周期里一直用这根管子交换消息。而且 Agent 的输出是流式的(LLM 一个字一个字吐),需要一种"单向推送 + 不期望回复"的消息类型。

JSON-RPC 的 Notification 就是为这种场景设计的。MCP 和 ACP 都大量使用 Notification 来流式推送 Agent 的思考过程、工具调用、输出片段

一张图看清楚选择


三、AI Agent 圈里的 JSON-RPC 应用

讲完为什么,看具体。下面这几个 Agent 圈的主流协议都选了 JSON-RPC 2.0:

3.1 LSP(Language Server Protocol)

LSP 是 JSON-RPC 最成功的应用,没有之一。

2016 年微软为 VS Code 推出 LSP,目标是让"编辑器"和"语言服务器"解耦------任何一个编辑器只要支持 LSP,就能接入任何语言服务器(Java、Go、Rust、TS......)。

LSP 的通信就是用 JSON-RPC 2.0 over stdio(或者 TCP)。典型的 LSP 消息:

json 复制代码
// 编辑器 → 语言服务器:打开文件
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "textDocument/didOpen",
  "params": {
    "textDocument": {
      "uri": "file:///project/main.ts",
      "languageId": "typescript",
      "version": 1,
      "text": "..."
    }
  }
}

// 语言服务器 → 编辑器:推送诊断(Notification,不期望回复)
{
  "jsonrpc": "2.0",
  "method": "textDocument/publishDiagnostics",
  "params": {
    "uri": "file:///project/main.ts",
    "diagnostics": [{"range": {...}, "message": "Unused variable"}]
  }
}

LSP 的成功让后来的协议设计者都把它当模板------ACP 基本是 LSP 在 AI Agent 领域的翻版。

3.2 MCP(Anthropic 的 Model Context Protocol)

MCP 是 Agent-to-Tool 的协议------Agent 通过 MCP 接入外部工具(数据库、搜索、日历、文件系统......)。

MCP 的通信也是 JSON-RPC 2.0:

json 复制代码
// Agent → MCP Server:列出可用工具
{
  "jsonrpc": "2.0",
  "id": "call-1",
  "method": "tools/list",
  "params": {}
}

// MCP Server → Agent:调用工具结果
{
  "jsonrpc": "2.0",
  "id": "call-1",
  "result": {
    "tools": [
      {"name": "sqlite_query", "description": "...", "inputSchema": {...}}
    ]
  }
}

// Agent → MCP Server:调用工具
{
  "jsonrpc": "2.0",
  "id": "call-2",
  "method": "tools/call",
  "params": {
    "name": "sqlite_query",
    "arguments": {"sql": "SELECT * FROM users"}
  }
}

MCP 默认跑 stdio,也能跑 HTTP + SSE(HTTP 用于请求-响应,SSE 用于服务端推送)。

3.3 ACP(Zed 的 Agent Client Protocol)

ACP 是 IDE-to-Agent 的协议------IDE 通过 ACP 调用 AI Agent。ACP 跟 LSP 的设计哲学一模一样:

json 复制代码
// IDE → Agent:新建会话
{
  "jsonrpc": "2.0",
  "id": "1",
  "method": "session/new",
  "params": {"cwd": "/project"}
}

// Agent → IDE:流式推送思考过程(Notification)
{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": {
    "sessionId": "abc",
    "update": {"sessionUpdate": "agent_thought_chunk", "thought": "Analyzing..."}
  }
}

// Agent → IDE:反向调文件工具(Request,期望回复)
{
  "jsonrpc": "2.0",
  "id": "agent-call-1",
  "method": "fs/read_text_file",
  "params": {"path": "/project/main.py"}
}

关键的反向调用场景 :Agent 要读 IDE 上的文件,反向发一个 Request 给 IDE,IDE 处理后回复 Response。同一根 stdio 管子里既有 IDE 发出去的 Request,也有 Agent 反向发过来的 Request。这就是双向对称。

3.4 Codex app-server(OpenAI 的 Agent Harness)

Codex 的 app-server 是 ACP 类协议的另一个实现(方法名不同,但模式完全一致)。它跑在 stdio 上,用 JSON-RPC 2.0 信封,双向通信:

json 复制代码
// IDE → Codex app-server:开启线程
{
  "jsonrpc": "2.0",
  "id": "init-1",
  "method": "thread/start",
  "params": {"cwd": "/project"}
}

// Codex app-server → IDE:流式推送输出(Notification)
{
  "jsonrpc": "2.0",
  "method": "item/agentMessage/delta",
  "params": {"threadId": "abc", "delta": "Let me check the file..."}
}

// Codex app-server → IDE:反向调工具(Request,需要 IDE 回复)
{
  "jsonrpc": "2.0",
  "id": "codex-tool-1",
  "method": "fs/readFile",
  "params": {"path": "/project/main.py"}
}

3.5 其他领域的经典应用

不止 AI 圈。JSON-RPC 在其他领域也是事实标准:

  • 以太坊节点 API :所有以太坊客户端都暴露 JSON-RPC 端口(默认 8545),MetaMask、Web3.js、Ethers.js 都用 JSON-RPC 跟节点通信。eth_sendTransactioneth_getBalance 这些方法全是 JSON-RPC
  • 比特币核心 RPC :Bitcoin Core 用 JSON-RPC over HTTP 暴露 getblocksendtoaddress 等方法
  • Minecraft RCON:Minecraft 服务器的远程管理用 JSON-RPC 风格的协议
  • Kodi、XBMC 媒体中心:用 JSON-RPC 暴露播放控制 API

这个协议 20 年来反复被重新发现,是因为它解决了一类问题------"长连接 + 双向 + 方法抽象 + 简单"------而且解决得很彻底。


四、写一个最小的 JSON-RPC 实现

概念讲够了,看代码。下面用 Node.js 手写一个最小的 JSON-RPC 端点,跑在 stdio 上。这个实现不到 100 行,跟 dsh、Codex、MCP 用的核心抽象是一样的

4.1 Transport 抽象

先抽象传输层------任何能"发一条消息 + 收一条消息"的东西都能塞进去:

typescript 复制代码
// transport.ts
export type JsonRpcMessage =
  | { jsonrpc: '2.0'; id: string | number; method: string; params?: unknown }           // Request
  | { jsonrpc: '2.0'; id: string | number; result?: unknown; error?: { code: number; message: string } }  // Response
  | { jsonrpc: '2.0'; method: string; params?: unknown }                                // Notification

export interface Transport {
  send(msg: JsonRpcMessage): void
  onMessage(cb: (msg: JsonRpcMessage) => void): void
  close(): void
}

4.2 stdio 传输实现

typescript 复制代码
// stdio-transport.ts
import { createInterface } from 'node:readline'
import type { Transport, JsonRpcMessage } from './transport.js'

export function stdioTransport(): Transport {
  const rl = createInterface({ input: process.stdin, crlfDelay: Infinity })
  const callbacks: ((msg: JsonRpcMessage) => void)[] = []

  rl.on('line', (line) => {
    if (!line.trim()) return
    try {
      callbacks.forEach(cb => cb(JSON.parse(line)))
    } catch (e) {
      console.error('parse error:', line)
    }
  })

  return {
    send(msg) { process.stdout.write(JSON.stringify(msg) + '\n') },
    onMessage(cb) { callbacks.push(cb) },
    close() { rl.close(); process.exit(0) },
  }
}

4.3 RPC 端点(双向对称)

这是最关键的部分------同一个端点既能"接收调用"也能"发起调用",这就是双向对称:

typescript 复制代码
// endpoint.ts
import type { Transport, JsonRpcMessage } from './transport.js'

export class RpcEndpoint {
  private handlers = new Map<string, (p: any) => any>()
  private pending = new Map<string | number, { resolve: (v: any) => void; reject: (e: Error) => void }>()
  private nextId = 1

  constructor(private transport: Transport) {
    transport.onMessage(m => this.route(m))
  }

  // 注册本端方法(对端可调)
  register(method: string, handler: (params: any) => any) {
    this.handlers.set(method, handler)
  }

  // 主动调对端方法(双向的关键)
  async call(method: string, params?: any): Promise<any> {
    const id = this.nextId++
    return new Promise((resolve, reject) => {
      this.pending.set(id, { resolve, reject })
      this.transport.send({ jsonrpc: '2.0', id, method, params })
    })
  }

  // 单向通知(不期望回复)
  notify(method: string, params?: any) {
    this.transport.send({ jsonrpc: '2.0', method, params })
  }

  private route(msg: JsonRpcMessage) {
    if ('method' in msg && 'id' in msg) {
      // Request:对端调我的方法
      const handler = this.handlers.get(msg.method)
      if (!handler) {
        this.transport.send({ jsonrpc: '2.0', id: msg.id, error: { code: -32601, message: 'method not found' } })
        return
      }
      Promise.resolve(handler(msg.params))
        .then(result => this.transport.send({ jsonrpc: '2.0', id: msg.id, result }))
        .catch(e => this.transport.send({ jsonrpc: '2.0', id: msg.id, error: { code: -32603, message: e.message } }))
    } else if ('id' in msg) {
      // Response:对端回复我之前发的 Request
      const p = this.pending.get(msg.id)
      if (!p) return
      this.pending.delete(msg.id)
      if ('error' in msg && msg.error) p.reject(new Error(msg.error.message))
      else p.resolve(msg.result)
    } else if ('method' in msg) {
      // Notification:单向通知,不回复
      const handler = this.handlers.get(msg.method)
      if (handler) Promise.resolve(handler(msg.params)).catch(() => {})
    }
  }
}

4.4 跑一遍:双向通信

typescript 复制代码
// demo.ts
import { RpcEndpoint } from './endpoint.js'
import { stdioTransport } from './stdio-transport.js'

const endpoint = new RpcEndpoint(stdioTransport())

// 注册本端方法:对端可以调 add
endpoint.register('add', (p: any) => p.a + p.b)

// 主动调对端的 greet 方法
endpoint.call('greet', { name: 'Alice' }).then(console.log)

// 发一个通知(不期望回复)
endpoint.notify('log', { level: 'info', msg: 'started' })

这 100 行代码就是 ACP、MCP、Codex app-server、LSP 的核心抽象。 它们的差异只是在上面"挂什么方法"和"用什么传输"------底下都是这同一个双向 RpcEndpoint。


五、工程上的几个细节

把协议讲清楚了,再聊几个工程上容易踩的坑。

5.1 id 怎么生成

协议没说 id 必须是字符串还是数字。实践中常见三种:

  • 自增整数(简单,但连接复用时有碰撞风险)
  • UUID(安全但长)
  • 计数器 + 前缀(比如 client-1agent-1

关键约束:同一个连接里 id 必须唯一。Response 用 id 找回对应的 Request,id 碰撞会乱。

5.2 Notification 没有 id,不能回复

如果你收到一个没有 id 的消息,它是 Notification------绝对不能回复(即使回一个 error 也不行)。这是协议明确规定的。

很多新手实现会"顺手"给 Notification 也回复一下,结果对端收到一个"孤儿 Response",匹配不上任何 pending Request,状态机就乱了。

5.3 错误码是约定的

协议规定了一组标准错误码:

错误码 含义
-32700 Parse error(JSON 解析失败)
-32600 Invalid Request(信封格式错)
-32601 Method not found
-32602 Invalid params
-32603 Internal error
-32000 到 -32099 服务器自定义错误

ACP 和 MCP 都在这些约定之上扩展了自己的业务错误码 (比如 ACP 的 session not found、MCP 的 tool rejected)。

5.4 长连接 + 背压

JSON-RPC 是异步的------你可以同时发出 100 个 Request,每个 id 不同,Response 乱序回来。端点层要用 Map<id, Promise> 维护 pending。

但如果对端处理不过来,pending 堆积会爆内存 ------这叫"背压"(backpressure)。Codex app-server 就遇到过这个问题,饱和时返回 -32001 Server overloaded; retry later 让客户端退避。

5.5 长连接断了怎么办

JSON-RPC 本身不管连接生命周期------它没有心跳、没有重连。这些都得在传输层处理:

  • stdio:子进程死了就是死了,父进程重新 spawn
  • WebSocket:库通常自带 ping/pong 心跳
  • TCP:自己实现心跳(每隔 N 秒发个空消息),断了自动重连

这也是为什么 JSON-RPC "简单"的另一面------它不管的事你得自己管。


写在最后

如果这一篇只记住一件事,我希望是这句:

JSON-RPC 2.0 = JSON 信封 + 三种消息类型(Request / Response / Notification)+ 传输无关。

简单到能写在 100 行代码里,强大到能撑起 LSP、ACP、MCP、Codex、以太坊这一整片生态。

2005 年发明,2016 年靠 LSP 翻红,2024-2025 年靠 MCP、ACP、Codex 再次翻红。一个协议能反复被"重新发现",说明它解决的是真正基础的问题------"两个程序通过一根双向管子互相调用方法",这件事在分布式系统、IDE、AI Agent 三个时代都是核心需求。

回到我们的场景

现在回头看开篇那个 Agent 场景:

  • 沙箱里的 Agent 想调网关的 tool/callllm/completebilling/query------需要反向 RPC
  • 网关想把 Agent 的流式输出推给前端------需要 Notification 单向推送
  • 一次会话里 Agent 和网关会互相发几十上百个调用------需要长连接

JSON-RPC 2.0 把这三件事一次都满足了。 这就是为什么下一篇文章设计那套架构时,网关 ↔ 沙箱之间选了 JSON-RPC over WebSocket------协议层天然契合,不用发明新东西。

相关参考资料

JSON-RPC 2.0 官方

AI Agent 圈的 JSON-RPC 应用

其他经典应用

Node.js 实现参考

相关推荐
武子康29 分钟前
开放权重之后,为什么 Agent 仍然无法复现:真正缺的是可重放行为证据
人工智能·llm·agent
十一捉一36 分钟前
挑战在 Coding Agent 时彻底推翻之前的方案
agent·ai编程
ovO43 分钟前
DeepSeek Harness 源码解读(四):一次 Turn 为什么会跑多个 Step
开源·agent·deepseek
JaydenAI1 小时前
[DeepSeek Harness插件内核-06]Context全面解析[自由扩展篇]
ai·agent·deepseek·harness·cordis
ShallWeL1 小时前
RAG 命中后的引用格式与拒答规则
agent·知识库·rag·智能体
DeepAgent1 小时前
AI Agent 工程实践(37):需求分析——一个 Agent 项目到底应该怎么拆
agent·需求分析
tachibana21 小时前
AI Agent 的记忆机制
人工智能·ai·大模型·llm·agent
ovO2 小时前
DeepSeek Harness 源码解读(三):七个核心服务怎样拼成一次 Agent 运行
开源·agent·deepseek
小羊433 小时前
从Prompt到Skill:专家经验的标准化封装指南
agent