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:
hailin 2026-01-23 07:51:19 -08:00
parent ad0f904f98
commit d9b4c72894
5 changed files with 1006 additions and 1 deletions

View File

@ -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签证要多少分
AI189签证需要至少65分才能提交EOI。
目前获邀分数线约为85-95分具体取决于职业。
主要加分项:年龄、英语、工作经验、学历、州担保等。
```
## 文件清单
| 文件 | 状态 | 说明 |
|------|------|------|
| `intent-classifier.ts` | ✅ 已创建 | 意图分类器 |
| `response-gate.ts` | ✅ 已创建 | 回复质量门控 |
| `system-prompt.ts` | 🔄 待更新 | 加入 ReAct 框架 |
| `claude-agent.service.ts` | 🔄 待更新 | 集成三层架构 |
## 后续优化方向
1. **机器学习分类** - 用训练数据替代规则分类
2. **动态长度调整** - 根据反馈自动调整建议长度
3. **个性化门控** - 根据用户偏好调整检查规则
4. **A/B 测试** - 对比优化前后的用户满意度

View File

@ -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;
}

View File

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

View File

@ -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');
}

View File

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