
1. 为什么 Vibe Coding 场景下必须吃透 Codex Agent LoopVibe 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_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_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。生成之后复制出来不要提交到 git建议放到环境变量里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/apiModel ID 取决于你想用哪个模型来驱动 Agent Loop。Codex 类场景通常需要一个指令遵循强、支持长上下文、对 JSON schema 敏感的模型。你可以在模型对话页面先试一下模型对工具 schema 的响应质量地址是 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。选好之后把 Model ID 记下来比如gpt-5.2-codex这类标识。如果你打算长期跑编码 Agent、频繁做多轮工具调用可以考虑 Coding Plan路径是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。它更适合这种「一轮对话里多次 function_call」的消耗模式比按次调用更划算。前置准备到这里就够了。接下来进入核心部分把 Codex 的请求体结构复现出来并让 Loop 真正转起来。3. 可复制配置复现 Codex 请求体与 Agent Loop这一节是全文重点。我会给出一个最小可运行的 Agent Loop 实现包含请求体组装、工具注册、function_call 解析、结果回流四个环节。你可以直接复制到本地跑。3.1 请求体三段式instructions / input / toolsCodex 的请求体可以抽象成三个字段。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_modedanger-full-access, networkon, approvalnever }, { 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 工具注册表与 handlerCLI 端需要维护一个工具注册表把 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_callname 是exec_commandarguments 是一段 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_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。注意 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_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各端点的字段说明对照着看能省不少时间。6. 从 Loop 到生产Coding Plan 与后续路径把最小 Loop 跑通只是第一步。真正要在项目里用起来你还需要处理几件事上下文压缩、工具权限控制、多轮状态持久化、以及成本控制。上下文压缩方面Codex 的做法是频繁发 commentary 中间进度避免长时间失联同时在 AGENTS.md 里强调「只读必要内容、优先摘要」。你可以模仿这个策略在每轮把旧的 function_call_output 做摘要替换而不是原样累积。工具权限控制方面instructions 里要明确禁止危险操作。比如「非用户明确要求不得执行 git reset --hard 或 git checkout --」。这条约束在自建时同样适用而且要在 handler 层再加一道校验不能只靠模型自觉。成本控制方面多轮 function_call 的 token 消耗比单轮对话高得多。如果你打算长期跑编码 AgentCoding Plan 的路径是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它针对这种高频多轮场景做了优化。模型选择上可以先用模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 对比几个候选模型在工具调用上的稳定性再决定长期用哪个。如果你用的是 Claude Code 这类工具接入方式类似核心还是三件套Base URL 填https://taotoken.net/apiKey 用控制台生成的Model ID 按你的场景选。配置片段和前面给的 JSON 结构一致只是外层封装不同。最后给一个实用技巧在 Loop 里加一个「工具调用轨迹」日志把每轮的 name、arguments、call_id、耗时、exit_code 记到一个 jsonl 文件里。跑一段时间后回看这个文件你能清楚看到模型在什么任务上容易绕圈、哪些工具调用是多余的、哪些 schema 设计导致参数反复出错。这比盯着单次输出调 prompt 有效得多。轨迹数据攒够了再考虑要不要把这套 loop 固化进训练那就是另一条路了。