把 DeepSeek 接进 Codex:从双后端切换到“GPT 主控、DS 执行”
这次折腾最初只是为了给 Codex 安装一个可直接调用的 PowerShell 7,最后却一路延伸成了 OpenAI 与 DeepSeek 双后端、对话继承、独立 worker 和 Token 成本的完整测试。
开始时的想法很简单:保留 Codex 原有的 ChatGPT 登录、项目和历史记录,同时增加一套 DeepSeek 入口。需要复杂分析时继续用 GPT,大量重复工作交给 DS;如果 GPT 额度耗尽,还能让 DS 在同一个工作区接手。
真正做下来后发现,这件事不能简单理解为“在模型下拉框里多放两个名称”。模型目录、provider、对话元数据和 GUI 进程是几层不同的状态。只改其中一层,界面可能看起来已经切换,实际请求却仍然发往原来的服务。
本文记录最后走通的方案,以及中间几次比较有代表性的失败。
一、环境与目标
这次配置时的环境如下:
- Windows 版 Codex GUI:
OpenAI.Codex 26.924.2738.0 - Codex CLI:
0.158.0-alpha.2.1 - PowerShell:
7.6.6 CODEX_HOME:C:\Users\Admin\.codex- 原默认模型:
gpt-5.6-sol
PowerShell 7 安装完成后,pwsh 已能从系统路径直接启动。接下来希望实现四个入口:
1 | |
同时有几个前提:
- 不破坏原有 ChatGPT 登录和历史对话
- GPT 仍是默认后端
- OpenAI 与 DeepSeek 共用项目目录、MCP、插件和权限配置
- DeepSeek Key 不写进普通命令参数,也不在日志中输出
- 所有配置修改可回滚,并且切回 GPT 后应恢复原状态
二、第一版双后端配置
初版方案使用了这些文件:
1 | |
思路是把 DeepSeek 注册为一个 Responses API provider,并通过 DEEPSEEK_API_KEY 读取认证信息。模型目录中加入:
1 | |
CLI 入口通过 profile 区分;GUI 没有同等稳定的 profile 启动参数,因此切换脚本只修改 config.toml 中受管理的顶层模型字段,再重启同一个 Codex App。
配置本身并不复杂,麻烦主要出在 Windows 的几处细节上。
三、切换脚本踩到的三个坑
1. PowerShell 固定大小集合
第一次输入 Key 后,脚本在向 .env 追加内容时失败:
1 | |
原因是 Get-Content 的结果虽然被转换成了列表类型,但得到的仍是固定大小集合,不能直接调用 Add()。
最后改为显式创建可变列表,再逐行加入原内容:
1 | |
修复后,Key 才能在隐藏输入后正常保存。测试用的占位 Key 也在验证结束后确认没有残留。
2. WindowsApps 内的 EXE 无法直接启动
第二个错误发生在重启 GUI 时:
1 | |
直接执行 C:\Program Files\WindowsApps 下的 ChatGPT.exe 会受到应用包权限限制。最终没有继续硬调 EXE,而是根据 Appx 包信息取得 AUMID,再通过系统注册的应用入口启动:
1 | |
这比写死 WindowsApps 下的版本目录可靠,也不会在应用升级后立即失效。
3. Windows PowerShell 找不到 Codex CLI
后来从新开的 PowerShell 窗口执行 codex-ds-gui,又出现:
1 | |
Codex 桌面应用自己的执行环境里有 CLI 路径,不代表外部启动的 Windows PowerShell 也继承了同一套 PATH。
最终在脚本中增加了逐级解析:
- 优先使用显式设置的
CODEX_CLI_PATH - 尝试
Get-Command codex.exe - 在 Codex 本地安装目录中查找最新的
codex.exe
这三个问题修完后,GPT → DS → GPT 的配置往返已经能通过严格检查,切回后主配置也能逐字节恢复。
但真正影响使用的故障还在后面。
四、最隐蔽的问题:模型名称切了,provider 没切
DeepSeek 模型出现在 GUI 下拉框以后,我在原来的 GPT 对话中选择 deepseek-flash,结果请求直接失败:
1 | |
切回 GPT 模式后,问题更奇怪:
- 某些旧对话仍然能正常请求 GPT
- 模型选择器里却只剩 DeepSeek
- 一旦在旧对话里选过 DS,就没有 GPT 选项可以切回
检查本地状态后,才发现当时混在一起的是三件事:
| 状态 | 决定什么 | 当时的实际值 |
|---|---|---|
models.json |
下拉框显示哪些模型 | 只有 DeepSeek |
对话 turn_context |
当前选择的模型名 | deepseek-flash |
对话 session_meta |
请求发往哪个 provider | openai |
因此界面显示“已经选中 DeepSeek”,实际组合却是:
1 | |
请求根本没有到达 DeepSeek API,自然也和 Key 是否正确无关。
这次排查得到的关键结论是:
模型下拉框里出现了某个名称,不代表当前对话已经切换到对应的 provider。
五、正确的 GPT / DS 切换方式
修复后的规则变得很明确。
GPT 模式
切回 GPT 时,移除 DeepSeek 专用的:
1 | |
让 Codex 重新使用内建 OpenAI provider 和 GPT 模型目录。
DS 模式
进入 DS 时,再启用:
- DeepSeek provider
- DeepSeek 专用模型目录
- 对应 API Key 环境变量
并在这个状态下创建新的 DS 对话。
OpenAI 与 DeepSeek 可以共用同一个 CODEX_HOME、工作区、插件和侧栏数据库,但一条已经创建的对话仍带有自己的 provider 元数据。最稳妥的方式不是在原 GPT 对话里直接点选 DS 模型,而是在正确的后端状态下新建对话。
实际 API 最小测试也已经通过:
1 | |
这次可以确认请求确实到达了 DeepSeek,而不是仅仅在界面上改了模型名。
六、对话记录如何继承
完成双后端切换后,下一个问题是:DS 能不能继续 GPT 的现有工作?
这里需要区分“工作区”和“聊天记录”。
同项目新建 DS 对话
新对话可以直接读取同一个项目中的:
- 代码和文档
AGENTS.md- Git 提交和当前 diff
- 测试结果
- 交接文件
但它不会自动拥有旧 GPT 对话里的全部聊天内容。
这其实是最稳妥的日常接班方式。项目的真实状态本来就应该留在文件和 Git 中,而不是只存在于几十轮聊天里。
Fork 原对话
如果关键背景确实只存在于聊天中,可以 fork 原 thread。这样会复制完整历史并创建一个新的对话 ID,再把新对话绑定到 DeepSeek provider。
实际测试中,GPT 可以读取 DS 对话,DS 也可以读取 GPT 对话;fork 后的 DS 对话同样保留此前的上下文。
它更像是:
1 | |
原对话不会被覆盖,fork 之后两边也不会自动同步。
Resume 原对话
CLI 可以尝试用另一 profile resume 原 thread,并保持同一个对话 ID。实际探测时,Codex 成功读取了原历史,也显示目标模型已经变成 DeepSeek,但因为该对话仍被桌面应用占用而停止:
1 | |
彻底退出 GUI 后可以继续测试,但这种方式还会受到 provider 元数据、模型选择器和客户端锁的影响,不适合作为长期流程。
所以最终选择是:
- 日常接班:同项目新建 DS 对话,读取交接文件
- 必须继承聊天:fork
- 原 ID 的
resume --profile:只用于应急或实验
七、内置子代理不能直接跨到 DeepSeek
配置做到这里时,我原本希望 GPT 主对话直接生成一个使用 DeepSeek 的内置子代理。
实际测试结果是否定的。即使本地自定义 agent 指定了 DeepSeek,ChatGPT 账户的编排通道仍会拒绝第三方模型:
1 | |
这和前面的旧对话错配看起来很像,但发生层级不同:这次是内置 worker 的创建仍然经过 ChatGPT/OpenAI 路由,不能仅凭一个本地 agent 配置就跨到第三方 provider。
因此没有保留一个“配置上存在、实际必然报错”的 DS 子代理,而是改成独立进程:
1 | |
它固定使用:
1 | |
GPT 主控通过命令行把边界明确的任务一次性交给这个 worker。它和主代理共享工作区,但不经过内置子代理的跨 provider 路由。
最小真实调用返回:
1 | |
到这里,“GPT 主控 + DS 执行”才算真正可用。
八、目前采用的协作模式
日常使用时,GPT 负责:
- 理解需求和拆分任务
- 架构、风险与范围判断
- 高风险或不确定修改
- 审查 DS 的实际 diff
- 运行最终验证并整合结果
DS worker 负责:
- 批量修改同类文件
- 补测试和注释
- 机械性重构
- 搜索调用点和整理影响范围
- 修复成批的 lint / type errors
- 运行测试并归纳失败项
典型调用方式如下:
1 | |
--ephemeral 适合一次性的批量工作,不额外保存长期聊天记录。如果任务很长、需要保留 DS 对话,则去掉这个参数。
这里真正重要的不是“把任务交给 DS”这句话,而是同时限定:
- 可以修改哪些文件或目录
- 哪些行为不能改变
- 用什么命令验收
- 最后必须返回什么
- 是否允许提交 Git commit
此外不要让 GPT 和 DS 同时修改同一批文件。需要并行写入时,应拆到不同模块或独立 worktree;否则顺序执行更省事。
九、DS 是否真的能节省 GPT Token
答案是可以,但并不是只要调用一次 DS 就会节省。
我做过一次“让 DS 设计骑自行车的鹈鹕,再生成图片”的测试。链路是:
1 | |
这次 DS 只使用了 527 token,而 GPT 所在对话因为历史很长,多次工具往返累计了大量上下文输入,其中绝大部分虽然命中缓存,仍然会被统计。最终图片也是专用图像模型生成的,DS 只负责提示词,并没有直接输出 PNG。
这个测试证明协作链路可用,却不是一个省 Token 的好例子。为了让 DS 写一小段提示词,GPT 仍然要理解需求、调用 worker、读取结果、调用图像工具并汇总,多了一层协调成本。
真正适合委派的,是能让 DS 独立完成一整块工作的任务。例如:
- 修改几十个结构相似的文件
- 完成一批测试并自行运行验证
- 扫描大范围调用点并形成结果文件
- 按既定规则完成机械迁移
比较理想的节省方式是:
- GPT 一次完成分析和任务定义
- DS 一次读取、修改并验证
- GPT 最后只检查 diff 和测试结果
如果 GPT 与 DS 反复来回讨论,或者 DS 只负责很小的一步,通常省不到什么,甚至可能更耗。
十、额度耗尽后的交接
如果 GPT 额度已经完全用完,就不能再依赖 GPT 去创建或调度 DS worker。更可靠的做法,是平时就把项目状态写入工作区:
1 | |
然后在同一个项目中新建 DS 对话,先让它读取这些内容和 Git 状态,再继续工作。
只有关键上下文尚未整理、确实全部留在聊天记录中时,才有必要 fork 完整对话。长历史不仅成本更高,其中的旧工具输出、图片和临时判断也未必都值得交给另一个模型重新理解。
十一、托盘图标与进程退出
最后还顺手处理了一个容易误判的问题:切换后,Windows 托盘里的 Codex 图标会在鼠标移上去时突然消失。
原因不是托盘菜单坏了,而是旧切换脚本关闭主窗口后只等待 8 秒,发现还有 ChatGPT.exe 就强制结束全部进程。Explorer 留下了没有对应进程的“幽灵图标”,直到鼠标经过时才刷新掉。
修复后改为:
- 优先请求应用正常退出
- 最多等待 30 秒,让它保存状态并关闭辅助进程
- 不再自动执行强制终止
- 如果应用仍未退出,停止切换并提示手工处理
这部分和模型无关,但也说明 GUI 切换不能只考虑配置文件。一个能成功改写 TOML 的脚本,如果没有正确处理进程生命周期,依然不算可靠。
十二、总结
这次配置最后形成了两条不同的路径:
1 | |
前者用于人直接进入某个 provider 的完整对话环境;后者用于在 GPT 项目中,把重复、范围明确、可以独立验证的工作交给 DS。
最值得记住的几点是:
- 模型名称、模型目录、provider 和 thread 元数据不是同一个状态。
- 已有 GPT 对话不能因为在下拉框中选择 DS,就自动变成 DeepSeek 对话。
- 工作区可以共享,对话历史要通过交接文件或 fork 继承。
- 当前内置子代理不能可靠地跨到第三方 provider,独立 worker 更实际。
- DS 只有承担完整的大块执行工作时,才真正有机会节省 GPT Token。
- 无论使用哪个模型,最终都要以 diff、测试和可回滚状态作为验收依据。
一开始我想解决的是“怎么在 Codex 里切换两个模型”,最后得到的答案却是:
模型切换解决的是谁来继续对话;worker 委派解决的是谁来完成工作。
把这两件事分开之后,整个流程反而简单了。