Codex 的「调整方向」为什么让我惊为天人:从 Steer 看懂 Agent 是怎么工作的

第一次真正让我意识到“Agent 和普通聊天模型不是一种东西”的功能,不是什么多 Agent 协作,也不是什么花里胡哨的 MCP。
而是 Codex 里一个看起来很不起眼的按钮:
调整方向(Steer)。
很多 Agent 在执行任务的时候,我如果突然发现它方向跑偏了,通常只有两种选择:
-
等它把这轮任务完整做完,再发送下一条消息;
-
直接终止当前任务,然后重新告诉它正确方向。
Codex 却有第三种玩法。
它正在工作的时候,我可以直接发一句:
等等,不要继续查前端了,问题应该在数据库。
然后它不会终止整个任务,也不会把这句话老老实实排到任务结束以后。
它会完成手头当前这一小步,然后突然:
好的,那我调整方向检查数据库。
第一次看到的时候,我是真的有点:
这是什么黑科技?
后来 OpenAI 把 Codex 的大量客户端与 Agent Runtime 源码公开出来以后,这套机制终于可以直接扒开看了。
结果发现,它既没有往大模型“正在思考的大脑”里强行塞一句话,也没有什么神秘的实时修改推理。
真正精彩的是它背后的 Agent 运行方式。

一、先理解一件事:一个 Agent 任务,不等于一次模型调用
我们平时看 Codex 工作,很容易产生一种错觉:
我:帮我修复这个登录 Bug
Codex:思考……读取文件……修改代码……运行测试……再次修改……完成。看起来像模型从头到尾进行了一次漫长的思考。
实际上并不是。
一个真实的 Agent 任务更接近:
模型思考 ↓读取代码 ↓模型再次思考 ↓修改文件 ↓模型再次思考 ↓运行测试 ↓模型再次思考 ↓发现问题 ↓继续修改也就是:
思考 → 执行 → 观察结果 → 再思考 → 再执行……
不断循环。
比如模型第一次可能只决定:
我先读取
auth.ts。
Codex 帮它读取文件。
文件内容回来以后,再调用一次模型:
看完了,问题可能在 Session,我继续检查这里。
然后又执行工具。
测试结果回来以后,又调用一次模型。
所以一个我们肉眼看到的“完整任务”,底下可能进行了很多次模型调用。
这就是 Agent Loop——翻成人话,其实就是:
Agent 不断重复“想一下 → 干一下 → 看结果 → 再想一下”。

理解这一点以后,Steer 就没那么玄学了。
二、Steer 没有打断模型的大脑,它插在了两次思考之间
假设 Codex 当前正在:
模型思考↓决定读取 auth.ts↓读取 auth.ts这时我突然发:
不对,我已经确认问题在数据库,不要继续查 Session。
Codex 并不会做到这种科幻操作:
模型正在生成第 1837 个推理 token ↓主人突然发消息 ↓直接把新信息塞进模型当前的大脑 ↓第 1838 个 token 突然改变方向至少目前公开的 Codex Steer 并不是这么实现的。
真正发生的是:
当前动作正在执行 ↓主人发送新消息 ↓Codex:先记下来 ↓当前动作结束 ↓把主人的消息加入上下文 ↓下一次调用模型 ↓模型看到新要求 ↓自然改变方向OpenAI 的源码里甚至直接写了一句:
“Pending input is drained into history before building the next model request.”
翻译成人话就是:
用户中途发来的待处理消息,会在构建下一次模型请求之前加入历史。

