know-engine

项目亮点与设计原则

项目亮点 🔥 本地 Reranking — 零外部依赖的精排方案 基于 ONNX Runtime 在 JVM 进程内直接运行 BGE RERANKER 模型,无需部署独立的 Reranking 微服务。采用双重检查锁单例模式,模型仅…

TL;DR

项目亮点 🔥 本地 Reranking — 零外部依赖的精排方案 基于 ONNX Runtime 在 JVM 进程内直接运行 BGE RERANKER 模型,无需部署独立的 Reranking 微服务。采用双重检查锁单例模式,模型仅…

项目亮点

🔥 本地 Reranking — 零外部依赖的精排方案

基于 ONNX Runtime 在 JVM 进程内直接运行 BGE-RERANKER 模型,无需部署独立的 Reranking 微服务。采用双重检查锁单例模式,模型仅加载一次,全生命周期复用。解决了 macOS Monterey 兼容性问题(降级 onnxruntime 至 1.17.1),并支持 JAR 包内模型文件的 classpath 自动解析与临时文件释放。

🔥 父子分段检索 — 语义完整性的关键保障

检索时命中细粒度子分段后,自动通过 parentChunkId 回溯到父分段的完整文本进行替换,同时通过 brotherChunkId 补全同级的兄弟分段。这一机制解决了"切片后语义截断"的 RAG 经典痛点,在保持检索精度的同时,为 LLM 提供完整的上下文窗口。

🔥 三源智能路由 — 一句话自动选数据源

KnowEngineQueryRouter 利用 LLM 对用户 Query 进行语义分析,输出 {intent, strategy, confidence} 结构化决策,自动路由到 Elasticsearch / MySQL / Neo4j 中最合适的数据源。路由失败自动降级为空结果,保障系统鲁棒性。Text2SQL 路径:当 strategy = relational_db 时,路由到 SqlDatabaseContentRetriever,LLM 根据 table_meta 表存储的表结构元数据生成 SQL,自动注入当前日期等上下文变量。Text2Cypher 路径:当 strategy = graph_db 时,路由到 Neo4jText2CypherRetriever,LLM 根据图数据库 Schema 自动生成 Cypher 查询,无需预先编写查询模板。Cypher 查询失败时自动降级回退到 ES 检索

🔥 流式 RAG 管道 — 进度感知的端到端流式架构

在 DefaultRetrievalAugmentor 的标准流程中,通过装饰器模式(ProgressAwareContentRetriever / ProgressAwareContentAggregator)注入进度回调,将阻塞式 RAG 操作调度到 Schedulers.boundedElastic(),通过 Flux.create() + publishOn(Schedulers.parallel()) 实现进度消息与 LLM Token 的有序混合推送。SSE 事件类型扩展为 4 类:[PROGRESS](处理进度通知)、[REFERENCE](RAG 引用溯源)、[CARD](结构化交互卡片)、Token(逐字文本)。其中 [CARD] 事件由 LLM 在特定场景输出结构化 JSON 标记触发,后端解析后转换为 [CARD]:{"type":"vehicle_info", "data":{...}} 格式推送,前端根据卡片类型渲染对应 UI 组件(表格、时间线、对比视图等),支持点击展开详情、跳转链接等交互操作。钉钉机器人接入同一流式管道:钉钉消息回调经签名验证后,调用 ChatApplicationService.streamChat() 流式收集完整回复,通过钉钉 API 回复消息;长回复自动分片发送,[CARD] 事件自动转为钉钉 ActionCard 消息格式,[REFERENCE] 引用来源附带在回复中提升可信度。

🔥 Spring 事件驱动 — 文档处理的优雅编排

文档从上传到入库全程通过 Spring ApplicationEvent 串联,Controller 只负责发布事件,业务逻辑完全由 EventListener 异步驱动。结合 XXL-Job 补偿任务,实现"事件驱动 + 定时兜底"的最终一致性保障。

🔥 分布式锁注解 — 声明式并发控制

