你有没有想过,一个 Agent 应用在后台跑任务时,进程突然崩溃了,会发生什么?用户发出去的消息会不会丢?正在执行的工具调用会不会重复?如果这是个发邮件的任务,邮件会不会被发两遍?

Rakazo 这个开源项目把这些问题当成核心设计目标来处理。它不是一个简单的“调模型、返回结果”的 Demo,而是一套把 Agent 变成**持久化协作对象**的完整工程实现:每个 Bot 有独立身份、对话、记忆和工作环境,用户可以从 Web、Electron 或 Expo 客户端随时与它交流,任务在后台执行,进程重启后还能接着干。

阅读它的源码,最值得关注的是**一次请求如何变成后台工作**,以及**进程重启、用户审批和外部操作怎样影响后续执行**。

核心思路:把“意图”和“执行”彻底分开

Rakazo 的架构分工很明确:**Pi** 负责模型与工具(Tool)的循环,**Rakazo** 负责围绕这个循环建立可持久化的产品状态。两者之间的接口,是理解整个仓库的起点。

从运行架构上看,用户请求先写入数据库,后台执行由独立 Worker 接管。模型请求不需要与用户的 HTTP 请求保持同样长的生命周期。API 层用 Hono / oRPC / Better Auth 接收请求,把业务记录和事件写入 PostgreSQL,然后通过 Graphile Worker(一个基于 PostgreSQL 的队列系统,不是独立的消息中间件)把任务交给 Worker 进程。Worker 再调用 PiAgentRuntime 执行模型与工具的循环。

这种设计的好处是:**用户发完消息就可以关掉页面,后台任务照常跑**。

领域模型:一条聊天消息背后保存了什么

Rakazo 把领域模型拆得很细,这是理解恢复逻辑的关键。

同一个 Run 在等待审批后恢复,可以保留身份并产生新的 Attempt。

这里有个细节特别重要:Message 表有 `(threadId, clientNonce)` 唯一约束。客户端超时重发时,系统能找回原来的发送结果,而不是产生第二条工作。这解决了分布式系统里经典的“幂等”问题。

沿着 threads.send 走完一次请求

真实 RPC 名是 `threads.send`。入口先确认模型可用,再解析当前用户有权访问的 Thread,最后调用 `sendThreadMessage`。

发送事务把 Message、Run 与 Event 放在一起提交,这是为了避免“用户看到了消息,后台却没有工作”这种半套状态。如果已有可引导的 Run,补充内容会进入 SteeringMessage(中途引导,即在 Agent 运行中接收用户补充的输入),Pi 在下一轮处理前认领这些输入。

事务提交后才发 Job。Job 只需携带 runId,Worker 重新读取权威状态;同一 Run 使用稳定的 `run:<runId>` Job key。入队失败会记录错误,但不撤销已经持久化的发送,后面由 Reconciler 补发。

Lease、Fence 与 Reconciler:分别防什么

这是 Rakazo 最硬核的部分,也是它区别于普通 Agent Demo 的关键。

**Lease(租约)** 解决“谁现在可以执行”。`continueRun` 用条件更新竞争执行权,初始 Lease 为 5 分钟,每 60 秒续期。

**Fence(栅栏令牌)** 解决“旧执行者回来后还能否写入”。它随接管递增,用来拒绝旧 Worker 的写入。举个例子:Worker A 的 Fence 是 7,断线后 Lease 过期,Worker B 用 Fence 8 接管。即使 A 又拿到模型返回值,也不能再用 Fence 7 完成这次 Run。

**Reconciler(状态协调器)** 默认每 30 秒扫描 queued Run 和 Lease 过期的运行,重新安排 Job。多个 Worker 通过 PostgreSQL advisory lock 选出执行扫描的一方,避免重复扫描。

这套机制保证了:**崩溃恢复不是“任意一条指令都能无损续跑”,而是重读业务状态、重建 Runtime 并核对已有操作**。

审批为什么还需要 uncertain

用户批准的是保存下来的具体请求。恢复后,Executor 会核对工具、目标资源和请求参数,不能让模型重新拼出一个相似但含义不同的操作。

真正困难的是远端已经接受请求,本地却来不及记录成功。例如邮件发出后 Worker 崩溃,恢复时仍看到 executing。源码会把这种结果视为 **uncertain**,不自动再次发送。

