From 50fc660d08bb7e762a06f8204674cc28fc48d51a Mon Sep 17 00:00:00 2001 From: wei-py Date: Mon, 14 Sep 2026 14:13:29 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E5=A5=91=E7=BA=A6=E4=B8=8E=E7=AE=A1=E9=81=93?= =?UTF-8?q?=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 48 ++++++++++++++++++++------------------ SDK-INTEGRATION.md | 58 ++++++++++++++++++++++++++++++++++++++++------ client/README.md | 32 ++++++++++++++++++++----- 3 files changed, 103 insertions(+), 35 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e4de5a1..ef83091 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` 生成 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` 配置,启动时自动检测不匹配并告警** diff --git a/SDK-INTEGRATION.md b/SDK-INTEGRATION.md index 9990ca8..f5315ba 100644 --- a/SDK-INTEGRATION.md +++ b/SDK-INTEGRATION.md @@ -276,9 +276,10 @@ POST /open-api/auth/token | 接口 | 方法 | 说明 | |---|---|---| -| `/ai/chat` | GET | 同步对话(已统一为 ChatPipeline,支持普通/RAG 自动判断) | -| `/ai/chat/stream` | GET | SSE 流式对话(OpenAI Chat Completions 兼容格式,已统一为 ChatPipeline) | -| `/ai/chat/sources` | GET | 获取 RAG 引用来源 | +| `/ai/chat/result` | GET | SDK/管理后台同步对话,直接返回完整 JSON 结果(正文、工具事件、建议、实际引用) | +| `/ai/chat` | GET | 已发布的外部纯文本同步接口,继续保留;SDK 不回退到此接口 | +| `/ai/chat/stream` | GET | SSE 流式对话(OpenAI Chat Completions 兼容格式,流内携带实际引用) | +| `/ai/chat/sources` | GET | 已发布的独立显式检索接口,继续保留;对话客户端不调用此接口二次检索 | | `/ai/sdk/conversation/list` | GET | 会话列表 | | `/ai/sdk/conversation/{id}/messages` | GET | 会话消息 | | `/ai/sdk/conversation/{id}` | DELETE | 删除会话 | @@ -286,7 +287,48 @@ POST /open-api/auth/token | `/category/tree` | GET | 知识库分类树 | | `/feedback` | POST | 消息反馈 | -> **已废弃**:`/ai/assistant_app/chat/server_sent_event` 和 `/ai/assistant_app/chat/sse_emitter` 已移除。旧路径 `/ai/assistant_app/chat/sync`、`/ai/assistant_app/chat/sse`、`/ai/assistant_app/chat/rag/sse`、`/ai/assistant_app/rag/sources` 仍保留(向后兼容)但已废弃,请统一使用 `/ai/chat`、`/ai/chat/stream`、`/ai/chat/sources`。 +> **已废弃**:`/ai/assistant_app/chat/server_sent_event` 和 `/ai/assistant_app/chat/sse_emitter` 已移除。旧路径 `/ai/assistant_app/chat/sync`、`/ai/assistant_app/chat/sse`、`/ai/assistant_app/chat/rag/sse`、`/ai/assistant_app/rag/sources` 仍保留(向后兼容)但已废弃。自有对话客户端统一使用 `/ai/chat/result`、`/ai/chat/stream`,引用复用同次回答的实际命中,不额外发起检索。 + +#### 同步结果与引用契约 + +`GET /ai/chat/result` 与 `/ai/chat` 使用完全相同的参数:`message`(必填)、`chatId`、`roleId`、`accountId`、`systemPrompt`、`enableRag`、`rewriteStrategy`、`categoryId`、`categoryIds`、`imageUrls`。图片仍按逗号分隔并解码 URL;角色提示词、MCP 工具与分类权限仍由服务端解析,客户端不能用分类参数覆盖角色范围。`/ai/**` 由 SDK JWT / 管理后台 JWT 鉴权守卫。 + +响应直接是 JSON(**没有** `success/data` 外层): + +```json +{ + "text": "回答正文", + "mcpEvents": [], + "suggestions": [], + "sources": [{ + "documentId": "1960000000000000001", + "title": "退货说明", + "sourceName": "售后手册.pdf", + "chunkIndex": 0, + "score": 0.12, + "snippet": "实际用于本次回答的知识库片段" + }] +} +``` + +`sources` 只来自本次授权检索后注入答案的命中文档。每项固定包含 `documentId: string|null`、`title: string|null`、`sourceName: string|null`、`chunkIndex: number|null`、`score: number|null`、`snippet: string|null`。ID 始终为字符串,不能转为 JavaScript Number;snippet 含截断标记在内最多 160 字符。score 保留原来源接口的 distance 语义(检索实现只提供 score 时使用 score),不要一律假定越大越相关。FAQ、无 RAG、熔断或错误降级返回 `sources: []`。推荐问题仍按需另行生成。 + +#### 流内引用(同时适用于 Open API) + +`/ai/chat/stream` 和 `/open-api/chat/stream` 保留 OpenAI envelope。正常正文结束后、`finish_reason: "stop"` 与 `[DONE]` **之前**恰好追加一条扩展元数据 chunk: + +```text +data: {"id":"chatcmpl-example","object":"chat.completion.chunk","created":1789344000,"model":"configured-model","choices":[{"index":0,"delta":{"content":"回答正文"},"finish_reason":null}]} + +data: {"id":"chatcmpl-example","object":"chat.completion.chunk","created":1789344000,"model":"configured-model","choices":[],"sources":[]} + +data: {"id":"chatcmpl-example","object":"chat.completion.chunk","created":1789344000,"model":"configured-model","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} + +data: [DONE] + +``` + +上例为无引用的回答;RAG 回答的 `sources` 使用上述相同结构。所有 chunk 共享 `id/created/model`。引用元数据不是正文,不得拼入回答;通用 OpenAI 客户端可忽略该扩展字段。完整 `[DONE]` 事件到达即结束读取,无需等待网络 EOF;主动取消或异常不会重跑生成/检索。首条仅含 `role` 的协议帧不是答案首 token。 ### 6.3 Open API 对话接口(第三方系统直接调用) @@ -301,10 +343,12 @@ POST /open-api/auth/token | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `categoryIds` | Query | 否 | 知识库分类 ID,逗号分隔(非角色场景用) | -| `rewriteStrategy` | Query | 否 | RAG 查询重写策略:REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY(默认) | +| `rewriteStrategy` | Query | 否 | NONE(默认,原问题直接检索)/ REWRITE / TRANSLATION / COMPRESSION / MULTI_QUERY | | `enableRag` | Query | 否 | 是否启用 RAG 检索(默认 true),false 时走普通对话 | -**认证方式:** 所有 `/open-api/**` 和 `/ai/**` 请求通过 `X-API-Key` Header 或 Bearer Token 鉴权。 +`POST /open-api/chat` 保留 `success/data` 包装与 `data.reply`、`data.chatId`,并在 `data` 内增加 `mcpEvents`、`suggestions`、`sources`,与同步完整结果使用相同引用结构。 + +**认证方式:** `/open-api/**` 通过 API Key 鉴权;`/ai/**` 使用 `Authorization: Bearer `,不能直接用 API Key 代替 JWT。 ### 6.3 管理接口(需要管理后台 JWT) @@ -342,7 +386,7 @@ POST /open-api/auth/token | `theme` | String | 'light' | 界面主题:light / dark | | `streaming` | Boolean | true | 流式回复 | | `enableRag` | Boolean | true | RAG 知识库检索 | -| `rewriteStrategy` | String | 'REWRITE' | 查询重写策略 | +| `rewriteStrategy` | String | 'NONE' | 默认不调用 LLM 预处理;可显式选择查询重写策略 | | `quickReplies` | String[] | [] | 快捷问题列表 | | `showClear` | Boolean | true | 显示清空按钮 | | `showAdminPanel` | Boolean | false | 显示管理入口 | diff --git a/client/README.md b/client/README.md index f772624..9eed265 100644 --- a/client/README.md +++ b/client/README.md @@ -78,7 +78,8 @@ SDK 产物位于 `client/dist/` 目录: | `requestDomain` | `string` | ✅ | — | P0 | 后端 API 域名 | | `userId` | `string` | ❌ | — | P0 | 宿主用户标识 → 后端 `accountId` | | `roleId` | `number` | ❌ | — | P0 | 客服角色 ID | -| `enableRag` | `boolean` | ❌ | `false` | P1 | 启用 RAG 知识库检索对话(走 `/ai/chat/stream` 接口,`enableRag=true`) | +| `enableRag` | `boolean` | 可选 | `true` | P1 | 同步和流式对话均启用 RAG 知识库检索 | +| `rewriteStrategy` | `string` | 可选 | `"NONE"` | P1 | 默认不调用查询重写模型;显式支持 `REWRITE` / `TRANSLATION` / `COMPRESSION` / `MULTI_QUERY` | | `categoryId` | `number` | ❌ | — | P1 | 默认知识库分类 | | `showCategorySwitch` | `boolean` | ❌ | `false` | P1 | 是否显示知识库下拉切换 | | `title` | `string` | ❌ | `"AI 智能助手"` | P0 | 弹窗标题 | @@ -92,7 +93,7 @@ SDK 产物位于 `client/dist/` 目录: | `theme` | `string` | ❌ | `"light"` | P2 | 主题模式:`"light"` / `"dark"` | | `showTeaser` | `boolean` | ❌ | `true` | P1 | 首访提示气泡(延迟 1.5s 弹出) | | `teaserText` | `string` | ❌ | i18n 默认 | P1 | 提示气泡文字,留空使用语言包默认值 | -| `streaming` | `boolean` | ❌ | `true` | P0 | 是否启用 SSE 流式输出 | +| `streaming` | `boolean` | 可选 | `true` | P0 | `true` 使用 SSE;`false` 使用 `/ai/chat/result` 获取完整 JSON 答案及同次来源 | | `locale` | `string` | ❌ | `"zh-CN"` | P2 | 界面语言:`zh-CN` / `en` | | `debug` | `boolean` | ❌ | `true` | P0 | 是否输出调试日志 | @@ -118,7 +119,7 @@ SDK 产物位于 `client/dist/` 目录: 默认开启(`streaming: true`),AI 回复逐字输出,支持: - 流式追加到气泡,实时滚动到底部 - 流中断兜底:保留已接收内容 + 灰色提示 -- 无流内容时自动降级为同步请求 +- 收到完整 `[DONE]` 行立即结束并释放 reader,不等待服务器关闭连接;空流不再发起同步重试 ### 4.2 Markdown 渲染 @@ -187,7 +188,7 @@ ChatbotSDK.init({ - 默认折叠,只显示标题行,点击展开/折叠 - 显示文档名称、摘要、来源文件、分块编号、相关度 -- 来源数据从 `/ai/chat/sources` 接口获取 +- 来源来自本次答案实际命中的文档,与正文使用同一个请求;SDK 不再调用 `/ai/chat/sources` 做二次检索 --- @@ -247,18 +248,37 @@ SDK 全流程结构化日志,带 `[ChatbotSDK]` 前缀: ### P0 — 基础对话 ``` -GET /ai/chat # 同步对话 +GET /ai/chat/result # 同步 JSON 对话(streaming=false) GET /ai/chat/stream # SSE 流式对话 ``` ### P1 — 知识库联动 ``` GET /ai/chat/stream # RAG 增强流式对话(enableRag=true) -GET /ai/chat/sources # RAG 引用来源 GET /category/tree # 分类树(下拉框数据源) GET /category/list # 分类列表 ``` +### 答案与来源协议 + +默认 `rewriteStrategy: 'NONE'`,主链不再使用 LLM 意图分类,也不默认做查询重写;本地寒暄和 FAQ 匹配仍有效。用户显式配置其他重写策略时保持该策略,不强制覆盖。 + +同步 `/ai/chat/result` 直接返回 JSON(没有 `success/data` 包装): + +```json +{"text":"答案正文","mcpEvents":[],"suggestions":[],"sources":[]} +``` + +流式 `/ai/chat/stream` 继续使用 OpenAI `chat.completion.chunk` envelope。正文完成后、`finish_reason: "stop"` / `[DONE]` 之前发送一次元数据 chunk: + +```json +{"id":"chat-id","object":"chat.completion.chunk","created":1700000000,"model":"model-name","choices":[],"sources":[{"documentId":"1234567890123456789","title":"文档标题","sourceName":"manual.pdf","chunkIndex":0,"score":0.9,"snippet":"160 字以内摘要"}]} +``` + +`sources` 各字段允许 `null`;`documentId` 始终为字符串以保持雪花 ID 精度。FAQ、无 RAG、熔断答案返回空数组。SDK 仅将 `choices[].delta.content` 渲染为正文,来源扩展字段交给对应消息的来源卡片,未知 OpenAI 客户端可忽略该扩展。 + +解析器支持 UTF-8 分片、CRLF、多行 data 与 EOF 残留;`[DONE]` 终止整个流且完成回调只执行一次。取消或切换会话不会把旧请求引用写入新消息。原 `/ai/chat` 文本接口和 `/ai/chat/sources` 显式检索接口仍是后端发布 API,但不是 SDK 对话回退路径。 + ### P2 — 会话管理 ``` GET /conversation/list # 会话列表