Arianna 需要一个编码代理后端。不是聊天模型，不是代理框架。而是一个接收提示、使用工具、然后退出的子进程。Pi 以 Unix 级别的简洁性做到了这一点，而其背后的设计选择比功能列表更重要。

这是"黑暗工厂日记"系列的一篇文章，该系列记录了构建 Arianna——一个完全自主的软件开发工厂——的过程。

前两篇文章讨论了上下文管理：单次运行内的读取剪枝（#4），以及任务之间的上下文重置（#5）。两者都假设存在一个可以被干净地启动、提示和停止的编码代理。本文讨论的是代理本身。

## 工厂对编码代理的需求

Arianna 是一个流水线引擎。它解析一个有向无环图（DAG），遍历节点，在每个 `codergen` 节点需要将工作移交给能够编写代码、运行测试、编辑文件和使用终端的组件。引擎用 Go 编写。编码代理是一个子进程。

需求很明确：

- **子进程控制。** 启动它，发送提示，获取结构化输出，停止它。没有持久服务器，没有 HTTP，只有通过 stdin/stdout 的 JSONL。
- **真正可用的工具。** 终端、文件编辑、浏览器、搜索。不是玩具沙箱，而是能构建真正软件的真实工具。
- **模型无关。** 工厂将不同的模型路由到不同的节点。一个节点可能用 Opus 做规划，下一个用 Sonnet 做实现，再用本地模型做审查。代理不能绑定到某个单一供应商。
- **在有帮助的地方保持会话。** 在工厂还在决定做什么的时候——规格展开、完成定义（Definition of Done）、规划、对齐、协调——我想要连续性。我不想让实现工人拖着一个膨胀的实时线程从一个任务到另一个任务。两种模式都必须工作。
- **无需修改源码即可扩展。** 工厂需要注入行为：轮次预算、循环检测、输出截断、结构化输出模式。这不应该要求修改代理的源代码。

我评估了几个选项。Claude Code 很好但与 Anthropic 紧密耦合。Codex 只支持 OpenAI。Aider 很强但为交互使用而设计。Cursor 是一个 IDE，不是子进程。

Pi 满足所有条件。

## Unix 风格的简洁

Pi 是 Mario Zechner（@badlogic）的一个 monorepo 项目。包的划分非常清晰：

- `pi-ai`：统一的多供应商 LLM API
- `pi-agent-core`：带有工具调用和状态管理的代理运行时
- `pi-coding-agent`：交互式 CLI
- `pi-tui`：支持差异渲染的终端 UI
- `pi-pods`：管理 vLLM 部署的 CLI

我一直回到的一点是 RPC 模式。你用 `--rpc` 启动 Pi，它就通过 stdin/stdout 交换 JSONL。没有服务器，没有端口，没有 HTTP。引擎发送命令，Pi 处理它，返回结构化响应。这就是 Unix 工具的组合方式。`cat PROMPT.md | pi` 可以工作。从 Go 代码中将其作为子进程生成并以编程方式管理其生命周期也可以。

```
Arianna Engine (Go)  ←→  Pi subprocess  ←→  LLM provider
                          ↕
                      Terminal / File system / Browser
```

在 Arianna 中，Go 引擎可以在协调阶段保持 Pi 进程存活，然后在工作转变为代码时启动一个新的有边界循环。重要的不是"到处都是会话"，而是能够将边界放在正确的位置。

## 只在有帮助的地方使用会话模式

日记 #5 主张每个新鲜的上下文窗口一个任务。对于执行阶段，我仍然相信这一点。我上次表达得不够精确的地方是：连续性究竟在哪里真正有价值。

实时会话有价值的地方是协调阶段：规格展开、完成定义的综合、计划发散、对峙、协调。这仍然是一个推理问题。系统在决定构建什么、哪些约束重要、哪些阻碍是真实的、哪些权衡已经确定。如果仅从文件中在每个子步骤之后重建这些信息，虽然可能，但会有信息损失。

损失来自两个方向。"迷失在中间"表明从长上下文中的检索退化是不均匀的，中间部分最弱。而 JetBrains 发现基于摘要的压缩降低了成本，但增加了13-15%的步骤。所以如果协调工作仍然是一个有边界的推理弧线，我宁愿保持该线程活跃，而不是反复将其压缩和重新加载。

我不想的是把"规划 → 实现 → 测试"当作一个无差别的实时对话。一旦协调者发出任务合约，下一阶段就需要一个硬边界。实现应该启动一个新循环：读取计划，读取交接内容，检查当前代码，做一个有边界的任务，写入轨迹，停止，然后从下一个循环重新开始。

这是我确定的部分。会话连续性在代码开始变动之前最有价值。

```
[一个实时协调会话]
  展开 spec → 综合完成定义 → 起草计划 → 对峙+协调
                                        ↓
                                    task_plan.json
                                    handoff.md
                                        ↓
  实现一个任务 → 提交+轨迹 → 验证门控 → 从新循环开始
```

Pi 之所以重要，是因为它让引擎来选择。引擎可以在规格和规划阶段保持一个实时协调会话，然后在执行时切换到新的有边界循环。

