v0.1 约 11 分钟
下载 Word 版

项目复盘

不是更强的提示词,而是更稳的控制回路

一次 Agent 编程项目复盘

  • 作者:Key of the Door(GitHub:Key-of-door)
  • 版本:v0.1
  • 版本日期:2026-07-11
  • 许可:CC BY-SA 4.0
  • 项目仓库:Key-of-door/code-risk-review-v0.1
  • 项目 Release:v0.1(c5054ac)
  • 配套材料:《Agent 协作控制回路:一个面向复现的最小实践框架》

Word 下载版:下载《不是更强的提示词,而是更稳的控制回路》v0.1

我一开始只想做一个轻量的代码风险审查工具,并借这个项目补足自己的实现能力。

项目叫 Code Risk Review v0.1。它只做 Python 静态分析,不运行用户代码,不保存用户源码,不承诺代码安全,不提供安全认证,也不替代人工审查、测试和交付判断。

它的目标很克制:在交付前,帮助用户发现当前规则范围内可识别的风险,并说明原因、边界和下一步建议。

后来让我意外的,不是工具本身,而是开发过程。

这个项目推进得异常顺利。规则增加、前后端接入、错误状态、测试、mutation 和最后的发布收尾,没有像我预期的那样逐渐堆成混乱。

起初我以为,只是因为 Agent 足够强,或者所有人都在用类似的方式开发。后来我才意识到,自己真正遇到的问题不是“如何让 Agent 写代码”,而是:

当 Agent 在不完整意图下拥有较高实现自由时,怎样防止它把产品悄悄改写成另一个东西?

我后来把这种现象称为 Agent 语义漂移。

它不一定表现为报错。

Agent 可能误解边界、补全没有说清的假设,甚至在代码能够运行、测试仍然通过、文档彼此一致的情况下,把产品实现成另一个东西。

方法不是预先设计出来的

我并没有在项目开始时准备一套完整方法论。

相反,它是在开发过程中,被一个个具体问题逼出来的。

我经常说:“好了,继续学习吧。”

表面上,我是在让 Agent 继续讲解概念、给出练习;实际上,我也会通过它的解释和我的回答,观察它是否仍然正确理解产品边界。

关键判断随后会被写入 API 契约、工程护栏或 README,避免它们只存在于对话上下文中。

我也会问:“感觉怎么样?”

这不是为了让 Agent 表态,而是为了让它暴露当前理解:

  • 它是否把阶段性约束当成了永久原则
  • 是否把前后端职责混在了一起
  • 是否忘记“不运行代码、不保存源码、不承诺安全”等基本边界
  • 是否在没有明确授权的情况下扩大了修改范围。

后来我意识到,这些看似随意的对话,实际承担了 理解检查点 的作用。

工程护栏文档就发生过一次这样的偏移。

它最初被理解成永久不变的工程宪法;经过讨论后才重新明确:其中既有长期原则,也有只服务于 v0.1 当前阶段的工程护栏。

这让我逐渐形成一个判断:

人必须保留产品定义权与解释权。

Agent 可以拥有较大的实现自由,但不能默认拥有重新定义产品的权力。

约束上层,放开实现层

这套做法不是把 Agent 锁成一段只能机械执行的脚本。

产品方向、用户承诺、职责边界、状态语义、隐私限制和单一事实源属于上层约束,必须由人明确决定。

接口契约、测试和 release 条件,则把这些判断落成可以检查的结构。

但在这些边界之内,Agent 可以自由选择具体实现路径:

  • 函数怎样拆分
  • AST 如何遍历
  • 测试如何组织
  • 局部逻辑怎样重构
  • 在既有契约内,哪种实现更清晰、更稳妥。

这些细节不必由人逐行指定。

换句话说:

语义自由度要小,实现自由度可以大。

这有点像飞控系统。

飞行员给出意图,系统持续限制不可接受的状态。飞机不必沿着唯一轨迹飞行,但不能失去控制。

这里的“飞控”只是一个帮助理解的比喻,并不意味着我已经建立了严格的控制论模型。它只是让我更容易理解:约束 Agent,并不等于锁死 Agent 的能力。

