Codex 在 SELECT 里写了 user.handle,但列名其实是 user.username。或者调了 sendEmail(to, subject, body),可签名是 sendEmail({ to, subject, html })。或者硬编码了 https://api.example.com,而你项目里这个 URL 走的是 process.env.API_URL。代码看着对,但要么不编译、要么跑不起来,更糟的是跑到了错的目标上。
最快的修法: 在 prompt(或 AGENTS.md)开头加一条规则:「写任何涉及外部函数、schema 列或环境变量的代码前,先读它所在的源文件,并在回答里原样引述那一行。引述不出来就停下来问。」 这一条就能逮住绝大多数不安全假设,因为它逼着 Codex 去开文件,而不是从训练先验里猜。
底层问题是「自信幻觉」:Codex 没读你的真实文件,而是从见过的形状里推断。没有 grounding,能力再强的模型也会猜。下面是完整的诊断、按目标分类的 prompt(schema、签名、环境变量),以及怎么验证结果。
你属于哪一类
把症状对到最可能的原因,然后直接跳到对应修法。
| 生成代码里的症状 | 最可能的原因 | 先查什么 | 修法 |
|---|---|---|---|
| 列名 / 表名错 | Codex 根本没开 schema | 记录里有没有读 schema.prisma? | Step 2 |
| 参数顺序 / 形状错 | 从 import 或网红库推断签名 | 在记录里搜有没有读定义文件 | Step 1、Step 3 |
| 环境变量名或类型错 | 编了名字;把 string 当 number | 对照 .env.example 比对 | Step 4 |
| 用了根本不存在的符号 | 纯幻觉 | grep 找定义 | Step 5 |
| 看着对但实际是历史旧值 | AGENTS.md 或 JSDoc 过期 | git log -1 比 doc 和源 | Step 6 |
常见原因
按命中率从高到低排。
1. Codex 没开文件,从 import 推断
Prompt 里点了文件名,但 Codex 根本没打开过。它从这个函数在别处被 import 的样子、或训练先验里猜了形状,参数细节就错了。
如何判断:在 Codex TUI 里跑 /status 看哪些目录在范围内,再翻记录找有没有读相关文件的 tool 调用。如果没有读到那个文件,就是 Codex 凭想象。(在 workspace-write 模式下 Codex 可以 自动读你的文件,但它得自己选择去读,而且只能在 workspace 边界内。在 prompt 里点了文件名,不等于强制读了。)
2. AGENTS.md 描述的是过去的现实
你的 AGENTS.md 写 users 表有 handle,6 个月前你改名成了 username,文档没更新,Codex 信了它。这件事现在比以前更要命:Codex 会把多个 AGENTS.md 按 root 在前、leaf 在后拼接,直到 project_doc_max_bytes(截至 2026 年 6 月默认 32 KiB)为止,所以一个过期的根文件会在每一轮都喂给 Codex 错的事实。
如何判断:在 AGENTS.md 里 grep Codex 用的那个错符号。在里面就是文档过期。
3. Codex 套了「网红」泛模式
sendEmail(to, subject, body) 是无数 Node.js 博客里的经典写法,但你项目用的是 options-object 变体。Codex 默认走了网红那个。
如何判断:错签名匹配某个明显的开源库形状,说明 Codex 套了常见模式,没套你的代码。
4. Schema 在 Codex 没读的生成文件里
Prisma 的 schema.prisma 是源,但 TypeScript 类型在生成的 node_modules/.prisma/client/index.d.ts 里。Codex 两个都没读,退回去猜。
如何判断:错类型或错列出现在本该用 Prisma 生成类型的地方。看 Codex 的 read 列表里有没有 schema.prisma 或生成的 .d.ts。
5. 环境变量名 / 形状是编的
你写 STRIPE_KEY=sk_test_...,Codex 用了 STRIPE_API_KEY(多了个词)。或者把 process.env.PORT 直接当数字用,但环境变量永远是 string,你的代码得用 parseInt 或 Number()。
如何判断:Codex 的环境变量名和形状对照 .env.example,对不上的基本都是编的。
6. 过度相信过期的注释 / JSDoc
函数有 JSDoc 写 @param userId: string,但实现现在其实接受 number | string。Codex 信了 doc,传了 string,跑起来也没事,直到别处某个 caller 传了 number,而类型系统又没拦住。
如何判断:实现签名和 JSDoc 对不上。JSDoc 是谎言,实现才是真相。
最短修复路径
按收益从高到低,Step 1 一步就能逮住绝大多数不安全假设。开始前有个好用的小技巧:在规划阶段用 /permissions 把 Codex 切到只读(或启动时加 --sandbox read-only),这样它没得选,必须先读、先引述,才可能写出任何东西。
Step 1:先引述再写
每个生成代码的 prompt 里都加上:
写任何要调用外部函数 / schema / 环境变量的代码前:
1. 读它定义所在的文件。
2. 在回答里原样引述相关行(签名 / 列 / export)。
3. 然后才写调用代码。
如果引述不出来(文件没找到、不编译),停下来问。
不要从 import 推断,也不要靠训练先验。
多了一步,但根除了「我以为长这样」这种失败。
Step 2:schema 强制先读
你要对 `users` 写查询。
先:读 `prisma/schema.prisma`,把 `User` 模型原样引述。
然后:只用引述里出现过的列写查询。
缺列就建议加(不要假设有)。
没用 ORM 就指向 migration 或原始 DDL 文件。如果用 Prisma,也告诉 Codex 它可以读生成的 node_modules/.prisma/client/index.d.ts 拿精确类型。
Step 3:函数签名强制先读
你要调 `sendEmail`。
先:读 `src/lib/email.ts`,把 export 签名引述出来。
然后:按引述的签名写调用。
引述不出来,文件可能根本不存在,这种不一致最好在任何代码落地前就显出来。
Step 4:环境变量锚到 .env.example
你要用一个环境变量。
1. 读 `.env.example`,引述相关行。
2. 用 `.env.example` 里出现过的精确名字——不要编变体。
3. 记住环境变量值都是 string,需要数字就用 parseInt / Number 转换。
4. 没有这个变量的话建议加(不要假设有)。
Step 5:写完用 grep 验证
不要信汇报。核实引用的符号真的存在:
# 新代码里每个独立标识符都 grep 一下定义
grep -rn "function sendEmail\|export const sendEmail\|export function sendEmail" src/
grep -rn "model User\|interface User" prisma/ src/
grep 不到,就说明 Codex 用了不存在的符号。然后让编译器和真实运行收尾:npx tsc --noEmit 能查出 grep 查不到的错误形状,跑一遍快速测试能逮到那些存在但取值错了的环境变量。
Step 6:靠 AGENTS.md 之前先刷新
如果你的 AGENTS.md 比它描述的代码还旧,而代码又变得很快,它就在骗 Codex。查时间戳:
# 关键 doc 和关键文件的最后更新时间
git log -1 --format="%ai" AGENTS.md
git log -1 --format="%ai" prisma/schema.prisma
git log -1 --format="%ai" src/lib/email.ts
如果 schema 或 email 文件比 AGENTS.md 新,就刷新 AGENTS.md 或删掉过期段,让 Codex 直接读源。由于 Codex 把拼接后的 AGENTS.md 链截到 32 KiB、且 root 在前,根文件要保持精简——臃肿的全局文件会挤掉那些其实准确的目录级说明。
怎么确认修好了
- 回答里引述了真实的行。 一个接地的回答会在生成代码前先贴出你文件里真实的签名 / 列 / 环境变量行。没有引述,就是又猜了。
grep能找到每个被引用的符号(Step 5)。哪里都搜不到,就说明有个幻觉标识符溜进去了。- 类型检查干净。
npx tsc --noEmit(或你语言的等价命令)通过,没有「property does not exist」或参数个数错误。 - 它跑在对的目标上。 确认代码里用的 URL 或 key 解析到的是环境变量,而不是硬编码的字面量。
预防建议
- 把「先引述再写」写进
AGENTS.md当永久规则,而不是每个 prompt 重复一遍。 - 在
AGENTS.md里直接点名 schema、环境变量、函数签名的源文件。 - 把生成类型(Prisma、GraphQL codegen)当真相,让 Codex 去看它们。
- 根
AGENTS.md保持精简,别撞上 32 KiB 上限挤掉准确的嵌套文件;季度审一次,删掉与代码不符的段落。 - 风险高的规划阶段用
/permissions让 Codex 跑read-only模式,逼它先读后写。 - Codex 写完后 grep 引用的符号,缺任何一个就是幻觉。
- AI 的自信和正确率没关系。用编译器和 grep 验证,别信回答的语气。
常见问题
在 prompt 里点了文件名,Codex 就会去读它吗?
不会。在 workspace-write 模式(默认的「Auto」沙箱)下 Codex 可以 读 workspace 里的文件,但读不读由它自己定。点名只是个提示,不是保证。「先引述再写」规则把提示变成硬要求,因为没有引述出来的回答就是不完整的。
只读模式能彻底让 Codex 不再幻觉吗?
不能,但它把失败模式换成了一个有用的形态。read-only 模式下 Codex 不能改文件,所以它会先产出一个计划让你审,然后才落地代码。它仍然得先读、先引述才能接地这个计划,而这正是你要的 grounding。要应用改动就切回 workspace-write。
Codex 坚持说某个列存在,但我明明删掉了,为什么?
几乎都是文档过期。在 AGENTS.md 和各处 JSDoc 里 grep 那个旧名字,再用 git log -1 比 doc 和 schema。如果 schema 后来才改过,那 doc 就在每一轮都喂给 Codex 一个过时事实,直到你修掉它。
环境变量名对了,但运行时取的值不对,怎么办?
这不是假设的 bug,变量是存在的。检查你有没有做类型转换(parseInt(process.env.PORT))、.env 和 .env.example 的名字是否一致、以及进程是不是真的加载了 .env(Codex 的沙箱默认不会注入你 shell 里的环境变量)。
怎么看 Codex 到底读了什么?
翻 TUI 记录找文件读取的 tool 调用,再跑 /status 确认哪些目录在范围内。如果你关心的那个文件从没被读到、也不在范围里,那就是你的根因。
相关阅读
外部参考:Codex agent approvals & security 和 Codex configuration reference(project_doc_max_bytes 与沙箱模式说明)。
标签: #Codex #Coding Agent #排查 #排查 #不安全假设