基于 Redisson + AOP 实现的 @DistributeLock 注解,支持 SpEL 表达式动态 Key、超时自动续期、等待超时等策略,一行注解即可保护关键业务操作(如文档上传、切片)的幂等性。

🔥 多格式文档的智能切片能力

针对 Markdown,MarkdownHeaderParentTextSplitter 基于标题层级进行 Parent-Child Chunking,支持代码块保护、按 chunkSize 二次切割、以及父子关系元数据注入。针对 Excel/CSV,ExcelSplitter 实现了 RAGFlow 风格的双模式输出(键值对模式和 HTML 表格模式),并且支持按字符数智能分块,确保同一行数据不会被拆到不同分片中。文件类型检测则通过文件头魔数实现,避免依赖文件扩展名。

🔥 RAG 引用溯源

ProgressAwareContentAggregator 在重排序完成后,会从检索结果中提取文档来源、分片内容、Rerank 分数等元数据,组装成 RagReference 结构,既异步持久化到数据库,也实时通过 [REFERENCE] 事件推送给前端,让每一次回答都有据可查。

🔥 雪花算法 ChunkId — 分布式唯一标识

所有知识分段的 chunkId 均通过自研 SnowflakeIdGenerator 生成,基于雪花算法(64 位:1 位符号位 + 41 位时间戳 + 10 位工作机器 ID + 12 位序列号),确保在分布式环境下全局唯一、趋势递增。相比数据库自增 ID,雪花 ID 天然支持分库分表、跨服务关联;相比 UUID,雪花 ID 更短、可排序、对索引更友好。parentChunkId 和 brotherChunkId 同样使用雪花 ID,贯穿切片 → 存储 → 检索 → 扩展全链路。在文档多版本管理场景中,parentVersionDocId 通过雪花 ID 关联历史版本链,knowledge_segment.metadata 中的 docVersion 字段与雪花 ID 配合,实现版本热切换时的精准过滤——修改 ES 中 docVersion 的 Filter 条件即可切换活跃版本,无需重新向量化,雪花 ID 的趋势递增特性确保版本时序可追溯。expire_date 字段与版本元数据联动,文档到期后自动从检索结果中过滤。

🔥 分布式锁 — 文档处理并发控制

