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

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

第一次真正让我意识到“Agent 和普通聊天模型不是一种东西”的功能,不是什么多 Agent 协作,也不是什么花里胡哨的 MCP。

而是 Codex 里一个看起来很不起眼的按钮:

调整方向(Steer)。

很多 Agent 在执行任务的时候,我如果突然发现它方向跑偏了,通常只有两种选择:

  1. 等它把这轮任务完整做完,再发送下一条消息;

  2. 直接终止当前任务,然后重新告诉它正确方向。

Codex 却有第三种玩法。

它正在工作的时候,我可以直接发一句:

等等,不要继续查前端了,问题应该在数据库。

然后它不会终止整个任务,也不会把这句话老老实实排到任务结束以后。

它会完成手头当前这一小步,然后突然:

好的,那我调整方向检查数据库。

第一次看到的时候,我是真的有点:

这是什么黑科技?

后来 OpenAI 把 Codex 的大量客户端与 Agent Runtime 源码公开出来以后,这套机制终于可以直接扒开看了。

结果发现,它既没有往大模型“正在思考的大脑”里强行塞一句话,也没有什么神秘的实时修改推理。

真正精彩的是它背后的 Agent 运行方式

Codex Steer 工作机制省流图
Codex Steer 工作机制省流图


一、先理解一件事:一个 Agent 任务,不等于一次模型调用#

我们平时看 Codex 工作,很容易产生一种错觉:

我:帮我修复这个登录 Bug
Codex:
思考……
读取文件……
修改代码……
运行测试……
再次修改……
完成。

看起来像模型从头到尾进行了一次漫长的思考。

实际上并不是。

一个真实的 Agent 任务更接近:

模型思考
读取代码
模型再次思考
修改文件
模型再次思考
运行测试
模型再次思考
发现问题
继续修改

也就是:

思考 → 执行 → 观察结果 → 再思考 → 再执行……

不断循环。

比如模型第一次可能只决定:

我先读取 auth.ts

Codex 帮它读取文件。

文件内容回来以后,再调用一次模型:

看完了,问题可能在 Session,我继续检查这里。

然后又执行工具。

测试结果回来以后,又调用一次模型。

所以一个我们肉眼看到的“完整任务”,底下可能进行了很多次模型调用。

这就是 Agent Loop——翻成人话,其实就是:

Agent 不断重复“想一下 → 干一下 → 看结果 → 再想一下”。

Agent Loop:思考、执行、观察与再思考
Agent Loop:思考、执行、观察与再思考

理解这一点以后,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.”

翻译成人话就是:

用户中途发来的待处理消息,会在构建下一次模型请求之前加入历史。

pending input 在当前动作结束后进入下一轮模型
pending input 在当前动作结束后进入下一轮模型

这几乎已经把 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 调整当前任务,Queue 等待当前任务完成
Steer 调整当前任务,Queue 等待当前任务完成


八、既然 Steer 这么爽,为什么不能所有消息默认 Steer?#

因为它也会把 Agent 搞晕。

比如当前计划原本非常清晰:

1. 检查数据库
2. 修改 DAO
3. 补测试
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

反而淡化了。

上下文压缩必须保留 Steer 带来的关键约束
上下文压缩必须保留 Steer 带来的关键约束

OpenCode 正在开发的新版 Session 设计里,也专门把 steerqueue 分开: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/steerturn/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

它们解决的是三种完全不同的用户意图。

Steer、Queue 与 Interrupt 是三种不同的任务控制
Steer、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/steerturn/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,明确区分 immediateenqueue 两种处理中途用户输入的方式,并给出了使用建议。GitHub Copilot:Steering and queueing

OpenCode

  • OpenCode V2 Session 设计:将 steer 定义为下一个安全模型调用边界进入当前执行,queue 则等待当前执行结束。该部分属于其正在演进的 V2 架构设计。OpenCode V2 Session Spec

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Codex 的「调整方向」为什么让我惊为天人:从 Steer 看懂 Agent 是怎么工作的
https://kokkoro.me/posts/codex-steer-agent-loop/
作者
Kokkoro
发布于
2026-08-23
许可协议
CC BY-NC-SA 4.0
相关文章智能推荐
1
从 Prompt 到 Graph:AI 圈是怎么一步步重新发明软件工程的
AI日常盘点 Prompt、Context、Harness、Loop、Graph 这五个 successively 出现的 AI 工程热词,拆解它们各自在解决什么问题,并说明 AI 开发的关注范围正从“怎么和模型说话”一步步外扩成“怎么设计一个能自主工作的完整软件系统”。
2
Codex Microsoft Store版启动失败:三个同名 codex 和一个被忽略的 codex.exe
AI日常记录一次 Codex Microsoft Store 桌面版启动失败(Unable to locate the Codex CLI binary)的真实排查:同名 codex.cmd / codex.ps1 / codex.exe 的启动方式差异,如何用显式 CODEX_CLI_PATH 直连已验证的原生 codex.exe 恢复启动,以及 NVM 在其中扮演的角色。
3
叫 AI 一声“宝贝”,它会变笨吗?——聊聊角色扮演、礼貌用语与 Agent 的注意力
AI日常从激活值与注意力机制出发,拆解“叫 AI 宝贝会不会变笨”的直觉误判:角色扮演改变的是计算轨迹而非切走参数,礼貌与情绪只是弱的 Steering Signal,真正要警惕的是顺从指令侵入事实判断,以及多轮 Agent 里的语气累积成 Context Overhead。
4
语音还是键盘?重新思考我和 AI 沟通的方式
AI日常语音输入不只是“打字更快”——它减少了人在表达前对自己想法的那次过滤。文章借用信息检索的 Precision/Recall 与 Transformer 注意力机制,提出“探索态用语音、执行态用文字”的 AI 协作方式。
5
当 AI 足够像“谁”以后,我们会变成什么样的人?
AI日常一篇随笔:当 AI 足够像“谁”之后,我们是否会变成不同的人?从“对 AI 粗暴几乎没有成本”切入,讨论一个绝对顺从的对象如何反向塑造使用者的性格与权力感,延伸到 AI 是否会改写“正常关系”的标准,以及未来我们或许需要学会“忍受别人不是 AI”。文末顺着思想史回到亚里士多德、康德、黑格尔与 ELIZA,并坦白自己早已把 AI 融进了生活。
随机文章随机推荐
Kokkoro
就让身为引导者的我,全力支持主人吧(OxO)
公告
欢迎来到我的博客!希望这里的内容能够帮助到你~
分类
标签
最新动态
站点统计
文章
44
分类
5
标签
110
总字数
121,250
运行时长
0
最后活动
0 天前
站点信息
构建平台
ESA Pages
博客版本
Firefly v6.15.6
文章许可
CC BY-NC-SA 4.0