四个收尾闭环

在项目收尾阶段,我们通过几个小闭环,把这套做法落成了可验证的工程证据。

1. source_size:字段语义到底是什么

source_size 原本存在“Python 字符数”和“UTF-8 字节数”之间的歧义。

如果这个字段用于输入限制和资源边界,UTF-8 字节数是更一致的定义。

于是,项目将 source_size 统一定义为源码按 UTF-8 编码后的字节数,并同步完成了:

  • API 契约更新
  • 中英文混合输入测试
  • 边界行为验证
  • 将实现故意改回字符数计算的 mutation。

在该提交对应的验证环境中,34 个测试通过,mutation checker 中的 9 项预设变异全部被测试杀死。

对应提交:14e746e

这份证据证明,现有测试能够阻止“UTF-8 字节数退化为 Python 字符数”这一已明确设计的语义退化。

它不证明所有与编码、输入大小或资源消耗有关的问题都已被覆盖。

2. HTTP 请求体限制:入口边界和业务边界不能混在一起

这里需要区分两类“输入过大”:

一种是 HTTP 请求体在读取前就超过入口上限,应返回:

413 / REQUEST_BODY_TOO_LARGE

另一种是请求已经进入业务层,但其中的源码超过分析上限,应返回:

SOURCE_TOO_LARGE

两者发生在不同层级,代表不同的失败语义,不能被合并成同一个错误。

项目随后在读取 rfile 前,根据可解析的 Content-Length 检查请求体大小,并补充了真实 HTTP 测试和禁用入口限制的 mutation。

在该提交对应的验证环境中,36 个测试通过,mutation checker 中的 10 项预设变异全部被测试杀死。

对应提交:4c5c256

这份证据证明,在当前实现和测试覆盖的 HTTP 输入路径中,带有可解析 Content-Length 的超限请求能够在进入 JSON 解析和源码分析前被拒绝。

它不自动证明所有传输编码、代理配置或部署环境都受到完全相同的保护。

3. filename:辅助元数据不能成为第二个事实源

上传文件时,前端只传递 File.name,后端只接受不包含路径信息的干净 basename,并拒绝:

  • 路径片段
  • 驱动器前缀
  • .
  • ..
  • 非字符串输入。

filename 可以作为规则判断中的有限上下文,但不能让前端长出第二套风险判断逻辑,也不能仅凭文件名生成未经后端规则确认的风险事实。

这个闭环最终覆盖了:

  • 前端 payload
  • 后端输入校验
  • HTTP 测试
  • API 契约
  • 对应的 mutation。

在该提交对应的验证环境中,40 个测试通过,mutation checker 中的 11 项预设变异全部被测试杀死。

对应提交:1d85c2e

这里守住的不只是“文件名是否合法”,而是:

风险事实由谁生成,谁没有资格重新定义它。

4. report_depth:披露深度不能改变分析事实

摘要和详情原本只是一个体验需求,但它很容易被实现成两套分析路径:

  • summary 少分析一部分
  • detail 多分析一部分。
  • 如果这样实现,report_depth 就不再只是披露深度,而会开始改变产品事实。

我们先确定了一组不变量:

  • 规则层始终生成完整风险对象
  • report_depth 只在后端序列化阶段裁剪字段
  • 风险命中、数量、等级、置信度、行动优先级和状态语义不得改变
  • 前端不能补写未请求的原因、建议或验证步骤。

最终,summary 和 detail 的区别只存在于信息披露深度,而不存在于分析事实本身。

在该提交对应的验证环境中,44 个测试通过,mutation checker 中的 12 项预设变异全部被测试杀死。

对应提交:f030294

这个闭环也让我更明确地理解了 单一事实源:

每一类核心事实,都应有一个权威定义和生成来源。其他层级可以传递、裁剪、排序和展示,但不能重新定义它。

让测试不只检查代码能不能跑

项目中大量实现由 Agent 完成。

但我没有因此把产品解释权交给 Agent。

