1. 为什么 Vibe Coding 场景下必须吃透 Codex Agent Loop
Vibe Coding 这个词最近被聊得很多,但真正落到工程上,它描述的其实是一种交互节奏:你用自然语言描述意图,Agent 自己决定读哪些文件、跑哪些命令、改哪几行代码,然后把结果回传给你,你基于结果继续下一句。这个节奏能不能跑顺,取决于底层那条 Agent Loop 是否透明、可控、可观测。Codex 是目前把这条链路做得最工程化的实现之一,它把「模型推理」和「本地工具执行」拆成了两个明确阶段,中间用一套结构化的消息协议连接。
我这次要拆的就是这条链路:从你在终端敲下一句话开始,到 Codex 组装请求体、模型吐出 function_call、CLI 解析执行、结果回流、模型再推理,直到最终输出 final_answer。每一步的输入输出长什么样、状态怎么流转、哪些字段是你可以自己改的,都会给出可复制的配置片段和本地验证方法。
适合谁看:已经用过大模型 API、对 function calling 有基本概念、想自己搭一个 coding agent 或者想调优现有 CLI 工具的开发者。如果你只是想让 AI 帮你写个函数,那直接用现成工具就行;但如果你想理解「为什么 Agent 有时候会卡住」「为什么它不调用我定义的工具」「为什么多轮之后上下文爆了」,那这条 Loop 的每一环你都得心里有数。
核心检索词先摆出来:Codex Agent Loop 是一套「模型生成工具调用文本 → 本地 CLI 解析执行 → 结果作为新消息回流模型」的多轮闭环机制。它不是一个黑盒,请求体结构、工具 schema、phase 字段、AGENTS.md 注入方式都是公开可观察的。你要做的是在自己的环境里把这套结构复现出来,然后逐步替换成自己的工具和技能。
下面按「原问题 → 前置准备 → 可复制配置 → 验证请求 → 错排查 → 后续路径」的顺序展开。技术部分会占大头,拿 Key 和配环境的部分我会压缩到刚好够用。
2. TaoToken 前置:把模型端点与 Key 准备好
在复现 Agent Loop 之前,你得先有一个能接受结构化请求、支持工具调用字段的模型端点。Codex 的请求体走的是 Responses 风格的三段式(instructions / input / tools),所以你的接入层必须能透传这些字段,而不是只接受一个 messages 数组。
TaoToken 在这里的角色是提供一个兼容的 API 入口,让你不用自己维护多套厂商 SDK 就能把请求打出去。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数,直接拼路径即可。
你需要准备三样东西,我把它叫做「三件套」:Base URL、API Key、Model ID。这三者在后面所有配置片段里都会出现,缺一个请求就会失败。
Base URL 填https://taotoken.net/api。API Key 在控制台生成,路径是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。生成之后复制出来,不要提交到 git,建议放到环境变量里:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"Model ID 取决于你想用哪个模型来驱动 Agent Loop。Codex 类场景通常需要一个指令遵循强、支持长上下文、对 JSON schema 敏感的模型。你可以在模型对话页面先试一下模型对工具 schema 的响应质量,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 。选好之后把 Model ID 记下来,比如gpt-5.2-codex这类标识。
如果你打算长期跑编码 Agent、频繁做多轮工具调用,可以考虑 Coding Plan,路径是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。它更适合这种「一轮对话里多次 function_call」的消耗模式,比按次调用更划算。
前置准备到这里就够了。接下来进入核心部分:把 Codex 的请求体结构复现出来,并让 Loop 真正转起来。
3. 可复制配置:复现 Codex 请求体与 Agent Loop
这一节是全文重点。我会给出一个最小可运行的 Agent Loop 实现,包含请求体组装、工具注册、function_call 解析、结果回流四个环节。你可以直接复制到本地跑。
3.1 请求体三段式:instructions / input / tools
Codex 的请求体可以抽象成三个字段。instructions是系统级行为规范,input是多轮消息数组,tools是函数工具定义列表。下面是一个可复制的 JSON 骨架:
{ "model": "gpt-5.2-codex", "instructions": "You are a coding agent. Prefer rg over grep. Use apply_patch for small edits. Never run git reset --hard unless explicitly asked.", "input": [ { "role": "developer", "content": "sandbox_mode=danger-full-access, network=on, approval=never" }, { "role": "user", "content": "读取 example.py 的第 60 到 95 行并总结" } ], "tools": [ { "type": "function", "name": "exec_command", "description": "Runs a command in a PTY, returning output or a session ID for ongoing interaction.", "strict": false, "parameters": { "type": "object", "properties": { "cmd": { "type": "string", "description": "Shell command to execute." } }, "required": ["cmd"], "additionalProperties": false } } ] }三个字段的分工要记清楚。instructions决定模型「是谁、怎么干活」,input决定模型「看到什么上下文」,tools决定模型「能调用什么」。工具调用本质上仍然是文本生成,模型只是按 schema 吐出一段 JSON 字符串,真正执行在你本地。
3.2 工具注册表与 handler
CLI 端需要维护一个工具注册表,把 name 映射到具体的执行函数。下面是一个 Node.js 版本的最小实现:
const registry = { exec_command: { schema: { type: "object", properties: { cmd: { type: "string" } }, required: ["cmd"], additionalProperties: false }, handler: async (args) => { const { execSync } = require("child_process"); try { const out = execSync(args.cmd, { encoding: "utf8", timeout: 30000 }); return { output: out, exit_code: 0 }; } catch (e) { return { output: e.stdout || e.message, exit_code: e.status || 1 }; } } } };注册表的作用是双重的:一方面把 schema 塞进请求体的tools字段发给模型,另一方面在收到 function_call 时按 name 找到 handler 执行。这两件事必须用同一份 schema,否则模型生成的参数和你校验的规则会对不上。
3.3 Loop 主循环:从 function_call 到 function_call_output
主循环的逻辑是:发请求 → 检查返回里有没有 function_call → 有就执行 → 把结果作为新消息追加到 input → 再发请求 → 直到返回 final_answer。下面是核心循环:
async function runAgentLoop(userInput, maxTurns = 10) { const input = [{ role: "user", content: userInput }]; for (let turn = 0; turn < maxTurns; turn++) { const body = { model: process.env.MODEL_ID, instructions: INSTRUCTIONS, input, tools: Object.entries(registry).map(([name, t]) => ({ type: "function", name, description: t.description || "", parameters: t.schema })) }; const resp = await fetch(`${process.env.TAOTOKEN_BASE_URL}/responses`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${process.env.TAOTOKEN_API_KEY}` }, body: JSON.stringify(body) }); const data = await resp.json(); const item = data.output?.[0]; if (!item) throw new Error("empty output"); if (item.type === "function_call") { const tool = registry[item.name]; const args = JSON.parse(item.arguments); const result = await tool.handler(args); input.push(item); input.push({ type: "function_call_output", call_id: item.call_id, output: JSON.stringify(result) }); continue; } return item.content; } throw new Error("max turns exceeded"); }这段代码里有两个关键点。第一,call_id必须原样带回,它是把 function_call 和 function_call_output 配对的唯一标识。第二,input数组是累积的,每一轮都把历史消息带上,这就是为什么多轮之后上下文会膨胀。
3.4 AGENTS.md 与 Skills 注入
Codex 会在用户输入之前自动插入几类 developer 消息,其中最重要的是 AGENTS.md 聚合内容。你可以模仿这个做法,把项目规范、可用技能列表、触发规则拼成一段文本注入:
const agentsMd = ` # AGENTS.md instructions for ${process.cwd()} ## Available Skills - sync-fork-upstream: Sync a long-lived fork with upstream. Path: ~/.codex/skills/sync-fork-upstream/SKILL.md ## How to use skills - Trigger when user mentions $SkillName or task matches description. - Read only necessary parts of SKILL.md. - Prefer summarizing over pasting large content. `; input.unshift({ role: "user", content: agentsMd });这样模型在推理时就知道「有哪些本地工作流可用、什么时候该用、怎么读」。Skills 和 MCP 的区别在于:MCP 偏远程能力暴露,Skills 偏本地工作流加知识包。两者最终都以文本形式进入上下文。
3.5 phase 字段:区分中间进度与最终答案
Codex 用phase字段区分 commentary 和 final_answer。前者是中间状态更新,后者是本轮闭环输出。你在自建时也建议保留这个设计,方便做流式展示和日志追踪:
{ "role": "assistant", "phase": "commentary", "content": "正在读取文件..." } { "role": "assistant", "phase": "final_answer", "content": "第 60-95 行主要是配置解析逻辑。" }到这里,一个最小可运行的 Agent Loop 就搭好了。下一节验证它是否真的转起来了。
4. 验证请求:观察一次完整工具调用的生命周期
配置写完不代表能跑通。这一节给出具体的验证步骤,让你亲眼看到 function_call 从生成到执行到回流的全过程。
4.1 准备测试文件
先造一个测试目标,方便观察工具调用:
mkdir -p ~/agent-loop-test && cd ~/agent-loop-test for i in $(seq 1 120); do echo "line $i: example text for showcase" >> example.py; done这样 example.py 有 120 行,足够触发「读取指定行范围」的工具调用。
4.2 发起第一轮请求并打印原始返回
在循环里加一行日志,把每轮的原始返回打出来:
console.log("=== turn", turn, "==="); console.log(JSON.stringify(data.output, null, 2));然后调用:
runAgentLoop("读取 example.py 的第 60 到 95 行并总结").then(console.log);预期你会看到第一轮返回里有一条type: "function_call",name 是exec_command,arguments 是一段 JSON 字符串,里面 cmd 类似sed -n '60,95p' example.py。call_id 是一串唯一标识。
4.3 观察 function_call_output 回流
第二轮请求发出后,打印 input 数组的长度和最后两条消息。你应该看到:
{ "type": "function_call", "name": "exec_command", "arguments": "{\"cmd\":\"sed -n '60,95p' example.py\"}", "call_id": "call_abc123" } { "type": "function_call_output", "call_id": "call_abc123", "output": "{\"output\":\"line 60: example text...\",\"exit_code\":0}" }模型在第三轮看到这条 output 后,通常就不再发 function_call,而是直接返回phase: final_answer的自然语言总结。整个 Loop 转了两到三轮,状态流转清晰可见。
4.4 用 curl 单独验证端点连通性
如果你怀疑是端点问题而不是代码问题,可以先用 curl 打一发最小请求:
curl -s https://taotoken.net/api/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "'"$MODEL_ID"'", "input": [{"role":"user","content":"say ok"}] }' | head -c 500返回里能看到模型输出就说明 Base URL 和 Key 没问题,问题在 Loop 逻辑里。这一步能帮你快速定位故障层。
4.5 记录每轮耗时与 token 消耗
在循环里加计时:
const t0 = Date.now(); const resp = await fetch(...); console.log("turn", turn, "latency", Date.now() - t0, "ms");实测下来,一次 exec_command 调用加回流大约 3 到 8 秒,取决于命令执行时间和模型推理速度。如果某一轮超过 30 秒,大概率是命令卡住了或者上下文太大导致推理变慢。
验证通过后,你就有了一个可观测、可调试的 Agent Loop。接下来处理常见故障。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节按真实报错来组织。每个报错给出触发条件和修复方法。
5.1 401 Unauthorized
最常见。触发条件:Authorization 头缺失、Key 写错、Key 前后有空格、环境变量没导出。检查顺序:
echo "key length: ${#TAOTOKEN_API_KEY}" echo "base: $TAOTOKEN_BASE_URL"如果 key length 是 0,说明环境变量没生效。如果长度正常但还是 401,去控制台重新生成一个 Key,路径是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。注意 Base URL 不要写成带 UTM 的官网地址,API 基址就是https://taotoken.net/api。
5.2 local proxy failed
这个报错通常出现在你本地配了某个转发层,但转发层没起来或者端口不对。触发条件:代码里把 Base URL 指向了http://localhost:xxxx之类的本地地址,但那个服务没运行。修复方法:直接把 Base URL 改成https://taotoken.net/api,去掉本地转发层。如果你确实需要本地转发做日志抓取,先确认转发进程在监听,再确认转发目标写的是正确的 API 基址。
5.3 reading 'choices' of undefined
这个报错说明你的解析代码假设返回体里有choices字段,但实际返回结构不是 Chat Completions 格式。Codex 风格的请求走的是 Responses 结构,输出在output数组里,不是choices。修复:
// 错误写法 const content = data.choices[0].message.content; // 正确写法 const item = data.output?.[0]; const content = item?.content;如果你用的是 Chat Completions 端点,那返回里才有 choices。两种端点的解析逻辑不能混用。先确认你打的是哪个路径,再改解析。
5.4 OAuth 相关报错
触发条件:你用了某个 CLI 工具的登录态,但 token 过期或者 scope 不对。这类报错的特征是返回里带invalid_grant或token expired。修复方法:重新走一遍登录流程,或者干脆改用 API Key 方式接入,避免 OAuth 状态维护。在自建 Loop 里,直接用 Bearer Key 最省事。
5.5 工具调用参数校验失败
报错形如additionalProperties is not allowed或required property missing。原因是模型生成的 arguments 不符合你注册的 schema。两个修复方向:一是把 schema 放宽,比如把additionalProperties设为 true;二是在 instructions 里更明确地约束参数格式。实测下来,schema 越严格,模型越容易踩坑,建议先用宽松 schema 跑通,再逐步收紧。
5.6 循环不终止
触发条件:模型反复调用同一个工具,或者每轮都发 function_call 但结果没变化。修复:加 maxTurns 上限(前面代码里已经加了),并在 instructions 里写明「如果工具输出已足够回答问题,直接给出 final_answer,不要重复调用」。另外检查 function_call_output 的 call_id 是否和上一轮 function_call 一致,不一致会导致模型认为调用没完成。
排障时如果拿不准是端点问题还是代码问题,先回到第 4.4 节的 curl 验证,把变量隔离出来。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各端点的字段说明,对照着看能省不少时间。
6. 从 Loop 到生产:Coding Plan 与后续路径
把最小 Loop 跑通只是第一步。真正要在项目里用起来,你还需要处理几件事:上下文压缩、工具权限控制、多轮状态持久化、以及成本控制。
上下文压缩方面,Codex 的做法是频繁发 commentary 中间进度,避免长时间失联,同时在 AGENTS.md 里强调「只读必要内容、优先摘要」。你可以模仿这个策略,在每轮把旧的 function_call_output 做摘要替换,而不是原样累积。
工具权限控制方面,instructions 里要明确禁止危险操作。比如「非用户明确要求,不得执行 git reset --hard 或 git checkout --」。这条约束在自建时同样适用,而且要在 handler 层再加一道校验,不能只靠模型自觉。
成本控制方面,多轮 function_call 的 token 消耗比单轮对话高得多。如果你打算长期跑编码 Agent,Coding Plan 的路径是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,它针对这种高频多轮场景做了优化。模型选择上,可以先用模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 对比几个候选模型在工具调用上的稳定性,再决定长期用哪个。
如果你用的是 Claude Code 这类工具,接入方式类似,核心还是三件套:Base URL 填https://taotoken.net/api,Key 用控制台生成的,Model ID 按你的场景选。配置片段和前面给的 JSON 结构一致,只是外层封装不同。
最后给一个实用技巧:在 Loop 里加一个「工具调用轨迹」日志,把每轮的 name、arguments、call_id、耗时、exit_code 记到一个 jsonl 文件里。跑一段时间后回看这个文件,你能清楚看到模型在什么任务上容易绕圈、哪些工具调用是多余的、哪些 schema 设计导致参数反复出错。这比盯着单次输出调 prompt 有效得多。轨迹数据攒够了,再考虑要不要把这套 loop 固化进训练,那就是另一条路了。