feat(agent): implement 3-layer architecture for better response quality
Implement a three-layer architecture to improve AI response quality: Layer 1 - Intent Classifier (intent-classifier.ts): - Classifies user intent into 6 types: SIMPLE_QUERY, DEEP_CONSULTATION, ACTION_NEEDED, CHAT, CLARIFICATION, CONFIRMATION - Determines suggested response length based on intent type - Detects follow-up questions and extracts entities (visa types, etc.) - Uses keyword matching for fast classification (no API calls) Layer 2 - ReAct Agent (system-prompt.ts): - Adds ReAct thinking framework to system prompt - 4-step process: Understand -> Evaluate -> Act -> Generate - Emphasizes concise responses, avoids redundant phrases - Injects intent classification results to guide response strategy Layer 3 - Response Gate (response-gate.ts): - Quality checks: length, relevance, redundancy, completeness, tone - Logs gate results for analysis and future optimization - Can trim responses and remove redundant expressions Integration (claude-agent.service.ts): - Integrates all 3 layers in sendMessage flow - Dynamically adjusts max_tokens based on intent type - Collects full response for gate analysis Documentation: - Added AGENT_THREE_LAYER_ARCHITECTURE.md with detailed design docs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ad0f904f98
commit
d9b4c72894
|
|
@ -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 思维模式:
|
||||
|
||||
```
|
||||
<thinking_framework>
|
||||
在回答用户问题时,请遵循以下思维框架:
|
||||
|
||||
1. 理解 (Understand)
|
||||
- 用户的核心问题是什么?
|
||||
- 用户的真实需求是什么?
|
||||
- 这是后续问题还是新问题?
|
||||
|
||||
2. 评估 (Evaluate)
|
||||
- 我当前掌握的信息是否足够?
|
||||
- 是否需要调用工具获取更多信息?
|
||||
- 之前的对话是否已经回答过类似问题?
|
||||
|
||||
3. 行动 (Act)
|
||||
- 如果信息不足,调用合适的工具
|
||||
- 优先使用:知识库搜索 > 经验召回 > 网络搜索
|
||||
|
||||
4. 生成 (Generate)
|
||||
- 直接回答核心问题
|
||||
- 避免冗余的开场白和结束语
|
||||
- 控制回复长度,抓住重点
|
||||
</thinking_framework>
|
||||
```
|
||||
|
||||
### 工具循环
|
||||
|
||||
- 最多支持 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 测试** - 对比优化前后的用户满意度
|
||||
|
|
@ -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<StreamChunk> {
|
||||
// ========== 第一层:意图分类 ==========
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, string>;
|
||||
/** 是否为后续问题(基于上下文) */
|
||||
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<string, string[]> = {
|
||||
'knowledge-search': ['知识', '文章', '政策', '规定', '信息', '资料'],
|
||||
'search-web': ['最新', '新闻', '近期', '实时', '当前', '现在'],
|
||||
'get-weather': ['天气', '温度', '下雨', '晴天'],
|
||||
'experience-recall': ['之前', '上次', '经验', '类似'],
|
||||
'user-memory-recall': ['我之前', '我上次', '我的', '记得我'],
|
||||
'immigration-assessment': ['评估', '打分', '分数', '资格'],
|
||||
'calculate': ['计算', '算', '多少钱', '费用'],
|
||||
};
|
||||
|
||||
// 移民相关类别关键词
|
||||
private immigrationCategories: Record<string, string[]> = {
|
||||
'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<string, string> {
|
||||
const entities: Record<string, string> = {};
|
||||
|
||||
// 签证子类
|
||||
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();
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
Loading…
Reference in New Issue