第 24 章 · RPC 与远程驱动
Harness 锚点|判别清单第 3 项「状态持久化」——用 RPC + 会话树远程驱动 agent。
Agent 不能只活在你的终端里
那个卖家的选品 Agent 要变成给同行用的产品了,第一个现实问题就冒出来:总不能让每个卖家都在自己电脑的终端里敲命令吧?他们要的是打开浏览器就能用;而且有些调研任务一跑就是几十分钟,卖家关掉网页去忙别的、回头还能看到进度;甚至他想在手机上随时瞄一眼 Agent 跑到哪了。
这些需求归结成一句话:Agent 得运行在别处(服务器),用户通过某种协议远程驱动它。具体来说有三类场景:
- 一个 Web 界面,用户在浏览器里用 Agent——Agent 得跑在服务器上。
- 一个长时间任务,要跑几十分钟——不能占着你的终端。
- 一个 App,想调用远端的 Agent 能力。
而 Agent 又是有状态的(stateful):它有会话树、上下文、正在用的工具、跑到一半的任务。远程驱动一个有状态的东西,不能只是“发个请求收个结果”那么简单——你得能中途接管、断线重连、回到会话的某个历史节点。
模式:把 Agent 变成可远程操作的有状态服务
要远程驱动一个有状态的 Agent,需要三样东西,缺一不可:
- 可寻址的会话(addressable session):每个会话有 ID,能被远程精确定位、随时“接上”(回顾第 12 章——会话是可持久化、可寻址的)。没有这个,远程端根本不知道要驱动的是哪个会话。
- 一套远程指令协议(remote protocol):远程端能发出“继续这个会话”“调用这个工具”“导航到会话树的某个节点”“从这里 fork 一个分支”等指令,而且指令格式两端约定好、能序列化传输。
- 状态在服务端(server-side state):会话状态存在服务端,客户端只是“驱动”它——这样客户端断开重连,状态还在。手机息屏了、浏览器关了,服务端那个会话照样活着。
这套东西通常用 RPC(Remote Procedure Call,远程过程调用) 来实现:客户端像调用本地函数一样,调用运行在远端的 Agent 操作——底层的网络传输、序列化、连接管理都被 RPC 框架藏起来了。
有状态 RPC 和无状态 REST 的区别
这里要点破一个关键区别,否则容易把它做成普通的 Web API。
- 一个无状态(stateless)的 REST 接口,每个请求都是独立的:请求里带上所有信息,服务端处理完就忘。这对“查个价、下个单”够用。
- 但 Agent 是有状态的:第二条消息的含义,依赖第一条建立的上下文;你回退到会话树的某个节点再 fork,依赖整棵树都还在服务端。
所以驱动 Agent 的 RPC 更像是“远程操作一个活着的对象”,而不是“发一串互不相干的请求”。这也解释了为什么需要“可寻址会话”——每个远程指令都得先说清“我要操作哪个会话”,服务端才能把它接到正确的状态上下文里。
用 pi 实现:RPC 与会话树导航
这正是 pi 的一项核心能力(也是它 fork 增量里的重点)。pi 提供了:
- RPC 模式(
packages/coding-agent/src/modes/rpc/):让外部程序通过 RPC 远程驱动 agent。 - 会话树导航(session tree navigation)(
core/agent-session-tree.ts):远程端能在会话树里前后移动、分叉。 - pi-web 远程(
docs/pi-web-remote.md):一个真实的应用——Web 前端经 RPC 远程操作后端的 Agent。
概念上,远程驱动像这样:
// 客户端(如 Web 后端)通过 RPC 连接到运行 Agent 的服务
const client = await pi.connectRpc({ endpoint: "..." });
// 像调本地一样:在指定会话上追加消息并推进
await client.session(sessionId).send("继续分析这份数据");
// 在会话树里导航:回到某个节点、开新分支
await client.session(sessionId).navigateTo(nodeId);
await client.session(sessionId).fork();
注意每个调用都以 session(sessionId) 开头——这就是“可寻址会话”的落地:先定位到哪个会话,再对它下指令。因为状态在服务端、会话可寻址,这套机制天然支持:Web 驱动(第 33 章的后台 Agent)、跨机器交接(第 19 章的 handoff)、断线恢复(第 12 章)——客户端断了重连,session(sessionId) 一接,还是原来那个会话。
互操作:RPC、MCP、A2A 分别解决什么
Agent 生态里有几个相近的词,很容易混:
| 机制 | 你在连接什么 | 典型问题 | 本书位置 |
|---|---|---|---|
| MCP | Agent 接外部工具 / 数据源 | “我怎么复用别人写好的工具?” | 第 5 章 |
| RPC | 客户端远程驱动一个有状态 Agent | “Web/App 怎么操作远端会话?” | 本章 |
| A2A / Agent-to-Agent | 一个 Agent 把任务交给另一个 Agent 服务 | “不同 Agent 系统之间怎么协作?” | 第 18、23 章 |
判断方式很简单:
- 如果对方是一个工具能力,用 MCP 或普通工具封装。
- 如果对方是一个有状态会话,用 RPC 远程驱动。
- 如果对方是一个独立 Agent 服务,把它当成远端协作者:要定义任务输入、能力边界、会话 ID、返回格式和失败语义。
在 pi 视角下,A2A 不需要神秘化。最保守的做法,是把远端 Agent 暴露成一个受控工具或 RPC 服务:本地 Agent 发出任务请求,远端 Agent 在自己的会话里执行,最后返回结构化结果与 trace 链接。这样既保留了 Agent 协作能力,又不把两个系统的上下文硬塞到一起。
关键原则是:跨 Agent 传任务,不传一整个脑子。 交给对方的应该是目标、约束、必要上下文和可验证输出,而不是把本地完整会话原样丢过去。
与原书的对比:轻量 RPC vs 重型工作流引擎
原书这一部分讲的是 Temporal——一个重型的分布式工作流引擎,提供持久化执行、自动重试、跨服务编排等强能力,适合大规模、高可靠的企业编排。
pi 的 RPC + 会话树是一条更轻的路:
| Temporal(原书) | pi RPC + 会话树 | |
|---|---|---|
| 定位 | 重型分布式工作流引擎 | 轻量远程驱动有状态 Agent |
| 能力 | 持久执行、自动重试、跨服务 | 远程驱动、会话导航、断线恢复 |
| 复杂度 | 高,需独立部署 | 低,harness 内建 |
| 适合 | 大规模企业编排 | 中小规模、Web/App 驱动 Agent |
诚实地说:如果你要做的是需要“持久执行 + 强可靠性保证”的大规模编排,Temporal 这类引擎仍是更成熟的选择——pi 的 RPC 不替代它。但对绝大多数“把 Agent 搬上 Web/服务端”的需求,pi 内建的这套已经够用,而且轻得多。用对量级,别过度工程。
真实案例:pi-app 怎么远程驱动一个本地 Agent
上面讲的都是原理,来看一个真实的产品——pi-app(pi 的 Web + macOS 界面)。它就是“通过 RPC 远程驱动有状态 Agent”的活样本,而且把这件事做到了手机上:你可以用手机浏览器,完整控制跑在家里电脑上的 pi 会话——发消息、切模型、fork 分支、读文件,一样不少。
它的实现恰好印证了本章的每个要点:
- 状态在服务端:pi-app 不自建存储,直接读写本机的
~/.pi/agent/会话目录——和 CLI 用的是同一份数据。你在终端里跑的会话,浏览器里能接着看、接着聊(第 12 章的“可寻址会话”落地)。 - 一套远程指令经由 RPC:pi-app 的新能力统一走
pi RPC → rpc-manager → UI这条链,前端只是“驱动”服务端的AgentSession,而非自己实现一套 Agent 逻辑。 - 默认关闭,显式打开:远程访问默认不开(只监听本机)。要开启,得在设置里生成配对链接 / 二维码,手机扫码写入一个 httpOnly 会话 cookie;脚本场景则用
Authorization: Bearer <token>。
启动远程模式就一行命令:
pi-web --remote --hostname 0.0.0.0 # 开启远程鉴权,绑定局域网
# 配合 Tailscale 更安全:绑到 tailnet 内网地址
pi-web --remote --hostname $(tailscale ip -4)
没有公网 IP?pi-app 还内建了隧道方案(Tailscale Funnel / Cloudflare Quick Tunnel),一条命令拿到临时 HTTPS 公网 URL——但仍然要求先配对 + token 鉴权才能访问。这是一个很好的安全示范:远程能力要“默认关闭、显式开启、始终鉴权”(呼应第 26、27 章)。
值得学的一点:pi-app 坚持“能力来自 pi,表达来自 pi-web”——它不重新实现 Agent,只是给同一个 pi 运行时套一层界面。这正是第 35 章“在 harness 之上构建”的现实写照。
动手看看
在本机试一次“远程”驱动:开启 pi-web --remote,用同一台机器的浏览器连上去,发一条消息,再打开终端 pi 列一下会话——你会发现浏览器里那个会话在终端也看得到、还能接着聊。这一步能直观地让你感到“状态在服务端、会话可寻址”到底意味着什么。
再故意断一次线:网页发消息后立刻刷新(模拟断线重连),看会话状态是不是还在、能不能接着往下。对照一下“如果状态存在客户端会怎样”——你就明白为什么第 3 样东西(服务端状态)是远程驱动的地基。
实战中的几个坑
坑一:把有状态 Agent 当无状态 API 做。
- 现象:每个请求都重新初始化,上下文全丢,Agent 像失忆一样。
- 原因:套用了无状态 REST 的思路,没有可寻址会话。
- 对策:让会话在服务端持久、带 ID,每个远程指令先定位会话再执行。
坑二:远程能力默认对公网敞开。
- 现象:一开远程就监听
0.0.0.0且无鉴权,任何人都能驱动你的 Agent。 - 原因:图省事没加鉴权,或误以为“内网安全”。
- 对策:遵循“默认关闭、显式开启、始终鉴权”——配对/token 必备,能绑内网就别绑公网(呼应第 28 章)。
坑三:长任务占着连接,断线就前功尽弃。
- 现象:几十分钟的任务,网络一抖就中断、进度全丢。
- 原因:把任务进度绑在客户端连接上,而非服务端状态上。
- 对策:任务状态存服务端,客户端只是“观察/驱动”;断线后靠
session(id)重新接上。
坑四:过度工程,给小需求上重型工作流引擎。
- 现象:就想把 Agent 搬上 Web,却先部署了一整套 Temporal 集群。
- 原因:没匹配量级,盲目追求“企业级可靠”。
- 对策:中小规模用 harness 内建的 RPC 就够;确有“持久执行 + 强可靠”刚需时再上 Temporal。
对比:其他框架
让有状态 Agent 可远程操作,各家给的方案深浅不一,核心都要解决“可寻址会话 + 远程协议 + 服务端状态”。
| 框架 | 远程驱动怎么做 | 特点 |
|---|---|---|
| LangGraph | LangGraph Platform(配 LangServe)把图部署成服务,内建 checkpointer 做状态持久/恢复/分支 | 一等的部署方案,远程调用与断点续跑是强项 |
| CrewAI | 聚焦编排,远程部署多是“把 Crew 包成服务自己起” | 无内建可寻址会话树/远程状态协议,需自行搭服务与持久化 |
| PydanticAI | 不内建远程运行时,通常嵌进自己的 Web 框架(如 FastAPI) | 状态管理由你负责,融入既有应用灵活 |
| Agno | AgentOS 就是运行时,可把 Agent 作服务运行并带监控,内建 Memory 提供持久状态 | 适合直接跑成可远程调用的服务 |
| pi | 内建 RPC 模式 + 可寻址会话树 + pi-web 远程 | harness 层的轻量远程驱动,用对量级、别过度工程 |
一句话点破:无论用哪家,能不能“断线重连、回到历史节点、跨机器接管”,都取决于服务端有没有把会话做成可寻址的持久状态——这才是远程驱动的地基,协议只是门面。(原书方向的 Temporal / Restate 则是更重型的持久化工作流引擎,可承载 Agent 编排。)
小结
- 生产 Agent 常需远程运行:Web 界面、长任务、App 调用——Agent 跑在别处,用户远程驱动。
- 远程驱动有状态 Agent 需要三样:可寻址会话 + 远程指令协议 + 服务端状态,通常用 RPC 实现;它像“远程操作一个活对象”,不同于无状态 REST。
- pi 内建 RPC 模式 + 会话树导航 + pi-web 远程,天然支持 Web 驱动、跨机器交接、断线恢复,每个指令都以
session(id)先定位会话。 - MCP、RPC、A2A 不是一回事:MCP 接工具,RPC 驱动有状态会话,A2A 传递任务给远端 Agent 服务。
- pi-app 是活样本:手机浏览器完整驱动本机 Agent,坚持“状态在服务端、能力来自引擎、默认关闭显式开启始终鉴权”。
- 相比原书的 Temporal(重型工作流引擎),pi 是更轻的路——中小规模、Web/App 驱动够用,用对量级、别过度工程。
远程跑起来了,但你怎么知道它跑得好不好、花了多少钱、错在哪?下一章讲可观测性。