iconsulting/packages/services/conversation-service/src/infrastructure/claude/strategy/strategy-engine.service.ts

576 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 策略执行引擎
*
* 负责:
* 1. 管理对话的咨询阶段状态
* 2. 判断阶段转移
* 3. 生成阶段引导指令注入到System Prompt
* 4. 从对话中提取用户信息
*/
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Anthropic from '@anthropic-ai/sdk';
import {
DEFAULT_CONSULTING_STRATEGY,
ConsultingStage,
ConsultingStrategy,
StageId,
DirectiveType,
RequiredInfo,
TransitionCondition,
ConversionPath,
getStageById,
getNextStage,
getUncollectedInfo,
detectUserIntents,
CORE_USER_INFO,
} from './default-strategy';
/**
* 对话的咨询状态
*/
export interface ConsultingState {
// 当前使用的策略ID
strategyId: string;
// 当前阶段
currentStageId: StageId;
// 当前阶段已进行的轮次
stageTurnCount: number;
// 已收集的用户信息
collectedInfo: Record<string, unknown>;
// 评估结果(如果已评估)
assessmentResult?: {
recommendedPrograms: string[]; // 推荐的移民类型
suitabilityScore: number; // 匹配度 0-100
highlights: string[]; // 优势
concerns: string[]; // 需要注意的点
};
// 用户选择的转化路径
conversionPath?: ConversionPath;
// 阶段历史
stageHistory: Array<{
stageId: StageId;
enteredAt: Date;
exitedAt?: Date;
turnsInStage: number;
}>;
}
/**
* 策略引擎服务
*/
@Injectable()
export class StrategyEngineService {
private client: Anthropic;
constructor(private configService: ConfigService) {
const baseUrl = this.configService.get<string>('ANTHROPIC_BASE_URL');
const isProxyUrl = baseUrl && baseUrl.match(/^\d+\.\d+\.\d+\.\d+/);
if (isProxyUrl) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
this.client = new Anthropic({
apiKey: this.configService.get<string>('ANTHROPIC_API_KEY'),
baseURL: baseUrl || undefined,
});
}
/**
* 初始化新对话的咨询状态
*/
initializeState(): ConsultingState {
return {
strategyId: DEFAULT_CONSULTING_STRATEGY.id,
currentStageId: StageId.GREETING,
stageTurnCount: 0,
collectedInfo: {},
stageHistory: [
{
stageId: StageId.GREETING,
enteredAt: new Date(),
turnsInStage: 0,
},
],
};
}
/**
* 获取当前策略
*/
getStrategy(): ConsultingStrategy {
// 目前只有默认策略,后续可从数据库加载
return DEFAULT_CONSULTING_STRATEGY;
}
/**
* 获取当前阶段
*/
getCurrentStage(state: ConsultingState): ConsultingStage {
const stage = getStageById(state.currentStageId);
if (!stage) {
// 如果阶段无效,回到开场
return getStageById(StageId.GREETING)!;
}
return stage;
}
/**
* 评估是否需要转移阶段
*/
async evaluateTransition(
state: ConsultingState,
userMessage: string,
): Promise<{ shouldTransition: boolean; targetStageId?: StageId; reason?: string }> {
const currentStage = this.getCurrentStage(state);
const userIntents = detectUserIntents(userMessage);
// 按优先级检查每个转移条件
const sortedTransitions = [...currentStage.transitions].sort((a, b) => b.priority - a.priority);
for (const transition of sortedTransitions) {
const conditionsMet = await this.checkConditions(
transition.conditions,
state,
userMessage,
userIntents,
);
if (conditionsMet) {
return {
shouldTransition: true,
targetStageId: transition.targetStageId,
reason: transition.description,
};
}
}
return { shouldTransition: false };
}
/**
* 检查转移条件是否满足
*/
private async checkConditions(
conditions: TransitionCondition[],
state: ConsultingState,
userMessage: string,
userIntents: string[],
): Promise<boolean> {
// 所有条件都要满足AND 逻辑)
for (const condition of conditions) {
const met = await this.checkSingleCondition(condition, state, userMessage, userIntents);
if (!met) return false;
}
return true;
}
/**
* 检查单个条件
*/
private async checkSingleCondition(
condition: TransitionCondition,
state: ConsultingState,
userMessage: string,
userIntents: string[],
): Promise<boolean> {
switch (condition.type) {
case 'INFO_COLLECTED':
if (condition.infoKeys) {
const collectedCount = condition.infoKeys.filter(
key => state.collectedInfo[key] !== undefined
).length;
if (condition.minCount) {
return collectedCount >= condition.minCount;
}
return collectedCount === condition.infoKeys.length;
}
return false;
case 'USER_INTENT':
if (condition.intents) {
return condition.intents.some(intent => userIntents.includes(intent));
}
return false;
case 'TURNS_EXCEEDED':
if (condition.maxTurns !== undefined) {
return state.stageTurnCount >= condition.maxTurns;
}
return false;
case 'KEYWORD':
if (condition.keywords) {
const lowerMessage = userMessage.toLowerCase();
return condition.keywords.some(kw => lowerMessage.includes(kw.toLowerCase()));
}
return false;
case 'ASSESSMENT_DONE':
return state.assessmentResult !== undefined;
case 'CUSTOM':
// 自定义条件用LLM判断
if (condition.customCondition) {
return this.evaluateCustomCondition(condition.customCondition, userMessage, state);
}
return false;
default:
return false;
}
}
/**
* 用LLM评估自定义条件
*/
private async evaluateCustomCondition(
condition: string,
userMessage: string,
state: ConsultingState,
): Promise<boolean> {
try {
const response = await this.client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 10,
messages: [
{
role: 'user',
content: `判断以下条件是否满足,只回答 true 或 false
条件:${condition}
用户最新消息:${userMessage}
已收集的用户信息:${JSON.stringify(state.collectedInfo)}
回答true/false`,
},
],
});
const text = response.content[0].type === 'text' ? response.content[0].text : '';
return text.toLowerCase().includes('true');
} catch {
return false;
}
}
/**
* 执行阶段转移
*/
transitionToStage(state: ConsultingState, targetStageId: StageId): ConsultingState {
// 更新历史记录
const currentHistory = state.stageHistory[state.stageHistory.length - 1];
if (currentHistory) {
currentHistory.exitedAt = new Date();
currentHistory.turnsInStage = state.stageTurnCount;
}
// 创建新状态
return {
...state,
currentStageId: targetStageId,
stageTurnCount: 0,
stageHistory: [
...state.stageHistory,
{
stageId: targetStageId,
enteredAt: new Date(),
turnsInStage: 0,
},
],
};
}
/**
* 增加阶段轮次
*/
incrementTurnCount(state: ConsultingState): ConsultingState {
return {
...state,
stageTurnCount: state.stageTurnCount + 1,
};
}
/**
* 从对话中提取用户信息
*/
async extractUserInfo(
userMessage: string,
assistantMessage: string,
existingInfo: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const infoToExtract = CORE_USER_INFO.filter(info => !existingInfo[info.key]);
if (infoToExtract.length === 0) {
return {};
}
try {
const response = await this.client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 500,
messages: [
{
role: 'user',
content: `从以下对话中提取用户信息。只提取能明确确定的信息,不要猜测。
用户消息:${userMessage}
助手回复:${assistantMessage}
需要提取的字段:
${infoToExtract.map(i => `- ${i.key}: ${i.description}`).join('\n')}
返回JSON格式只包含能确定的字段。如果没有任何信息可提取返回 {}
示例:{"age": 35, "education": "硕士"}
JSON`,
},
],
});
const text = response.content[0].type === 'text' ? response.content[0].text : '{}';
// 提取JSON部分
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
return JSON.parse(jsonMatch[0]);
}
return {};
} catch (error) {
console.error('[StrategyEngine] Failed to extract user info:', error);
return {};
}
}
/**
* 执行资格评估
*/
async performAssessment(
collectedInfo: Record<string, unknown>,
): Promise<ConsultingState['assessmentResult']> {
const age = collectedInfo.age as number | undefined;
const education = collectedInfo.education as string | undefined;
const university = collectedInfo.university as string | undefined;
const workYears = collectedInfo.workYears as number | undefined;
const annualIncome = collectedInfo.annualIncome as number | undefined;
const recommendedPrograms: string[] = [];
const highlights: string[] = [];
const concerns: string[] = [];
let suitabilityScore = 50;
// 高才通A类年薪250万港币以上约220万人民币
if (annualIncome && annualIncome >= 2200000) {
recommendedPrograms.push('高才通A类');
highlights.push('年薪达到高才通A类标准250万港币');
suitabilityScore += 30;
}
// 高才通B类全球百强大学+3年以上工作经验
const top100Universities = ['清华', '北大', '复旦', '上海交通', '浙大', '中科大', '南京大学', '香港大学', '港中文', '港科大'];
const isTop100 = university && top100Universities.some(u => university.includes(u));
if (isTop100 && workYears && workYears >= 3) {
recommendedPrograms.push('高才通B类');
highlights.push('名校毕业+工作经验充足');
suitabilityScore += 25;
}
// 高才通C类全球百强大学毕业工作经验不足3年
if (isTop100 && (!workYears || workYears < 3)) {
recommendedPrograms.push('高才通C类');
highlights.push('名校毕业');
concerns.push('工作经验较少C类有年度名额限制');
suitabilityScore += 15;
}
// 优才计划
if (education && ['硕士', '博士', 'MBA'].some(e => education.includes(e))) {
recommendedPrograms.push('优才计划');
highlights.push('高学历加分');
suitabilityScore += 15;
}
if (workYears && workYears >= 5) {
if (!recommendedPrograms.includes('优才计划')) {
recommendedPrograms.push('优才计划');
}
highlights.push('工作经验丰富');
suitabilityScore += 10;
}
// 专才计划:需要香港雇主
if (collectedInfo.hasHKConnection) {
recommendedPrograms.push('专才计划');
highlights.push('有香港雇主/工作机会');
suitabilityScore += 20;
}
// 年龄因素
if (age) {
if (age >= 18 && age <= 39) {
highlights.push('年龄优势明显');
suitabilityScore += 10;
} else if (age >= 40 && age <= 50) {
concerns.push('年龄在优才计划中扣分');
} else if (age > 50) {
concerns.push('年龄偏大,建议尽快申请');
}
}
// 如果没有明确推荐,默认推荐优才
if (recommendedPrograms.length === 0) {
recommendedPrograms.push('优才计划');
concerns.push('需要更多信息做精准评估');
}
return {
recommendedPrograms: [...new Set(recommendedPrograms)], // 去重
suitabilityScore: Math.min(100, suitabilityScore),
highlights,
concerns,
};
}
/**
* 构建阶段引导注入到System Prompt
*/
buildStageGuidance(state: ConsultingState): string {
const strategy = this.getStrategy();
const stage = this.getCurrentStage(state);
const uncollectedInfo = getUncollectedInfo(stage.id, state.collectedInfo);
const parts: string[] = [];
// ========== 当前阶段信息 ==========
parts.push('## 【咨询流程控制】');
parts.push('');
parts.push(`### 当前阶段:${stage.name}(第${stage.order}阶段共8阶段`);
parts.push(`**阶段目标**${stage.goal}`);
parts.push(`**当前轮次**${state.stageTurnCount + 1}/${stage.maxTurns}`);
parts.push('');
// ========== 行为指令 ==========
parts.push('### 行为指令(必须遵守)');
const allDirectives = [...strategy.globalDirectives, ...stage.directives]
.sort((a, b) => b.priority - a.priority);
for (const directive of allDirectives) {
const prefix = {
[DirectiveType.MUST_DO]: '✓ 必须',
[DirectiveType.SHOULD_DO]: '○ 建议',
[DirectiveType.MUST_NOT]: '✗ 禁止',
[DirectiveType.PREFER]: '☆ 优先',
}[directive.type];
parts.push(`${prefix}${directive.content}`);
}
parts.push('');
// ========== 信息收集进度 ==========
if (Object.keys(state.collectedInfo).length > 0 || uncollectedInfo.length > 0) {
parts.push('### 用户信息收集进度');
// 已收集的
for (const [key, value] of Object.entries(state.collectedInfo)) {
const info = CORE_USER_INFO.find(i => i.key === key);
parts.push(`${info?.label || key}${value}`);
}
// 未收集的(当前阶段需要的)
for (const info of uncollectedInfo.slice(0, 3)) { // 最多显示3个
parts.push(`${info.label}:待收集(建议问法:"${info.askingStrategy}"`);
}
parts.push('');
}
// ========== 评估结果 ==========
if (state.assessmentResult) {
parts.push('### 评估结果(基于已收集信息)');
parts.push(`**推荐方案**${state.assessmentResult.recommendedPrograms.join('、')}`);
parts.push(`**匹配度**${state.assessmentResult.suitabilityScore}`);
if (state.assessmentResult.highlights.length > 0) {
parts.push(`**优势**${state.assessmentResult.highlights.join('')}`);
}
if (state.assessmentResult.concerns.length > 0) {
parts.push(`**注意事项**${state.assessmentResult.concerns.join('')}`);
}
parts.push('');
}
// ========== 阶段转移提示 ==========
parts.push('### 阶段转移判断');
parts.push('根据用户回复,判断是否满足以下转移条件:');
for (const transition of stage.transitions.slice(0, 3)) {
parts.push(`- 如果【${transition.description}】→ 进入【${getStageById(transition.targetStageId)?.name}】阶段`);
}
parts.push('');
// ========== 专家联系方式(如果在转化/对接阶段)==========
if ([StageId.CONVERSION, StageId.HANDOFF].includes(state.currentStageId)) {
parts.push('### 可用资源');
parts.push(`- 付费评估服务:¥${strategy.paidServices.assessmentPrice}${strategy.paidServices.description}`);
parts.push(`- 人类专家微信:${strategy.expertContact.wechat}`);
parts.push(`- 专家电话:${strategy.expertContact.phone}`);
parts.push(`- 工作时间:${strategy.expertContact.workingHours}`);
parts.push('');
}
// ========== 回复要求 ==========
parts.push('### 回复要求');
parts.push(`- 建议长度:${stage.suggestedResponseLength}字左右`);
parts.push('- 结尾必须有推进对话的问题或明确的下一步引导');
parts.push('- 不要一次给太多信息,要循序渐进');
return parts.join('\n');
}
/**
* 更新收集的信息
*/
updateCollectedInfo(
state: ConsultingState,
newInfo: Record<string, unknown>,
): ConsultingState {
return {
...state,
collectedInfo: {
...state.collectedInfo,
...newInfo,
},
};
}
/**
* 设置评估结果
*/
setAssessmentResult(
state: ConsultingState,
result: ConsultingState['assessmentResult'],
): ConsultingState {
return {
...state,
assessmentResult: result,
};
}
/**
* 设置转化路径
*/
setConversionPath(
state: ConsultingState,
path: ConversionPath,
): ConsultingState {
return {
...state,
conversionPath: path,
};
}
}