《FGO》影之国圣杯战线图文教程汇总 影之国的舞斗会圣杯战线图文操作步骤
2026-08-08
2026-08-11 0
最近一段时间在做一个桌面的Agent项目,一直在研究 Codex 的实现,也尝试把它的本地工具能力复用到自己的项目里。刚开始看代码时,我脑子里有一个很朴素的理解:Codex 不就是一个会调用 工具的大模型吗?
但真正顺着源码往下追,很快就会发现这个理解太薄了。
模型确实负责推理,也会生成 Tool Call,但它并不天然拥有当前工作目录、长进程和文件权限这些操作系统状态。真正把工作区信息提供给模型,并保证命令在正确目录执行的,是模型外面的 Agent Harness。一次持续几分钟甚至几小时的任务,也不是靠模型单次回答完成的。
这一篇是“拆解 Codex”系列的第一篇,我们先不钻进某个 Tool 的参数,也不急着分析每个 Rust 模块,而是先回答一个最基础的问题:
当用户输入一句话时,Codex 为什么能够连续读代码、运行命令、修改文件、执行测试,最后再给出结论?
本文分析基于本地 codex 仓库的 f3465047d1ee 版本。
假设我们给 Codex 一个很常见的任务:
请修复 APP-1427:登录成功后偶发跳回登录页。Bug 单中附有复现步骤和客户端日志,请定位根因、完成修复并验证。
如果只是普通聊天模型,它最多根据我们贴进去的 Bug 描述和代码给出修改建议。但 Coding Agent 的关键,是能把这句话转换成一个持续执行的工作流。
这条链路里,模型负责理解任务、分析结果和决定下一步;工具负责读取、搜索、修改和执行;Agent Loop 则把两者连接起来,让一次 Tool Result 成为下一次模型判断的输入。
在第一轮模型判断之前,对当前工作目录生效的 AGENTS.md 已经在会话或 Turn 的上下文构建阶段被发现并加入指令。只有工作目录或规则作用域发生变化时,系统才需要刷新适用的规则。
说白了,这不是一次问答,而是模型在项目规则约束下,借助 Skill 和工具不断观察、行动、再观察的闭环。
从职责上看,可以把 Codex 分成三层:

