|
|
|
@ -16,18 +16,18 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 |
|
|
|
./mvnw spring-boot:run |
|
|
|
|
|
|
|
# 运行测试 |
|
|
|
./mvnw test |
|
|
|
sh ./mvnw test -DskipTests=false |
|
|
|
|
|
|
|
# 运行单个测试类 |
|
|
|
./mvnw test -Dtest=SupportBotApplicationTests |
|
|
|
sh ./mvnw test -DskipTests=false -Dtest=SupportBotApplicationTests |
|
|
|
|
|
|
|
# 运行单个测试方法 |
|
|
|
./mvnw test -Dtest=SupportBotApplicationTests#testRag |
|
|
|
sh ./mvnw test -DskipTests=false -Dtest=SupportBotApplicationTests#testRag |
|
|
|
``` |
|
|
|
|
|
|
|
**前提条件**: PostgreSQL 12+ 需运行且安装 PGVector 扩展,数据库 `support_bot` 需存在。`knowledge_category`、`knowledge_document`、`ai_model_config` 等表由 `DatabaseInitConfig` 自动创建,无需手动建表。 |
|
|
|
|
|
|
|
**测试说明**: `@SpringBootTest` 集成测试需要运行中的 PostgreSQL 和有效的 DashScope API Key;`ChatPipelineTests` 为隔离的 Mockito 编排测试,无需 DB 或 API Key。Surefire 默认跳过测试,执行时显式加 `-DskipTests=false`,例如 `./mvnw test -DskipTests=false -Dtest=ChatPipelineTests`。 |
|
|
|
**测试说明**: `@SpringBootTest` 集成测试需要运行中的 PostgreSQL 和有效的 DashScope API Key。低延迟契约测试无需 DB 或真实 API Key:`ChatPipelineTests` 覆盖 FAQ/分类隔离/查询策略,`ChatModelFactoryTests` 用本地 HTTP 服务验证实际模型请求,`AnswerTransportTests` 覆盖同次引用与流生命周期,`ChatResultEndpointTests` 覆盖同步接口与权限。Surefire 默认跳过测试,运行这些测试使用 `sh ./mvnw test -DskipTests=false -Dtest=ChatPipelineTests,ChatModelFactoryTests,AnswerTransportTests,ChatResultEndpointTests`。前端流协议测试:`node --test frontend/tests/chat-protocol.test.mjs`;SDK 传输与会话生命周期测试:在 `client/` 运行 `npm exec -- vitest run tests/api.test.ts tests/chat.test.ts`。 |
|
|
|
|
|
|
|
**访问地址**: 前端管理页面 `http://localhost:9090/index.html`,API 文档 `http://localhost:9090/doc.html`(Knife4j) |
|
|
|
|
|
|
|
@ -46,29 +46,33 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 |
|
|
|
当前使用 `DatabaseChatMemory`(PostgreSQL 持久化),无文件型 ChatMemory(早期的 `FileBasedChatMemory` 已删除,Kryo 依赖一并移除)。 |
|
|
|
|
|
|
|
### 统一对话管道(重构后) |
|
|
|
对话管道由 `ChatPipeline`(编排层)+ `RagPipeline`(RAG 检索层)+ `AssistantApp`(执行层)组成: |
|
|
|
对话管道由 `ChatPipeline`(编排层)+ `RagPipeline`(RAG 检索层)+ `AssistantApp`(执行层)组成(更新:2026-09-14,默认零 LLM 预处理): |
|
|
|
|
|
|
|
``` |
|
|
|
用户请求 |
|
|
|
→ 鉴权/角色解析(Controller) |
|
|
|
→ AssistantApp 熔断检查(熔断直接降级,不执行 FAQ / 检索) |
|
|
|
→ ChatPipeline.buildRequest(ChatContext) |
|
|
|
→ enableRag=false:普通对话(不调用 FAQ / 意图路由) |
|
|
|
→ enableRag=true:完整 FAQ 三级匹配(角色分类隔离,命中直接返回,跳过意图分类) |
|
|
|
→ 未命中:寒暄词快速路径 / IntentRouter 意图路由(CHITCHAT/FAQ/RAG) |
|
|
|
→ 高置信 CHITCHAT:纯对话 |
|
|
|
→ 其余:RagPipeline.retrieve(FAQ 异常时重试 → 查询重写 → 统一检索) |
|
|
|
→ 组装 finalMessage + finalSystemPrompt + 资料块 |
|
|
|
→ AssistantApp.chat / chatStream(构建 ChatClientRequestSpec → call/stream) |
|
|
|
→ enableRag=false:普通对话(不调用 FAQ / 检索) |
|
|
|
→ enableRag=true:完整 FAQ 三级匹配(角色分类隔离,命中直接返回) |
|
|
|
→ 未命中:本地寒暄词判断(无意图分类 LLM) |
|
|
|
→ 寒暄命中:纯对话 |
|
|
|
→ 其余:RagPipeline.retrieve(仅 FAQ 异常时重试 → 默认 NONE 原文检索) |
|
|
|
→ 显式 REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY 才调用重写 LLM |
|
|
|
→ 统一向量检索 → 命中/未命中日志仅记录一次 |
|
|
|
→ 组装 finalMessage + finalSystemPrompt + 资料块 + 当次命中文档 |
|
|
|
→ AssistantApp.chat / chatStream(安全检查 + 会话记忆 + 一次答案生成;FAQ 不生成) |
|
|
|
→ 同步 JSON / SSE metadata 返回当次 sources(不二次检索) |
|
|
|
``` |
|
|
|
|
|
|
|
- **ChatPipeline**: 纯编排,不持有 ChatClient;产出 `ChatRequest` 决策对象。启用 RAG 时先做完整 FAQ 匹配,标准答案命中不调用意图 LLM;仅 `completedCleanly=true` 的未命中允许 `retrieve(ctx, true)` 跳过重复 FAQ,异常未命中仍保留 RAG 中的 FAQ 重试 |
|
|
|
- **RagPipeline**: 统一 RAG 检索,所有策略(含 MULTI_QUERY)均走"手动检索 + 资料块注入 system prompt"模式,不再使用 `RetrievalAugmentationAdvisor` 的 query augmenter |
|
|
|
- **RAG 查询重写策略**: 由 `RagPipeline` 统一路由,`AssistantApp` 等旧方法已移除 |
|
|
|
- **IntentRouter**: 已在 ChatPipeline 接入,`AiController.shouldBypassKnowledgeRetrieval` 已移除。`doubao-seed-2-0-*` 的意图分类请求单独设置 `reasoning_effort=minimal`(关闭深度思考)和 `max_tokens=128`,避免小型分类任务先等待长思维链;只覆盖当前分类请求,不修改缓存模型或主回答配置,其他模型保持原参数。请求序列化及降级由 `IntentRouterTests` 验证 |
|
|
|
- **ChatPipeline**: 纯编排,不持有 ChatClient;产出 `ChatRequest` 决策对象。启用 RAG 时先做完整 FAQ 匹配,命中直接返回;未命中仅做本地寒暄判断,其他问题直接检索。仅 `completedCleanly=true` 的未命中允许 `retrieve(ctx, true)` 跳过重复 FAQ,异常未命中仍保留 RAG 中的 FAQ 重试 |
|
|
|
- **RagPipeline**: 统一 RAG 检索,所有策略(含 MULTI_QUERY)均走"手动检索 + 资料块注入 system prompt"模式;检索命中/未命中日志仅由此层记录,不在编排层重复写入 |
|
|
|
- **RAG 查询重写策略**: 默认 `NONE`,省去串行意图分类和查询重写 LLM 调用。显式 `REWRITE` / `TRANSLATION` / `COMPRESSION` / `MULTI_QUERY` 保留;`COMPRESSION` 从会话记忆读取最近 10 条消息,将指代追问补全为独立检索问题。默认原文检索不做指代补全,主回答仍通过 `MessageChatMemoryAdvisor` 使用会话记忆 |
|
|
|
- **本地寒暄路径**: `ChatPipeline.isChitchat` 复用于对话和独立来源检索;不使用意图分类 LLM |
|
|
|
- **分类过滤**: 统一由 `CategoryFilter` 工具类处理(`parse`/`normalize`/`buildExpression`) |
|
|
|
- **AssistantApp 入口**: `chat(ChatContext)` / `chatStream(ChatContext)` / `retrieveSources(ChatContext)`,旧方法(`doChat*`、`doChatWithRag*`)已移除 |
|
|
|
- **Open API**: `OpenApiController` 已接入 `ChatPipeline`,补齐角色/RAG/FAQ/MCP/分类隔离能力 |
|
|
|
- **正文完成与引用加载解耦**: 管理聊天页及 SDK 测试页在正文完成后立即解除发送状态,引用来源后台补齐到原消息;请求参数固定为发送时的会话/角色/检索配置,清空或切换后的旧引用不会写入新对话 |
|
|
|
- **引用复用**: 自有客户端不调用 `/ai/chat/sources`。同步 `/ai/chat/result` 直接返回 `{text,mcpEvents,suggestions,sources}`;流式在正文完成后、stop / `[DONE]` 前发送一次 `choices:[]` + `sources` 的 OpenAI 扩展 metadata chunk,FAQ/非 RAG/熔断来源为空。已发布 `/ai/chat` 文本和 `/ai/chat/sources` 独立检索接口保留 |
|
|
|
|
|
|
|
### 文档处理管道 |
|
|
|
`DocumentService.uploadDocument()` 统一流程:文档提取(官方 `org.springframework.ai.reader.tika.TikaDocumentReader` / `MarkdownDocumentReader` / `JsonReader`)→ `OverlapTokenTextSplitter` 分块 → 为每块写 metadata → 按批向量化(默认 50 块/批,配置项 `knowledge.vector.batch-size`)`pgVectorVectorStore.add(batch)` 入库。每个分块的 metadata 注入 `documentId`、`chunkIndex`、`sourceName`、`title`、`categoryId`、`enabled` 关联 `knowledge_document` 表。 |
|
|
|
@ -117,6 +121,7 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 |
|
|
|
- **启动校验**: `ModelConfigLoader` 在应用就绪后检查 DB 中每种 App 类型是否有活跃配置,并对 DashScope 提供商比较 DB 与 yml 的 API Key 一致性 |
|
|
|
- **API Key 脱敏**: 前端展示时只显示前 4 位 + `****` + 后 4 位 |
|
|
|
- **ChatModel 运行时切换**: 通过 `ChatModelFactory` 按 DB 活跃配置动态创建/缓存 ChatModel(包括 DashScope,不再复用 yml 自动配置的 Bean),配置变更时立即生效(无需重启)。**OpenAI 兼容路径使用自定义 `completionsPath`**(与 EmbeddingModelFactory 的 embeddingsPath 对应),baseUrl 已含版本段的厂商(moonshot `/v1`、volcengine `/api/v3`、zhipu `/api/paas/v4`)设为 `/chat/completions`,其余使用默认 `/v1/chat/completions` |
|
|
|
- **Seed 2.0 思考策略**: `volcengine` + `doubao-seed-2-0-*` 的 `CHAT` 默认 `reasoning_effort=minimal`(关闭思考);高级参数 `extraConfig.reasoningEffort` 可显式设为 `minimal/low/medium/high`,清空恢复 CHAT 快速默认。`RAG_REWRITE` 留空及其他模型沿用厂商默认;工厂白名单仅作用于 Seed 2.0,前端编辑保留其他 `extraConfig` 字段 |
|
|
|
- **EmbeddingModel 运行时切换**: 通过 `EmbeddingModelFactory` + `DynamicEmbeddingModel` 代理,按 DB 活跃配置动态创建/缓存 EmbeddingModel,`PgVectorStoreConfig` 和 `InMemoryVectorStoreConfig` 注入 `DynamicEmbeddingModel`,向量化模型配置变更后无需重启即可生效 |
|
|
|
- **多提供商支持**: DashScope(通义千问)+ OpenAI 兼容提供商(DeepSeek / 豆包 / Kimi / 智谱 / OpenAI),ChatModel 和 EmbeddingModel 均通过对应 API 手动构建 |
|
|
|
- **缓存刷新**: 模型配置增删改激活时 Controller 自动调用 `ChatModelFactory.clearCache()` + `EmbeddingModelFactory.clearCache()` + `AssistantApp.clearCache()`;MCP Server 增删改/启停/全量刷新时 `McpServerConfigController` 亦会调用 `AssistantApp.clearCache()`(避免继续使用旧的 MCP 工具集) |
|
|
|
@ -275,7 +280,7 @@ catch (e) { toast('操作失败', 'error') } |
|
|
|
- `ChatPipeline.buildRequest()` 的决策分支(意图路由、FAQ、RAG、纯对话路径)发生变更 |
|
|
|
- `RagPipeline.retrieve()` 的检索流程(查询重写策略、检索方式、资料拼装)发生变更 |
|
|
|
- `AssistantApp` 的 Advisor 链成员或顺序发生变更(如新增/移除 Advisor) |
|
|
|
- 新增管道阶段组件(如 `IntentRouter`、`SuggestionGenerator`、`SimpleCircuitBreaker` 等)或移除现有组件 |
|
|
|
- 新增管道阶段组件(如 `SuggestionGenerator`、`SimpleCircuitBreaker` 等)或移除现有组件 |
|
|
|
- 组件间调用关系调整(如原来 A→B 改为 A→C→B) |
|
|
|
|
|
|
|
**图表元数据**: `PipelineFlow.vue` 中 DSL 首行有 `%%graph-meta` 注释标记最后更新时间,修改图表时必须更新该日期。 |
|
|
|
@ -345,8 +350,8 @@ catch (e) { toast('操作失败', 'error') } |
|
|
|
- **Chat SDK**: `handleFeedback()` 已连接后端 API,同时保留 localStorage 作为乐观 UI 缓存 |
|
|
|
- **会话导出**: `ConversationService.exportConversation()` 导出的 TXT 中包含反馈信息 |
|
|
|
|
|
|
|
### 意图识别 + FAQ 精准匹配(P0-003) |
|
|
|
- **IntentRouter**: LLM 单次调用做意图分类(FAQ/RAG/CHITCHAT);结构化输出由标准组件 `BeanOutputConverter<IntentResult>` 生成 JSON Schema 指令并反序列化结果,解析失败/结果非法降级为 RAG |
|
|
|
### 本地路由 + FAQ 精准匹配(P0-003) |
|
|
|
- **本地路由**: 完整 FAQ 优先,未命中后仅精确寒暄词跳过 RAG;默认不调用 LLM 做意图分类 |
|
|
|
- **FaqMatchEngine**: 三级匹配策略 — 精确匹配 → 关键词匹配 → 向量语义匹配(阈值 `knowledge.faq.semantic-threshold`,默认 0.85) |
|
|
|
- **FAQ 向量化**: 复用现有 `DynamicEmbeddingModel`,向量存入 `faq_embedding` 表,新增/修改 FAQ 时异步计算 |
|
|
|
- **similar_questions 字段**: 使用 String 类型存储 JSON 数组字符串(PostgresJsonTypeHandler 期望对象格式,故不用 typeHandler) |
|
|
|
@ -367,12 +372,11 @@ catch (e) { toast('操作失败', 'error') } |
|
|
|
- **异步写入**:`LlmCallTraceService.recordAsync` 走 `@Async("traceExecutor")`(有界线程池见 `AsyncExecutorConfig`,替代默认 `SimpleAsyncTaskExecutor` 线程爆炸隐患;`RagHitLogService` 已一并切换)。 |
|
|
|
- **查询面板**:前端「系统设置 → 提示词追踪」(`PromptTracePanel.vue`,admin),支持筛选/搜索/详情(来源标注)/聚合统计/并排对比/跳转编辑(deep-link `?roleId=`、`?key=ai_system_prompt`)。 |
|
|
|
- **安全声明**:`system_prompt`/`user_message`/`ai_response` 属敏感数据(提示词为商业秘密),当前**明文存储**、仅 admin 可查,落库前做敏感词脱敏(`ContentSafetyService.mask`)+ AI 回复截断;保留期限由 `system_config.llm_trace_retention_days` 控制(默认 30 天,`@Scheduled` 每日 2 点自动清理)。字段级加密留二期。 |
|
|
|
- **预留**:`account_id`/`api_key_id` 列预留租户隔离与个人信息删除权,本期不写入;旁路 LLM 调用(意图识别/查询重写/建议生成)追踪留二期(表加 `trace_type`/`source` 字段)。 |
|
|
|
- **预留**:`account_id`/`api_key_id` 列预留租户隔离与个人信息删除权,本期不写入;旁路 LLM 调用(显式查询重写/建议生成)追踪留二期(表加 `trace_type`/`source` 字段)。 |
|
|
|
|
|
|
|
## 已知 TODO |
|
|
|
|
|
|
|
- `DocumentService.updateDocumentMetadata()`: Spring AI 无直接更新 vector_store metadata 的 API,向量元数据同步留后续 |
|
|
|
- `DocumentService.searchDocuments()`: **分类过滤已实现**(`FilterExpressionBuilder` 组合 `enabled` + `categoryId` 过滤表达式,向量检索异常时回退到本地 metadata 过滤)—— 原「Spring AI filter 支持有限」的 TODO 已不成立 |
|
|
|
- `CompressionQueryRewriter`: 当前传入空历史列表 |
|
|
|
- MyBatis Plus `mybatis-plus-spring-boot3-starter` 不含 `PaginationInnerInterceptor`,分页通过 SQL `LIMIT/OFFSET` 手动实现 |
|
|
|
- `PgVectorStoreConfig.dimensions(1024)` 硬编码了向量维度,切换非 1024 维的 Embedding 模型时需修改并重建 vector_store 表 → **已修复:维度由 `knowledge.vector.dimension` 配置,启动时自动检测不匹配并告警** |