第一次让编程 Agent 独立处理完整需求时,我很容易被结果打动:页面、接口、类型和测试都齐了,代码组织得甚至比赶工时更整洁。真正把它接进原有流程,问题才暴露出来。它完成的是一个逻辑自洽的新功能,不一定是这个系统允许发生的变更。

这两者的差距,恰好是软件工程最难被压缩的部分。真实仓库里,业务规则通常不在一个文件中:权限来自网关上下文,状态是数据库字段和队列事件共同推导的,旧客户端依赖未写进接口文档的行为,发布还要兼容未完成的数据迁移。Agent 能快速补齐局部实现,却不会天然知道哪些历史约束值得保留。

下面用一个做过匿名化处理的内部任务平台需求说明这套流程。需求表面很简单:在失败任务列表中增加“批量重试”。仓库是常见的 React、Node.js、PostgreSQL 和消息队列组合,前端展示派生状态,服务端负责租户隔离和状态转换,Worker 按至少一次投递消费任务。

如果只按页面文案理解,批量重试不过是多选框加一个循环调用。真正进入生产前,需要回答的却是:哪些失败允许重试,谁有权限重试,处理中任务如何排除,同一任务会不会被重复入队,五十个任务中有三个状态已经变化时返回什么,以及操作是否能被审计。

编程 Agent 在真实仓库中的交付时序:合同、仓库事实、隔离修改、验证证据与人工评审

图 1:Agent 不是从提示直接跳到代码。仓库事实和任务合同共同约束实现,验证证据再交给评审者做最后判断。

先把一句需求还原成状态变化

我不会先让 Agent 写代码,而是让它把需求翻译成业务结果和不变量。批量重试的目标不是“多发几次请求”,而是“操作者能对当前租户内、处于可重试失败状态的任务发起一次可追踪的新执行”。

这句话直接带出五条不能被实现细节稀释的约束:

  1. 任务必须属于当前租户,不能相信浏览器提交的 tenantId
  2. 只有 failed 且错误类型允许重试的任务可以进入新一轮执行。
  3. 每个任务同一时刻只能存在一个有效执行,按钮双击和网络重试不能重复入队。
  4. 批量操作允许部分成功,但必须逐项返回稳定结果,不能只给一个笼统的 200 或 500。
  5. 谁在什么时间重试了哪些任务,需要进入审计链路。

任务合同因此会写成业务语言,而不是文件清单:

type RetryBatchContract = {
  outcome: "retry-eligible-failed-runs";
  invariants: [
    "tenant scope comes from authenticated context",
    "at most one active attempt per run",
    "every accepted retry has an audit record",
    "partial failure is explicit per item",
  ];
  limits: { maxBatchSize: 50; maxAttemptsPerRun: 5 };
  outOfScope: ["change retry policy", "redesign run state model"];
};

outOfScope 很重要。Agent 容易发现现有状态模型不够优雅,然后顺手把一次交付变成状态机重构。这个判断可能没有错,但它扩大了审查面,也把能否发布与另一项高风险工作绑在一起。先把需求做对,再单独处理架构债务,通常更稳妥。

第一轮只建立仓库事实,不允许改文件

我会要求 Agent 从用户动作沿调用链向下追,而不是根据文件名随机搜索。这个需求至少需要确认六个位置:列表状态从哪里得到、单条重试入口怎样鉴权、领域层在哪里判断状态转换、队列消息如何生成幂等键、Worker 怎样领取任务,以及相邻测试使用什么构造方式。

第一次探索的交付物是一张很短的仓库地图:

边界 当前事实 对本次变更的影响
Web 列表 displayStatus 是多个字段的派生值 不能把页面文本当领域状态提交
API 租户来自认证中间件 请求体不接受 tenantId
Domain 单条重试已有 canRetry(run, policy) 批量入口必须复用同一规则
Database 活跃 attempt 有唯一约束 幂等应落在事务边界,不只在 UI 防抖
Queue 至少一次投递 Worker 仍要按 attemptId 去重
Audit 记录操作者、资源和 before/after 批量结果要保留父操作 ID

如果 Agent 找到了另一套看似更方便的写法,我会让它先解释为什么现有边界不适用。真实仓库中,局部一致性通常比新抽象更有价值。评审者熟悉的事务模式、错误码和测试夹具,本身就是维护成本的一部分。

探索阶段还要先读取工作区状态。未提交修改不是噪声,更不能为了“恢复干净基线”而回滚。Agent 只拥有本次任务产生的差异;遇到重叠文件时,先理解现有修改,再决定能否安全合并。

变更计划按风险切片,不按前后端目录切片

“先写接口,再写页面”不够具体。我更关心每一步是否能独立证明一个业务事实。这个需求可以拆成四个切片:

  1. 为现有单条重试补齐领域规则和反例,固定 canRetry 的语义。
  2. 增加批量命令,在一个事务中领取可重试任务、创建 attempt 和 outbox 事件。
  3. 定义逐项结果协议,让前端正确表达成功、跳过和冲突。
  4. 增加多选交互,并在提交后按服务端结果更新列表,而不是乐观地把全部行改成“重试中”。

这里最值得审查的是第二步。一个常见但不可靠的实现是:先查询任务,再用 Promise.all 循环调用单条重试。查询与写入之间状态可能变化,任何中途失败都会留下难以解释的半成品,数据库成功和消息发送也可能分裂。

