1. Codex 改 FastAPI 代码后为什么必须用 Git 分支隔离改动用 Codex 给 FastAPI 项目加功能最危险的不是它写不出代码而是它写得太顺、顺手把无关逻辑一起改了。我遇到过最典型的一次只想给订单接口加一个status过滤参数结果 Codex 顺手把 repository 层的返回结构从List[Order]改成了List[dict]测试全绿但另一个依赖Order对象的导出接口直接崩了。这类隐性缺陷靠肉眼看代码很难第一时间发现。所以核心检索词先讲清楚Codex 是 AI 编程助手能读项目、改代码、补测试Git 分支是隔离容器让 AI 的改动不污染主分支pytest 是回归验证网把「AI 说改好了」变成「测试证明没改坏」。这套组合适合所有用 AI 辅助维护 FastAPI 后端、又不想被隐性缺陷坑的开发者。我给自己定的纪律很简单AI 改代码可以但必须走「分支隔离 → 测试回归 → diff 审查」三步。分支让你随时能丢弃重来pytest 让你知道功能是否还正确diff 让你确认改动范围没有越界。三者缺一风险就会从「可控」变成「赌运气」。这篇文章围绕一个真实场景展开给 FastAPI 的GET /orders接口增加可选status查询参数支持pending、paid、shipped、cancelled四种状态。需求不大但坑不少——repository 层原本只有list_all()service 层没有参数校验现有测试只覆盖「返回全部订单」。如果直接让 AI 改它很可能顺手重构无关代码这正是我们要防的。下面按「先分析、再计划、后动手」的顺序把可复制的分支命名、提交规范、pytest 用例模板、CI 触发配置全部给出来并演示一次 AI 改动从提交到验证的完整流程。你可以直接照着改自己的项目。2. TaoToken 前置准备给 Codex 配好可用的模型入口在讲 Git 和 pytest 之前得先把 Codex 的模型入口配好否则后面所有步骤都跑不起来。我实测下来用 TaoToken 作为统一入口比较省事它提供兼容 OpenAI 的接口Codex、Cline、Claude Code 这类工具都能接Base URL 和 Key 一次配好换工具不用重配。先拿 Key。打开官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 API Key。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite Key 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。API 基础地址统一用 https://taotoken.net/api 注意这个地址不加 UTM 参数直接填就行。Codex 的配置通常落在~/.codex/config.toml或项目级配置里。下面是一份可复制的 TOML 片段路径和字段名按你本地实际文件为准# ~/.codex/config.toml model gpt-5-codex model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chat然后在 shell 里导出 Key别写进代码export TAOTOKEN_API_KEYsk-你的Key如果你用的是 Cline 或 Claude Code配置思路一样都是三件套Base URL 填https://taotoken.net/apiKey 填上面创建的Model ID 填你套餐里可用的模型名。Cline 的 MCP 配置里如果出现baseUrl字段同样指向这个地址。Codex 的auth.json如果存在里面只放引用不要把明文 Key 提交到 Git。这里有个坑要提前说很多人把 Key 直接写进config.toml然后提交结果 Key 泄露。正确做法是用环境变量配置文件里只写env_key。另外模型名和套餐权益会随版本变化具体以你控制台里显示的可用模型为准别照抄网上的旧模型名。配好之后先做一次最小验证确认 Codex 能正常对话再进入项目改造。验证入口可以用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一句「你好请回复当前可用模型名」看是否正常返回。如果这一步就报 401先别往下走去第 5 节排查。3. 可复制配置分支命名、提交规范与 pytest 用例模板这一节是全文最该收藏的部分全部是可复制的配置和模板。先建项目骨架再定分支规范最后给 pytest 模板。项目结构如下和后面所有命令的路径保持一致order-service/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── models.py │ ├── schemas.py │ ├── repository.py │ └── service.py ├── tests/ │ ├── __init__.py │ ├── conftest.py │ └── test_order_service.py ├── requirements.txt └── README.md分支命名我固定用feature/前缀加需求短名比如这次是feature/order-status-filter。创建命令git checkout -b feature/order-status-filter提交规范用 Conventional Commits 的简化版AI 改动单独一个提交方便回滚feat(order): add optional status filter to GET /orders - repository.list_all() 支持 status 参数 - service.list_orders() 校验非法状态返回 400 - 新增 6 个 pytest 用例覆盖合法/非法/缺省场景pytest 用例模板重点在conftest.py里放共享 fixture避免每个测试重复建 client# tests/conftest.py import pytest from fastapi.testclient import TestClient from app.main import app pytest.fixture def client(): return TestClient(app)测试文件用parametrize覆盖四种合法状态再单独写非法状态和缺省场景# tests/test_order_service.py import pytest def test_list_orders_without_status_returns_all(client): resp client.get(/orders) assert resp.status_code 200 assert len(resp.json()) 4 pytest.mark.parametrize(status, [pending, paid, shipped, cancelled]) def test_list_orders_with_valid_status(client, status): resp client.get(/orders, params{status: status}) assert resp.status_code 200 assert len(resp.json()) 1 assert resp.json()[0][status] status def test_list_orders_with_invalid_status_returns_400(client): resp client.get(/orders, params{status: unknown}) assert resp.status_code 400CI 触发配置用 GitHub Actions推送到 main 或开 PR 时自动跑测试# .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.12 - run: pip install -r requirements.txt - run: pytest tests/ -vrequirements.txt 里至少要有这些fastapi uvicorn pytest httpx注意httpx是TestClient的依赖漏了会报RuntimeError: The starlette.testclient module requires the httpx package。这个坑我在第 5 节会再展开。4. 验证请求一次 AI 改动从提交到测试通过的完整流程配置齐了现在走一遍完整流程。第一步不是让 Codex 改代码而是让它先读项目、输出分析提示词里明确「不要修改任何文件」请分析当前项目 order-service 的代码结构回答 1. GET /orders 从入口到数据返回的完整调用链 2. repository 层有哪些查询方法是否支持按状态过滤 3. service 层是否校验查询参数 4. 现有测试覆盖哪些场景缺哪些 5. 增加 status 参数涉及哪些文件和函数 输出文件路径和函数名不要给修改建议不要改任何文件。Codex 返回的调用链应该是main.py路由 →service.list_orders()→repository.list_all()。如果它说的和实际不符说明它没读懂项目这时候别继续。第二步让它出计划仍然不改代码并明确允许和禁止修改的文件基于分析为「增加 status 查询参数」制定修改计划 1. 每个文件的修改点函数名、改动内容 2. 新增测试用例清单正常边界 3. 明确禁止修改的文件 4. 修改后要运行的测试命令 只允许改 app/main.py、app/service.py、app/repository.py、tests/test_order_service.py 禁止改 models.py、schemas.py、requirements.txt不要重构无关代码。计划确认后第三步才让 Codex 实现。核心代码大致如下VALID_STATUSES定义在 repository 层service 层引用它做校验避免两处维护# app/repository.py from typing import List, Optional from app.models import Order VALID_STATUSES {pending, paid, shipped, cancelled} class OrderRepository: def __init__(self) - None: self._orders: List[Order] [ Order(id1, customer张三, statuspending), Order(id2, customer李四, statuspaid), Order(id3, customer王五, statusshipped), Order(id4, customer赵六, statuscancelled), ] def list_all(self, status: Optional[str] None) - List[Order]: if status is None: return self._orders return [o for o in self._orders if o.status status]# app/service.py from typing import List, Optional from fastapi import HTTPException from app.repository import OrderRepository, VALID_STATUSES class OrderService: def __init__(self) - None: self._repo OrderRepository() def list_orders(self, status: Optional[str] None) - List[dict]: if status is not None and status not in VALID_STATUSES: raise HTTPException(status_code400, detailf非法状态值: {status}) return [o.dict() for o in self._repo.list_all(statusstatus)]# app/main.py from typing import Optional from fastapi import FastAPI from app.service import OrderService app FastAPI() service OrderService() app.get(/orders) def list_orders(status: Optional[str] None): return service.list_orders(statusstatus)第四步跑测试cd order-service pytest tests/ -v预期输出六条 PASSED缺省返回全部、四种合法状态各一条、非法状态返回 400。全部通过后最关键的一步是查 diffgit diff --stat git diff app/git diff --stat列出所有被改文件和行数。如果出现计划外的models.py或requirements.txt立刻警惕。我实际检查时 Codex 只改了计划内四个文件但我也遇到过它顺手「优化」其他代码的情况所以这步绝对不能省。第五步让 Codex 以审查者身份再看一遍自己的改动提示词要求它只报告问题、不改代码以资深代码审查者身份审查当前分支 git diff检查 1. 逻辑错误或边界遗漏 2. 安全风险参数注入、敏感信息泄露 3. 有无无关改动 4. 测试是否覆盖关键场景 5. 风格是否与项目一致 每个问题标严重程度给文件和行号只报告不修改。人工逐行读 diff重点看有没有硬编码 Key、异常被吞、日志打印敏感数据。确认无误后合并git checkout main git pull origin main git merge feature/order-status-filter git push origin main合并后再跑一次pytest tests/ -v确认没引入冲突。如果配了 CI推送后会自动触发测试失败会直接标红坏代码进不了主分支。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节按真实报错来遇到哪个查哪个。401 Unauthorized。最常见的原因是 Key 没导出或导出后没生效。先确认echo $TAOTOKEN_API_KEY有值再确认配置文件里env_key写的是TAOTOKEN_API_KEY而不是别的名字。如果 Key 是从控制台复制的注意别带前后空格。还有一种情况是 Key 被撤销了去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一个。local proxy failed。这个报错通常出现在工具尝试走本地代理但代理没起来。检查你的工具配置里有没有多余的proxy字段有就删掉让请求直连https://taotoken.net/api。另外确认 Base URL 没有写成https://taotoken.net/api/带尾斜杠有些工具对尾斜杠敏感会拼出双斜杠导致路由失败。reading choices 相关报错。典型信息是Error reading choices或choices field missing。这多半是模型名填错了或者用了wire_api chat但模型实际走的是 responses 接口。先确认 Model ID 和控制台里可用模型一致再检查wire_api字段。如果换模型后仍报错把wire_api改成responses试一次。OAuth 相关报错。如果你用的是 Claude Code 这类走 OAuth 的工具报OAuth token expired或invalid_grant说明授权过期了。重新走一遍授权流程或者改用 API Key 方式接入。用 TaoToken 的话直接填 Base URL Key Model ID 三件套即可不需要 OAuth。pytest 报 httpx 缺失。报错RuntimeError: The starlette.testclient module requires the httpx package直接pip install httpx并写进 requirements.txt。测试通过但功能不对。测试只能验证「代码按测试预期运行」不能验证「需求理解正确」。建议在让 Codex 实现前先让它输出对需求的理解人工确认后再动手。AI 改坏了原有功能。只要在独立分支上操作直接丢弃git checkout main git branch -D feature/order-status-filter。这也是为什么第一步就要建分支。6. 语义一致 CTA把 Codex 工作流接到你的项目里整套流程跑下来核心就三件事先分析后动手、用分支隔离风险、测试和 diff 审查不可省。Codex 负责提速Git 负责兜底pytest 负责证明。三者配合AI 改代码的风险就从「赌运气」变成「可控」。如果你还没配好模型入口先去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 拿 Key接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各工具的配置示例。想先验证模型能不能正常对话用 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一句测试即可。如果你打算长期用 Codex 做编码和 Agent 任务Coding Plan 比按次调用更划算入口在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 用户走 Anthropic 接入的话参考 https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-code-anthropicutm_campaignrewrite 。最后留一个我踩过的坑别因为测试全绿就跳过git diff。测试覆盖的是你想到的场景diff 覆盖的是你没想到的改动。两者都看才算真正守住质量。