1. 为什么需要给 Coding Agent 加一层工程控制
1.1 从“能跑”到“跑得稳”的鸿沟
过去一年,命令行里的 Coding Agent 从新鲜玩具变成了不少团队日常开发流程的一部分。你给它一个任务描述,它自己拆解、自己写代码、自己跑测试、自己修 bug,整个过程看起来非常顺畅。但真正把它放进一个需要持续交付的项目里,问题就暴露出来了:它跑到一半卡住了你不知道;它改错了文件你没法回滚;它连续调用了十几个工具之后上下文爆了,你只能从头再来。这些问题的本质不是模型能力不够,而是缺少一层工程控制。
Pi-Harness 要解决的就是这个问题。它不是一个新模型,也不是一个替代品,而是给 Pi Coding Agent 套上的一套“外骨骼”——让 Agent 的执行过程变得可观测、可恢复、可编排。你可以把它理解成给一个能力很强但行为不太稳定的实习生配了一个项目经理、一个版本控制员和一个流程引擎。实习生负责干活,项目经理负责盯着进度、记录每一步做了什么、出了问题能倒回去重来、多个任务之间能协调调度。
这套东西适合谁?如果你只是偶尔用 Coding Agent 写个小脚本,可能感受不到痛点。但如果你在以下场景里用过 Agent,那 Pi-Harness 的思路就值得仔细看看:让 Agent 在真实代码仓库里做多文件修改、让 Agent 跑完整的测试套件并根据失败结果自动修复、让多个 Agent 并行处理不同模块的任务、需要审计 Agent 每一步操作以便复盘。这些场景的共同特点是:执行链路长、状态变化多、出错代价高。没有控制层,你就是在裸奔。
1.2 可观测、可恢复、可编排到底指什么
这三个词听起来像产品经理的术语,但落到工程实现上,每一个都有非常具体的含义。
可观测不是简单地打日志。它要求 Agent 的每一次决策、每一次工具调用、每一次文件读写、每一次命令执行都被结构化地记录下来,并且这些记录之间要有因果关系。比如 Agent 决定修改utils.py是因为它先读了main.py发现了一个 import 错误,这个因果链要能被追溯。光有日志不够,还需要有指标——执行了多少步、每步耗时多少、工具调用成功率是多少、上下文窗口消耗了多少。这些数据能帮你判断 Agent 是在高效工作还是在原地打转。
可恢复的核心是状态快照和回滚能力。Agent 执行到第 15 步时崩溃了,你能不能从第 14 步的状态继续,而不是从第 1 步重来?Agent 做了一个错误的修改,你能不能精确回滚那一次文件变更而不影响之前的正确修改?这需要 Harness 在每一步执行前后对工作区状态做快照,并且能够根据执行历史重建任意一步的现场。这跟数据库的事务日志思路很像——每一步都是一个可回放的操作记录。
可编排解决的是多任务、多 Agent 的协调问题。一个复杂项目可能需要先让 Agent A 做代码分析,再让 Agent B 根据分析结果做重构,最后让 Agent C 跑测试验证。这些步骤之间有依赖关系,需要串行或并行调度。更进一步,多个 Agent 可能同时操作同一个仓库的不同部分,需要避免冲突。编排层要提供任务定义、依赖管理、并发控制和结果聚合的能力。
1.3 为什么是 Pi Coding Agent 而不是别的
Pi Coding Agent 本身的设计有几个特点让它特别适合加 Harness。第一,它的执行模型是基于步骤的——每一步做一个决策、执行一个动作、观察一个结果,这种离散的步骤结构天然适合做快照和回放。第二,它的工具调用接口是标准化的,Harness 可以在工具调用层做拦截和记录,不需要修改 Agent 本身的代码。第三,Pi 的上下文管理是显式的,Harness 可以监控上下文消耗并在必要时触发压缩或重置。
换句话说,Pi 提供了一个足够清晰的控制面,Harness 才能在这个面上做文章。如果 Agent 本身是一个黑盒,内部状态完全不可见,那 Harness 能做的事情就非常有限。这也是为什么 Pi-Harness 选择绑定 Pi 而不是做一个通用的 Agent 控制层——通用方案往往意味着对每个 Agent 都要做适配,而深度绑定一个 Agent 可以把控制粒度做得更细。
2. 可观测层的核心设计与落地细节
2.1 结构化事件流:不只是打日志
可观测层的基础是一个结构化的事件流。Agent 每执行一步,Harness 就产生一个事件对象,包含以下字段:
{ "event_id": "evt_20250115_001", "parent_id": "evt_20250115_000", "timestamp": 1736899200.123, "step_index": 15, "event_type": "tool_call", "tool_name": "file_edit", "tool_input": {"path": "src/utils.py", "old": "...", "new": "..."}, "tool_output": {"success": true, "bytes_written": 234}, "context_usage": {"tokens_used": 45200, "tokens_limit": 128000}, "duration_ms": 340, "agent_thought": "修复 import 错误,需要把 from utils import helper 改成 from src.utils import helper" }这个事件对象的设计有几个关键点。parent_id建立了事件之间的因果关系——当前事件是由哪个事件触发的。agent_thought记录了 Agent 在做这个动作之前的推理过程,这对于事后复盘至关重要。context_usage让你能实时看到上下文窗口的消耗趋势,提前预警。duration_ms帮你识别哪些操作是性能瓶颈。
我实测下来,事件流用 JSON Lines 格式落盘最方便——每行一个 JSON 对象,追加写入,不需要维护复杂的文件结构。查询的时候用jq或者写个简单的 Python 脚本就能做过滤和聚合。不要一上来就上数据库,文件系统在大多数场景下够用,而且更容易做快照和迁移。
2.2 因果链追踪:从“发生了什么”到“为什么发生”
光有事件流还不够,你需要能回答“Agent 为什么做了这个决定”。这需要把事件流组织成因果链。具体做法是:每个事件除了记录自己的parent_id,还要记录它读取了哪些前置事件的结果。比如 Agent 修改utils.py的事件,它的输入里包含了之前读取main.py的事件 ID 和读取utils.py的事件 ID。
这样当你想复盘“为什么 Agent 会做出这个修改”时,你可以沿着因果链往上追溯:修改 utils.py → 因为读了 main.py 发现 import 错误 → 因为跑了测试发现 main.py 报错 → 因为执行了测试命令。整条链路清晰可见。
实现上,Harness 需要在 Agent 的每一步决策时,把当前上下文里引用到的事件 ID 提取出来,作为当前事件的caused_by字段。这需要 Agent 的推理过程对 Harness 可见——Pi 的agent_thought字段正好提供了这个信息。如果 Agent 的推理是黑盒,那就只能靠工具调用的输入输出来推断因果关系,精度会差很多。
注意:因果链的粒度不要太细。如果每个 token 都记录因果关系,事件流会爆炸。我的经验是只在工具调用和关键决策点建立因果链接,中间的语言模型推理过程只记录摘要。
2.3 指标聚合与实时监控
事件流是原始数据,指标是聚合后的洞察。Harness 需要实时计算以下几类指标:
| 指标类别 | 具体指标 | 用途 |
|---|---|---|
| 执行效率 | 每秒步数、平均步耗时、工具调用成功率 | 判断 Agent 是否在高效工作 |
| 上下文健康 | 上下文使用率、压缩触发次数、重置次数 | 预警上下文溢出风险 |
| 错误模式 | 连续失败次数、重复操作次数、回滚次数 | 识别 Agent 陷入循环或方向错误 |
| 资源消耗 | 总 token 消耗、总工具调用次数、总执行时长 | 成本核算和容量规划 |
这些指标不需要复杂的监控系统,一个简单的内存聚合器加定期落盘就够了。关键是阈值告警——当连续失败次数超过 3 次,或者上下文使用率超过 85%,Harness 应该主动介入,而不是等 Agent 自己发现。
我踩过的一个坑是:一开始只记录了成功指标,没有记录失败和重试。结果 Agent 在一个循环里反复尝试同一个错误操作,从指标上看步数在增加、工具调用在增加,看起来“很忙”,但实际上没有任何进展。后来加了“重复操作检测”——如果连续 3 步的工具调用输入高度相似但输出都是失败,就触发告警并强制中断。这个机制救了好几次。
2.4 可视化:让非工程师也能看懂 Agent 在干什么
事件流和指标主要是给工程师看的。但很多时候你需要向非技术背景的人解释 Agent 做了什么、为什么花了这么长时间、问题出在哪里。这时候一个简单的可视化界面就很有价值。
不需要做复杂的 Dashboard。一个基于终端的实时视图就够了:左边显示当前步骤和 Agent 的思考摘要,中间显示工具调用流水,右边显示上下文使用率和关键指标。用 Python 的rich库或者 Node 的ink都能快速搭出来。关键是实时更新和颜色编码——绿色表示成功,黄色表示警告,红色表示错误。这样即使不懂技术的人也能一眼看出“现在卡住了”。
3. 可恢复机制:快照、回滚与断点续跑
3.1 状态快照的设计取舍
可恢复的前提是能在任意时间点捕获完整状态。Agent 执行过程中的状态包括:工作区文件状态、对话历史、工具调用记录、上下文窗口内容。这四者缺一不可。
工作区文件状态用 Git 来管理是最自然的选择。Harness 在每一步执行前自动创建一个 Git stash 或者临时 commit,记录当前工作区的完整状态。这样回滚的时候只需要git checkout到对应的 commit 就行。但这里有个性能问题:如果每一步都做全量 commit,仓库会迅速膨胀。我的做法是只在关键步骤做 commit——比如每次文件修改后、每次测试运行后。中间的纯读取操作不需要快照。
对话历史和工具调用记录用事件流本身就能重建。因为事件流是追加写入的,每个事件都有完整的输入输出,从事件流回放就能恢复出任意一步的对话状态。上下文窗口内容稍微麻烦一点,因为上下文是动态压缩的,同一个对话在不同时间点的上下文窗口内容可能不同。解决方案是在每次上下文压缩时记录压缩后的完整上下文快照,回放时从最近的快照开始重建。
实操心得:快照的存储位置很重要。不要放在工作区目录里,否则 Agent 可能会误操作快照文件。我一般放在
~/.pi-harness/snapshots/<session_id>/下面,按步骤编号命名。每个快照是一个目录,包含workspace.tar.gz(工作区文件)、context.json(上下文快照)、events.jsonl(截至该步骤的事件流)。
3.2 回滚策略:精确回滚与批量回滚
回滚分两种场景。第一种是精确回滚——Agent 在第 15 步做了一个错误的文件修改,你想只撤销这一步的修改,保留第 14 步之前的所有正确修改。这需要 Harness 能识别出第 15 步具体改了哪些文件、改了哪些行,然后做反向操作。如果第 15 步是纯文件修改,用git revert对应的 commit 就行。如果第 15 步还包含了其他副作用(比如安装了依赖、启动了服务),那就需要更复杂的补偿逻辑。
第二种是批量回滚——Agent 从第 10 步开始方向就错了,你想回到第 9 步的状态重新来。这就是直接git checkout到第 9 步的快照,然后从事件流里截断第 10 步之后的所有记录,重新开始执行。
两种场景的实现难度差别很大。精确回滚需要 Harness 理解每一步的语义,知道哪些操作是可逆的、哪些是不可逆的。批量回滚只需要快照和恢复,实现简单但代价是丢失中间步骤的所有工作。我的建议是:默认提供批量回滚,精确回滚作为高级功能,只在 Agent 明确标记了“可逆操作”时才启用。
3.3 断点续跑:从崩溃中恢复
Agent 执行过程中可能因为各种原因中断——进程被杀、网络超时、上下文溢出、工具调用卡死。断点续跑的目标是:从最后一个成功步骤继续执行,而不是从头开始。
实现上,Harness 需要维护一个“执行游标”,记录当前执行到哪一步、下一步应该做什么。每次成功执行一步后更新游标并持久化。崩溃恢复时,读取游标,从对应的快照恢复工作区和上下文,然后从下一步继续。
这里有个细节:如果崩溃发生在某一步的执行过程中(比如文件写到一半),那这一步的状态是不完整的。Harness 需要能识别这种“脏状态”并回退到上一步的干净快照。我的做法是给每一步的执行加一个“事务标记”——执行前写入step_15_in_progress标记,执行成功后改为step_15_completed。恢复时如果发现标记是in_progress,就回退到第 14 步重新执行第 15 步。
# 简化的断点续跑逻辑 def resume_from_checkpoint(session_id): cursor = load_cursor(session_id) last_step = cursor['last_completed_step'] if cursor['current_step_status'] == 'in_progress': # 回退到上一步 restore_snapshot(session_id, last_step) next_step = last_step + 1 else: restore_snapshot(session_id, last_step) next_step = last_step + 1 return next_step实测下来,断点续跑能节省大量时间。一个跑了 40 步、耗时 15 分钟的任务,如果第 38 步崩溃,没有断点续跑就要重跑 15 分钟,有了断点续跑只需要重跑最后 2 步,大概 1 分钟。对于需要反复调试 Agent 行为的场景,这个效率提升非常明显。
3.4 快照的清理与存储优化
快照不能无限增长。一个跑了 100 步的任务,如果每步都做全量快照,存储占用会非常可观。我的清理策略是:
- 保留最近 10 步的完整快照
- 每 10 步保留一个“里程碑快照”
- 超过 50 步之前的快照只保留里程碑
- 任务完成后只保留最终快照和里程碑快照
这样存储占用从 O(n) 降到 O(n/10 + 10),对于大多数任务来说完全可接受。另外,快照可以用增量方式存储——只记录相对于上一个快照的文件变更,恢复时逐层应用。但这会增加恢复的复杂度,我一般只在存储压力大的时候才启用。
4. 可编排层:任务定义、依赖管理与并发控制
4.1 任务定义:用声明式配置描述工作流
可编排的第一步是让用户能清晰地定义“我要 Agent 做什么”。最直观的方式是声明式配置——用户写一个 YAML 或 JSON 文件,描述任务步骤和依赖关系,Harness 负责调度执行。
name: refactor-auth-module steps: - id: analyze agent: pi prompt: "分析 src/auth/ 目录下的代码,找出所有硬编码的密钥和配置" outputs: [analysis_report] - id: refactor agent: pi depends_on: [analyze] prompt: "根据分析报告,将硬编码的密钥和配置提取到环境变量中" inputs: [analysis_report] outputs: [refactored_files] - id: test agent: pi depends_on: [refactor] prompt: "运行测试套件,确保重构没有破坏现有功能" inputs: [refactored_files] outputs: [test_results] - id: fix agent: pi depends_on: [test] condition: "test_results.failed > 0" prompt: "根据测试失败信息修复问题" inputs: [test_results]这个配置描述了一个四步工作流:分析、重构、测试、修复。depends_on定义了执行顺序,condition定义了条件执行——只有测试失败时才执行修复步骤。inputs和outputs定义了步骤之间的数据传递。
这种声明式配置的好处是:工作流本身是可版本控制的、可复用的、可审计的。你可以把常用的工作流保存成模板,下次直接引用。你也可以在配置里加注释,解释每一步的意图。相比让 Agent 自己决定下一步做什么,声明式配置给了你更多的控制权。
4.2 依赖管理与执行调度
有了任务定义,下一步是调度执行。调度器需要解决几个问题:如何确定执行顺序、如何处理并行、如何处理失败。
执行顺序由依赖关系决定。最简单的做法是拓扑排序——把任务定义转换成有向无环图,然后按拓扑序执行。但实际场景中往往需要更灵活的调度:有些步骤可以并行执行(比如两个独立的代码分析任务),有些步骤必须串行(比如先重构再测试)。
我的做法是给每个步骤加一个parallel_group标签,同一组内的步骤可以并行执行,不同组之间按依赖关系串行。调度器维护一个就绪队列——所有依赖已满足且未被调度的步骤进入就绪队列,调度器从队列中取出步骤分配给可用的 Agent 实例执行。
def schedule(steps): ready_queue = [s for s in steps if not s.depends_on] running = {} completed = set() while ready_queue or running: # 启动就绪步骤 for step in ready_queue: if can_start(step, running): agent = acquire_agent() future = executor.submit(run_step, step, agent) running[step.id] = future ready_queue.remove(step) # 等待任意一个完成 done, _ = wait(running.values(), return_when=FIRST_COMPLETED) for future in done: step_id = get_step_id(future) result = future.result() completed.add(step_id) del running[step_id] # 将新就绪的步骤加入队列 for s in steps: if s.id not in completed and s.id not in running: if all(d in completed for d in s.depends_on): ready_queue.append(s)这个调度逻辑不复杂,但有几个细节需要注意。第一,can_start要检查资源约束——比如同时运行的 Agent 数量上限、某些步骤需要独占资源。第二,失败处理——如果一个步骤失败,依赖它的步骤应该被标记为“阻塞”而不是“失败”,这样用户可以只重跑失败步骤及其下游。第三,超时处理——每个步骤应该有超时限制,超时后强制终止并标记为失败。
4.3 并发控制与冲突避免
多个 Agent 并行执行时,最大的风险是冲突。两个 Agent 同时修改同一个文件,后写的会覆盖先写的。两个 Agent 同时跑测试,可能因为端口冲突或临时文件冲突而互相干扰。
解决冲突的第一道防线是任务划分——在定义工作流时,尽量让不同 Agent 操作不同的文件或目录。如果做不到,就需要在 Harness 层做锁管理。我的做法是给每个文件路径维护一个读写锁:Agent 在修改文件前必须获取写锁,读取文件前获取读锁。锁的粒度可以到文件级,也可以到目录级。
class FileLockManager: def __init__(self): self.locks = {} # path -> (read_count, write_owner) def acquire_write(self, path, agent_id): if path in self.locks: read_count, write_owner = self.locks[path] if write_owner is not None or read_count > 0: raise LockConflict(f"{path} is locked") self.locks[path] = (0, agent_id) def release_write(self, path, agent_id): if path in self.locks and self.locks[path][1] == agent_id: del self.locks[path]锁管理会增加复杂度,但能避免很多诡异的问题。我遇到过两个 Agent 同时修改package.json,结果一个 Agent 添加的依赖被另一个 Agent 的修改覆盖了,导致后续步骤全部失败。加了锁之后这类问题就消失了。
注意:锁的粒度不要太细。如果每个文件都加锁,管理开销会很大。我的经验是按目录加锁——同一个目录下的文件共享一个锁。这样既避免了大部分冲突,又不会让锁管理成为瓶颈。
4.4 结果聚合与工作流输出
工作流执行完成后,Harness 需要把各个步骤的输出聚合起来,形成一个完整的结果报告。这个报告应该包含:每个步骤的执行状态、耗时、关键输出、产生的文件变更、遇到的错误和警告。
聚合的方式取决于工作流的定义。如果步骤之间是简单的线性依赖,聚合就是按顺序拼接各步骤的输出。如果有并行分支,聚合需要合并各分支的结果。我一般用 JSON 格式输出最终报告,结构如下:
{ "workflow": "refactor-auth-module", "status": "completed", "total_duration_ms": 45000, "steps": [ {"id": "analyze", "status": "completed", "duration_ms": 12000, "output": "..."}, {"id": "refactor", "status": "completed", "duration_ms": 18000, "output": "..."}, {"id": "test", "status": "completed", "duration_ms": 10000, "output": "..."}, {"id": "fix", "status": "skipped", "reason": "test passed"} ], "artifacts": ["src/auth/config.py", "src/auth/service.py"], "metrics": {"total_tokens": 85000, "tool_calls": 42} }这个报告可以直接作为 CI/CD 流水线的输入,也可以存档供后续审计。关键是结构要稳定——字段名和类型不要随意变动,否则下游的解析逻辑会崩。
5. 实操中踩过的坑与排查技巧
5.1 上下文溢出:最隐蔽的杀手
上下文溢出是 Coding Agent 最常见的失败模式,也是最难排查的。Agent 不会直接报错说“上下文满了”,而是会表现出各种奇怪的行为:开始重复之前的操作、忽略最近的指令、输出质量突然下降。如果你没有监控上下文使用率,很难定位到根因。
我的排查流程是:先看事件流里context_usage的变化曲线。如果发现使用率在某个步骤后突然飙升到 90% 以上,那基本可以确定是上下文问题。然后看那个步骤做了什么——通常是读取了一个大文件或者执行了一个输出很长的命令。解决方案有两种:一是让 Agent 在读取大文件时做摘要而不是全文加载,二是触发上下文压缩,把早期的对话历史压缩成摘要。
Harness 可以在上下文使用率超过阈值时自动触发压缩。压缩策略我一般用“保留最近 N 轮对话 + 早期对话摘要”的方式。N 的取值取决于任务复杂度,简单任务 N=5 就够,复杂任务可能需要 N=10。压缩后的上下文要重新计算 token 数,确保不会再次溢出。
5.2 工具调用死循环:识别与中断
Agent 陷入死循环的表现是:连续多步调用同一个工具,输入略有不同但输出都是失败,Agent 的思考内容开始重复。这种情况如果不及时中断,会浪费大量 token 和时间。
识别死循环的简单方法是计算连续失败次数。如果连续 3 次工具调用都返回失败,且失败原因相似,就触发告警。更精确的方法是计算工具调用输入的相似度——如果连续 3 次调用的输入相似度超过 80%,即使没有失败,也可能是死循环。
中断死循环后,Harness 应该给 Agent 一个“重置提示”——把当前状态和失败历史总结一下,让 Agent 重新思考方向。我试过直接中断不給提示,Agent 恢复后往往还会走回老路。给了重置提示后,Agent 改变策略的概率明显提高。
5.3 快照恢复失败:常见原因与修复
快照恢复失败通常有几个原因:快照文件损坏、Git 仓库状态不一致、上下文快照与事件流不匹配。排查时先检查快照文件的完整性——workspace.tar.gz能不能正常解压,context.json能不能正常解析。如果快照文件没问题,再检查 Git 状态——有时候 Agent 在快照之后做了git reset或git rebase,导致快照对应的 commit 不存在了。
修复方法是:如果快照文件损坏,回退到上一个可用的快照;如果 Git 状态不一致,用git reflog找到对应的 commit 并恢复;如果上下文不匹配,从事件流重新构建上下文。最坏的情况是快照完全不可用,那就只能从头开始,但这种情况很少见——只要快照是定期做的,总有一个可用的。
5.4 并发冲突:锁超时与死锁处理
并发场景下最常见的问题是锁超时和死锁。锁超时是指一个 Agent 获取了锁但长时间不释放,导致其他 Agent 一直等待。死锁是指两个 Agent 互相等待对方持有的锁。
处理锁超时的策略是给锁加超时时间——如果 Agent 持有锁超过 N 秒还没有释放,强制释放并标记该 Agent 为异常。N 的取值取决于操作类型,文件修改一般 30 秒够,跑测试可能需要 5 分钟。死锁的预防方法是给锁加顺序——所有 Agent 必须按固定顺序获取锁,比如按文件路径的字母序。这样就不会出现循环等待。
def acquire_locks(paths, agent_id, timeout=30): sorted_paths = sorted(paths) # 固定顺序避免死锁 acquired = [] try: for path in sorted_paths: lock_manager.acquire_write(path, agent_id, timeout) acquired.append(path) except LockConflict: for path in acquired: lock_manager.release_write(path, agent_id) raise这个简单的排序就能避免大部分死锁问题。我实测下来,加了排序之后死锁发生率从每周几次降到几乎为零。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent 重复操作 | 上下文溢出或死循环 | 检查 context_usage 和连续失败次数 | 触发压缩或中断重置 |
| 快照恢复失败 | 快照损坏或 Git 状态不一致 | 检查快照文件完整性和 git reflog | 回退到上一个可用快照 |
| 并发冲突 | 锁超时或死锁 | 检查锁持有时间和获取顺序 | 加超时和排序 |
| 执行速度突然变慢 | 工具调用阻塞或上下文过大 | 检查 duration_ms 和 token 消耗 | 优化工具调用或压缩上下文 |
| 输出质量下降 | 上下文污染或模型退化 | 检查最近的事件流和上下文内容 | 重置上下文或回退到早期快照 |
这张表是我在实际运维中总结出来的,覆盖了 80% 以上的常见问题。遇到新问题时,先对照这张表排查,大部分情况都能快速定位。
6. 从单 Agent 到多 Agent 的扩展思路
6.1 多 Agent 协作的模式选择
当任务复杂度超过单个 Agent 的处理能力时,就需要多个 Agent 协作。常见的协作模式有三种:流水线模式、评审模式和竞争模式。
流水线模式是最简单的——Agent A 的输出作为 Agent B 的输入,依次传递。适合有明确阶段划分的任务,比如“分析→设计→实现→测试”。评审模式是一个 Agent 执行、另一个 Agent 审查,审查不通过就打回重做。适合对质量要求高的场景。竞争模式是多个 Agent 同时执行同一个任务,然后选最好的结果。适合创意类任务,但成本较高。
Pi-Harness 的编排层可以支持这三种模式。流水线模式用depends_on就能表达。评审模式需要加一个review步骤,它的输入是执行步骤的输出,输出是“通过”或“打回”。竞争模式需要加一个select步骤,从多个并行执行的输出中选一个。
6.2 Agent 间的通信与状态共享
多 Agent 协作的核心问题是通信。Agent 之间不能直接对话,必须通过 Harness 中转。Harness 维护一个共享状态空间,每个 Agent 可以读写。写入时加锁,读取时获取快照。
共享状态空间的设计要避免两个问题:一是状态爆炸——所有 Agent 的所有输出都往里面塞,很快就不堪重负。二是状态污染——一个 Agent 的错误输出被另一个 Agent 当成正确输入。我的做法是给共享状态加命名空间和版本号。每个 Agent 只能读写自己命名空间下的状态,跨命名空间访问需要显式声明。版本号用于检测状态是否被意外修改。
6.3 多 Agent 场景下的可观测性挑战
多 Agent 场景下,事件流的复杂度会指数级上升。单个 Agent 的事件流是一条线,多个 Agent 的事件流是一张网。你需要能区分哪些事件属于哪个 Agent、哪些事件是跨 Agent 交互产生的。
我的做法是给每个事件加agent_id字段,跨 Agent 交互的事件加cross_agent标记。可视化的时候用不同颜色区分不同 Agent,用连线表示跨 Agent 交互。这样即使有 5 个 Agent 同时运行,你也能看清楚谁在干什么、谁在等谁。
排查多 Agent 问题时,关键是找到“瓶颈 Agent”——那个拖慢了整个工作流的 Agent。通过对比各 Agent 的步耗时和等待时间,很容易定位到瓶颈。然后针对瓶颈 Agent 做优化——可能是给它更多的上下文预算,可能是简化它的任务,也可能是给它换一个更强的模型。
7. 部署与运维的实战建议
7.1 本地开发与远程执行的取舍
Pi-Harness 可以跑在本地开发机上,也可以跑在远程服务器上。本地跑的好处是文件访问快、调试方便、不需要网络。远程跑的好处是资源充足、可以长时间运行、不占用本地机器。
我的建议是混合模式:日常开发和调试在本地跑,长时间运行的任务和需要大量资源的任务在远程跑。Harness 的配置里加一个execution_target字段,指定任务在哪里执行。本地和远程共享同一套事件流格式和快照格式,这样任务可以在本地和远程之间迁移。
远程执行时要注意文件同步——Agent 在远程修改的文件需要同步回本地。我一般用rsync做增量同步,每次任务完成后同步一次。如果任务执行时间长,可以定期同步中间结果。
7.2 资源限制与成本控制
Coding Agent 的资源消耗主要是 token 和计算时间。不加限制的话,一个复杂任务可能消耗几十万 token,成本相当可观。Harness 需要提供资源限制机制:每个任务的最大 token 数、最大执行时间、最大工具调用次数。超过限制时强制终止并报告。
成本控制的另一个手段是缓存。很多操作是重复的——比如读取同一个文件、执行同一个命令。Harness 可以缓存这些操作的结果,Agent 再次请求时直接返回缓存,不消耗 token。缓存的失效策略要小心——文件修改后对应的缓存要失效,否则 Agent 会拿到过时的数据。
7.3 日志轮转与存储清理
事件流和快照会持续占用存储。一个活跃的 Harness 实例每天可能产生几百 MB 的事件流和快照。需要定期清理:事件流保留最近 30 天,快照保留最近 100 个任务。清理策略要可配置,不同团队的需求不同。
日志轮转用标准的logrotate就行,按天切割、压缩旧文件、删除超过保留期的文件。快照清理稍微复杂一点,因为快照之间有依赖关系——删除一个快照可能影响其他快照的恢复。我的做法是只删除“里程碑快照”之外的快照,里程碑快照永久保留。里程碑快照的选取标准是:任务开始、任务完成、关键步骤完成。
7.4 安全边界:Agent 能做什么、不能做什么
Coding Agent 在真实仓库里操作,安全边界必须清晰。Harness 需要定义 Agent 的权限范围:能访问哪些目录、能执行哪些命令、能修改哪些文件。超出范围的操作直接拒绝。
权限控制用白名单机制最安全。配置文件里列出允许的目录和命令,Agent 的操作必须匹配白名单才能执行。白名单要尽量窄——只开放必要的权限,不要图省事开放整个仓库。我见过因为开放了.git目录导致 Agent 误删分支的案例,教训很深刻。
另外,敏感文件要加保护——.env、密钥文件、证书文件等,Agent 只能读取不能修改,或者完全禁止访问。Harness 在文件操作层做拦截,匹配到敏感文件路径时直接拒绝。
8. 我个人的一些实践体会
Pi-Harness 这套东西我用了大概三个月,从最初的简单日志记录到现在比较完整的控制层,中间迭代了很多次。最大的体会是:可观测性不是锦上添花,而是必需品。没有可观测性,你根本不知道 Agent 在干什么,出了问题只能靠猜。有了可观测性,大部分问题都能在发生前预警,发生中定位,发生后复盘。
可恢复性的价值在长任务上体现得最明显。一个跑 30 分钟的任务,如果第 25 分钟崩溃了,没有恢复机制就要重跑 30 分钟,有了恢复机制只需要重跑 5 分钟。这个效率差异在频繁调试 Agent 行为的时候尤其重要。
可编排性目前用得还比较少,主要是在多模块重构的场景下。但我觉得这是未来的方向——单个 Agent 的能力有上限,多个 Agent 协作能突破这个上限。编排层的成熟度决定了多 Agent 协作的上限。
最后分享一个小技巧:Harness 的事件流不要只用来做监控,还可以用来做训练数据。Agent 的成功执行轨迹是很好的微调数据,失败轨迹是很好的负样本。我最近在尝试用事件流里的成功轨迹做 few-shot 示例,效果比随机选示例好很多。这个方向后续还可以继续挖。