From 2e9dfd20c7765e7a26e1ab0c3d189db1e0daae65 Mon Sep 17 00:00:00 2001 From: linshen <32978552+linshenkx@users.noreply.github.com> Date: Mon, 23 Jun 2025 21:54:34 +0800 Subject: [PATCH] =?UTF-8?q?feat(LLMService):=20=E9=87=8D=E6=9E=84=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E5=8F=91=E9=80=81=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=BB=93=E6=9E=84=E5=8C=96=E5=93=8D=E5=BA=94=E4=B8=8E?= =?UTF-8?q?think=E6=A0=87=E7=AD=BE=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 `LLMService` 类,新增结构化消息发送方法,返回包含推理内容的响应 - 增强错误处理,确保API调用失败时提供友好的错误提示 - 实现流式处理中的think标签支持,优化推理内容与正文的分离 - 更新相关接口与类型定义,确保兼容性与可读性 - 添加新的单元测试与集成测试,验证结构化响应与think标签处理的正确性 - 更新文档,说明新的API使用方式与最佳实践 --- docs/scratchpad.md | 38 +- packages/core/src/services/llm/service.ts | 268 +++++++++++++-- packages/core/src/services/llm/types.ts | 41 ++- packages/core/src/services/model/defaults.ts | 10 +- .../core/tests/integration/llm/openai.test.js | 324 +++++++++++++++++- .../core/tests/unit/llm/llmParams.test.ts | 9 +- .../unit/llm/think-tag-processing.test.ts | 133 +++++++ 7 files changed, 773 insertions(+), 50 deletions(-) create mode 100644 packages/core/tests/unit/llm/think-tag-processing.test.ts diff --git a/docs/scratchpad.md b/docs/scratchpad.md index d9b1c70d..c3ce7cac 100644 --- a/docs/scratchpad.md +++ b/docs/scratchpad.md @@ -110,4 +110,40 @@ const completionConfig: any = { --- **最后更新**: 2025年1月 -**文档状态**: 已简化,移除过时内容,保留核心技术要点 \ No newline at end of file +**文档状态**: 已简化,移除过时内容,保留核心技术要点 + +## 任务:LLM服务结构化响应重构审查 - 2024-01-XX + +### 代码审查建议 + +#### 1. Think标签处理器重构建议 +当前 `processStreamContentWithThinkTags` 方法过于复杂,建议创建独立的 `ThinkTagProcessor` 类: + +```typescript +class ThinkTagProcessor { + private isInThinkMode = false; + private buffer = ''; + + processChunk(content: string, callbacks: StreamHandlers): void { + // 简化的处理逻辑 + } + + reset(): void { + this.isInThinkMode = false; + this.buffer = ''; + } +} +``` + +#### 2. 错误处理增强 +- 推理内容解析失败时的降级策略 +- 不同提供商API差异的统一处理 + +#### 3. 测试覆盖建议 +- 添加错误场景的测试用例 +- 性能测试:大量think标签的处理性能 +- 边界条件:恶意构造的think标签 + +#### 4. 文档更新需求 +- API文档需要更新,说明新的结构化响应格式 +- 使用示例和最佳实践指南 \ No newline at end of file diff --git a/packages/core/src/services/llm/service.ts b/packages/core/src/services/llm/service.ts index 5df88e0c..039d31b4 100644 --- a/packages/core/src/services/llm/service.ts +++ b/packages/core/src/services/llm/service.ts @@ -1,4 +1,4 @@ -import { ILLMService, Message, StreamHandlers, ModelInfo, ModelOption } from './types'; +import { ILLMService, Message, StreamHandlers, LLMResponse, ModelInfo, ModelOption } from './types'; import { ModelConfig } from '../model/types'; import { ModelManager, modelManager as defaultModelManager } from '../model/manager'; import { APIError, RequestConfigError, ERROR_MESSAGES } from './errors'; @@ -130,9 +130,9 @@ export class LLMService implements ILLMService { } /** - * 发送OpenAI消息 + * 发送OpenAI消息(结构化格式) */ - private async sendOpenAIMessage(messages: Message[], modelConfig: ModelConfig): Promise { + private async sendOpenAIMessageStructured(messages: Message[], modelConfig: ModelConfig): Promise { const openai = this.getOpenAIInstance(modelConfig); const formattedMessages = messages.map(msg => ({ @@ -152,15 +152,54 @@ export class LLMService implements ILLMService { messages: formattedMessages, ...restLlmParams // Spread other params from llmParams }; - const response = await openai.chat.completions.create(completionConfig); - return response.choices[0].message.content || ''; + try { + const response = await openai.chat.completions.create(completionConfig); + + // 处理响应中的 reasoning_content 和普通 content + const choice = response.choices[0]; + if (!choice?.message) { + throw new Error('未收到有效的响应'); + } + + let content = choice.message.content || ''; + let reasoning = ''; + + // 处理推理内容(如果存在) + // SiliconFlow 等提供商在 choice.message 中并列提供 reasoning_content 字段 + if ((choice.message as any).reasoning_content) { + reasoning = (choice.message as any).reasoning_content; + } else { + // 检测并分离content中的think标签 + const thinkMatch = content.match(/(.*?)<\/think>/s); + if (thinkMatch) { + reasoning = thinkMatch[1]; + content = content.replace(/.*?<\/think>/s, '').trim(); + } + } + + const result: LLMResponse = { + content: content, + reasoning: reasoning || undefined, + metadata: { + model: modelConfig.defaultModel, + finishReason: choice.finish_reason || undefined + } + }; + + return result; + } catch (error) { + console.error('OpenAI API调用失败:', error); + throw new Error(`OpenAI API调用失败: ${error instanceof Error ? error.message : String(error)}`); + } } + + /** - * 发送Gemini消息 + * 发送Gemini消息(结构化格式) */ - private async sendGeminiMessage(messages: Message[], modelConfig: ModelConfig): Promise { + private async sendGeminiMessageStructured(messages: Message[], modelConfig: ModelConfig): Promise { // 提取系统消息 const systemMessages = messages.filter(msg => msg.role === 'system'); const systemInstruction = systemMessages.length > 0 @@ -190,16 +229,29 @@ export class LLMService implements ILLMService { ? conversationMessages[conversationMessages.length - 1].content : ''; - // 如果没有用户消息,返回空字符串 + // 如果没有用户消息,返回空响应 if (!lastUserMessage) { - return ''; + return { + content: '', + metadata: { + model: modelConfig.defaultModel + } + }; } // 发送消息并获取响应 const result = await chat.sendMessage(lastUserMessage); - return result.response.text(); + + return { + content: result.response.text(), + metadata: { + model: modelConfig.defaultModel + } + }; } + + /** * 格式化Gemini历史消息 */ @@ -231,9 +283,9 @@ export class LLMService implements ILLMService { } /** - * 发送消息 + * 发送消息(结构化格式) */ - async sendMessage(messages: Message[], provider: string): Promise { + async sendMessageStructured(messages: Message[], provider: string): Promise { try { if (!provider) { throw new RequestConfigError('模型提供商不能为空'); @@ -254,10 +306,10 @@ export class LLMService implements ILLMService { }); if (modelConfig.provider === 'gemini') { - return this.sendGeminiMessage(messages, modelConfig); + return this.sendGeminiMessageStructured(messages, modelConfig); } else { // OpenAI兼容格式的API,包括DeepSeek和自定义模型 - return this.sendOpenAIMessage(messages, modelConfig); + return this.sendOpenAIMessageStructured(messages, modelConfig); } } catch (error: any) { if (error instanceof RequestConfigError || error instanceof APIError) { @@ -268,7 +320,18 @@ export class LLMService implements ILLMService { } /** - * 发送消息(流式) + * 发送消息(传统格式,只返回主要内容) + */ + async sendMessage(messages: Message[], provider: string): Promise { + const response = await this.sendMessageStructured(messages, provider); + + // 只返回主要内容,不包含推理内容 + // 如果需要推理内容,请使用 sendMessageStructured 方法 + return response.content; + } + + /** + * 发送消息(流式,支持结构化和传统格式) */ async sendMessageStream( messages: Message[], @@ -304,6 +367,102 @@ export class LLMService implements ILLMService { } } + + + /** + * 处理流式内容中的think标签(用于流式场景) + */ + private processStreamContentWithThinkTags( + content: string, + callbacks: StreamHandlers, + thinkState: { isInThinkMode: boolean; buffer: string } + ): void { + // 如果没有推理回调,直接发送到主要内容流 + if (!callbacks.onReasoningToken) { + callbacks.onToken(content); + return; + } + + // 将新内容添加到缓冲区 + thinkState.buffer += content; + let remaining = thinkState.buffer; + let processed = ''; + + while (remaining.length > 0) { + if (!thinkState.isInThinkMode) { + // 不在think模式中,查找标签 + const thinkStartIndex = remaining.indexOf(''); + + if (thinkStartIndex !== -1) { + // 找到了开始标签 + // 发送开始标签前的内容到主要流 + if (thinkStartIndex > 0) { + const beforeThink = remaining.slice(0, thinkStartIndex); + callbacks.onToken(beforeThink); + processed += beforeThink + ''; + } else { + processed += ''; + } + + // 进入think模式 + thinkState.isInThinkMode = true; + remaining = remaining.slice(thinkStartIndex + 7); // 7 = ''.length + } else { + // 没有找到开始标签 + // 检查buffer末尾是否可能是不完整的标签开始 + if (remaining.endsWith('<') || remaining.endsWith('标签 + const thinkEndIndex = remaining.indexOf(''); + + if (thinkEndIndex !== -1) { + // 找到了结束标签 + // 发送结束标签前的内容到推理流 + if (thinkEndIndex > 0) { + const thinkContent = remaining.slice(0, thinkEndIndex); + callbacks.onReasoningToken(thinkContent); + } + + // 退出think模式 + thinkState.isInThinkMode = false; + processed += remaining.slice(0, thinkEndIndex) + ''; + remaining = remaining.slice(thinkEndIndex + 8); // 8 = ''.length + } else { + // 没有找到结束标签 + // 检查buffer末尾是否可能是不完整的结束标签 + if (remaining.endsWith('<') || remaining.endsWith(' { - // 获取流式OpenAI实例 - const openai = this.getOpenAIInstance(modelConfig, true); - - const formattedMessages = messages.map(msg => ({ - role: msg.role, - content: msg.content - })); - try { + // 获取流式OpenAI实例 + const openai = this.getOpenAIInstance(modelConfig, true); + + const formattedMessages = messages.map(msg => ({ + role: msg.role, + content: msg.content + })); + console.log('开始创建流式请求...'); const { timeout, // Handled in getOpenAIInstance @@ -343,17 +502,49 @@ export class LLMService implements ILLMService { console.log('成功获取到流式响应'); // 使用类型断言来确保TypeScript知道这是流式响应 + let accumulatedReasoning = ''; + let accumulatedContent = ''; + + // think标签状态跟踪 + const thinkState = { isInThinkMode: false, buffer: '' }; + for await (const chunk of stream as any) { + // 处理推理内容(SiliconFlow 等提供商在 delta 中提供 reasoning_content) + const reasoningContent = chunk.choices[0]?.delta?.reasoning_content || ''; + if (reasoningContent) { + accumulatedReasoning += reasoningContent; + + // 如果有推理回调,发送推理内容 + if (callbacks.onReasoningToken) { + callbacks.onReasoningToken(reasoningContent); + } + await new Promise(resolve => setTimeout(resolve, 10)); + } + + // 处理主要内容 const content = chunk.choices[0]?.delta?.content || ''; if (content) { - callbacks.onToken(content); - // 添加小延迟,让UI有时间更新 + accumulatedContent += content; + + // 使用流式think标签处理 + this.processStreamContentWithThinkTags(content, callbacks, thinkState); + await new Promise(resolve => setTimeout(resolve, 10)); } } console.log('流式响应完成'); - callbacks.onComplete(); + + // 构建完整响应 + const response: LLMResponse = { + content: accumulatedContent, + reasoning: accumulatedReasoning || undefined, + metadata: { + model: modelConfig.defaultModel + } + }; + + callbacks.onComplete(response); } catch (error) { console.error('流式处理过程中出错:', error); callbacks.onError(error instanceof Error ? error : new Error(String(error))); @@ -398,9 +589,16 @@ export class LLMService implements ILLMService { ? conversationMessages[conversationMessages.length - 1].content : ''; - // 如果没有用户消息,返回空字符串 + // 如果没有用户消息,发送空响应 if (!lastUserMessage) { - callbacks.onComplete(); + const response: LLMResponse = { + content: '', + metadata: { + model: modelConfig.defaultModel + } + }; + + callbacks.onComplete(response); return; } @@ -409,10 +607,13 @@ export class LLMService implements ILLMService { const result = await chat.sendMessageStream(lastUserMessage); console.log('成功获取到流式响应'); + + let accumulatedContent = ''; for await (const chunk of result.stream) { const text = chunk.text(); if (text) { + accumulatedContent += text; callbacks.onToken(text); // 添加小延迟,让UI有时间更新 await new Promise(resolve => setTimeout(resolve, 10)); @@ -420,7 +621,16 @@ export class LLMService implements ILLMService { } console.log('流式响应完成'); - callbacks.onComplete(); + + // 构建完整响应 + const response: LLMResponse = { + content: accumulatedContent, + metadata: { + model: modelConfig.defaultModel + } + }; + + callbacks.onComplete(response); } catch (error) { console.error('流式处理过程中出错:', error); callbacks.onError(error instanceof Error ? error : new Error(String(error))); diff --git a/packages/core/src/services/llm/types.ts b/packages/core/src/services/llm/types.ts index 76efdc32..5299e6c0 100644 --- a/packages/core/src/services/llm/types.ts +++ b/packages/core/src/services/llm/types.ts @@ -12,9 +12,34 @@ export interface Message { content: string; } +/** + * LLM响应结构 + */ +export interface LLMResponse { + content: string; + reasoning?: string; + metadata?: { + model?: string; + tokens?: number; + finishReason?: string; + }; +} + +/** + * 流式响应处理器 + * 支持传统格式和结构化格式 + */ export interface StreamHandlers { + // 主要内容流(必需,向后兼容) onToken: (token: string) => void; - onComplete: () => void; + + // 推理内容流(可选,新增功能) + onReasoningToken?: (token: string) => void; + + // 完成回调(现在传递完整响应,向后兼容通过可选参数) + onComplete: (response?: LLMResponse) => void; + + // 错误回调 onError: (error: Error) => void; } @@ -39,14 +64,24 @@ export interface ModelOption { */ export interface ILLMService { /** - * 发送消息 + * 发送消息(传统格式,返回合并的字符串) + * @deprecated 建议使用 sendMessageStructured 获得更好的语义支持 * @throws {RequestConfigError} 当参数无效时 * @throws {APIError} 当请求失败时 */ sendMessage(messages: Message[], provider: string): Promise; /** - * 发送流式消息 + * 发送消息(结构化格式) + * @throws {RequestConfigError} 当参数无效时 + * @throws {APIError} 当请求失败时 + */ + sendMessageStructured(messages: Message[], provider: string): Promise; + + + + /** + * 发送流式消息(支持结构化和传统格式) * @throws {RequestConfigError} 当参数无效时 * @throws {APIError} 当请求失败时 */ diff --git a/packages/core/src/services/model/defaults.ts b/packages/core/src/services/model/defaults.ts index 63154a5c..9e38431f 100644 --- a/packages/core/src/services/model/defaults.ts +++ b/packages/core/src/services/model/defaults.ts @@ -47,8 +47,8 @@ export const defaultModels: Record = { openai: { name: 'OpenAI', baseURL: 'https://api.openai.com/v1', - models: ['gpt-4', 'gpt-3.5-turbo'], - defaultModel: 'gpt-3.5-turbo', + models: ['gpt-4o', 'gpt-4o-mini', 'gpt-4', 'gpt-3.5-turbo', 'o1', 'o1-mini', 'o1-preview', 'o3', 'o4-mini'], + defaultModel: 'gpt-4o-mini', apiKey: OPENAI_API_KEY, enabled: !!OPENAI_API_KEY, provider: 'openai', @@ -69,7 +69,7 @@ export const defaultModels: Record = { deepseek: { name: 'DeepSeek', baseURL: 'https://api.deepseek.com/v1', - models: ['deepseek-chat'], + models: ['deepseek-chat', 'deepseek-reasoner'], defaultModel: 'deepseek-chat', apiKey: DEEPSEEK_API_KEY, enabled: !!DEEPSEEK_API_KEY, @@ -80,8 +80,8 @@ export const defaultModels: Record = { siliconflow: { name: 'SiliconFlow', baseURL: 'https://api.siliconflow.cn/v1', - models: ['Pro/deepseek-ai/DeepSeek-V3'], - defaultModel: 'Pro/deepseek-ai/DeepSeek-V3', + models: ['Qwen/Qwen3-8B'], + defaultModel: 'Qwen/Qwen3-8B', apiKey: SILICONFLOW_API_KEY, enabled: !!SILICONFLOW_API_KEY, provider: 'siliconflow', diff --git a/packages/core/tests/integration/llm/openai.test.js b/packages/core/tests/integration/llm/openai.test.js index ca9be3aa..296ab306 100644 --- a/packages/core/tests/integration/llm/openai.test.js +++ b/packages/core/tests/integration/llm/openai.test.js @@ -32,6 +32,14 @@ describe('OpenAI API 真实连接测试', () => { // 选择第一个可用的密钥和对应的配置 const getModelConfig = () => { + if (process.env.SILICONFLOW_API_KEY || process.env.VITE_SILICONFLOW_API_KEY) { + return { + key: 'siliconflow', + apiKey: process.env.SILICONFLOW_API_KEY || process.env.VITE_SILICONFLOW_API_KEY, + baseURL: 'https://api.siliconflow.cn/v1', + defaultModel: 'Qwen/Qwen3-8B' + }; + } if (process.env.OPENAI_API_KEY || process.env.VITE_OPENAI_API_KEY) { return { key: 'openai', @@ -48,14 +56,6 @@ describe('OpenAI API 真实连接测试', () => { defaultModel: 'deepseek-chat' }; } - if (process.env.SILICONFLOW_API_KEY || process.env.VITE_SILICONFLOW_API_KEY) { - return { - key: 'siliconflow', - apiKey: process.env.SILICONFLOW_API_KEY || process.env.VITE_SILICONFLOW_API_KEY, - baseURL: 'https://api.siliconflow.cn/v1', - defaultModel: 'Pro/deepseek-ai/DeepSeek-V3' - }; - } if (process.env.ZHIPU_API_KEY || process.env.VITE_ZHIPU_API_KEY) { return { key: 'zhipu', @@ -190,7 +190,7 @@ describe('OpenAI API 真实连接测试', () => { expect(typeof response).toBe('string'); expect(response.length).toBeGreaterThan(0); // 由于设置了max_tokens=100,响应应该相对较短 - expect(response.length).toBeLessThan(500); + expect(response.length).toBeLessThan(200); } catch (error) { console.error(`高级参数测试失败 (${modelConfig.key}):`, error.message); if (error.message.includes('400')) { @@ -200,4 +200,310 @@ describe('OpenAI API 真实连接测试', () => { throw error; } }, 30000); + + it('应该能兼容处理所有模型的响应格式(reasoning_content + think标签 + 普通文本)', async () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = createLLMService(modelManager); + + try { + // 测试通用兼容性处理 + await modelManager.updateModel(modelConfig.key, { + apiKey: modelConfig.apiKey, + baseURL: modelConfig.baseURL, + defaultModel: modelConfig.defaultModel, + enabled: true, + provider: 'openai', + llmParams: { + temperature: 0.1, + max_tokens: 100 + } + }); + + const testMessages = [ + { + role: 'user', + content: '请简单回答:什么是AI?' + } + ]; + + // 测试非流式处理 + const result = await llmService.sendMessage(testMessages, modelConfig.key); + + expect(result).toBeTruthy(); + expect(typeof result).toBe('string'); + expect(result.length).toBeGreaterThan(0); + + console.log('兼容性测试结果:', { + hasThinkTags: result.includes(''), + hasContent: result.length > 0, + result: result + }); + + // 测试流式处理 + let streamResult = ''; + let tokenCount = 0; + let isCompleted = false; + let hasError = false; + + await llmService.sendMessageStream(testMessages, modelConfig.key, { + onToken: (token) => { + streamResult += token; + tokenCount++; + }, + onComplete: (response) => { + isCompleted = true; + }, + onError: (error) => { + hasError = true; + console.error('流式测试错误:', error); + } + }); + + expect(hasError).toBe(false); + expect(isCompleted).toBe(true); + expect(streamResult.length).toBeGreaterThan(0); + expect(tokenCount).toBeGreaterThan(0); + + console.log('流式兼容性测试结果:', { + tokenCount, + hasThinkTags: streamResult.includes(''), + streamLength: streamResult.length, + isCompleted + }); + + } catch (error) { + console.error('兼容性测试失败:', error); + throw error; + } + },60000); + + it('应该能正确处理reasoning_content的流式输出', async () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = createLLMService(modelManager); + + try { + // 配置模型 + await modelManager.updateModel(modelConfig.key, { + apiKey: modelConfig.apiKey, + baseURL: modelConfig.baseURL, + defaultModel: modelConfig.defaultModel, + enabled: true, + provider: 'openai', + llmParams: { + temperature: 0.1, + max_tokens: 2000 + } + }); + + const testMessages = [ + { + role: 'user', + content: '你是谁' + } + ]; + + // 模拟包含reasoning_content的流式响应 + let fullResult = ''; + let tokenCount = 0; + let hasThinkTags = false; + let thinkTagsClosed = false; + let isCompleted = false; + let hasError = false; + + await llmService.sendMessageStream(testMessages, modelConfig.key, { + onToken: (token) => { + fullResult += token; + tokenCount++; + + // 检查think标签的完整性 + if (token.includes('')) { + hasThinkTags = true; + } + if (token.includes('')) { + thinkTagsClosed = true; + } + }, + onComplete: (response) => { + isCompleted = true; + }, + onError: (error) => { + hasError = true; + console.error('流式测试错误:', error); + } + }); + + // 等待流式完成 + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log('reasoning_content流式测试结果:', { + tokenCount, + hasThinkTags, + thinkTagsClosed, + isCompleted, + hasError, + resultLength: fullResult.length, + fullResult: fullResult + }); + + expect(isCompleted).toBe(true); + expect(hasError).toBe(false); + expect(tokenCount).toBeGreaterThan(0); + expect(fullResult.length).toBeGreaterThan(0); + + // 如果有think标签,检查它们是否正确闭合 + const thinkOpenCount = (fullResult.match(//g) || []).length; + const thinkCloseCount = (fullResult.match(/<\/think>/g) || []).length; + + if (thinkOpenCount > 0) { + expect(thinkOpenCount).toBe(thinkCloseCount); + console.log(`✅ Think标签匹配: ${thinkOpenCount} 个开始标签, ${thinkCloseCount} 个结束标签`); + } + + } catch (error) { + console.error('reasoning_content流式测试失败:', error); + throw error; + } + }); + + it('应该能使用结构化API发送消息', async () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = createLLMService(modelManager); + + try { + // 配置模型 + await modelManager.updateModel(modelConfig.key, { + apiKey: modelConfig.apiKey, + baseURL: modelConfig.baseURL, + defaultModel: modelConfig.defaultModel, + enabled: true, + provider: 'openai', + llmParams: { + temperature: 0.3, + max_tokens: 100 + } + }); + + const testMessages = [ + { + role: 'user', + content: '请简单回答:什么是AI?' + } + ]; + + // 测试结构化API + const response = await llmService.sendMessageStructured(testMessages, modelConfig.key); + + expect(response).toBeDefined(); + expect(typeof response).toBe('object'); + expect(response.content).toBeDefined(); + expect(typeof response.content).toBe('string'); + expect(response.content.length).toBeGreaterThan(0); + + // 检查元数据 + expect(response.metadata).toBeDefined(); + expect(response.metadata.model).toBe(modelConfig.defaultModel); + + console.log('结构化API测试结果:', { + hasContent: response.content.length > 0, + hasReasoning: !!response.reasoning, + content: response.content, + reasoning: response.reasoning, + model: response.metadata?.model + }); + + } catch (error) { + console.error('结构化API测试失败:', error); + throw error; + } + }, 60000); + + it('应该能使用结构化回调进行流式处理', async () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = createLLMService(modelManager); + + try { + // 配置模型 + await modelManager.updateModel(modelConfig.key, { + apiKey: modelConfig.apiKey, + baseURL: modelConfig.baseURL, + defaultModel: modelConfig.defaultModel, + enabled: true, + provider: 'openai', + llmParams: { + temperature: 0.1, + max_tokens: 1500 + } + }); + + const testMessages = [ + { + role: 'user', + content: '请简单回答:什么是AI?' + } + ]; + + let contentTokens = ''; + let reasoningTokens = ''; + let finalResponse = null; + let contentTokenCount = 0; + let reasoningTokenCount = 0; + let isCompleted = false; + let hasError = false; + + await llmService.sendMessageStream(testMessages, modelConfig.key, { + onToken: (token) => { + contentTokens += token; + contentTokenCount++; + }, + onReasoningToken: (token) => { + reasoningTokens += token; + reasoningTokenCount++; + }, + onComplete: (response) => { + finalResponse = response; + isCompleted = true; + }, + onError: (error) => { + hasError = true; + console.error('结构化流式测试错误:', error); + } + }); + + // 等待流式完成 + await new Promise(resolve => setTimeout(resolve, 1000)); + + console.log('结构化流式测试结果:', { + contentTokenCount, + reasoningTokenCount, + isCompleted, + hasError, + content: contentTokens, + reasoning: reasoningTokens, + finalResponse: finalResponse + }); + + expect(isCompleted).toBe(true); + expect(hasError).toBe(false); + expect(finalResponse).toBeDefined(); + expect(finalResponse.content).toBeDefined(); + expect(contentTokenCount).toBeGreaterThan(0); + expect(contentTokens.length).toBeGreaterThan(0); + + // 验证内容一致性 + expect(contentTokens).toBe(finalResponse.content); + + // 如果有推理内容,验证一致性 + if (reasoningTokenCount > 0) { + expect(reasoningTokens).toBe(finalResponse.reasoning || ''); + } + + } catch (error) { + console.error('结构化流式测试失败:', error); + throw error; + } + }, 60000); }); \ No newline at end of file diff --git a/packages/core/tests/unit/llm/llmParams.test.ts b/packages/core/tests/unit/llm/llmParams.test.ts index 680c5905..f45a44a4 100644 --- a/packages/core/tests/unit/llm/llmParams.test.ts +++ b/packages/core/tests/unit/llm/llmParams.test.ts @@ -476,6 +476,8 @@ describe('LLM Parameters (llmParams) Functionality', () => { describe('Gemini Specific Parameters', () => { if (hasGeminiKey && geminiConfig) { it('should accept valid maxOutputTokens for Gemini provider', async () => { + // 添加间隔,避免频率限制,先等10秒 + await new Promise(resolve => setTimeout(resolve, 10000)); const storage = new LocalStorageProvider(); const modelManager = new ModelManager(storage); const llmService = createLLMService(modelManager); @@ -499,9 +501,10 @@ describe('LLM Parameters (llmParams) Functionality', () => { expect(response).toBeDefined(); expect(typeof response).toBe('string'); expect(response.length).toBeGreaterThan(0); - }, 30000); - + }, 60000); it('should accept valid candidateCount for Gemini provider', async () => { + // 添加间隔,避免频率限制,先等10秒 + await new Promise(resolve => setTimeout(resolve, 10000)); const storage = new LocalStorageProvider(); const modelManager = new ModelManager(storage); const llmService = createLLMService(modelManager); @@ -525,7 +528,7 @@ describe('LLM Parameters (llmParams) Functionality', () => { expect(response).toBeDefined(); expect(typeof response).toBe('string'); expect(response.length).toBeGreaterThan(0); - }, 30000); + }, 60000); } }); diff --git a/packages/core/tests/unit/llm/think-tag-processing.test.ts b/packages/core/tests/unit/llm/think-tag-processing.test.ts new file mode 100644 index 00000000..902e6c20 --- /dev/null +++ b/packages/core/tests/unit/llm/think-tag-processing.test.ts @@ -0,0 +1,133 @@ +import { describe, it, expect, vi } from 'vitest'; +import { LLMService } from '../../../src/services/llm/service'; +import { ModelManager } from '../../../src/services/model/manager'; +import { LocalStorageProvider } from '../../../src/services/storage/localStorageProvider'; + +describe('Think标签处理测试', () => { + + describe('流式处理', () => { + it('应该能正确处理流式think标签:开始标签 -> 推理内容 -> 结束标签 -> 正文', () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = new LLMService(modelManager); + + const mockCallbacks = { + onToken: vi.fn(), + onReasoningToken: vi.fn(), + onComplete: vi.fn(), + onError: vi.fn() + }; + + const thinkState = { isInThinkMode: false, buffer: '' }; + + // 模拟流式处理:分多个chunk接收 + // Chunk 1: 开始标签 + (llmService as any).processStreamContentWithThinkTags('', mockCallbacks, thinkState); + + // Chunk 2: 推理内容第一部分 + (llmService as any).processStreamContentWithThinkTags('我需要思考', mockCallbacks, thinkState); + + // Chunk 3: 推理内容第二部分 + (llmService as any).processStreamContentWithThinkTags('这个问题', mockCallbacks, thinkState); + + // Chunk 4: 结束标签 + (llmService as any).processStreamContentWithThinkTags('', mockCallbacks, thinkState); + + // Chunk 5: 正文内容 + (llmService as any).processStreamContentWithThinkTags('这是最终答案', mockCallbacks, thinkState); + + // 验证推理内容被正确分离 + expect(mockCallbacks.onReasoningToken).toHaveBeenCalledWith('我需要思考'); + expect(mockCallbacks.onReasoningToken).toHaveBeenCalledWith('这个问题'); + + // 验证正文内容被正确发送 + expect(mockCallbacks.onToken).toHaveBeenCalledWith('这是最终答案'); + + // 验证没有将标签内容发送到主流 + expect(mockCallbacks.onToken).not.toHaveBeenCalledWith(''); + expect(mockCallbacks.onToken).not.toHaveBeenCalledWith(''); + }); + + it('应该能处理单个chunk包含完整think标签的情况', () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = new LLMService(modelManager); + + const mockCallbacks = { + onToken: vi.fn(), + onReasoningToken: vi.fn(), + onComplete: vi.fn(), + onError: vi.fn() + }; + + const thinkState = { isInThinkMode: false, buffer: '' }; + + // 单个chunk包含完整的think标签 + (llmService as any).processStreamContentWithThinkTags( + '前面内容推理过程后面内容', + mockCallbacks, + thinkState + ); + + expect(mockCallbacks.onToken).toHaveBeenCalledWith('前面内容'); + expect(mockCallbacks.onReasoningToken).toHaveBeenCalledWith('推理过程'); + expect(mockCallbacks.onToken).toHaveBeenCalledWith('后面内容'); + }); + + it('应该能处理跨chunk的think标签', () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = new LLMService(modelManager); + + const mockCallbacks = { + onToken: vi.fn(), + onReasoningToken: vi.fn(), + onComplete: vi.fn(), + onError: vi.fn() + }; + + const thinkState = { isInThinkMode: false, buffer: '' }; + + // Chunk 1: 包含开始标签的一部分 + (llmService as any).processStreamContentWithThinkTags('前面推理开始', mockCallbacks, thinkState); + + // Chunk 3: 推理内容和部分结束标签 + (llmService as any).processStreamContentWithThinkTags('推理结束正文内容', mockCallbacks, thinkState); + + expect(mockCallbacks.onToken).toHaveBeenCalledWith('前面'); + expect(mockCallbacks.onReasoningToken).toHaveBeenCalledWith('推理开始'); + expect(mockCallbacks.onReasoningToken).toHaveBeenCalledWith('推理结束'); + expect(mockCallbacks.onToken).toHaveBeenCalledWith('正文内容'); + }); + + it('应该能处理没有推理回调的流式情况', () => { + const storage = new LocalStorageProvider(); + const modelManager = new ModelManager(storage); + const llmService = new LLMService(modelManager); + + const mockCallbacks = { + onToken: vi.fn(), + onComplete: vi.fn(), + onError: vi.fn() + // 注意:没有 onReasoningToken + }; + + const thinkState = { isInThinkMode: false, buffer: '' }; + + (llmService as any).processStreamContentWithThinkTags( + '推理过程正文内容', + mockCallbacks, + thinkState + ); + + // 当没有推理回调时,应该直接发送原始内容 + expect(mockCallbacks.onToken).toHaveBeenCalledWith('推理过程正文内容'); + }); + }); +}); \ No newline at end of file