这几乎已经把 Steer 的机关直接写在注释里了。
三、那个神秘的 pending input,其实就是一个“小纸条篮子”
源码里会出现一个词:
pending input第一次看挺唬人,其实:
-
input:输入; -
pending:等待处理。
所以它就是:
用户已经发过来了,但 Agent 还没来得及处理的新消息。
可以把它想象成桌边一个小篮子。
Codex 正在修代码:
正在跑测试……我突然:
测试结束后别继续查 Session,先看数据库。
Codex:
小纸条篮子:
[主人:别继续查 Session,先看数据库]测试结束以后:
Codex:看看篮子……
哦,主人刚才说方向要改。
↓把这句话加入上下文↓调用模型然后模型下一轮自然会看到:
原始目标:修复登录 Bug
已经完成:检查 auth.ts运行测试……
最新用户要求:不要继续查 Session,优先检查数据库于是它自己重新规划。
没有黑魔法。
但是工程体验非常漂亮。
四、源码里甚至真的有一个叫 turn/steer 的接口
Codex App Server 公开的协议里,存在一个非常直白的接口:
turn/steer它的官方定义大致就是:
给当前正在运行的 Turn追加用户输入,而不是启动一个新的 Turn。
这里的 Turn 可以简单理解成:
用户眼中的这一轮完整任务。
比如:
“帮我把这个登录 Bug 修掉”从用户角度,这是一个 Turn。
内部虽然可能已经:
思考 → 读代码 → 思考 → 改代码 → 测试 → 思考……循环十几轮,但用户仍然认为这是同一个任务。
Codex 的 turn/steer 还要求提供当前正在执行的 expectedTurnId。
如果任务已经结束了,或者你试图 Steer 的任务已经不是当前任务,服务器会拒绝。
这很好地说明:
Steer 不是偷偷新开了一轮,而是真的把消息送进当前正在执行的任务。
五、真正关键的一行源码
Codex 的 run_turn() 中有一段非常有意思的逻辑。
源码里有这样一个判断:
let needs_follow_up = model_needs_follow_up || has_pending_input;翻成人话:
还要不要继续当前任务?
=模型自己觉得还需要继续 或者用户中途还有新消息没处理也就是说,本来模型可能觉得:
好了,我差不多完成了。
结果 Codex Runtime 一看:
等等。
主人刚刚还塞了一张小纸条进来。于是:
别结束。继续下一轮。下一轮模型自然就会看到新的要求。
如果把整个机制极度简化成伪代码,大概就是:
while 任务没有结束:
完成当前一步
if 用户中途发了消息: 把消息加入上下文
调用模型继续思考
if 模型不需要继续 and 没有新消息: 结束任务注意,这只是方便理解的伪代码,并不是 Codex 原始源码。
但基本思想就是这么朴素。
六、所以 Steer 真正操纵的不是模型,而是 Agent Loop
这是我觉得整件事情里最重要的一句话:
Steer 操纵的是 Agent 的执行循环,而不是正在运行的大模型本身。
模型本身并不一定需要知道:
“嗨,我现在支持一种叫 Steering 的高级功能。”
对于模型而言,它只是下一次被调用的时候突然发现聊天记录里多了一句:
用户刚刚补充:不要动 Session,检查数据库。
模型:
好,那我重新规划。
真正负责实现 Steer 的是模型外面的 Agent Runtime。
也就是:
什么时候调用模型?
什么时候运行工具?
什么时候接受新输入?
新输入属于当前任务还是下一任务?
什么时候结束任务?这些问题。
这也是为什么我后来越来越觉得:
Agent 产品真正有意思的地方,已经不只是模型能力,而是模型外面的运行系统。
七、Steer 和 Queue 到底有什么区别?
很多 Agent 更常见的是 Queue,也就是排队。
比如 Agent 正在执行:
任务 A:修登录 Bug我中途说:
修完以后再补一下 README。
Queue 会:
任务 A████████████████完成
↓
任务 B更新 README████████████非常合理。
但是如果我说:
等等,登录问题不是 Session,是数据库!
还是 Queue:
任务 A:继续沿着错误方向干 20 分钟……完成
↓
任务 B:“其实问题在数据库。”这就有点喜剧了。
所以两种输入本质上表达的是完全不同的意思。
Steer
你现在做的这件事,方向需要调整。
例如:
不要重构整个模块,只修这个 Bug。
Queue
你现在这件事做完以后,还有另一件事。
例如:
修完以后顺便补一下测试。
GitHub Copilot SDK 现在也已经正式把这两个模式区分开。
其官方文档中:
-
immediate:Steering,影响当前 Turn; -
enqueue:Queueing,等当前 Turn 完成以后再处理。
GitHub 给出的示例也非常形象:
Steer:“实际上不要创建那个文件,换一种实现。”
Queue:“这个完成以后,再把测试修一下。”这说明 Steering 已经不只是 Codex 一个奇怪的小功能,而开始成为 Agent Runtime 中一种明确的交互语义。

八、既然 Steer 这么爽,为什么不能所有消息默认 Steer?
因为它也会把 Agent 搞晕。
比如当前计划原本非常清晰:
1. 检查数据库2. 修改 DAO3. 补测试4. 验证结果用户连续:
等等,先看看 Controller。
不对,还是先看数据库。
要不先重构一下?
哦对了,顺便更新 README。模型下一轮看到的上下文就开始变成:
原任务+第一次调整+第二次调整+第三次调整+第四次调整+之前已经执行了一半的结果Agent 需要不断重新理解:
当前真正目标到底是什么?
这会增加注意力负担,也容易造成计划混乱。
GitHub Copilot 官方文档现在甚至专门建议:
-
默认优先使用 Queue;
-
Steering 主要用于纠偏;
-
Steering 信息应该简短;
-
不要连续大量 Steering;
-
如果方向变化已经非常大,最好直接终止当前任务重新开始。
所以 Steer 并不是:
Queue 2.0。
而是两种不同工具。
九、Steer 还有一个隐藏麻烦:上下文压缩
Agent 现在动不动工作几十分钟甚至几个小时。
过程中可能:
读取几十个文件调用几十次工具产生大量测试输出修改大量代码这些东西不可能永远全部塞给模型。
上下文快满的时候,Agent 通常需要进行压缩:
把之前发生的事情总结一下,只保留重要信息。
正常任务很好总结:
目标:修复登录 Bug。
已经确认:问题来自数据库字段映射。
已完成:DAO 修改。
剩余:测试。但是有 Steer 以后可能是:
原任务:修登录 Bug
↓做到一半
用户:不要修改 Session,问题在数据库
↓Agent 改变方向
↓继续执行那么压缩系统必须理解:
“不要修改 Session”并不是一个独立的新任务。
它是:
原任务中途增加的一条重要约束。
如果压缩时把这种关系搞丢,可能最后只剩一句:
用户讨论了 Session 与数据库问题。
最关键的:
不要动 Session
反而淡化了。

