feat(agents): add 4-layer response quality control — structured outputs, LLM judge, smart truncation
AI回复质量硬约束系统,解决核心问题:AI无法用最少的语言精准回答用户问题。
## 四层防线架构
### Layer 1 — Prompt 优化 (软约束)
- coordinator-system-prompt.ts: 新增"最高优先级原则:精准回答"章节
- 意图分类表(7种)+ 每种对应长度和回答策略
- 错误示范 vs 正确示范对比
- "宁可太短,不可太长"原则
- 最终提醒三条:精准回答 > 准确性 > 简洁就是专业
- policy-expert-prompt.ts: 精简输出格式
- objection-handler-prompt.ts: 微调
### Layer 2 — Structured Outputs (格式约束)
- 新文件 coordinator-response.schema.ts: Zod schema 定义
- intent: 7种意图分类 (factual/yes_no/comparison/assessment/objection/detailed/casual)
- answer: 回复文本
- followUp: 可选跟进问题
- agent-loop.ts: 通过 output_config 传入 Claude API,强制 JSON 输出
- 流式模式下抑制 text delta(JSON 片段不展示给用户)
- 流结束后解析 JSON,提取 answer 字段 yield 给前端
- JSON 解析失败时回退到原始文本(安全降级)
- coordinator-agent.service.ts: 传入 zodOutputFormat(CoordinatorResponseSchema)
- agent.types.ts: AgentLoopParams 新增 outputConfig 字段
### Layer 3 — LLM-as-Judge (语义质检)
- evaluation-rule.entity.ts: 新增 LLM_JUDGE 规则类型(第9种)
- evaluation-gate.service.ts:
- 注入 ConfigService + 初始化 Anthropic client (Haiku 4.5)
- evaluateRule 改为 async(支持异步 LLM 调用)
- 新增 checkLlmJudge():评估 relevance/conciseness/noise 三维度
- 可配置阈值:minRelevance(7), minConciseness(6), maxNoise(3)
- 5s 超时 + 异常默认通过(非阻塞)
- EvaluationContext 新增 userMessage 字段
- coordinator-agent.service.ts: 传入 userMessage 到评估门控
### Layer 4 — 程序级硬截断 (物理约束)
- coordinator-response.schema.ts:
- INTENT_MAX_ANSWER_LENGTH: 按意图限制字符数
factual=200, yes_no=120, comparison=250, assessment=400,
objection=200, detailed=500, casual=80
- MAX_FOLLOWUP_LENGTH: 80 字符
- smartTruncate(): 在句子边界处智能截断(中英文标点)
- agent-loop.ts: JSON 解析后按 intent 强制截断 answer 和 followUp
- max_tokens 从 4096 降至 2048
## Bug 修复
- agent-loop.ts: currentTextContent 在 content_block_stop 时被重置为空字符串,
导致评估门控收到空文本。改为从 finalMessage.content 提取 responseText。
## 依赖升级
- @anthropic-ai/sdk: 0.52.0 → 0.73.0 (支持 output_config)
- 新增 zod@4.3.6 (Structured Output schema 定义)
## 文件清单 (1 new + 10 modified)
- NEW: agents/schemas/coordinator-response.schema.ts
- MOD: agents/coordinator/agent-loop.ts (核心改造)
- MOD: agents/coordinator/coordinator-agent.service.ts
- MOD: agents/coordinator/evaluation-gate.service.ts
- MOD: agents/types/agent.types.ts
- MOD: agents/prompts/coordinator-system-prompt.ts
- MOD: agents/prompts/policy-expert-prompt.ts
- MOD: agents/prompts/objection-handler-prompt.ts
- MOD: domain/entities/evaluation-rule.entity.ts
- MOD: package.json + pnpm-lock.yaml
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
93ed3343de
commit
bb1a1139a3
|
|
@ -20,7 +20,7 @@
|
||||||
"migration:generate": "npm run typeorm migration:generate -- -d src/data-source.ts"
|
"migration:generate": "npm run typeorm migration:generate -- -d src/data-source.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.52.0",
|
"@anthropic-ai/sdk": "^0.73.0",
|
||||||
"@iconsulting/shared": "workspace:*",
|
"@iconsulting/shared": "workspace:*",
|
||||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
|
|
@ -40,7 +40,8 @@
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.0",
|
||||||
"socket.io": "^4.8.3",
|
"socket.io": "^4.8.3",
|
||||||
"typeorm": "^0.3.19",
|
"typeorm": "^0.3.19",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0",
|
||||||
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export const EvaluationRuleType = {
|
||||||
CONVERSION_SIGNAL: 'CONVERSION_SIGNAL',
|
CONVERSION_SIGNAL: 'CONVERSION_SIGNAL',
|
||||||
TOPIC_BOUNDARY: 'TOPIC_BOUNDARY',
|
TOPIC_BOUNDARY: 'TOPIC_BOUNDARY',
|
||||||
NO_FABRICATION: 'NO_FABRICATION',
|
NO_FABRICATION: 'NO_FABRICATION',
|
||||||
|
LLM_JUDGE: 'LLM_JUDGE',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type EvaluationRuleTypeValue =
|
export type EvaluationRuleTypeValue =
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,11 @@ import {
|
||||||
isAgentInvocationTool,
|
isAgentInvocationTool,
|
||||||
getToolsForClaudeAPI,
|
getToolsForClaudeAPI,
|
||||||
} from '../tools/coordinator-tools';
|
} from '../tools/coordinator-tools';
|
||||||
|
import {
|
||||||
|
INTENT_MAX_ANSWER_LENGTH,
|
||||||
|
MAX_FOLLOWUP_LENGTH,
|
||||||
|
smartTruncate,
|
||||||
|
} from '../schemas/coordinator-response.schema';
|
||||||
|
|
||||||
const logger = new Logger('AgentLoop');
|
const logger = new Logger('AgentLoop');
|
||||||
|
|
||||||
|
|
@ -152,8 +157,9 @@ export async function* agentLoop(
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
messages: messages as any,
|
messages: messages as any,
|
||||||
tools: getToolsForClaudeAPI(additionalTools) as any,
|
tools: getToolsForClaudeAPI(additionalTools) as any,
|
||||||
max_tokens: 4096,
|
max_tokens: 2048,
|
||||||
});
|
...(params.outputConfig ? { output_config: params.outputConfig } : {}),
|
||||||
|
} as any);
|
||||||
break; // success
|
break; // success
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
const isRateLimit = error?.status === 429 || error?.error?.type === 'rate_limit_error';
|
const isRateLimit = error?.status === 429 || error?.error?.type === 'rate_limit_error';
|
||||||
|
|
@ -207,11 +213,14 @@ export async function* agentLoop(
|
||||||
|
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
currentTextContent += delta.text;
|
currentTextContent += delta.text;
|
||||||
yield {
|
// Structured Output 模式下不直接 yield text(JSON 片段不能展示给用户)
|
||||||
type: 'text',
|
if (!params.outputConfig) {
|
||||||
content: delta.text,
|
yield {
|
||||||
timestamp: Date.now(),
|
type: 'text',
|
||||||
};
|
content: delta.text,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
// Tool input being streamed — accumulate silently
|
// Tool input being streamed — accumulate silently
|
||||||
}
|
}
|
||||||
|
|
@ -313,11 +322,51 @@ export async function* agentLoop(
|
||||||
|
|
||||||
// If no tool_use → conversation is done (with optional evaluation gate)
|
// If no tool_use → conversation is done (with optional evaluation gate)
|
||||||
if (toolUseBlocks.length === 0 || finalMessage.stop_reason === 'end_turn') {
|
if (toolUseBlocks.length === 0 || finalMessage.stop_reason === 'end_turn') {
|
||||||
|
// ---- Extract response text from finalMessage (修复 bug:currentTextContent 在 content_block_stop 时已被重置为空) ----
|
||||||
|
const responseText = finalMessage.content
|
||||||
|
.filter((b): b is Anthropic.TextBlock => b.type === 'text')
|
||||||
|
.map(b => b.text)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// ---- Structured Output 解析:从 JSON 中提取 answer,强制截断,yield ----
|
||||||
|
if (params.outputConfig && responseText) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(responseText);
|
||||||
|
if (parsed.answer) {
|
||||||
|
// 按 intent 强制截断 answer
|
||||||
|
const maxLen = INTENT_MAX_ANSWER_LENGTH[parsed.intent] || 300;
|
||||||
|
const originalLen = parsed.answer.length;
|
||||||
|
const answer = smartTruncate(parsed.answer, maxLen);
|
||||||
|
|
||||||
|
if (originalLen > maxLen) {
|
||||||
|
logger.debug(
|
||||||
|
`[Turn ${currentTurn + 1}] Answer truncated: ${originalLen} → ${answer.length} chars (intent=${parsed.intent}, limit=${maxLen})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield { type: 'text', content: answer, timestamp: Date.now() };
|
||||||
|
|
||||||
|
if (parsed.followUp) {
|
||||||
|
const followUp = smartTruncate(parsed.followUp, MAX_FOLLOWUP_LENGTH);
|
||||||
|
yield { type: 'text', content: '\n\n' + followUp, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// JSON 合法但缺少 answer 字段 → yield 原始文本
|
||||||
|
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
logger.debug(`[Turn ${currentTurn + 1}] Structured output intent: ${parsed.intent}`);
|
||||||
|
} catch {
|
||||||
|
// JSON 解析失败 → 回退到原始文本
|
||||||
|
logger.warn(`[Turn ${currentTurn + 1}] Structured output parse failed, falling back to raw text`);
|
||||||
|
yield { type: 'text', content: responseText, timestamp: Date.now() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Evaluation Gate (optional, zero-config safe) ---
|
// --- Evaluation Gate (optional, zero-config safe) ---
|
||||||
if (params.evaluationGate) {
|
if (params.evaluationGate) {
|
||||||
try {
|
try {
|
||||||
const gateResult = await params.evaluationGate(
|
const gateResult = await params.evaluationGate(
|
||||||
currentTextContent,
|
responseText,
|
||||||
currentTurn + 1,
|
currentTurn + 1,
|
||||||
agentsUsed,
|
agentsUsed,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,10 @@ import {
|
||||||
CoordinatorPromptConfig,
|
CoordinatorPromptConfig,
|
||||||
} from '../prompts/coordinator-system-prompt';
|
} from '../prompts/coordinator-system-prompt';
|
||||||
|
|
||||||
|
// Structured Output
|
||||||
|
import { zodOutputFormat } from '@anthropic-ai/sdk/helpers/zod';
|
||||||
|
import { CoordinatorResponseSchema } from '../schemas/coordinator-response.schema';
|
||||||
|
|
||||||
// Specialist Services
|
// Specialist Services
|
||||||
import { PolicyExpertService } from '../specialists/policy-expert.service';
|
import { PolicyExpertService } from '../specialists/policy-expert.service';
|
||||||
import { AssessmentExpertService } from '../specialists/assessment-expert.service';
|
import { AssessmentExpertService } from '../specialists/assessment-expert.service';
|
||||||
|
|
@ -242,6 +246,7 @@ export class CoordinatorAgentService implements OnModuleInit {
|
||||||
messageCount: context.previousMessages?.length || 0,
|
messageCount: context.previousMessages?.length || 0,
|
||||||
hasConverted: false,
|
hasConverted: false,
|
||||||
agentsUsed: agentsUsedInLoop,
|
agentsUsed: agentsUsedInLoop,
|
||||||
|
userMessage: userContent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Gate 失败时,将失败教训异步保存为系统经验(fire-and-forget)
|
// Gate 失败时,将失败教训异步保存为系统经验(fire-and-forget)
|
||||||
|
|
@ -274,6 +279,7 @@ export class CoordinatorAgentService implements OnModuleInit {
|
||||||
currentTurnCount: 0,
|
currentTurnCount: 0,
|
||||||
currentCostUsd: 0,
|
currentCostUsd: 0,
|
||||||
evaluationGate: evaluationGateCallback,
|
evaluationGate: evaluationGateCallback,
|
||||||
|
outputConfig: { format: zodOutputFormat(CoordinatorResponseSchema) as any },
|
||||||
};
|
};
|
||||||
|
|
||||||
// 6. Create tool executor
|
// 6. Create tool executor
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
import {
|
import {
|
||||||
IEvaluationRuleRepository,
|
IEvaluationRuleRepository,
|
||||||
EVALUATION_RULE_REPOSITORY,
|
EVALUATION_RULE_REPOSITORY,
|
||||||
|
|
@ -39,6 +41,8 @@ export interface EvaluationContext {
|
||||||
messageCount: number;
|
messageCount: number;
|
||||||
hasConverted: boolean;
|
hasConverted: boolean;
|
||||||
agentsUsed: string[];
|
agentsUsed: string[];
|
||||||
|
/** 用户原始消息 — LLM_JUDGE 需要用来评估回复的相关性 */
|
||||||
|
userMessage?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Result of a single rule check */
|
/** Result of a single rule check */
|
||||||
|
|
@ -68,11 +72,18 @@ export class EvaluationGateService {
|
||||||
private readonly logger = new Logger(EvaluationGateService.name);
|
private readonly logger = new Logger(EvaluationGateService.name);
|
||||||
private cache = new Map<string, { rules: EvaluationRuleEntity[]; expiresAt: number }>();
|
private cache = new Map<string, { rules: EvaluationRuleEntity[]; expiresAt: number }>();
|
||||||
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||||
|
private anthropicClient: Anthropic;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(EVALUATION_RULE_REPOSITORY)
|
@Inject(EVALUATION_RULE_REPOSITORY)
|
||||||
private readonly repo: IEvaluationRuleRepository,
|
private readonly repo: IEvaluationRuleRepository,
|
||||||
) {}
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
this.anthropicClient = new Anthropic({
|
||||||
|
apiKey: this.configService.get<string>('ANTHROPIC_API_KEY'),
|
||||||
|
baseURL: this.configService.get<string>('ANTHROPIC_BASE_URL') || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry: evaluate all applicable rules
|
* Main entry: evaluate all applicable rules
|
||||||
|
|
@ -90,7 +101,7 @@ export class EvaluationGateService {
|
||||||
const results: RuleCheckResult[] = [];
|
const results: RuleCheckResult[] = [];
|
||||||
|
|
||||||
for (const rule of rules) {
|
for (const rule of rules) {
|
||||||
const check = this.evaluateRule(rule, context);
|
const check = await this.evaluateRule(rule, context);
|
||||||
results.push({
|
results.push({
|
||||||
ruleId: rule.id,
|
ruleId: rule.id,
|
||||||
ruleName: rule.name,
|
ruleName: rule.name,
|
||||||
|
|
@ -172,10 +183,10 @@ export class EvaluationGateService {
|
||||||
// Rule Evaluation (pure functions)
|
// Rule Evaluation (pure functions)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
private evaluateRule(
|
private async evaluateRule(
|
||||||
rule: EvaluationRuleEntity,
|
rule: EvaluationRuleEntity,
|
||||||
context: EvaluationContext,
|
context: EvaluationContext,
|
||||||
): { passed: boolean; message?: string } {
|
): Promise<{ passed: boolean; message?: string }> {
|
||||||
switch (rule.ruleType) {
|
switch (rule.ruleType) {
|
||||||
case EvaluationRuleType.FIELD_COMPLETENESS:
|
case EvaluationRuleType.FIELD_COMPLETENESS:
|
||||||
return this.checkFieldCompleteness(rule.config, context);
|
return this.checkFieldCompleteness(rule.config, context);
|
||||||
|
|
@ -193,6 +204,8 @@ export class EvaluationGateService {
|
||||||
return this.checkTopicBoundary(rule.config, context);
|
return this.checkTopicBoundary(rule.config, context);
|
||||||
case EvaluationRuleType.NO_FABRICATION:
|
case EvaluationRuleType.NO_FABRICATION:
|
||||||
return this.checkNoFabrication(rule.config, context);
|
return this.checkNoFabrication(rule.config, context);
|
||||||
|
case EvaluationRuleType.LLM_JUDGE:
|
||||||
|
return this.checkLlmJudge(rule.config, context);
|
||||||
default:
|
default:
|
||||||
this.logger.warn(`Unknown rule type: ${rule.ruleType}`);
|
this.logger.warn(`Unknown rule type: ${rule.ruleType}`);
|
||||||
return { passed: true };
|
return { passed: true };
|
||||||
|
|
@ -484,6 +497,77 @@ export class EvaluationGateService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLM_JUDGE: 使用 Haiku 4.5 作为评审员,语义评估回复质量
|
||||||
|
* config: { minRelevance?: number, minConciseness?: number, maxNoise?: number, timeoutMs?: number }
|
||||||
|
*/
|
||||||
|
private async checkLlmJudge(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
context: EvaluationContext,
|
||||||
|
): Promise<{ passed: boolean; message?: string }> {
|
||||||
|
const minRelevance = (config.minRelevance as number) ?? 7;
|
||||||
|
const minConciseness = (config.minConciseness as number) ?? 6;
|
||||||
|
const maxNoise = (config.maxNoise as number) ?? 3;
|
||||||
|
const timeoutMs = (config.timeoutMs as number) ?? 5000;
|
||||||
|
|
||||||
|
if (!context.userMessage || !context.responseText) {
|
||||||
|
return { passed: true }; // 缺少信息时跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
const judgePrompt = `评估AI回复质量。打分0-10:
|
||||||
|
- relevance: 是否直接回答了用户的问题(0=完全无关,10=精准回答)
|
||||||
|
- conciseness: 是否足够简洁(0=极度冗长,10=言简意赅)
|
||||||
|
- noise: 是否包含用户没问的多余信息(0=无噪音,10=全是噪音)
|
||||||
|
|
||||||
|
用户消息: ${context.userMessage}
|
||||||
|
AI回复: ${context.responseText}
|
||||||
|
|
||||||
|
以JSON输出: {"relevance":N,"conciseness":N,"noise":N,"reason":"一句话原因"}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await Promise.race([
|
||||||
|
this.anthropicClient.messages.create({
|
||||||
|
model: 'claude-haiku-4-5-20251001',
|
||||||
|
max_tokens: 200,
|
||||||
|
messages: [{ role: 'user', content: judgePrompt }],
|
||||||
|
}),
|
||||||
|
new Promise<never>((_, reject) =>
|
||||||
|
setTimeout(() => reject(new Error('LLM Judge timeout')), timeoutMs),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
||||||
|
// 提取 JSON(Haiku 可能在 JSON 前后包裹 markdown 代码块)
|
||||||
|
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
||||||
|
if (!jsonMatch) {
|
||||||
|
this.logger.warn(`LLM Judge returned non-JSON: ${text.substring(0, 100)}`);
|
||||||
|
return { passed: true };
|
||||||
|
}
|
||||||
|
const scores = JSON.parse(jsonMatch[0]);
|
||||||
|
|
||||||
|
const passed =
|
||||||
|
scores.relevance >= minRelevance &&
|
||||||
|
scores.conciseness >= minConciseness &&
|
||||||
|
scores.noise <= maxNoise;
|
||||||
|
|
||||||
|
if (!passed) {
|
||||||
|
this.logger.debug(
|
||||||
|
`LLM Judge failed: relevance=${scores.relevance}, conciseness=${scores.conciseness}, noise=${scores.noise}. ${scores.reason || ''}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
passed,
|
||||||
|
message: passed
|
||||||
|
? undefined
|
||||||
|
: `LLM评审未通过: 相关性=${scores.relevance}/10, 简洁度=${scores.conciseness}/10, 噪音=${scores.noise}/10. ${scores.reason || ''}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.warn(`LLM Judge error (non-fatal): ${error}`);
|
||||||
|
return { passed: true }; // 失败时默认通过(非阻塞)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Feedback Builder
|
// Feedback Builder
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,67 @@ ${companyName} 是${companyDescription}。
|
||||||
- **有策略感**:懂得何时推进、何时暂停、何时转换话题
|
- **有策略感**:懂得何时推进、何时暂停、何时转换话题
|
||||||
- **有节奏感**:对话有起承转合,不堆砌信息也不空洞敷衍
|
- **有节奏感**:对话有起承转合,不堆砌信息也不空洞敷衍
|
||||||
|
|
||||||
|
## 1.4 最高优先级原则:精准回答
|
||||||
|
|
||||||
|
**这是你最重要的行为准则,优先于本文档中的所有其他指导。**
|
||||||
|
|
||||||
|
### 核心法则:先回答,再展开
|
||||||
|
|
||||||
|
收到用户消息后,你的思维流程必须是:
|
||||||
|
1. **识别意图**:用户到底在问什么?(一个具体问题?一个情况咨询?情绪表达?闲聊?)
|
||||||
|
2. **直接回答**:用最少的文字给出准确答案
|
||||||
|
3. **按需展开**:只有在用户明显需要更多信息时才补充
|
||||||
|
|
||||||
|
### 意图分类与回答策略
|
||||||
|
|
||||||
|
| 意图类型 | 识别特征 | 回答策略 | 长度 |
|
||||||
|
|---------|---------|---------|------|
|
||||||
|
| **直接事实问题** | "X的条件是什么""Y需要多少钱""Z怎么申请" | 直接给出答案,不加铺垫和寒暄 | 1-3句 |
|
||||||
|
| **是非判断问题** | "我能不能申请X""Y行不行""这个符合吗" | 先给明确结论(能/不能/视情况),再用一句话说理由 | 1-2句 |
|
||||||
|
| **对比选择问题** | "A和B哪个好""应该选哪个" | 直接推荐最优选项+一句话理由,用户追问再展开 | 2-3句 |
|
||||||
|
| **评估请求** | "帮我看看适合什么""评估一下" | 需要信息则直接问缺什么;信息够则调Agent评估 | 按需 |
|
||||||
|
| **情绪/犹豫表达** | "太贵了""不确定""再想想" | 共情一句+核心事实一句+一个引导问题 | 2-3句 |
|
||||||
|
| **复杂政策咨询** | 用户明确要求详细了解某个类别 | 调 policy_expert,但只提取用户关心的部分 | 150-300字 |
|
||||||
|
| **闲聊/打招呼** | "你好""在吗""谢谢" | 简短自然回应 | 1句 |
|
||||||
|
|
||||||
|
### 错误示范 vs 正确示范
|
||||||
|
|
||||||
|
**用户问:"高才通需要什么条件?"**
|
||||||
|
|
||||||
|
错误(冗长、不聚焦):
|
||||||
|
> 您好!感谢您对高才通计划的关注。高端人才通行证计划(Top Talent Pass Scheme)是香港入境处推出的一项吸引全球顶尖人才的计划。该计划分为A、B、C三个类别,每个类别都有不同的申请条件。
|
||||||
|
>
|
||||||
|
> A类:申请前一年年薪达到250万港币以上...
|
||||||
|
> B类:全球百强大学学位加3年工作经验...
|
||||||
|
> C类:全球百强大学学位但工作经验不足3年...
|
||||||
|
>
|
||||||
|
> 如果您想了解更多细节,或者想评估一下自己适合哪个类别,欢迎告诉我您的背景情况,我可以为您做一个初步分析。
|
||||||
|
|
||||||
|
正确(直接、精准):
|
||||||
|
> 高才通分三个类别:
|
||||||
|
> - **A类**:年薪达250万港币
|
||||||
|
> - **B类**:全球百强大学学位 + 3年以上工作经验
|
||||||
|
> - **C类**:全球百强大学学位,工作经验不足3年(年度限额1万人)
|
||||||
|
>
|
||||||
|
> 您的情况更接近哪一类?
|
||||||
|
|
||||||
|
**用户问:"我清华毕业的,能申请高才通吗?"**
|
||||||
|
|
||||||
|
错误:
|
||||||
|
> 清华大学是中国最顶尖的学府之一,在多个全球大学排名中都名列前茅。关于高才通计划,清华大学确实在全球百强大学名单中,这意味着您可能符合B类或C类的学历要求。不过我们还需要了解更多信息才能准确评估...
|
||||||
|
|
||||||
|
正确:
|
||||||
|
> 可以。清华在百强名单内,满足高才通的学历要求。您毕业几年了?有3年以上工作经验走B类,不足3年走C类。
|
||||||
|
|
||||||
|
### 绝对禁止的回答模式
|
||||||
|
|
||||||
|
1. **信息堆砌**:把知道的全部倒出来,不管用户问的是什么
|
||||||
|
2. **过度铺垫**:回答前先说一段"感谢您的提问""这是个很好的问题"
|
||||||
|
3. **重复用户的问题**:开头用"关于您问到的XX..."
|
||||||
|
4. **不给结论**:罗列一堆信息但不给明确答案
|
||||||
|
5. **自说自话**:用户没问的内容主动展开讲解
|
||||||
|
6. **模板化回答**:每次都是同一个结构(先概述、再详解、再注意事项、再引导)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
@ -100,10 +161,12 @@ ${companyName} 是${companyDescription}。
|
||||||
- includeProcessSteps:如果用户问到流程,设为 true
|
- includeProcessSteps:如果用户问到流程,设为 true
|
||||||
- includeRequirements:如果用户问到条件/要求,设为 true
|
- includeRequirements:如果用户问到条件/要求,设为 true
|
||||||
|
|
||||||
**输出处理**:
|
**输出处理——只提取用户需要的部分**:
|
||||||
- 政策专家会返回结构化的政策信息,包含要点概述、条件列表、流程步骤、注意事项
|
- 政策专家会返回完整的结构化信息(要点、条件、流程、注意事项)
|
||||||
- 你需要将这些信息重新组织为自然的对话语言,而不是照搬格式
|
- **你绝不能全部搬运给用户**。根据用户的具体问题,只提取直接相关的1-2个要点
|
||||||
- 保留关键数据和细节,但让表达更加口语化和易于理解
|
- 例如:用户问"高才通需要什么学历" → 只提取学历条件,不要附带年薪条件、签证安排等
|
||||||
|
- 用自然对话语言呈现,不要照搬【政策要点】【申请条件】这种格式标题
|
||||||
|
- 如果用户想了解更多,他们会追问
|
||||||
|
|
||||||
## 2.2 评估专家 Agent(invoke_assessment_expert)
|
## 2.2 评估专家 Agent(invoke_assessment_expert)
|
||||||
|
|
||||||
|
|
@ -587,12 +650,14 @@ ${companyName} 是${companyDescription}。
|
||||||
- 适当使用口语化表达,但保持专业度
|
- 适当使用口语化表达,但保持专业度
|
||||||
- 避免过度使用客服话术("感谢您的咨询"、"请问还有其他需要帮助的吗")
|
- 避免过度使用客服话术("感谢您的咨询"、"请问还有其他需要帮助的吗")
|
||||||
|
|
||||||
**回复长度控制**:
|
**回复长度控制——短即是好**:
|
||||||
- 简单问题:2-3句话足矣
|
- **默认短回答**:除非用户明确要求详细解释,否则一律简短回答
|
||||||
- 一般咨询:控制在200字以内
|
- 简单问题:1-2句话
|
||||||
- 评估结果:可以较长,但结构清晰,300-500字
|
- 是非判断:1句结论 + 1句理由
|
||||||
- 政策详解:如果用户要求详细了解,可以更长,但要分段
|
- 一般咨询:控制在100字以内
|
||||||
- **绝不要**发送超过500字的纯文本块——用格式化来组织长内容
|
- 评估结果:结构清晰,200-300字
|
||||||
|
- 只有用户说"详细说说""展开讲讲"时才给长回答
|
||||||
|
- **宁可太短,不可太长**——用户可以追问,但被信息轰炸后会关闭页面
|
||||||
|
|
||||||
**对话节奏**:
|
**对话节奏**:
|
||||||
- 每次回复都应该以一个**问题或明确的下一步建议**结尾,保持对话流动
|
- 每次回复都应该以一个**问题或明确的下一步建议**结尾,保持对话流动
|
||||||
|
|
@ -600,12 +665,17 @@ ${companyName} 是${companyDescription}。
|
||||||
- 但也不要每次都以问题结尾——偶尔以一个有价值的信息或观点结尾也很好
|
- 但也不要每次都以问题结尾——偶尔以一个有价值的信息或观点结尾也很好
|
||||||
- 感受对话的"温度":如果用户兴致高涨,可以聊得更深入;如果用户回复简短,适当放慢节奏
|
- 感受对话的"温度":如果用户兴致高涨,可以聊得更深入;如果用户回复简短,适当放慢节奏
|
||||||
|
|
||||||
**避免的模式**:
|
**避免的模式(这些会让你像一个低质量AI)**:
|
||||||
- 不要用"首先/其次/最后"的三段式回复(太像AI了)
|
- 不要用"首先/其次/最后"的三段式回复
|
||||||
- 不要在每次回复开头都重复用户的问题("您问到关于XXX...")
|
- 不要在回复开头重复用户的问题("关于您问到的XXX...")
|
||||||
- 不要用过多的修饰词和排比句
|
- 不要用过多的修饰词和排比句
|
||||||
- 不要使用 emoji 或颜文字
|
- 不要使用 emoji 或颜文字
|
||||||
- 不要连续使用感叹号
|
- 不要连续使用感叹号
|
||||||
|
- 不要说"这是一个好问题"
|
||||||
|
- 不要说"感谢您的咨询/提问"
|
||||||
|
- 不要在每段开头加"首先""其次""另外"
|
||||||
|
- 不要把同一个意思用不同的话重复表达
|
||||||
|
- 不要在已经给了答案后又总结一遍
|
||||||
|
|
||||||
## 5.3 隐私与安全——底线原则
|
## 5.3 隐私与安全——底线原则
|
||||||
|
|
||||||
|
|
@ -985,13 +1055,13 @@ ${categoriesList}
|
||||||
|
|
||||||
推荐处理:
|
推荐处理:
|
||||||
1. 调用 invoke_memory_manager (load_context) → 确认是新用户
|
1. 调用 invoke_memory_manager (load_context) → 确认是新用户
|
||||||
2. 热情但不过分地打招呼
|
2. 简短打招呼 + 一个问题了解方向
|
||||||
3. 用一个开放性问题了解用户的兴趣方向
|
3. **不要长篇介绍公司和各种类别**
|
||||||
4. 不要在第一轮就开始信息收集
|
|
||||||
|
|
||||||
参考回复:
|
参考回复:
|
||||||
"您好!欢迎咨询,我是${companyName}的移民顾问。很高兴为您服务。
|
"您好!请问您对哪种移民方式比较感兴趣?或者简单说说您的情况,我帮您分析最适合的路径。"
|
||||||
香港目前有多种移民路径,包括人才引进、留学转移民、投资移民等。请问您对哪个方向比较感兴趣?或者可以简单说说您的情况,我帮您分析适合的路径。"
|
|
||||||
|
(注意:不需要自我介绍、不需要列举所有类别、不需要说"很高兴为您服务")
|
||||||
|
|
||||||
## 11.2 场景:用户直接问政策问题
|
## 11.2 场景:用户直接问政策问题
|
||||||
|
|
||||||
|
|
@ -999,19 +1069,20 @@ ${categoriesList}
|
||||||
|
|
||||||
推荐处理:
|
推荐处理:
|
||||||
1. 调用 invoke_policy_expert ({ query: "高才通B类申请条件要求", category: "GEP" })
|
1. 调用 invoke_policy_expert ({ query: "高才通B类申请条件要求", category: "GEP" })
|
||||||
2. 基于返回的结构化信息,用自然语言呈现
|
2. **只提取B类的条件,不要附带A类C类的信息**
|
||||||
3. 结尾引导用户评估自己的适配度
|
3. 结尾用一个短问题引导
|
||||||
|
|
||||||
## 11.3 场景:用户表达疑虑
|
## 11.3 场景:用户表达疑虑
|
||||||
|
|
||||||
用户:"你们的服务费用是不是太高了?我看别的公司便宜很多。"
|
用户:"你们的服务费用是不是太高了?我看别的公司便宜很多。"
|
||||||
|
|
||||||
推荐处理:
|
推荐处理:
|
||||||
1. 调用 invoke_objection_handler ({ objection: "服务费用太高,竞品更便宜", ... })
|
1. 调用 invoke_objection_handler
|
||||||
2. 先共情("我理解费用是很重要的考虑因素")
|
2. **回复不超过100字**:共情一句 + 核心价值一句 + 引导一句
|
||||||
3. 用价值而非价格来回应
|
3. 不展开长篇论述
|
||||||
4. 不诋毁竞争对手
|
|
||||||
5. 提供灵活方案(分期、先做评估等)
|
参考回复:
|
||||||
|
"理解您的顾虑,费用确实是重要的考量因素。我们的服务包含从评估到获批的全流程跟进,包括材料审核和入境处沟通。您可以先做一个初步评估了解可行性,再决定是否需要全程服务。"
|
||||||
|
|
||||||
## 11.4 场景:信息收集完毕,准备评估
|
## 11.4 场景:信息收集完毕,准备评估
|
||||||
|
|
||||||
|
|
@ -1072,24 +1143,24 @@ ${categoriesList}
|
||||||
# 第十三章:最终提醒
|
# 第十三章:最终提醒
|
||||||
# ═══════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
记住你的核心使命:
|
记住你的核心使命(按优先级排列):
|
||||||
|
|
||||||
1. **你是用户唯一的对话伙伴**——所有专家 Agent 都在幕后,用户只看到你。
|
1. **精准回答问题**——用户问什么就答什么,用最少的话给出最准确的答案。这是第一优先级。
|
||||||
|
|
||||||
2. **准确性是生命线**——宁可说"我需要确认一下",也不要给出可能错误的信息。绝不编造政策。
|
2. **准确性是生命线**——宁可说"我需要确认一下",也不要给出可能错误的信息。绝不编造政策。
|
||||||
|
|
||||||
3. **先是人,再是顾问**——用户首先需要被倾听和理解,其次才是专业信息。
|
3. **简洁就是专业**——真正的专家不说废话。能用一句话说清楚的,绝不用一段。
|
||||||
|
|
||||||
4. **流程是指引,不是枷锁**——根据用户的实际情况灵活调整,不要教条地执行流程。
|
4. **你是用户唯一的对话伙伴**——所有专家 Agent 都在幕后,用户只看到你。
|
||||||
|
|
||||||
5. **每次对话都是信任的建立**——用户可能在做人生中最重要的决定之一。对这份信任心怀敬畏。
|
5. **先是人,再是顾问**——用户首先需要被倾听和理解,但"倾听"不等于"长篇大论地回应"。
|
||||||
|
|
||||||
6. **转化是水到渠成**——当你真正帮助用户理清了移民路径,付费服务是自然的下一步,不需要强推。
|
6. **流程是指引,不是枷锁**——根据用户的实际情况灵活调整。
|
||||||
|
|
||||||
7. **你代表公司形象**——你的每一句话都影响用户对 ${companyName} 的认知。专业、真诚、有温度。
|
7. **转化是水到渠成**——帮用户理清路径,付费是自然结果,不需要强推。
|
||||||
|
|
||||||
|
**最后的提醒:每次生成回复前,重新审视一遍——有没有多余的句子?有没有用户没问到的信息?能不能再删掉一半?**
|
||||||
|
|
||||||
现在,准备好迎接用户的第一条消息。
|
现在,准备好迎接用户的第一条消息。
|
||||||
|
|
||||||
深呼吸,进入角色,开始吧。
|
|
||||||
`.trim();
|
`.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ export function buildObjectionHandlerPrompt(): string {
|
||||||
- **empathyResponse**:纯共情内容,不包含任何反驳或解决方案。
|
- **empathyResponse**:纯共情内容,不包含任何反驳或解决方案。
|
||||||
- **factualRebuttal**:纯事实和数据,作为建议回复的素材。如果通过 search_knowledge 获取到了信息,在此引用。
|
- **factualRebuttal**:纯事实和数据,作为建议回复的素材。如果通过 search_knowledge 获取到了信息,在此引用。
|
||||||
- **successStoryReference**:如果找到了相关案例则填写,否则返回 null。不要编造案例。
|
- **successStoryReference**:如果找到了相关案例则填写,否则返回 null。不要编造案例。
|
||||||
- **suggestedResponse**:这是最终呈现给用户的完整回复,应当自然地融合共情、事实、案例和方案,不要像列表一样生硬罗列。长度控制在 200-400 字。
|
- **suggestedResponse**:这是最终呈现给用户的完整回复。**长度控制在 80-150 字**,要求:共情一句 + 核心事实一句 + 引导一句。不要写成小作文。用户的注意力很短,冗长的回答会适得其反。
|
||||||
- **followUpQuestion**:一个自然的跟进问题,目的是推动对话向评估或预约方向发展。
|
- **followUpQuestion**:一个自然的跟进问题,目的是推动对话向评估或预约方向发展。
|
||||||
|
|
||||||
# 重要提醒
|
# 重要提醒
|
||||||
|
|
|
||||||
|
|
@ -124,41 +124,43 @@ export function buildPolicyExpertPrompt(): string {
|
||||||
|
|
||||||
# 输出格式要求
|
# 输出格式要求
|
||||||
|
|
||||||
你的输出将由 Coordinator Agent 整合处理,因此需遵循以下格式规范:
|
**核心原则:只回答被问到的问题,不要输出完整模板。**
|
||||||
|
|
||||||
## 标准政策回答格式
|
你的输出将由 Coordinator Agent 整合处理。Coordinator 最头疼的问题是收到太多无关信息,不得不全部转述给用户。你必须帮助 Coordinator 减轻负担。
|
||||||
|
|
||||||
回答必须包含以下结构化元素(按需选择):
|
## 输出规则
|
||||||
|
|
||||||
1. **政策要点概述**:用 2-3 句话概括核心要点
|
1. **聚焦问题**:Coordinator 传来的 query 问什么,你就答什么。问条件就只答条件,问流程就只答流程,不要附带其他。
|
||||||
2. **详细条件列表**:逐项列出具体条件,使用标准格式
|
2. **精简表达**:用最少的文字传递准确信息。能用3行说清楚的,不要用10行。
|
||||||
3. **申请流程**(如适用):分步骤说明
|
3. **按需分段**:只包含与问题直接相关的段落,不要每次都输出完整的5段模板。
|
||||||
4. **重要注意事项**:特别提醒、常见误区、近期变动
|
4. **标注来源**:在末尾简要标注信息出处。
|
||||||
5. **信息来源标注**:标注信息出自知识库的哪些条目
|
|
||||||
|
|
||||||
## 格式范例
|
## 格式范例
|
||||||
|
|
||||||
|
**问题**:"高才通B类的学历要求"
|
||||||
|
|
||||||
|
**正确输出**(聚焦问题):
|
||||||
|
\`\`\`
|
||||||
|
持有全球百强大学的学士/硕士/博士学位,且申请前5年内拥有至少3年全职工作经验。百强名单参考QS/THE/US News/ARWU四大排名综合认定,每年可能更新。
|
||||||
|
|
||||||
|
来源:知识库 - TTPS政策详解
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
**错误输出**(信息过载):
|
||||||
\`\`\`
|
\`\`\`
|
||||||
【政策要点】
|
【政策要点】
|
||||||
高端人才通行证计划(TTPS)B类面向全球百强大学的毕业生,需具备一定工作经验。
|
高端人才通行证计划分为A/B/C三类...(用户没问其他类别)
|
||||||
|
|
||||||
【申请条件】
|
【申请条件】
|
||||||
- 持有全球百强大学的学士/硕士/博士学位
|
- B类条件...
|
||||||
- 申请前5年内拥有至少3年全职工作经验
|
- A类条件...(用户没问A类)
|
||||||
- 百强大学名单参考四大排名(QS/THE/US News/ARWU)综合认定
|
- C类条件...(用户没问C类)
|
||||||
- 不设行业限制
|
|
||||||
|
|
||||||
【申请流程】
|
【申请流程】
|
||||||
1. 准备学历证明、工作经验证明等材料
|
1. 准备材料...(用户没问流程)
|
||||||
2. 通过入境处在线系统提交申请
|
|
||||||
3. 审批周期约4周
|
|
||||||
4. 获批后6个月内激活签证
|
|
||||||
|
|
||||||
【注意事项】
|
【注意事项】
|
||||||
- 百强大学名单每年更新,以申请时的有效名单为准
|
...
|
||||||
- 远程学习或非全日制学位可能不被认可
|
|
||||||
|
|
||||||
【来源】知识库 - TTPS政策详解 / 百强大学名单
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/**
|
||||||
|
* Coordinator Response Schema — Structured Output
|
||||||
|
*
|
||||||
|
* 强制 Coordinator 输出结构化 JSON,包含意图分类和简洁回答。
|
||||||
|
* 通过 Anthropic API 的 output_config 实现硬约束。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const CoordinatorResponseSchema = z.object({
|
||||||
|
intent: z.enum([
|
||||||
|
'factual_question', // 直接事实问题:"X的条件是什么"
|
||||||
|
'yes_no_question', // 是非判断问题:"我能不能申请X"
|
||||||
|
'comparison_question', // 对比选择问题:"A和B哪个好"
|
||||||
|
'assessment_request', // 评估请求:"帮我评估一下"
|
||||||
|
'objection_expression', // 情绪/犹豫表达:"太贵了"/"怕被拒"
|
||||||
|
'detailed_consultation', // 复杂政策咨询:明确要求详细了解
|
||||||
|
'casual_chat', // 闲聊/打招呼:"你好"
|
||||||
|
]),
|
||||||
|
answer: z.string().describe('直接回答用户的文本,简洁精准,默认100字以内'),
|
||||||
|
followUp: z.string().optional().describe('跟进引导问题(可选)'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CoordinatorResponse = z.infer<typeof CoordinatorResponseSchema>;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Intent-based Answer Length Limits (中文字符数)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/** 每种意图对应的最大 answer 长度(字符数)— 程序级硬约束 */
|
||||||
|
export const INTENT_MAX_ANSWER_LENGTH: Record<string, number> = {
|
||||||
|
factual_question: 200, // 1-3句,直接给答案
|
||||||
|
yes_no_question: 120, // 1-2句,结论+理由
|
||||||
|
comparison_question: 250, // 2-3句,推荐+理由
|
||||||
|
assessment_request: 400, // 按需但有上限
|
||||||
|
objection_expression: 200, // 共情+事实+引导
|
||||||
|
detailed_consultation: 500, // 复杂咨询允许较长
|
||||||
|
casual_chat: 80, // 1句
|
||||||
|
};
|
||||||
|
|
||||||
|
/** followUp 问题最大长度 */
|
||||||
|
export const MAX_FOLLOWUP_LENGTH = 80;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能截断:在句子边界处截断,避免截断在句子中间
|
||||||
|
*/
|
||||||
|
export function smartTruncate(text: string, maxLen: number): string {
|
||||||
|
if (text.length <= maxLen) return text;
|
||||||
|
|
||||||
|
const truncated = text.substring(0, maxLen);
|
||||||
|
|
||||||
|
// 在截断范围内找最后一个句子结束符
|
||||||
|
const sentenceEnders = ['。', '!', '?', ';', '. ', '! ', '? '];
|
||||||
|
let lastEnd = -1;
|
||||||
|
for (const ender of sentenceEnders) {
|
||||||
|
const idx = truncated.lastIndexOf(ender);
|
||||||
|
if (idx > lastEnd) lastEnd = idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果在后半段找到句子边界,在那里截断
|
||||||
|
if (lastEnd > maxLen * 0.5) {
|
||||||
|
return text.substring(0, lastEnd + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有好的边界,硬截断
|
||||||
|
return truncated + '...';
|
||||||
|
}
|
||||||
|
|
@ -280,6 +280,8 @@ export interface AgentLoopParams {
|
||||||
turnCount: number,
|
turnCount: number,
|
||||||
agentsUsed: string[],
|
agentsUsed: string[],
|
||||||
) => Promise<import('../coordinator/evaluation-gate.service').GateResult>;
|
) => Promise<import('../coordinator/evaluation-gate.service').GateResult>;
|
||||||
|
/** Structured Output — 传入 Claude API 的 output_config */
|
||||||
|
outputConfig?: { format: Record<string, unknown> };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Claude API 消息格式 */
|
/** Claude API 消息格式 */
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,8 @@ importers:
|
||||||
packages/services/conversation-service:
|
packages/services/conversation-service:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk':
|
'@anthropic-ai/sdk':
|
||||||
specifier: ^0.52.0
|
specifier: ^0.73.0
|
||||||
version: 0.52.0
|
version: 0.73.0(zod@4.3.6)
|
||||||
'@iconsulting/shared':
|
'@iconsulting/shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../shared
|
version: link:../../shared
|
||||||
|
|
@ -162,6 +162,9 @@ importers:
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.1
|
version: 9.0.1
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.6
|
||||||
|
version: 4.3.6
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@nestjs/cli':
|
'@nestjs/cli':
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
|
|
@ -863,6 +866,19 @@ packages:
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@anthropic-ai/sdk@0.73.0(zod@4.3.6):
|
||||||
|
resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
json-schema-to-ts: 3.1.1
|
||||||
|
zod: 4.3.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@babel/code-frame@7.27.1:
|
/@babel/code-frame@7.27.1:
|
||||||
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
@ -2293,7 +2309,6 @@ packages:
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@nestjs/core@10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2):
|
/@nestjs/core@10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2):
|
||||||
resolution: {integrity: sha512-MhiSGplB4TkadceA7opn/NaZmJhwYYNdB8nS8I29nLNx3vU+8aGHBiueZgcphEVDETZJSfc2VA5Mn/FC3JcsrA==}
|
resolution: {integrity: sha512-MhiSGplB4TkadceA7opn/NaZmJhwYYNdB8nS8I29nLNx3vU+8aGHBiueZgcphEVDETZJSfc2VA5Mn/FC3JcsrA==}
|
||||||
|
|
@ -2325,6 +2340,7 @@ packages:
|
||||||
uid: 2.0.2
|
uid: 2.0.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@nestjs/jwt@10.2.0(@nestjs/common@10.4.21):
|
/@nestjs/jwt@10.2.0(@nestjs/common@10.4.21):
|
||||||
resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==}
|
resolution: {integrity: sha512-x8cG90SURkEiLOehNaN2aRlotxT0KZESUliOPKKnjWiyJOcWurkF3w345WOX0P4MgFzUjGoZ1Sy0aZnxeihT0g==}
|
||||||
|
|
@ -2343,7 +2359,7 @@ packages:
|
||||||
'@nestjs/core': ^10.0.0
|
'@nestjs/core': ^10.0.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
body-parser: 1.20.3
|
body-parser: 1.20.3
|
||||||
cors: 2.8.5
|
cors: 2.8.5
|
||||||
express: 4.22.1
|
express: 4.22.1
|
||||||
|
|
@ -2368,7 +2384,6 @@ packages:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@nestjs/schedule@4.1.2(@nestjs/common@10.4.21)(@nestjs/core@10.4.21):
|
/@nestjs/schedule@4.1.2(@nestjs/common@10.4.21)(@nestjs/core@10.4.21):
|
||||||
resolution: {integrity: sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==}
|
resolution: {integrity: sha512-hCTQ1lNjIA5EHxeu8VvQu2Ed2DBLS1GSC6uKPYlBiQe6LL9a7zfE9iVSK+zuK8E2odsApteEBmfAQchc8Hx0Gg==}
|
||||||
|
|
@ -2426,7 +2441,7 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/platform-express': 10.4.21(@nestjs/common@10.4.21)(@nestjs/core@10.4.21)
|
'@nestjs/platform-express': 10.4.21(@nestjs/common@10.4.21)(@nestjs/core@10.4.21)
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
@ -2441,7 +2456,7 @@ packages:
|
||||||
typeorm: ^0.3.0
|
typeorm: ^0.3.0
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 10.4.21(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/core': 10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/core': 10.4.21(@nestjs/common@10.4.21)(@nestjs/platform-express@10.4.21)(@nestjs/websockets@10.4.21)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
reflect-metadata: 0.2.2
|
reflect-metadata: 0.2.2
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
typeorm: 0.3.28(ioredis@5.9.1)(pg@8.16.3)(ts-node@10.9.2)
|
typeorm: 0.3.28(ioredis@5.9.1)(pg@8.16.3)(ts-node@10.9.2)
|
||||||
|
|
@ -2468,7 +2483,6 @@ packages:
|
||||||
reflect-metadata: 0.2.2
|
reflect-metadata: 0.2.2
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@nodelib/fs.scandir@2.1.5:
|
/@nodelib/fs.scandir@2.1.5:
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
|
|
@ -5231,7 +5245,6 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
dev: false
|
|
||||||
|
|
||||||
/debug@4.4.3:
|
/debug@4.4.3:
|
||||||
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
|
|
@ -7321,6 +7334,14 @@ packages:
|
||||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/json-schema-to-ts@3.1.1:
|
||||||
|
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.4
|
||||||
|
ts-algebra: 2.0.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/json-schema-traverse@0.4.1:
|
/json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
@ -10088,7 +10109,6 @@ packages:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- supports-color
|
- supports-color
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
dev: false
|
|
||||||
|
|
||||||
/socket.io@4.8.3:
|
/socket.io@4.8.3:
|
||||||
resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==}
|
resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==}
|
||||||
|
|
@ -10591,6 +10611,10 @@ packages:
|
||||||
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/ts-algebra@2.0.0:
|
||||||
|
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/ts-api-utils@1.4.3(typescript@5.9.3):
|
/ts-api-utils@1.4.3(typescript@5.9.3):
|
||||||
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
|
resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue