TL;DR
当模型在思考模式下只输出 reasoning、长时间不输出 content(退化重复循环)时,服务端的思考缓冲不会被清空,并且会跨 turn.boundary / 跨重试继续累加到同一条 assistant 消息上,最终单条消息的 reasoning 膨胀到 674,006 字符。这条消息会让会话点进去就卡死(渲染进程 164.8% CPU、内存 9 分钟涨 150 MB),用户只能重启 Studio 才能恢复。
环境
| 项目 |
值 |
| Ekko Studio |
0.7.23(Windows 11 / git-bash) |
| Agent Runtime |
Hermes Agent(bridge,hermes-bridge-run-* 线程) |
| 模型 |
第三方 OpenAI 兼容中转的 deepseek-flash(思考模式默认开启) |
| 服务端 |
resources/webui/dist/server/index.js |
| 数据 |
~/.hermes-web-ui/hermes-web-ui.db + profile 的 state.db |
| 受影响会话 |
mtzixen10tkll2(6061 messages) |
现象 / 实测数据
1. 单条 assistant 消息的 reasoning 体积异常
SELECT id, role, LENGTH(COALESCE(reasoning,'')) AS r
FROM messages WHERE session_id='<sid>' ORDER BY r DESC LIMIT 5;
189482 674,006
189382 475,649
188936 451,774
184590 223,670
184585 222,139
181522 20,927 ← 断崖,正常量级
5 条巨型的重复率 84%–98%(同一条消息内计数):
"OK." ×31,702 "Let me write" ×15,044 ← 674k 那条
"hmm:" ×43,854 ← 223k 那条
2. 该消息不是单次生成,而是多次累加
- 单条消息内同一句用户提示词被重述 7 次:位置
[0, 450448, 453547, 482317, 508079, 511681, 632378]
- 该轮日志:4 次请求构建、0 次正常完成(16 分钟后被用户
/abort)
- 算术验证:674,006 字符 ≈ 15 万 token,而思考模式单次输出上限默认 64K token(实测该端点严格执行
max_tokens,max_tokens=60 时 completion_tokens=60, finish_reason=length)→ 物理上不可能是单次生成
3. 会话打开即卡死(前端渲染,不是后端)
后端 [context-history] context read 9–29 ms ← 不慢
渲染进程 pid 24776 164.8% CPU
内存 319 MB → 472 MB(9 分钟)
服务端 socket 反复重连(app cache: hit),客户端一直拿到旧的巨型消息。
4. 自我放大(跨轮)
按时间顺序,这 5 条单调增长:
09-19 12:17 222,139
09-19 12:32 223,670
09-20 10:37 451,774 ← 翻倍
09-20 21:40 475,649
09-21 10:09 674,006
因为该 provider 的请求带 tools,历史轮次的 reasoning_content 必须完整回传且会被拼接进上下文(DeepSeek 官方 thinking mode 规则),所以上一轮的退化文本会被喂回给模型,下一轮退化更重。
根因定位(服务端 dist/server/index.js,变量名已压缩)
① reasoning 增量无上限累加
else if (E === "reasoning.delta" || E === "thinking.delta") {
let T = String(J.text || "");
if (T) {
n.bridgePendingReasoningContent = (n.bridgePendingReasoningContent || "") + T;
let j = H6(n, r, a); // 按 runMarker 复用同一条 assistant 消息
j.reasoning = (j.reasoning || "") + T;
j.reasoning_content = (j.reasoning_content || "") + T;
}
l(E, { event: E, run_id: o.run_id, text: T });
}
② 消息查找按 runMarker + finish_reason == null 复用同一行
function P2e(t, e) {
return [...t.messages].reverse()
.find(n => n.runMarker === e && n.role === "assistant" && n.finish_reason == null);
}
→ 同一次 run 内的多次尝试/重连,全部续写同一条消息。
③ turn.boundary 时,content 为空就提前返回,reasoning 缓冲不清空 ← 这是关键
function WG(t, e, n) {
let r = t.bridgePendingAssistantContent || "",
a = t.bridgePendingReasoningContent || "";
if (!r.trim()) return t.bridgeAssistantMessageId; // ← 模型只思考、没吐正文 → 直接 return
...
t.bridgePendingAssistantContent = "";
t.bridgePendingReasoningContent = ""; // ← 只有 content 非空时才会执行到这里
...
}
退化循环的典型表现正是只有 reasoning、没有 content,于是:
turn.boundary → 提前 return(缓冲保留)→ 下一次尝试继续 += → 循环 N 次 → 单条消息累积到 67 万字符。
④ 全程没有长度上限:bundle 内搜不到任何 reasoning 截断 / 上限常量。
期望行为
- 模型只输出 reasoning 不输出 content 时,
turn.boundary 也应该清空(或至少收口)reasoning 缓冲,不应跨尝试累加。
- 单条消息的 reasoning 应有长度上限(超出截断并标记
truncated),不应无上限增长到几十万字符。
- 前端渲染思考内容时应对超长文本做截断/虚拟化,避免单条消息冻住整个渲染进程。
- (附带)服务端
sessionMap 没有淘汰机制:用户从数据库侧修掉坏数据后,Studio 仍在用内存副本,必须重启才生效(resumeSession 里 a || (a = await z6(...)))。建议提供强制重载入口或加 TTL。
复现要点
- 使用一个「思考模式下会陷入重复循环」的模型/端点(本次是第三方中转的
deepseek-flash;该端点还会因 ALB idle 掐断长流,进一步触发重试)。
- 长会话 + 带
tools 的请求(Hermes 每轮都带 22 个 tool 定义)。
- 让模型某轮只产思考不产正文(重复循环)。
- 观察
messages.reasoning 长度在多次重试后线性增长,直到会话打开即卡死。
相关 issue
TL;DR
当模型在思考模式下只输出 reasoning、长时间不输出 content(退化重复循环)时,服务端的思考缓冲不会被清空,并且会跨
turn.boundary/ 跨重试继续累加到同一条 assistant 消息上,最终单条消息的reasoning膨胀到 674,006 字符。这条消息会让会话点进去就卡死(渲染进程 164.8% CPU、内存 9 分钟涨 150 MB),用户只能重启 Studio 才能恢复。环境
hermes-bridge-run-*线程)deepseek-flash(思考模式默认开启)resources/webui/dist/server/index.js~/.hermes-web-ui/hermes-web-ui.db+ profile 的state.dbmtzixen10tkll2(6061 messages)现象 / 实测数据
1. 单条 assistant 消息的 reasoning 体积异常
5 条巨型的重复率 84%–98%(同一条消息内计数):
2. 该消息不是单次生成,而是多次累加
[0, 450448, 453547, 482317, 508079, 511681, 632378]/abort)max_tokens,max_tokens=60时completion_tokens=60, finish_reason=length)→ 物理上不可能是单次生成3. 会话打开即卡死(前端渲染,不是后端)
服务端 socket 反复重连(
app cache: hit),客户端一直拿到旧的巨型消息。4. 自我放大(跨轮)
按时间顺序,这 5 条单调增长:
因为该 provider 的请求带
tools,历史轮次的reasoning_content必须完整回传且会被拼接进上下文(DeepSeek 官方 thinking mode 规则),所以上一轮的退化文本会被喂回给模型,下一轮退化更重。根因定位(服务端
dist/server/index.js,变量名已压缩)① reasoning 增量无上限累加
② 消息查找按
runMarker+finish_reason == null复用同一行→ 同一次 run 内的多次尝试/重连,全部续写同一条消息。
③
turn.boundary时,content 为空就提前返回,reasoning 缓冲不清空 ← 这是关键退化循环的典型表现正是只有 reasoning、没有 content,于是:
turn.boundary→ 提前 return(缓冲保留)→ 下一次尝试继续+=→ 循环 N 次 → 单条消息累积到 67 万字符。④ 全程没有长度上限:bundle 内搜不到任何 reasoning 截断 / 上限常量。
期望行为
turn.boundary也应该清空(或至少收口)reasoning 缓冲,不应跨尝试累加。truncated),不应无上限增长到几十万字符。sessionMap没有淘汰机制:用户从数据库侧修掉坏数据后,Studio 仍在用内存副本,必须重启才生效(resumeSession里a || (a = await z6(...)))。建议提供强制重载入口或加 TTL。复现要点
deepseek-flash;该端点还会因 ALB idle 掐断长流,进一步触发重试)。tools的请求(Hermes 每轮都带 22 个 tool 定义)。messages.reasoning长度在多次重试后线性增长,直到会话打开即卡死。相关 issue
node:sqlite同步 I/O)—— 相关但不同:本例消息条数只有 6061,后端读取仅 9–29 ms,卡的是单条消息体积引发的前端渲染。