基于 Redisson + AOP 实现的 @DistributeLock 注解,支持 SpEL 表达式动态 Key(如 #document.docId)、场景隔离(scene)、超时自动续期、等待超时等策略。在文档切片(split)和向量化(embedAndStore)等关键操作上,通过 @DistributeLock(scene = "document-split", keyExpression = "#document.docId", waitTime = 0) 实现同一文档的互斥处理:当 waitTime = 0 时,已持有锁的请求直接失败,避免重复切片或并发写入导致数据不一致。

🔥 Excel 双模式智能切片 — 表格数据的 RAG 适配

针对 Excel/CSV 的结构化数据特点,ExcelSplitter 提供两种输出模式: - 键值对模式(默认):将每行数据转换为 表头1:值1; 表头2:值2; ... 格式,语义自包含,适合直接进行向量检索 - HTML 表格模式:保留原始表格结构,按 chunkSize 智能分块输出

片段,每个分块自动携带表头,确保独立可读 HTML 模式下采用行级完整性保障:同一行数据绝不会被拆分到不同分片中,即使该行超过 chunkSize,也保证至少包含一行。分块间通过共享表头实现上下文自足,解决了大表格跨分片后的信息丢失问题。文件格式检测通过文件头魔数实现(ZIP 头 → .xlsx,OLE 头 → .xls,逗号+换行 → .csv),不依赖文件扩展名,防止用户重命名导致的解析错误。同时支持 BOM 编码自动检测(UTF-8/UTF-16BE/UTF-16LE)。

🔥 表格与图片跨分片完整性保障

Markdown 文档中的表格和图片在被 chunkSize 二次切割时面临语义截断风险,本系统通过以下机制解决: - 父子分段保留完整原文:超出 chunkSize 的分片会保留一份完整原文(标记 skipEmbedding = 1,不参与向量检索),同时生成多个子分片(携带 parentChunkId)。检索时命中子分片后,自动通过 parentChunkId 回溯到 Redis 缓存中的父分片完整文本进行替换,确保表格和图片的上下文完整性 - 兄弟分段自动补全:通过 brotherChunkId + brotherChunkIndex + brotherChunkTotal 元数据,检索命中任一子分片时自动补全同级所有兄弟分段,拼出完整内容 - 代码块保护:Markdown 切片器识别 ``` 代码围栏,将整个代码块作为一个不可分割的单元处理,避免代码被截断

🔥 虚拟线程 + Flash 模型 — 异步标题生成零延迟

新会话首条消息发出后,系统通过 Thread.ofVirtual().name("title-summary-" + conversationId).start(...) 启动 Java 21 虚拟线程,异步调用 qwen3.5-flash 轻量模型生成对话标题并回写数据库。关键设计: - 零阻塞:虚拟线程不占用平台线程,不阻塞 SSE 流式响应的首 Token 输出 - Flash 模型选型:使用 qwen3.5-flash 而非 qwen-max,标题生成任务对推理能力要求低,Flash 模型响应更快、成本更低 - 关闭思考模式:通过 customParameters(Map.of("enable_thinking", false)) 关闭 qwen3.5-flash 的 CoT 思考过程,进一步降低延迟 - 优雅降级:标题生成失败时保留临时标题,不影响对话功能

🔥 意图识别 — LLM Structured Output 约束

通过 LangChain4j 的 @AiService + @SystemMessage + @JsonPropertyDescription 注解体系,将意图识别结果约束为 IntentRecognitionResult Record 类型,包含 reasoning(推理过程)、related(是否相关)、intent(意图分类)、entities(结构化实体)四个字段。LLM 输出被强制对齐到 Java 类型系统,避免了自由文本输出的不确定性。识别流程在 RAG 管道前端通过 Mono.fromCallable().subscribeOn(Schedulers.boundedElastic()) 调度,不阻塞 WebFlux 事件循环。

🔥 多级缓存 Chunk 检索 — HashMap + Redis + MySQL 三级缓存

KnowledgeSegmentServiceImpl.getTextByChunkId() 实现了 HashMap → Redis → MySQL 的三级缓存策略,用于父子分段检索时快速获取父分段完整文本: - L1 缓存(HashMap):以 chunkId 为 Key,在当前线程缓存父分段完整文本,避免同一次检索中重复查询 Redis 和数据库 - L2 缓存(Redis):以 chunkId 为 Key,30 秒 TTL,命中即返回 - L3 回源(MySQL):Redis 未命中时查询 knowledge_segment 表,结果写入 Redis - 防缓存击穿:查询结果为空时缓存空字符串,避免同一不存在的 chunkId 反复穿透到数据库 这一机制在父子分段检索场景下尤为关键:每条检索结果都可能触发父分段回溯,三级缓存将数据库查询压力降低一个量级。 在文档检索权限体系中,L2 缓存同时缓存权限元数据(accessibleBy),检索时在 EmbeddingSearchRequest 中注入 Filter 条件按当前用户身份过滤,权限变更时通过事件驱动批量失效关联缓存并更新 ES 索引,确保权限变更实时生效。权限过滤与版本过滤(docVersion)均通过 ES Filter 条件实现,与三级缓存协同工作,在保障数据安全的同时不影响检索性能。

🔥 LLM JSON 输出修复 — 7 步容错管道

LLM 在结构化输出场景中经常产生不符合 JSON 规范的结果(如包裹 Markdown 代码块、使用中文引号、尾部多余逗号等)。JsonUtil.fixJson() 实现了 7 步修复管道: 1. Markdown 代码块提取:剥离 json ... 包裹 2. 前后垃圾字符移除:定位首个 { / [ 到末尾 } / ] 之间的有效 JSON 3. 引号修复:中文引号 → 英文引号,单引号 → 双引号 4. 尾部逗号修复:移除 }, / ], 中的多余逗号 5. 缺失引号修复:为无引号的键名自动添加双引号 6. 转义字符修复:清理字符串中的非法换行符、制表符 7. 最终容错:以上修复后仍无效,将原始文本包装为 {"content": "..."} 结构 修复后通过 ObjectMapper.readTree() 验证有效性,确保下游代码始终拿到合法 JSON。该工具广泛用于意图识别、查询路由、查询改写等所有依赖 LLM 结构化输出的环节。

🔥 工厂模式多策略切片 — DocumentSplitterFactory

DocumentSplitterFactory 通过工厂模式封装 5 种切片策略,根据用户选择的 SplitType 动态创建对应的 DocumentSplitter: | 切片策略 | 实现类 | 适用场景 | | --- | --- | --- | | LENGTH | DocumentByWordSplitter | 纯文本按字数切分 | | TITLE | MarkdownHeaderParentTextSplitter | Markdown 按标题层级切分,保留父子关系 | | REGEX | DocumentByRegexSplitter | 自定义正则表达式切分 | | SEPARATOR | DocumentByRegexSplitter | 按指定分隔符切分 | | SMART | MarkdownHeaderParentTextSplitter | 智能切分,自动计算 overlap(chunkSize × 10%) |

Excel/CSV 类型文档则独立走 ExcelSplitter 处理,不经过工厂模式。前端 upload.html 根据选择的切片方式动态显示/隐藏参数组(overlap、titleLevel、regex、separator),实现配置与策略的联动。

🔥 查询改写异步回写 — 虚拟线程不阻塞主流程

KnowEngineQueryTransformer 在完成查询改写后,通过 Thread.ofVirtual().name("query-transform-" + chatMessageId).start(...) 将改写结果异步回写到 chat_message 表的 transformContent 字段。回写操作与主流程完全解耦:即使回写失败,也不影响改写后的 Query 继续进入检索和生成环节。这一设计使得查询改写的 4 维策略(简洁改写、抽象概念改写、错别字纠正、车型信息标准化)在提升检索质量的同时,对端到端延迟零贡献。

设计原则

  • 事件驱动优于直接调用:文档处理流程通过 Spring Event 解耦,Controller 瘦身,职责清晰;权限变更同样通过事件驱动批量更新 ES 索引;权限变更同样通过事件驱动批量更新 ES 索引
  • 最终一致性:事件驱动 + XXL-Job 补偿,“异步优先,定时兜底”
  • 流式优先:全链路 Reactor 响应式,SSE 逐 Token 推送,进度可见;多渠道输出(Web/钉钉)共享同一流式管道;多渠道输出(Web/钉钉)共享同一流式管道
  • 声明式并发控制:@DistributeLock 注解一行搞定幂等保护
  • 本地推理优先:Reranking 在 JVM 进程内完成,省去网络往返,降低延迟
  • 虚拟线程优先:所有 LLM 辅助调用(标题生成、查询改写回写)均使用虚拟线程,零阻塞主流程
  • 容错优先:LLM JSON 输出修复管道、缓存空值防击穿、标题生成失败保留临时标题、Text2Cypher 降级回退 ES,处处留有余地
  • 分布式 ID 全链路:雪花算法生成的 ChunkId 贯穿切片 → 存储 → 检索 → 扩展,全局唯一可追溯;版本链路通过 parentVersionDocId 关联,时序可追溯
  • 权限内建:权限信息从文档写入切片 metadata → ES 索引 → 检索 Filter,全链路内建,变更实时生效 ```
版本提示

模型、框架与接口会持续变化。涉及版本号、参数与生产配置时,请在实践前对照对应官方文档。

LLMentor系统化学习大模型应用工程

内容来自个人课程知识库备份,并经过结构化整理。技术版本持续演进,生产使用前请结合官方文档验证。