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 { ImmigrationToolsService } from './tools/immigration-tools.service';
|
||||||
import { buildSystemPrompt, SystemPromptConfig } from './prompts/system-prompt';
|
import { buildSystemPrompt, SystemPromptConfig } from './prompts/system-prompt';
|
||||||
import { KnowledgeClientService } from '../knowledge/knowledge-client.service';
|
import { KnowledgeClientService } from '../knowledge/knowledge-client.service';
|
||||||
|
import { intentClassifier, IntentResult, IntentType } from './intent-classifier';
|
||||||
|
import { responseGate } from './response-gate';
|
||||||
|
|
||||||
export interface FileAttachment {
|
export interface FileAttachment {
|
||||||
id: string;
|
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
|
* 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
|
* Send a message and get streaming response with tool loop support
|
||||||
* Uses Prompt Caching to reduce costs (~90% savings on cached system prompt)
|
* Uses Prompt Caching to reduce costs (~90% savings on cached system prompt)
|
||||||
* Supports multimodal messages with image attachments
|
* Supports multimodal messages with image attachments
|
||||||
|
* Implements 3-layer architecture: Intent Classification -> ReAct Agent -> Response Gate
|
||||||
*/
|
*/
|
||||||
async *sendMessage(
|
async *sendMessage(
|
||||||
message: string,
|
message: string,
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
attachments?: FileAttachment[],
|
attachments?: FileAttachment[],
|
||||||
): AsyncGenerator<StreamChunk> {
|
): 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();
|
const tools = this.immigrationToolsService.getTools();
|
||||||
|
|
||||||
// Fetch relevant system experiences and inject into prompt
|
// Fetch relevant system experiences and inject into prompt
|
||||||
|
|
@ -178,6 +219,7 @@ export class ClaudeAgentService implements OnModuleInit {
|
||||||
const dynamicConfig: SystemPromptConfig = {
|
const dynamicConfig: SystemPromptConfig = {
|
||||||
...this.systemPromptConfig,
|
...this.systemPromptConfig,
|
||||||
accumulatedExperience,
|
accumulatedExperience,
|
||||||
|
intentHint: intent, // 注入意图分类结果
|
||||||
};
|
};
|
||||||
const systemPrompt = buildSystemPrompt(dynamicConfig);
|
const systemPrompt = buildSystemPrompt(dynamicConfig);
|
||||||
|
|
||||||
|
|
@ -221,6 +263,9 @@ export class ClaudeAgentService implements OnModuleInit {
|
||||||
const maxIterations = 10; // Safety limit
|
const maxIterations = 10; // Safety limit
|
||||||
let iterations = 0;
|
let iterations = 0;
|
||||||
|
|
||||||
|
// 根据意图分类调整 max_tokens
|
||||||
|
const maxTokens = this.calculateMaxTokens(intent);
|
||||||
|
|
||||||
// System prompt with cache_control for Prompt Caching
|
// System prompt with cache_control for Prompt Caching
|
||||||
// Cache TTL is 5 minutes, cache hits cost only 10% of normal input price
|
// Cache TTL is 5 minutes, cache hits cost only 10% of normal input price
|
||||||
const systemWithCache: Anthropic.TextBlockParam[] = [
|
const systemWithCache: Anthropic.TextBlockParam[] = [
|
||||||
|
|
@ -231,6 +276,9 @@ export class ClaudeAgentService implements OnModuleInit {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 用于收集完整响应以进行门控检查
|
||||||
|
let fullResponseText = '';
|
||||||
|
|
||||||
while (iterations < maxIterations) {
|
while (iterations < maxIterations) {
|
||||||
iterations++;
|
iterations++;
|
||||||
|
|
||||||
|
|
@ -238,7 +286,7 @@ export class ClaudeAgentService implements OnModuleInit {
|
||||||
// Create streaming message with cached system prompt
|
// Create streaming message with cached system prompt
|
||||||
const stream = await this.client.messages.stream({
|
const stream = await this.client.messages.stream({
|
||||||
model: 'claude-sonnet-4-20250514',
|
model: 'claude-sonnet-4-20250514',
|
||||||
max_tokens: 4096,
|
max_tokens: maxTokens,
|
||||||
system: systemWithCache,
|
system: systemWithCache,
|
||||||
messages,
|
messages,
|
||||||
tools: tools as Anthropic.Tool[],
|
tools: tools as Anthropic.Tool[],
|
||||||
|
|
@ -269,6 +317,7 @@ export class ClaudeAgentService implements OnModuleInit {
|
||||||
} else if (event.type === 'content_block_delta') {
|
} else if (event.type === 'content_block_delta') {
|
||||||
if (event.delta.type === 'text_delta') {
|
if (event.delta.type === 'text_delta') {
|
||||||
hasText = true;
|
hasText = true;
|
||||||
|
fullResponseText += event.delta.text; // 收集完整响应
|
||||||
yield {
|
yield {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
content: event.delta.text,
|
content: event.delta.text,
|
||||||
|
|
@ -305,6 +354,14 @@ export class ClaudeAgentService implements OnModuleInit {
|
||||||
|
|
||||||
// If no tool uses, we're done
|
// If no tool uses, we're done
|
||||||
if (toolUses.length === 0) {
|
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' };
|
yield { type: 'end' };
|
||||||
return;
|
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
|
* System prompt builder for the immigration consultant agent
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { IntentResult, IntentType } from '../intent-classifier';
|
||||||
|
|
||||||
export interface SystemPromptConfig {
|
export interface SystemPromptConfig {
|
||||||
identity?: string;
|
identity?: string;
|
||||||
conversationStyle?: string;
|
conversationStyle?: string;
|
||||||
accumulatedExperience?: string;
|
accumulatedExperience?: string;
|
||||||
adminInstructions?: string;
|
adminInstructions?: string;
|
||||||
|
/** 意图分类结果,用于指导回复策略 */
|
||||||
|
intentHint?: IntentResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const buildSystemPrompt = (config: SystemPromptConfig): string => `
|
export const buildSystemPrompt = (config: SystemPromptConfig): string => `
|
||||||
|
|
@ -76,6 +80,33 @@ ${config.identity || '专业、友善、耐心的移民顾问'}
|
||||||
## 对话风格
|
## 对话风格
|
||||||
${config.conversationStyle || '专业但不生硬,用简洁明了的语言解答'}
|
${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