这是一种保守的恢复选择,不是跨所有外部服务实现 exactly-once(精确一次)。它宁可留下需要核对的结果,也不把“不知道是否成功”直接当成“可以重试”。

恢复时,`resolveDuplicateEffectGate` 会根据已保存的 ExternalEffect(外部副作用记录,保存一次外部操作的请求、审批和结果)状态决定后续动作:

安全边界:有审批机制,不代表所有 Tool 都会询问

源码明确把 shell、write_file、browser_act、computer_act 等列入审批豁免集合。也就是说,“每次执行危险动作前都会弹窗”并不符合实现;电脑环境与 Tool 权限本身就必须承受模型可能作出的操作。

密钥保存使用 AES-256-GCM 加密,并把记录标识绑定为附加认证数据。但它保护的是保存后的凭据,不会自动阻止拥有电脑执行权的程序访问已经注入环境的秘密。

还有一个容易漏看的部署条件:`SANDBOX_COMPUTER_EGRESS=restricted` 只标记电脑网络,真正的阻断依赖宿主防火墙规则。默认出站模式仍可访问宿主、局域网和云元数据地址。

记忆、历史和工作目录要分开理解

MarkdownMemoryStore 虽然以 Markdown 为可移植形式,当前实现仍把文档与修订保存在数据库。它的基础搜索是先取出作用域内文档,再用字符串包含关系过滤。因此,不能仅凭“Memory”名称推断底层使用了向量数据库。

注入模型的持久记忆默认有 32 KiB 上限,并明确标为可能过时的数据。历史压缩另有游标和摘要:使用摘要前,要确认后续消息连续,避免摘要之后缺了一段对话。

长期 Bot、临时 Subagent 与三端协作

持久 Bot 会创建数据库对象和独立 Thread,可以选择自己的电脑、模型与 Routine。临时 Subagent 则是父轮次中的辅助 Pi Agent,没有独立 Bot 对话;最多同时运行 4 个,不能继续嵌套。

三端(Web、Electron、Expo)共享的是同一服务端事实,而不是三份独立的 Agent 后端。Electron 的“在本机运行”实际启动 Compose 后端,退出应用后该后端仍可继续运行。

客户端同步依靠 Snapshot 与有序 Event。Web 的 reducer 根据 `run.started`、`thread.cleared` 等事件更新展示状态,订阅循环从已见 cursor 重连;heartbeat 只说明连接还活着,不应推进业务游标。

测试与部署:代码提供了哪些证据

仓库具备 Vitest、集成测试、端到端测试、模型模拟和电脑回放等入口。测试覆盖审批重放、租约、租户边界和 Provider 契约。

自托管有明确运维边界:Compose 同时运行数据库、API、Worker 和电脑 Supervisor;Supervisor 挂载 Docker socket,因此属于高信任组件。API 与 Worker 需要常驻进程,单纯的 Serverless 请求处理器不足以承载当前运行方式。两者共享持久 DATA_DIR,当前 Home Store 使用本地文件系统,跨主机部署需要共享文件系统。

这个思路能用到哪里

如果你在构建任何需要**后台执行 + 崩溃恢复**的 AI 应用,Rakazo 的这套设计可以直接借鉴:

1. **把意图、运行、尝试拆成三个对象**,恢复逻辑会清晰很多。

2. **用 Lease + Fence 解决并发执行权**,比简单的锁更健壮。

3. **外部副作用要显式记录状态**,尤其是“不知道是否成功”的状态,宁可让用户核对,也不要自动重试。

4. **幂等键(nonce)是必须的**,客户端超时重发是常态。

要注意的坑:PostgreSQL 同时承担业务、队列和协调职责,连接压力集中;executor.ts 有 9,900 行,router.ts 有 7,458 行,修改核心路径需要跨审批、恢复和多端状态核对回归。

Rakazo 证明了:**Agent 从“能跑”到“能可靠地跑”,中间隔着一整套工程化设计**。如果你想让自己的 Agent 应用真正可用,这份源码值得仔细读一遍。

阅读原文 → 返回 AI 技术文档

内容与图片版权归原作者所有 · 原文: https://yindongliang.com/docs/AI/Agents/rakazo-source-analysis/