如果继续拆开中间的 Harness,最核心的是五部分:
AGENTS.md、Skills、Plugins 和 MCP 并不是另一套 Agent Loop。它们分别从“工作规则”和“外部能力”两个方向扩展这套 Harness。
下面逐个看。
Codex 当前的主循环入口位于:
codex-rs/core/src/session/turn.rs
其中 run_turn 的源码注释已经把基础逻辑说得很清楚:模型会返回 Assistant Message 或 Function Call;如果返回 Function Call,Codex 执行工具,并在下一次请求中把结果交还给模型;如果模型只返回最终消息,这个 Turn 就可以结束。
抽掉错误处理、事件上报和上下文压缩后,它的骨架大致可以理解成:
loop { // 1. 构造这一次模型看到的上下文 let step_context = session.capture_step_context(turn_context).await; let history = session.clone_history().await; // 2. 请求模型,并持续消费流式响应 let mut stream = request_model( step_context, history.for_prompt(...), ).await?; let mut in_flight = FuturesOrdered::new(); let mut needs_follow_up = false; while let Some(event) = stream.next().await { if let ResponseEvent::OutputItemDone(item) = event? { match ToolRouter::build_tool_call(item.clone())? { // 3. 模型返回 Tool Call:交给 Runtime 执行 Some(call) => { in_flight.push_back(Box::pin( tool_runtime .clone() .handle_tool_call(call, cancellation_token.child_token()) )); needs_follow_up = true; } // 普通 Message 或 Reasoning:记录为会话 Item None => session .record_conversation_items(turn_context, &[item]) .await, } } } // 4. 等待工具完成,并把 Tool Result 写回 History while let Some(tool_result) = in_flight.next().await { session .record_conversation_items(turn_context, &[tool_result?.into()]) .await; } // 5. 有 Tool Result 时进入下一轮,让模型继续判断 if !needs_follow_up { break; }}真实源码没有把这些动作全部写在 run_turn 同一层:run_sampling_request 构造 ToolRouter 和 ToolCallRuntime,try_run_sampling_request 消费模型流,handle_output_item_done 识别并调度 Tool Call,最后由 drain_in_flight 等待工具完成并把 Tool Result 写回 Conversation History。
除此之外,真实实现还要处理用户中途追加的消息、重试、取消、Hooks、Context Window、自动 Compaction,以及可能并行返回的多个 Tool Call。
但最稳定的核心没有变:
模型判断 → 执行动作 → 观察结果 → 再次判断
所以,模型负责产生“下一步应该做什么”,Agent Loop 负责让这一步真的发生,并把结果重新送回模型。两者组合起来,才会出现我们看到的持续工作能力。
问题来了,第二次请求模型时,Codex 应该发送什么?
显然不能只发送最初那句“修复 APP-1427”。模型还需要知道:
AGENTS.md 规则;在当前实现里,这部分不是一个简单的 Vec。
运行层至少可以看到三个不同生命周期的对象:
Session:承载整个线程级的服务、历史和活动任务;TurnContext:固定一轮任务使用的模型、权限、模式和配置;StepContext:固定某一次 Sampling 看到的环境、MCP Tool 快照和 AGENTS.md 状态。这里 StepContext 很关键。一次 Turn 可能包含很多次模型请求,而工作区、可用环境、MCP 工具列表在期间可能变化。Codex 会为一次具体的 Sampling 捕获一致的请求视图,保证“展示给模型的工具”和“随后真正执行的工具”来自同一份上下文。
然后,Conversation History 会经过 for_prompt(...) 转换成模型输入。随着任务持续运行,Tool Output、Assistant Message 和用户追加信息不断进入历史。
上下文快到窗口上限时,run_turn 还会触发自动 Compaction,把冗长历史压缩成后续仍可继续工作的状态。
因此,上下文工程并不是“把整个仓库全部塞进 Prompt”。更准确地说,它是在每个决策点构造一个有边界、可复现、对当前动作足够的信息快照。
模型决定运行命令时,返回的不是一段直接交给操作系统的代码,而是结构化 Tool Call,例如:
{ "name": "exec_command", "arguments": { "cmd": "npm test", "workdir": "/workspace/project" }}Tool 系统需要完成两件不同的事情:
当前源码中的 ToolRouter 很直接地体现了这两个世界:
pub struct ToolRouter { registry: ToolRegistry, model_visible_specs: Vec,} model_visible_specs 是模型能看见的契约,registry 则保存真正可以执行这些契约的 Runtime。
在每次 Sampling 前,built_tools(...) 会根据当前 StepContext 重新构建工具视图。这里合并的不只是 Codex 内置工具,还可能包括:
模型返回结果后,ToolRouter 把 Response Item 转成内部 ToolCall,再构造 ToolInvocation。其中不仅有参数,还带着 Session、StepContext、取消信号、Call ID 和 Diff Tracker。
这也是为什么 Tool 不能简单理解成一个函数。
模型看到的是能力契约;Router 负责识别意图;Registry 负责找到 Runtime;Handler 才负责进入具体执行流程。
到了这一层,模型的意图才真正变成系统动作。
以最常见的命令执行为例,exec_command 需要处理:
如果命令没有在当前等待窗口内结束,Codex 会返回一个可继续操作的 session_id。后续 write_stdin 并不会启动第二个 Shell,而是继续向原来的进程或 PTY 写入内容,并读取增量输出。
修改文件也是类似的。
apply_patch 不是让模型随便覆盖一个文件。它需要解析 Patch、核对上下文、验证目标路径,再把文件变化转换成可追踪的 Diff 和 Tool Result。失败时,错误会回到 Agent Loop,让模型重新读取文件或调整 Patch。
所以 Tool 和 Runtime 也不是一回事:
一个可以运行任意命令、读写本地文件的 Agent,如果只依靠 Prompt 约束,风险非常高。
Codex 把安全边界继续下沉到了执行层。当前 TurnContext 中就包含:
命令执行前,系统会结合 Tool 请求、配置和当前权限判断:
不同平台的底层实现并不相同。macOS 使用 Seatbelt;Linux 当前主要通过 Bubblewrap 构造文件系统沙箱,并配合 Seccomp 和 PR_SET_NO_NEW_PRIVS 限制进程与网络能力,Landlock 只保留为兼容旧版本的 Legacy 路径;Windows 则使用基于 Restricted Token 等机制的专用沙箱后端。但它们服务的是同一个目标:让模型无法仅凭自己的一句话越过系统授权。
安全规则不能只写在 System Prompt 里。Prompt 是给模型看的,Sandbox 才是模型绕不过去的执行边界。
把上面的部分重新串起来,“修复 APP-1427 的登录跳转问题”会经过下面这条链路:

实际过程可以简化成这样:
TurnContext,把适用的 AGENTS.md 规则和可用能力加入上下文;StepContext,向模型提供历史、环境、工具以及可用 Skill 的描述;这里没有哪个单独模块能够称为“Codex”,模型、Loop、Context、Tools、Runtime 和 Safety 缺一块,体验都会完全不同。
模型只负责根据当前输入生成下一步动作,Agent Loop 负责不断调用工具执行、观察和继续。模型本身无状态并不维护上下文,Agent 维护上下文并在每次请求时候把上下文发送给模型。
Tool Spec 是模型可见的契约,ToolRouter 和 ToolRegistry 负责路由,真正操作系统的是对应 Handler 和 Runtime。
模型输入除了对话,还包括项目规则、工作区状态、权限、工具定义、Skills、环境信息和经过压缩的历史。
这一篇只是先建立对Agent的全局认识,相当于一张地图,沿着这张地图继续往下追,还有几个更值得弄清楚的问题:
这些问题,才是理解 Codex 从“会调用工具的模型”走向完整桌面 Agent 的关键。