空想不运行的解决方案
#7270
Replies: 1 comment 4 replies
|
很多很多次遇到这样的事,官方好像不知道一样,不管是网页版还是这个都有这种情况 |
4 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
背景
我们在长会话中遇到一个稳定出现的失败模式:某一轮模型什么都没做,但系统认为它正常完成了。用户侧的感受是「它发了几个『好』,然后就结束了」。
追查后发现这是两个环节叠加的结果——退化思考被反复回传形成自我强化,而"只有思考"的完成又不会被重试。下面分开说。
现象
1. 空转的一轮被判为正常结束,因此永不重试
模型在思考通道里退化(反复输出「好。写。发。」一类无意义片段)后,该轮既没有正文、也没有任何工具调用。此时 DSH 不报错、不触发重试,会话看起来「正常完成」,但任务一步都没推进。用户只能重新发一次同样的请求,那一轮就白过了。
2. 退化的思考被逐轮回传,形成自我强化
serialize.ts会在每个带 reasoning 的轮次把思考原文回传(reasoning_content/thinking)。一旦某轮退化,这段退化文本就进入了后续每一轮的输入,模型倾向于模仿自己刚才的模式,于是下一轮更严重。实测的恶化轨迹:密度 8.6% → 11.1% → 持续 5 轮以上,越滚越重。
(我们理解回传本身是协议要求——
serialize.ts的注释说明 tool-call 轮必须回传 CoT。这里想讨论的是回传策略是否可配置,见方案 C。)实测数据
在一个 157 轮的真实会话中(
deepseek-v4-flash-vision-exp,reasoningEffort: high,Messages 协议):同一会话中,退化思考的规模:
疑似根因
packages/llm/llm-deepseek/src/protocols/chat-completions/translate.ts与packages/llm/llm-deepseek/src/protocols/messages/translate.ts中的完成判据:两者都只覆盖「完全没有内容块」的情况。而一个只打开过 reasoning 块、随后以
stop结束的响应,块数为 1,因此被当作成功。translate.spec.ts中有一条测试固定了当前行为:同时
serialize.ts的注释解释了理由——在 "a v4-flash greeting" 这类场景下,模型可以完全在 reasoning 通道里回答。我们理解这是有意的设计,因此下面的建议都尽量保持默认语义不变。不过在 agent 场景下,一轮没有任何可执行产物(无正文、无工具调用)时把它判为成功,会让 loop 失去唯一的恢复机会。
serialize.ts注释引用的协议依据(thinking_mode 要求 tool-call 轮回传 CoT)解决的是「回传什么」,而不是「何时算完成」。建议的方案
方案 A(最小改动):把 reasoning-only 完成判为
EMPTY_RESPONSEEMPTY_RESPONSE已在retry-policy.ts的DEFAULT_RETRYABLE_CODES首位(默认最多 5 次、指数退避 500ms → 10s),因此这一改动会让 loop 自动重试。我们在本机验证过这条路径可用。方案 B(保守):给方案 A 加配置开关
保持默认行为不变,新增例如
llm-deepseek的reasoningOnlyCompletion: 'stop' | 'empty-response',让部署自行选择。如果维护者认为 v4-flash 那种「完全在思考通道回答」的场景必须保留,B 同样能解决我们的问题。
方案 C(治本):让历史思考的回传策略可配置
方案 A/B 解决的是「空转不被重试」,但空转本身仍然会发生。成因是退化思考被逐轮回传、自我强化。建议把回传策略做成可配置项,例如:
我们不建议改变默认行为——协议依赖回传,这点我们清楚。这里只是希望长会话部署能有一个逃生口。我们在本机用本地规则实现过一版(按"行去重后剩余比例"与"中文字符单字密度"判定退化,退化块压缩为一行说明,正常历史仅在上下文占用超阈值时精简),效果是 reasoning 字符量下降约 77%、循环污染不再回灌,且正常思考的结论与文件行号都保留了。
关于"用插件解决"
我们首先尝试了插件路线(因为 CONTRIBUTING 说明目前不接受外部 PR,并鼓励社区插件)。但架构上不可行:三个可能触及请求的扩展点,
llm/stream对 agent 构造的请求只读(a loop-built request ... listeners read it, never rewrite it),agent/request明确不能改写 messages,session.surface只支持整条消息的区间遮蔽(且需要维护 tool-call / tool-result 配对)。处理「reasoning-only 完成」这件事只能发生在判定完成的地方,也就是 adapter 的 translate 步骤——这是我们建议改核心的原因。
复现条件与判断信号
deepseek-v4-flash-vision-exp(其他 flash 系可能类似)reasoningEffort: high(我们观测到时为max/high)判断信号:会话日志中出现
assistant/message,其 content 只有reasoning块且文本高度重复。复现是概率性的,但特征明确。如果需要,我们可以提供更详细的会话统计数据。
English summary
A turn whose completion contains only
reasoningblocks and ends withstopis classified as a successful stop, so a turn that produced no text and no tool call is never retried and the task silently stalls — users see "it emits a few '好' and then just ends".Measured on a real 157-turn session (1275 assistant messages): 23 messages had reasoning > 0 while text == 0 and tool calls == 0, five of them consecutive. One degenerate thinking block was 38,658 characters, 83% of which was a single character. Degenerate thinking is replayed every turn as CoT passback and therefore reinforces itself across subsequent turns.
Proposals: (A) classify
stopwith no non-reasoning block asEMPTY_RESPONSE(that code is first inDEFAULT_RETRYABLE_CODES, so the loop retries automatically); (B) the same behind a config switch; (C) make historical reasoning passback configurable so long sessions can opt out of replaying degenerate thinking. We also note that a plugin cannot fix this:llm/streamis read-only for loop-built requests,agent/requestcannot mutate messages, andsession.surfaceonly shadows whole messages./
All reactions