LLM Agent 底层揭秘:大模型如何通过 JSON-RPC 2.0 协议跨进程调工具?

文章定位 :全栈架构 / AI Agent 通识教学 核心标签AI Agent JSON-RPC 2.0 CompletableFuture MCP协议 Java并发

很多开发者在接触 AI Agent 的工具调用(Tool Calling)时,往往只停留在"大模型返回了一段 JSON 参数"这一层。但一个工业级的 Agent 框架(如基于 MCP - Model Context Protocol 构建的系统),究竟是如何把大模型的意图,精准、安全地传输给本地脚本或远端微服务的?

如果遇到本地脚本死循环,Java 主线程会不会被彻底拖垮?

本文将从 Agent 的 RPC 2.0 协议入手,带你硬核拆解一条完整的 Agent 工具通信链路 ,并深度解析如何利用 CompletableFuture 实现优雅的 Pending(挂起)请求与超时管理。这不仅是后端开发的高频面试题,更是构建稳健 AI 系统的核心基石。

1. 破冰:大模型 ToolCall 与真实工具执行的"鸿沟"

当大模型(LLM)决定调用工具时,它输出的仅仅是一个意图结构,比如:

json 复制代码
{
  "id": "call_1",
  "function": {
    "name": "mcp__chrome-devtools__navigate_page",
    "arguments": "{\"url\":\"https://github.com\"}"
  }
}

这个结构是 Agent 体系的"输入",但底层的工具服务(比如一个用 Node.js 写的浏览器控制脚本,或者一个 Python 写的远端 RAG 服务)根本不认识这个对象。

为了抹平语言和进程间的差异,工业界引入了标准协议------JSON-RPC 2.0。我们需要一个组装层,把 LLM 的意图翻译成标准 RPC 请求:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "navigate_page",
    "arguments": {
      "url": "https://github.com"
    }
  }
}

2. 架构拆解:协议层与传输层的正交设计

在优秀的 Agent 底层通信链路中,"发什么包"和"怎么发包"必须被严格拆分开来。

协议层(JsonRpcClient)

全权负责 JSON-RPC 2.0 的请求/响应组装、ID 分配以及生命周期(Pending)管理。它不关心底层是跑在同一个机器上的脚本,还是远在天边的云服务。

传输层(Transport)

负责真正的 I/O 交互。根据工具的部署形态,我们通常会做两种路由:

传输模式 适用场景 底层实现核心
StdioTransport 本地脚本工具(如 npxuvx 启动的工具) ProcessBuilder 启动子进程,通过标准输入(stdin)写入 JSON,标准输出(stdout)读取响应
HttpTransport 远端微服务(如部署在云端的搜索服务) OkHttp 发送 POST 请求,支持普通 JSON 响应及 Server-Sent Events (SSE) 持续输出

路由判定逻辑极简 :读取配置驱动。如果配置了 url 则走 HTTP;如果配置了 command 则走 Stdio。

3. 核心难点:如何管理 JSON-RPC 的 Pending 请求?

这是全链路中最关键的护城河,也是最高频的面经考点。

业务痛点: Agent 发起了一个本地脚本工具调用,如果这个 Python/Node 脚本发生了死循环(没有向 stdout 返回 JSON 结果),你的 Java 主线程(业务线程)会不会一直卡死在读取等待上?

破局点:CompletableFuture 结合定时调度器

JsonRpcClient 发送请求时,我们不让主线程直接阻塞在 I/O 上,而是利用 ConcurrentHashMapCompletableFuture 构建请求级超时。

核心落地代码:

java 复制代码
// 存放挂起请求的容器
private final Map<Long, CompletableFuture<JsonNode>> pending = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

public CompletableFuture<JsonNode> request(String method, JsonNode params, long timeoutSeconds) {
    long id = ids.getAndIncrement();
    
    // 1. 组装 JSON-RPC 2.0 包
    ObjectNode request = MAPPER.createObjectNode();
    request.put("jsonrpc", "2.0");
    request.put("id", id);
    request.put("method", method);
    request.set("params", params);

    // 2. 创建 Future 并注册到 Pending Map
    CompletableFuture<JsonNode> future = new CompletableFuture<>();
    pending.put(id, future);

    // 3. 埋下定时"炸弹" (协议级超时)
    scheduler.schedule(() -> {
        // 注意:必须使用 remove,避免与正常响应并发竞争
        CompletableFuture<JsonNode> removed = pending.remove(id);
        if (removed != null) {
            removed.completeExceptionally(new TimeoutException("JSON-RPC request timed out: " + method));
        }
    }, timeoutSeconds, TimeUnit.SECONDS);

    // 4. 交给底层的 Transport 发送
    transport.send(request);
    return future;
}