更合适的边界是让数据库事务保存新 attempt 与 outbox 事件,提交后由发布器发送队列消息:

WITH candidates AS (
  SELECT r.id
  FROM workflow_runs r
  WHERE r.tenant_id = $1
    AND r.id = ANY($2)
    AND r.status = 'failed'
    AND r.retryable = true
  FOR UPDATE
), attempts AS (
  INSERT INTO run_attempts (run_id, requested_by, status)
  SELECT id, $3, 'pending' FROM candidates
  ON CONFLICT (run_id) WHERE status IN ('pending', 'running')
  DO NOTHING
  RETURNING id, run_id
)
INSERT INTO outbox_events (topic, aggregate_id, payload)
SELECT 'run.retry.requested', run_id,
       jsonb_build_object('attemptId', id, 'runId', run_id)
FROM attempts
RETURNING aggregate_id;

这段 SQL 不是为了炫技。它把三个关键事实放在同一提交点:候选任务仍然符合条件、活跃执行不会重复创建、成功领取的任务一定留下待发布事件。未被领取的 ID 再根据当前状态映射成 not_foundnot_retryablealready_active,返回给页面。

接口不会只返回一个 success: true

{
  "operationId": "retry_01J...",
  "items": [
    { "runId": "r-101", "status": "accepted", "attemptId": "a-301" },
    { "runId": "r-102", "status": "skipped", "reason": "already_active" },
    { "runId": "r-103", "status": "rejected", "reason": "not_retryable" }
  ]
}

批量接口的价值不是替前端少发几个请求,而是把并发语义、权限和部分成功放进一个可以维护的领域边界。

给 Agent 的自主权必须和后果匹配

从任务合同到分层证据的 Agent 变更控制面

图 2:上下文、变更范围和执行资源都有预算。任何一项超出合同,Agent 都应停下来重新确认,而不是自行扩大任务。

我允许 Agent 自主读取符号、追调用链、修改局部代码、运行测试和整理差异,但不会把所有决定一起交出去。

业务语义仍由人确认。比如“超时失败是否允许重试”取决于外部副作用:任务可能已经向第三方提交成功,只是回执超时。单看数据库状态,Agent 无法判断重试是否安全。

公共协议和数据模型也需要人工承担。它们会影响其他团队和未来迁移,不应因为当前实现方便就改变。生产发布、数据删除、外部通知等不可逆动作,则必须有独立授权。

相反,可逆、局部、已有模式可依循的工作适合让 Agent 连续完成。关键不在于模型能力强弱,而在于错误发生后的影响半径和恢复成本。

验证要能发现“实现和测试一起理解错”

同一个 Agent 根据同一份上下文生成代码和测试,容易形成自洽闭环。测试会证明它实现的规则成立,却未必证明真实规则成立。因此验证来源要刻意分开。

这次变更至少需要五层证据:

层级 关键验证 能发现什么
静态 类型、Lint、生产构建 接口漂移、打包和环境问题
领域 状态转换表和反例 对可重试条件的错误理解
集成 真实数据库并发提交 重复 attempt、租户越界、事务分裂
消费 同一消息投递两次 Worker 幂等是否真实成立
UI 部分成功、长列表、移动端 页面是否诚实表达服务端结果

并发用例不能只 mock 仓储层。需要两个请求同时重试同一个任务,并验证数据库最终只有一个活跃 attempt、一个可发布事件,另一个请求收到稳定的 already_active。租户用例则用相同 run ID 组合不同认证上下文,确认越界资源不会通过错误信息泄露存在性。

浏览器检查也不是“页面能打开”。我会实际选择一批混合状态任务,触发批量重试,确认按钮在提交期间禁用、逐项结果可见、失败行仍可定位、列表刷新后与服务端一致,并检查最长错误文案和窄屏布局。

交付物不是一句“已完成”

Agent 结束时,需要把结果压缩成评审者能复核的证据包:行为发生了什么变化,复用了哪些边界,哪些命令和场景已经验证,哪些风险没有覆盖,以及如何打开预览复现关键路径。

我更希望看到“并发重试已用真实 PostgreSQL 验证;外部队列故障只验证到 outbox 积压告警,未做灾备演练”,而不是“所有测试通过,功能已完善”。前一种表述给出了证据边界,后一种只是情绪判断。

这也是我使用编程 Agent 后最大的变化。实现候选变便宜了,审查注意力反而更珍贵。工程师需要把时间从逐字符生产代码,转移到定义问题、固定边界、设计独立验证和判断剩余风险。

Agent 可以连续工作很久,也可以比人更耐心地搜索和运行工具。它真正进入真实仓库的前提,不是一次生成更多文件,而是整个过程有可追踪状态、有停止条件、尊重现有工作,并且最终有人能够沿着证据判断:这项变更为什么可以进入生产。

这篇描述的是“Agent 进仓库之后”的工作协议,它的前提是执行环境本身被隔离好了——文件、网络、密钥和资源预算的边界见编码 Agent 的沙箱。评审端的验收标准——意图、不变量、证据与剩余风险——在AI 辅助编程之后的代码评审里提前定好了,这里只是把它们落到了真实仓库。