# Chat API 返回内容区分设计文档 ## 概述 本文档描述如何将 Chat API 的返回内容区分为"上文"、"引用"、"正文"、"深度思考"四个部分。 ## 当前实现分析 ### 现有返回结构 ```typescript { type: 'content' | 'sources' | 'historyId'; data: any } ``` ### 现有问题 1. **content 类型过于笼统**:混合了搜索状态、调试信息、AI正文等多种内容 2. **缺少上文返回**:历史对话被格式化后传给LLM,但没有返回给客户端 3. **缺少深度思考**:没有支持 reasoning/thinking 过程的返回 ## 设计方案 ### 新的返回类型定义 ```typescript export type ChatStreamType = | 'history' // 上文:历史对话内容 | 'sources' // 引用:搜索结果来源 | 'content' // 正文:AI生成的回答 | 'thinking' // 深度思考:推理过程 | 'status' // 状态:搜索进度、调试信息 | 'historyId'; // 历史ID:聊天会话ID export interface ChatStreamChunk { type: ChatStreamType; data: any; } ``` ### 各类型详细说明 #### 1. history - 上文 **用途**:返回历史对话内容,让客户端可以显示上下文 **数据结构**: ```typescript { type: 'history', data: { messages: Array<{ role: 'user' | 'assistant'; content: string; }>; } } ``` **触发时机**:在聊天开始时返回 #### 2. sources - 引用 **用途**:返回搜索结果来源,包含文件名、内容片段、相似度分数 **数据结构**: ```typescript { type: 'sources', data: Array<{ fileName: string; content: string; score: number; chunkIndex: number; fileId: string; }> } ``` **触发时机**:在搜索完成后、生成正文前返回 #### 3. content - 正文 **用途**:返回AI生成的回答内容(纯文本) **数据结构**: ```typescript { type: 'content', data: string // 流式返回的文本片段 } ``` **触发时机**:LLM生成过程中持续返回 #### 4. thinking - 深度思考 **用途**:返回AI的推理过程(如Chain-of-Thought) **数据结构**: ```typescript { type: 'thinking', data: string // 流式返回的思考过程 } ``` **触发时机**:在正文之前返回(如果模型支持) #### 5. status - 状态 **用途**:返回搜索进度、调试信息等 **数据结构**: ```typescript { type: 'status', data: { stage: 'searching' | 'search_complete' | 'generating' | 'debug'; message: string; details?: any; } } ``` **触发时机**:在整个过程中穿插返回 #### 6. historyId - 历史ID **用途**:返回聊天会话ID **数据结构**: ```typescript { type: 'historyId', data: string // 会话ID } ``` **触发时机**:在聊天开始时返回 ## 需要修改的文件 ### 后端文件 1. **server/src/chat/chat.service.ts** - 修改 `streamChat` 方法的返回类型 - 区分不同类型的 content - 添加 history 类型返回 - 添加 thinking 类型支持 2. **server/src/api/api-v1.controller.ts** - 更新 SSE 响应格式 - 确保所有类型都被正确处理 3. **server/src/types.ts** - 添加新的类型定义 ### 前端文件 4. **web/services/chatService.ts** - 更新流式响应处理逻辑 - 区分不同类型的 chunk 5. **web/components/ChatInterface.tsx** 或相关组件 - 更新UI渲染逻辑 - 分别显示上文、引用、正文、深度思考 ## 实现步骤 ### 第一阶段:类型定义 1. 在 `server/src/types.ts` 中添加新的类型定义 2. 更新 `ChatStreamChunk` 接口 ### 第二阶段:后端实现 3. 修改 `chat.service.ts` 中的 `streamChat` 方法 - 添加 history 类型返回 - 将 content 细分为 status、thinking、content - 保持 sources 不变 4. 更新 `api-v1.controller.ts` 确保正确处理新类型 ### 第三阶段:前端实现 5. 更新 `chatService.ts` 处理新的流式响应 6. 更新聊天界面组件,分别渲染不同类型的 chunk ## 兼容性考虑 - 保持向后兼容:旧的 `content` 类型仍然可用 - 新增类型是可选的,不影响现有功能 - 前端可以逐步适配新类型 ## 深度思考支持(健壮性设计) ### 设计原则 **不预判模型能力,而是动态检测响应内容** 优点: - 无需维护模型列表 - 自动适配任何新模型 - 对不支持的模型零开销 - 始终保持健壮性 ### 动态检测机制 #### 核心思路 在流式响应过程中,实时检测是否出现 thinking 标记: - 如果检测到 → 解析并返回 `thinking` + `content` - 如果未检测到 → 按普通 `content` 处理 #### 流式处理状态机 ```typescript enum StreamState { WAITING, // 等待首个 chunk IN_THINKING, // 正在接收 thinking 内容 IN_CONTENT, // 正在接收 content 内容 COMPLETED // 完成 } interface StreamProcessor { state: StreamState; buffer: string; // 缓冲区,用于检测标签 thinkingContent: string; // 累积的 thinking 内容 contentContent: string; // 累积的 content 内容 thinkingDetected: boolean; // 是否检测到 thinking } ``` #### 处理流程 ```typescript async function* processStream(stream: AsyncIterable<{content: string}>) { const processor: StreamProcessor = { state: StreamState.WAITING, buffer: '', thinkingContent: '', contentContent: '', thinkingDetected: false, }; for await (const chunk of stream) { processor.buffer += chunk.content; // 状态机处理 switch (processor.state) { case StreamState.WAITING: // 检测是否以 thinking 标签开头 if (processor.buffer.includes('')) { processor.state = StreamState.IN_THINKING; processor.thinkingDetected = true; // 提取 之后的内容 const parts = processor.buffer.split(''); processor.buffer = parts[1] || ''; } else if (processor.buffer.length > 100) { // 超过 100 字符仍未检测到 thinking,判定为普通内容 processor.state = StreamState.IN_CONTENT; yield { type: 'content', data: processor.buffer }; processor.buffer = ''; } break; case StreamState.IN_THINKING: // 检测 thinking 结束标签 if (processor.buffer.includes('')) { const parts = processor.buffer.split(''); processor.thinkingContent += parts[0]; processor.buffer = parts[1] || ''; processor.state = StreamState.IN_CONTENT; // 返回完整的 thinking 内容 yield { type: 'thinking', data: processor.thinkingContent }; // 如果 buffer 中有内容,作为 content 开始 if (processor.buffer) { yield { type: 'content', data: processor.buffer }; processor.buffer = ''; } } else { // 继续累积 thinking 内容 // 为了流式显示,可以分批返回 yield { type: 'thinking', data: chunk.content }; } break; case StreamState.IN_CONTENT: // 直接返回 content yield { type: 'content', data: chunk.content }; break; } } // 处理剩余 buffer if (processor.buffer) { if (processor.state === StreamState.IN_THINKING) { yield { type: 'thinking', data: processor.buffer }; } else { yield { type: 'content', data: processor.buffer }; } } } ``` ### 三种场景自动适配 #### 场景1:普通模型(不支持 thinking) **检测过程**: 1. 收到首个 chunk 2. 等待 100 字符仍未检测到 `` 3. 判定为普通内容 **返回**: - 仅 `content` 类型 - 无 `thinking` 类型 #### 场景2:Reasoning 模型但未触发 **检测过程**: 1. 收到首个 chunk 2. 等待 100 字符仍未检测到 `` 3. 判定为普通内容 **返回**: - 仅 `content` 类型 - 无 `thinking` 类型 #### 场景3:Reasoning 模型且触发 **检测过程**: 1. 收到首个 chunk 2. 检测到 `` 标签 3. 进入 thinking 状态 4. 收集直到 `` 5. 之后的内容作为 content **返回**: - `thinking` 类型(思考过程) - `content` 类型(正文) ### 多格式支持 支持多种 thinking 标签格式: ```typescript const THINKING_PATTERNS = { // 格式1: ... xml: { start: //i, end: /<\/thinking>/i, }, // 格式2: ```thinking\n...\n``` codeBlock: { start: /```thinking/i, end: /```/i, }, // 格式3: [THINKING]...[/THINKING] bracket: { start: /\[THINKING\]/i, end: /\[\/THINKING\]/i, }, }; // 自动检测使用哪种格式 function detectThinkingFormat(buffer: string): keyof typeof THINKING_PATTERNS | null { for (const [format, pattern] of Object.entries(THINKING_PATTERNS)) { if (pattern.start.test(buffer)) { return format as keyof typeof THINKING_PATTERNS; } } return null; } ``` ### Thinking 内容解析 支持多种 thinking 格式: ```typescript function extractThinking(response: string): { thinking: string; content: string } { // 格式1: ... const thinkingTagMatch = response.match(/([\s\S]*?)<\/thinking>/i); if (thinkingTagMatch) { const thinking = thinkingTagMatch[1].trim(); const content = response.replace(/[\s\S]*?<\/thinking>/i, '').trim(); return { thinking, content }; } // 格式2: ```thinking\n...\n``` const codeBlockMatch = response.match(/```thinking\n([\s\S]*?)\n```/i); if (codeBlockMatch) { const thinking = codeBlockMatch[1].trim(); const content = response.replace(/```thinking\n[\s\S]*?\n```/i, '').trim(); return { thinking, content }; } // 格式3: [THINKING]...[/THINKING] const bracketMatch = response.match(/\[THINKING\]([\s\S]*?)\[\/THINKING\]/i); if (bracketMatch) { const thinking = bracketMatch[1].trim(); const content = response.replace(/\[THINKING\][\s\S]*?\[\/THINKING\]/i, '').trim(); return { thinking, content }; } // 未检测到 thinking,返回原内容 return { thinking: '', content: response }; } ``` ### 前端健壮性处理 前端需要能够处理所有情况: ```typescript // 流式响应处理 function handleStreamChunk(chunk: ChatStreamChunk) { switch (chunk.type) { case 'history': setHistory(chunk.data.messages); break; case 'sources': setSources(chunk.data); break; case 'thinking': // 只有收到 thinking 类型时才显示深度思考区域 setThinking(prev => prev + chunk.data); setShowThinking(true); break; case 'content': setContent(prev => prev + chunk.data); break; case 'status': setStatus(chunk.data); break; } } // 渲染时的健壮性检查 function renderChat() { return (
{/* 上文:只有有内容时才显示 */} {history.length > 0 && } {/* 引用:只有有内容时才显示 */} {sources.length > 0 && } {/* 深度思考:只有有内容时才显示 */} {showThinking && thinking.length > 0 && ( )} {/* 正文:始终显示 */}
); } ``` ### 配置选项 在模型配置中添加 reasoning 支持选项: ```typescript // 模型配置扩展 interface ModelConfig { // ... 现有字段 // 新增:reasoning 配置 reasoning?: { enabled: boolean; // 是否启用 reasoning 解析 format?: 'auto' | 'thinking_tag' | 'reasoning_field'; // 格式检测方式 }; } ``` ### 降级策略总结 | 模型类型 | 是否支持 | 是否触发 | 返回类型 | 前端显示 | |----------|----------|----------|----------|----------| | 普通模型(如 GPT-4) | ❌ | - | content | 仅正文 | | Reasoning模型未触发 | ✅ | ❌ | content | 仅正文 | | Reasoning模型已触发 | ✅ | ✅ | thinking + content | 深度思考 + 正文 | ### 错误处理 ```typescript // 解析 thinking 时的错误处理 try { const { thinking, content } = extractThinking(fullResponse); if (thinking) { yield { type: 'thinking', data: thinking }; } yield { type: 'content', data: content }; } catch (error) { // 解析失败时,降级为普通 content logger.warn('Failed to parse thinking, falling back to content', error); yield { type: 'content', data: fullResponse }; } ``` ## 测试计划 ### 基础功能测试 1. 测试 history 类型正确返回历史对话 2. 测试 sources 类型正确返回引用来源 3. 测试 content 类型正确返回正文 4. 测试 status 类型正确返回状态信息 ### 深度思考健壮性测试 5. **测试普通模型**:使用 GPT-4 等不支持 reasoning 的模型 - 验证不返回 thinking 类型 - 验证只返回 content 类型 - 验证前端不显示深度思考区域 6. **测试 Reasoning 模型未触发**:使用 DeepSeek-R1 但提问简单问题 - 验证不返回 thinking 类型 - 验证只返回 content 类型 7. **测试 Reasoning 模型已触发**:使用 DeepSeek-R1 提问复杂问题 - 验证返回 thinking 类型 - 验证返回 content 类型 - 验证前端正确显示深度思考区域 8. **测试解析容错**:模拟 malformed thinking 标签 - 验证解析失败时降级为 content - 验证不抛出异常 ### 前端渲染测试 9. 测试前端正确渲染所有类型 10. 测试前端在没有 thinking 时不显示深度思考区域 11. 测试前端在有 thinking 时正确显示深度思考区域