1. 从一次失控的 AI 修改说起
上周我在维护一个 FastAPI 项目时,让 Codex 帮忙重构用户认证模块。任务本身不复杂:把散落在三个文件里的 token 校验逻辑收敛到一个 service 里。Codex 很快给出了修改,测试也通过了。但合并之后,同事发现登录接口的响应时间从 80ms 涨到了 400ms------Codex 在重构时顺手把 Redis 缓存调用改成了每次都查数据库。
问题不在 Codex 本身,而在于我的工作流:没有先让 AI 制定修改计划,没有限制修改范围,没有在独立分支上验证,也没有用 git diff 逐行审查。这次事故让我重新梳理了一套「可回滚、可审查、可测试」的 AI 辅助开发流程。
这篇文章以真实项目为例,完整演示如何用 Codex 配合 Git 分支和 pytest,构建一条安全可控的 AI 开发流水线。文章中的命令、代码和提示词都可以直接复制使用。
2. 项目背景与目标
为了演示,我准备了一个精简但真实的 FastAPI 项目。它包含用户注册、登录和获取个人信息三个接口,其中登录接口依赖一个简单的 token 校验函数。
项目目录结构如下:
text
ai_dev_workflow/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI 入口
│ ├── auth.py # token 生成与校验
│ ├── models.py # 数据模型
│ └── storage.py # 内存存储(模拟数据库)
├── tests/
│ ├── __init__.py
│ ├── test_auth.py # 认证相关测试
│ └── test_api.py # 接口测试
├── requirements.txt
└── README.md
核心代码已经写好并通过测试。我们的任务是:让 Codex 把 token 校验逻辑从 main.py 中抽离到 auth.py,并保证所有测试通过、行为不变。
先看当前 main.py 的内容:
python
from fastapi import FastAPI, Header, HTTPException
from app.auth import create_token, verify_token
app = FastAPI()
@app.get("/users/me")
def get_current_user(authorization: str = Header(...)):
# 这里存在重复的 token 解析逻辑,需要重构
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization header")
token = authorization.removeprefix("Bearer ").strip()
payload = verify_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return {"user_id": payload["user_id"], "name": payload["name"]}
auth.py 中已有 create_token 和 verify_token,但缺少一个「从 Authorization 头解析 token」的函数。这正是本次重构的目标:把 main.py 里的解析逻辑下沉到 auth.py,让接口层更干净。
3. 第一步:让 Codex 先读项目,不急着改代码
很多开发者拿到 Codex 第一句话就是「帮我改一下」,这很容易让 AI 在没理解项目结构的情况下动手。正确做法是先让 AI 阅读项目、定位相关文件,并输出一份修改计划。
下面是我使用的第一个 Codex 提示词:
text
请阅读当前项目(FastAPI + pytest),不要修改任何代码。
任务目标:
1. 理解项目目录结构和各文件职责。
2. 定位 token 校验逻辑在哪些文件中出现。
3. 找出 main.py 中重复的 token 解析代码。
输出要求:
1. 列出相关文件清单。
2. 说明当前 token 校验逻辑的重复位置。
3. 给出一个重构方案,说明要新增什么函数、修改哪些文件。
4. 明确本次重构不涉及的范围(例如不修改数据模型、不改变接口路径)。
在输出计划之前,不要修改任何文件。
这个提示词的关键在于「不要修改任何代码」和「输出计划之前不要动手」。Codex 会先扫描项目,返回一份重构方案。拿到方案后,我人工确认了以下几点:
- 新增函数
parse_bearer_token放在auth.py; main.py只调用新函数,不保留解析逻辑;- 不修改
verify_token的签名和返回结构; - 不改变任何接口路径和参数。
确认无误后,进入下一步。
4. 第二步:创建 Git 分支,隔离 AI 修改
在让 Codex 动手之前,先创建一个独立分支。这样无论 AI 改出什么问题,都不会影响主分支,随时可以丢弃重来。
bash
# 进入项目目录
cd ai_dev_workflow
# 确保当前在干净的主分支上
git checkout main
git pull origin main
# 创建并切换到功能分支
git checkout -b refactor/token-parsing
# 确认当前分支
git branch --show-current
输出应为:
text
refactor/token-parsing
创建分支后,先跑一遍现有测试,确认基线是绿的:
bash
python -m pytest -q
如果基线测试失败,说明项目本身有问题,不应该让 AI 在坏基线上继续叠加修改。基线通过后,再让 Codex 开始重构。
5. 第三步:用 Codex 实施重构并补充测试
现在可以给 Codex 下发正式的修改任务。提示词必须包含:任务目标、允许修改范围、禁止修改内容、测试要求和验证方式。
text
请基于当前项目实施以下重构,并补充相应测试。
任务目标:
1. 在 app/auth.py 中新增函数 parse_bearer_token(authorization: str) -> dict。
- 输入为 Authorization 请求头原始字符串。
- 如果格式不是 "Bearer <token>",抛出 HTTPException(401)。
- 如果 token 无效或过期,抛出 HTTPException(401)。
- 返回 payload 字典。
2. 修改 app/main.py 中的 get_current_user,改用 parse_bearer_token。
3. 删除 main.py 中重复的 token 解析逻辑。
允许修改范围:
- app/auth.py
- app/main.py
- tests/test_auth.py
- tests/test_api.py
禁止修改内容:
- app/models.py
- app/storage.py
- 接口路径、请求参数、响应结构
- verify_token 和 create_token 的签名
测试要求:
1. 在 tests/test_auth.py 中为 parse_bearer_token 补充单元测试。
2. 覆盖:合法 token、缺失 Bearer 前缀、空 token、无效 token。
3. 运行 python -m pytest -q,确保全部测试通过。
验证方式:
- 修改完成后,运行 pytest 并报告结果。
- 输出 git diff --stat 摘要,列出修改了哪些文件。
这个提示词把边界划得很清楚。Codex 只能动四个文件,不能碰模型和存储层。测试要求覆盖了正常和异常路径,验证方式要求 AI 自己跑测试并报告结果。
Codex 完成修改后,我得到如下 diff 摘要:
text
app/auth.py | +18
app/main.py | -12
tests/test_auth.py | +24
tests/test_api.py | +6
新增的 parse_bearer_token 函数如下:
python
from fastapi import HTTPException
def parse_bearer_token(authorization: str) -> dict:
if not authorization.startswith("Bearer "):
raise HTTPException(status_code=401, detail="Invalid authorization header")
token = authorization.removeprefix("Bearer ").strip()
if not token:
raise HTTPException(status_code=401, detail="Missing token")
payload = verify_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="Invalid or expired token")
return payload
重构后的 main.py 变得非常干净:
python
from fastapi import FastAPI, Header
from app.auth import parse_bearer_token
app = FastAPI()
@app.get("/users/me")
def get_current_user(authorization: str = Header(...)):
payload = parse_bearer_token(authorization)
return {"user_id": payload["user_id"], "name": payload["name"]}
注意:parse_bearer_token 内部直接抛 HTTPException,这意味着 auth.py 现在依赖 FastAPI。如果未来想复用这个函数到非 Web 场景,需要再解耦。这是 Codex 方案的一个潜在问题,我在人工审查阶段会重点检查。
6. 第四步:运行测试并检查 git diff
Codex 报告测试通过,但我不会直接相信。我会自己再跑一遍,并逐行检查 diff。
bash
# 运行全部测试
python -m pytest -q
# 查看详细 diff
git diff
# 查看统计摘要
git diff --stat
测试输出:
text
============================= test session starts =============================
collected 8 items
tests/test_auth.py .... [ 50%]
tests/test_api.py .... [100%]
============================== 8 passed in 0.42s ==============================
8 个测试全部通过。接下来逐行检查 git diff,重点看三件事:
- 是否修改了禁止范围的文件 ------用
git diff --stat确认只有四个文件变动; - 是否引入了无关改动------例如格式化、变量重命名、注释删除;
- 是否改变了接口行为------例如响应结构、状态码、错误信息。
我注意到 Codex 在 auth.py 中新增的 parse_bearer_token 直接依赖 FastAPI 的 HTTPException。这不算错误,但值得记录。如果项目后续要拆分纯 Python 层和 Web 层,这个函数需要再抽象。我在代码审查时给 Codex 提了这个问题,它给出了一个不依赖 FastAPI 的版本:
python
class TokenParseError(Exception):
pass
def parse_bearer_token(authorization: str) -> dict:
if not authorization.startswith("Bearer "):
raise TokenParseError("Invalid authorization header")
token = authorization.removeprefix("Bearer ").strip()
if not token:
raise TokenParseError("Missing token")
payload = verify_token(token)
if payload is None:
raise TokenParseError("Invalid or expired token")
return payload
然后在 main.py 中捕获 TokenParseError 并转换为 HTTPException。这样 auth.py 就不依赖 FastAPI 了。这个改进是我在人工审查阶段提出的,Codex 负责实现------这正是「AI 辅助开发」的正确姿势:AI 提方案、写代码,人做判断和决策。
7. 第五步:人工代码审查清单
AI 生成的代码必须经过人工审查。我总结了一份适用于 Codex 修改的审查清单:
| 检查项 | 说明 |
|---|---|
| 修改范围 | 是否只动了允许的文件?用 git diff --stat 确认 |
| 接口兼容性 | 请求参数、响应结构、状态码是否变化? |
| 异常处理 | 是否覆盖了空值、非法格式、过期 token? |
| 依赖方向 | 是否引入了不合理的依赖(如业务层依赖 Web 框架)? |
| 敏感信息 | 是否有 API Key、密码、Token 被硬编码或打印? |
| 测试覆盖 | 新增代码是否有对应测试?异常路径是否覆盖? |
| 无关改动 | 是否顺手改了格式化、变量名、注释? |
针对本次重构,我重点确认了异常处理。Codex 的初版方案把 HTTPException 直接抛在 auth.py 里,虽然功能正确,但让业务层依赖了 Web 框架。经过一轮提示词迭代,改为自定义异常 + 接口层转换,职责更清晰。
8. 第六步:合并与回滚策略
测试通过、审查无误后,合并分支:
bash
# 切回主分支
git checkout main
# 合并功能分支
git merge refactor/token-parsing
# 推送远程
git push origin main
如果合并后发现线上问题,回滚也很简单:
bash
# 回滚到上一个稳定版本
git revert HEAD
# 或者直接重置到合并前
git reset --hard HEAD~1
git revert 会生成一个新的反向提交,适合已经推送到远程的情况;git reset --hard 会丢弃提交历史,只适合本地未推送的分支。生产环境优先使用 git revert。
9. 完整工作流总结
下面是本次实战的完整流程图:
#mermaid-svg-TcqxLonIELdTBSwN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TcqxLonIELdTBSwN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TcqxLonIELdTBSwN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TcqxLonIELdTBSwN .error-icon{fill:#552222;}#mermaid-svg-TcqxLonIELdTBSwN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TcqxLonIELdTBSwN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TcqxLonIELdTBSwN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TcqxLonIELdTBSwN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TcqxLonIELdTBSwN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TcqxLonIELdTBSwN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TcqxLonIELdTBSwN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TcqxLonIELdTBSwN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TcqxLonIELdTBSwN .marker.cross{stroke:#333333;}#mermaid-svg-TcqxLonIELdTBSwN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TcqxLonIELdTBSwN p{margin:0;}#mermaid-svg-TcqxLonIELdTBSwN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-TcqxLonIELdTBSwN .cluster-label text{fill:#333;}#mermaid-svg-TcqxLonIELdTBSwN .cluster-label span{color:#333;}#mermaid-svg-TcqxLonIELdTBSwN .cluster-label span p{background-color:transparent;}#mermaid-svg-TcqxLonIELdTBSwN .label text,#mermaid-svg-TcqxLonIELdTBSwN span{fill:#333;color:#333;}#mermaid-svg-TcqxLonIELdTBSwN .node rect,#mermaid-svg-TcqxLonIELdTBSwN .node circle,#mermaid-svg-TcqxLonIELdTBSwN .node ellipse,#mermaid-svg-TcqxLonIELdTBSwN .node polygon,#mermaid-svg-TcqxLonIELdTBSwN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-TcqxLonIELdTBSwN .rough-node .label text,#mermaid-svg-TcqxLonIELdTBSwN .node .label text,#mermaid-svg-TcqxLonIELdTBSwN .image-shape .label,#mermaid-svg-TcqxLonIELdTBSwN .icon-shape .label{text-anchor:middle;}#mermaid-svg-TcqxLonIELdTBSwN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-TcqxLonIELdTBSwN .rough-node .label,#mermaid-svg-TcqxLonIELdTBSwN .node .label,#mermaid-svg-TcqxLonIELdTBSwN .image-shape .label,#mermaid-svg-TcqxLonIELdTBSwN .icon-shape .label{text-align:center;}#mermaid-svg-TcqxLonIELdTBSwN .node.clickable{cursor:pointer;}#mermaid-svg-TcqxLonIELdTBSwN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-TcqxLonIELdTBSwN .arrowheadPath{fill:#333333;}#mermaid-svg-TcqxLonIELdTBSwN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-TcqxLonIELdTBSwN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-TcqxLonIELdTBSwN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TcqxLonIELdTBSwN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-TcqxLonIELdTBSwN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TcqxLonIELdTBSwN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-TcqxLonIELdTBSwN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-TcqxLonIELdTBSwN .cluster text{fill:#333;}#mermaid-svg-TcqxLonIELdTBSwN .cluster span{color:#333;}#mermaid-svg-TcqxLonIELdTBSwN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-TcqxLonIELdTBSwN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-TcqxLonIELdTBSwN rect.text{fill:none;stroke-width:0;}#mermaid-svg-TcqxLonIELdTBSwN .icon-shape,#mermaid-svg-TcqxLonIELdTBSwN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-TcqxLonIELdTBSwN .icon-shape p,#mermaid-svg-TcqxLonIELdTBSwN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-TcqxLonIELdTBSwN .icon-shape .label rect,#mermaid-svg-TcqxLonIELdTBSwN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-TcqxLonIELdTBSwN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-TcqxLonIELdTBSwN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-TcqxLonIELdTBSwN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
否
是
需求说明
Codex 阅读项目
输出修改计划
人工确认计划
创建 Git 分支
运行基线测试
Codex 修改代码
Codex 补充测试
运行 pytest
测试通过?
检查 git diff
人工代码审查
审查通过?
合并分支
推送并监控
这套流程的核心原则是:AI 负责执行,人负责决策。Codex 可以快速阅读项目、生成代码、补充测试,但修改计划、范围边界、最终审查必须由人来把控。
10. 常见错误与解决方法
错误 1:没有先让 AI 读项目就直接改
症状:Codex 修改了错误的文件,或者重复实现了已有功能。
解决:先发「只读分析」提示词,让 AI 输出项目结构和修改计划,确认后再动手。
错误 2:没有限制修改范围
症状:Codex 顺手格式化了整个项目,diff 里出现大量无关改动。
解决:在提示词中明确「允许修改范围」和「禁止修改内容」,用 git diff --stat 检查。
错误 3:只信 AI 的测试结果
症状:Codex 报告「测试通过」,但实际没跑或只跑了部分测试。
解决:自己运行 python -m pytest -q,并检查测试数量是否合理。
错误 4:在主分支上直接让 AI 改
症状:AI 改坏了,无法快速回滚。
解决:始终在独立分支上操作,合并前确保基线测试通过。
11. 安全注意事项
使用 Codex 辅助开发时,必须注意以下几点:
- 不要把 API Key、密码、Token 写进代码。Codex 生成的代码如果包含敏感信息,审查时必须删除。
- 使用环境变量保存敏感配置。例如数据库密码、第三方服务密钥,一律从环境变量读取。
- 不向 AI 提交生产环境密码。在提示词中不要粘贴真实密钥,用占位符代替。
- 检查日志和配置文件中的敏感信息。Codex 可能在调试代码中加入打印语句,合并前检查。
- 限制 AI 可以修改的文件范围。在提示词中明确允许和禁止的文件列表。
- 使用最小权限原则。本地开发账号不要使用生产环境权限。
- AI 生成代码必须经过测试和安全检查。不要因为测试通过就跳过人工审查。
- 不要直接让 AI 修改生产环境。所有修改先在本地分支验证,再走合并流程。
12. FAQ
Q1:Codex 和 ChatGPT Plus / ChatGPT Pro 是什么关系?
Codex 是 OpenAI 面向开发者的 AI 编程工具,可以理解为「能直接操作代码仓库的 AI 助手」。ChatGPT Plus 和 ChatGPT Pro 是 ChatGPT 的订阅套餐,具体包含哪些功能、是否包含 Codex 使用权限,会随版本和地区变化,请以 OpenAI 当前官方页面为准。
Q2:每次都要让 AI 先输出计划吗?
对于涉及多个文件、可能影响接口行为的修改,强烈建议先输出计划。对于单文件的小改动(如修一个变量名),可以直接修改,但仍要检查 diff。
Q3:Codex 生成的测试可信吗?
Codex 生成的测试可以作为基础,但需要人工补充边界条件和异常路径。本次实战中,Codex 自动生成的测试覆盖了合法 token 和无效 token,但「空 token」「缺失 Bearer 前缀」是我在提示词中明确要求的。
Q4:如果 Codex 修改了禁止范围的文件怎么办?
用 git checkout -- <文件> 恢复该文件,然后在提示词中再次强调范围限制。如果反复出现,说明提示词不够清晰,需要更明确地列出允许修改的文件清单。
Q5:如何确认 Codex 没有引入安全漏洞?
人工审查时重点检查:敏感信息是否硬编码、输入是否校验、异常是否被吞掉、依赖是否引入不安全的版本。必要时用 pip-audit 或 npm audit 检查依赖安全。
13. 总结
Codex 是强大的 AI 编程助手,但它不是「自动驾驶」。真正可靠的 AI 开发流程,是把 Codex 放进一个受控的 Git 工作流里:先读项目、再定计划、然后开分支、改代码、补测试、查 diff、人工审查、最后合并。
本次实战中,Codex 完成了 token 解析逻辑的重构,并生成了对应的单元测试。但真正让这次重构安全落地的,是分支隔离、基线测试、diff 审查和人工决策。这套流程适用于 FastAPI、Spring Boot、React 等任何技术栈,核心思想是一致的:AI 提效,人控质量。
具体功能和可用范围可能随版本、账号和地区变化,请以 OpenAI 当前官方页面为准。