在这里插入图片描述

先说一个反直觉的事:大模型自己根本不会干活。

你让它"帮我修这个 bug",它不会真的去翻代码、跑测试、改文件。它只会做一件事——吐字。一个字一个字往外蹦。

那 GitHub 的 Codex 是怎么做到"真的去干活"的?秘密就在一个叫 Harness 的东西里。今天我就把它拆开给你看。


先泼盆冷水:模型只是个"嘴"

很多人以为 AI 编程助手是"一个很聪明的大脑,自己会动手"。

真相是:模型就是个只会说话的嘴。它连磁盘都碰不到,更别说执行命令、改文件了。

那"干活"的到底是谁?

是一个包在模型外面的循环。这个循环负责:

  1. 把该让模型知道的东西,组装好喂给它
  2. 把模型能用的工具摆到它面前
  3. 让模型"说"它想干什么
  4. 替它真的去执行
  5. 把执行结果再喂回去
  6. 循环,直到模型说"完"

这个循环,在 Codex 的代码里就叫 Harness(马具、挽具)。

名字起得挺贴切——马不会自己拉车,得套上挽具,由人牵着缰绳走。模型不会自己干活,得套上 Harness,由循环推着它一步步走。

一句话:Harness = 包在模型外面的 agent 循环。

它不是单独的组件,而是 RegularTaskrun_turnrun_sampling_request 这么一串循环逻辑,主要实现在 codex-rs/core 里。


一个问题进来,先走五层

你敲下一句话,它不会直接砸到模型脸上。中间隔了五层,像餐厅点菜一样,一层层往下传:

在这里插入图片描述

  1. 输入层:TUI / CLI / App Server 客户端——你在这里打字
  2. 路由层:App Server 的 JSON-RPC,决定是开新一轮(turn/start)还是插话(turn/steer
  3. 调度层:Core 的 submission_loop,按指令分发
  4. HarnessRegularTask + run_turn,组上下文、暴露工具、采样、跑工具——核心在这
  5. 事件层: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_loopOp 分发,调 turn_input::handle

注意,这个函数只做路由判断,绝不傻等采样。它给你三个答案之一:

结果含义
Started线程空闲,创建 TurnContextspawn_task(RegularTask) 开跑
Steered已有活跃 turn,把输入追加进去
NotSubmitted被拒了(turn id 不对、这轮不能插话等)

Harness 到底在循环什么

在这里插入图片描述

说白了就是下面这几步,反复转:

  1. 组装上下文——模型能看到的:instructions、world state、AGENTS.md、skills、历史对话
  2. 暴露工具——这一步给模型摆出它能调用的工具
  3. 发起一次流式请求(Responses API)
  4. 执行工具调用(可能要过审批、进沙箱)
  5. 把工具结果写回 history
  6. 再采样,直到模型结束,然后发 TurnComplete

几个关键函数,先混个脸熟:

函数文件干嘛的
RegularTask.runtasks/regular.rs外层 turn 任务
run_turnsession/turn.rs内层 agent 循环(主角)
capture_step_context_...session/mod.rs冻结工具/环境/AGENTS.md
build_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 是个很"懒"的壳,只干三件事:

  1. 立刻发 TurnStarted,让 UI 先渲染起来,别让用户干等
  2. 尽量复用启动时预热好的 ModelClientSession(WebSocket / sticky routing,省得每次重新握手)
  3. 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注入技能说明和显式启用的 connector
Hooks + 记录输入run_hooks_and_record_inputshook 能拦截输入;通过了才把用户文本写进 history

这里有个细节值得注意:capture_step_context 冻结的是"本步"的快照。也就是说,这一轮里组上下文、暴露工具、派发工具,用的都是同一份视图,不会出现"前面说有一套工具、后面真调用时又变了"的错位。

工具怎么来的?build_tool_router 从几个来源拼:核心工具(shell、apply_patch、多 agent 等)、MCP 工具、扩展工具、动态工具。还有个细节:Guardian review 这一轮不会暴露 MCP 工具——审核的时候不给它太多武器,挺合理。


模型到底"看到"了什么

模型拿到的不是原始聊天文本,而是一个组装好的 Prompt

Prompt {
    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 instructions
  • world state(工作区、权限、模型、环境)
  • AGENTS.md、skills、plugin 说明
  • 历史对话
  • 本轮用户问题
  • 之前的 tool call / tool result

base_instructions 来自 Session.get_base_instructions(),跟 history 里的 contextual fragments 是分开的。

一句话:模型看到的,是一份被精心拼好的"上下文全家桶",而不是你打的那句话本身。


内层循环:ReAct,采样→工具→再采样

run_turn 里有个 loop,每一圈就是一次采样请求。这是整个 Harness 的心脏:

在这里插入图片描述

这其实就是经典的 ReAct(Reason + Act) 循环:

  1. 模型思考——它先"说"出想法(流式吐字)
  2. 调用工具——它提出要调用某个工具
  3. 结果回灌——Harness 执行完,把结果写回 history
  4. 回到第 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 会检查三个东西:

  1. model_needs_follow_up——还有工具没跑完,或子 agent 往 mailbox 投了消息
  2. has_pending_input——模型跑着时你又打了一句(steer)
  3. token_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

事件路径:

  1. Session.send_event / send_event_raw_with_persistence
  2. 按策略持久化 rollout
  3. App Server conversation.next_event()
  4. apply_bespoke_event_handling
  5. ServerNotificationTurnStarted、item 增量、TurnCompleted 等)
  6. TUI 渲染

一个完整例子:它到底经历了什么

你问:「这个函数为什么 panic?」

  1. TUI / App Server 把它映射成 TurnInput::UserInput
  2. Core 返回 StartedRegularTaskTurnStarted
  3. 刷新 AGENTS.md、MCP、工具表,冻结进 StepContext
  4. 把 world state、skills、用户问题写进 history
  5. history + base_instructions + tools 发给模型
  6. 模型先流式输出「我先看代码」,再调用 shell / 搜索 / 读文件等工具
  7. Harness 执行工具(可能先审批),把结果写回 history
  8. 再采样,模型写出解释(以 delta 流式发出)
  9. Stop hooks 放行 → TurnComplete
  10. UI 早就把流式文字展示出来了,TurnComplete 正式收尾

一句话总结

Codex 的 Harness = RegularTask 包一层 turn,run_turn 反复做「组 Prompt → 流式采样 → 执行工具 / compact / hook → 再采样」,直到模型不再需要跟进。

模型负责"想",Harness 负责"做"。一个只会吐字,一个真正动手。两者加起来,才是你看到的那个"会干活的 AI"。


附录:关键文件速查

路径职责
tui/src/chatwidget.rs接收用户提交
tui/src/app/thread_routing.rsturn/startturn/steer
app-server/src/request_processors/turn_processor.rsApp Server turn API
core/src/codex_thread.rs线程侧 submit / start-or-steer
core/src/session/handlers.rssubmission_loop
core/src/session/turn_input.rsStart / steer / 拒绝
core/src/tasks/regular.rs外层 harness 任务
core/src/tasks/mod.rsspawn_taskTurnComplete
core/src/session/turn.rsrun_turn、采样、prompt
core/src/session/mod.rsStep context、world state、事件
core/src/session/turn_context.rsTurnContext
core/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)到底是怎么"瘦身"的——想看的人多就安排。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