有趣的是协调者内部发生的事情。当模型在阶段之间切换时（比如用 Opus 做协调、用 Sonnet 做分派），引擎发送一个 `set_model` RPC。对话历史保持不变，模型改变了。同一个线程，不同的大脑。

这给了我真正需要模型多样性的地方：不同的模型在范围、完成定义和计划质量上争论，而不会丢弃之前的推理。一旦任务合约被冻结，合约而非对话就成为连续性机制。

## 会话即树

大多数代理框架将对话视为一个扁平列表。Pi 不是。在内部，Pi 的 SessionManager 将每个对话存储为一棵树。每个条目有一个 id 和一个 parentId。会话文件是仅追加的：条目永远不会被修改或删除。当你分支时，叶指针移动到一个更早的条目，下一条消息成为该条目的子节点。旧分支保持完整。

这是将 git 的数据模型应用于对话状态。会话文件就是对象存储。`getBranch()` 从任何条目走到根节点。`getTree()` 返回按时间戳排序子节点的完整树。`branch(fromId)` 等同于 `git checkout`。`branchWithSummary()` 等同于带有提交消息的 `git checkout -b`，解释了为什么你离开了旧分支。

```
root: 系统提示
  → 用户：展开功能范围
    → 助手：起草计划A
      → 用户：协调阻碍
        → 助手：更新计划
          → 用户：尝试不同的计划形式
            → 助手：计划B
                        ↗ branch(B.id)
```

树结构意味着没有东西会丢失。如果计划A风险太高，引擎可以从B分支并尝试计划B，而不破坏A尝试的历史记录。扩展可以遍历两个分支来了解尝试了什么。`branch_summary` 条目捕获了为什么旧路径被放弃，所以新分支以该上下文开始。

在 Arianna 中，这体现在两个地方。

**规划发散。** 完成定义和规划分支可以从同一个协调状态分叉，探索不同的视角，然后在整合时收敛。这是共享推理上下文的一个好用法，因为工作仍然是"决定应该发生什么"，而不是"去修改代码"。

```
协调会话（展开后的规格）
  ↘ DoD：正常路径视角
  ↘ DoD：失败路径视角
  ↘ DoD：集成视角
  → 整合完成定义
```

**多样性重试。** 如果规划走进死胡同，引擎可以从决策前的节点分支并切换模型。相同的推理历史，不同的规划者。对于实现重试，我再次想要相同的循环纪律：重新加载交接内容、失败轨迹和当前仓库状态，然后运行另一个有边界循环。

## 扩展作为控制面

Arianna 需要在不修改代理源代码的情况下向代理注入行为。轮次预算、循环检测、工具输出截断、上下文使用警告、结构化输出模式。这些是工厂的关注点，不是代理的关注点。

Pi 的扩展系统通过钩子处理这些：`before_agent_start`、`tool_call`、`turn_end`、`tool_result`、`before_provider_request`、`model_select`。Arianna 扩展（`arianna-extension.ts`）利用这些来：

- 强制执行每个节点的轮次预算（如果代理空转则终止）
- 检测操作循环（同一工具调用重复N次）
- 限制工具轮次预算（每轮最大工具调用次数）
- 用流水线上下文丰富系统提示
- 在过大的工具输出填满窗口之前截断它们
- 当上下文使用超过阈值时发出警告
- 为需要的节点注入结构化输出模式

替代方案是修改全局 Pi 状态的 RPC setter。扩展方案胜出，因为它是作用域化的：扩展按会话加载，不持久化到磁盘，不影响其他 Pi 进程。简而言之：多进程流水线中的全局状态修改是在自找麻烦。

## 图边界处的上下文交接

我在三月下旬进行的语义交接实验发现了 Arianna 上下文管理中真正的边界。当系统还在决定做什么时，实时协调会话是有帮助的。但它不会消除执行开始时交接的需求。

在 `reconcile_plan → implement` 的边界处，我不希望下一阶段依赖整个协调线程。我想要一个滚动的 `handoff.md` 产物，大约30行，包含两个部分："现在什么是真实的"和"下一步必须做什么"。小到足以适应任何上下文窗口，具体到足以避免重建税。

这不是会话模式的更便宜的备选方案。它本身就是重点。协调连续性在工作被定义时保护推理。交接文件是保护意图而不将过时推理拖入执行的边界对象。

这也与日记 #5 的新鲜上下文论点干净地连接起来。上下文窗口是工作记忆。交接文件是工作记忆之间的桥梁。

## 为什么这对工厂很重要

每个编码代理框架都想成为平台。Pi 不想。它想做一个好的子进程。这对于编排者与执行者分离的工厂来说，是正确的设计。

引擎决定记忆在哪里有帮助，在哪里有害。Pi 让我能在规格、完成定义和规划阶段保持一个实时的协调线程。它让我在想尝试不同的推理路径时分支该线程。它让我在工作转变为代码时启动一个新的执行循环。

这是我真正关心的功能列表：执行前的连续性，执行中的强边界。如果有什么的话，我预期这个边界随时间会进一步偏向新鲜执行者，而不是相反。Pi 给了我做这个选择的空间，而不需要把自己变成平台。

---

*获取新文章，请通过邮件订阅*
