范围契约与任务边界


文档摘要

范围契约与任务边界 本节摘要:模型不知道工作在哪结束。范围契约(scope contract)是一份按任务的文件,说明工作从哪开始、到哪结束、溢出时怎么回滚。它把「待在范围内」从愿望变成检查。诊断很经典:Agent 会蔓延——任务是「修登录 bug」,diff 却动了登录路由、邮件助手、数据库驱动、README、发布脚本;每处触碰当时都有貌似合理的理由,合在一起却是一个与被评审的不同的改动。范围蔓延是 Agent 工作里最被低估监控的失败模式,因为 Agent 每一步都以善意叙述。修法不是更严的提示,而是一份盘上的契约说明承诺了什么、加一个检查把结果对比承诺。

范围契约与任务边界

本节摘要:模型不知道工作在哪结束。范围契约(scope contract)是一份按任务的文件,说明工作从哪开始、到哪结束、溢出时怎么回滚。它把「待在范围内」从愿望变成检查。诊断很经典:Agent 会蔓延——任务是「修登录 bug」,diff 却动了登录路由、邮件助手、数据库驱动、README、发布脚本;每处触碰当时都有貌似合理的理由,合在一起却是一个与被评审的不同的改动。范围蔓延是 Agent 工作里最被低估监控的失败模式,因为 Agent 每一步都以善意叙述。修法不是更严的提示,而是一份盘上的契约说明承诺了什么、加一个检查把结果对比承诺。本节定义范围契约的七字段(task_id/goal/allowed_files/forbidden_files/acceptance_criteria/rollback_plan/approvals_required)——没有 forbidden_files 的契约是不完整的,负空间是契约的一半;用 glob 而非裸路径(真实仓库会移文件);把回滚纳入范围(列不出回滚的契约不该被批准)。范围检查是 diff 检查:Agent 写 diff,检查器读 diff+允许 glob+禁用 glob+验收命令,每个违规是一条验证门可拒的标记发现。本节还给出两层高度:任务契约约束一个任务,不约束项目——所以需要第二原语 feature_list.json(Agent 会话开始读的项目 backlog),「一次一个功能」从提示里一行 Agent 能自圆其说绕过的文字,变成它从盘读的值、验证门强制的检查。最后给四条让收益持久的生产模式:违规预算(非二元失败,Claude Code/Cursor 用的 agent-guardrails)、按路径族严重度不对称(docs 警告 vs scripts/migrations/config-prod 阻断)、时间与网络预算(范围不只文件)、多契约最小权限合并语义(允许交集/禁用并集/时间取最小)。一位跑 specsmaxxing 的实践者报告:三周内兔洞率从 52% 降到 21%,没换 Agent——契约干了活,不是模型。

对应原课程:Phase 14 · Lesson 36 · scope-contracts(原英文 phases/14-agent-engineering/36-scope-contracts/docs/en.md)。前置:第 32 节(最小工作台)、第 33 节(规则即约束)。

学习目标

阅读完本节,你应当能够:

  1. 写一份 Agent 任务开始读、验证器任务结束读的范围契约
  2. 指定允许文件、禁用文件、验收标准、回滚计划、审批边界
  3. 实现一个把 diff 对比契约、标记违规的范围检查器
  4. 让范围蔓延可见、自动、可审查
  5. 区分任务契约功能列表两层范围高度,并说出多契约的最小权限合并语义。

一、问题与直觉

Agent 会蔓延。任务是「修登录 bug」。diff 却动了登录路由、邮件助手、数据库驱动、README、发布脚本。每处触碰当时都有貌似合理的理由。合在一起,它们是一个与被评审的不同的改动。

范围蔓延是 Agent 工作里最被低估监控的失败模式,因为 Agent 每一步都以善意叙述。修法不是更严的提示。修法是一份盘上的契约说明承诺了什么,加一个检查把结果对比承诺。

范围契约里放什么

字段 目的
task_id 链到板上的任务
goal 审查者能核验的一句话
allowed_files Agent 可写的 glob
forbidden_files Agent 即便无意也不能碰的 glob
acceptance_criteria 证明完成的测试命令或断言行
rollback_plan 需停机时操作员能执行的一段话
approvals_required 范围外需显式人签字的动作

没有 forbidden_files 的契约是不完整的。负空间是契约的一半

用 glob,不用裸路径

真实仓库会移文件。把契约钉到 glob(app/**/*.pytests/test_signup*.py),这样会话间的重构不会让契约失效。

回滚是范围的一部分