OpenCode 正在开发的新版 Session 设计里,也专门把 steer 与 queue 分开:Steer 在下一个安全的模型调用边界进入当前执行,而 Queue 保持 FIFO,等当前执行准备结束后才处理。其设计文档也明确提到了 Steering 与上下文压缩之间的语义关系。
所以一个看起来只是:
“允许用户中途发消息。”
的小功能,实际上会一路影响:
Agent Loop↓上下文管理↓任务状态↓消息队列↓上下文压缩↓UI↓日志与任务回放这也是为什么它并不是加个按钮那么简单。
十、还有一个所有软件最终都逃不过的问题:时间竞争
假设 Agent:
正在完成任务██████████████░就在最后那一瞬间,我发送:
等等,还有一个关键约束……
同时 Agent:
任务完成!两件事只差几毫秒。
那么问题来了:
这句话到底算当前任务的 Steer,还是下一个任务?
这就是软件工程里非常经典的并发竞争问题。
Codex 的 turn/steer 要求提供 expectedTurnId,其实就在帮助解决这种问题:
我要调整的必须还是我刚才看到的那个任务。
如果当前执行已经换掉,就不能把消息稀里糊涂塞进去。
果然,不管技术发展到什么程度,最后还是逃不过:
“这两个事件到底谁先发生?”
熟悉的味道。
十一、那 Steer 和“直接中断,再发送一条消息”又有什么区别?
这个问题一开始也让我非常困惑。
因为从大模型本身来看,两者真的很像。
都是:
之前的上下文+用户的新要求↓下一次模型调用区别其实主要发生在模型外面。
Steer
当前 Agent 任务继续活着↓当前步骤完成↓加入新要求↓继续同一个执行循环相当于:
继续干,但换个方向。
Interrupt
停止当前 Agent 执行↓结束这次运行↓保留已经正式记录下来的状态↓用户重新发送消息↓启动新的执行相当于:
停,我们从已经保存下来的进度继续说。
这里可以把它想象成游戏里的“存档点”。
假设 Agent 当前实际已经走到了这里:
实际执行进度:
████████████████████░░░ ↑ 这里按了 Esc但最近一次已经完整结束、能够稳定保留下来的模型状态可能还停在:
最近存档点:
█████████████████ ↑ 已保存状态Interrupt 做的不是把模型暂停在:
“我刚刚想到第 1837 个 token,回来继续想第 1838 个。”
而更像是:
█████████████████ ↑ 从存档点继续当前还没有完整提交的一小段模型生成、某些正在进行的内部步骤,并不等于能够原封不动地恢复。
Steer 则不一样。
它没有主动结束当前执行:
████████████████████ ↑ 到达安全节点 + 收到用户新要求 ↓██████████████████████████Agent 只是完成手头这一小步,在下一次模型调用前读到新消息,然后继续往下跑。
所以从这个角度看:
Steer 更像在当前游戏流程中收到新的任务提示。
而:
Interrupt 更像退出当前运行,再从最近可靠的存档状态重新进入。
不过这里还有一个非常重要的区别:
“存档”只针对 Agent 的运行状态,不代表现实世界自动回滚。
例如 Agent 已经:
修改 foo.py↓保存文件↓开始运行测试↓你按下 Interrupt那么 foo.py 已经发生的修改通常依然存在。
同样,已经完成的命令、产生的 Git Diff、创建的文件,也不会因为 Interrupt 自动消失。
所以 Interrupt 不是:
时间倒流。
它只是:
停止 Agent 继续往下执行。
Codex 的 App Server 也明确把 turn/steer 与 turn/interrupt 作为两个独立操作:
-
turn/steer:向正在运行的 Turn 加入输入; -
turn/interrupt:请求取消当前 Turn,并让其以interrupted状态结束。
所以最形象的理解依然是:
Steer 是打方向盘。
Interrupt 是踩刹车。
而“存档点”这个例子则解释了:
为什么踩完刹车以后重新启动,和单纯在行驶过程中打一下方向盘,并不是完全一样的事情。
发现 Agent:
“这里实现方向稍微不对。”
打方向盘。
发现 Agent:
“等等!你怎么开始删数据库了?!”
别 Steering 了。
踩刹车。
十二、Agent 越来越能跑长任务,Steering 可能就越重要
以前 AI Coding 更像:
我发一句↓模型回答↓我再发一句整个任务几十秒。
即便方向有点错:
算了,等它说完。
成本并不高。
现在越来越多 Agent 开始:
分析仓库↓制定计划↓修改多个模块↓运行测试↓检查失败↓重新修改↓验证↓继续……一次任务可能持续十几分钟、几十分钟,甚至更久。
这时候如果五分钟的时候我就已经发现:
你方向错了。
只有 Queue:
那你继续错二十分钟吧,等你结束我再说。
只有 Interrupt:
好吧,把现在这个执行停掉,我们重新开始。
都不够舒服。
Steer 刚好填补了中间这块:
你已经做的东西不用全部推翻,但是从这里开始换方向。
于是人类在 Agent 中的角色也发生了一点变化。
以前是:
Human ↓Agent ↓完成 ↓Human 验收逐渐变成:
Human ↓Think → Act → Observe ↑ ↓ └──── Steer ────┘ ↓ Think ↓ Act人类不再只是:
发任务的人。
也可以变成:
Agent 长任务执行过程中的方向控制者。
这其实非常接近真实的软件协作。
一个程序员工作二十分钟,你发现他理解错需求,不可能说:
“没事,你先按照错误需求全部写完,写完我再告诉你。”
正常情况显然是:
“等等,这里方向不对。”
然后继续。
十三、理想状态可能不是 Steer vs Queue,而是三种控制
讨论到最后,我反而觉得未来 Agent 最自然的交互应该同时存在三种模式:
Steer继续做,但调整方向。
Queue这件事做完,再做另一件事。
Interrupt / Break停,这件事不要继续了。对应日常语言其实非常直观:
“这里不要用 Redis,改成本地缓存。”→ Steer
“做完以后顺便把 README 更新一下。”→ Queue
“停停停,这整个需求我刚才说错了。”→ Interrupt它们解决的是三种完全不同的用户意图。

