2026-08-23AI 观察Tao

从易失循环到崩溃自愈:Pi AgentHarness v2 的架构哲学

剖析 Pi 项目 Durable AgentHarness v2 的架构设计:如何用「先记意图后落事实」、树与多泳道解耦、关卡序列化以及受控变异管线,终结大模型 Agent 的崩溃脏状态与并发竞态。

目录共 5 节
  1. 意图先行与预配 ID:无需分布式事务的崩溃自愈
  2. 树与泳道:像 Git Worktree 一样并发演进的会话体系
  3. 保护 KV Cache 的纪律:关卡序列化与单线变异
  4. 确定性网关模型:从易失黑盒到可单步调试的确定系统
  5. 从易失性循环到数据库级执行引擎

参考架构设计Pi AgentHarness v2 Design Document

核心模块:packages/agent/src/harness · 源码实现:earendil-works/pi

设计目标:为生产环境提供具备崩溃恢复能力(Durability)、多泳道并发执行、确定性步进测试与严格保护模型 KV 缓存的工业级 Agent 执行引擎。

在绝大多数开源演示与教程里,一个大语言模型智能体(Agent)的核心通常被简化为一个朴素的 while 循环:接收输入、组装上下文、发起模型请求、解析工具调用、执行工具,然后把结果追加回数组继续下一轮。在单次运行的 Demo 或脚本中,这套逻辑足以令人惊艳;但只要将它推向真实的生产系统——面对长时间运行的任务、突发的进程崩溃、多线程并行的交互、不稳定的网络连接以及昂贵的上下文计费——这套看似优雅的简单循环就会瞬间暴露出工业级支撑的巨大真空。

当宿主进程在一次写文件工具执行到一半时断电,重启后的系统该如何得知当前任务进行到了哪一步?它会丢失用户的上一句纠错,还是会把已经部分执行的外部工具再次重放?如果多个 Slack 线程或者子任务需要同时在同一个会话分支上工作,状态机如何在不产生脏读脏写的前提下维持单写者秩序?更进一步,当用户在模型推理的瞬间插话,系统是在当前请求中间硬塞一条消息从而击穿服务商的 KV 缓存,还是能够精确维持上下文单调递增的纪律?

Pi 项目在重构其核心运行时所产出的 Durable AgentHarness v2 设计,给出了迄今为止关于 Agent 执行引擎最为严密、也最具数据库思维的工程答卷。它没有走许多框架堆砌抽象概念的老路,而是回归分布式系统与存储底座的本源,用一套精简至极的公理与状态机,彻底终结了智能体系统的崩溃脏态与并发竞态。

flowchart TD
    App[应用层 / UI] -->|prompt, steer, abort| Harness[AgentHarness 运行时]
    Harness -->|快照 + 顺序事件| App
    Harness -->|拦截 Hook| Ext[扩展系统]
    Harness --> Lanes[并发泳道: main, thread-1, ...<br/>单泳道互斥,多泳道并行]
    Lanes --> Loop[步进原语: Step Primitives<br/>模型请求 / 工具调用批次]
    Loop --> Provider[LLM Provider / Deferred Handle]
    Loop --> Tools[外部工具集群]
    Harness --> Session[Session 核心状态<br/>共享对话树 · 独立泳道操作日志 · 全局事实]
    Session --> Storage[(存储层: SQLite / JSONL / Memory)]

意图先行与预配 ID:无需分布式事务的崩溃自愈

在传统后端架构中,保证多步骤操作原子性往往依赖于两阶段提交(2PC)或数据库事务。然而在 Agent 的执行世界中,工具调用往往包含不可逆的外部现实世界副作用(如发出 HTTP 请求、修改本地代码库或执行 Shell 命令),而大语言模型的流式生成本身也是无法挂载在事务中的网络 I/O。试图用重量级分布式事务包裹 Agent 是一条死胡同。

AgentHarness v2 确立了一条极为锋利的持久化黄金公理(The Durability Rule)

在发生任何实际副作用之前,必须先持久化写入一条意图记录(Intent Record),并在记录中指明即将发生的事情以及预分配的结果 ID;当副作用执行完成后,再将真正的结果以该预配 ID 作为实体追加到存储中。

    用户发起 prompt("fix the bug")
