如何实现高质量的 AI Code Review
第 2 章 · 80–100 分钟

从 Prompt 到 Harness:整体架构与核心思路

沿一次真实审查请求拆开入口、状态机、批次、MCP、Agent 与结果验证的职责。

查看 Markdown 版本

一个 Prompt 为什么控制不了完整审查

设想一条很长的指令:先检查环境,再读取全部改动;按文件依赖分组,每组审查后提交结构化结果;高风险问题要复核;所有批次完成后检查遗漏,最后生成总结。模型读懂这段话并不困难。困难在于任务运行几十分钟后,系统怎样知道它真的完成了每一步。

如果模型直接回复“已检查所有文件”,这句话是自我报告,不是完成证据。它可能跳过后段文件,可能把两个步骤合并,也可能在上下文变长后只给概括性意见。Prompt 可以表达期望,却无法独立保存事务状态、核对文件集合、阻止越级调用、恢复中断任务或让多个执行者共享进度。

这不意味着 Prompt 没用。Prompt 负责把当前任务、规则、上下文和输出格式告诉模型。问题在于我们曾让它同时承担四种职责:说明目标、保存进度、控制流程、验收结果。后面三种职责更适合确定性程序。

Harness 的作用,就是把这些工程职责从 Prompt 中拿出来。它是围绕模型的一套运行支架:准备上下文,暴露工具,保存状态,限定权限,验证结果,处理失败并记录指标。模型仍负责代码语义分析;系统不再把完整流程寄托在模型的工作记忆里。

Anthropic 的工程文章区分了 workflow 与 agent:Workflow 由预定义代码路径编排模型和工具,Agent 则由模型动态决定过程与工具使用。[1] 本课程案例是混合结构。宏观审查路径由状态机规定,因此更像 workflow;在每个批次内部,Agent 可以搜索仓库、阅读关联文件并决定分析方法。这个区分能避免一个误解:采用 Agent,不等于把所有控制权交给模型。

先画出系统边界

一条本地 AICR 请求至少涉及四层。为了公开教学,下面把与业务无关的入口合并为“CLI/调用方”,保留实现中的核心服务和工具名。

flowchart LR
  U["开发者 / 生码 Agent"] --> C["CLI 或调用入口"]
  C --> P["Planner / CR Server"]
  P --> DB["Session / Batch / File / Issue"]
  P --> M["MCP Server"]
  M --> A["审查 Agent"]
  A --> W["本地仓库与工具"]
  A --> M
  M --> P
  P --> V["Verifier / Extension Checks"]
  V --> DB
  DB --> S["汇总与反馈"]
图中关系须结合相邻正文与来源理解。

调用方负责发起任务和携带必要身份、仓库、分支或 MR 信息。Planner 负责创建审查计划、推进状态、切分批次和决定下一步。MCP Server 把后端能力转换为模型可调用的工具。Agent 阅读当前 Prompt,在工作区内分析代码,再提交结构化结果。Verifier 和扩展检查不相信“我完成了”,而是检查结果格式、工作量和后续复核条件。

这里的 Server 不是“更大的模型”。它是确定性控制面。Agent 也不是数据库状态的所有者。只要这条边界明确,系统就能回答三个重要问题:谁可以推进状态,谁可以生成问题,谁可以判定流程完成。

MCP 官方规范采用 Host–Client–Server 架构。Host 管理多个 Client、权限和生命周期;每个 Client 与一个特定 Server 保持 1:1 有状态会话;Server 暴露 tools、resources 和 prompts,并通过能力协商声明支持什么。[2] 课程案例没有机械复制规范名词,但遵守相同方向:模型侧通过受控工具访问后端能力,而不是直接修改审查数据库。

一次请求怎样穿过系统

下面沿一条正常请求走一遍。先只看交接,不急着记状态编号。

入口:创建或复用任务

开发者在本地工作区触发 AICR。入口校验参数,识别仓库和改动范围,然后调用 Setup。远端模式还会处理异步任务、仓库准备和回调,第 8 章再展开。

入口不应每次都无条件创建新任务。同一份改动可能因为页面刷新、网络重试或外部平台重复回调而被多次触发。系统需要内容哈希和会话状态来判断:复用已完成结果、继续未完成 Session,还是创建新 Session。幂等从入口开始,不能等 Agent 已经运行后再补。

