如果你想让 Codex 成为真正服务于自己项目的本地自定义 Agent,那 TOML、AGENTS.md 和优先级这三个词会是你绕不开的关卡。我最早以为把配置里的模型名改成 DeepSeek 就能直接跑,结果命令行反复报错,最后才明白,接入点、项目指令、配置覆盖顺序这三样东西才是整个自定义体系的骨架。这篇文章记录的是我实际折腾两周后整理出来的完整思路,适合已经装好 Codex 命令行工具、想进一步做本地化或第三方模型接入的开发者。不吹不黑,照着做基本能跑通,但更关键的是理解它背后的逻辑。
1. 配置 Codex 之前,先想清楚三件事
很多人一上来就直接搜配置文件路径,然后把参数抄一遍就完事。这样也不是不行,只是遇到问题你会完全不知道从哪下手。我的建议是,动手之前先把三件事弄明白:Codex 默认在跑什么,TOML、AGENTS.md、优先级分别管什么,以及改坏了怎么快速回退。
1.1 Codex 默认在跑什么
Codex 默认情况下是一个偏向"云端自动执行"的终端编程助手。它启动后会自动读取你的配置,连接模型服务,然后在一个受控的沙盒环境里执行命令、读写文件、调用工具。你给它一句"帮我修一下测试",它会自己规划步骤、跑测试、看结果、改代码。
在这个过程中,有三个东西决定了它的行为:
- 模型从哪来:默认走 OpenAI 官方接口,模型名、接口地址、鉴权方式都在
config.toml里。 - 行为按什么规矩来:它进入项目后会自动寻找
AGENTS.md,把它当作项目操作的"说明书"。 - 冲突听谁的:当全局配置、项目配置、会话里的临时要求之间出现冲突时,有一套优先级规则决定最终结果。
这三条线正好对应当前的标题:TOML、AGENTS.md 和优先级。把它们拆开看,配置工作就没那么玄乎了。
1.2 TOML、AGENTS.md、优先级各干各的
我刚接触时最大的误解,是想用一份配置文件把什么都塞进去。后面发现这是三个不同层面的东西,至少在心里要分成三层:
TOML 是"接线图",负责把请求送到正确的模型服务。改 provider、改 base_url、改模型名,都是在这里完成。
AGENTS.md 是"操作手册",负责告诉 Agent 这个项目有什么特殊规矩。比如测试命令是什么、哪个目录不能动、代码风格用什么。
优先级是"指挥规则",负责在这些配置和指令交叉作用时决定谁说了算。比如你全局配了一个模型,项目里又指定了另一个,到底用哪个?再比如根目录和子目录都有 AGENTS.md,Agent 听谁的?这就是优先级问题。
打个比方可能更好理解:TOML 决定了你开车走的路线,AGENTS.md 决定了你车上的驾驶规范,优先级则是在交叉路口遇到交警时听交警的还是听红绿灯的。
1.3 动手前先做好备份和最小验证
这是我从一次惨痛教训里总结出来的。当时我为了接一个第三方模型,把~/.codex/config.toml改得面目全非,结果 Codex 连启动都报错,我又记不清原来写了什么,只能凭记忆重写,浪费了大把时间。
现在我的习惯是,每次改配置之前先给整个配置目录做一次快照:
cp -r ~/.codex ~/.codex.bak.$(date +%Y%m%d%H%M)如果你习惯用 dotfiles 管理配置文件,也可以先提交一次 git commit。改完之后不要急着去跑大任务,先开一个最简会话,让它执行一个简单命令,比如"输出当前目录结构"。这样能最快确认配置是否生效。
2. 手写 config.toml:把模型接入点彻底搞清楚
config.toml是 Codex 的核心配置文件,默认位置在~/.codex/config.toml。它的语法比 JSON 友好,但注意它和 JSON 不一样:没有花括号包住全部内容,而是靠方括号来分组。这里我最常犯的错误就是写错层级的缩进和表名,导致字段被放到了错误的位置。
2.1 一份最基础的配置长什么样
如果你是纯默认使用,配置甚至可以短到只有两三行:
# ~/.codex/config.toml model = "gpt-5" model_provider = "openai"model指定模型名,model_provider指定模型走哪个 provider。所谓的 provider,就是一个"通道",它告诉 Codex 该把请求发到哪个地址、用哪个鉴权环境变量。
一旦你要接第三方模型或本地模型,就得自己定义 provider。定义一个 provider 的通用写法是这样的:
[model_providers.custom] name = "自定义服务名" base_url = "https://your-api-endpoint.example/v1" env_key = "CUSTOM_API_KEY" wire_api = "chat"name是人类可读的名字,base_url是接口地址,env_key告诉 Codex 从哪个环境变量里读密钥,wire_api是通信协议类型。这里的wire_api要重点记住,它有两个值:chat和responses。Codex 本身默认偏向新的 responses 协议,但很多第三方服务和本地模型只实现老的 chat completions 协议,不把wire_api设成chat,就会出现"接口不存在"之类的报错。
2.2 接入 DeepSeek 这类第三方服务的完整写法
现在很多人想把 Codex 接到 DeepSeek 这类 OpenAI 兼容模型上,操作其实不复杂。我的配置文件里是这么写的:
model = "deepseek-chat" model_provider = "deepseek" [model_providers.deepseek] name = "DeepSeek" base_url = "https://api.deepseek.com/v1" env_key = "DEEPSEEK_API_KEY" wire_api = "chat"然后在你当前的 shell 环境里设置密钥:
export DEEPSEEK_API_KEY="sk-你的密钥"注意我特别在这一小节里把wire_api = "chat"写了进去。因为 DeepSeek 官方接口目前走的是 chat completions 格式,如果漏掉这一行,Codex 可能会用默认的 responses 协议去请求,最后你会看到"路径不存在"这种莫名其妙的现象。这个坑我踩过,后来才发现问题根本不在网络,而在协议不匹配。
另外,base_url的后缀/v1也很关键。有的服务商让你填https://api.deepseek.com不带/v1,有的则要求带上。我的经验是,凡是说自己兼容 OpenAI API 的,绝大多数都要求/v1路径。拿不准的时候,可以用 curl 直接看服务商文档里的示例。
2.3 密钥别直接写进配置文件
有一点想特别强调:config.toml里不要写任何密钥明文。哪怕这个文件只在你自己的电脑上,我也建议用env_key的方式。因为配置文件很容易被同步到网盘、git 仓库或者被某个截图带跑,而 API Key 一旦泄露,损失不可控。
env_key的机制是:Codex 在启动时从指定的环境变量里读取密钥,而不是从配置文件里读。环境变量可以在~/.bashrc、~/.zshrc里设置,也可以临时在终端里 export。对本地模型来说,密钥可能并不需要真实值,但 Codex 仍然需要一个环境变量存在,否则会提示找不到密钥。这种时候你可以设置一个占位符,比如:
export OLLAMA_API_KEY="local-placeholder"3. AGENTS.md 到底怎么写,Agent 才会真听你的
配置文件解决的是"请求发去哪"的问题,而AGENTS.md解决的是"项目该按什么规矩执行"的问题。Codex 在进入一个项目目录时,会自动查找这个文件,并把里面的内容作为额外的系统指令注入到对话上下文里。
3.1 为什么不用把项目规范写进 TOML
有人会问,既然 TOML 是配置,为什么不把"禁止修改 migrations 目录"也写进去?答案很简单:TOML 能表达的是结构化的键值参数,而 AGENTS.md 要表达的是自然语言规则。Agent 对自然语言指令的理解远比解析配置项要好。而且 AGENTS.md 可以跟着项目走,你把这个目录发给同事或者放到新机器上,规则依然有效。TOML 则会跟着用户主目录走,是"人"的配置,不是"项目"的配置。
所以我的习惯是:跟人走的放~/.codex/config.toml,跟项目走的放项目根目录AGENTS.md。
3.2 AGENTS.md 的存放位置和继承关系
AGENTS.md 可以放在多个层级。我现在常用的结构是这样:
~/.codex/AGENTS.md:全局偏好,比如"所有代码注释用中文""默认使用 Python 3.11"。- 项目根目录
AGENTS.md:项目级约定,比如"测试命令是 pytest tests/ -q""不要动生成的代码文件"。 - 子目录
AGENTS.md:如果你在非常复杂的 monorepo 里,可以在具体模块目录放一个更细的说明。
它们的覆盖关系,我的理解是:越靠近当前工作目录的 AGENTS.md,优先级越高。也就是说,子目录里的规则会覆盖或补充项目根目录里的规则,项目根目录会覆盖或补充全局规则。如果你发现一个指令老是没生效,先看看是不是被更外层的 AGENTS.md 里的同话题规则给稀释了。对 Agent 来说,多份规则同时存在时,它可能不会丢弃任何内容,而是把它们都当作参考。所以要避免在多个文件里写互相矛盾的话。
3.3 写一份高质量 AGENTS.md 的实操模板
我踩过最大的坑,是把 AGENTS.md 写成"论文"。写一大堆正确的废话,Agent 反而不知道该抓什么重点。后来我总结出一个比较好用的结构:总体约定 + 常用命令 + 禁止事项。
下面是我给一个 Python 项目写的示例:
# 项目指令 ## 总体约定 - 代码要求 Python 3.11 及以上,使用类型注解。 - 注释和文档使用中文,git commit 信息使用英文。 - 新功能必须包含对应测试。 ## 常用命令 - 安装依赖:pip install -e ".[dev]" - 运行测试:pytest tests/ -q - 代码格式化:ruff format . ## 禁止事项 - 不要修改 migrations/ 目录下的文件。 - 不要提交 .env 文件到版本控制。 - 不要在未运行测试的情况下改动核心逻辑。这个模板的好处是短、直接、每条都可以执行。Agent 读取后能快速形成"在这个项目里该怎么干活"的预期。如果你写了一条很模糊的规则,比如"代码质量要高",那 Agent 只能靠猜。不如写清楚"测试覆盖率不能低于 80%"。
我从测试中发现,AGENTS.md 里的指令应该尽量使用祈使句和明确条件,少用形容词。因为模型对形容词的理解是有弹性的,对具体动作的理解则更稳定。
4. 优先级不是"后写的覆盖先写"那么简单
现在来说标题里的第三个关键词:优先级。这部分我觉得是最容易让人懵的。很多人以为,配置的优先级就是"谁写在后面听谁的",实际情况要复杂一些。
4.1 配置层级的先后关系
先从大的层面试着梳理。Codex 的配置来源大致有这么几层:
- 命令行里显式传递的参数,优先级最高。
- 环境变量,比如 API Key 的选择和变更。
- 配置文件
config.toml。 - 内置默认值。
什么意思呢?如果你在启动 Codex 时显式指定了某个模型,那么 config.toml 里的model字段就算写了别的,也会被命令行参数压过。反过来,如果 config.toml 里什么都没写,Codex 就会用内置默认值。
这个设计其实和很多命令行工具的惯例一致:命令参数 > 配置文件 > 默认值。在排查问题的时候,我会建议先确认"当前会话到底有没有通过参数指定过什么"。有时候是历史命令被 shell 自动补全带进来了,就会悄悄改变实际生效的配置。
4.2 最常见的问题出在 provider 覆盖
我在本地实验时遇到过这么个情况:config.toml 里明明把model_provider写成了本地服务,但实际请求还是发去了默认的 OpenAI 地址。后来排查了半天发现,是会话启动时我用了某个启动参数,那个参数里指定了 provider 为 openai,把文件里的配置覆盖掉了。
这类问题在同时配置多个 provider 时非常常见。你有 openai、deepseek、ollama 三个 provider 躺在配置里,只要model_provider没有明确指向,或者某个隐藏参数把 provider 锁定了,请求就会走错地方。
我的排查经验是:不要同时把多个 provider 全设为"能用",只留当前要用的那个,其余的先注释掉。等跑通了再加回来。这样可以极大减少"配置生效了但生效的不是我想要的"这种问题。
4.3 用 profile 或独立配置来管理多环境
如果你确实需要经常在云端模型、第三方模型、本地模型之间切换,建议不要反复注释代码块,而是把多套配置拆成 profile 或者用环境变量隔离。Codex 本身支持通过命令行参数选择 profile 的用法,不同版本参数名可能略有差异,启动时可以用--help看下。核心思想是每一套环境一个独立配置块,用的时候指定:
- 日常贵但强的模型:官方服务。
- 预算敏感的批量任务:DeepSeek 这类第三方。
- 完全离线的调试:Ollama 本地模型。
这样做最大的好处是,你不会陷入"改一行配置、跑一次验证、改错了再回滚"的循环。优先级规则的真正意义,不是让你研究出谁最优先,而是让你主动选择当前场景下谁最优先。
5. 把 Codex 接到本地模型:Ollama 的完整实战
前面讲了配置和指令,现在说说真正的本地化。既然标题里有"本地自定义 Agent",这一步不能少。我目前最常用的本地模型运行时是 Ollama。选它没什么特别深刻的原因,主要是它安装简单、跨平台,而且直接提供 OpenAI 兼容的接口,配 Codex 不需要额外写一层转换服务。
5.1 完整配置步骤
第一步,安装 Ollama。装好后默认服务地址是http://127.0.0.1:11434。如果你访问那台机器的另一个端口,记得把防火墙和监听地址一起确认下。
第二步,拉取一个适合编码的模型。我做过几次对比之后,觉得从实用角度优先看 qwen2.5-coder 系列和 llama3.1 系列。以 qwen2.5-coder 14b 为例:
ollama pull qwen2.5-coder:14b第三步,确认本地接口的 OpenAI 兼容地址。Ollama 启动后,http://127.0.0.1:11434/v1就是 OpenAI 兼容端点。你可以先用 curl 验证:
curl http://127.0.0.1:11434/v1/models如果能返回模型列表,说明接口是通的。这个验证步骤别跳过,很多后面出现的"连不上"问题,在这一步就能发现。
第四步,修改config.toml增加一个 local provider:
model = "qwen2.5-coder:14b" model_provider = "ollama" [model_providers.ollama] name = "Ollama" base_url = "http://127.0.0.1:11434/v1" env_key = "OLLAMA_API_KEY" wire_api = "chat"第五步,在 shell 里设置占位密钥:
export OLLAMA_API_KEY="local-placeholder"然后启动一个最简会话,让它做一些基础的文件操作。如果这一步通了,再让它写一小段逻辑代码。不要一上来就丢一个"重构整个项目"的大任务给它,先把链路确认稳固。
5.2 本地模型常见的坑
本地模型和云端模型在使用体验上有很大差异。最大的坑是上下文窗口和工具调用能力。有些模型参数写得很大,实际推理时一旦塞入太多工具定义,就开始出现漏调用、重复调用甚至直接终止执行的情况。
我的建议是,本地模型更适合做中等规模任务:写单元测试、做代码解释、按规范改一个函数。不太适合让它做整个 monorepo 级别的自动重构。另外,本地模型的推理速度和显存直接相关,14B 模型在 24G 显存上表现尚可,在 8G 显存上就会明显掉速。你要是手头硬件有限,可以先跑 7B 甚至更小的模型,不要盲目追求参数量。
5.3 为什么 base_url 结尾要不要带 /v1 是个高频错误
接 Ollama 时,base_url要写http://127.0.0.1:11434/v1,这个/v1很关键。如果你写成了http://127.0.0.1:11434,Codex 会把请求发到 Ollama 的根路径,路由对不上,结果就是 404 或者路由不存在的错误。同理,接 DeepSeek 时也要求/v1结尾。
这个细节特别容易忽略,因为 Web 上很多教程直接给了完整地址,你复制过来是通的,但你不知道自己实际上在改什么。一旦换一个没有/v1的服务商,就开始瞎猜原因。
6. 踩坑记录:端点配置失败与沙盒执行错误怎么查
最后这部分,把我实际碰到的两个典型报错和排查思路完整记录下来。它们非常具有代表性。
6.1 "本地端点切换失败"这类报错,问题通常不在网络
有一次我在调试自定义模型服务,命令行直接弹出一串英文报错,大意是"切换本地端点失败,处理 codex endpoint /responses 时出了问题"。我当时的条件反射是去查网络通不通、Key 对不对,折腾了半天都没用。最后才发现,问题根本不在网络,而是 Codex 默认在请求/responses这个新协议接口,而我连的那个本地服务只实现了旧的/v1/chat/completions。
解决办法就是前面提到的wire_api = "chat"。这行一加上,请求就切换成了 chat 协议,报错马上消失。
所以遇到端点类报错,我的排查顺序固定是:
- 先确认
base_url对不对,用 curl 直连看看返回什么。 - 再确认
wire_api是否和服务端能力匹配。 - 最后才去检查 Key 和环境变量。
把网络放最后,不是因为网络不会出问题,而是大部分报错其实是协议不匹配和路径错误,查网络纯粹浪费时间。
6.2 "agent execution terminated due to error"的排查路径
这个报错看起来非常吓人,好像整个 Agent 都崩了。我遇到时第一反应是模型太弱,或者是沙盒环境坏了。其实这类错误往往发生在 Agent 执行阶段,而不是模型接入阶段。也就是说,模型已经成功连上,但在执行某个命令、调用某个工具时崩了。
我的排查办法是把问题拆成两段看:
先用最小会话测试,让 Agent 只做一件不涉及工具调用的事,比如"介绍一下这个项目";如果这一步正常,说明模型接入没问题。然后让 Agent 执行一个简单命令,比如"列出当前目录文件";如果这一步崩了,问题就在沙盒执行层。这时候可以试试降低请求上下文、换更稳定的模型,或者把明确冲突的 AGENTS.md 指令简化。
下面是我整理的一张速查表,方便你在遇到类似问题时快速对照:
| 报错特征 | 大概率原因 | 排查顺序 |
|---|---|---|
| 路径不存在或找不到 /responses | 服务端不兼容 responses 协议 | 将 provider 的 wire_api 改成 chat |
| 401、403 鉴权失败 | API Key 没传对 | 检查 env_key 对应的环境变量 |
| 404 路由不存在 | base_url 漏了 /v1 或多了路径 | 用 curl 访问 /v1/models 对比 |
| Agent 执行中途终止 | 沙盒工具或代码解释器异常 | 用无工具的最小会话隔离问题 |
6.3 多用最小复现,少用大项目验证
这是我在整个配置实战里最想强调的工程习惯:每次只改一个变量。无论是改模型、改 provider 还是改 AGENTS.md,一次只动一处,然后立刻用一个三五秒就能完成的会话任务验证。不要想着"反正都改了,一起看看行不行",因为一旦出问题,你根本无法判断是哪一处改动导致的。
如果你在多个 provider 之间反复切换还经常出错,我建议回到最简配置:只留一个 provider、一个模型、一个 AGENTS.md。跑通了,再一件一件加回来。这个过程虽然看起来慢,但你节省的是排查问题的几倍时间。
我自己现在把 Codex 日常场景固定成三套配置:云端模型跑核心任务,DeepSeek 做预算敏感的批量请求,Ollama 完全离线调试。配置这东西没有标准答案,但只要你把 TOML、AGENTS.md、优先级这三条线理清,后面接任何新模型都不会再手忙脚乱。如果你也有类似的折腾经历,欢迎把报错贴出来,我们一起看看是哪一层优先级抢走了控制权。