H   before_run                          (Hook 拦截,可注入消息或覆盖系统提示)
R   operation_started                   (写入意图记录:记录 operation kind 与预配 entry IDs)
E   user message                        (追加用户消息 Entry,ID 与意图预配完全一致)
R   step_attempt                        (写入意图记录:assistant step, attempt 1)
E   assistant message [tool call]       (模型返回工具调用,持久化 Entry)
H   before_tool                         (Hook 校验工具参数)
R   tool_started                        (写入意图记录:记录工具名称、生效参数、重放策略与预配 resultEntryId)
E   tool result                         (工具执行完毕,以预配 ID 写入结果 Entry)
R   operation_finished                  (写入收尾记录:completed)

在这套协议下,存储层根本不需要跨多条记录的原子性,每一次日志写入(Record)和每一次内容追加(Entry)本身都是原子独立的。系统在任何两个操作之间遭遇崩溃,其留在持久化介质中的状态都只有两种可能:要么意图尚未记录(等同于操作从未被接受,调用方得到拒绝);要么意图已经落盘,但对应的结果 Entry 尚未出现。

这种设计让进程恢复变成了一场纯粹的折叠计算(Reduction)。新启动的进程在加载会话时,无需猜测复杂的运行时现场,只需比对操作日志中的意图与树上的实际条目:

  • 如果发现了 tool_started 记录,但不存在该预配 ID 的 tool_result 条目,恢复引擎便能立即判定崩溃发生在此次工具执行期间。此时系统会根据该工具在启动时声明的幂等安全级别(Replay Safety)做出决断:若工具声明为 safe,则使用已持久化的有效参数重新执行;若声明为 never,则直接写入一条合成的「执行被中断」结果条目,决不冒险二次执行非幂等操作。
  • 如果发现了 step_attempt 记录但未产生对应的模型消息 Entry,系统便知道这是一次未决的模型请求,恢复时自动启动下一次重试尝试,同时累加已持久化的重试计数,防止无限重启循环。

更重要的是,所有的计费数据(Usage Record)都在每次模型请求结算的瞬间被独立写入,甚至先于结果的分类与重试决策。即使模型返回的内容因超出上下文窗口而被丢弃,或者在写入消息前遭遇崩溃,已经消耗的 Token 账本也不会在恢复过程中蒸发。系统彻底将计费的持久性结果的持久性剥离开来。


树与泳道:像 Git Worktree 一样并发演进的会话体系

多数 Agent 系统在设计会话历史时,往往将其建模为线性数组。当引入多分支、撤销、回滚或者多智能体协作时,这种线性结构便会迅速崩溃。为了支持分支,部分系统采用了树状结构,但随之而来的问题是:谁拥有这棵树的当前焦点?如果主对话在进行的同时,一个后台子任务也需要在这棵树上读取上下文并产出结论,二者该如何隔离?

AgentHarness v2 提出了**共享被动树(Passive Tree)与独立主动泳道(Active Lanes)**的二元解耦模型。

共享对话树 (Append-Only, 被动数据)           活跃泳道 (Active Lanes)
a ── b ── c ── d                          main            → d   (专属操作日志: op_started...)
      └── e ── f                          slack:171943…   → f   (专属操作日志: op_started...)

全局事实 (Global Facts): name = "重构认证模块", label(b) = "checkpoint-1"

在这套模型中:

  1. 对话树是纯粹、共享且被动的。它由一个个带有 parentId 的条目(Entry)组成,只允许单向追加,从不修改或删除任何节点。对话树本身不属于任何特定的执行流程,也不记录任何诸如「当前正在运行哪一步」的编排状态。
  2. 泳道是主动的工作位置。一个泳道本质上是树上的一个命名位置(叶子节点指针)加上串行化在其上的操作日志(Operation Log)。系统默认拥有 main 泳道,而应用层可以根据外部实体创建任意多的命名泳道(例如一个 Slack 线程 ID、一个邮件回复链、或者一个子智能体 ID)。

