做完这套东西,你会得到一个这样的工作流:在仓库里给某个 Issue 打上一个 auto-fix 标签,几分钟后出现一个 PR——里面有复现 bug 的新测试、一处最小改动的修复、一份机器人自己写的审查意见,以及跑绿的 CI。你唯一要做的事是点开 PR,看一眼 diff,点 Merge 或者打回去。
这篇文章不教你训练模型,只教你把 Codex 这类编码智能体嵌进一个真实仓库的日常流程里。整个过程可以拆成三件事:让仓库变得"机器可读"、把权限收紧到刚好够用、把"改代码"拆成复现、修复、审查、提交四段并分别约束。
前置条件清单
动手前先确认这几件事,缺一件后面都会卡住:
- 一个有像样测试的仓库。 没有测试的仓库跑自动修 Bug,等于让一个从没读过代码的新人闭着眼睛改生产代码。这是整套方案能否成立的分水岭,比任何提示词技巧都重要。
- 一条固定的测试命令。 比如
make test、npm test、pytest,退出码能反映成功失败,本地和 CI 跑出来结果一致。 - 能跑 CI 的平台。 下文以 GitHub Actions 举例,其他平台逻辑一样。
- Codex 的可用访问方式。 Codex 有终端 CLI 和云端任务等多种形态,具体开通方式、可用模型、命令参数请以官方文档为准。
- 一个专用机器人身份的令牌。 不要用你自己的账号。令牌权限收窄到"可以推分支、可以开 PR",明确不给"直接推 main"的能力。
- 分支保护规则已开启。 main 分支要求 PR + 至少一个人工 approve + CI 通过才能合并。这是最后一道闸门,必须提前设好。
- 一个能复现 bug 的最小环境。 最好用容器或 devcontainer 固化依赖,避免"在我机器上能跑"。
第 1 步:把仓库整理成智能体看得懂的样子
智能体不是靠猜,它是靠读。你仓库里如果有一堆同名的测试文件、三套并存的构建脚本、README 里写着过时的跑法,它就会踩坑。
先做三件小事:
1. 统一入口。 把所有构建、测试、lint 命令收进 Makefile 或 package.json 的 scripts,智能体只需要记住几条命令。
2. 测试可以单独跑。 支持"只跑某个文件里某个用例",否则智能体改一行代码等十分钟,循环不起来。
3. 跑通一次干净环境。 从空目录 clone 下来,按 README 从头做一遍,把漏掉的步骤补进脚本。
```makefile
Makefile 示例
.PHONY: setup test test-one lint
setup: ## 安装依赖,CI 和本地都用这条
pip install -r requirements-dev.txt
test: ## 跑全部测试
pytest -q
test-one: ## 用法:make test-one T=apps/orders/tests/test_refund.py
pytest -q $(T)
lint:
ruff check . && black --check .
```
第 2 步:写 AGENTS.md,相当于给智能体的入职手册
Codex 会读取仓库里的 AGENTS.md 作为长期指令。这份文件的质量,直接决定它第一稿的准确率。放在仓库根目录,覆盖全仓库的规矩;如果某个子目录有特殊约定,可以在子目录再放一份,就近的那份优先(具体查找规则以官方文档为准)。
写得好的 AGENTS.md 长这样:
```markdown
AGENTS.md
这个仓库是什么
Django + Celery 的订单服务。核心业务代码在 apps/orders/,
API 层在 apps/api/,不要跨层直接调用 models。
常用命令
- 安装依赖:
make setup - 跑全部测试:
make test(任何改动之后都必须跑这一条) - 跑单个测试:
make test-one T=<路径> - 风格检查:
make lint
改代码的规矩
1. 先写一个能复现问题的失败测试,再改实现。顺序不能反。
2. 一次只改一个逻辑点,不顺手重构、不顺手升级依赖、不顺手改格式。
3. 不要修改 apps/*/tests/ 下已有测试的断言来让测试通过。
如果认为测试本身写错了,停下来汇报,不要自己改。
4. 不要新增第三方依赖。确实需要时,先输出理由并停止。
5. 改动超过 5 个文件就停下来汇报,不要继续。
6. 只使用仓库里已有的工具函数和 API,不要凭记忆写不存在的接口。
代码从哪里看起
- 退款逻辑:
apps/orders/services/refund.py - 订单状态机:
apps/orders/models.py - 金额相关工具:
apps/common/money.py
不要碰的地方
migrations/:除非任务明确要求,否则不生成迁移文件。.env*、secrets/:任何情况下都不要读取或修改。
```
注意第 3 条和第 5 条。它们是在堵两个最常见的失败模式:改测试让它通过,和改动范围失控。
第 3 步:把权限收紧到刚好够用
这一步比提示词重要。给智能体的权限,应该是"能在工作区里读写代码、能跑测试",而不是"能访问网络、能推任何分支、能读环境变量"。
Codex CLI 的配置大致分两块:审批策略(哪些命令需要你确认)和沙箱模式(它能写到哪、能不能联网)。具体字段名和取值以官方文档为准,下面是结构示意:
```toml
~/.codex/config.toml —— 字段名与取值请以官方文档为准
model = "<按官方文档填写你账号可用的模型>"
审批策略:决定哪些操作需要人工确认
无人值守场景通常设成"失败时再上报"或"仅工作区内自动执行"
approval_policy = "on-failure"
沙箱:只允许写当前工作区,不允许写到工作区之外
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
默认断网。需要装依赖时临时打开,装完立刻关掉。
network_access = false
```
再叠加两层隔离:
- 令牌层:机器人令牌只给仓库的
contents: write和pull-requests: write,不要给 admin、不要给 workflows 写权限。 - 仓库层:main 开启分支保护,要求 PR + CI 绿 + 人工 approve。这样即使智能体抽风想直接推 main,也会被平台挡下来。
一个实用技巧:把"装依赖"和"跑智能体"分成两个阶段。第一个阶段联网装依赖,第二个阶段断网跑智能体。这样它既装得上包,又不会偷偷去查什么东西或者把代码发到外面。
第 4 步:定义输入契约——Issue 模板
自动修 Bug 最大的浪费,是花二十分钟修一个根本没描述清楚的问题。用一个模板把输入卡死:
```markdown
---
name: 可自动修复的 Bug
about: 打上 auto-fix 标签后,机器人会尝试自动修复
labels: ["bug", "auto-fix"]
---
现象
结算页选择"部分退款"后,退款金额显示为 99.99,实际应为 100.00。
复现步骤
1. 创建订单,金额 199.99
2. 发起部分退款,退款比例 50%
3. 查看退款记录金额
期望行为
退款金额 = 100.00(四舍五入到分)
实际行为
退款金额 = 99.99
可能相关的模块
退款计算,apps/orders/services/refund.py
是否稳定复现
是,三次都复现
```
"是否稳定复现"这一栏很有用。不稳定的 bug 不要让机器人碰,它会一路改到把整个模块重写。
第 5 步:第一段——只复现,不修复
这是整套流程里最值钱的一步。 让智能体先把 bug 变成一个失败的测试,确认它真的失败了,再允许它动实现代码。
这样做有两个好处:一是防止它"幻觉式修复"——改了一堆无关代码,测试正好过了;二是失败测试本身就是证明修复有效的证据。
提示词示例(注意把"不许改实现"写死):
```text
你现在的角色是复现者,不是修复者。
任务来源:Issue #1234(内容见文末)
允许你做的事:
- 阅读仓库里任何文件
- 在 apps/orders/tests/ 下新增测试文件
- 运行
make test-one T=<你新增的测试文件>
禁止你做的事:
- 修改任何非测试文件
- 修改任何已存在的测试文件
- 新增第三方依赖
请按顺序完成:
1. 写出一个最小测试,能反映 Issue 里描述的期望行为。
2. 运行它,把完整命令和输出贴出来。
3. 如果它没有失败,说明你没有真正复现,立刻停止并汇报,
不要猜测、不要继续修改代码。
输出格式:
状态:<REPRODUCED | NOT_REPRODUCED>
新增文件:<路径>
运行的命令:<命令>
失败信息摘要:<关键几行>
```
如果返回 NOT_REPRODUCED,直接终止这一轮,在 Issue 里留言说明复现失败。别让它硬猜,猜出来的修复一定不可靠。
第 6 步:第二段——最小改动修复,循环跑测试
复现成功之后进入修复。这里的核心约束是"一次一个逻辑点 + 每次改完都跑全量测试",避免它一口气改五个地方,测试过了也不知道是哪个改动生效的。
```text
你现在的角色是修复者。
输入:上一步新增的失败测试文件(已在工作区),Issue #1234 的原始描述。
目标:让 make test 全部通过,且改动尽可能小。
工作方式(必须遵守):
1. 先读懂失败测试断言的期望行为,再看 apps/orders/services/refund.py
里对应的实现,用一句话说明你认为问题出在哪。
2. 只改一个逻辑点。改完立刻运行 make test。
3. 如果测试仍然失败,回退这一处改动,重新分析,不要叠加第二处改动。
4. 最多尝试 5 轮。第 5 轮还不过,停止,输出:
- 你已经排除的假设
- 你怀疑但没验证的方向
- 需要人补充的信息
5. 不要修改测试文件里的断言。不要为了让测试通过而吞掉异常、
加 try/except 兜底或返回默认值。
全部通过后,输出:
git diff --stat的结果- 你改了哪几处、每处为什么改
make test的最终输出摘要
```
第 4 条那个"最多 5 轮"很关键。没有上限的循环会烧掉大量时间和额度,而且往往在第 3 轮之后就开始瞎改了。
第 7 步:第三段——换个身份审查 diff
让同一个上下文继续审自己的代码,效果很差,它会觉得自己的改动理所当然。要么开一个全新的会话,要么明确让它切换成"只读审查者",且不给它修改权限。
```text
你现在的角色是严格的代码审查者。你只能读,不能改任何文件。
给你一份改动 diff 和一份已经跑绿的测试结果。
请逐条检查并输出结论:
1. 改动范围:有没有改到与 Issue 无关的文件?
2. 测试诚信:有没有修改已有测试的断言?有没有跳过、
xfail 或者注释掉测试?
3. 异常处理:有没有新增的 try/except 把异常吃掉?
有没有为了通过测试而返回默认值、空列表或 None?
4. 边界情况:金额、时间、分页、空集合、并发这几类,
这次改动会不会引入新的边界问题?
5. 可读性:命名是否与周围代码一致?有没有引入新的重复逻辑?
6. 有没有引入新的依赖或新的外部调用?
最后给出一行结论:
VERDICT: APPROVE
或
VERDICT: REQUEST_CHANGES
如果是后者,列出必须修改的点,按严重程度排序。
```
审查环节不需要 100% 正确,它的作用是把明显的问题挡在人工 review 之前,让人类看到的是一个已经过一遍筛子的 PR。
第 8 步:第四段——生成 PR
前三步都过了,再走提交。用脚本固化,别让智能体自由发挥 git 命令。
```bash
#!/usr/bin/env bash
.github/open_pr.sh
set -euo pipefail
ISSUE_NUMBER="${ISSUE_NUMBER:?需要设置 ISSUE_NUMBER}"
BRANCH="auto-fix/issue-${ISSUE_NUMBER}"
每次都从最新的 main 拉分支,避免在旧代码上做修复
git fetch origin main
git checkout -b "$BRANCH" origin/main
git add -A
git commit -m "fix(orders): 修正部分退款金额四舍五入错误 (#${ISSUE_NUMBER})"
只推 PR 分支,绝不推 main
git push -u origin "$BRANCH"
gh pr create \
--base main \
--head "$BRANCH" \
--title "fix: 修正部分退款金额四舍五入错误 (#${ISSUE_NUMBER})" \
--body-file .github/pr_body.md \
--label "auto-fix" \
--reviewer "your-team"
```
.github/pr_body.md 里建议固定写清楚三件事:关联的 Issue、机器人自审的结论、人工需要重点看的地方。比如:
```markdown
关联
Closes #1234
改动说明
- 新增失败测试:
apps/orders/tests/test_refund_rounding.py - 修改:
apps/orders/services/refund.py中部分退款金额的计算顺序,
改为先四舍五入到分再做比例运算
自审结论
VERDICT: APPROVE
审查者已确认:未改动已有测试、未新增异常兜底、改动 2 个文件
请人工重点确认
- 金额精度策略是否符合财务口径(是否应该用 Decimal 而不是 float)
```
第 9 步:接上 CI,让它无人值守地跑
把上面几段串成一条流水线,用 Issue 标签触发:
```yaml
.github/workflows/auto-fix.yml
name: auto-fix-bug
on:
issues:
types: [labeled]
jobs:
fix:
if: github.event.label.name == 'auto-fix'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write # 推分支需要
pull-requests: write # 开 PR 需要
issues: write # 回写评论需要
steps:
- uses: actions/checkout@v4 # 版本号以该 Action 的官方页面为准
with:
fetch-depth: 0
阶段一:有网,装依赖
- name: 安装依赖
run: make setup
阶段二:跑智能体,这一步建议断网(以官方文档为准配置沙箱)
- name: 调用 Codex 非交互模式
run: |
具体子命令与参数以官方文档为准
codex exec "$(cat .github/prompts/fix.md)"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
ISSUE_BODY: ${{ github.event.issue.body }}
阶段三:提交 PR
- name: 提交 PR
run: bash .github/open_pr.sh
env:
GH_TOKEN: ${{ secrets.BOT_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
```
注意 permissions 是收窄的,timeout-minutes 是防止卡死烧额度,if 条件是防止别人随便打个标签就触发。
常见坑与排错
改测试让它通过。 最典型的翻车方式。表现是 diff 里出现了对已有断言的修改,或者新增了 @pytest.mark.skip。处理办法:AGENTS.md 里明文禁止 + 审查环节专门检查 + CI 里加一条"测试文件是否被修改"的检查,改了就让人工确认。
改动范围失控。 一个改金额四舍五入的任务,最后动了 12 个文件,顺带升级了依赖。处理办法:AGENTS.md 写死文件数量上限,审查环节第一项就查范围。
反复修不好,越改越乱。 设置最大尝试轮数,超了就停下来输出"已排除的假设",而不是继续瞎试。有时候停下来交给人的信息,比它再跑十轮有用。
测试不稳定(flaky)。 智能体会追着一个随机失败的测试乱改。动手前先把 flaky 测试隔离掉,或者用固定随机种子。
依赖装不上。 八成是环境没固化。用容器或 devcontainer 把依赖钉死,make setup 一定要能在干净环境里跑通。
幻觉 API。 它会写 import 一个仓库里根本不存在的模块。AGENTS.md 里要求"只用仓库里已有的工具函数",审查环节检查新增的 import。
权限给太大。 有人图省事直接给了个人账号的完整令牌。一旦智能体理解错了任务,它能做的事情远超你的预期。永远用最小权限的机器人身份。
密钥泄露。 把 .env 放进工作区,智能体读进去之后可能写进日志或提交。.gitignore 要好,AGENTS.md 里把 .env* 列为禁区,CI 里注入密钥而不是放文件。
上下文太长。 仓库很大时它会迷路。AGENTS.md 里写清"代码从哪里看起",把关键文件路径列出来,比让它自己 grep 整个仓库高效得多。
下一步建议
先用这套流程跑最低风险的任务:lint 报错修复、类型标注补全、依赖小版本升级、日志信息补充。这些任务有明确的"对错"判定,不需要业务判断,跑通了再上真正的 bug 修复。
跑顺之后可以做三件事:一是把成功的案例回填进 AGENTS.md——每次它犯的错,都变成一条新规矩;二是加度量,记录"自动 PR 的合并率"和"人工在合并前需要改多少行",前者衡量可靠性,后者衡量质量,比看它跑了多少次有用得多;三是扩展到更复杂的任务,比如给已有功能加测试、把重复代码抽出来,但每次扩展前都问自己一句:这个任务的正确性能被测试验证吗?
最后提醒一句:无论自动化做得多好,main 分支的合并权永远保留在人类手里。这套系统的定位是把你从"找问题、写复现、搭架子"里解放出来,而不是替你做决定。