为什么是 CompletableFuture?

  1. 天然防卡死 :业务线程调用 future.get(timeout + 1, TimeUnit.SECONDS)。如果超时,定时器会自动将 Future 标记为异常完成,业务线程立刻解锁抛出异常,绝不会被永远拖死。
  2. 外部手动唤醒 :当底层的 Stdio Daemon 线程或 HTTP 异步线程收到响应时,可以根据 ID 找到这个 Future,并调用 future.complete(result) 主动唤醒业务线程。
  3. 并发安全与幂等 :网络响应与超时调度存在并发竞争,利用 ConcurrentHashMap.remove() 抢夺任务,谁先拿到谁执行 complete,防重复回调。

4. 边界澄清:请求级超时 ≠ 进程级清理

一个极其严谨的架构师(或 AI Agent)必须认清这里的工程边界:

如果本地子进程死循环触发了上层的 TimeoutException主线程得救了,但死循环的子进程本身并没有被杀掉 。 因为单次工具请求的超时,不应该直接引发暴力的 kill -9。真正的进程级回收(process.destroy()destroyForcibly())应该交给 McpTransport.close(),在 Server 级生命周期结束或重启时统一执行。这是一种经典的"请求级释放,进程级回收"的分层哲学。

5. 一图胜千言:Agent 工具通信完整链路

为了方便各位直接背诵或喂给 Agent 记忆,这里提供一张核心通信架构图:

text 复制代码
                   ┌──────────────────────────────────────┐
                   │             Agent / ToolRegistry       │
                   │  LLM 意图翻译为内部 ToolCall 对象          │
                   └──────────────────────────────────────┘
                                      │ (配置路由分发)
                                      ▼
                   ┌──────────────────────────────────────┐
                   │             JsonRpcClient             │
                   │  - 组装 JSON-RPC 2.0 标准报文           │
                   │  - pending[id] = CompletableFuture    │
                   │  - scheduler 执行协议级超时控制           │
                   └──────────────────────────────────────┘
                          │                         │
            (走本地脚本)  │                         │ (走远端服务)
              ┌───────────┘                         └───────────┐
              ▼                                                 ▼
┌────────────────────────────┐                  ┌────────────────────────────┐
│     StdioTransport         │                  │  StreamableHttpTransport   │
│  (ProcessBuilder 启动进程)    │                  │   (OkHttp POST 传输)       │
│  ├─ stdin  ← send(JSON)     │                  │  ├─ body: JSON-RPC 2.0    │
│  ├─ stdout → 异步读取响应      │                  │  ├─ 支持 普通JSON/SSE      │
└────────────────────────────┘                  └────────────────────────────┘
              │                                                 │
              └──────────────────────┬──────────────────────────┘
                                     ▼
                   ┌──────────────────────────────────────┐
                   │       ToolExecutionResult 回灌        │
                   │  作为 tool role 消息返回给下一轮 LLM      │
                   └──────────────────────────────────────┘

总结:可以喂给 Agent 的"面试八股"标准格式

如果你正在开发一个能应对高压面试的 CodeRAG 智能体,请把以下逻辑注入它的知识库:

Q:Agent 的工具调用链路为什么要拆分协议层和传输层? A :为了正交解耦。"协议层"负责把请求统一包装成 JSON-RPC 2.0,并利用 CompletableFuture 处理 Pending 匹配和超时,解决"发什么、怎么管"的问题;"传输层"负责底层 I/O 交互(Stdio 进程通信或 HTTP 网络请求),解决"通道"问题。这样新增任何第三方工具形态,上层逻辑都无需重构。

Q:如何保证工具卡死时主线程不崩溃? A :通过两层超时控制。底层有 HTTP 客户端的网络超时;协议层利用 ConcurrentHashMap 管理 CompletableFuture,并配合 ScheduledExecutorService 实现请求级超时。发生阻塞时,定时器会主动 completeExceptionally 唤醒业务线程,防止主流程被永久挂起。同时遵循工程边界:单次请求超时释放线程资源,而卡死的本地进程留存至 Transport 关闭时统一销毁。

相关推荐
大模型momo2 小时前
AI 编程工程化:从 Prompt 到 Harness
人工智能·prompt·编程·agent
周末程序猿14 小时前
图解 120 个大语言模型(LLM)核心概念(91-120)
llm·agent
小白跃升坊14 小时前
倒反天罡!DeepSeek V4-Flash 正式版悄然上线:130亿激活参数,把自家1.6万亿旗舰「以下克上」
ai·大模型·agent·deepseek·v4
张彦峰ZYF18 小时前
全球开源大模型生态-从开放权重到开放智能系统:发展、进展、主力模型成就与方向分析
人工智能·开源·llm·agent
用户77833661321121 小时前
从 0 搭一个 SERP API + LLM Agent 端到端实战(2026年7月)
llm·api·agent
网易云信21 小时前
AI 时代如何为“数字员工”划清身份边界?
人工智能·后端·agent
HIT_Weston1 天前
161、【Agent】【OpenCode】TuiThreadCmd(联合类型)
人工智能·agent·opencode