行业资讯

刚开源的 Codex 到底是怎么“干活“的?把它的 Harness 拆开一看,全是循环

发布时间:2026/8/23 19:40:34
刚开源的 Codex 到底是怎么“干活“的?把它的 Harness 拆开一看,全是循环 先说一个反直觉的事大模型自己根本不会干活。你让它帮我修这个 bug它不会真的去翻代码、跑测试、改文件。它只会做一件事——吐字。一个字一个字往外蹦。那 GitHub 的 Codex 是怎么做到真的去干活的秘密就在一个叫Harness的东西里。今天我就把它拆开给你看。文章目录先泼盆冷水模型只是个嘴一个问题进来先走五层一个问题怎么钻进 Core从 TUI 出发路由开新局还是插话App Server 做翻译官Core 的队列先排队不阻塞Harness 到底在循环什么外层循环RegularTask就管三件事第一次采样前它偷偷干了七件事模型到底看到了什么内层循环ReAct采样→工具→再采样流怎么被处理工具到底怎么执行一圈采样之后看三个标志输出是怎么飞回你屏幕的一个完整例子它到底经历了什么一句话总结附录关键文件速查先泼盆冷水模型只是个嘴很多人以为 AI 编程助手是一个很聪明的大脑自己会动手。真相是模型就是个只会说话的嘴。它连磁盘都碰不到更别说执行命令、改文件了。那干活的到底是谁是一个包在模型外面的循环。这个循环负责把该让模型知道的东西组装好喂给它把模型能用的工具摆到它面前让模型说它想干什么替它真的去执行把执行结果再喂回去循环直到模型说完这个循环在 Codex 的代码里就叫Harness马具、挽具。名字起得挺贴切——马不会自己拉车得套上挽具由人牵着缰绳走。模型不会自己干活得套上 Harness由循环推着它一步步走。一句话Harness 包在模型外面的 agent 循环。它不是单独的组件而是RegularTask→run_turn→run_sampling_request这么一串循环逻辑主要实现在codex-rs/core里。一个问题进来先走五层你敲下一句话它不会直接砸到模型脸上。中间隔了五层像餐厅点菜一样一层层往下传输入层TUI / CLI / App Server 客户端——你在这里打字路由层App Server 的 JSON-RPC决定是开新一轮turn/start还是插话turn/steer调度层Core 的submission_loop按指令分发HarnessRegularTaskrun_turn组上下文、暴露工具、采样、跑工具——核心在这事件层Core 事件 → App Server 通知 → UI 渲染每一层各管一摊解耦得挺干净。这也是为什么你能在 TUI、CLI、App Server 三种界面里用同一个 Codex 内核——换的只是第一层的点菜窗口。一个问题怎么钻进 Core从 TUI 出发你在聊天框里回车这一下会变成一个AppCommand通常是UserTurn然后ChatWidget.submit_op发出一个AppEvent::CodexOp(...)。路由开新局还是插话这里有个很贴心的设计thread_routing.rs如果现在正有一轮回答在跑你新输入的话会被并进去turn_steer——就像你打断别人说话对方接着你的话茬继续。如果空闲就开新一轮turn_start。App Server 做翻译官turn_processor.rs干了几件事校验输入长度、把 v2 的输入项翻译成 Core 认识的UserInput、组装环境/权限/模型覆盖项最后调用CodexThread.start_or_steer_turn(...)。Core 的队列先排队不阻塞CodexThread把请求包成Op::TurnInput丢进 session 的提交通道。submission_loop按Op分发调turn_input::handle。注意这个函数只做路由判断绝不傻等采样。它给你三个答案之一结果含义Started线程空闲创建TurnContextspawn_task(RegularTask)开跑Steered已有活跃 turn把输入追加进去NotSubmitted被拒了turn id 不对、这轮不能插话等Harness 到底在循环什么说白了就是下面这几步反复转组装上下文——模型能看到的instructions、world state、AGENTS.md、skills、历史对话暴露工具——这一步给模型摆出它能调用的工具发起一次流式请求Responses API执行工具调用可能要过审批、进沙箱把工具结果写回 history再采样直到模型结束然后发TurnComplete几个关键函数先混个脸熟函数文件干嘛的RegularTask.runtasks/regular.rs外层 turn 任务run_turnsession/turn.rs内层 agent 循环主角capture_step_context_...session/mod.rs冻结工具/环境/AGENTS.mdbuild_promptsession/turn.rs组装模型请求run_sampling_requestsession/turn.rs一次采样带重试handle_output_item_donestream_events_utils.rs区分文本和工具调用ToolCallRuntime.handle_tool_calltools/parallel.rs真正执行工具build_tool_routertools/spec_plan.rs组装工具表外层循环RegularTask就管三件事RegularTask.run是个很懒的壳只干三件事立刻发TurnStarted让 UI 先渲染起来别让用户干等尽量复用启动时预热好的ModelClientSessionWebSocket / sticky routing省得每次重新握手run_turn返回后如果队列里还排着用户输入就再跑一轮伪代码长这样发 TurnStarted 复用预热好的 ModelClientSession loop: last_agent_message run_turn(...) if input_queue 没有排队输入: return last_agent_message # 否则把排队的用户消息再喂一轮 run_turn第一次采样前它偷偷干了七件事很多人以为采样就是直接把问题丢给模型。其实在第一次真正采样前run_turn先做了一堆铺垫都在session/turn.rs步骤函数目的排空迟到的 hookdrain_async_hook_results把上一轮结束后才完成的 hook 结果入账预压缩run_pre_sampling_compact下次请求可能超窗时先压缩上下文解析 MCP/插件required_mcp_servers_for_input看plugin这类提及决定拉哪些 MCP冻结本步视图capture_step_context...工具、环境、AGENTS.md、能力用同一份快照写 world staterecord_context_updates...让 cwd、权限、模型、环境对模型可见Skills/pluginsbuild_skills_and_plugins注入技能说明和显式启用的 connectorHooks 记录输入run_hooks_and_record_inputshook 能拦截输入通过了才把用户文本写进 history这里有个细节值得注意capture_step_context冻结的是本步的快照。也就是说这一轮里组上下文、暴露工具、派发工具用的都是同一份视图不会出现前面说有一套工具、后面真调用时又变了的错位。工具怎么来的build_tool_router从几个来源拼核心工具shell、apply_patch、多 agent 等、MCP 工具、扩展工具、动态工具。还有个细节Guardian review 这一轮不会暴露 MCP 工具——审核的时候不给它太多武器挺合理。模型到底看到了什么模型拿到的不是原始聊天文本而是一个组装好的PromptPrompt { input // history.for_prompt(...) tools // tool_router.model_visible_specs() parallel_tool_calls // true base_instructions // session 系统提示 output_schema // 可选的结构化输出 }那个input是history.for_prompt(...)合并出来的大杂烩developer / user instructionsworld state工作区、权限、模型、环境AGENTS.md、skills、plugin 说明历史对话本轮用户问题之前的 tool call / tool resultbase_instructions来自Session.get_base_instructions()跟 history 里的 contextual fragments 是分开的。一句话模型看到的是一份被精心拼好的上下文全家桶而不是你打的那句话本身。内层循环ReAct采样→工具→再采样run_turn里有个loop每一圈就是一次采样请求。这是整个 Harness 的心脏这其实就是经典的ReActReason Act循环模型思考——它先说出想法流式吐字调用工具——它提出要调用某个工具结果回灌——Harness 执行完把结果写回 history回到第 1 步——带着结果再让模型想一次流怎么被处理try_run_sampling_request读 SSE 流按事件分门别类模型事件Harness 动作文本/推理增量立刻发AgentMessageContentDelta/ReasoningContentDelta边想边给你看消息/推理完成ItemStarted/ItemCompleted写入 history工具调用完成设needs_follow_up排队一个 tool future流结束但工具没跑完等工具跑完写回结果再采样工具到底怎么执行stream_events_utils.rs里三步走先把 tool call item 写进 history →ToolCallRuntime.handle_tool_call执行 handler → 设needs_follow_up true。tools/parallel.rs里有个挺讲究的并发控制可并行工具比如多个只读查询拿读锁能同时跑互斥工具比如改文件的 shell拿写锁串行执行执行过程中还可能卡在用户审批上exec / patch / permissions。结果变成FunctionCallOutput写回 history下一圈采样就能看到。关键点模型自己永远不碰磁盘。所有副作用都走ToolRouter。这层隔离就是安全感的来源。一圈采样之后看三个标志run_turn会检查三个东西model_needs_follow_up——还有工具没跑完或子 agent 往 mailbox 投了消息has_pending_input——模型跑着时你又打了一句steertoken_limit_reached/ 新 context window——要不要在 turn 中间压缩然后分情况要跟进且快超窗 →run_auto_compact再continue只要跟进 → 带着 history 里的工具结果continue模型说完了 → 跑run_turn_stop_hooks。hook 可以should_block并塞一个 continuation prompt强制再采样否则内层循环 break。之后RegularTask再看一遍 input queue决定要不要再跑一轮。输出是怎么飞回你屏幕的有个体验细节流式文字在采样过程中就发出去了UI 根本不用等TurnComplete。所以你能看到模型边想边打而不是憋半天一次性吐出来。任务收尾时spawn_tasktasks/mod.rs会发TurnComplete带last_agent_message或TurnAborted。事件路径Session.send_event/send_event_raw_with_persistence按策略持久化 rolloutApp Serverconversation.next_event()apply_bespoke_event_handlingServerNotificationTurnStarted、item 增量、TurnCompleted等TUI 渲染一个完整例子它到底经历了什么你问「这个函数为什么 panic」TUI / App Server 把它映射成TurnInput::UserInputCore 返回StartedRegularTask发TurnStarted刷新 AGENTS.md、MCP、工具表冻结进StepContext把 world state、skills、用户问题写进 history把history base_instructions tools发给模型模型先流式输出「我先看代码」再调用shell/ 搜索 / 读文件等工具Harness 执行工具可能先审批把结果写回 history再采样模型写出解释以 delta 流式发出Stop hooks 放行 →TurnCompleteUI 早就把流式文字展示出来了TurnComplete正式收尾一句话总结Codex 的 Harness RegularTask包一层 turnrun_turn反复做「组 Prompt → 流式采样 → 执行工具 / compact / hook → 再采样」直到模型不再需要跟进。模型负责想Harness 负责做。一个只会吐字一个真正动手。两者加起来才是你看到的那个会干活的 AI。附录关键文件速查路径职责tui/src/chatwidget.rs接收用户提交tui/src/app/thread_routing.rsturn/start与turn/steerapp-server/src/request_processors/turn_processor.rsApp Server turn APIcore/src/codex_thread.rs线程侧 submit / start-or-steercore/src/session/handlers.rssubmission_loopcore/src/session/turn_input.rsStart / steer / 拒绝core/src/tasks/regular.rs外层 harness 任务core/src/tasks/mod.rsspawn_task、TurnCompletecore/src/session/turn.rsrun_turn、采样、promptcore/src/session/mod.rsStep context、world state、事件core/src/session/turn_context.rsTurnContextcore/src/stream_events_utils.rs流 item → 文本或工具core/src/tools/spec_plan.rs工具表core/src/tools/parallel.rs并行 / 串行工具运行时app-server/src/bespoke_event_handling.rsCore 事件 → 客户端通知参考代码https://github.com/openai/codex如果你觉得这篇对你有用点个赞、转给身边写代码的朋友。下一篇可以聊聊 Codex 的上下文压缩compact到底是怎么瘦身的——想看的人多就安排。