Setup:把模糊改动变成可审查对象

setupSession 做的工作比“调用模型”更基础:创建或更新 Session,收集改动文件,过滤不需要审查的类型,计算内容哈希,尝试复用历史文件结果,再调用批次分配器生成 Batch。完成后,Session 进入 ready 状态,current_step 仍表示未正式开始,allowed_next_step 指向 0。

文件过滤必须可追溯。依赖锁文件、生成产物、二进制资源或超大文件可能被排除,但排除不能悄悄发生。否则最终“完成 100% 审查”只覆盖了过滤后的集合,读者却以为覆盖了 MR 全部文件。好的 Setup 会保存原始集合、有效集合和排除原因。

历史复用也有边界。文件路径相同不代表内容相同;内容相同但规则版本改变,也未必可以复用。当前实现使用内容哈希和 Session 状态参与判断。工程上还应考虑模型、Prompt、规则与工具版本,把它们纳入结果有效性的定义。

Planner:只下发当前任务

Setup 完成后,Planner 不把全流程一次性塞给 Agent。它读取 allowed_next_step,调用相应处理函数,生成当前阶段的 Prompt。Agent 每次拿到的不是“请自行完成一切”,而是明确的下一项工作和结构化提交要求。

在批次审查阶段,Planner 还会读取当前 current_batch_index,只提供当前 Batch 的文件、规则与上下文。完成一个 Batch 后,服务端才决定继续下一个、进入扩展检查、回退重审或生成汇总。

MCP Bridge:把状态转换为工具契约

模型无法直接调用 Egg Service。MCP Server 注册两个核心工具:

  • cr_shift_left_batch_review:根据 session_id 领取或继续当前允许的审查任务。
  • cr_shift_left_post_batch_results:提交 batch_results,扩展阶段还可携带 extension_results

工具名不是重点,契约才是。领取工具只需要 Session ID,服务端根据状态返回当前 Prompt;提交工具要求每条结果包含文件、行号、评分、分类、内容及可选代码片段。Agent 不传“下一步应该是 3”,也不直接把 current_batch_index 加一。状态推进权留在后端。

Agent:在批次内部执行语义工作

Agent 收到当前批次后读取改动,必要时搜索定义、调用方和测试。它可以根据代码决定先看哪个文件,也可以用工具补足上下文。这是模型应有的自由:在受控工作单元里选择分析路径。

Agent 产出的是候选 Issue,而不是最终真理。一个候选问题至少要回答:问题在哪,现有代码是什么,风险为何成立,严重度和类别是什么,怎样改进。高质量评论还要区分“必须修复的正确性问题”和“可选的维护建议”。这些字段让后端能够检查完整性,也让后续指标有稳定对象。

提交与验证:先验收,再推进

Agent 调用提交工具后,Processor 根据当前状态解释 payload。如果状态是 2,batch_results 会进入验证;如果缺少预期结果,实现会降级回 Step 1,而不是假装完成。验证通过后保存 Issue、更新 Batch 和 Session,再决定下一个状态。

验证不等同于让另一个模型说“看起来不错”。可确定检查优先写成代码:字段是否完整,文件是否属于当前批次,行号是否合理,是否重复提交,审查耗时与问题密度是否落在配置范围,包含高风险修复建议时字段是否齐全。模型复核适合处理语义相关性,不能替代这些机械约束

汇总:从多批结果生成可读结论

所有批次和必要扩展检查结束后,状态进入 3。汇总从数据库中的结构化 Issue 生成,而不是要求模型回忆此前对话。它可以按风险和类别组织问题,链接文件位置,说明覆盖和排除项,并为指标与反馈保留 Issue ID。

sequenceDiagram
  participant U as 调用方
  participant S as Setup/Planner
  participant D as 状态数据
  participant M as MCP Server
  participant A as Agent
  participant V as Verifier
  U->>S: 创建审查请求
  S->>D: 创建 Session、File、Batch
  U->>M: batch_review(session_id)
  M->>S: processSession()
  S->>D: 读取 allowed_next_step
  S-->>M: 当前阶段 Prompt
  M-->>A: 当前批次与规则
  A->>A: 搜索、阅读、分析
  A->>M: post_batch_results(...)
  M->>S: 结构化结果
  S->>V: 验证当前提交
  V-->>S: pass / retry / extension
  S->>D: 保存 Issue 并推进状态
  S-->>A: 下一批、扩展检查或汇总
