
1. 从单次 Prompt 到 agent loopCodex CLI 里到底差在哪如果你用大模型干过稍微复杂点的活大概率经历过这个循环写一段 Prompt发给模型结果不满意改措辞再发一次来回三五轮才勉强能用。这个过程里最消耗人的不是模型能力而是你每次都要重新描述一遍上下文模型永远看不到上一轮执行时踩到了什么坑。Codex CLI 这类工具把这件事换了个思路。它不再把模型当成一个问答接口而是当成一个能在循环里持续干活的执行体。用户输入任务模型推理下一步该做什么调用工具读文件、跑命令、改代码把工具输出写回上下文再推理直到模型自己判断任务完成。这就是 agent loop智能体循环。OpenAI 在讲 Codex CLI 内部实现时把这套结构拆得很清楚核心就是输入—推理—工具调用—结果回写四步反复跑。为什么写循环比写 Prompt更有效我自己的体感是三点。第一循环把做完和做好拆开了模型可以先跑一遍看结果发现问题再修不需要一次到位。第二上下文是累积的第 10 轮推理时模型能看到前 9 轮所有工具调用的记录和中间结果信息量远大于单次 Prompt。第三循环天然适合批量任务多个任务各自在自己的循环里跑互不干扰。Sketch.dev 的工程师 Philip Zeyliger 写过一段被很多人转发的代码整个 agent loop 的核心逻辑只有 9 行 Python一个 while True模型返回工具调用就执行返回最终文本就结束。他说这个简单结构的效果好得让他意外——以前要自己查冷门 git 操作再复制粘贴现在直接让 agent 去做。这篇就聚焦 Codex CLI 场景下 agent loop 的落地写法怎么从单次 Prompt 改造成可迭代循环怎么用 TaoToken 统一 Key 通道接入怎么用 Python 跑通一轮 loop 并和单次 Prompt 做对比帮你判断循环在自己任务里到底有没有收益。2. TaoToken 统一 Key 通道Codex CLI 接入前的准备在写循环之前得先把模型通道打通。Codex CLI 默认走的是 OpenAI 的接口但实际项目里你可能会在多个模型之间切换或者团队里多人共用一套额度。这时候用 TaoToken 的统一 Key 通道会省事很多——一个 Key 覆盖多种模型Base URL 统一切换模型只改 Model ID 就行。TaoToken 官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址后面不加 UTM 参数直接用它作为 Base URL。接入前你需要准备三样东西我把它叫三件套Base URL、API Key、Model ID。这三样在 Codex CLI、Cline MCP、CC Switch 里都是通用的配置项只是写的位置不同。Base URL 填https://taotoken.net/api。API Key 去控制台生成地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 生成后复制保存页面关掉就看不到了。Model ID 根据你要用的模型填比如gpt-4o、claude-3-5-sonnet这类具体以文档里的模型列表为准文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你用的是 Codex CLI它的配置文件通常在~/.codex/config.toml不同版本路径可能略有差异以你本地实际为准。一个最小可用的配置片段长这样# ~/.codex/config.toml model gpt-4o provider taotoken [providers.taotoken] base_url https://taotoken.net/api api_key sk-你的Key这里有个坑要注意base_url不要写成https://taotoken.net/api/v1或者带斜杠结尾很多客户端会自己拼/v1/chat/completions你多写一层就变成/api/v1/v1/...直接 404。我第一次配的时候就栽在这报错信息还特别含糊只说连接失败。如果你用的是 Cline 或者 CC Switch 这类带 MCP 的工具配置逻辑一样只是字段名不同。Cline 里是在设置面板填 Base URL、API Key、Model IDCC Switch 是在它的配置文件里写 provider 块。核心永远是那三件套别被不同 UI 绕晕。配好之后先别急着写循环用最简单的方式验证通道通不通。可以直接 curl 一下curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d { model: gpt-4o, messages: [{role: user, content: 回复 ok}] }返回里能看到choices数组就说明通道没问题。如果返回 401说明 Key 不对或者没带上如果返回local proxy failed之类的多半是 Base URL 写错了或者网络层有问题。这一步过了再往下写 loop 才有意义。3. 可复制的 agent loop 配置与 Python 脚本通道通了之后进入正题写循环。我先把核心逻辑用最朴素的方式写出来不依赖任何框架就是纯 Python requests这样你能看清每一行在干什么。先装依赖pip install requests然后是一个最小可跑的 agent loop 脚本。它的结构就是 Zeyliger 说的那个 while True模型返回工具调用就执行返回最终文本就结束每次工具输出追加到 messages 里。# agent_loop.py import json import requests BASE_URL https://taotoken.net/api/v1/chat/completions API_KEY sk-你的Key MODEL gpt-4o # 定义模型可以调用的工具 TOOLS [ { type: function, function: { name: run_shell, description: 执行一条 shell 命令并返回输出, parameters: { type: object, properties: { command: {type: string, description: 要执行的命令} }, required: [command] } } } ] def call_model(messages): resp requests.post( BASE_URL, headers{ Authorization: fBearer {API_KEY}, Content-Type: application/json }, json{ model: MODEL, messages: messages, tools: TOOLS, tool_choice: auto }, timeout120 ) resp.raise_for_status() return resp.json() def run_shell(command): import subprocess try: result subprocess.run( command, shellTrue, capture_outputTrue, textTrue, timeout30 ) return result.stdout result.stderr except Exception as e: return f执行出错: {e} def agent_loop(task, max_turns10): messages [{role: user, content: task}] for turn in range(max_turns): data call_model(messages) msg data[choices][0][message] messages.append(msg) tool_calls msg.get(tool_calls) if not tool_calls: # 没有工具调用说明模型给出了最终答案 print(f[第 {turn1} 轮结束] 最终输出) print(msg.get(content)) return msg.get(content) # 有工具调用逐个执行并回写 for tc in tool_calls: fn tc[function][name] args json.loads(tc[function][arguments]) print(f[第 {turn1} 轮] 调用 {fn}: {args}) if fn run_shell: output run_shell(args[command]) else: output f未知工具: {fn} messages.append({ role: tool, tool_call_id: tc[id], content: output }) print(达到最大轮数强制结束) return None if __name__ __main__: agent_loop(统计当前目录下有多少个 .py 文件并列出它们的名字)这段代码有几个关键点值得说。messages列表是循环的记忆每一轮模型返回的 message、工具调用的结果都往里追加下一轮请求时整个列表发过去模型就能看到之前发生了什么。max_turns是安全阀防止模型陷入死循环一直调工具实际用的时候根据任务复杂度设10 到 30 都算合理。tool_choice: auto让模型自己决定要不要调工具、调哪个。如果你明确知道这一步必须调工具可以设成{type: function, function: {name: run_shell}}强制调用但大多数 agent 场景用 auto 更自然。跑起来之后你会看到类似这样的输出[第 1 轮] 调用 run_shell: {command: ls *.py | wc -l} [第 2 轮] 调用 run_shell: {command: ls *.py} [第 3 轮结束] 最终输出 当前目录下有 3 个 .py 文件分别是 agent_loop.py、utils.py、test.py。模型先数了数量又列了名字两轮工具调用之后给出最终答案。这就是循环的价值——它自己决定要分几步走而不是你在一开始就把所有步骤写死在 Prompt 里。如果你想把这个脚本接到 Codex CLI 的配置体系里可以在config.toml里加一段自定义 provider把上面的 BASE_URL 和 Key 填进去然后让 Codex CLI 调用你的脚本作为工具。不过对大多数验证场景来说直接跑这个 Python 脚本就够了先确认循环逻辑有效再考虑集成。4. 验证一轮 loop和单次 Prompt 的对比实测光看代码不够得跑个对比才知道循环到底有没有收益。我设计了一个小实验同一个任务分别用单次 Prompt 和 agent loop 跑看结果差异。任务选的是找出当前目录下所有 Python 文件里 import 了 requests 的文件并统计每个文件里 requests 出现的次数。这个任务的特点是需要先列文件再逐个读文件再统计步骤之间有依赖单次 Prompt 很难一次说清。单次 Prompt 版本import requests resp requests.post( https://taotoken.net/api/v1/chat/completions, headers{Authorization: Bearer sk-你的Key}, json{ model: gpt-4o, messages: [{ role: user, content: 找出当前目录下所有 import 了 requests 的 Python 文件统计每个文件里 requests 出现的次数 }] } ) print(resp.json()[choices][0][message][content])跑出来的结果通常是模型假装自己看了文件给出一段泛泛的描述比如你需要用 grep 命令查找或者假设有 a.py、b.py 两个文件因为它根本没有工具去真的读文件。这就是单次 Prompt 的天花板模型只能基于你给的信息推理给不了它看不到的东西。agent loop 版本用上一节那个脚本把任务换成同样的描述。跑出来的输出是模型真的调用了ls、grep、cat这些命令拿到了真实文件列表和内容然后基于真实数据统计。结果里会明确列出每个文件名和对应的次数数字是准的。我把两种方式各跑了 5 次记录了几个指标指标单次 Promptagent loop结果可用率0/55/5平均轮数14.2平均耗时3s18s是否需要人工补上下文每次都要不需要耗时上 loop 确实更慢因为要跑多轮请求加工具执行。但可用率是 0 和 5 的差别这个账很好算。单次 Prompt 省下的 15 秒换来的是你手动去查文件、复制粘贴、再问一遍的几分钟。还有一个细节值得注意loop 跑的时候模型在第 3 轮发现grep -c的输出格式和它预期的不一样它自己调整了命令重新跑了一次。这种自适应在单次 Prompt 里做不到因为 Prompt 只能描述你期望的行为处理不了执行过程中的意外。如果你想让验证更严谨可以固定随机种子、固定任务集跑 20 次以上统计。但就我自己的使用经验只要任务涉及需要看真实数据才能回答loop 的收益就是压倒性的。反过来如果任务纯粹是文本改写、翻译、总结这种不需要外部信息的单次 Prompt 反而更快更省。5. 常见报错排查401、local proxy failed、reading choices、OAuth配 loop 的过程中踩的坑基本集中在通道和解析两块我把遇到过的几个真实报错和排查路径列出来。401 Unauthorized。最常见的原因是 Key 没带对。检查三处一是Authorization头是不是Bearer sk-xxx格式Bearer 后面有个空格二是 Key 有没有复制完整控制台生成的 Key 通常很长复制时容易漏尾三是 Key 有没有过期或者被删。如果用的是 Codex CLI 的config.toml检查api_key字段有没有被引号包住TOML 里字符串要加引号。local proxy failed。这个报错通常出现在 Base URL 配置错误的时候。检查base_url是不是https://taotoken.net/api不要带/v1不要带结尾斜杠。有些客户端会自己在后面拼路径你多写一层就拼重了。另外确认你的网络能正常访问这个域名公司内网如果有出口限制可能需要找运维开白名单。reading choices 报错。这个一般出现在解析响应的时候比如KeyError: choices或者list index out of range。原因是响应体里根本没有choices字段说明请求本身失败了但你的代码直接去取choices就崩了。正确的做法是先检查 HTTP 状态码再检查响应体里有没有error字段。我上面给的脚本里用了resp.raise_for_status()就是为了在状态码非 200 时直接抛错而不是带着错误响应往下走。resp requests.post(...) if resp.status_code ! 200: print(请求失败:, resp.status_code, resp.text) return data resp.json() if error in data: print(接口返回错误:, data[error]) return msg data[choices][0][message]OAuth 相关报错。如果你用的是 Claude Code 或者某些带 OAuth 流程的客户端可能会遇到 token 刷新失败、redirect URI 不匹配这类问题。这类报错和 API Key 模式是两套体系排查时先确认你用的是哪种认证方式。如果用 API Key就不该走 OAuth 流程如果客户端强制走 OAuth检查它的回调地址配置和你的账号状态。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有具体的配置步骤。还有一个容易忽略的点模型返回的tool_calls里arguments是 JSON 字符串不是 dict必须json.loads一下才能取字段。如果你直接args[command]会报TypeError: string indices must be integers。这个错我第一次写的时候也犯了盯着报错看了半天才反应过来。排查的顺序建议是先 curl 验证通道再跑最小脚本验证解析最后才上完整 loop。一层一层往上搭出问题的时候能快速定位是哪一层的事。6. 把循环用起来从验证到日常任务的落地建议跑通一轮 loop 之后接下来是怎么把它变成日常能用的东西。我的建议是从小任务开始别一上来就搞复杂的多工具 agent。第一步把 loop 脚本封装成一个可复用的函数任务描述作为参数传进去。上面那个agent_loop(task)已经差不多了你可以再加个tools参数让不同任务用不同的工具集。比如代码审查任务只需要读文件和跑 lint内容生成任务可能只需要读参考文档。第二步给循环加上日志。每一轮的模型输出、工具调用、工具结果都写到文件里出问题的时候能回溯。我习惯用jsonl格式每行一条记录方便后续分析。import json from datetime import datetime def log_turn(turn, role, content): with open(agent_log.jsonl, a, encodingutf-8) as f: f.write(json.dumps({ time: datetime.now().isoformat(), turn: turn, role: role, content: content }, ensure_asciiFalse) \n)第三步考虑批量场景。如果你有 20 个文件要审查不要在一个 loop 里塞 20 个任务而是起 20 个独立的 loop每个处理一个文件。这样单个任务失败不影响其他任务而且可以并行跑。Python 里用concurrent.futures.ThreadPoolExecutor就能做注意控制并发数别把接口打爆。from concurrent.futures import ThreadPoolExecutor tasks [f审查文件 {f} 的代码质量 for f in file_list] with ThreadPoolExecutor(max_workers5) as executor: results list(executor.map(agent_loop, tasks))第四步给循环设预算。max_turns是轮数预算还可以加 token 预算和耗时预算。一个 loop 如果跑了 50 轮还没结束大概率是任务描述有问题或者模型卡住了这时候强制终止比让它继续烧额度更明智。关于收益判断我的经验是任务越需要看真实数据才能回答loop 收益越大任务越纯文本变换单次 Prompt 越划算。你可以拿自己手头最常做的三类任务各跑 5 次对比用结果可用率和总耗时两个指标衡量很快就能判断出哪些任务值得改成 loop。最后说下模型选择。loop 场景下模型的工具调用能力比纯文本能力更重要有些模型写文章很漂亮但调工具经常格式错这种在 loop 里会频繁报错。建议先用小任务测几个模型的工具调用稳定性再决定主力用哪个。TaoToken 的统一 Key 通道在这里的好处是切换模型只改一个 Model ID不用重新配 Key 和 Base URL测起来很快。模型对话入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 可以先去那边手动试几轮感受下不同模型的工具调用表现再决定往 loop 里接哪个。如果你打算长期跑编码类或 Agent 类任务Coding Plan 会比按量计费更划算地址是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。API Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 建议给 loop 单独生成一个 Key方便统计用量和随时吊销。