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();