列出怎么回滚,强制契约作者思考什么可能出错。一份你无法回滚的契约,是一份不该被批准的契约

范围检查是 diff 检查

Agent 写 diff。检查器读 diff、允许 glob、禁用 glob、跑过的验收命令列表。每个违规是一条验证门可拒的标记发现。

两层范围高度:功能列表与任务契约

范围契约约束一个任务。它不约束项目。一个 Agent 可以完美待在登录修复的契约内,却仍可能在下一轮决定项目还需要设置页、深色模式开关、路由重写。契约从没被问「项目范围内是哪些工作」,只被问「任务范围内是哪些文件」。

这第二高度需要自己的原语:一个 Agent 会话开始读的 feature_list.json。它是项目 backlog,机器可读、有序。Agent 挑恰好一个 statustodo 的功能,把它的 id 写进活动范围契约,被禁止在同一会话开始第二个功能。「一次一个功能」不再是提示里 Agent 能自圆其说绕过的一行,而变成它从盘读的值、验证门强制的检查。

{ "project": "knowledge-base", "active": "import-pdf", "features": [ { "id": "import-pdf", "status": "in_progress", "goal": "把 PDF 导入库", "done_when": "pytest tests/test_import.py 且示例 PDF 出现在库视图" }, { "id": "full-text-search", "status": "todo", "goal": "搜文档文本并排序", "done_when": "查询返回带摘录的排序结果" }, { "id": "cite-answers", "status": "todo", "goal": "答案带来源引用", "done_when": "每个答案渲染至少一个可点引用" } ] }
字段 目的
active 当前会话可碰的单一功能;空表示挑一个并设上
features[].id 范围契约 task_id 指向的稳定 slug
features[].status todo/in_progress/done/blocked;同时只有一个 in_progress
features[].goal 审查者能核验的一句话
features[].done_when in_progress 翻成 done 的验收行

两条规则让列表承重而非装饰。第一,「至多一个 in_progress」这条不变量本身是一条启动检查(第 33 节):列表显示两个时,会话拒绝启动,直到人解决。第二,功能列表是文件,不是聊天消息——聊天会滚出上下文,文件跨会话跨 Agent 持久。交接(第 40 节)把完成功能的状态写回 done,让下次会话打开的是准确的板而非重新推导还剩什么。

契约与列表按最小权限组合(同下面的合并):任务契约的 allowed_files 必须坐在活动功能所碰之内,绝不在其外。

二、从零实现

原课程 code/main.py 实现:

  • scope_contract.json 模式(JSON Schema 子集,glob 数组)。
  • 一个 diff 解析器,把动过的文件列表加跑过的命令列表变成 RunSummary
  • 一个返回 (violations, in_scope, off_scope)scope_check
  • 两个演示运行:一个待在范围内,一个蔓延。检查器用确切文件与原因标记蔓延。

Step 1:范围契约

{ "task_id": "T-102", "goal": "给登录路由加输入校验", "allowed_files": ["app/auth/login.py", "tests/test_login.py"], "forbidden_files": ["scripts/release.sh", "migrations/**", "config/prod/**"], "acceptance_criteria": ["pytest tests/test_login.py"], "rollback_plan": "git checkout app/auth/login.py tests/test_login.py", "approvals_required": ["新增依赖"] }

Step 2:范围检查(diff 对比 glob)

def scope_check(diff_files, run_cmds, contract): allowed = contract["allowed_files"]; forbidden = contract["forbidden_files"] off_scope = [f for f in diff_files if not any(fnmatch(f, g) for g in allowed)] violated = [f for f in diff_files if any(fnmatch(f, g) for g in forbidden)] return {"off_scope": off_scope, "forbidden_hit": violated, "in_scope": not off_scope and not violated}

Step 3:功能列表(项目高度)

def pick_feature(feature_list): in_prog = [f for f in feature_list["features"] if f["status"] == "in_progress"] if len(in_prog) > 1: raise StartupCheckFail("多个 in_progress") # 启动检查 if len(in_prog) == 1: return in_prog[0] todo = next(f for f in feature_list["features"] if f["status"] == "todo") todo["status"] = "in_progress"; feature_list["active"] = todo["id"] atomic_write("feature_list.json", json.dumps(feature_list)) return todo

运行 python3 code/main.py 会输出:契约、两次运行、每运行裁决、一份保存的 scope_report.json

生产模式(让收益持久)

一位跑「specsmaxxing」(调 Agent 前用 YAML 写范围契约)的实践者报告:三周内兔洞率从 52% 降到 21%,没换 Agent。契约干了活,不是模型。三条模式让收益持久。