图中关系须结合相邻正文与来源理解。

这张图不是实现证据。证据来自 setup.tsprocessor.ts、两个 MCP Tool、verifier.ts 与数据模型。图的作用是把它们组织成一条可复述的请求链。

五个核心数据对象

只看服务调用容易误以为状态都存在 Prompt 里。真正让任务可恢复的是持久对象。

Session:一次审查的总账

Session 保存顶层生命周期:执行模式、状态、当前步骤、下一步许可、当前批次、文件数、行数、批次数、完成数、问题数、开始和结束时间。它还是外部请求与内部对象的关联点。

Session 不应该塞入所有结果详情。它保存汇总和指针,具体文件、批次和问题使用独立对象。这样更新一个 Issue 不必重写整份会话,也便于按批次恢复。

File:审查范围的快照

File 对象记录路径、diff 规模、内容哈希、过滤或复用状态,以及它属于哪个 Session。它回答“本次到底要审什么”。如果只把文件列表写在 Prompt 中,任务中断后很难证明哪些文件已经处理。

内容哈希让系统区分“同一路径的新内容”和“可以复用的相同内容”。但哈希只证明字节关系,不能证明规则环境相同。课程后续会把版本维度纳入评估讨论。

Batch:受控工作单元

Batch 把多个 File 组织为一次 Agent 可以处理的工作量。当前实现主要以约 800–1200 diff 行作为理想范围,同时结合语义分组、拆分、合并、去重和完整性检查。旧设计材料曾出现 800–1500,这属于版本差异,不应混成一个固定行业阈值。

Batch 要保存索引、文件集合、工作量、状态、重试与结果统计。它让系统可以说“第 4 批失败,重新执行第 4 批”,而不是整次审查从头开始。

Issue:可反馈、可度量的问题单元

Issue 是一条候选审查问题。位置、评分、类别、描述、现有代码、改进代码、采纳状态和有效性围绕它组织。后续采纳率的分子分母,本质上是在筛选 Issue。

Issue 必须有稳定标识。开发者拒绝一条评论、最终代码采纳建议、测试发现漏报,都需要关联到具体对象。若结果只存在一段汇总文本中,数据闭环就无法建立。

PendingTask 与 MemoryRule:跨执行与跨会话状态

远端模式使用 PendingTask 表达排队、认领、执行、回调和终态;MemoryRule 保存由历史反馈形成的规则。前者让一次任务跨进程恢复,后者让不同 Session 共享经过筛选的经验。第 8 章和第 5/7 章分别展开。

