diff --git a/docs/AGENT_THREE_LAYER_ARCHITECTURE.md b/docs/AGENT_THREE_LAYER_ARCHITECTURE.md new file mode 100644 index 0000000..4fea6b8 --- /dev/null +++ b/docs/AGENT_THREE_LAYER_ARCHITECTURE.md @@ -0,0 +1,252 @@ +# iConsulting Agent 三层架构设计 + +## 问题背景 + +当前 AI 回复存在以下问题: +- 回复过于冗长,没有抓住用户核心需求 +- 重复性表达多,效率低 +- 缺乏对用户意图的准确理解 +- 没有自我评估机制 + +## 设计目标 + +作为 Agent,应该做到: +1. **准确理解用户意图** - 基于对话上下文、历史经验判断用户当前需要什么 +2. **自我评估** - 判断 AI 是否已经给出用户想要的答案 +3. **主动行动** - 如果未达到用户需求,主动调用工具寻找解决方案 +4. **简洁高效** - 回答抓住重点,简短有效 + +## 三层架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 用户消息输入 │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 第一层:意图分类器 │ +│ IntentClassifier │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ • 分析用户意图类型(简单查询/深度咨询/需要行动/闲聊) ││ +│ │ • 确定建议的最大回复长度 ││ +│ │ • 识别是否需要调用工具 ││ +│ │ • 检测是否为后续问题 ││ +│ │ • 提取关键实体(签证类型、职业等) ││ +│ └─────────────────────────────────────────────────────────┘│ +│ 输出:IntentResult { type, maxResponseLength, needsTools } │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 第二层:ReAct Agent │ +│ Claude API + Tool Loop │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ 思考 (Thought) ││ +│ │ └─► 分析用户真实需求 ││ +│ │ └─► 评估当前信息是否足够 ││ +│ │ └─► 决定是否需要调用工具 ││ +│ │ ││ +│ │ 行动 (Action) ││ +│ │ └─► 调用合适的工具获取信息 ││ +│ │ └─► 支持最多10轮工具调用 ││ +│ │ ││ +│ │ 观察 (Observation) ││ +│ │ └─► 分析工具返回结果 ││ +│ │ └─► 决定是否需要继续行动 ││ +│ │ ││ +│ │ 生成 (Generate) ││ +│ │ └─► 根据意图分类结果控制回复长度 ││ +│ │ └─► 聚焦用户核心问题 ││ +│ └─────────────────────────────────────────────────────────┘│ +│ 输出:AI 回复文本 │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 第三层:回复质量门控 │ +│ ResponseGate │ +│ ┌─────────────────────────────────────────────────────────┐│ +│ │ 质量检查: ││ +│ │ • 长度检查 - 是否超过建议长度 ││ +│ │ • 相关性检查 - 是否回答了用户问题 ││ +│ │ • 冗余检查 - 是否包含重复/冗余表达 ││ +│ │ • 完整性检查 - 是否包含必要信息 ││ +│ │ • 语气检查 - 是否符合场景要求 ││ +│ │ ││ +│ │ 优化处理: ││ +│ │ • 裁剪过长回复(在句子边界处裁剪) ││ +│ │ • 移除冗余表达 ││ +│ └─────────────────────────────────────────────────────────┘│ +│ 输出:优化后的最终回复 │ +└─────────────────────────┬───────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ 最终回复输出 │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 第一层:意图分类器 (IntentClassifier) + +### 意图类型 + +| 类型 | 说明 | 建议长度 | 需要工具 | +|------|------|----------|----------| +| `SIMPLE_QUERY` | 简单查询,直接回答 | 300字 | 否 | +| `DEEP_CONSULTATION` | 深度咨询,需要知识库 | 800字 | 是 | +| `ACTION_NEEDED` | 需要执行操作 | 500字 | 是 | +| `CHAT` | 闲聊寒暄 | 100字 | 否 | +| `CLARIFICATION` | 需要追问澄清 | 150字 | 否 | +| `CONFIRMATION` | 确认/否定 | 200字 | 否 | + +### 分类规则 + +1. **关键词匹配** - 快速识别意图模式 +2. **上下文分析** - 判断是否为后续问题 +3. **实体提取** - 识别签证类型、职业等关键信息 +4. **工具推荐** - 根据意图推荐合适的工具 + +### 代码位置 + +`packages/services/conversation-service/src/infrastructure/claude/intent-classifier.ts` + +## 第二层:ReAct Agent + +### ReAct 思维框架 + +在 System Prompt 中注入 ReAct 思维模式: + +``` + +在回答用户问题时,请遵循以下思维框架: + +1. 理解 (Understand) + - 用户的核心问题是什么? + - 用户的真实需求是什么? + - 这是后续问题还是新问题? + +2. 评估 (Evaluate) + - 我当前掌握的信息是否足够? + - 是否需要调用工具获取更多信息? + - 之前的对话是否已经回答过类似问题? + +3. 行动 (Act) + - 如果信息不足,调用合适的工具 + - 优先使用:知识库搜索 > 经验召回 > 网络搜索 + +4. 生成 (Generate) + - 直接回答核心问题 + - 避免冗余的开场白和结束语 + - 控制回复长度,抓住重点 + +``` + +### 工具循环 + +- 最多支持 10 轮工具调用 +- 每轮:思考 → 调用工具 → 观察结果 → 决定下一步 +- 当信息足够时停止调用,生成最终回复 + +### 代码位置 + +`packages/services/conversation-service/src/infrastructure/claude/prompts/system-prompt.ts` + +## 第三层:回复质量门控 (ResponseGate) + +### 检查项 + +| 检查项 | 说明 | 未通过处理 | +|--------|------|------------| +| `length` | 长度是否超标 | 裁剪至建议长度 | +| `relevance` | 是否与问题相关 | 标记警告 | +| `redundancy` | 是否有冗余表达 | 移除冗余 | +| `completeness` | 是否信息完整 | 标记警告 | +| `tone` | 语气是否合适 | 标记警告 | + +### 冗余表达清单 + +``` +- "根据我的了解" +- "让我来帮您" +- "非常感谢您的咨询" +- "希望以上信息对您有所帮助" +- "如果您还有其他问题" +- ... +``` + +### 代码位置 + +`packages/services/conversation-service/src/infrastructure/claude/response-gate.ts` + +## 集成流程 + +```typescript +// claude-agent.service.ts 伪代码 + +async sendMessage(message, history) { + // 第一层:意图分类 + const intent = intentClassifier.classify(message, history); + + // 构建带 ReAct 框架的 System Prompt + const systemPrompt = buildSystemPrompt({ + ...config, + intentHint: intent, // 注入意图信息 + }); + + // 第二层:ReAct Agent 处理 + const response = await this.claudeClient.chat({ + systemPrompt, + messages: history, + tools: intent.needsTools ? this.getTools() : [], + maxTokens: this.calculateMaxTokens(intent), + }); + + // 第三层:回复质量门控 + const gateResult = responseGate.check(response, intent, message); + + // 返回优化后的回复 + return gateResult.optimizedResponse || response; +} +``` + +## 预期效果 + +### 改进前 +``` +用户:189签证要多少分? + +AI:非常感谢您的咨询!关于澳大利亚189独立技术移民签证, +这是一个非常重要的移民类别。让我来为您详细解答。 + +首先,189签证是一种打分制签证,需要达到一定的分数才能获邀。 +根据目前的政策...(以下省略500字) + +希望以上信息对您有所帮助!如果您还有其他问题,欢迎继续咨询。 +``` + +### 改进后 +``` +用户:189签证要多少分? + +AI:189签证需要至少65分才能提交EOI。 +目前获邀分数线约为85-95分,具体取决于职业。 + +主要加分项:年龄、英语、工作经验、学历、州担保等。 +``` + +## 文件清单 + +| 文件 | 状态 | 说明 | +|------|------|------| +| `intent-classifier.ts` | ✅ 已创建 | 意图分类器 | +| `response-gate.ts` | ✅ 已创建 | 回复质量门控 | +| `system-prompt.ts` | 🔄 待更新 | 加入 ReAct 框架 | +| `claude-agent.service.ts` | 🔄 待更新 | 集成三层架构 | + +## 后续优化方向 + +1. **机器学习分类** - 用训练数据替代规则分类 +2. **动态长度调整** - 根据反馈自动调整建议长度 +3. **个性化门控** - 根据用户偏好调整检查规则 +4. **A/B 测试** - 对比优化前后的用户满意度 diff --git a/packages/services/conversation-service/src/infrastructure/claude/claude-agent.service.ts b/packages/services/conversation-service/src/infrastructure/claude/claude-agent.service.ts index 14a4f9d..09dd122 100644 --- a/packages/services/conversation-service/src/infrastructure/claude/claude-agent.service.ts +++ b/packages/services/conversation-service/src/infrastructure/claude/claude-agent.service.ts @@ -4,6 +4,8 @@ import Anthropic from '@anthropic-ai/sdk'; import { ImmigrationToolsService } from './tools/immigration-tools.service'; import { buildSystemPrompt, SystemPromptConfig } from './prompts/system-prompt'; import { KnowledgeClientService } from '../knowledge/knowledge-client.service'; +import { intentClassifier, IntentResult, IntentType } from './intent-classifier'; +import { responseGate } from './response-gate'; export interface FileAttachment { id: string; @@ -81,6 +83,34 @@ export class ClaudeAgentService implements OnModuleInit { }; } + /** + * Calculate max tokens based on intent classification + * 中文平均每字符约 1.5 tokens,加上一定余量 + */ + private calculateMaxTokens(intent: IntentResult): number { + // 字符数转 tokens(中文约 1.5 tokens/字符,取 2 以留余量) + const tokensPerChar = 2; + const baseTokens = intent.maxResponseLength * tokensPerChar; + + // 根据意图类型调整 + switch (intent.type) { + case IntentType.CHAT: + return Math.max(256, baseTokens); // 闲聊最少 256 tokens + case IntentType.SIMPLE_QUERY: + return Math.max(512, baseTokens); // 简单查询最少 512 tokens + case IntentType.CLARIFICATION: + return Math.max(256, baseTokens); // 澄清最少 256 tokens + case IntentType.CONFIRMATION: + return Math.max(384, baseTokens); // 确认最少 384 tokens + case IntentType.DEEP_CONSULTATION: + return Math.min(4096, Math.max(1024, baseTokens)); // 深度咨询 1024-4096 + case IntentType.ACTION_NEEDED: + return Math.min(2048, Math.max(768, baseTokens)); // 需要行动 768-2048 + default: + return 2048; // 默认 2048 + } + } + /** * Fetch and format approved system experiences for injection */ @@ -165,12 +195,23 @@ export class ClaudeAgentService implements OnModuleInit { * Send a message and get streaming response with tool loop support * Uses Prompt Caching to reduce costs (~90% savings on cached system prompt) * Supports multimodal messages with image attachments + * Implements 3-layer architecture: Intent Classification -> ReAct Agent -> Response Gate */ async *sendMessage( message: string, context: ConversationContext, attachments?: FileAttachment[], ): AsyncGenerator { + // ========== 第一层:意图分类 ========== + const conversationHistory = context.previousMessages?.map(msg => ({ + role: msg.role, + content: msg.content, + })) || []; + const intent = intentClassifier.classify(message, conversationHistory); + + console.log(`[ClaudeAgent] Intent classified: ${intent.type}, maxLength: ${intent.maxResponseLength}, needsTools: ${intent.needsTools}`); + + // ========== 第二层:ReAct Agent ========== const tools = this.immigrationToolsService.getTools(); // Fetch relevant system experiences and inject into prompt @@ -178,6 +219,7 @@ export class ClaudeAgentService implements OnModuleInit { const dynamicConfig: SystemPromptConfig = { ...this.systemPromptConfig, accumulatedExperience, + intentHint: intent, // 注入意图分类结果 }; const systemPrompt = buildSystemPrompt(dynamicConfig); @@ -221,6 +263,9 @@ export class ClaudeAgentService implements OnModuleInit { const maxIterations = 10; // Safety limit let iterations = 0; + // 根据意图分类调整 max_tokens + const maxTokens = this.calculateMaxTokens(intent); + // System prompt with cache_control for Prompt Caching // Cache TTL is 5 minutes, cache hits cost only 10% of normal input price const systemWithCache: Anthropic.TextBlockParam[] = [ @@ -231,6 +276,9 @@ export class ClaudeAgentService implements OnModuleInit { }, ]; + // 用于收集完整响应以进行门控检查 + let fullResponseText = ''; + while (iterations < maxIterations) { iterations++; @@ -238,7 +286,7 @@ export class ClaudeAgentService implements OnModuleInit { // Create streaming message with cached system prompt const stream = await this.client.messages.stream({ model: 'claude-sonnet-4-20250514', - max_tokens: 4096, + max_tokens: maxTokens, system: systemWithCache, messages, tools: tools as Anthropic.Tool[], @@ -269,6 +317,7 @@ export class ClaudeAgentService implements OnModuleInit { } else if (event.type === 'content_block_delta') { if (event.delta.type === 'text_delta') { hasText = true; + fullResponseText += event.delta.text; // 收集完整响应 yield { type: 'text', content: event.delta.text, @@ -305,6 +354,14 @@ export class ClaudeAgentService implements OnModuleInit { // If no tool uses, we're done if (toolUses.length === 0) { + // ========== 第三层:回复质量门控(日志记录) ========== + if (fullResponseText) { + const gateResult = responseGate.check(fullResponseText, intent, message); + console.log(`[ClaudeAgent] Response gate: passed=${gateResult.passed}, length=${fullResponseText.length}/${intent.maxResponseLength}`); + if (!gateResult.passed && gateResult.suggestions) { + console.log(`[ClaudeAgent] Gate suggestions: ${gateResult.suggestions.join(', ')}`); + } + } yield { type: 'end' }; return; } diff --git a/packages/services/conversation-service/src/infrastructure/claude/intent-classifier.ts b/packages/services/conversation-service/src/infrastructure/claude/intent-classifier.ts new file mode 100644 index 0000000..d1a4aff --- /dev/null +++ b/packages/services/conversation-service/src/infrastructure/claude/intent-classifier.ts @@ -0,0 +1,299 @@ +/** + * 意图分类器 - 第一层 + * 快速分析用户意图,决定回复策略 + */ + +export enum IntentType { + /** 简单查询 - 直接回答,无需工具 */ + SIMPLE_QUERY = 'SIMPLE_QUERY', + /** 深度咨询 - 需要知识库检索 */ + DEEP_CONSULTATION = 'DEEP_CONSULTATION', + /** 需要行动 - 需要调用工具执行操作 */ + ACTION_NEEDED = 'ACTION_NEEDED', + /** 闲聊 - 简短友好回复 */ + CHAT = 'CHAT', + /** 澄清 - 用户意图不明,需要追问 */ + CLARIFICATION = 'CLARIFICATION', + /** 确认 - 用户确认/否定之前的回答 */ + CONFIRMATION = 'CONFIRMATION', +} + +export interface IntentResult { + type: IntentType; + confidence: number; + /** 建议的最大回复长度(字符数) */ + maxResponseLength: number; + /** 是否需要调用工具 */ + needsTools: boolean; + /** 建议使用的工具 */ + suggestedTools?: string[]; + /** 检测到的关键实体 */ + entities?: Record; + /** 是否为后续问题(基于上下文) */ + isFollowUp: boolean; +} + +interface Message { + role: 'user' | 'assistant'; + content: string; +} + +/** + * 意图分类器 + * 使用规则 + 关键词匹配快速分类,无需 API 调用 + */ +export class IntentClassifier { + // 简单查询关键词 + private simpleQueryPatterns = [ + /^(什么是|是什么|哪个|哪些|多少|几|怎么样|好不好)/, + /^(请问|想问|问一下)/, + /(是多少|要多久|需要什么|有哪些)/, + /^(可以吗|能不能|行不行)/, + ]; + + // 深度咨询关键词 + private deepConsultationPatterns = [ + /(怎么办|如何|怎样|怎么做|该怎么)/, + /(详细|具体|解释|说明|介绍)/, + /(比较|对比|区别|差异)/, + /(流程|步骤|过程|程序)/, + /(条件|要求|资格|标准)/, + /(材料|文件|清单|准备)/, + /(评估|分析|建议|推荐)/, + ]; + + // 需要行动的关键词 + private actionPatterns = [ + /(帮我|帮忙|请帮|麻烦)/, + /(查询|查一下|搜索|找一下|查找)/, + /(计算|算一下|估算)/, + /(预约|申请|提交|办理)/, + /(发送|转发|通知)/, + ]; + + // 闲聊关键词 + private chatPatterns = [ + /^(你好|您好|hi|hello|嗨|早|晚)/i, + /^(谢谢|感谢|多谢|thanks)/i, + /^(再见|拜拜|bye|88)/i, + /^(好的|明白|知道了|了解|收到)/, + /(哈哈|呵呵|嘿嘿|😀|👍|🙏)/, + ]; + + // 确认/否定关键词 + private confirmationPatterns = [ + /^(是的|对|对的|没错|正确|是啊|嗯)/, + /^(不是|不对|错了|不|否)/, + /^(还有|另外|还想问|补充)/, + ]; + + // 工具关键词映射 + private toolKeywords: Record = { + 'knowledge-search': ['知识', '文章', '政策', '规定', '信息', '资料'], + 'search-web': ['最新', '新闻', '近期', '实时', '当前', '现在'], + 'get-weather': ['天气', '温度', '下雨', '晴天'], + 'experience-recall': ['之前', '上次', '经验', '类似'], + 'user-memory-recall': ['我之前', '我上次', '我的', '记得我'], + 'immigration-assessment': ['评估', '打分', '分数', '资格'], + 'calculate': ['计算', '算', '多少钱', '费用'], + }; + + // 移民相关类别关键词 + private immigrationCategories: Record = { + 'skilled-migration': ['技术移民', '189', '190', '491', '打分', '职业评估', 'EOI'], + 'employer-sponsored': ['雇主担保', '482', '494', '186', '雇主', '工签'], + 'business-investment': ['商业移民', '投资移民', '188', '132', '商业', '投资'], + 'family-reunion': ['家庭团聚', '配偶', '父母', '子女', '820', '801', '143'], + 'student-visa': ['学生签证', '500', '485', '留学', '毕业生'], + 'visitor-visa': ['旅游签', '600', '访客', '探亲'], + }; + + /** + * 分类用户意图 + */ + classify( + userMessage: string, + conversationHistory: Message[] = [], + ): IntentResult { + const text = userMessage.trim(); + const isFollowUp = this.detectFollowUp(text, conversationHistory); + + // 1. 检测闲聊 + if (this.matchPatterns(text, this.chatPatterns)) { + return { + type: IntentType.CHAT, + confidence: 0.9, + maxResponseLength: 100, + needsTools: false, + isFollowUp, + }; + } + + // 2. 检测确认/否定 + if (this.matchPatterns(text, this.confirmationPatterns) && isFollowUp) { + return { + type: IntentType.CONFIRMATION, + confidence: 0.85, + maxResponseLength: 200, + needsTools: false, + isFollowUp: true, + }; + } + + // 3. 检测需要行动 + const actionTools = this.detectActionTools(text); + if (this.matchPatterns(text, this.actionPatterns) || actionTools.length > 0) { + return { + type: IntentType.ACTION_NEEDED, + confidence: 0.8, + maxResponseLength: 500, + needsTools: true, + suggestedTools: actionTools.length > 0 ? actionTools : undefined, + entities: this.extractEntities(text), + isFollowUp, + }; + } + + // 4. 检测深度咨询 + if (this.matchPatterns(text, this.deepConsultationPatterns)) { + return { + type: IntentType.DEEP_CONSULTATION, + confidence: 0.85, + maxResponseLength: 800, + needsTools: true, + suggestedTools: ['knowledge-search'], + entities: this.extractEntities(text), + isFollowUp, + }; + } + + // 5. 检测简单查询 + if (this.matchPatterns(text, this.simpleQueryPatterns)) { + return { + type: IntentType.SIMPLE_QUERY, + confidence: 0.8, + maxResponseLength: 300, + needsTools: false, + entities: this.extractEntities(text), + isFollowUp, + }; + } + + // 6. 消息太短或不清楚,需要澄清 + if (text.length < 5 && !isFollowUp) { + return { + type: IntentType.CLARIFICATION, + confidence: 0.7, + maxResponseLength: 150, + needsTools: false, + isFollowUp, + }; + } + + // 7. 默认:根据长度和内容判断 + const hasImmigrationKeywords = this.detectImmigrationCategory(text); + if (hasImmigrationKeywords) { + return { + type: IntentType.DEEP_CONSULTATION, + confidence: 0.7, + maxResponseLength: 600, + needsTools: true, + suggestedTools: ['knowledge-search'], + entities: this.extractEntities(text), + isFollowUp, + }; + } + + // 默认为简单查询 + return { + type: IntentType.SIMPLE_QUERY, + confidence: 0.6, + maxResponseLength: 400, + needsTools: false, + entities: this.extractEntities(text), + isFollowUp, + }; + } + + /** + * 检测是否为后续问题 + */ + private detectFollowUp(text: string, history: Message[]): boolean { + if (history.length === 0) return false; + + // 代词检测 + const pronouns = ['这个', '那个', '它', '这', '那', '上面', '刚才', '前面']; + if (pronouns.some(p => text.includes(p))) return true; + + // 省略主语的短问题 + if (text.length < 20 && !text.includes('我')) return true; + + // 以连接词开头 + const connectors = ['那', '然后', '接着', '还有', '另外', '所以']; + if (connectors.some(c => text.startsWith(c))) return true; + + return false; + } + + /** + * 匹配模式 + */ + private matchPatterns(text: string, patterns: RegExp[]): boolean { + return patterns.some(p => p.test(text)); + } + + /** + * 检测需要的工具 + */ + private detectActionTools(text: string): string[] { + const tools: string[] = []; + for (const [tool, keywords] of Object.entries(this.toolKeywords)) { + if (keywords.some(kw => text.includes(kw))) { + tools.push(tool); + } + } + return tools; + } + + /** + * 检测移民类别 + */ + private detectImmigrationCategory(text: string): string | null { + for (const [category, keywords] of Object.entries(this.immigrationCategories)) { + if (keywords.some(kw => text.includes(kw))) { + return category; + } + } + return null; + } + + /** + * 提取实体 + */ + private extractEntities(text: string): Record { + const entities: Record = {}; + + // 签证子类 + const visaMatch = text.match(/\b(189|190|491|482|494|186|188|132|820|801|143|500|485|600)\b/); + if (visaMatch) { + entities.visaSubclass = visaMatch[1]; + } + + // 移民类别 + const category = this.detectImmigrationCategory(text); + if (category) { + entities.category = category; + } + + // 职业 + const occupationMatch = text.match(/(会计|工程师|IT|程序员|护士|厨师|电工|木工|焊工)/); + if (occupationMatch) { + entities.occupation = occupationMatch[1]; + } + + return entities; + } +} + +// 单例导出 +export const intentClassifier = new IntentClassifier(); diff --git a/packages/services/conversation-service/src/infrastructure/claude/prompts/system-prompt.ts b/packages/services/conversation-service/src/infrastructure/claude/prompts/system-prompt.ts index d435319..e18b102 100644 --- a/packages/services/conversation-service/src/infrastructure/claude/prompts/system-prompt.ts +++ b/packages/services/conversation-service/src/infrastructure/claude/prompts/system-prompt.ts @@ -2,11 +2,15 @@ * System prompt builder for the immigration consultant agent */ +import { IntentResult, IntentType } from '../intent-classifier'; + export interface SystemPromptConfig { identity?: string; conversationStyle?: string; accumulatedExperience?: string; adminInstructions?: string; + /** 意图分类结果,用于指导回复策略 */ + intentHint?: IntentResult; } export const buildSystemPrompt = (config: SystemPromptConfig): string => ` @@ -76,6 +80,33 @@ ${config.identity || '专业、友善、耐心的移民顾问'} ## 对话风格 ${config.conversationStyle || '专业但不生硬,用简洁明了的语言解答'} +## 思维框架 (ReAct) +在回答每个问题时,请遵循以下思维框架: + +### 1. 理解 (Understand) +- 用户的核心问题是什么?不要被表面问题误导 +- 用户的真实需求是什么?可能是信息、确认、或具体帮助 +- 这是新问题还是基于之前对话的后续问题? + +### 2. 评估 (Evaluate) +- 我当前掌握的信息是否足够回答? +- 是否需要调用工具获取更准确的信息? +- 之前的对话是否已经回答过类似问题?避免重复 + +### 3. 行动 (Act) - 仅在必要时 +- 如果信息不足,优先调用知识库搜索 +- 如果需要用户历史,调用用户记忆工具 +- 如果需要实时信息,调用网络搜索 + +### 4. 生成 (Generate) - 核心要求 +- **直接回答核心问题**,不要绕圈子 +- **避免冗余开场白**:不要说"让我来帮您"、"非常感谢您的咨询" +- **避免冗余结束语**:不要说"希望对您有帮助"、"如果还有问题请继续问" +- **控制回复长度**:简单问题简短回答,复杂问题分点说明 +- **聚焦关键信息**:用户问什么答什么,不要过度扩展 + +${config.intentHint ? buildIntentGuidance(config.intentHint) : ''} + ## 工具使用说明 你有以下工具可以使用: @@ -101,3 +132,61 @@ ${config.adminInstructions || '暂无'} 请始终保持专业、热情的态度,帮助用户了解香港移民政策,并在适当时机引导用户使用付费评估服务。 `; + +/** + * 根据意图分类结果生成回复指导 + */ +function buildIntentGuidance(intent: IntentResult): string { + const parts: string[] = ['## 当前回复指导']; + + // 意图类型指导 + switch (intent.type) { + case IntentType.SIMPLE_QUERY: + parts.push('- **意图类型**: 简单查询'); + parts.push('- **回复策略**: 直接给出答案,无需展开解释'); + break; + case IntentType.DEEP_CONSULTATION: + parts.push('- **意图类型**: 深度咨询'); + parts.push('- **回复策略**: 提供详细信息,可分点说明,但避免重复'); + break; + case IntentType.ACTION_NEEDED: + parts.push('- **意图类型**: 需要行动'); + parts.push('- **回复策略**: 优先执行操作,然后简述结果'); + break; + case IntentType.CHAT: + parts.push('- **意图类型**: 闲聊'); + parts.push('- **回复策略**: 简短友好回复,适时引导到移民话题'); + break; + case IntentType.CLARIFICATION: + parts.push('- **意图类型**: 需要澄清'); + parts.push('- **回复策略**: 礼貌追问,帮助用户明确需求'); + break; + case IntentType.CONFIRMATION: + parts.push('- **意图类型**: 确认/否定'); + parts.push('- **回复策略**: 根据确认结果继续或调整,保持简洁'); + break; + } + + // 长度限制 + parts.push(`- **建议回复长度**: 不超过 ${intent.maxResponseLength} 字`); + + // 后续问题提示 + if (intent.isFollowUp) { + parts.push('- **注意**: 这是后续问题,请结合上下文回答,避免重复之前已说过的内容'); + } + + // 推荐工具 + if (intent.suggestedTools && intent.suggestedTools.length > 0) { + parts.push(`- **建议使用工具**: ${intent.suggestedTools.join(', ')}`); + } + + // 识别到的实体 + if (intent.entities && Object.keys(intent.entities).length > 0) { + const entityStr = Object.entries(intent.entities) + .map(([k, v]) => `${k}: ${v}`) + .join(', '); + parts.push(`- **识别到的关键信息**: ${entityStr}`); + } + + return parts.join('\n'); +} diff --git a/packages/services/conversation-service/src/infrastructure/claude/response-gate.ts b/packages/services/conversation-service/src/infrastructure/claude/response-gate.ts new file mode 100644 index 0000000..e4d5fa9 --- /dev/null +++ b/packages/services/conversation-service/src/infrastructure/claude/response-gate.ts @@ -0,0 +1,308 @@ +/** + * 回复质量门控 - 第三层 + * 检查和优化 AI 回复质量 + */ + +import { IntentType, IntentResult } from './intent-classifier'; + +export interface GateResult { + /** 是否通过门控 */ + passed: boolean; + /** 原始回复 */ + originalResponse: string; + /** 优化后的回复(如果需要) */ + optimizedResponse?: string; + /** 门控检查详情 */ + checks: GateCheck[]; + /** 建议(如果未通过) */ + suggestions?: string[]; +} + +export interface GateCheck { + name: string; + passed: boolean; + message?: string; +} + +/** + * 回复质量门控 + */ +export class ResponseGate { + // 冗余短语 + private redundantPhrases = [ + '根据我的了解', + '根据我所知', + '我来为您解答', + '让我来帮您', + '非常感谢您的咨询', + '感谢您的提问', + '希望以上信息对您有所帮助', + '如果您还有其他问题', + '祝您一切顺利', + '希望能够帮到您', + '以下是详细信息', + '以下是具体内容', + ]; + + // 重复结构 + private repetitivePatterns = [ + /首先[,,].*其次[,,].*然后[,,].*最后/, + /第一[,,].*第二[,,].*第三[,,].*第四/, + /(需要注意的是[,,]?){2,}/, + ]; + + /** + * 检查回复质量 + */ + check( + response: string, + intent: IntentResult, + userMessage: string, + ): GateResult { + const checks: GateCheck[] = []; + let optimizedResponse = response; + const suggestions: string[] = []; + + // 1. 长度检查 + const lengthCheck = this.checkLength(response, intent); + checks.push(lengthCheck); + if (!lengthCheck.passed) { + optimizedResponse = this.trimResponse(optimizedResponse, intent.maxResponseLength); + suggestions.push(`回复过长,已从 ${response.length} 字裁剪至 ${optimizedResponse.length} 字`); + } + + // 2. 相关性检查 + const relevanceCheck = this.checkRelevance(response, userMessage, intent); + checks.push(relevanceCheck); + if (!relevanceCheck.passed) { + suggestions.push('回复可能与用户问题不够相关'); + } + + // 3. 冗余检查 + const redundancyCheck = this.checkRedundancy(optimizedResponse); + checks.push(redundancyCheck); + if (!redundancyCheck.passed) { + optimizedResponse = this.removeRedundancy(optimizedResponse); + suggestions.push('已移除冗余表达'); + } + + // 4. 完整性检查 + const completenessCheck = this.checkCompleteness(response, intent); + checks.push(completenessCheck); + if (!completenessCheck.passed) { + suggestions.push('回复可能不够完整,建议补充关键信息'); + } + + // 5. 语气检查(针对闲聊) + if (intent.type === IntentType.CHAT) { + const toneCheck = this.checkTone(optimizedResponse); + checks.push(toneCheck); + } + + const allPassed = checks.every(c => c.passed); + + return { + passed: allPassed, + originalResponse: response, + optimizedResponse: allPassed ? undefined : optimizedResponse, + checks, + suggestions: suggestions.length > 0 ? suggestions : undefined, + }; + } + + /** + * 长度检查 + */ + private checkLength(response: string, intent: IntentResult): GateCheck { + const length = response.length; + const maxLength = intent.maxResponseLength; + + // 允许 20% 的弹性 + const passed = length <= maxLength * 1.2; + + return { + name: 'length', + passed, + message: passed + ? `长度合适 (${length}/${maxLength})` + : `回复过长 (${length}/${maxLength})`, + }; + } + + /** + * 相关性检查 + */ + private checkRelevance( + response: string, + userMessage: string, + intent: IntentResult, + ): GateCheck { + // 提取用户消息中的关键词 + const userKeywords = this.extractKeywords(userMessage); + + // 检查回复中是否包含关键词 + const matchedKeywords = userKeywords.filter(kw => response.includes(kw)); + const relevanceScore = userKeywords.length > 0 + ? matchedKeywords.length / userKeywords.length + : 1; + + // 如果是后续问题,降低阈值 + const threshold = intent.isFollowUp ? 0.3 : 0.5; + const passed = relevanceScore >= threshold; + + return { + name: 'relevance', + passed, + message: `相关性: ${Math.round(relevanceScore * 100)}%`, + }; + } + + /** + * 冗余检查 + */ + private checkRedundancy(response: string): GateCheck { + const foundRedundant = this.redundantPhrases.filter(phrase => + response.includes(phrase) + ); + const hasRepetitive = this.repetitivePatterns.some(pattern => + pattern.test(response) + ); + + const passed = foundRedundant.length === 0 && !hasRepetitive; + + return { + name: 'redundancy', + passed, + message: passed + ? '无冗余表达' + : `发现 ${foundRedundant.length} 处冗余`, + }; + } + + /** + * 完整性检查 + */ + private checkCompleteness(response: string, intent: IntentResult): GateCheck { + // 根据意图类型检查必要元素 + let passed = true; + let message = '回复完整'; + + switch (intent.type) { + case IntentType.DEEP_CONSULTATION: + // 深度咨询应包含具体信息 + if (response.length < 100) { + passed = false; + message = '深度咨询回复过于简短'; + } + break; + + case IntentType.ACTION_NEEDED: + // 需要行动的回复应包含结果或状态 + const hasResult = response.includes('已') || + response.includes('完成') || + response.includes('结果') || + response.includes('找到') || + response.includes('查询到'); + if (!hasResult && intent.needsTools) { + passed = false; + message = '缺少操作结果'; + } + break; + + case IntentType.CLARIFICATION: + // 澄清应包含问句 + if (!response.includes('?') && !response.includes('?')) { + passed = false; + message = '澄清应包含追问'; + } + break; + } + + return { name: 'completeness', passed, message }; + } + + /** + * 语气检查 + */ + private checkTone(response: string): GateCheck { + // 闲聊应该简短友好 + const isFriendly = response.includes('您好') || + response.includes('!') || + response.length < 100; + + return { + name: 'tone', + passed: isFriendly, + message: isFriendly ? '语气友好' : '可以更友好些', + }; + } + + /** + * 提取关键词 + */ + private extractKeywords(text: string): string[] { + // 移除常见停用词 + const stopWords = ['的', '是', '在', '我', '有', '和', '与', '了', '着', '也', '就', '都', '而', '及', '到', '把', '被', '让', '给', '对', '从', '以', '为', '这', '那', '它', '他', '她', '吗', '呢', '吧', '啊', '哦', '嗯']; + + // 简单分词(按标点和空格) + const words = text.split(/[,。!?、;:""''【】()\s]+/).filter(w => + w.length >= 2 && !stopWords.includes(w) + ); + + return [...new Set(words)]; + } + + /** + * 裁剪回复 + */ + private trimResponse(response: string, maxLength: number): string { + if (response.length <= maxLength) return response; + + // 尝试在句子边界裁剪 + const sentences = response.split(/(?<=[。!?])/); + let trimmed = ''; + + for (const sentence of sentences) { + if ((trimmed + sentence).length <= maxLength) { + trimmed += sentence; + } else { + break; + } + } + + // 如果没有找到合适的句子边界,强制裁剪 + if (trimmed.length < maxLength * 0.5) { + trimmed = response.substring(0, maxLength - 3) + '...'; + } + + return trimmed; + } + + /** + * 移除冗余表达 + */ + private removeRedundancy(response: string): string { + let cleaned = response; + + for (const phrase of this.redundantPhrases) { + cleaned = cleaned.replace(new RegExp(phrase + '[,,。]?', 'g'), ''); + } + + // 清理多余空格和开头标点 + cleaned = cleaned.replace(/^\s*[,,。]+/, '').trim(); + + return cleaned; + } + + /** + * 快速优化回复(不做完整检查) + */ + optimize(response: string, maxLength: number): string { + let optimized = this.removeRedundancy(response); + optimized = this.trimResponse(optimized, maxLength); + return optimized; + } +} + +// 单例导出 +export const responseGate = new ResponseGate();