违规预算,非二元失败。 agent-guardrails(Claude Code、Cursor、Windsurf、Codex 经 MCP 用的 OSS 合并门)按任务发一个 violationBudget:预算内的小范围滑出作为警告浮现;只在预算超时合并门才拒。配 violationSeverity: "error" | "warning"。预算是一道能交付的门与一道被讨厌它的团队关掉的门之间的差别。

按路径族的严重度不对称。 越界写到 docs/** 通常是 warn;越界写到 scripts/**migrations/**config/prod/** 恒为 block。这种不对称必须活在契约里,不在运行时里,因为它是项目特定的、每任务变化。

文件预算旁的时间与网络预算。 一个 time_budget_minutes 字段约束墙钟;运行时拒绝无再审批地越过它继续。一个 network_egress 主机名白名单防 Agent 偷偷打一个不属于任务的外部 API。这些也是范围维度;文件 glob 必要但不充分。

多契约合并语义(最小权限)。 当两个范围契约适用时(如项目级契约加任务级契约),合并是:交集 allowed_files(两个契约都允许该路径)、并集 forbidden_files(任一可禁)、time_budget_minutes 取最严(min)、approvals_required 累积。network_egressNone 即不强制、[] 即全拒、[...] 即白名单;合并下 None 让位对方、两列表交集、全拒保持全拒。在契约模式里声明这点,让合并机械化、可审查。

💡 设计要点:范围契约把「待在范围内」从语言约束变成diff 检查。这与第 33 节「规则即约束」同源——语言约束是愿望,diff 检查是测试。功能列表则把范围从「任务」抬到「项目」,堵住「Agent 完美待在登录契约内却擅自开设置页」这类越界。两层叠加按最小权限,正好对应第 31 节的「授权策略」原语——允许/禁用 glob 是 ACL,审批是权限格。

三、框架对比

运行时 范围如何用
Claude Code 斜杠命令 /scope 命令写契约并钉为会话上下文;子智能体行动前读契约
GitHub PR 契约作 JSON 推进 PR 体或检入工件;CI 对合并 diff 跑范围检查器
LangGraph interrupts 范围违规触发 interrupt;处理器问人「契约要扩还是 Agent 要退」
agent-guardrails 合并门 违规预算 + 严重度分层

契约随任务旅行。任务关闭时,契约归档到 outputs/scope/closed/

四、可复用产物

原课程 outputs/skill-scope-contract.md:为任务描述生成范围契约,加一个 CI 里对每个 Agent diff 跑的、glob 感知的检查器。

五、练习

  1. (Easy)network_egress 字段列允许的外部主机。碰其他主机的运行拒掉。
  2. (Medium) 扩展检查器,对 docs/** 软失败、对 scripts/** 硬失败。论证不对称。
  3. (Medium) 让契约用静态规则集(无 LLM)从 goal 字段推导 allowed_files。第一个边界情况会出什么问题?
  4. (Hard)time_budget_minutes,墙钟一超即拒继续。
  5. (Hard) 对同一 diff 跑两个契约。两个都适用时正确的合并语义是什么?

本节要点回顾

  1. 范围契约把「待在范围内」从愿望变检查:按任务文件,说开始/结束/回滚。
  2. 七字段:task_id/goal/allowed_files/forbidden_files/acceptance/rollback/approvals。
  3. 没有 forbidden_files 的契约不完整:负空间是契约的一半。
  4. 用 glob 不用裸路径:真实仓库会移文件,glob 抗重构。
  5. 回滚是范围一部分:列不出回滚的契约不该被批准。
  6. 范围检查是 diff 检查:对 diff+允许/禁用 glob+验收命令,每违规一条标记发现。
  7. 两层高度:任务契约约束任务,功能列表(feature_list.json)约束项目;一次一个功能。
  8. 功能列表两规则:「至多一个 in_progress」是启动检查;是文件非聊天消息,跨会话跨 Agent 持久。
  9. 四条生产模式:违规预算(非二元,agent-guardrails)、按路径族严重度不对称(docs 警告 vs scripts/migrations/prod 阻断)、时间+网络预算、最小权限合并(允许交集/禁用并集/时间 min)。
  10. specsmaxxing receipts:52%→21% 兔洞率,没换 Agent——契约干活非模型。

下一节,我们做实「反馈」这一面——运行时反馈循环:如何把每次 shell 命令、工具调用的真实输出捕获进循环,让 Agent 看到失败、据此修正,而不是在 400 错误上宣布成功。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U