erDiagram
  SESSION ||--o{ FILE : contains
  SESSION ||--o{ BATCH : plans
  BATCH }o--o{ FILE : groups
  BATCH ||--o{ ISSUE : produces
  SESSION ||--o{ ISSUE : aggregates
  SESSION ||--o| PENDING_TASK : executes_as
  MEMORY_RULE }o--o{ ISSUE : learned_from
图中关系须结合相邻正文与来源理解。

ER 图省略了真实表名、组织字段和大量实现列,只表达教学关系。关系不是说每个数据库都必须这样设计;它说明这些对象分别解决范围、工作量、结果、异步生命周期和跨任务反馈。

allowed_next_step:把建议变成许可

当前主链是 0 → 1 → 2 → 2.x → 3。它不是进度百分比,而是服务端允许执行的动作类型。

状态含义谁执行主要工作进入下一状态前的证据
0前置与环境检查Server 读取配置,Agent 按提示确认环境Session 可运行,配置和身份满足条件
1下发并执行当前批次审查Server 选 Batch,Agent 分析代码有当前批次及明确提交契约
2接收并验证批次结果Server / Verifier结构、范围和质量检查通过,或得到重试理由
2.x执行条件化扩展检查Agent + 扩展检查编排器recheck、记忆、过滤等当前所需检查完成
3生成最终汇总Server / Agent 按持久结果汇总所有必要批次和扩展检查已达到终态

current_step 表示当前记录到的阶段,allowed_next_step 表示允许的下一动作。二者分开能表达“刚完成 Step 1,下一次提交必须按 Step 2 解释”。如果只保存一个 status=running,服务端收到 payload 时不知道它是批次结果、扩展结果还是重复请求。

下面是 processor.ts 的简化节选。无关日志、监控和字段已移除,状态值与分发关系保持不变。

typescript
// 简化节选:状态由服务端读取,调用方不能自选处理分支
const session = await getSession(sessionId);

switch (session.allowed_next_step) {
  case "0":
    return executeStep0(session);
  case "1":
    return executeStep1(session);
  case "2":
    return handleStep2(session, batchResults);
  case "2.x":
    return handleStep2x(session, extensionResults);
  case "3":
    return executeStep3(session);
  default:
    return { error: "INVALID_STEP" };
}

这个 switch 的价值不在语法,而在所有权:Processor 从数据库读取状态,再选择处理函数。Agent 即使在 Prompt 中写“请进入汇总”,也不能绕过未完成的批次。

正常状态循环

一次 Batch 的常见循环是 1 → 2 → 1。Step 1 下发任务并把许可推进到 2;Agent 提交结果;Step 2 验证通过,若还有批次就增加索引并回到 1。所有批次完成后,系统可能进入 2.x3

缺失结果时的降级

如果状态为 2,调用却没有 batch_results,当前实现不会生成空结果并继续。handleStep2 会把 Batch 状态回退为 pending,把 allowed_next_step 设回 1,再重新下发审查。2.x 缺少扩展结果时也会清理扩展状态并回退。

这是一个实用的失败处理原则:payload 与当前状态不匹配时,选择可解释的恢复点。恢复动作必须幂等,避免重复保存 Issue 或跳过 Batch。

扩展状态为什么写成 2.x

扩展检查不是单个固定步骤。它可以根据问题、模式和配置进入 recheck、negative memory、接口契约、任务说明、positive memory、filter 或 fix。把它统一标成 2.x,再由 extension_check_state 保存内部子状态,能让主链保持稳定,同时允许检查序列演进。

这种设计也有代价。日志和指标必须同时记录主状态与子状态,否则所有扩展耗时都堆在 2.x。工具 payload 也要明确当前期望哪种扩展结果,不能让 Agent 猜。

链式 Prompt:每一步只承担当前责任

Harness 并没有消灭 Prompt,而是改变 Prompt 的粒度。promptBuilder.ts 会根据环境检查、当前批次、验证反馈、扩展检查和汇总生成不同提示。每份 Prompt 都可以包含四类信息:当前事实、当前任务、可用工具、提交契约。

“当前事实”包括 Session ID、批次索引、文件范围、规则和已知上下文。“当前任务”只描述这一阶段要完成的判断。“可用工具”告诉 Agent 如何搜索和提交。“提交契约”规定字段、何时调用工具以及不能自行宣布的状态。

链式 Prompt 的优点是局部可调试。如果某一批没有按格式提交,可以检查对应阶段的提示和工具 schema;如果高风险复核遗漏,可以检查扩展阶段;如果汇总不完整,可以对照数据库 Issue。单一巨型 Prompt 失败时,很难知道是理解、记忆、工具还是状态出了问题。

它也有风险。不同 Prompt 可能重复或冲突,旧规则可能残留,阶段切换时必要上下文可能丢失。因此 Prompt 需要版本管理和契约测试。至少应验证状态名、工具名、必填字段和禁止越级指令与代码一致。

Harness 的四层职责

为了设计新系统,可以把职责压缩成四层,而不是照抄类名。

Planner:决定做什么

Planner 根据改动范围与当前状态决定批次、顺序和下一任务。它维护全局完成条件,但不替 Agent 阅读每一行代码。Planner 的输出必须可持久化:任务列表、状态转换和失败原因不能只存在于一次模型回复。

Team Lead / Verifier:决定是否算完成

Verifier 负责验收。确定性条件写成程序,语义条件可以调用独立复核。它不能只看“输出是否很长”,而要检查当前工作单元是否完整、证据字段是否可用、结果是否与文件范围一致。

Bridge / MCP:控制怎样访问能力

Bridge 把后端能力暴露为窄工具。工具参数越清楚,越容易校验和审计。不要提供一个万能 execute(action, payload) 让模型自行拼动作;也不要让工具直接接受“目标状态”。工具应围绕业务动作设计,例如领取当前批次、提交当前结果。

Executor / Agent:完成语义分析

Executor 在受限工作区中读取代码、调用搜索和测试工具,生成候选 Issue。它的自治范围由批次、权限与停止条件共同决定。模型更强可以改善这一层,却不能取消其他三层。

flowchart TB
  P["Planner:选择任务与状态"] --> B["Bridge:暴露窄工具契约"]
  B --> E["Executor:分析当前批次"]
  E --> T["Verifier:检查完成证据"]
  T -- "通过" --> P
  T -- "不通过" --> R["重试 / 降级 / 熔断"]
  R --> P
图中关系须结合相邻正文与来源理解。

Anthropic 所说的 orchestrator-workers 与 evaluator-optimizer 模式可作为公共参照:中央编排者动态拆解任务,工作者执行;评价者依据清楚标准给反馈并循环改进。[1] 本案例与它们相似,但状态机、数据库和 MCP 工具让流程具备持久与恢复能力。不要因为结构相似,就声称某篇文章证明了当前实现最优。

正常路径之外的六个失败点

架构是否可靠,取决于失败时还能否解释和恢复。

失败点一:Setup 范围不完整

文件过滤错误或 diff 获取失败,会让后续所有步骤在错误集合上“成功”。应记录总文件、有效文件、排除文件和原因,并在汇总展示覆盖边界。

失败点二:批次不均衡

一个 Batch 太大,Agent 容易概括;太小则上下文被切碎,跨文件关系丢失。当前 800–1200 行只是案例实现的主要理想范围,不是通用答案。分批还要考虑语义依赖、文件类型和仓库基线。

失败点三:重复领取或重复提交

网络重试可能重复调用工具。领取动作应返回同一当前任务,提交动作应根据 Session、Batch 和结果标识去重。只靠前端按钮禁用无法保证幂等。

失败点四:状态与 payload 不匹配

状态是 2,Agent 却提交扩展结果;状态是 2.x,调用方没有 extension payload。Processor 必须拒绝或回退,日志要记录期望与实际,不要把未知字段悄悄丢弃后推进。

失败点五:验证门被配置关闭

实现包含耗时、密度、内容长度和高风险建议检查,但兜底配置中部分阈值是 0 或近似关闭值,运行时又可能被数据库热配置覆盖。架构图上有 Verifier,不代表每个质量门在所有环境都生效。第 7 章会区分设计目标、默认值和运行值。

失败点六:扩展检查循环不终止

recheck 或 fix 如果不断返回需要继续,任务会卡在 2.x。每个子状态需要重试上限、超时、熔断和可人工接管的终态。失败不是 running 的另一种写法,必须有明确分类。

怎样观察这条链路

没有可观测性,状态机只是在数据库里移动字符串。每次处理至少应绑定 Session 级 trace,并记录阶段名、Batch 索引、开始结束时间、结果数量、验证结论和错误类别。

当前实现把 allowed_next_step 转换为类似 processor_step_2_x 的阶段名,并在 MCP 工具调用处记录领取和提交阶段。这让监控可以回答“卡在哪个状态”“某个工具失败还是审查超时”“重试是否集中在特定 Batch”。远端执行器还需要队列等待和仓库准备指标。

日志不能记录全部 Prompt、令牌和业务代码而不设边界。公开或多租户系统应脱敏仓库地址、账号、任务文本和代码片段。更适合长期保留的是哈希、版本、计数、状态与错误分类;需要原文排障时使用受控采样和访问审计。

源码观察:从三处验证架构

阅读真实仓库时,不必从目录第一行读到最后一行。沿请求链抓三个观察点即可建立骨架。

第一处看 setup.tssetupSession。记录它创建哪些对象,如何过滤文件、计算哈希、复用历史结果和生成 Batch。检查完成时写入的 Session 状态。

第二处看 processor.tsprocessSession。确认 switch 的输入来自数据库 Session,不来自 Agent 参数;再追踪 Step 1 怎样选择批次、Step 2 怎样验证、缺失 payload 怎样回退、什么时候进入 2.x 与 3。

第三处看两个 MCP 工具。cr_shift_left_batch_review 只传 session_id 并调用 Processor;cr_shift_left_post_batch_results 传结构化结果,同样把处理权交给 Processor。工具本身不重写状态机,这是 Bridge 应有的薄度。

完成这三处后,再按问题进入 batchAllocator.tsverifier.tsextensionChecker.tspromptBuilder.ts 和数据模型。这样阅读的依据是因果链,而不是文件大小。

状态预测练习

在做状态预测前,还需要补一层容易被架构图隐藏的设计:Batch 怎样形成,以及工具契约怎样证明“当前批次”没有被越界处理。

分批不是按行数机械切片

最简单的分批方法是把 diff 每 1000 行截断。它会制造两个问题。第一,一个组件的类型、实现和测试可能被切进三个批次,Agent 在每批都只看到不完整语义。第二,某个 900 行生成文件可能独占一个 Batch,真正需要审查的十几个小文件却被挤到另一个超载批次。

当前 batchAllocator.ts 采用多阶段思路。先按目录、文件关系和语义线索形成初始组,再根据工作量拆分过大的组、合并过小的组,最后做去重和完整性校验。约 800–1200 diff 行是主要理想范围,不是第一优先级压倒所有语义关系的硬切线。

分批结果至少要满足三个不变量。每个有效 File 必须属于某个 Batch;同一个 File 不应在普通审查阶段被无意重复分配;所有 Batch 的文件并集应等于 Setup 认定的有效文件集合。三个不变量都可以由程序检查,不需要模型判断。

跨 Batch 依赖无法完全消除。可采用两种补偿。一种是在 Prompt 中附带只读的关联摘要或符号信息,让当前批次知道外部契约;另一种在所有批次完成后执行扩展检查,专门查跨批次冲突。补偿信息要受预算控制,否则分批在物理上存在,模型上下文却仍然承载全仓库。

Batch 大小还会影响指标。大 Batch 可能让后段文件漏检,小 Batch 增加调用次数、成本和重复评论。后续做召回率实验时,应把批次规模作为实验变量记录,而不是只看最终 F1。若同时改模型、Prompt、批次和规则,就无法知道提升来自哪里。

工具 schema 是协议,不只是参数校验

领取工具输入只有 session_id,意味着调用者不能指定“我要第 8 批”。服务端依据 Session 返回当前允许的任务。这个设计缩小了越权面:即使 Agent 记错索引,也不能绕过前七批。

提交工具中的 batch_results 是数组,每项包含文件路径、行号、评分、类别和问题内容,并允许带现有代码、改进代码等字段。schema 能阻止字段类型明显错误,却不能证明语义真实。例如 Agent 可以提交一个属于其他批次的合法路径,也可以给不存在的行号。服务端仍需把 payload 与当前 Batch、diff 和文件快照交叉检查。

扩展检查复用同一提交入口,是为了让 Processor 统一读取状态并解释 payload。它也要求错误信息足够清楚:是当前状态不接收 batch_results,还是 extension_results 缺少当前子检查字段。模糊的“参数错误”会让 Agent 盲目重试。

一个稳健的工具响应不只返回 Prompt。机器可读部分还可以包含 session_status、当前 Batch、下一步类型、验证是否通过和稳定错误码;面向 Agent 的自然语言说明负责解释动作。把所有信息塞进一段文字,会让调用方再次依赖模型解析状态。

状态转换需要并发保护

本地单人运行也可能出现并发:用户重复点击、MCP 超时后重试、旧请求晚到。两个提交同时读取 allowed_next_step=2,都验证并保存,就可能重复 Issue、重复增加完成数,甚至把索引推进两次。

常见防护是带条件更新:只有数据库仍处于期望状态和版本时才提交转换。也可以为 Session 增加版本号,更新时使用 compare-and-swap;Issue 使用 Session、Batch 与内容指纹组成唯一键;完成计数从 Batch 状态聚合,而不是无条件 +1。课程案例的远端 PendingTask 使用软认领和同仓库分支串行处理类似问题,第 8 章会详细讨论。

状态机设计还要列出非法转换。1 → 3 不应因为 Agent 提前总结而发生;完成态不应重新接受普通结果;失败恢复只能回到明确检查点。把非法转换写成测试,比在 Prompt 里强调“绝对不要跳步”可靠。

结果保存要先于下一任务下发

假设 Step 2 验证通过后先返回下一批 Prompt,异步保存 Issue 随后失败。Agent 已开始下一批,数据库却仍认为上一批未完成。恢复时可能重复审查,也可能丢结果。

较安全的顺序是:验证 payload,事务性保存 Issue 与 Batch 状态,更新 Session 的下一步许可,提交成功后再返回新 Prompt。跨数据库或外部系统无法单事务时,使用幂等事件和可重放的 outbox,而不是假设网络调用一定成功。

这条顺序解释了为什么“链式对话”不等于状态机。对话可以把下一段文字发出来,只有持久提交才能证明上一阶段已经形成可恢复事实

两种替代架构及其边界

理解 Harness 不必把当前实现当唯一答案。比较替代方案更容易看清选择条件。

第一种是无状态 PR Bot:每次收到 webhook,就取 diff、调用模型、发布评论。它组件少、反馈快,适合小改动和可容忍失败的建议型场景。缺点是难以恢复长任务,跨批完成证明弱,重复触发和上下文选择需要额外处理。若团队只想给 5 个文件的 MR 做非阻断提示,这可能是更合适的起点。

第二种是完全自治 Agent:给仓库、目标和工具,让模型自行规划、审查、复核并总结。它对未知任务有弹性,也减少显式流程代码;但完成证明、成本上限、权限和可复现性更难控制。高风险或规模化服务通常仍需要外部超时、预算、状态和人工批准。

当前混合 Harness 位于两者之间:Server 固定主状态和完成条件,Agent 在批次内部保持自治。它付出的代价是更多数据对象、迁移、监控和契约维护。只有当任务规模、自动化调用或恢复要求确实存在时,这些代价才合理。

下面不是标准答案式测验,而是检查你是否掌握状态所有权。

情况一:Session 的 allowed_next_step 是 1,当前批次索引为 3。调用领取工具后,服务端应下发第 3 批任务,并把后续期望调整为提交结果。Agent 无权自行把索引改成 4。

情况二:状态是 2,但提交调用没有 batch_results。当前实现回退到 Step 1,把当前 Batch 恢复为 pending,再次下发审查。它不会把空数组当成“没有发现问题”,因为“未提交结果”和“审查后确认零问题”语义不同。

情况三:状态是 2.x,扩展检查返回完成。Processor 应根据 extension_check_state 判断还有没有下一个扩展子任务;有则继续 2.x,无则进入下一个 Batch 或状态 3。Agent 的文字总结不能代替这个判断。

情况四:所有 Batch 标记完成,但有一个 File 从未属于任何 Batch。即使状态准备进入 3,完整性检查也应阻止汇总或标记范围缺陷。完成批次数不等于文件覆盖完整。

如果能解释这四种情况,你已经抓住 Harness 的核心:模型产生语义判断,外部系统保存事实并决定许可。

从 Prompt-only 迁移到 Harness 的最小路径

新团队不必一次建设全部组件。可以按风险逐层外置。

先把输出变成稳定 schema,让每条 Issue 有位置、类别、严重度、证据和建议。随后增加 Session,保存请求、状态和结果。再把大改动切成 Batch,由服务端发放当前批次。之后加入确定性验证、重试和终态。最后再扩展记忆、自动采纳、远端队列和复杂复核。

每一步都应该解决一个可观察问题。若当前任务只有几个文件、失败可人工重跑,完整状态机可能得不偿失;若需要长时间运行、跨几十个文件、被外部平台自动调用,持久状态和幂等就不是“高级功能”,而是基本正确性。

这一判断也符合公开工程建议:先使用能满足任务的最简单方案,只在性能与可靠性需要时增加 agentic 复杂度。[1] 简单不是少写代码,而是让每个组件都有清楚失败模式。一个 300 行万能 Prompt 往往比五个窄接口更难验证。

参考文献

  1. Anthropic. Building effective agents. 2024-12-19.
  2. Model Context Protocol. Architecture, Specification 2025-06-18.
  3. GitHub Docs. About GitHub Copilot code reviewUsing GitHub Copilot code review.
  4. GitLab Docs. GitLab Duo in merge requests.
  5. Nelson F. Liu 等. Lost in the Middle: How Language Models Use Long Contexts. TACL, 2023.