您的位置:首页 > 手游攻略 > LLM Agent 底层揭秘:大模型如何通过 JSON-RPC 2.0 协议跨进程调工具?

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

作者:互联网  时间: 2026-08-03 11:01:00  

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

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

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

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

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

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

 复制代码{"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 请求:

 复制代码{"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 构建请求级超时。

核心落地代码:

 复制代码// 存放挂起请求的容器private final Map<Long, CompletableFuture<JsonNode>> pending = newConcurrentHashMap<>();private finalScheduledExecutorServicescheduler= 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 记忆,这里提供一张核心通信架构图:

 复制代码                   ┌──────────────────────────────────────┐                   │             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 关闭时统一销毁。

最新游戏

更多

Copyright©2010-2019. All rights reserved | 波波三国游戏官网|[email protected]

备案编号:湘ICP备2022015115号-4