文章目录
- 前言
- 1. 先把三个词摆清楚,不然后面全白看
- 2. 工具调用:step 为什么总说“还没完”
- 2.1 没有工具调用呢?模型直接说完了
- 3. 三个入口,长得像,干的事完全不同
- 3.1 自激问题:文件观察器把自己玩醒了
- 4. 结束前的最后一扇窗:turn-stopping
- 4.1 那个著名的三步测试
- 4.2 这扇窗也能让循环停不下来
- 5. 更隐蔽的“多跑一次”:重试
- 6. max-tokens:一旦达到,翻不了案
- 7. 产品层面:别用一个“正在思考”盖住所有阶段
- 8. 结尾:排查清单,拿去抄
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看, 传送门https://blog.csdn.net/HHX_01
前言
你见过这种界面吗:回答已经完整显示了,Agent 还在转圈;或者它刚说了一句“完成了”,转头又发起一次模型请求。
这画面,像极了开会时领导说“我讲完了”,然后大家又默默听了四十分钟。
先别急着骂它死循环。它可能不是疯了,只是太有责任感。模型输出结束和任务结束,本来就是两套判断——就像你女朋友说“我没事”,和“我真的没事”,中间隔着一整个宇宙。
这一篇接着拆 DeepSeek Harness0.1.6-alpha.2,提交ddefc45fbc7f8e46dd73185e68295696d1297887。重点看agent.ts里的turn()和step():一个管运行周期,一个管单步执行。界面上的状态,都得对照这两个函数的返回条件来看。
1. 先把三个词摆清楚,不然后面全白看
动手之前,先把三个单位摆出来。这三位长得像,但绝对不是三胞胎,顶多算同款发型:
| 单位 | 在这里表示什么 |
|---|---|
| request / attempt | 一次模型调用尝试,可能成功,也可能失败后重试 |
| step | 一次已经进入的循环步骤,包含请求尝试及对应工具执行 |
| turn | 从turn/start到turn/end的一次运行周期,可以包含多个 step |
结论先放这儿:一次请求返回,不代表这个 step 的动作都做完了;一个 step 结束,也不代表 turn 就能收工。另外,同一个 step 遇到能恢复的请求错误,还可能原地重试。
翻译成人话:外卖显示“已送达”,不代表饭已经在你的桌子上;老板说“会开完了”,也没人敢第一个站起来走。这三个单位的关系,就像点外卖、等外卖、吃外卖——你以为是一件事,其实是三件事,而且账单还分开算。
2. 工具调用:step 为什么总说“还没完”
最常见的路径:模型生成一条工具调用,流结束后,Harness 提交 assistant 消息,执行工具,记录结果。这时候step()返回的不是 completed,而是null——意思是:本 turn 还得继续,除非工具执行明确给出了结束信号。
翻译一下:step()不是拒绝下班,是它知道还有活没交接完。
为什么?因为工具跑完,还得再来一次模型请求处理结果。就像你让同事去查个数据,他查完回来,你还得看数据、给结论——工具只是把活干完,收尾永远得你自己来。第一篇里的读、改、测流程就是这样:三次工具请求之后还有一次最终说明,总共四次请求。
注意,这只是默认路径,不是铁律。源码里有工具主动 conclude turn 的分支,取消和异常也能终止过程。实现调度器的时候,别照着口头解释写一个无条件的continue——那相当于把“他应该回来了”当成“他已经在座位上了”。
2.1 没有工具调用呢?模型直接说完了
假设模型直接给出一段完整文字,step()确实可以返回 completed。但turn()还有一步:检查收件箱——有没有属于下一步的输入?
这个检查是为了接住模型生成期间冒出来的新事实。比如文件监控插件发现依赖刚被改动,或者用户补了一句“顺便考虑一下空数组”。这类输入如果会影响当前任务,运行时就不能只凭模型刚才那句 finish 就把 turn 掐了。
这就好比你已经说了“晚安”,老婆突然说“对了,还有一件事”——睡意全无,turn 也别想结束。
3. 三个入口,长得像,干的事完全不同
源码里有三个长相相近、语义不同的入口:
followup(message) → next-turn,并唤醒 steer(message) → next-step,并唤醒 inject(message) → next-step,不主动唤醒followup()是把输入排到后续的 turn;steer()是影响接下来这一步的输入;inject()更适合插件补上下文:运行中可以接续消费,空闲时就先留着,等下一次唤醒。
这仨的区别,用生活场景解释就是:
followup:你老婆说“没事”——这话是留给以后吵架用的;steer:她说“你再想想”——直接影响你接下来这一步;inject:同事在你桌上贴了张便利贴——你忙完才看,不打断你。
3.1 自激问题:文件观察器把自己玩醒了
这三个入口还决定了一件事:输入要不要主动唤醒循环。
如果文件观察器每次发现变化都唤醒模型,那 Agent 自己改文件又会触发观察器——好家伙,这不是 Agent,这是俄罗斯套娃版的《土拨鼠之日》。多出一圈调用还算轻的,严重时能连续自激:模型累死,账单烧穿。
选inject可以避免把每个观察事件都变成新的唤醒请求。但插件自己也得有点数:别无限注入重复信息。你往别人桌上贴便利贴可以,贴一万张同内容的,那叫骚扰。
4. 结束前的最后一扇窗:turn-stopping
当 step 已经具备结束条件、且下一步收件箱为空时,循环会发出一个串行扩展点:agent/turn-stopping。
注意,这不是“已经结束”的通知。它处在结束前的窗口期,监听者仍然可以往里面加下一步输入。等监听完成后,循环会再检查一遍收件箱和取消信号,然后才决定是否退出。
你可以把它理解成登机广播:广播说“请登机”,但你没上飞机之前,广播会一直循环,而且你随时可能发现自己忘带了充电宝。
4.1 那个著名的三步测试
仓库里有一个受控测试:模型脚本依次回答step 1、step 2、step 3,三次回复都是纯文字,没有工具调用。监听者在 stopping 窗口检查已完成的步骤数,不足三步就steer('continue')。
最终断言:同一个 turn 里跑出三步、发生三次请求。这个测试我们实际跑通了。
它证明的是:运行时允许在结束前追加工作。不是模型自己忽然决定多想两轮——模型没那么有主见,它只是老实回答,是监听者在后面喊“继续,继续,别停”。
4.2 这扇窗也能让循环停不下来
这个扩展点是把双刃剑。一个插件如果每次 stopping 都塞入新消息,那模型无论说多少次“完成”,turn 都不一定结束。
这时候光改提示词、让模型“回答完就停止”,解决不了问题。真正的驱动条件不在这——就像你让一个话痨“说到点就停”,他诚恳地点点头,然后继续说了二十分钟。
该检查的是:输入是谁追加的、什么时候追加的、有没有终止条件,以及外层有没有预算和用户取消机制。扩展点提供的是“继续运行”的能力,继续条件和停止条件得由业务逻辑自己定。你可以给它加菜,但得有人负责喊“够了”。
5. 更隐蔽的“多跑一次”:重试
还有一种更隐蔽的情况:模型请求失败后的重试。step()内部本身就有一层尝试循环;firstAttempt保证本 step 的用户消息只承认一次,不会每次重试都重复追加。
同一份步骤输入、同一次预处理,可能要经过多个调用尝试才拿到有效输出。这就像考试没及格重考——题目还是那道题,但你不能把上次的答案直接粘上去,监考老师认得你。
所以,数数的时候要有自己的口径:
- 算成本,数 request;
- 看业务进展,数 step;
- 看用户这一轮怎么收束,数 turn。
把三个数字揉成一句“Agent 调用了 N 次”,最需要解释的信息就丢了——信息量直接从 4K 掉到 240p。
日志也要分清:有效的 assistant 消息,和没成功落到模型历史里的 attempt,是两回事。重试失败的片段可以留作诊断材料,但不能随手当成“已经说给模型听”的历史——这就像草稿纸上的演算过程,不能直接算作答卷的一部分。日志这块儿,下一篇接着拆。
6. max-tokens:一旦达到,翻不了案
源码还记录了max-tokens:这个结束事实在 turn 里是黏性的。一旦某个 step 达到上限,后面就算正常完成,也不能把整个 turn 的结果悄悄降回普通 completed。
最终回复完整,不代表之前的步骤没发生过截断。这个状态必须保留。调用方得知道运行经历过什么,才能决定要不要向用户提示、要求补做、或者记录诊断。
这不是说凡是碰到 token 上限的任务都失败了——而是不能把重要的运行事实藏起来。就像你面试表现完美,但简历上那段空窗期,HR 还是看得见的。
7. 产品层面:别用一个“正在思考”盖住所有阶段
从产品角度看,最好不要用一个“正在思考”的状态盖住所有阶段。模型正在生成、工具仍在执行、结束前扩展还没处理完——这是三种不同的等待。
用户看到完整文字后还在等,至少应该有机会知道系统在等什么。不然用户只会觉得:这玩意儿是不是卡了?——和你看加载圈转了十秒时的想法一模一样。
不过,要展示这些状态,就得订阅运行时的事实,不能靠文字内容猜。回复里出现“已完成”不构成状态转换;出现“让我检查一下”,也不保证后面真的执行了工具。
翻译一下:男朋友说“马上到”,和真的到了,是两码事;领导说“我考虑一下”,和真的考虑了,也是两码事。
8. 结尾:排查清单,拿去抄
这一篇相关的loop.spec.ts共 65 个测试、inbox.spec.ts共 7 个测试,都在固定提交上通过。重点包括:停止窗口追加下一步、空闲注入不启动 turn、工具执行中的注入顺序。它们用受控回复核对调度语义,不衡量真实模型的任务成功率。
所以,turn 结不结束,取决于五件事:模型动作、工具结束信号、收件箱、结束前扩展、取消状态。排查“为什么还在跑”的时候,逐项过一遍:
- 模型是不是还在生成?——数 request
- 工具是不是还在执行?——看 step 返回
- 收件箱里是不是还有货?——查 inbox
- 有没有插件在 stopping 窗口疯狂加菜?——查扩展监听者
- 外层有没有预算和取消机制?——问产品
只改模型的结束措辞,改变不了运行时条件。话痨不会因为你说“说完了就闭嘴”就闭嘴,运行时也不会因为模型说“完成了”就真的完成。
最后说句实在话:如果上面五条都查完了还没头绪,先重启。重启治不好的,才轮到我们接着看代码。
P.S. 无意间发现了一个巨牛的人工智能教程,非常通俗易懂,对AI感兴趣的朋友强烈推荐去看看,传送门https://blog.csdn.net/HHX_01