2 Commits
5a553ec813
...
5c75cb3f71
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
5c75cb3f71 |
fix(ai): 流式回答零内容时不再返回空回答,并标记 EMPTY_COMPLETION
问题(与上一个提交同一个现场):/ai/chat/stream 只发出「开场 role 帧 + finish_reason=stop 帧
+ [DONE]」。这两个帧都是本地生成的、与模型无关(startWith / concatWith),所以这就是一个
**空回答**:客户端表现为一直停在加载态,而 llm_call_trace 把它记为 status=COMPLETE 且
error_type 为空,服务端全程没有告警痕迹 —— 线上持续 10 天、7/204 次,直到客户端报障才发现。
根因(数据库证据,非推断):llm_call_trace 全库 204 条 COMPLETE 记录中,ai_response 为空的有
7 条,且这 7 条 **100% 都 completion_tokens >= max_tokens**(精确等于配置的 2000)。CHAT 活跃
模型 deepseek-v4-flash 会把推理 token 计入 completion_tokens;开启 RAG 后 prompt_tokens 涨到
1035~3557,推理阶段把 max_tokens=2000 的预算吃干,可见答案零 token 产出。Spring AI 只取
delta.content(推理内容进 metadata 的 reasoningContent,不拼进正文),于是上游分片全为空串,
被 preserveTrailingWhitespace 的 current.isEmpty() 分支静默吞掉(该分支无日志、无计数),最终
只剩本地生成的首尾帧。同一问题的同步兜底请求因 URL 不带 enableRag、prompt 仅 334 token,
预算有富余而返回了正常答案 —— 这也解释了「流式空、同步正常」的观感。
- chatStreamOpenAi 增加零内容兜底,帧序仍为 role → 内容… → 兜底内容 → stop → [DONE]
(客户端解析器无需改动):统计非空白内容分片数与上游原始分片数,内容分片为 0 时补一帧内容
① 先复用同一 spec 做一次非流式调用(subscribeOn(boundedElastic)),成功则作为内容帧发出,
用户完全无感 —— 覆盖「provider 流式分片不带内容、但非流式正常」这类成因;
② 非流式也为空则发一条明确提示 —— 覆盖「预算被推理吃光」这类成因:非流式走的是同一套预算与
同样的 prompt,同样会返回空,所以两级都不可省。
- 可观测性:兜底发生时打 WARN(含上游分片数、内容分片数、兜底来源),并在 llm_call_trace 记
error_type=EMPTY_COMPLETION,后台「提示词追踪」可直接筛出这类记录。
- MyLoggerAdvisor:修 after() 的真实 NPE —— getResult() 在 generations 为空时返回 null
(provider 在流末下发 "choices":[] 的用量分片即命中),相邻的 ContentSafetyAdvisor 判了空、
它没判,会抛 NPE 打断整条 advisor 链;同时补注释说明「流式下 AI Response: 为空是必然现象」
(BaseAdvisor.adviseStream 只在携带 finish_reason 的收尾分片回调 after,那片 delta 内容天然
为空),避免下次排查被这行日志误导。
- README:修正流格式契约 —— data: 后**无空格**(原文档写作 data: {...},按文档实现的自研解析器
会一个字节都取不到),并补充首帧只含 delta.role、末帧 delta 为空对象 + finish_reason=stop、
全链路不发任何 event: 具名事件这三个特征。
注意:本提交不改变「推理吃光 max_tokens 预算」这一根因,只把「静默的空回答」变成「可见、不破坏
体验的失败」。真正恢复答案需要把 CHAT 的 max_tokens 从 2000 调大(配置层,另行处理)。
验证:mvn clean package -P prod 通过;编译产物已确认含 EMPTY_COMPLETION 与提示文案;用真实 SDK
回归新帧序 —— 兜底内容被正常渲染、无 stream_empty、客户端不再触发自身兜底(syncFallbackCalls=0)。
|
1 month ago |
|
|
20d9a889ac |
fix(sdk): 流式零内容时降级路径未渲染答案,界面永久停在「正在思考...」
问题:第三方页面反馈「对话框一直显示正在思考...,但控制台里请求与响应数据都正常」。 定位:服务端这次的流式响应只回了「开场 role 帧 + finish_reason=stop 帧 + [DONE]」三个控制帧、 零内容(服务端成因见后一个提交)。SDK 的 sendStreamMessage 在 onDone 里发现零内容时确实会 降级再发一次同步 /ai/chat,但该分支只写 `chatRequest(...).then(resolve)` —— 既不渲染气泡也不 收掉 loading;而 produceAIReply 的 streaming 分支把「渲染气泡 + 收 loading」全权委托给了 onChunk 回调,这条路径拿不到 onChunk。结果同步拿到的答案被静默丢弃、loading 永不消失。 - 降级路径补齐 hideLoading + renderAIBubble(复用 dom.ts 已有实现,终态与非流式分支一致) - 同一函数另一处同类出口:首个 token 之前点「停止生成」时 loading 同样不会被收,且会把一条 空 AI 消息写进历史。补 hideLoading;并在 produceAIReply 加守卫:aiContent 为空串时不 push、 不落库、不派发 csk:newMessage(避免历史里多出空气泡) - 流式零内容时上报 onError code=stream_empty,不再静默;README 补该错误码与触发方式说明 验证:用逐字复刻线上那 3 帧的 mock 流驱动真实 SDK(puppeteer)——降级拿到的答案被正常渲染成 气泡、「正在思考...」消失、onError 收到 stream_empty,请求序列与线上 HAR 一致;正常带内容的流 不受影响(errors 为空、syncFallbackCalls=0);首 token 前中止时 loading 消失且历史无空消息。 client 单测 92 个全绿。 (static/sdk/test.html 仅两条 link 顺序变化,hash 未变,属构建产物顺序抖动) |
1 month ago |
6 changed files with 151 additions and 9 deletions
-
2README.md
-
3client/README.md
-
32client/src/chat.ts
-
17src/main/java/com/wok/supportbot/advisor/MyLoggerAdvisor.java
-
104src/main/java/com/wok/supportbot/app/AssistantApp.java
-
2src/main/resources/static/sdk/test.html
Write
Preview
Loading…
Cancel
Save
Reference in new issue