这种结构极其神似 Git 的工作区机制:对话树就是 Git 的提交对象图,而泳道就是检出在独立工作区(Worktree)中的分支指针。创建泳道不复制任何历史数据,两条泳道可以同时以同一个历史节点为起点。当各自发起新的对话与工具调用时,它们在各自的泳道操作日志中记录步骤,在树上追加子节点,并独立向前推进各自的叶子指针。

两个泳道在同一个 Harness 实例中并行执行,彼此之间不存在任何锁竞争或跨泳道协调,存储层只需通过递增的全局序列号(Sequence Number)将它们的物理写入交织落盘。当其中一条泳道遭遇错误或挂起时,其他泳道毫发无损。


保护 KV Cache 的纪律:关卡序列化与单线变异

在大模型 Agent 的实际工程成本中,服务商的 Prompt/KV 缓存命中率往往决定了整个系统的响应延迟与账单上限。以主流模型架构为例,只要请求上下文的前缀发生改变,从变动点开始的所有后续 KV 缓存都将全数失效,导致请求重新全量计算并被全额计费。

一个典型的破坏场景发生在智能体多轮运行的中途:假设模型正在执行一个长达 30 秒的重构工具,此时用户在界面上输入了一条追加指令「顺便加上单元测试」。如果系统为了实时响应,立即将这条用户消息强行插到当前会话的尾部,那么当工具执行完毕并追加结果时,提供商看到的上下文顺序就会变成 [用户指令, 工具结果],从而彻底破坏上一轮请求建立的缓存前缀。

AgentHarness v2 对此设立了严格的单调追加上下文(Append-Only Context)铁律

在一个泳道的连续请求之间,模型上下文必须严格保持仅在尾部增长。绝不允许在先前的请求尾部之前插入任何内容。

[用户发起 Prompt]
      ↓
[进入 Turn] ── 运行 Assistant Step ──→ (用户此时发起 Steer 纠偏) ──→ 写入 queue_enqueued 日志 (暂不入树)
      ↓                                                                          │
[执行工具批次] ── 完成并追加 tool_result Entry                                     │
      ↓                                                                          │
[抵达 Checkpoint 关卡] ←──────────────────────────────────────────────────────────┘
      ├─ 1. 应用排队的延迟写入 (Deferred Writes)
      ├─ 2. 消费队列中的用户纠偏 (Steer / Follow-Up Messages)
      └─ 3. 检查上下文窗口压力,必要时执行自动上下文压缩 (Compaction)
      ↓
[进入下一个 Turn] ── 模型在完整的尾部追加上下文上发起请求,KV Cache 完美命中

为了兼顾「随时接收用户输入」与「严格保护上下文前缀」,Harness 引入了排队意图与关卡(Checkpoint)机制

  • 当模型或工具正在执行期间,用户发起的纠偏输入(steer)、后续任务(followUp)或配置修改,在接口层会立即作为 queue_enqueuedwrite_deferred 记录写入泳道操作日志,并向调用方返回成功。
  • 这些输入在此时不会直接作为消息 Entry 插入对话树,因此不会干扰正在进行的模型推理与工具交互。
  • 只有当一个回合(Turn)完全结束、抵达预设的关卡(Checkpoint)时,系统才会按严格顺序批量消化排队的延迟写入与纠偏消息,将它们统一追加在当前叶子节点的末尾。

与此配合的是泳道的受控变异管线(Lane Mutation Line)。系统为每个泳道维持一个局部的 Promise FIFO 队列,所有依赖状态的决断(如判断是否可以结束运行、消费队列条目、执行中断)都在微秒级的内存状态校验与单次磁盘写入中完成。耗时漫长的网络请求与工具执行则在管线外部运行。这种将轻量状态决断串行化、繁重 I/O 外置的架构,在数学上完全消除了分布式并发中最臭名昭著的「检查与行动(Check-then-Act)」竞态条件。


确定性网关模型:从易失黑盒到可单步调试的确定系统

