判断一个 AI 编码助手能不能进入真正的工程现场,最直接的标准不是它能不能生成漂亮代码,而是有没有稳定可用的 Code Mode:让模型在受控目录里读取文件、修改文件、执行命令、观察结果,再继续修到通过。Dex Horthy 发布的《AI That Works》第 72 期以“面向可扩展软件的 Code Mode”为主题,表面看是在介绍一种产品能力,实际讨论的却是工程问题:当项目会持续增长、模块会不断增加、多人会长期协作时,AI 生成的代码应当以什么方式进入代码库。
下面的内容不围绕某个具体产品,而围绕一条可复现的工程主线:Code Mode 到底是什么,为什么它和聊天问答有本质区别;要让代理安全地改代码,仓库需要做哪些准备;一次完整会话如何从任务说明走到合并请求;以及最常见的失败模式怎么排查。适合正在研究如何把 AI 编码工具引入项目的后端工程师、技术负责人和内部工具建设者。
1. 先理解 Code Mode:从“给一段代码”到“在项目里执行修改”
1.1 为什么要把 Code Mode 和普通对话模式分开
很多 AI 产品在界面上层会把交互分成不同的模式,有的叫 Ask,有的叫 Plan,有的叫 Agent,Code Mode 通常对应“可以直接修改工作区文件并执行命令”的那一档。名称有差异,但能力边界基本一致:Code Mode 不再只是生成一段文本让你自己复制,而是会操作真实文件、运行真实命令、读取真实报错,然后拿着报错继续修改。
这种差异看起来只是自动化程度不同,实际影响的是风险边界。普通对话模式下,模型最坏只是给了一段不够正确的代码,你复制过去后还有一层“人工粘贴、人工运行、人工看错”的保护。进入 Code Mode 后,模型可以直接改代码、运行测试、安装依赖,这意味着错误不再只停留在文本层,而会落到工作区里。错误后果也从“代码写得不对”扩展为“文件被改错、测试环境被污染、额外进程被启动”。
对于单个脚本项目,这种风险很小。但面向可扩展软件时,仓库里可能同时有十几个服务、上百个模块、严格的接口约定和稳定的历史行为。任何一个写权限配置错误,都可能让一次本应只改一个接口的任务同时触碰配置、迁移脚本、鉴权逻辑等无关区域。因此,讨论 Code Mode 时首先要建立的概念是:Code Mode 是一种可以执行真实修改的工程能力,而不是“说话更准确”的聊天窗口。
1.2 Code Mode 的本质是一个“感知、行动、验证”循环
Code Mode 真正区别于普通问答的地方,在于它进入了一个持续循环:
- 感知:读取任务文件、文件树、关键函数定义、现有测试和报错。
- 行动:在允许的目录中修改文件,或运行经过授权的命令。
- 验证:执行测试、静态检查、构建命令,观察是否通过。
- 再感知:根据失败输出定位问题,回到第 1 步,直到满足验收条件或触发停止条件。
Chat 模式下,模型给出的是一次性回答。Code Mode 下,模型每次行动后都会收到新的系统状态,相当于把一个长任务拆成了多轮“小决策”。这也是为什么处理复杂改动时需要 Code Mode:如果只是让模型一次性输出整个 diff,它很难处理中途冒出的编译错误、测试失败、数据结构不一致等问题;而循环式执行让它可以基于实际反馈修正。
| 对比点 | 普通对话模式 | Code Mode 工作流 |
|---|---|---|
| 直接产出 | 建议文本和代码块 | 工作区文件变更与验证结果 |
| 是否操作仓库文件 | 通常否 | 是 |
| 验证方式 | 用户手动复制运行 | 模型可执行测试并读取失败反馈 |
| 主要风险 | 代码贴错位置或漏改 | 改动越权、执行了不受控命令 |
| 人工工作重心 | 复制、粘贴、修改、验证 | 审阅 diff、检查验收结果、处理边界 |
1.3 为什么“可扩展软件”要单独强调 Code Mode
可扩展软件通常有两个特点:一是代码规模会持续增长,二是模块之间需要保持稳定的接口和契约。对 Code Mode 来说,这两个特点直接影响任务设计。
在只有几十个文件的仓库中,模型即使不依赖精确任务说明,也可能靠模糊猜测找到需要改动的位置。在几百个文件、多个模块的仓库中,模糊猜测会显著增加改错位置的概率。真正适合 Code Mode 的场景,是把大型改动拆成有明确边界的小任务,每一次改动都像一次小规模的重构或特性开发;这个任务越原子、越可验证,Code Mode 产出越可靠。
可以这样粗略理解 Code Mode 的实际价值:
Code Mode 的可靠性,大约等于任务说明的清晰度、测试覆盖的完整度、权限约束的严格度这三者的乘积。任何一项为零,整体结果都会变得不可靠。这也是“面向可扩展软件”这个视角下最值得记住的判断。
2. 改造代码库:让代理安全操作的前提条件
2.1 用“任务说明文件”启动,而不是用一句提示词启动
很多人使用 AI 编码工具时,习惯在对话框里写“请给订单接口加分页”,然后期待模型自己把整个项目搞清楚。在小项目里这可能行得通,因为模型能看到的上下文足够还原需求;但在可扩展软件中,上下文往往不够,模型会靠猜测补全,于是产生“改了文件但改错位置”“加了参数但破坏调用方”等问题。
更稳妥的启动方式是把任务写成独立文件,放在仓库的tasks目录下。任务文件至少包含“为什么做、目标、涉及文件、验收标准、禁止项”五部分。
# 任务 T-72-001:给订单查询接口增加分页 ## 为什么做 当前 `/api/orders` 一次返回全量订单,订单量增长后响应时间变长。 这是接口容量扩展的第一步,先把数据获取方式改成可分页查询。 ## 目标 - 在 `src/order/query.py` 中支持 `limit` 与 `offset` 参数。 - 默认 `limit=20`,上限 `100`。 - 返回结构保持不变:`items`、`total`。 - 不能引入数据库全表扫描。 ## 涉及文件 - `src/order/query.py` - `tests/test_order.py` - `docs/api/order.md` ## 验收标准 1. 新增测试覆盖默认值、越界值、正常翻页。 2. 运行 `pytest tests/test_order.py -q` 全部通过。 3. `git diff --stat` 只显示上述三个文件。 ## 禁止项 - 不要修改鉴权、配置和迁移模块。 - 不要格式化与任务无关的文件。“禁止项”看起来是防御性要求,实际上非常关键。它等价于告诉模型哪些区域即使看起来相关也不能碰,避免它在修复边界问题时顺手改掉其他逻辑。涉及文件的清单也不是约束,而是帮助模型把检索范围收敛到可扩展软件中的局部模块。
2.2 目录权限分成三类:可写、只读、禁止
Code Mode 工具通常会提供工作区或权限配置。即使工具本身没有严格权限控制,也应该在任务启动前约定三层目录边界:
- 可写目录:本次任务允许修改的源码和测试目录。
- 只读目录:可以读取用于理解背景,但不能修改的公共模块、接口定义、配置模板。
- 禁止目录:密钥、环境变量、生产配置、CI 配置、历史迁移等不应被触碰的区域。
下面是一个示意配置,实际产品参数名会不同,但结构可以复用。
{ "workspace": "./workspace/order-svc", "readWriteDirs": ["src/order", "tests"], "readOnlyDirs": ["src/shared", "migrations"], "denyDirs": [".git", "secrets", "config/production"], "allowedCommands": ["python", "pytest", "git status", "git diff", "ls"], "verifyCommands": ["pytest tests/test_order.py -q"] }推荐的默认策略是“最小化写权限”:一开始把所有目录都设为只读,只把本次任务明确涉及的文件目录加入可写白名单。如果任务执行中发现还需要改动一个额外文件,正确的做法是中断任务、更新权限配置、重新启动,而不是放任模型自行扩大范围。这样做的代价是操作上多了一步,收益是每一次改动范围都可审计。
2.3 依赖、基线测试和密钥隔离要提前处理
在启动 Code Mode 会话之前,至少要确认三件事:
第一,依赖已经安装完成,并且版本已经通过 lock 文件锁定。不要让代理在会话中途执行网络安装,否则会产生不可重现的环境。也不要让代理自己选择依赖版本,否则很容易“装上刚好能跑但不是项目约束的版本”。
cd workspace/order-svc git checkout -b feat/order-pagination python -m venv .venv .venv/bin/pip install -r requirements-dev.txt第二,先记录基线测试结果。只要 Baseline 是绿色的,后续出现的失败就可以明确归因于本次 Code Mode 会话产生的改动;如果 Baseline 本身是红的,代理很容易在修复旧问题和实现新功能之间来回拉扯。
.venv/bin/pytest -q第三,密钥与日志隔离。Code Mode 会运行命令并读取输出,环境变量中不应注入生产密钥;临时调试信息也不应打印到测试日志中。更稳妥的做法是给代理使用独立的测试数据库,并且确认.env、secrets等目录不会进入任何可写范围。
3. 走通一次 Code Mode:分页接口改造的最小闭环
3.1 用一个命令把任务、权限和验证串起来
当仓库结构、权限目录、任务文件都准备好后,一次 Code Mode 会话可以按下面的方式启动。下面的命令是示意写法,具体工具会使用自己的参数名,但核心要素应该一致:指定模式、指定任务、指定可写范围、指定验证命令。
agent start \ --mode code \ --task tasks/T-72-001-add-pagination.md \ --allow-write src/order tests/test_order.py \ --read-only src/shared migrations \ --verify "pytest tests/test_order.py -q"启动后,模型会先读取任务文件,再定位涉及的源码。关键点在于,不要让它在一开始就提交任何内容。整个会话应该停留在“有 diff、未提交”的状态,后续由人工决定是否提交。
3.2 观察模型如何完成一次小改造
以一个简单的订单查询函数为例。改造前的代码可能是:
def list_orders(db, user_id): sql = "SELECT * FROM orders WHERE user_id = ?" return db.execute(sql, (user_id,)).fetchall()任务目标是增加分页参数,而返回结构要稳定。一个合理的改造结果如下:
def list_orders(db, user_id, limit=20, offset=0): limit = max(1, min(limit, 100)) offset = max(0, offset) sql = ( "SELECT * FROM orders WHERE user_id = ? " "ORDER BY id DESC LIMIT ? OFFSET ?" ) rows = db.execute(sql, (user_id, limit, offset)).fetchall() total_sql = "SELECT COUNT(*) AS c FROM orders WHERE user_id = ?" total = db.execute(total_sql, (user_id,)).fetchone()["c"] return {"items": rows, "total": total}需要注意的是,这里的limit被限制在 1 到 100 之间,offset不允许为负数。这就是验收标准中“越界值”应该覆盖的路径。如果模型只加了两个参数,却没有处理边界,说明它对验收标准的理解还不完整,需要返回修正。
对应的最小测试可以写成:
def test_list_orders_default_limit(db, fake_orders): result = list_orders(db, user_id=1) assert len(result["items"]) == 20 assert result["total"] == 50 def test_list_orders_limit_max(db, fake_orders): result = list_orders(db, user_id=1, limit=1000) assert len(result["items"]) == 100 assert result["total"] == 50 def test_list_orders_offset(db, fake_orders): first = list_orders(db, user_id=1, limit=10, offset=0) second = list_orders(db, user_id=1, limit=10, offset=10) assert [item["id"] for item in first] != [item["id"] for item in second]这三个测试分别覆盖默认值、越界值和翻页行为。如果模型在实现时使用了错误的返回结构,或者没有处理limit上限,测试会直接抛出失败。这就是 Code Mode 中“验证循环”的价值:测试不是事后补充,而是驱动模型修正行为的反馈信号。
3.3 验证与合入前的检查点
当 Code Mode 会话报告“完成”后,不要立刻信任它的结论。人工需要按顺序执行一组命令,确认产物的实际状态:
git diff --stat git diff --check .venv/bin/pytest tests/test_order.py -q一个正常的结果可能类似于:
src/order/query.py | 18 +++++++------- tests/test_order.py | 31 ++++++++++++++++++++++ 2 files changed, 49 insertions(+), 6 deletions(-) 3 passed in 0.12s在这个检查点里,至少要确认三件事:改动是否只落在任务允许的范围内;代码是否通过静态层面的空白和换行检查;新增测试是否真正覆盖了新的代码路径。只有这三项都通过,才应该把改动放入待审阅的分支。
4. 关键参数与取舍:权限、上下文和迭代上限
4.1 常用配置参数速查
Code Mode 工具的能力差异很大,但配置项可以归纳成一张通用的参数表。理解每个参数“调大调小会怎样”,比记住某个产品的具体字段更重要。
| 参数 | 含义 | 常见值 | 调大的影响 | 调小的影响 |
|---|---|---|---|---|
readWriteDirs | 允许修改的目录 | 本次任务源码与测试目录 | 模型操作范围更大,误改风险上升 | 任务可能因权限不足而无法完成 |
readOnlyDirs | 可读但不可写目录 | 公共模块、接口定义 | 便于理解结构 | 边界过严时模型缺少背景信息 |
allowedCommands | 允许执行的命令 | pytest、git diff、ls | 排查能力更强 | 模型无法验证修改结果 |
maxSteps | 最大的行动轮数 | 20 到 50 | 有机会处理复杂失败 | 防止无限循环,但可能提前中断 |
verifyCommands | 每次修改后运行的验证命令 | 相关模块测试 | 及时发现回归 | 只在最后验证,问题定位困难 |
autoCommit | 是否自动提交 | 建议关闭 | 流程更快 | 需要人工确认才能产生提交 |
最容易被忽略的是autoCommit。在可扩展软件场景中,不建议让 Code Mode 自动提交到分支,更不建议自动推送到远程。把产物保持在“未提交的 diff”状态,人工审阅后统一提交,才能保证每一笔进入代码库的改动都经过明确确认。
4.2 迭代上限:让模型在有限轮数内收敛
Code Mode 执行过程中可能遇到反复修改同一类错误的情况。如果没有任何上限,模型会一直尝试,浪费执行时间,也可能在最后一次尝试中引入更大范围的重构。
工程上建议给会话设置两类停止条件:一是验证命令通过,二是达到最大轮数。达到最大轮数后,不要直接让模型“再想想”,而是收集现有失败日志,分析是在哪个环节卡住,再决定是调整任务说明、补充测试还是缩小改动范围。这就像人工开发遇到困难时,应该先定位问题而不是盲目重试。
agent start \ --task tasks/T-72-001-add-pagination.md \ --max-steps 30 \ --stop-on-verify-pass如果 30 轮内没有通过,通常说明任务粒度太大,或者允许写入的范围太宽,使模型在探索上花费了过多轮次。此时把任务再拆小,会是更有效的处理方式。
4.3 上下文管理:不要让代理一次性读完整文件
可扩展软件的源码文件往往较长。如果模型在定位问题时直接读取整个文件,很快就会消耗大量上下文窗口,导致后续真正需要关注的概念、接口和日志没有空间。观察 Code Mode 会话时,最常见的问题不是“模型不会改”,而是“模型把上下文浪费在了无关文件上”。
可以在启动时的系统约束里加入类似于下面的执行顺序:
处理任务时按以下顺序操作: 1. 先运行 find 和 grep 定位模块结构,不要直接 cat 整个大文件。 2. 定位函数定义后,只读取函数前后各 20 行上下文。 3. 每次修改后只运行相关模块测试,不要一开始就跑全量测试。 4. 完整报错写入日志文件后,读取最后 200 行,不要读完整 stdout。 5. 全部完成后再整体查看 git diff,给出变更摘要。日志处理也应遵循同样的思路。将测试输出重定向到文件,再让模型读取文件尾部,可以有效避免大量输出刷掉上下文。
.venv/bin/pytest tests/test_order.py -q > /tmp/agent_test.log 2>&1 tail -n 200 /tmp/agent_test.log5. 常见失败模式与排查路径
5.1 从“基线、权限、日志、diff”四个层面倒推
当一次 Code Mode 会话的结果不符合预期时,不要立刻怀疑模型能力,先按固定顺序排查:
- 环境基线是否正常。启动前测试是否为绿色;如果启动前就是红的,本次会话结果不能作为判断依据。
- 任务说明是否准确。有没有写明“涉及文件”和“禁止项”;如果一句需求启动,模型改错位置几乎是必然结果。
- 权限配置是否生效。查看会话执行记录,确认模型是否尝试读取只读目录、写入禁止目录、运行白名单之外的命令。
- 日志是否完整。验证命令如果真的执行了,会返回退出码和输出;如果根本没有执行日志,就谈不上“测试通过”。
- diff 是否符合预期。最后通过
git diff确认实际改动,而不是相信模型的文字总结。
这个顺序的价值在于,它能避免在错误层面做无意义的重试。权限配置错了,重新生成十次也没有用;任务说明模糊,给模型更多轮数只会让错误的改动范围变得更大。
5.2 典型现象与处理方案
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 模型改了任务之外的文件 | 可写目录过宽或任务说明中缺少禁止项 | git diff --stat对照任务文件确认范围 | 撤销无关改动,收紧readWriteDirs,补全禁止项 |
| 新增功能成功但旧测试变红 | 只验证了新场景,忽略了回归 | 检查每次修改后是否运行相关模块全部测试 | 将验证命令设为相关模块测试全集,不要只跑新增用例 |
| 模型说“测试通过”,但测试实际没有执行 | 命令被吞、权限失败、路径错误 | 查看执行日志中的命令和退出码 | 将验证命令执行的完整输出落盘,确认退出码 |
| 会话中途上下文超长 | 模型读取了过多大文件或完整日志 | 观察哪一步 token 消耗增长最快 | 规定只能使用 grep 定位和 tail 读取末尾日志 |
| 改动反复不收敛 | 任务粒度过大,模型在多个问题间来回切换 | 查看最近几轮是否都在同一处失败 | 拆解子任务,一次只解决一个文件或一个功能点 |
| 测试通过但行为错误 | 测试断言太弱,没有覆盖新代码路径 | 检查新增测试是否在改动前会失败 | 先写失败测试,再让 Code Mode 实现代码 |
5.3 不要接受“没有失败日志的成功”
Code Mode 会话中最隐蔽的失败是“假成功”。模型可能输出“已完成,所有测试通过”,但实际执行记录里测试根本没有运行,或者运行的是错误路径上的测试文件。
避免假成功的办法有两个。第一,验证命令要带退出码检查,让真正失败时能留下明确信号。第二,人工重新执行一次验证命令,不依赖模型的自述。即使模型的修改完全正确,这一步也不能省略,因为人工重跑验证本身就是进入代码库前的门禁。
.venv/bin/pytest tests/test_order.py -q echo "exit=$?"看到exit=0才能视为通过。如果命令因为路径错误根本没有运行,退出码会明确暴露问题,而不是被模型的语言描述掩盖。
6. 面向可扩展软件的 Code Mode 落地规范
6.1 任务分解:一次 Code Mode 只做一个原子变更
可扩展软件中的改动往往横跨接口、实现、测试、文档。如果让模型在一个会话里完成全链路改动,任务说明会很长,模型也很难确定每一步的优先级。更稳妥的分解方式是让一次 Code Mode 只处理一个原子变更。
以分页接口为例,可以拆成三次独立会话:
- 第一次:在数据访问层增加带
limit和offset的查询函数,并补齐单元测试。 - 第二次:在 API 层暴露新参数,同时保持旧的默认行为。
- 第三次:更新接口文档,补充调用示例和参数说明。
每一次会话都有独立的涉及文件、验收标准和禁止项,排错时定位更清晰,代码审查的粒度也更合适。面向可扩展软件时,“任务越小,可验证性越强”,这一原则比追求单次生成代码量更重要。
6.2 发布前的双重门禁:模型执行、人工审查、CI 复验
Code Mode 完成后的改动,至少要经过三道门禁才能进入主干:
第一道是执行验证。任务规定的测试、静态检查和构建命令都必须真实执行并返回成功。
第二道是人工审查。审查者重点看git diff是否只涉及任务范围内的文件、是否存在绕过边界的行为、测试是否覆盖了真实的新路径、是否引入敏感信息或硬编码配置。
第三道是 CI 复验。本地环境容易存在“本地能过、CI 不过”的差异,因此本地验证通过不等于 CI 绿灯。真正合入前,至少要跑一次基于隔离环境的完整流水线。
结合 Code Mode 场景,发布前可以按下面的清单逐项确认:
- [ ] 启动前已记录 Baseline 测试为绿色。
- [ ] 任务说明文件包含目标、涉及文件、验收标准和禁止项。
- [ ]
git diff --stat的改动范围与任务说明一致。 - [ ] 新增测试在改动前会失败,改动后能通过。
- [ ] 所有相关旧测试没有被修改或变红。
- [ ] 验证命令由人工重新执行,退出码为 0。
- [ ] 代码中未出现密钥、绝对路径、调试日志和环境依赖。
- [ ] 没有自动提交或自动推送行为。
6.3 区分学习环境、开发环境与生产环境的使用方式
Code Mode 可以用于不同成熟度的环境,但约束强度应该不同。
学习环境可以使用一次性容器,代码库无敏感信息,即使模型误删文件或安装错误依赖,销毁容器即可恢复。开发环境应该使用独立分支,并在启动前确认本地基线测试为绿色。生产环境则需要更严格的门禁:历史不可变、权限最小化、所有操作保留可审计日志、代码必须经过人工审查和 CI。
| 环境 | Code Mode 使用方式 | 关键控制 |
|---|---|---|
| 学习环境 | 直接操作演示仓库 | 容器隔离、无真实密钥 |
| 开发环境 | 本地分支内执行 | 基线测试、任务文件、git diff 审查 |
| 生产环境 | 原则上不直接操作 | 代码审查、CI 复验、权限审计、回滚预案 |
生产环境真正关心的不是“模型能不能写代码”,而是“一段代码进入生产前的过程是否可控”。过程可控,Code Mode 才可能成为可扩展软件开发流程的一部分;过程不可控,即使模型输出质量再高,风险也会抵消收益。
7. 收尾:把 Code Mode 作为工程流程而不只是工具能力
7.1 一次会话成功不等于长期可靠
在一个能长期增长的软件项目里,Code Mode 能否稳定发挥作用,取决于三件持续性工作:任务说明是否越写越清晰,测试体系是否足够支撑验证,权限和门禁是否能在每次会话中严格执行。工具会升级,模型会变强,但如果这三件事没有建立起来,任何一次成功都可能只是偶然。
从《AI That Works》第 72 期的讨论看,真正面向可扩展软件的 Code Mode,已经不再是“提示词技巧”问题,而是把 AI 执行纳入软件工程流程的问题。这和人类开发者进入团队后要接受的工程约束在方向上完全一致。
7.2 建议的第一个练习
如果想把 Code Mode 引入自己的项目,不必一开始就改造大模块。可以找一个现有的小接口,按照本文的模板写一份任务说明,允许修改只限于一个源码文件和一个测试文件,然后观察整个会话过程。
重点关注三件事:
- 模型是否在限定文件内完成了改动。
- 测试是否真正验证了改动后的行为。
- 人工审阅 diff 时,能否快速判断改动是否正确。
把这一套最小流程跑通之后,再逐步扩大任务范围、增加模块数量、加入 CI 门禁。对新手来说,最需要养成的不是“让模型写更多代码”的习惯,而是“让模型在更小范围内把代码确认正确”的习惯。Code Mode 的可靠边界,最终取决于你为它设计的工程边界。