wanghanlin
|
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)。
|
2 weeks ago |
wanghanlin
|
2001dc3a0f
|
fix(doc): 修复重新处理文档会按 2000 字截断预览重建导致内容丢失
问题
- knowledge_document.content 上传时被截断到 2000 字符(有意的预览设计),
但 reprocessDocument 把它当原文重建分块,而 DocumentProcessingService 以
cleanBeforeFirstAttempt=true 运行(先删光旧向量)——对超过 2000 字符的文档
执行 POST /document/batch/reprocess 会永久丢失其余内容
修复
- content 改存原文全文(列已是 TEXT,无需 DDL),并写 extra_config.contentComplete
作为完整性标记
- 实体 content 加 @JsonIgnore:全文任何接口都不返回(否则 GET /document/list
无字段投影,每行都会带上整篇正文);文档详情接口单独组装响应,返回 2000 字预览
(键名仍是 content)与 contentTruncated 标志,前端据此提示截断
- reprocessDocument 的数据源按优先级解析:
1) 有 contentComplete 标记 → 用库内全文(语义最忠实,JSON 的 fields/pointer
模式不受影响,也不依赖文件是否还在)
2) 历史遗留文档(无标记,content 是截断预览)→ 按 fileType 从原始文件重解析
(md → MarkdownDocumentLoader,json → JsonDocumentLoader,其余 → Tika),
成功后回填全文并打标记,此后不再依赖原始文件
3) 两者都不可用 → 明确报错拒绝,绝不静默按截断预览重建
无法完整还原时在标记 PROCESSING 之前抛错,事务回滚,文档状态与已有向量都不受影响
- contentTruncated 判据覆盖历史数据:长度恰好等于上限且无 contentComplete 标记时
同样判定为已截断,避免把截断预览误报成完整内容
- 同步 content 列注释(DatabaseInitConfig / init-database.sql / knowledge-base.sql),
该注释会在下次启动幂等刷新到库
验证(真实库 + 557 篇存量文档环境)
- 上传 4891 字符正文:库内存全文,详情接口返回 2000 字预览 + contentTruncated=true
- 重新处理:chunk_count 与首次完全一致(修复前会降为个位数),内容无丢失
- 模拟历史数据(截断 + 无标记):有文件时从文件重解析成功并回填全文;
文件也丢失时明确拒绝,且文档状态与 chunk_count 保持不变(向量未被破坏)
- 回填后再删文件重跑仍成功(已自愈);GET /document/list 不再含 content
- RAG 对话回归正常
已知降级
- JSON 的 basic/fields/pointer 解析模式上传时未持久化,历史 JSON 文档从文件重解析
只能按 basic 还原(不丢数据,仅抽取口径可能变化,日志有 WARN)
- 存量中疑似被截断的文档需重跑一次才能回填全文(均已保留原始文件,均可恢复)
|
3 weeks ago |
wanghanlin
|
84eea7564f
|
refactor: 自造轮子改用 Spring AI 标准组件、清理死代码并修复既有缺陷
组件替换(改用 1.1.x 标准组件)
- 文档提取:删除自写 TikaDocumentReader(内部直接 new org.apache.tika.Tika()),
改用官方 org.springframework.ai.reader.tika.TikaDocumentReader。
pom 早已引入 spring-ai-tika-document-reader 却从未使用其类,属典型「引了标准依赖却手写实现」
- 意图识别:IntentRouter 的手写正则解析(含去 BOM/零宽字符/全角空格等约 60 行防御逻辑)
改用标准 BeanOutputConverter<IntentResult>,保留原有降级语义(空输入/解析失败 → RAG)
- 推荐问题:SuggestionGenerator 的哨兵字符串 + 代码块剥离 + 按行降级解析改用
ChatClient.entity(ParameterizedTypeReference<List<String>>),整体删除 SuggestionResponseParser
- 分块:删除 MyTokenTextSplitter 薄壳,新增 OverlapTokenTextSplitter
分块器 overlap 缺陷修复(本次验证中发现并修复)
- 旧实现把 knowledge.chunk.overlap 传进了 TokenTextSplitter 第 2 个形参 minChunkSizeChars 位,
overlap 从未生效(Spring AI 的 TokenTextSplitter 根本没有 overlap 形参)
- 新实现继承标准 TextSplitter,复刻 TokenTextSplitter 全部切分语义,仅把前进步长改为
chunkSize - overlap,使重叠真正生效
- 验证中发现:标点截断会缩短本块消耗的 token 数,与 overlap 叠加后可把前进步长压到
1 个 token,分块数膨胀 10 倍(实测 349 块 vs 修复后 44 块)。故仅当截断后仍能前进
至少 (chunkSize - overlap) / 2 个 token 时才采用该截断,否则宁可切断句子
- overlap=0 时与标准 TokenTextSplitter 逐块一致(已对比验证)
- ⚠️ 存量文档需重新分块 + 重新向量化(POST /document/batch/reprocess),否则新旧向量口径混杂
其他既有缺陷修复
- RerankerService:原用 new RestTemplate() 且无任何超时,慢 provider 会把检索线程拖到
TCP 超时;改用 RestClient + JdkClientHttpRequestFactory 显式设置 connect/read 超时(各 3s)
- McpServerConfigController:MCP Server 增删改/启停/全量刷新后未清 AssistantApp 的
ChatClient 缓存,导致继续使用旧工具集;现补调 clearCache()
死代码清理(均已 grep 确认零引用)
- 删除 FileBasedChatMemory、ReReadingAdvisor(零装配且 before() 逻辑为 no-op)、
SseEventBuilder(零引用)、SuggestionResponseParser
- McpToolCallback 删除 EVENTS ThreadLocal 与 drainEvents()/resetEvents()(零调用),
保留在用的 MCP_EVENTS_KEY/MCP_ROUNDS_KEY ToolContext 机制
文档
- 同步更新 CLAUDE.md / README.md / DEPLOY.md 的版本号、组件说明与架构描述
- 修正 CLAUDE.md 中与代码不符的既有描述:主启动类并未排除 PgVectorStoreAutoConfiguration
(项目用的是非 starter 坐标,classpath 上本就没有该自动配置);分类过滤实现已完成,
原「Spring AI filter 支持有限」的 TODO 已不成立
|
4 weeks ago |
wanghanlin
|
d0cd06f9fc
|
fix(document): 修复批量上传向量化只成功小文档问题并加固处理链路
- 移除逐 chunk 串行 AI 关键词提取(MyKeywordEnricher):产出 excerpt_keywords 全库无检索消费点,却将外部调用放大 chunkCount 倍且与在线对话共用模型抢限流
- 向量化改按批入库(默认 50 块/批, knowledge.vector.batch-size)逐批 try-catch 隔离:失败批只记录缺失区间继续,已入库块保留,error_message 聚合"已入库 x/y 块+缺失区间+原因",chunk_count 改为实际入库块数
- 新增文档级失败自动整体重试(≤2 次, 仅 timeout/429/5xx 等瞬时错误):重试前清残留向量防重复;处理期间删除文档即终止写入避免孤儿向量;外层兜底保证状态不悬挂 PROCESSING
- Embedding 客户端(DashScope/OpenAI 兼容/豆包多模态)补显式 connect 10s/read 60s 超时,避免慢响应无限挂起占死 documentExecutor 线程
- 文档同步 CLAUDE.md/README.md,新增 knowledge.vector.batch-size 配置说明
|
1 month ago |
wanghanlin
|
6be019082e
|
docs: 同步对话接口迁移与 OpenAI SSE 格式说明
- README.md、SDK-INTEGRATION.md、client/README.md、client/CLAUDE.md 四处接口表更新为新路径 /ai/chat、/ai/chat/stream、/ai/chat/sources
- 补充流式接口 OpenAI Chat Completions SSE 格式说明(data: JSON chunk + data: [DONE])
- 删除废弃的 /ai/assistant_app/* 路径与 rag/sync 描述
|
2 months ago |
wanghanlin
|
8e586a8c35
|
求移除未使用的 PRODUCT_EXTRACT(商品信息抽取)类型配置
|
3 months ago |
wanghanlin
|
26d6f9823b
|
增加项目数据库初始化sql
增加前端模型配置校验
设置默认向量维度为1024(豆包模型低向量维度)
|
3 months ago |
wanghanlin
|
6f861fcf79
|
二期-文档列表完善批量操作、上传校验、去重、分块配置与上传进度功能
|
3 months ago |
wanghanlin
|
cff087fad0
|
一期-前端重构
|
3 months ago |
wanghanlin
|
6876fce280
|
一期-更新说明
|
3 months ago |
Lin Yanxiang
|
78a65896fa
|
Update README.md
|
1 year ago |
Lin Yanxiang
|
4acba8ad93
|
Update README.md
|
1 year ago |
hygl
|
7884a9ec84
|
更新readme文件
|
1 year ago |
Lin Yanxiang
|
a6a98f0e60
|
Update README.md
|
1 year ago |
Lin Yanxiang
|
4a03b5f7df
|
Create README.md
|
1 year ago |