5. 约束、验证与恢复
这是什么:约束层 = 划出 Agent 不能碰的红线;验证层 = 让它能自己确认做对了;恢复层 = 出错时能退回来。
为什么用:Agent 的失败方式和人不一样——它不会因为害怕犯错而收敛,反而会一路往前跑,把错误放大。所以边界必须由外部强制。
如何用:本章讲三类硬约束怎么落、验证闭环怎么搭、以及失败后重试/回滚/降级的选择。
5.1 L6:约束要在机械层
先记住 OpenAI 那句话:
If it cannot be enforced mechanically, agents will deviate. (不能被机械执行的约束,Agent 迟早会偏离。)
Agent 和人不一样的地方在于:它不会因为怕犯错而收敛。人写到一半觉得「这里不太对」会停下来问一句;Agent 读到一半觉得「不太对」会换个思路继续往前,把错误一路放大。
所以约束不能只写在提示词里。提示词是「建议」,机械检查才是「规则」。
约束分三档,可靠性递减:
| 档位 | 手段 | 可靠性 | 例子 |
|---|---|---|---|
| 硬 | 沙箱、权限白名单、文件系统隔离 | 高 | 只读挂载、禁网络、禁 rm -rf |
| 半硬 | Linter、结构测试、CI 检查 | 中高 | 依赖方向反了就报错 |
| 软 | 提示词里的「请不要……」 | 低 | 「不要改 generated/ 目录」 |
原则:能在硬档解决的,不放软档。 每写一条「请不要 X」,先问一句:这能不能变成一个检查?
5.2 机械化约束:Linter 与结构测试
最典型的例子是架构分层。OpenAI 给每个业务领域定了固定分层:
Types → Config → Repo → Service → Runtime → UI
依赖方向不能反过来——Service 不该直接依赖 Runtime。光写在文档里没用,靠自定义 Linter 和结构测试:
# 结构测试:Service 层不许 import Runtime 层
ALLOWED = {
"types": {"types", "config"},
"config": {"types", "config"},
"repo": {"types", "config", "repo"},
"service": {"types", "config", "repo", "service"},
"runtime": {"types", "config", "repo", "service", "runtime"},
"ui": {"types", "config", "repo", "service", "runtime", "ui"},
}
def test_service_does_not_import_runtime():
for path in glob("server/services/**/*.py"):
for imported in imports_of(path):
layer = layer_of(imported)
if layer and layer not in ALLOWED["service"]:
assert False, (
f"{path} 引入了 {layer} 层的 {imported},"
f"应改成通过 Service 层调用。修复方式:"
f"把 {imported} 上移到 service 层再从那里导出。"
)
关键在报错信息里带修复方法。对比一下:
❌ ImportError: cannot import name 'Runtime' from 'server.runtime'
❌ server/services/order.py 违反了分层:Service 不允许直接依赖 Runtime 层。
修复方式:把 Runtime 的调用上移到 service/order.py,或在 service 层加一个薄封装。
第二条报错会被 Agent 直接消费成下一步动作;第一条只会让它重试同一个 import。
「报错要能指导下一步」这条,在约束层比在工具层更重要——因为违反约束的路径往往正是它不熟的路。
5.3 权限与沙箱
权限是约束的第一道门。主流 Agent 工具都有类似的三档:
| 档位 | 行为 | 什么时候用 |
|---|---|---|
| allow | 直接执行 | 只读、可逆的操作 |
| ask | 弹窗确认 | 有副作用但可逆的操作(写文件、装包) |
| deny | 直接拒绝 | 危险或不可逆的操作(推送生产、删分支、改密钥) |
opencode 的权限可以按命令模式配:
{
"permission": {
"bash": {
"git status": "allow",
"git diff": "allow",
"npm test": "allow",
"git push *": "ask",
"rm *": "deny",
"curl *": "deny"
}
}
}
模式支持通配符。注意 git push * 和 rm * 这种按命令模式的粒度比「按工具」细得多——同一把 bash 扳手,git status 放行、rm 拒绝。
沙箱是第二道门:就算模型想干坏事,也干不成。常见手段:
| 手段 | 挡什么 |
|---|---|
| 文件系统隔离 | 只能写工作目录,其余只读或不可见 |
| 网络隔离 | --network none,连不出去 |
| 权限降级 | 容器内用非 root 用户跑 |
| 资源限制 | 内存、CPU、进程数上限 |
| 超时杀进程树 | 死循环的进程连同子进程一起收掉 |
本仓库的 server/ 就实现了同一套思路(server/runner.py):环境变量白名单、输出截断、超时杀整棵进程树、Docker 沙箱模式(--network none --read-only --cap-drop ALL)。代码执行后端比 Agent 更危险——Agent 只是让模型能调工具,代码执行是让模型能跑任意代码。
5.4 验证闭环:让它能自己确认
L5 的核心问题:Agent 怎么知道自己做对了?
一个完整的验证闭环是:
生成 → 执行 → 观测 → 判断 → (不合格)修正 → 再执行
关键在「观测」这一步的产物要能指导修正。跑完测试只说「失败了」不够,得说「哪个用例、断言是什么、实际值是什么」。
三种验证手段,按可靠性排序:
| 手段 | 怎么验 | 能抓到什么 |
|---|---|---|
| 自动化测试 | npm test |
逻辑错误 |
| 结构/静态检查 | Linter、类型检查 | 架构违规、风格问题 |
| 端到端运行 | 启动应用、真点一遍 | 「能不能用」 |
Anthropic 的三智能体架构里,Evaluator 用 Playwright MCP 实际点击运行中的应用,再按产品设计深度、功能性、视觉设计、代码质量打分——这是把「运行应用」变成了一种验证手段。
有个反直觉的结论:Anthropic 在前端任务里把设计质量和原创性的权重设得高于功能性和代码质量,用来纠正模型「功能齐全但外观平庸」的默认输出。
5.5 自我评价偏差:验证要独立于生成
这里有个坑:用 AI 生成的测试来验证 AI 生成的代码,仍然像「用同一双眼睛检查自己的作业」(Böckeler 的评价)。
架构检查能阻止错误的依赖方向,清理任务能删除重复实现,但两者都不能证明用户流程符合预期。测试和实现都由同一类模型生成时,测试通过只说明两者共享的假设没有被打破。
解法是把生成和评估拆开:
| 做法 | 说明 |
|---|---|
| 独立 Evaluator | 另开一个 Agent 专门验收,不参与实现 |
| 换视角 | 实现 Agent 只写代码,验收 Agent 只跑流程、只提「能不能用」 |
| 人工抽查 | 高风险改动留人眼 |
5.6 失败恢复:重试、回滚、降级
出错了怎么办?三条路,按代价从低到高:
| 策略 | 什么时候用 | 代价 |
|---|---|---|
| 重试 | 错误是偶发的(网络抖动、CI 抖动) | 低,但要有次数上限 |
| 回滚 | 改动已经落地,但结果是错的 | 中,靠 Git |
| 降级 | 环境本身有问题(缺依赖、服务不通) | 中高,需要能换条路走 |
三个细节:
一、重试必须有上限。 Stripe 的 CI 环节最多跑 2 轮,不无限重试。无限重试的 Agent 会烧掉预算和时间,还可能把本来能发现的错误掩盖掉。
二、回滚要靠版本控制。 Agent 改一堆文件之后发现方向错了,Harness 得能一键回到干净状态:
# 改砸了,回到上一个提交
git reset --hard HEAD~1
# 或者更安全:先建分支,改砸了切回去
git checkout -b feature/xyz
这也是为什么 Agent 的工作最好在分支上进行——不是流程洁癖,是回滚成本的问题。
三、降级要有预案。 「测试跑不起来」不等于「活没法干」。可以降级成:跳过那部分测试、只做静态检查、改用人工确认。把降级路径提前写好,Agent 遇到环境问题才不会硬撞。
5.7 Harness 组件要跟着模型变
最后一条容易被忽略:Anthropic 发现,把模型从 Sonnet 4.5 换成 Opus 4.6 后,Sprint 机制可以完全移除,Evaluator 从「每个 Sprint 都检查」变成「最后只检查一次」。
他们的总结是:
Every component in a harness encodes an assumption about what the model can't do on its own, and those assumptions are worth stress testing. (Harness 的每个组件都编码了一个「模型自己做不到什么」的假设,而这些假设值得反复验证。)
所以 Harness 不是搭完就完的。模型升级了,回头检查一遍:哪些保护机制现在是多余的?哪些假设已经不成立了?删掉只增加上下文和编排成本的部分,Harness 才会越来越薄、越来越准。
对比一下两种配置的成本(Anthropic 的实测):
| 配置 | 耗时 | 花费 | 效果 |
|---|---|---|---|
| Solo Harness(单 Agent + 最少工具) | 20 分钟 | $9 | 跑不起来的半成品 |
| Full Harness(三 Agent + 完整工具链) | 6 小时 | $200 | 完整可用的应用 |
复杂任务差距还会拉大。但别一上来就 Full——从 Solo 开始,遇到具体卡点再加件。
动手改
- 把你项目里「请不要 X」这类提示词约束列个清单,逐条判断能不能变成机械检查,能转的就转
- 给现有的一个 Linter 或测试补上修复建议(报错信息里写清怎么改),然后让 Agent 处理一次这个报错,观察它是否一次就对
- 配一份权限清单:哪些命令 allow、哪些 ask、哪些 deny,尤其是
git push、rm、curl这三个 - 在你工作流里设计一次「失败回滚」:Agent 改砸了之后,一句指令怎么回到干净状态
小结
- 约束分三档:硬(沙箱权限)、半硬(Linter、结构测试)、软(提示词);能机械执行就别靠提示词
- 报错信息要带修复方法——约束层的报错尤其重要
- 权限按命令模式配,比按工具细
- 沙箱四件套:文件隔离、网络隔离、权限降级、资源与超时限制
- 验证闭环:生成 → 执行 → 观测 → 判断 → 修正;观测结果要能指导修正
- 自我评价偏差:验证要独立于生成,最好换个人(或换个 Agent)来验
- 恢复三策略:重试(有上限)、回滚(靠 Git,建议在分支上干)、降级(提前写预案)
- 模型升级后回头审 Harness,删掉已经多余的保护机制
下一章:极低花费下使用手机完成AI编程——开发网站,看一套 2C2G 服务器上的真实账本 🐸