而随着 Agent 越来越自主、单次任务越来越长,“执行过程中允许人类重新进入控制循环”大概会越来越重要。
Codex 的 Steer 最让我觉得漂亮的地方,也正在这里。
它没有发明什么可以实时修改大模型大脑的黑科技。
它只是非常认真地解决了一个特别朴素的问题:
Agent 已经工作半天了,我突然有句话想告诉它,怎么办?
答案是:
别把整个任务杀了。
等它手上的这一小步做完,把我的话递给它,然后继续。
就这么简单。
也就这么好用。
最后一个不负责任的小猜想
当然,上面说了这么多架构设计、Agent Loop、Human-in-the-loop……
我第一次用 Codex 的 Steer 时,脑子里的真实想法其实简单得多:
Codex 一个小任务动不动都能慢吞吞跑十几二十分钟。
OpenAI 不会是因为实在等不下去了,才火急火燎加了个 Steer:
“算了,至少让用户在它磨蹭的时候还能进去喊两句吧。”
——以上纯属调侃,没有任何证据表明 Steer 的诞生原因和 Codex 著名的“让我再检查一下”行为存在因果关系。
但是不得不说……
第一次发现我居然可以在 Codex 干活到一半的时候直接进去掰它方向,我是真的惊为天人。
原来不是:
“AI,你先干,我等你。”
而是终于开始有点:
“你先干,我在旁边看着,走歪了我喊你。”
的感觉了。
这可能才是 Agent 真正开始像一个“协作者”的地方。
参考资料与源码
OpenAI Codex
-
openai/codex:Codex 开源仓库。openai/codex GitHub 仓库 -
codex-rs/app-server/README.md:Codex App Server 协议,包含turn/steer、turn/interrupt、Turn 生命周期等定义。Codex App Server README -
codex-rs/core/src/session/turn.rs:Agent Turn 核心循环,可以看到 pending input 如何在下一次模型请求前进入历史,以及has_pending_input如何影响任务是否继续。Codex turn.rs 源码
GitHub Copilot
- GitHub Copilot SDK 官方文档:Steering and queueing,明确区分
immediate与enqueue两种处理中途用户输入的方式,并给出了使用建议。GitHub Copilot:Steering and queueing
OpenCode
- OpenCode V2 Session 设计:将
steer定义为下一个安全模型调用边界进入当前执行,queue则等待当前执行结束。该部分属于其正在演进的 V2 架构设计。OpenCode V2 Session Spec
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

