长期以来,编写 Agent 自动化测试一直是工程界的噩梦。由于大模型输出的非确定性、外部工具的不可控网络调用以及并发竞态的存在,多数测试要么只能做粗粒度的端到端黑盒验证,要么充斥着大量不可靠的 sleep 和脆弱的 Mock。系统无法在单元测试中重现「在第 3 个工具执行到第 200 毫秒且用户恰好点击了取消」的边缘极端崩溃场景。

AgentHarness v2 从架构根基上给出了优雅的解法:效果边界抽象(The Effects Boundary)与双驱动模式(Drive Modes)

flowchart LR
    subgraph Procedure[Agent 执行流程 (纯逻辑控制)]
        Direction[状态推进 / 意图生成]
    end

    Direction -->|一切写存储、调模型、调工具、跑 Hook 操作| FX[Effects 网关 (fx)]

    subgraph DualDrive[双运行模式]
        Auto[drive: 'automatic'<br/>生产环境: 直通底层 I/O]
        Manual[drive: 'manual'<br/>测试环境: GatedEffects 动作驻留网关]
    end

    FX --> DualDrive
    Manual --> ActionQueue[动作队列: peekAction / executeAction]

在 Harness 内部,核心逻辑流程被严格限制为只能持有单一的 fx 句柄,禁止直接访问数据库、模型 SDK 或外部工具。所有的外部副作用——追加日志、移动指针、流式请求模型、执行工具、运行 Hook、甚至 sleep 延时——全部收敛为 Effects 接口上的显式调用:

  1. 在生产环境中,系统配置为 drive: "automatic"fx 句柄直通真实底层,以零抽象损耗全速运转。
  2. 在测试环境中,系统无缝切换为 drive: "manual"。此时 fx 被一层 GatedEffects 门控代理所包裹。每一个即将执行的副作用都会自动在门控处挂起,并对外暴露出一个自描述的动作元数据(ActionInfo)。

测试代码可以通过 peekAction() 审查下一个即将执行的物理动作,使用 executeAction() 精确放行单一步骤,或者在两个动作释放的间隙直接调用 harness.close() 来模拟物理断电崩溃。随后,测试可以重新打开同一个存储后端并调用 resume(),从而机械化、穷尽式地验证状态机在整个生命周期中**每一个可能崩溃断点(Crash Sites X1~X5)**下的恢复正确性。

生产环境与测试环境执行的是同一套严丝合缝的状态机逻辑,驱动模式改变的仅仅是副作用释放的节奏。这一设计彻底将智能体运行时从难以捉摸的概率性黑盒,转化为了可离散化、可断点步进、可数学证明的确定性状态机。


从易失性循环到数据库级执行引擎

回顾软件工程的历史,计算架构的每一次成熟,都伴随着从「即兴的内存状态操控」走向「严格的数据抽象与持久化边界」:

  • 操作系统通过引入虚拟内存、文件系统与进程控制块,终结了早期裸机代码由于单点崩溃导致整机宕机的混乱;
  • 关系型数据库通过 Write-Ahead Logging(WAL)与 ACID 模型,将应用程序从繁琐的手工数据一致性维护中解放出来;
  • 现代大模型 Agent 的发展,正在经历一模一样的范式跃迁。

早期基于简单代码循环的 Agent 框架,如同裸机时代的内存脚本,将编排状态、会话历史、并发控制与外部 I/O 揉杂在易失的进程内存里,脆弱得不堪一击。而以 Pi Durable AgentHarness v2 为代表的下一代执行引擎,则清晰地宣告了这一蛮荒时期的终结。

通过意图先行的折叠恢复树与泳道的二元解耦单调追加的缓存纪律以及确定性的门控效果边界,系统不仅在工程上建立起抵御真实世界网络抖动、进程崩溃与高并发调度的防波堤,更在概念层面上为智能体软件树立了一套清晰、完备且高度自洽的工业标准。

当大语言模型本身的不确定性与创造力被牢牢包裹在一个具备严格确定性、可恢复性与持久性的底座之内时,真正能够承受工业级负载的自主智能体应用,才真正迎来了它们的破晓时刻。

发布自
atlasnote-editorial
发布日期
2026-08-23
标签
AIAgentarchitecture编程