Agent 可以选择实现路径,却不能决定:

  • report_depth 是否改变分析事实
  • filename 是否成为风险事实源
  • partial 是否可以被悄悄改成 user_error
  • 某项阶段性约束是否可以被写成永久原则
  • 测试通过以后,项目可以对外承诺什么。

这些边界必须由人定义,再由契约、测试和 mutation 固化。

测试长期保持全绿时,我反而开始担心:

它们会不会只是没有真正碰到关键退化点?

于是,我们开始故意破坏产品语义,而不只是破坏普通实现。

例如:

  • 把 partial 改成 user_error
  • 把 UTF-8 字节数改回字符数
  • 移除 HTTP 请求体入口限制
  • 关闭文件名校验
  • 让 report_depth 失去响应裁剪行为
  • 让字符串中的 eval( 被误判为真实函数调用。

普通 mutation 测试通常会问:

实现被改坏以后,测试会不会失败?

这里还需要再问一层:

产品承诺被悄悄改写以后,测试会不会失败?

我暂时把这种做法称为语义边界变异测试(semantic-boundary mutation testing):

  • 写出一条具体产品承诺
  • 找到它在代码中的实现位置
  • 设计一个最小的“背叛性修改”
  • 验证现有测试能否捕获它
  • 记录这份证据证明什么,以及不证明什么。

这不是证明测试能发现所有问题。

它只证明:

针对这些被明确设计出来的语义退化,测试已经不只是检查函数能否运行,而开始具备阻止产品变形的能力。

最后,项目在干净克隆环境中通过了 44 个测试,12 项预设变异全部被测试杀死。

release checklist、仓库卫生检查、文档一致性检查和 Git 历史,共同构成了发布前证据。

对应提交:4c4fe7e

公开准备阶段,仓库又在隔离克隆中完成了历史隐私重写,并补充 Apache-2.0 许可证、SECURITY.md、Python 3.10 的 Windows/Ubuntu CI,以及 v0.1 规则数量和文档语义对齐。

干净克隆验证对应的候选提交是 7496b05。此后只新增 release evidence 和发布顺序措辞校准,没有改变分析实现,也没有扩大 v0.1 的规则范围。最终 v0.1 标签与 GitHub Release 指向 c5054ac;该提交的 Windows 与 Ubuntu CI 均通过 44 个测试和 12 项预设变异验证。

最终发布:v0.1(c5054ac) · Release Evidence

测试之外,还需要审查语义 diff

测试只能阻止已经被写成断言的退化。

但现实中,一次修改也可能引入尚未被契约和测试描述的新行为。

因此,在较大的改动完成后,我还会重新检查:

  • 本次 diff 是否改变了产品的对外含义
  • 是否产生了新的产品承诺或默认行为
  • 某类核心事实是否被移动到了另一个层级
  • 是否出现了第二个事实源
  • 是否把阶段护栏写成了长期原则
  • Agent 是否修改了原本未被授权修改的范围
  • 当前实现是否仍然位于允许的实现自由区内。

我把这种检查称为 语义 diff 审查。

它不是要求人重新逐行设计代码,而是检查:

代码的变化,是否改变了产品是什么、用户会观察到什么,以及谁拥有事实解释权。

如果语义 diff 暴露了新的边界,下一步不应只是“这次人工记住”,而应决定是否需要补充契约、测试、mutation 或工程护栏。

与相邻工作的关系

在完成这个项目的主要工程闭环后,我开始寻找外界是否存在相似实践。

我发现,这套做法并不是发生在完全空白的方向上。

Spec-Driven Development、human-in-the-loop software agents、coding-agent harness engineering、Acceptance Test-Driven Development 和 mutation testing 等方向,都在处理相邻问题:

  • 如何把意图外化为规格
  • 如何让人在关键阶段保留控制
  • 如何通过确定性检查点约束 Agent
  • 如何验证测试是否真的能够发现退化。

其中,Disciplined Agentic Engineering(DAE) 是目前与本项目高度相邻的公开实践之一。

DAE 已经将 charter、行为规格、人工批准、检查点、ATDD 和 mutation testing 工程化为面向 Claude Code 的工作流和插件。

它与本文存在明显汇合:

  • 都不认为提示词本身足以维持长程任务中的约束
  • 都强调人保留关键决策
  • 都将规格、测试和 mutation 作为控制机制
  • 都试图防止 Agent 在长程实现中偏离原始目标。

本文不主张这些组成部分由本项目首次提出。

二者的关注重心也并不完全相同。

DAE 更接近一套较完整、工具化的纪律化 Agent 工程流水线;本文则更集中于一个较窄的问题:

当 Agent 在不完整意图下拥有较高实现自由时,如何由人保留产品解释权,并将具体产品承诺转化为可检查的语义边界、背叛性 mutation 和具有明确证明范围的版本证据?

本文尤其关注:

  • 产品解释权
  • 长期原则和阶段护栏的区分
  • 单一事实源
  • 理解检查点
  • 语义 diff
  • 面向产品承诺的语义边界 mutation
  • Git 决策证据
  • 一项验证证明什么,以及不证明什么
  • 失败如何回灌下一轮控制回路。

这些差异目前只由一个单人、单项目案例支持,尚不能证明它们能够稳定迁移到其他项目和团队。

相关工作链接

这不是答案,而是一个面向复现的实验基线

我暂时把这套做法称为 Agent 协作控制回路:

人定义产品 -> 外化长期原则、阶段护栏与接口契约 -> 理解检查点暴露 Agent 的当前理解 -> Agent 在实现自由区内工作 -> 人审查语义 diff -> 测试与 mutation 验证已知退化 -> 文档、Git 与 release evidence 保存证据 -> 失败报告补强观测点 -> 回到下一轮理解检查点

这只是一个单人、单项目样本。

它不能证明对所有模型、团队或项目都有效,也不适合所有任务。

对于低风险、小规模、无需交付的一次性任务,完整流程可能过重。

当使用者无法判断产品边界,也不准备参与关键决策时,文档、测试和检查点同样不能自动生成正确判断。

但我认为,它碰到了一个值得继续验证的问题:

当 Agent 不只是补全代码,而会主动解释需求、补全语义、生成测试、同步文档并修改系统时,我们如何防止它把产品悄悄写成另一个东西?

我把项目、文档、测试、mutation 和 Git 历史公开出来,不是为了宣布答案,也不是为了证明它是一套普适方法。

我希望它首先成为一个可以被检查的实验起点:

  • 别人可以复现
  • 可以指出它在哪里失效
  • 可以提交相反证据
  • 可以比较不同 Agent、项目和团队中的成本与收益
  • 可以记录哪些控制机制真正生效,哪些最终只成为形式主义。

如果你在自己的项目中尝试了这套做法,欢迎同时记录:

  • 它在哪里有效
  • 在哪里失效
  • 哪些偏移仍然没有被提前发现
  • 哪些流程产生了不必要的成本
  • 你是如何发现和修复这些缺口的。

成功案例说明控制回路在哪里生效。

失败报告则说明,它接下来应该在哪里继续生长。

具体的操作模板、理解检查点、约束分类表、语义边界 mutation 设计方法、release evidence 模板和 failure report 模板,将在配套的结构化方法材料中单独发布。

许可证说明

本文正文采用 CC BY-SA 4.0 协议发布。

分享、转载、翻译或改编时,请按照许可条款提供适当署名、许可链接并注明修改;在合理可行时,也请保留原文链接和版本日期。基于本文正文的改编内容应使用相同协议共享。

配套的结构化方法材料和核心术语文档是独立作品,采用 CC BY-NC-SA 4.0,以各自文件中的许可声明为准。

转载、引用或改编不代表作者认可转载方、改编方或其产品。请勿将阶段性结论包装为通用定论。

项目名称、Logo 和官方版本标识不包含在上述 Creative Commons 许可范围内;除非另有书面授权,不授予商标、背书或官方代表权。

本页许可

CC BY-SA 4.0 引用、转载或改编时,请同时保留作者、原文链接、版本日期,并注明是否修改。