Codex源码导读:Turn与Request的完整生命周期解析

发布时间:2026/10/31 9:04:04
Codex源码导读:Turn与Request的完整生命周期解析 把 Codex 装好、跑起来之后大多数人会停在“挺好用”这个阶段直到某个瞬间你开始好奇我敲下回车的那一刻程序里到底发生了什么我读 Codex 源码的起因就很朴素——有一次它回复得特别慢中间还夹杂着好几轮“正在运行命令”的提示我盯着终端想不明白这十几秒里代码是在等网络还是在本地做别的事带着这个疑问翻了源码才意识到一个关键事实你在 Codex 里看到的“一次对话往返”到了内部并不是一次简单的 API 调用。它被称为一个 Turn而这个 Turn 底下可能跑了好几轮 Request。搞清楚 Turn 的完整生命周期你就能解释很多表面现象为什么有时候响应被工具调用打断、为什么上下文长了之后早期的对话会被压缩、为什么会话文件里躺着一条条你没说过的话。这篇文章是 Codex 源码导读的第二部分。上一篇已经梳理过整体模块骨架这一篇我们顺着“用户发消息 → 模型返回 → 工具执行 → 再请求 → 最终回复”这条主线把一次 Turn 从出生到结束的每一步都过一遍。我会尽量用源码里的真实逻辑来讲但不贴大段原文毕竟不同版本代码会有出入主干逻辑才是我们真正要吃的。1. 先明确“Turn”到底指什么一次交互不是一次 API 调用1.1 从 TUI 里敲下回车的那一刻说起先还原一下你平时操作的场景。在终端里运行codex进入交互界面输入一句话回车。界面上开始流式地蹦出文字有时候中间会跳出类似[tool use]的提示紧接着能看到 bash 命令的执行输出然后模型继续说话直到它给出完整回复。如果只从“用户视角”看这整个过程就是一次聊天。但在源码里这一整段交互有一个明确的边界这个边界就是从用户输入到模型给出最终答复之间的完整过程它被叫作 Turn。注意这不是我随口起的名字在 Codex 的项目结构和内部日志里Turn 是一个真实存在的概念单元很多状态、文件命名、消息记录都围绕它展开。我读源码时最大的一个认知转变就是别再以为“你发一条消息 发一次 API 请求”。工具调用会把一次 Turn 撕成好几段 API 请求而用户完全感知不到这个切分只看得到“模型中途去跑了个命令”这种诡异现象。1.2 Turn 与 Request搞清楚这对概念源码就懂了一半源码导读类文章最容易犯的毛病就是概念名词满天飞却不先讲清楚层级关系。所以我先把 Turn 和 Request 做一个硬性区分后面所有章节都建立在这对概念上Request一次 HTTP 级别的模型 API 调用把当前的完整消息列表打包发给模型服务端拿到一段流式返回。这是最小单位。Turn从用户输入开始到模型产出最终答复为止的完整回合。一个 Turn 可以只包含一次 Request最简单的情况也可能因为工具调用而包含多次 Request。它们的关系用一句话概括Turn 是产品层的交互往返Request 是模型层的单次调用。工具调用是拆散它们的元凶——模型在一次 Request 里没把话说死而是返回“我要调用某工具”的意图Agent 就必须执行工具、把结果追加进消息列表、再发起一个新的 Request直到模型明确表示“我答完了”为止。我在读codex/agent.py的对话循环时印象最深的就是这个 while 结构外层是 Turn 的推进内层是 Request 的循环两者靠“模型这次返回了什么类型的内容”来衔接。源码里面对消息结束状态有几种区分比如模型回复了最终文本、模型还在请求工具、模型因为上下文截断而被迫停止等。每种状态对应不同的下一步动作。1.3 从状态机看一次 Turn 的骨架其实一次 Turn 的推进可以抽象成一条状态链。我在源码里总结出来的大致流程是这样的接收用户输入把它转成一条 user 消息追加到会话历史。带着完整历史向模型发起 Request。模型返回流式响应其中可能夹杂文本片段、也可能夹杂工具调用请求。如果请求工具就进入工具执行流程执行完把结果作为新消息追加回到第 2 步。如果不再请求工具说明模型已经给出最终答复Turn 结束。为了让你更直观地对比我把 Turn 和 Request 的差异整理成一张表维度TurnRequest触发条件用户输入一条消息Agent 需要向模型要一次回复是否包含工具执行可能包含多轮工具执行只负责“问”和“收”结束标志模型给出最终答复流式响应关闭在代码里的体现Agent 的外层循环一次流式请求的生命周期会话记录中如何体现从 user 消息到最终 assistant 消息的片段中间不可见的 API 往返理解这张表之后再去看源码里那一堆看似绕来绕去的循环就会觉得顺理成章。接下来我们按顺序拆解先看一次 Request 发起之前消息是怎么被“打包”的。2. 请求组装阶段一条用户消息如何变成发往模型的 Prompt2.1 配置与 Provider 解析是请求的“前置关卡”在 turn 真正开始之前Codex 要先完成一系列初始化这些工作决定了一次 Request 会发给谁、用什么模型、走什么协议。源码里的入口集中在codex/main.py和codex/cli.py它们负责把命令行参数和~/.codex/config.toml里的配置揉到一起生成一个全局配置对象。我重点说一下 Provider 的解析逻辑。Codex 不是只连官方接口它的设计里放了一个“模型提供方”的抽象层。你在配置里指定的model_provider会决定这次请求走哪套协议。源码里有两种 wire 协议一种走/responses端点一种走传统的/chat/completions端点。这两种协议在请求格式和流式事件的解析上差异很大但 Codex 把它们统一成了内部的请求/响应结构上层 Agent 完全不用关心细节。这里我踩过一个很实际的坑自定义 provider 时wire_api字段一旦写错代码会跑到完全不同的解析分支里报的错还特别隐晦——表面上是在说 endpoint 处理失败实际上是协议不匹配。排查这种问题第一步永远是看配置里wire_api的值是不是和对方服务端兼容。2.2 系统提示词Codex 的“职业素养”是拼出来的请求组装最关键的一步是构造消息列表。你输入的那句话只是其中一条 user 消息排在最前面的往往是一大段系统提示词还有可能跟着一堆工具定义、环境说明、仓库映射信息。源码里这些内容不是写死的单一字符串而是分模块拼接出来的。以我的阅读经验看系统提示词里至少包含这几块基础行为规范告诉模型它是 Codex、它应该怎么使用工具、回答时的格式偏好。环境信息操作系统类型、当前工作目录、shell 类型。代码仓库映射信息如果当前目录是 git 仓库会生成一个仓库结构的概要让模型知道有哪些文件可以动。工具使用说明每个工具的参数格式、使用场景、注意事项。这些内容加在一起往往比你真正的问题还要长。为什么这么做原因很简单——模型本身不具备对“当前环境”的感知它只能从消息文本里推断。Codex 把环境信息前置到系统提示词里相当于给模型戴了一副眼镜让它知道自己在哪台机器上、面对着什么代码库。读源码时看到这里我才理解为什么 Codex 在仓库场景下表现比裸调 API 稳定得多——不是模型变了是喂给模型的上下文变了。2.3 模型名校验背后藏着“配置即代码”的设计主张在组装请求的过程中有一个容易被忽略的校验步骤模型名检查。源码里会对当前选定的模型做一层验证如果模型名不在已知列表里就会直接报错而不是把错误请求发出去。很多人在自定义配置时被这个校验卡过我一开始也觉得它多管闲事——我用第三方兼容服务为什么非要认官方的模型名后来看了源码里的设计意图才明白这个校验是为了尽早失败。如果模型名拼错了直接发出去回来的是一个模糊的网络错误定位成本高得多而本地校验能在一秒内告诉你“这个名字有问题”。它本质上是把配置错误拦截在请求发出之前。所以如果你确实在用第三方模型正确做法不是绕过校验而是在 provider 配置里把模型名声明清楚让校验逻辑承认这个名字。3. Agent 主循环流式响应如何一步步改变 Agent 状态3.1 流式响应的增量处理逻辑请求发出之后就进入 Agent 主循环最核心的部分处理流式响应。Codex 的 API 请求基本都开了流式模式也就是说服务端不是一次性吐完所有内容而是按数据块chunk持续推送。源码里有一个流式响应的封装层它把底层不同的协议事件统一成一种内部事件流上层代码逐一处理这些事件。我读这段代码时的感受是流式处理最麻烦的点不在“接收”而在“增量状态维护”。模型输出的文本是一点一点到的但 Agent 必须随时知道“到现在为止模型想表达什么”。所以源码里维护了一个响应累积器每个 chunk 到达时做三件事把新增的文本片段追加到当前累积的回复内容里如果发现工具调用的数据块就开始累积工具调用的参数更新 Agent 的运行状态驱动界面层的显示。这里有一个很微妙的点工具调用的参数往往是跨多个 chunk 传输的可能在第一个 chunk 里出现工具名第二个 chunk 里才开始传参数 JSON。源码里要对这种“半成品”状态做容错不能因为参数还没到齐就报错。我在调试自己的集成时曾遇到过界面卡住不动的情况后来发现就是因为在流式处理时没有正确等待工具参数传输完毕。3.2 工具请求是主循环的“断点”流式响应会持续到模型给出一个明确的段落结束事件。此时 Agent 要做一个关键判断这次返回的内容到底属于哪种类型源码里的分叉逻辑大致是如果是普通文本并且没有任何工具调用说明模型打算直接回答用户Turn 走向结束。如果包含工具调用Agent 就不能继续当“传话筒”了它要暂停“向模型请求回复”的节奏转去执行工具。这个断点效应是整个 Turn 生命周期里最重要的转折之一。它解释了你在界面上看到的现象模型的话说到一半不说了冒出来一条工具执行提示等命令跑完了模型才接着“话头”继续说。其实模型本身没有记忆中断是 Agent 在中间插入了一次工具执行并把结果作为一条新消息喂给了模型模型基于结果继续生成。从源码的角度看这个断点是通过事件驱动实现的。流式响应处理完毕后Agent 检查累积结果里是否包含工具调用块如果有就进入工具调度流程如果没有就把累积的文本作为最终回复。整个循环的出口条件很简单但内部的状态管理比想象中复杂尤其是要处理“工具调了一半但参数非法”这一类异常分支。3.3 上下文长度检查是每个回合的暗哨在每轮 Request 发起之前还有一个容易被忽略的检查——上下文长度。源码里有一套 token 计数机制会在请求发出前估算当前消息列表占用的 token 数和模型的上下文窗口做对比。如果超出安全阈值就先做历史压缩再发请求。我第一次看这段代码时很疑惑为什么不在请求失败后再处理后来想明白了模型服务端的上下文限制是硬性的超了直接报错整个 Turn 就断了。前置检查虽然多花一点计算但能避免更昂贵的失败。源码里对超长历史的处理策略通常是把早期对话压缩成摘要丢掉细枝末节保住最近的几轮完整消息。这也解释了为什么长会话聊到后面模型对早期内容的记忆会变得模糊——不是模型“变笨了”是 Agent 主动把早期细节丢掉了换取了更长的有效生命周期。4. 工具调用的完整闭环从 ToolUse 到 ToolCallResult4.1 工具解析、校验与语义检查当主循环检测到工具调用意图时第一步是把流式累积出来的工具调用数据解析成结构化的 ToolUse 对象。这个对象包含工具 ID、工具名称、参数字典。源码里对这个解析过程异常严格尤其是参数部分——因为模型可能生成格式不完整的 JSON必须做容错处理。解析完之后还有一道校验关卡。Codex 内置了一些语义校验器比如仓库映射工具会对文件路径做合法性检查防止模型引用不存在的文件bash 工具会对命令做一些基础的安全检查。我在读代码时专门留意过这个设计它把“模型说什么就执行什么”的裸工具调用变成了“先验证再执行”的安全模式。虽然校验不完美但确实把一部分低级错误拦截在了执行之前。这里我见过最典型的失败案例是模型请求读取一个路径但路径里混入了相对路径和绝对路径的拼接校验器检测到路径不存在直接拒绝执行并返回错误信息。而后端模型拿到这个错误信息后通常会自动修正路径再试一次。所以你有时候会看到模型“自己纠错”——其实是工具校验反馈在起作用。4.2 权限模型与用户确认工具通过校验之后并不是立刻执行。Codex 有一套权限模型决定工具调用是否需要经过用户确认。这套逻辑在源码里做成了一条决策链先看配置里的权限模式再看工具本身的类型最后看命令的危险程度。我印象很深的一点是bail 类工具比如删除文件、批量改文件和只读类工具的默认待遇完全不同。只读命令在允许自动执行的模式下可以静默运行而高危操作即使开了自动模式也会停下来问一句。这套分级逻辑在源码里不是硬编码的 if-else而是有一张规则表每种工具声明自己的风险等级Agent 根据等级决定要不要打断用户。从用户视角看这就是界面上那几次“确认要执行吗”的来源。很多人觉得这个确认多余但读源码后你会发现没有这套确认机制一次模型幻觉可能就导致一整个目录被误删。确认不是流程仪式它是权限边界在交互层的体现。4.3 结果回填与下一轮 Request工具执行完成后Agent 把执行结果封装成 ToolCallResult包含退出码、标准输出、标准错误、执行时长等信息。然后它把这条结果转成一条带角色标识的消息追加到消息列表里再发起下一轮 Request。这里就是理解“为什么模型能记得工具执行结果”的关键模型本身没有记忆是 Agent 把结果写进了上下文。源码里这个过程发生在对话循环的尾部追加消息之后循环条件判断为“还需要继续”于是带着完整历史重新走一遍请求流程。模型看到工具结果后要么继续调用工具要么整理成最终答复。我建议你实际观察一次多轮工具调用的过程把会话文件打开会看到非常清晰的模式user 消息、assistant 工具请求、tool 执行结果、assistant 继续请求、tool 又一次执行结果……最后以一条纯文本的 assistant 消息收尾。整个 Turn 的生命周期就在这个交替序列里完整展开了。5. 一次 Turn 结束后留下什么会话持久化与调试手段5.1 JSONL 会话历史的结构每一次 Turn 执行的痕迹最终都会落到一个 JSONL 文件里。这是我在整个源码阅读过程中收获最大的一个发现。很多人只知道 Codex 会把会话存下来却没意识到这个文件本身就是调试利器。会话文件的位置通常在~/.codex/sessions/下的按时间命名的目录里文件名带一个会话 ID。每一行是一个 JSON 对象包含时间戳、UUID、类型、载荷四类信息。我在读文件时看到的主要类型有用户消息、助手消息、Agent 状态变更、系统日志等。这个设计的妙处在于它把“执行过程”和“上下文记录”统一了。你不仅能看到聊了什么还能看到 Agent 在什么时候进入了什么状态、什么时候开始执行工具、执行结果是什么。对源码阅读者来说这相当于一份现成的运行时日志。5.2--dump与 debug 日志的正确打开方式说到调试Codex 提供了两个我强烈建议你用的入口。第一个是--dump参数它会把你当前生效的配置、模型信息、Provider 设置全部打印出来。每次配置改乱了先跑一遍--dump看实际生效值比翻配置文件高效得多。第二个是 debug 日志。命令行里可以指定日志级别和日志文件路径把运行过程中的内部细节全部记录下来。我在定位自定义 provider 的问题时就是靠日志里的请求日志和响应状态码一步步缩小范围的。读日志的时候注意看几类关键词请求发出前的配置信息、流式响应的事件类型、工具调用的执行命令与退出码。这里有一个我在实践中养成的习惯复现问题前先清空旧日志加一行时间标记然后完整跑一次 Turn再回头慢慢翻日志。这样整个 Turn 的生命周期就从日志里完整浮现出来了。5.3 用会话文件复盘一次完整的 Turn我强烈建议你亲自动手做一次复盘开启一个交互会话问一个会触发工具调用的问题等 Turn 结束后打开对应的 JSONL 文件按行数一遍。你会看到类似这样的结构字段细节以你的版本为准{type: user_message, payload: {role: user, content: 帮我统计这个目录下的代码行数}} {type: agent_state, payload: {state: working}} {type: assistant_message, payload: {role: assistant, content: , tool_calls: [...]}} {type: agent_state, payload: {state: awaiting_tool}} {type: tool_execution, payload: {cmd: find . -name *.py | xargs wc -l}} {type: tool_result, payload: {exit_code: 0, stdout: ...}} {type: assistant_message, payload: {role: assistant, content: 统计完成总共...}} {type: agent_state, payload: {state: finished}}这一串记录就是一次 Turn 生命周期的完整影像。对照着看源码里的状态机逻辑很多抽象的概念瞬间就有了落脚点。6. 源码阅读过程中最容易踩的三个配置坑6.1model not supported报错到底在说什么很多人在配置里写了一个模型名然后报错说这个模型不被支持。我见过最离谱的一次是把自定义 provider 的name字段和model字段搞混了——验证逻辑拿到的是 provider 的 name而不是要用的模型名当然不匹配。源码里的校验逻辑其实很简单它维护一个已知模型的集合请求发起前拿当前配置解析出的模型名去集合里查。查不到就报错。所以排查方向只有两个要么模型确实不在支持列表里要么配置解析出来的模型名和你以为的不一致。先跑--dump看实际生效的模型名再决定是改配置还是换模型基本一步到位。6.2unrecognized configuration setting的排查路径这个警告我一开始也遇到过。配置写完了跑起来发现日志里写着“有 1 个配置项无法识别”。代码会忽略这个未知项继续运行但你的本意设置的功能就没有生效。源码里的配置解析逻辑是严格的白名单模式读到配置 TOML 后把每个字段和已知字段集合做对比不在名单里的就记一条警告。出现这种情况绝大多数原因是字段名拼错了或者用了旧版本的字段名或者是把某个应该在 provider 内部的字段写到了顶层。我的排查路径很固定先跑--dump看输出里缺了哪个配置块然后去源码的配置类定义里查字段名拼写对照着改。用上这个流程之后这类问题基本五分钟内解决。6.3 自定义 provider 接入第三方模型的正确姿势最后聊一个很多人问到的场景把 Codex 接到第三方模型服务上。我在配置里见过最省心的写法是这样的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注意几个关键点wire_api要和对方服务端兼容的协议保持一致env_key指定的环境变量就是你的密钥base_url不要带多余的路径后缀对方兼容 OpenAI 的话一般到/v1就够。配置完先跑codex --dump确认解析结果再发一条最简单的消息验证连通性。如果网络层的故障日志里会有很明确的状态码顺着那个方向查远比瞎猜配置要快。读源码读到这一步你会发现自己已经不只是在“用”Codex而是能预判它的行为、能读它的日志、能在它出错时很快定位到具体环节。这就是源码导读最大的回报——工具在你眼中从黑盒变成了透明的机器。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询