第一次让编程 Agent 独立处理完整需求时,我很容易被结果打动:页面、接口、类型和测试都齐了,代码组织得甚至比赶工时更整洁。真正把它接进原有流程,问题才暴露出来。它完成的是一个逻辑自洽的新功能,不一定是这个系统允许发生的变更。
这两者的差距,恰好是软件工程最难被压缩的部分。真实仓库里,业务规则通常不在一个文件中:权限来自网关上下文,状态是数据库字段和队列事件共同推导的,旧客户端依赖未写进接口文档的行为,发布还要兼容未完成的数据迁移。Agent 能快速补齐局部实现,却不会天然知道哪些历史约束值得保留。
下面用一个做过匿名化处理的内部任务平台需求说明这套流程。需求表面很简单:在失败任务列表中增加“批量重试”。仓库是常见的 React、Node.js、PostgreSQL 和消息队列组合,前端展示派生状态,服务端负责租户隔离和状态转换,Worker 按至少一次投递消费任务。
如果只按页面文案理解,批量重试不过是多选框加一个循环调用。真正进入生产前,需要回答的却是:哪些失败允许重试,谁有权限重试,处理中任务如何排除,同一任务会不会被重复入队,五十个任务中有三个状态已经变化时返回什么,以及操作是否能被审计。
图 1:Agent 不是从提示直接跳到代码。仓库事实和任务合同共同约束实现,验证证据再交给评审者做最后判断。
先把一句需求还原成状态变化
我不会先让 Agent 写代码,而是让它把需求翻译成业务结果和不变量。批量重试的目标不是“多发几次请求”,而是“操作者能对当前租户内、处于可重试失败状态的任务发起一次可追踪的新执行”。
这句话直接带出五条不能被实现细节稀释的约束:
- 任务必须属于当前租户,不能相信浏览器提交的
tenantId。 - 只有
failed且错误类型允许重试的任务可以进入新一轮执行。 - 每个任务同一时刻只能存在一个有效执行,按钮双击和网络重试不能重复入队。
- 批量操作允许部分成功,但必须逐项返回稳定结果,不能只给一个笼统的 200 或 500。
- 谁在什么时间重试了哪些任务,需要进入审计链路。
任务合同因此会写成业务语言,而不是文件清单:
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 只拥有本次任务产生的差异;遇到重叠文件时,先理解现有修改,再决定能否安全合并。
变更计划按风险切片,不按前后端目录切片
“先写接口,再写页面”不够具体。我更关心每一步是否能独立证明一个业务事实。这个需求可以拆成四个切片:
- 为现有单条重试补齐领域规则和反例,固定
canRetry的语义。 - 增加批量命令,在一个事务中领取可重试任务、创建 attempt 和 outbox 事件。
- 定义逐项结果协议,让前端正确表达成功、跳过和冲突。
- 增加多选交互,并在提交后按服务端结果更新列表,而不是乐观地把全部行改成“重试中”。
这里最值得审查的是第二步。一个常见但不可靠的实现是:先查询任务,再用 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_found、not_retryable 或 already_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 的自主权必须和后果匹配
图 2:上下文、变更范围和执行资源都有预算。任何一项超出合同,Agent 都应停下来重新确认,而不是自行扩大任务。
我允许 Agent 自主读取符号、追调用链、修改局部代码、运行测试和整理差异,但不会把所有决定一起交出去。
业务语义仍由人确认。比如“超时失败是否允许重试”取决于外部副作用:任务可能已经向第三方提交成功,只是回执超时。单看数据库状态,Agent 无法判断重试是否安全。
公共协议和数据模型也需要人工承担。它们会影响其他团队和未来迁移,不应因为当前实现方便就改变。生产发布、数据删除、外部通知等不可逆动作,则必须有独立授权。
相反,可逆、局部、已有模式可依循的工作适合让 Agent 连续完成。关键不在于模型能力强弱,而在于错误发生后的影响半径和恢复成本。
验证要能发现“实现和测试一起理解错”
同一个 Agent 根据同一份上下文生成代码和测试,容易形成自洽闭环。测试会证明它实现的规则成立,却未必证明真实规则成立。因此验证来源要刻意分开。
这次变更至少需要五层证据:
| 层级 | 关键验证 | 能发现什么 |
|---|---|---|
| 静态 | 类型、Lint、生产构建 | 接口漂移、打包和环境问题 |
| 领域 | 状态转换表和反例 | 对可重试条件的错误理解 |
| 集成 | 真实数据库并发提交 | 重复 attempt、租户越界、事务分裂 |
| 消费 | 同一消息投递两次 | Worker 幂等是否真实成立 |
| UI | 部分成功、长列表、移动端 | 页面是否诚实表达服务端结果 |
并发用例不能只 mock 仓储层。需要两个请求同时重试同一个任务,并验证数据库最终只有一个活跃 attempt、一个可发布事件,另一个请求收到稳定的 already_active。租户用例则用相同 run ID 组合不同认证上下文,确认越界资源不会通过错误信息泄露存在性。
浏览器检查也不是“页面能打开”。我会实际选择一批混合状态任务,触发批量重试,确认按钮在提交期间禁用、逐项结果可见、失败行仍可定位、列表刷新后与服务端一致,并检查最长错误文案和窄屏布局。
交付物不是一句“已完成”
Agent 结束时,需要把结果压缩成评审者能复核的证据包:行为发生了什么变化,复用了哪些边界,哪些命令和场景已经验证,哪些风险没有覆盖,以及如何打开预览复现关键路径。
我更希望看到“并发重试已用真实 PostgreSQL 验证;外部队列故障只验证到 outbox 积压告警,未做灾备演练”,而不是“所有测试通过,功能已完善”。前一种表述给出了证据边界,后一种只是情绪判断。
这也是我使用编程 Agent 后最大的变化。实现候选变便宜了,审查注意力反而更珍贵。工程师需要把时间从逐字符生产代码,转移到定义问题、固定边界、设计独立验证和判断剩余风险。
Agent 可以连续工作很久,也可以比人更耐心地搜索和运行工具。它真正进入真实仓库的前提,不是一次生成更多文件,而是整个过程有可追踪状态、有停止条件、尊重现有工作,并且最终有人能够沿着证据判断:这项变更为什么可以进入生产。
这篇描述的是“Agent 进仓库之后”的工作协议,它的前提是执行环境本身被隔离好了——文件、网络、密钥和资源预算的边界见编码 Agent 的沙箱。评审端的验收标准——意图、不变量、证据与剩余风险——在AI 辅助编程之后的代码评审里提前定好了,这里只是把它们落到了真实仓库。