文档驱动Agent:把行为规范写进代码,让AI编程更可控
1. 为什么这条帖子值得关注问题的本质不是模型的强弱而是没有把“预期”写下来这两年用 AI 辅助编程的人越来越多但一个现象也变得越来越常见同一个模型同一段代码在不同人手里结果差异极大。有人觉得智能体很聪明改几个文件、跑一轮测试就能交付有人却觉得它只是“高级补全工具”要么改错地方要么反复绕圈子。这个差异往往不是模型能力的问题而是上下文工程的问题。最近有一个思路很值得关注Show HN 上出现了一个方向叫 “Get agents to do what I want with code documentation”核心观点非常直接想让 agent 真正按你的意图执行与其一直去调系统提示词不如把期望写进代码文档里。文档不只是给人看的注释更应该是 agent 的“行为规范”。这篇文章不是推荐某一个具体工具而是想把这条思路背后的问题意识、原理、落地方式和容易踩的坑讲清楚。文章会围绕几件事展开agent 为什么经常“不听话”根本原因在哪。代码文档为什么是约束 agent 行为的好载体。如何设计一份“能驱动 agent”的文档而不是普通备注。如何用文档驱动的思路跑通一个最小示例。以及失败时怎么排查生产环境怎么落地。如果你正在做 agent 应用、或者在用 AI 编程助手重构项目这篇文章应该能帮你省下不少调试时间。2. 核心概念文档驱动 Agent 行为到底在强调什么2.1 “文档驱动”不等于写注释很多团队过去写文档是为了给下一任维护者看或者为了过代码评审。但这条路线的核心在于文档是给 agent 当“可执行规格说明”用的。在传统软件开发里代码和文档经常发生“漂移”代码改了文档没改最后文档失去信任。但在 agent 驱动的开发流程里文档的最大作用是稳定系统提示词之外的非确定性因素。Prompt 是瞬时的写在会话开始时很容易被对话历史稀释。而文档是持久的它放在代码库中每次 agent 读取代码时几乎都会遇到。也就是说你可以通过一份设计良好的 README、文档字符串或 spec 文件让 agent 在每次调用函数、修改模块、运行测试时都“看见”你期望的行为边界。2.2 Agent 的“意图不确定性”从哪里来从实践角度看agent 行为不稳定通常是三层原因叠加造成的第一层是模型本身的概率性。同样的 prompt 多次调用结果不完全一样。 第二层是上下文窗口的稀缺性。当代码库很大时很多细节可能根本没被 agent 读取到。 第三层是表达层的模糊性。你脑子里很清楚“要兼容旧接口”但如果没有写下来agent 只能靠猜。文档驱动要解决的主要是第三层并且间接缓解第二层。它的做法不是让文档变得更长而是让文档变得更“结构清晰、意图明确”。例如一个函数如果只写“处理用户输入”那 agent 可能在五个方向里选一个如果文档写明输入必须是什么格式哪些情况应该拒绝返回错误使用什么类型禁止调用哪些底层 API。agent 的选择空间就大大缩小了。2.3 这是“行为规范”不是普通说明稍等这里有一个很重要的概念需要区分行为规范specification与普通说明description。普通说明描述当前代码“是什么”。比如get_user 函数用于获取用户信息。行为规范描述代码“应该做什么、不应该做什么、在什么条件下做什么”。比如get_user(id) 行为规范 - 仅接受整数型 id字符串数字需先转换。 - 当 id 0 时返回 UserNotFoundError。 - 禁止直接拼接 SQL必须走 user_repository。 - 如果用户不存在记录一条 WARN 日志返回 None。 - 本函数不允许调用外部 HTTP 服务。两者的差别就像“这里有一台冰箱”和“冰箱使用手册中规定了开门时间、温度范围、存放要求”之间的差别。agent 拿到普通说明只能知道函数大意拿到行为规范才能按边界执行。3. 为什么代码文档是 agent 行为约束的好载体3.1 代码文档离“执行现场”最近要让 agent 根据指导行动指导信息必须出现在它做决策的地方。对于代码生成型 agent 来说决策现场就是正在编辑的代码文件。如果行为说明放在外部 Wiki、在线文档或者口头沟通里agent 很可能根本看不到。但如果你把规范放在同目录下的 README、模块级文档字符串、或者类型注释里agent 在读取代码时就一定会读到。这也是文档驱动的优势降低上下文注入的成本。你没有必要把整本产品手册都塞给大模型只需要把与当前文件、当前函数、当前模块相关的规范写好让 agent 在上下文窗口里“就近命中”。3.2 文档是稳定资产可以复用、评审、回滚在实际软件工程里提示词散落在聊天记录里是灾难。今天你给 agent 说“不要用 requests 库”明天新增一个文件时它可能又用了。但如果把这类约定写进项目文档相当于在代码库里建立了“标准操作程序”。每次 agent 读项目文件时约定自然被纳入上下文。另一个好处是可评审性。团队代码评审时你能拿着文档逐条检查 agent 的产出是否符合规范而不需要依赖“我看它表现还行”这种模糊判断。3.3 对交互式 agent 尤其有效这里要提一个很多人容易忽略的场景交互式 agent比如 Playwright test agents。如果你用 Playwright 写 UI 测试agent 需要知道页面结构、需要点击的元素、等待策略、是否需要处理弹窗等。这些信息如果只存在于 agent 的对话上下文里一旦测试场景变长agent 很容易在前面步骤中迷失。把测试行为规范写进测试文件头部的 docstring例如本文件是用户登录流程的 UI 自动化测试。 执行规则 - 所有定位优先使用># AGENTS.md 本项目是订单管理系统后端。 ## 技术栈 - Python 3.11 - FastAPI - PostgreSQL - SQLAlchemy 2.x ## 全局约定 1. 所有新增接口必须提供 Pydantic 请求模型禁止直接返回 dict。 2. 数据库操作必须走 repository 层禁止在路由函数中直接写 SQL。 3. 时间字段统一使用 UTC禁止使用本地时间。 4. 所有外部 API 调用必须添加超时与重试机制。 5. 禁止在日志中输出用户手机号、身份证号等敏感字段。 ## 常用命令 - 启动开发服务: uvicorn app.main:app --reload - 运行测试: pytest tests/ -v - 代码格式: ruff check src/ ruff format src/这样一个文件能让 agent 在生成代码时从一开始就带上项目的工程约定。4.2 函数级规范怎么写才有效函数级 docstring 是 agent 最常读取的信息源。写得好的标准是“机器可以直接执行”。以 Python 风格为例# 文件路径src/services/user_service.py from typing import Optional class UserNotFoundError(Exception): 用户不存在时抛出。 def get_user_by_id(user_id: int) - Optional[dict]: 根据用户 ID 获取用户信息。 Behavior Specification: - 唯一入口禁止绕过此函数直接访问 UserRepository。 - user_id 必须是 int大于 0否则抛出 ValueError。 - 用户不存在时抛出 UserNotFoundError不返回 None。 - 查询时必须排除 deletedTrue 的软删除用户。 - 此函数不允许调用外部 HTTP 服务。 Returns: 包含用户核心信息的字典字段包括: id, name, email, created_at。 Raises: ValueError: user_id 非正整数。 UserNotFoundError: 用户不存在或已被软删除。 ...这里的关键不只是写出参数类型而是把“行为边界”写明。agent 在实现这个函数时遇到边界情况会优先参照这些约束而不是凭空发挥。4.3 用 Spec 文件管理复杂业务规则当规则较多时函数 docstring 会变得臃肿。这时候可以单独建立一个 specs/ 目录用 Markdown 文件描述复杂业务规则。例如处理退款流程# specs/refund_rule.md # 退款行为规范 ## 适用场景 - 用户发起退款 - 客服后台发起退款 ## 规则 1. 订单状态必须为 paid否则拒绝退款。 2. 退款金额不得大于订单实付金额。 3. 已发货订单必须经过人工审核。 4. 退款成功后必须发送站内信和邮件通知。 5. 禁止在事务未提交前发送通知。 ## 禁止事项 - 禁止直接删除订单记录。 - 禁止修改订单历史快照。 ## 异常处理 - 退款接口调用失败时必须将任务写入 retry_queue。 - 重试次数上限为 3 次超过后标记 failed。文件可以很短但边界必须明确。agent 在实现退款相关代码时如果 prompt 引用了这个 spec行为会显著收敛。5. 最小示例用文档让 Agent 实现一个带约束的功能这一节我们来跑一个完整的示例。目标不是依赖某个具体 AI 工具而是展示文档驱动流程的通用模式。5.1 需求背景假设我们有一个简单项目需要让 agent 实现一个parse_duration函数把字符串时长解析成秒数。用户希望在 prompt 里少说废话所有约束都写在源代码文档中。项目目录duration_project/ ├── AGENTS.md └── src/ └── parser.py5.2 AGENTS.md 内容# AGENTS.md ## 项目目标 提供时长解析工具。 ## 已安装依赖 - Python 3.11 标准库不额外安装第三方包。 ## 全局约定 - 所有函数必须带有 Behavior Specification 文档。 - 禁止处理负数时间。 - 禁止使用 datetime 模块解析时长。 - 单元测试放在 tests/ 目录。5.3 源码文件中的函数规范在 src/parser.py 中先写空实现和完整规范# 文件路径src/parser.py def parse_duration(value: str) - int: 解析时长字符串并返回总秒数。 Behavior Specification: - 仅接受字符串输入。非字符串直接抛 TypeError。 - 支持格式: 30s, 5m, 2h, 1d, 1h30m, 90s。 - 数字必须是正数不支持负数、小数、科学计数法。 - 不允许混合大小写统一使用小写单位。 - 没有单位时默认为秒。 - 单位不合法时抛出 ValueError。 - 本函数不允许使用 datetime.timedelta 直接解析。 # 待实现 ...5.4 Agent 的 Prompt 只需要很短请实现 src/parser.py 中的 parse_duration 函数。 先阅读 AGENTS.md 和 src/parser.py 的 Behavior Specification。 按规范实现并补充一个最小测试。注意我们没有在 prompt 里重复任何规则。规则已经在文档里。5.5 一个合理的实现结果下面是一个满足约束的实现示例展示 agent 应该产出的近似结果# 文件路径src/parser.py import re _UNITS { s: 1, m: 60, h: 3600, d: 86400, } def parse_duration(value: str) - int: 解析时长字符串并返回总秒数。 Behavior Specification: - 仅接受字符串输入。非字符串直接抛 TypeError。 - 支持格式: 30s, 5m, 2h, 1d, 1h30m, 90s。 - 数字必须是正数不支持负数、小数、科学计数法。 - 不允许混合大小写统一使用小写单位。 - 没有单位时默认为秒。 - 单位不合法时抛出 ValueError。 - 本函数不允许使用 datetime.timedelta 直接解析。 - 遇到重复单位时累加。 if not isinstance(value, str): raise TypeError(fvalue must be str, got {type(value).__name__}) pattern r(\d)([smhd])? matches re.findall(pattern, value) consumed_length sum(len(num) len(unit) for num, unit in matches) if consumed_length ! len(value): raise ValueError(fInvalid duration format: {value!r}) total 0 for num, unit in matches: total int(num) * _UNITS.get(unit, 1) return total5.6 测试用例建议# 文件路径tests/test_parser.py import pytest from src.parser import parse_duration def test_parse_seconds(): assert parse_duration(30s) 30 def test_parse_minutes(): assert parse_duration(5m) 300 def test_parse_hours(): assert parse_duration(2h) 7200 def test_parse_days(): assert parse_duration(1d) 86400 def test_parse_combined(): assert parse_duration(1h30m) 5400 def test_parse_without_unit(): assert parse_duration(90) 90 def test_parse_invalid_unit(): with pytest.raises(ValueError): parse_duration(10x) def test_parse_negative_should_fail(): with pytest.raises(ValueError): parse_duration(-30s) def test_parse_non_string(): with pytest.raises(TypeError): parse_duration(30) def test_parse_repeated_units(): assert parse_duration(1m30s) 90这里要强调的是agent 的实现如果没有文档约束可能会用datetime.timedelta可能会允许负数也可能会把90直接当作非法输入。但有了行为规范它的自由度被大大限制。6. 如何验证文档驱动的效果6.1 不只验证“能不能跑”还要验证“行为是否收敛”很多人在评估 agent 时只看测试是否通过。但在文档驱动方法里你还要关注另一个维度在多次生成中agent 是否选择了相同的实现路径。比如同样让 agent 实现parse_duration如果第一次生成用正则第二次用timedelta第三次用硬编码判断说明文档并没有真正约束住行为。一个简单的验证方法是固定同一文档和同一 prompt运行多次生成检查实现方案是否一致是否遵守了禁止项是否生成了冗余代码是否误解了边界条件。6.2 用回归测试保证规范性行为规范最好配套对应测试。如果某个规则可以被自动化验证就这样写def test_parse_duration_does_not_use_datetime_timedelta(): import inspect import src.parser as parser source inspect.getsource(parser.parse_duration) assert timedelta not in source这种测试不算优雅但在约束 agent 产出时往往很有效。它把文档里的“禁止事项”变成了可执行的检查。6.3 区分任务成功与任务可复现如果你使用具备长任务执行能力的 agent还会遇到另一个问题任务在中断后能否继续按原目标运行。这正好呼应了一个重要的点deep agents interrupt。在 agent 长时间运行过程中用户可能正在对话中插入新指令或者要求 agent 修正中间结果。如果 agent 只依赖对话上下文一旦中断它可能丢失原始目标。文档驱动的优势在这里就很明显当 agent 需要重新聚焦时它回到代码文件就能重新读到“目标”和“边界”不需要依赖很早之前的对话记录。因此验证时除了关注最终结果还要关注中途打断后的恢复能力在 agent 运行到一半时插入一条新指令然后要求它继续原来的任务观察它是否仍然遵守文档中的行为规范。如果它能回到文档、重新读取约束说明这套方案是有效的。7. 常见问题与排查思路在实际落地中文档驱动方法也会出现问题。下表列出常见现象与排查方向。问题现象可能原因排查方式解决方案agent 忽略了文档中的禁止项文档位置离目标代码太远上下文未被读取检查 agent 是否读取了完整项目文件观察日志中的上下文摘要将关键禁止项复制到模块级 docstring 或目标文件头部文档太长agent 抓不住重点行为规范写成了大段散文缺乏边界列表检查文档结构是否包含“规则”“禁止事项”“异常处理”改用列表、短句、明确的关键词多次生成结果差异大文档只写了功能没写实现边界对比多次产出的实现方案补充实现约束、禁止调用 API、禁止使用某类语法函数级 docstring 没问题但模块行为跑偏缺少项目级或模块级规范先看 AGENTS.md 是否存在建立分层文档体系修改文档后agent 行为没有变化工具缓存了旧文档或 prompt 没有引用该文档清缓存、重新构建索引检查 prompt 中是否指定读取路径在 prompt 中显式要求 agent 阅读相关文档测试场景过长agent 中途改变规则依赖交互式上下文缺少持久化约束在长任务中打断 agent 并观察行为把关键规则下沉到测试文件或 spec 文件agent 过度遵守规范导致代码僵化规范中的边界条件写得过于细碎检查是否把非关键实现细节写死只约束对外行为和项目约定保留内部实现自由排查有一个通用顺序先确认文档是否被读取然后确认文档是否有明确边界最后确认 prompt 是否引入冲突指令。大部分问题出在前两步。8. 最佳实践与工程建议8.1 从“给人类看”到“给 Agent 看”的文档改造清单如果你准备在现有项目中应用文档驱动方法不必大规模重写文档。只需要按以下优先级处理先建立根目录的 AGENTS.md写清楚技术栈、常用命令、全局禁止项。为核心业务模块增加模块级 docstring写清楚模块边界。为高频修改的函数增加 Behavior Specification。把某些业务规则独立成 spec 文件并让代码注释指向该文件。在 prompt 固定模板中统一要求 agent 先阅读文档再动手。8.2 文档中的关键词与行为约束为了让文档更容易被 agent 解析建议使用稳定关键词Behavior Specification规则禁止事项边界条件异常处理Raises这些词本身并不神奇作用是让文档结构保持稳定。很多 agent 在判断“这条是约束还是背景描述”时依赖标题和关键词。8.3 不要把所有东西都写进文档文档驱动虽然有价值但过度使用同样有问题。不要把每个函数内部的算法细节、每行代码的选择理由都写进去。文档应该描述“外部行为和约束”不要描述“内部实现步骤”。例如不要写循环遍历列表将每个元素转为整数如果转换失败则跳过。这是实现细节限制了 agent 的自由度。应当写该函数接收字符串列表返回整数列表。 无法转换的元素跳过不抛异常不写日志。这才是行为规范。8.4 与提示词系统的分工文档驱动不是要替代提示词而是要分工。提示词负责描述“当前任务”比如“实现这个模块”“修复这个问题”。文档负责描述“长期不变的约定”比如 API 风格、事务边界、异常策略、禁止事项。提示词是瞬时的文档是持久的。如果把长期约定也写在 prompt 里每次调用都会重复消耗 token而且容易前后不一致。8.5 生产环境落地顺序在产线环境引入文档驱动模式时不建议一次性改全量。推荐按照以下顺序选择 1 到 2 个维护频繁的模块作为试点。为试点模块补充项目级和模块级规范。设置行为对比指标生成一致性、测试通过率、代码评审修改次数。运行至少一周对比文档驱动前后的输出质量。验证有效后再扩展到其他模块。这样既能控制风险也能沉淀出适合团队的规范模板。8.6 结合测试 Agent 的落地场景如果你正在建设自动化测试 agent比如用 Playwright 实现 UI 测试生成建议在测试文件头部加入约束文档。一个典型的做法登录流程自动化测试。 Behavior Specification: - 定位元素优先使用>