feat(agents): implement multi-agent collaboration architecture

借鉴 Claude Code 的架构模式,将单一 Agent 重构为 Coordinator + 6 Specialist 多 Agent 协作系统。

## 新增文件 (36个)

### 架构设计文档 (docs/architecture/, 12个)
- 00-overview.md ~ 11-prompt-templates.md: 完整架构设计,覆盖所有 Agent 的详细设计、
  Prompt 模板、协作流程、工具并发系统、动态上下文注入

### 多 Agent 系统 (infrastructure/agents/, 23个)
- coordinator/coordinator-agent.service.ts: 主协调器,替代 ClaudeAgentServiceV2
- coordinator/agent-loop.ts: 核心递归 async generator 循环(参考 Claude Code aM())
- coordinator/context-injector.service.ts: 动态上下文注入(8种上下文类型按优先级注入)
- specialists/base-specialist.service.ts: Agent 基类(封装 Claude API 调用 + prompt 缓存)
- specialists/policy-expert.service.ts: 政策专家 (Sonnet 4, temp=0)
- specialists/assessment-expert.service.ts: 评估专家 (Sonnet 4, temp=0)
- specialists/strategist.service.ts: 策略顾问 (Sonnet 4, temp=0.3)
- specialists/objection-handler.service.ts: 异议处理 (Sonnet 4, temp=0.2)
- specialists/case-analyst.service.ts: 案例分析 (Haiku, temp=0)
- specialists/memory-manager.service.ts: 记忆管理 (Haiku, temp=0)
- prompts/coordinator-system-prompt.ts: 协调器 Prompt(1095行,13章)
- prompts/{policy,assessment,strategist,objection,case,memory}-*-prompt.ts: 各专家 Prompt
- tools/coordinator-tools.ts: 16个工具定义(6 Agent 调用 + 10 直接工具)
- tools/tool-execution-queue.ts: 并发执行队列(isConcurrencySafe 控制并行/串行)
- types/agent.types.ts: Agent 配置、输入/输出类型定义
- types/stream.types.ts: 流式事件类型(含 agent_start/complete/coordinator_thinking)
- types/context.types.ts: 上下文注入类型
- agents.module.ts: NestJS 模块注册

### 前端 Agent 状态展示 (1个)
- AgentStatusIndicator.tsx: 多 Agent 工作状态组件(含动画)

## 修改文件 (15个)

### 后端集成
- conversation.service.ts: 切换到 CoordinatorAgentService
- conversation.gateway.ts: 新增 agent_start/agent_complete/coordinator_thinking 事件
- claude.module.ts: 引入 AgentsModule
- agents.module.ts: 注册 ImmigrationToolsService(复用旧版生产测试的工具实现)
- knowledge-client.service.ts: 新增 search()/getUserContext() 便捷方法

### 旧代码标记 @deprecated
- claude-agent.service.ts, claude-agent-v2.service.ts
- strategy-engine.service.ts, intent-classifier.ts, response-gate.ts

### 前端适配
- chatStore.ts: 新增 ActiveAgent/CompletedAgent/CoordinatorPhase 状态
- useChat.ts: 新增 WebSocket 事件处理
- ChatWindow.tsx: 集成 AgentStatusIndicator
- globals.css: 新增 agentPulse/agentSlideIn 动画

### 共享类型
- conversation.types.ts: 新增 AGENT_START/AGENT_COMPLETE/COORDINATOR_THINKING 事件

## 核心设计决策

1. **新旧结合**: Coordinator 的 10 个直接工具委托给旧版 ImmigrationToolsService
   (经过生产测试的 Google Search、汇率 API、新闻 API 等),6 个 Agent 调用工具
   走新的 Specialist Agent 系统
2. **递归 async generator**: agent-loop 支持流式输出 + 工具递归 + 成本/轮次控制
3. **并行 Agent 执行**: ToolExecutionQueue 根据 isConcurrencySafe 自动并行/串行
4. **Prompt 缓存**: 所有 Agent 的 system prompt 使用 cache_control: ephemeral
5. **速率限制重试**: 429/529 指数退避,最多 2 次
6. **向后兼容**: LegacyConversationContext 类型别名,StreamChunk 扩展不破坏现有结构

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-06 04:26:39 -08:00
parent 7f03a4d870
commit 16cc0e4c08
50 changed files with 14355 additions and 5 deletions

View File

@ -0,0 +1,208 @@
# 00 - 多 Agent 协作架构总览
## 1. 设计哲学
本系统借鉴 Claude Code 的核心架构理念:
> **程序做脚手架LLM 做决策。**
- 程序只负责:循环、注入上下文、执行工具、限流、权限
- LLM 负责:理解用户意图、选择策略、调用工具、判断完成
- 多个专业 Agent 各司其职,由 Coordinator 统一编排
与旧架构的根本区别:**不再用程序化状态机控制对话流程**,而是通过精心设计的 Prompt 引导 LLM 自主决策。
## 2. 整体架构图
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ web-client (React + Socket.io) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ ChatWindow │ │ AgentStatus │ │ MessageBubble │ │
│ └──────┬──────┘ └──────────────┘ └───────────────┘ │
│ │ WebSocket │
└─────────┼───────────────────────────────────────────────────┘
┌─────────▼───────────────────────────────────────────────────┐
│ Conversation Gateway (WebSocket) │
│ conversation.gateway.ts │
└─────────┬───────────────────────────────────────────────────┘
┌─────────▼───────────────────────────────────────────────────┐
│ Conversation Service │
│ conversation.service.ts │
│ (消息持久化、对话管理、Token 追踪) │
└─────────┬───────────────────────────────────────────────────┘
┌─────────▼───────────────────────────────────────────────────┐
│ Coordinator Agent Service (主协调器) │
│ coordinator-agent.service.ts │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Agent Loop (递归 async generator) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌─────────────────────┐ │ │
│ │ │ Context │───→│ Claude API (Sonnet) │ │ │
│ │ │ Injector │ │ + Coordinator Prompt │ │ │
│ │ │ (动态上下文) │ │ + Agent Tools │ │ │
│ │ └──────────────┘ └──────────┬──────────┘ │ │
│ │ │ │ │
│ │ ┌────────────▼────────────┐ │ │
│ │ │ Tool Execution Queue │ │ │
│ │ │ (并发/串行调度) │ │ │
│ │ └────────────┬────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────┼──────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │ │
│ │ │ Agent Tools │ │ Agent Tools │ │ Direct │ │ │
│ │ │ (invoke_xxx) │ │ (invoke_xxx) │ │ Tools │ │ │
│ │ │ → Specialist │ │ → Specialist │ │ (payment │ │ │
│ │ │ API Call │ │ API Call │ │ search) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ tool_results → 递归回 Agent Loop │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ Specialist Agents │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Policy │ │ Assessment │ │ Strategist │ │
│ │ Expert │ │ Expert │ │ │ │
│ │ (Sonnet) │ │ (Sonnet) │ │ (Sonnet) │ │
│ │ Tools: │ │ Tools: │ │ Tools: │ │
│ │ - search_kb │ │ - search_kb │ │ - get_user │ │
│ └──────────────┘ │ - get_user │ │ _context │ │
│ └──────────────┘ └──────────────┘ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Objection │ │ Case │ │ Memory │ │
│ │ Handler │ │ Analyst │ │ Manager │ │
│ │ (Sonnet) │ │ (Haiku) │ │ (Haiku) │ │
│ │ Tools: │ │ Tools: │ │ Tools: │ │
│ │ - search_kb │ │ - search_kb │ │ - save_mem │ │
│ │ - get_user │ │ - get_user │ │ - get_user │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ External Services │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Knowledge │ │ Payment │ │ Evolution │ │
│ │ Service │ │ Service │ │ Service │ │
│ │ (RAG+Neo4j)│ │ (Alipay/ │ │ (Analytics│ │
│ │ Port 3003 │ │ WeChat) │ │ +Learn) │ │
│ │ │ │ Port 3002 │ │ Port 3005 │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└──────────────────────────────────────────────────────────┘
```
## 3. 七个 Agent 一览
| Agent | 角色 | Model | 工具 | 触发时机 |
|-------|------|-------|------|----------|
| **Coordinator** | 主协调器,面对用户 | Sonnet | invoke_* + 直接工具 | 每次用户消息 |
| **Policy Expert** | 政策专家 | Sonnet | search_knowledge | 用户问政策、条件、流程 |
| **Assessment Expert** | 评估专家 | Sonnet | search_knowledge, get_user_context | 需要评估用户资格时 |
| **Strategist** | 策略顾问 | Sonnet | get_user_context | 需要对话策略建议时 |
| **Objection Handler** | 异议处理 | Sonnet | search_knowledge, get_user_context | 用户表达顾虑/犹豫 |
| **Case Analyst** | 案例分析 | Haiku | search_knowledge, get_user_context | 需要用案例说服用户 |
| **Memory Manager** | 记忆管理 | Haiku | save_user_memory, get_user_context | 需要保存/加载用户信息 |
## 4. 消息流转完整路径
```
1. 用户发送消息 (WebSocket: 'message' event)
2. ConversationGateway → ConversationService.sendMessage()
3. ConversationService:
│ a. 验证对话所有权
│ b. 保存用户消息
│ c. 构建 ConversationContext (last 20 messages + state)
│ d. 调用 CoordinatorAgentService.sendMessage()
4. CoordinatorAgentService → agentLoop():
│ a. ContextInjector 注入动态上下文:
│ - 用户历史记忆摘要
│ - 已收集的用户信息
│ - 对话统计(轮次、时长)
│ - 最近的评估结果
│ - 相关知识预检索
│ b. 调用 Claude API (Sonnet, streaming)
│ → yield: text chunks (流式传输给前端)
│ → yield: agent_start / agent_progress events
│ c. 如果 Claude 返回 tool_use:
│ │
│ ├── invoke_policy_expert({...})
│ │ → PolicyExpertService.execute()
│ │ → 独立 Claude API 调用 (Sonnet)
│ │ → 内部可调 search_knowledge (最多 3 轮)
│ │ → 返回结构化政策信息
│ │
│ ├── invoke_assessment_expert({...})
│ │ → AssessmentExpertService.execute()
│ │ → 独立 Claude API 调用 (Sonnet)
│ │ → 返回评估报告
│ │
│ ├── generate_payment({...}) [直接工具,不调 Agent]
│ │ → 调用 payment-service API
│ │ → 返回支付链接
│ │
│ └── get_current_datetime() [直接工具,不调 Agent]
│ → 返回当前时间
│ d. ToolExecutionQueue:
│ - Agent 调用之间可并行invoke_policy + invoke_memory 并行)
│ - Agent 调用与直接工具可并行
│ - 写操作串行save_user_memory
│ e. tool_results 返回后 → 递归回步骤 b
│ Coordinator 综合所有结果,生成最终回复
│ f. 无 tool_use → 自然结束递归
5. ConversationService:
│ a. 聚合完整回复
│ b. 保存助手消息
│ c. 更新对话统计 (tokens, message count)
│ d. 提取并保存咨询状态
6. ConversationGateway → WebSocket events:
- stream_start
- agent_start {agentName: "policy_expert"}
- stream_chunk {content: "..."}
- agent_complete {agentName: "policy_expert"}
- stream_chunk {content: "综合分析..."}
- stream_end {usage: {...}}
```
## 5. 与旧架构的关键区别
| 方面 | 旧架构 | 新架构 |
|------|--------|--------|
| 对话流程控制 | 策略引擎程序状态机8 阶段) | Coordinator PromptLLM 自主判断) |
| 意图分类 | IntentClassifier规则匹配 | LLM 自行理解意图 |
| 响应质量 | ResponseGate关键词检查 | Prompt 内置质量标准 |
| 工具调用 | 单 Agent 串行调 10 个工具 | Coordinator 编排,专家 Agent 各持少量工具 |
| 评估逻辑 | 程序化评分算法 | Assessment Expert (LLM + 知识库) |
| 并发能力 | 无 | ToolExecutionQueue 并发调度 |
| 上下文管理 | 固定拼接 last 20 messages | 动态注入 8+ 种上下文 |
| 扩展性 | 改代码加阶段/工具 | 新增 Agent + 注册工具 |
## 6. 技术栈
- **Runtime**: Node.js 18+ / NestJS 10
- **AI SDK**: @anthropic-ai/sdk 0.52+
- **Models**: claude-sonnet-4-20250514 (主力), claude-haiku (辅助)
- **Streaming**: AsyncGenerator + WebSocket (Socket.io)
- **Database**: PostgreSQL + pgVector (RAG), Neo4j (知识图谱), Redis (缓存)
- **Frontend**: React 18 + Zustand + Socket.io-client

View File

@ -0,0 +1,265 @@
# 01 - Coordinator Agent (主协调器) 设计详解
## 1. 核心职责
Coordinator 是唯一直接面对用户的 Agent。它的职责是
1. **理解用户意图** — 无需额外的 IntentClassifierLLM 自行判断
2. **编排专家 Agent** — 决定调用哪些专家、传什么参数
3. **综合信息** — 将多个专家的结果整合为自然的对话回复
4. **直接回答简单问题** — 闲聊、简单确认等不需要启动专家 Agent
5. **管理对话节奏** — 控制提问频率、推进对话进程
6. **维护咨询状态** — 在回复中自报当前阶段和收集到的信息
## 2. 模型与参数
```typescript
{
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
// Prompt Caching: system prompt 使用 cache_control
system: [
{ type: 'text', text: coordinatorPrompt, cache_control: { type: 'ephemeral' } }
],
}
```
## 3. 可用工具Tools
Coordinator 拥有两类工具:
### 3.1 Agent 调用工具(核心)
| 工具名 | 描述 | 输入参数 | 返回值 |
|--------|------|----------|--------|
| `invoke_policy_expert` | 查询移民政策详情 | `{query: string, category?: string}` | 政策解读文本 |
| `invoke_assessment_expert` | 评估用户移民资格 | `{userInfo: object, targetCategories?: string[]}` | 评估报告 JSON |
| `invoke_strategist` | 获取对话策略建议 | `{conversationSummary: string, currentStage: string}` | 策略建议文本 |
| `invoke_objection_handler` | 处理用户异议 | `{objection: string, userContext: string}` | 回应方案文本 |
| `invoke_case_analyst` | 查找类似案例 | `{userProfile: object, targetCategory: string}` | 案例分析文本 |
| `invoke_memory_manager` | 管理用户记忆 | `{action: 'load'\|'save'\|'extract', ...}` | 记忆数据 |
### 3.2 直接工具(不经过 Agent
| 工具名 | 描述 | 来源 |
|--------|------|------|
| `generate_payment` | 生成支付链接 | payment-service |
| `get_current_datetime` | 获取当前时间 | 本地 |
| `web_search` | 搜索网页 | Google API |
| `get_exchange_rate` | 获取汇率 | API / 缓存 |
| `fetch_immigration_news` | 获取移民新闻 | API / 缓存 |
## 4. Agent Loop 控制流
```typescript
// agent-loop.ts - 核心控制流伪代码
interface AgentLoopParams {
messages: Message[]; // 对话历史
systemPrompt: string[]; // 系统提示(含缓存控制)
tools: ToolDefinition[]; // 所有可用工具
maxTurns: number; // 最大递归轮次 (default: 15)
maxBudgetUsd: number; // 单次对话最大成本 (default: 0.50)
conversationId: string;
userId: string;
abortSignal?: AbortSignal; // 用户中断信号
}
async function* agentLoop(params: AgentLoopParams): AsyncGenerator<StreamEvent> {
let turnCount = 0;
let totalCost = 0;
// === Guard: 超限检查 ===
if (turnCount >= params.maxTurns) {
yield { type: 'error', code: 'MAX_TURNS_REACHED' };
return;
}
if (totalCost >= params.maxBudgetUsd) {
yield { type: 'error', code: 'BUDGET_EXCEEDED' };
return;
}
if (params.abortSignal?.aborted) {
yield { type: 'error', code: 'USER_ABORTED' };
return;
}
// === Step 1: 动态上下文注入 ===
const enrichedMessages = await contextInjector.inject({
messages: params.messages,
userId: params.userId,
conversationId: params.conversationId,
});
// === Step 2: 上下文压缩(如果接近 token 上限)===
const compactedMessages = await autoCompactIfNeeded(enrichedMessages);
// === Step 3: 调用 Claude API流式===
const stream = anthropic.messages.stream({
model: 'claude-sonnet-4-20250514',
system: params.systemPrompt,
messages: compactedMessages,
tools: params.tools,
max_tokens: 4096,
});
// === Step 4: 流式收集响应 ===
const assistantBlocks: ContentBlock[] = [];
for await (const event of stream) {
// 流式传输文本给前端
if (event.type === 'content_block_delta' && event.delta.type === 'text_delta') {
yield { type: 'text', content: event.delta.text };
}
// 收集所有 content blocks
collectBlocks(event, assistantBlocks);
}
// 记录 token 使用
const usage = stream.finalMessage().usage;
totalCost += calculateCost(usage);
yield { type: 'usage', usage, cost: totalCost };
// === Step 5: 提取 tool_use blocks ===
const toolUses = assistantBlocks.filter(b => b.type === 'tool_use');
// === Step 6: 无工具调用 → 自然结束 ===
if (toolUses.length === 0) {
// 从回复中提取状态更新
const stateUpdate = extractConsultingState(assistantBlocks);
if (stateUpdate) {
yield { type: 'state_update', state: stateUpdate };
}
return; // 递归终止
}
// === Step 7: 执行工具(并发队列)===
const toolResults: ToolResult[] = [];
for (const toolUse of toolUses) {
// 通知前端 Agent 开始工作
if (toolUse.name.startsWith('invoke_')) {
yield { type: 'agent_start', agentName: toolUse.name.replace('invoke_', '') };
}
}
// 通过 ToolExecutionQueue 并发执行
const results = await toolExecutionQueue.executeAll(toolUses);
for (const result of results) {
toolResults.push(result);
// 通知前端 Agent 完成
if (result.toolName.startsWith('invoke_')) {
yield { type: 'agent_complete', agentName: result.toolName.replace('invoke_', '') };
}
}
// === Step 8: 递归 — 把工具结果喂回 Coordinator ===
turnCount++;
const newMessages = [
...params.messages,
{ role: 'assistant', content: assistantBlocks },
...toolResults.map(r => ({
role: 'user',
content: [{ type: 'tool_result', tool_use_id: r.id, content: r.output }]
})),
];
yield* agentLoop({
...params,
messages: newMessages,
});
}
```
## 5. 递归终止条件
| 条件 | 触发 | 行为 |
|------|------|------|
| 无 tool_use | LLM 认为回复完成 | 自然终止,返回文本 |
| maxTurns 超限 | turnCount >= 15 | 强制终止yield error |
| maxBudgetUsd 超限 | 累计 API 成本超限 | 强制终止yield error |
| abortSignal | 用户点击停止 | 立即终止 |
| API 错误 | Claude API 返回错误 | 尝试 fallback model否则终止 |
## 6. Coordinator System Prompt 结构
详见 [11-prompt-templates.md](./11-prompt-templates.md),核心结构:
```
# 身份定义 (Identity)
你是 iConsulting 的资深香港移民顾问...
# 你的专家团队 (Your Expert Team)
你拥有 6 个专家助手,通过工具调用来获取他们的帮助...
- Policy Expert: 调用时机、输入输出格式
- Assessment Expert: ...
- ...
# 对话策略 (Conversation Strategy)
## 咨询阶段(你自行判断当前处于哪个阶段)
- 开场阶段:目标、行为、判断条件
- 需求了解:...
- 信息收集:...
- 评估推荐:...
- 异议处理:...
- 转化促成:...
# 回复规范 (Response Guidelines)
- 语气:专业但亲和
- 长度:根据场景自适应
- 结构:每次回复以推进性问题结尾
- 禁忌不承诺成功率、不一次问超过2个问题
# 状态报告 (State Reporting)
每次回复结束时附加 <consulting_state> 标签...
# 六大移民类别详解 (Immigration Categories)
QMAS/GEP/IANG/TTPS/CIES/TECHTAS 的详细条件...
# 业务规则 (Business Rules)
付费评估服务说明、专家对接流程...
```
## 7. 与 ConversationService 的接口
```typescript
// CoordinatorAgentService 对外暴露的接口
// 与旧的 ClaudeAgentServiceV2 保持相同的 AsyncGenerator 模式
interface CoordinatorAgentService {
sendMessage(params: {
conversationContext: ConversationContext;
userMessage: string;
attachments?: Attachment[];
userId: string;
conversationId: string;
deviceInfo?: DeviceInfo;
}): AsyncGenerator<StreamEvent>;
}
```
ConversationService 只需将注入从 `ClaudeAgentServiceV2` 切换到 `CoordinatorAgentService`,内部聚合逻辑不变。
## 8. 错误处理
```typescript
// Coordinator 的错误处理策略
try {
yield* agentLoop(params);
} catch (error) {
if (error instanceof RateLimitError) {
// 等待后重试
await sleep(error.retryAfter);
yield* agentLoop(params);
} else if (error instanceof ModelOverloadedError) {
// 降级到 Haiku
yield* agentLoop({ ...params, model: 'claude-haiku' });
} else {
// 返回友好错误消息
yield {
type: 'text',
content: '抱歉,系统遇到了临时问题。请稍后重试,或联系我们的人工顾问。'
};
yield { type: 'error', code: 'INTERNAL_ERROR', message: error.message };
}
}
```

View File

@ -0,0 +1,450 @@
# 02 - Policy Expert Agent (政策专家) 设计详解
## 1. 核心职责
Policy Expert 是系统中的**移民政策百科全书**。当用户提出与政策、条件、流程、时间线相关的问题时Coordinator 会调用此 Agent 获取准确的政策信息。
核心能力:
1. **精准检索政策** -- 通过 knowledge-service RAG 搜索最相关的政策条文
2. **结构化解读** -- 将复杂政策拆解为用户能理解的要点(条件、流程、时间、费用)
3. **引用出处** -- 每条关键信息附带来源(文章标题、官方条文编号)
4. **类别对比** -- 当用户问及多个类别时,能并列对比差异
5. **时效性标注** -- 标注政策的生效日期与是否有变更风险
> 设计原则:**宁可多查一次知识库,也不要凭 LLM 记忆编造政策细节。**
## 2. 模型与参数
```typescript
{
model: 'claude-sonnet-4-20250514', // 准确性要求高,不能用 Haiku
max_tokens: 2048,
temperature: 0, // 政策回答必须确定性,不需要创造力
system: [
{
type: 'text',
text: policyExpertPrompt,
cache_control: { type: 'ephemeral' }
}
],
}
```
选用 Sonnet 的理由:
- 政策解读需要高准确性hallucination 会导致用户做出错误决策
- 需要综合多条 RAG 结果进行推理整合
- Haiku 在长文本归纳和交叉引用方面表现不足
## 3. 可用工具 (Available Tools)
Policy Expert 只有 **1 个工具**,最大限度减少复杂度:
### 3.1 search_knowledge
```typescript
{
name: 'search_knowledge',
description: '搜索香港移民知识库,获取政策条文、申请条件、流程指南、费用标准等信息。每次搜索尽量用精确的关键词。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询内容,建议包含具体类别名称和关键词,如"TTPS高才通B类申请条件"'
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别代码,用于缩小搜索范围'
}
},
required: ['query']
}
}
```
**底层实现**:调用 knowledge-service 的 `POST /api/v1/knowledge/retrieve`
```typescript
// HTTP 请求
const response = await axios.post('http://knowledge-service:3003/api/v1/knowledge/retrieve', {
query: input.query,
category: input.category,
userId: context.userId,
limit: 5,
similarityThreshold: 0.7,
});
```
## 4. System Prompt 要点
```
# 身份
你是 iConsulting 的政策研究专家,专注于香港六大移民类别的政策解读。
# 核心原则
1. 一切回答必须基于知识库检索结果,不要凭记忆回答
2. 如果知识库没有相关信息,明确说明"未找到相关政策信息"
3. 每个关键论点必须标注来源
4. 区分"官方政策"和"实务经验"
5. 政策有变更风险的,加注提醒
# 搜索策略
- 第一次搜索:用用户原始问题的关键词
- 如果结果不够精准:用更具体的类别+条件进行二次搜索
- 如果涉及多个类别:分别搜索每个类别
- 最多进行 3 轮搜索
# 六大移民类别代码
- QMAS: 优才计划 (Quality Migrant Admission Scheme)
- GEP: 专才计划 (General Employment Policy)
- IANG: 留学IANG (Immigration Arrangements for Non-local Graduates)
- TTPS: 高才通计划 (Top Talent Pass Scheme)
- CIES: 投资移民 (Capital Investment Entrant Scheme)
- TECHTAS: 科技人才入境计划 (Technology Talent Admission Scheme)
# 输出格式
必须返回结构化 JSON包含 policy_summary、requirements、process_steps、important_notes、sources
```
## 5. 输入/输出格式
### 输入 (Coordinator 传入)
```typescript
interface PolicyExpertInput {
/** 用户的政策相关问题 */
query: string;
/** 指定的移民类别可选Coordinator 可能已判断出类别) */
category?: 'QMAS' | 'GEP' | 'IANG' | 'TTPS' | 'CIES' | 'TECHTAS';
}
```
### 输出 (返回给 Coordinator)
```typescript
interface PolicyExpertOutput {
/** 政策摘要:一段话概括核心结论 */
policy_summary: string;
/** 具体条件/要求列表 */
requirements: Array<{
item: string; // 条件项,如"年龄要求"
detail: string; // 具体说明
mandatory: boolean; // 是否为必要条件
}>;
/** 申请流程步骤 */
process_steps: Array<{
step: number;
title: string;
description: string;
estimated_time?: string; // 预计耗时
}>;
/** 重要提醒 */
important_notes: string[];
/** 引用来源 */
sources: Array<{
title: string;
article_id?: string;
relevance: number; // 0-1
}>;
/** 政策有效时间(如果知识库中有标注) */
effective_date?: string;
/** 是否有近期变更风险 */
change_risk?: 'low' | 'medium' | 'high';
}
```
## 6. 触发时机 (When to Trigger)
Coordinator 在以下场景调用 `invoke_policy_expert`
| 场景 | 示例用户消息 | Coordinator 传入的参数 |
|------|-------------|----------------------|
| 询问某类别条件 | "优才计划需要什么条件?" | `{query: "优才计划申请条件", category: "QMAS"}` |
| 询问申请流程 | "高才通怎么申请?" | `{query: "高才通申请流程步骤", category: "TTPS"}` |
| 询问费用 | "投资移民要多少钱?" | `{query: "投资移民资金门槛", category: "CIES"}` |
| 询问时间线 | "IANG多久能批下来" | `{query: "IANG审批时间", category: "IANG"}` |
| 类别对比 | "优才和高才通有什么区别?" | `{query: "优才计划vs高才通区别对比"}` |
| 政策变更 | "最近移民政策有变化吗?" | `{query: "香港移民政策最新变化"}` |
| 续签规则 | "签证到期后怎么续签?" | `{query: "签证续签条件流程", category: "TTPS"}` |
**不应触发的场景**
- 用户只是闲聊("你好"
- 用户在讨论付费服务
- 用户的问题是关于个人资格评估(应该调 Assessment Expert
## 7. 内部循环 (Internal Loop)
Policy Expert 有自己独立的 agent loop最多 **3 轮** tool 调用:
```
┌─────────────────────────────────────────────────┐
│ Policy Expert Internal Loop (max 3 turns) │
│ │
│ Turn 1: 初始搜索 │
│ ├── search_knowledge({query: 用户原问题}) │
│ ├── 分析结果:信息是否足够? │
│ │ ├── YES → 生成结构化输出,结束 │
│ │ └── NO → 继续 Turn 2 │
│ │ │
│ Turn 2: 补充搜索 │
│ ├── search_knowledge({query: 更精确的关键词}) │
│ ├── 综合 Turn 1 + Turn 2 结果 │
│ │ ├── 足够 → 生成输出,结束 │
│ │ └── 不够 → 继续 Turn 3 │
│ │ │
│ Turn 3: 最终搜索 │
│ ├── search_knowledge({query: 不同角度的查询}) │
│ ├── 综合所有结果,生成最终输出 │
│ └── 无论是否完整,必须结束并返回 │
└─────────────────────────────────────────────────┘
```
**内部循环伪代码**
```typescript
async function policyExpertLoop(input: PolicyExpertInput): Promise<PolicyExpertOutput> {
const messages: Message[] = [
{ role: 'user', content: JSON.stringify(input) }
];
for (let turn = 0; turn < 3; turn++) {
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-20250514',
system: policyExpertSystemPrompt,
messages,
tools: [searchKnowledgeTool],
max_tokens: 2048,
temperature: 0,
});
// 收集响应
const toolUses = response.content.filter(b => b.type === 'tool_use');
if (toolUses.length === 0) {
// LLM 认为信息足够,提取结构化输出
return extractPolicyOutput(response);
}
// 执行 search_knowledge
const results = await executeTools(toolUses);
// 将结果喂回
messages.push({ role: 'assistant', content: response.content });
messages.push({
role: 'user',
content: results.map(r => ({
type: 'tool_result',
tool_use_id: r.id,
content: r.output,
}))
});
}
// 达到最大轮次,强制生成输出
return forceGenerateOutput(messages);
}
```
## 8. 与其他 Agent 的关系
```
┌──────────────┐ invoke_policy_expert ┌──────────────┐
│ │ ──────────────────────────────────→ │ │
│ Coordinator │ PolicyExpertOutput │ Policy │
│ │ ←────────────────────────────────── │ Expert │
└──────────────┘ └──────┬───────┘
│ │
│ Coordinator 综合政策信息后 │ search_knowledge
│ 回复用户 ↓
│ ┌──────────────┐
│ │ Knowledge │
│ │ Service │
│ │ (Port 3003) │
│ └──────────────┘
├── 可能同时调用 Assessment Expert用户问完条件想知道自己是否符合
├── 可能同时调用 Case Analyst用政策+案例一起说服用户)
└── Policy Expert 的结果可能触发 Coordinator 后续调用 Objection Handler
```
**关键关系**
- **Coordinator → Policy Expert**单向调用Coordinator 传入问题Policy Expert 返回结构化政策信息
- **Policy Expert → Knowledge Service**:通过 HTTP API 检索知识库
- **Policy Expert 不直接与其他 Agent 交互**:所有 Agent 间通信都通过 Coordinator 中转
- **与 Assessment Expert 互补**Policy Expert 回答"政策是什么"Assessment Expert 回答"你是否符合"
## 9. 示例场景
### 场景 1询问高才通B类条件
**Coordinator 调用**
```json
{
"tool": "invoke_policy_expert",
"input": {
"query": "高才通B类的申请条件是什么需要什么学历和工作经验",
"category": "TTPS"
}
}
```
**Policy Expert 内部执行**
```
Turn 1: search_knowledge({query: "高才通B类申请条件学历工作经验", category: "TTPS"})
→ 知识库返回 3 条结果,包含 TTPS B类详细条件
→ 信息充足,直接生成输出
```
**返回结果**
```json
{
"policy_summary": "高才通计划B类面向全球百强大学毕业生要求在紧接申请日期前五年内获得全球百强大学颁授的学士或以上学位且在紧接申请日期前五年内累积至少三年的工作经验。不设名额限制。",
"requirements": [
{
"item": "学历要求",
"detail": "持有全球百强大学颁授的学士学位或以上学历以泰晤士高等教育世界大学排名、QS世界大学排名、US News全球最佳大学排名、上海交通大学世界大学学术排名中任一排名为准",
"mandatory": true
},
{
"item": "毕业时间",
"detail": "在紧接申请日期前五年内取得学位",
"mandatory": true
},
{
"item": "工作经验",
"detail": "在紧接申请日期前五年内累积至少三年工作经验",
"mandatory": true
},
{
"item": "良好品格",
"detail": "无犯罪记录、无不良入境记录",
"mandatory": true
}
],
"process_steps": [
{
"step": 1,
"title": "准备材料",
"description": "学位证书、成绩单、工作证明(雇主推荐信/劳动合同/社保记录)、身份证明文件",
"estimated_time": "1-2周"
},
{
"step": 2,
"title": "在线申请",
"description": "通过入境事务处网站提交电子申请表格",
"estimated_time": "1天"
},
{
"step": 3,
"title": "审批等待",
"description": "入境事务处审核申请",
"estimated_time": "4-6周"
},
{
"step": 4,
"title": "获批签注",
"description": "获批后前往中国签证申请服务中心办理签注手续",
"estimated_time": "1-2周"
}
],
"important_notes": [
"全球百强大学名单每年更新,以申请时公布的名单为准",
"工作经验不限行业和地域,但需提供有效证明文件",
"B类无年度名额限制获批后可获2年签证",
"首次入境后无需立即找到工作,可在港自由求职"
],
"sources": [
{
"title": "高端人才通行证计划申请指南 - 入境事务处",
"article_id": "ttps-guide-001",
"relevance": 0.95
},
{
"title": "全球百强大学综合名单2024/25年度",
"article_id": "ttps-univ-list-001",
"relevance": 0.88
}
],
"effective_date": "2022-12-28起实施",
"change_risk": "low"
}
```
### 场景 2多类别对比查询
**Coordinator 调用**
```json
{
"tool": "invoke_policy_expert",
"input": {
"query": "优才计划和高才通有什么区别?哪个更容易?"
}
}
```
**Policy Expert 内部执行**
```
Turn 1: search_knowledge({query: "优才计划vs高才通区别对比"})
→ 返回 2 条结果,但对比信息不够全面
Turn 2: search_knowledge({query: "QMAS优才计划申请条件评分标准", category: "QMAS"})
→ 补充优才计划详情
Turn 3: search_knowledge({query: "TTPS高才通三类ABC分类", category: "TTPS"})
→ 补充高才通详情
→ 综合三轮结果生成对比输出
```
**返回结果**
```json
{
"policy_summary": "优才计划(QMAS)采用综合计分制对年龄、学历、工作经验、语言能力等12项指标打分择优录取高才通(TTPS)条件更明确分ABC三类满足条件即可获批。总体而言如果你是百强大学毕业或高薪人士高才通更直接快速如果你条件综合但不满足高才通硬性标准优才计划是更灵活的选择。",
"requirements": [
{
"item": "优才计划 - 计分制",
"detail": "12项评分标准年龄、学历、工作年限、年收入、语言能力等满足6项为基本门槛总分越高越有竞争力",
"mandatory": true
},
{
"item": "高才通A类",
"detail": "年薪达250万港币约220万人民币无学历要求",
"mandatory": true
},
{
"item": "高才通B类",
"detail": "全球百强大学毕业 + 5年内有3年以上工作经验",
"mandatory": true
},
{
"item": "高才通C类",
"detail": "全球百强大学毕业 + 5年内少于3年工作经验年度限额10,000名",
"mandatory": true
}
],
"process_steps": [],
"important_notes": [
"审批速度:高才通 4-6 周,优才计划 9-12 个月",
"高才通条件明确、审批快、通过率高;优才计划竞争性强、周期长但灵活性更大",
"两者可同时申请,不冲突",
"优才获批后需有在港计划,高才通可自由求职"
],
"sources": [
{
"title": "香港六大人才引进计划对比分析",
"article_id": "compare-001",
"relevance": 0.92
}
]
}
```

View File

@ -0,0 +1,475 @@
# 03 - Assessment Expert Agent (评估专家) 设计详解
## 1. 核心职责
Assessment Expert 是系统中的**资格评估引擎**。当 Coordinator 收集到足够的用户信息后,调用此 Agent 对用户的移民资格进行全面评估。
核心能力:
1. **全量评估** -- 必须评估所有 6 个移民类别QMAS/GEP/IANG/TTPS/CIES/TECHTAS给出逐项打分
2. **精准匹配** -- 根据用户画像(年龄、学历、院校、收入、行业、工作经验)匹配最优类别
3. **TTPS A/B/C 分类判定** -- 严格按照收入/院校/经验年限区分三类
4. **QMAS 综合计分** -- 基于 12 项评分标准给出预估分数
5. **CIES 门槛判断** -- 3000 万港币硬性门槛评估
6. **排序推荐** -- 按适配度排名,给出首选、备选方案
7. **风险提示** -- 指出不足之处和改善建议
> 设计原则:**评估必须有理有据,每一项得分/扣分都要给出原因。** 替代旧架构中 `StrategyEngineService.performAssessment()` 的硬编码逻辑。
## 2. 模型与参数
```typescript
{
model: 'claude-sonnet-4-20250514', // 推理能力要求高
max_tokens: 3000, // 评估报告较长
temperature: 0, // 评估必须确定性
system: [
{
type: 'text',
text: assessmentExpertPrompt,
cache_control: { type: 'ephemeral' }
}
],
}
```
选用 Sonnet 的理由:
- 需要综合多维度信息进行交叉推理
- 评分逻辑复杂TTPS 三类判定、QMAS 12 项评分)
- 结果直接影响用户的移民决策,不容有误
## 3. 可用工具 (Available Tools)
Assessment Expert 有 **2 个工具**
### 3.1 search_knowledge
```typescript
{
name: 'search_knowledge',
description: '搜索知识库获取最新的评分标准、资格条件、政策变更信息,确保评估基于最新政策。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询,如"QMAS优才计划评分标准详细规则"'
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别代码'
}
},
required: ['query']
}
}
```
### 3.2 get_user_context
```typescript
{
name: 'get_user_context',
description: '获取用户的历史记忆信息,补充 Coordinator 传入参数中可能缺少的用户背景。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '用于检索相关记忆的查询,如"用户的学历工作背景"'
}
},
required: ['query']
}
}
```
## 4. System Prompt 要点
```
# 身份
你是 iConsulting 的移民资格评估专家。你的任务是根据用户信息,对香港六大移民类别进行全面评估。
# 评估原则
1. 必须评估所有 6 个类别,不能遗漏
2. 每个类别给出 0-100 的适配度评分
3. 评分必须有理有据,列出加分项和减分项
4. 信息不足的字段标注为"未知",不要猜测
5. 最终按得分排序,给出推荐方案
# TTPS 高才通判定规则
A类年薪 >= 250万港币约220万人民币无学历要求
B类全球百强大学毕业 + 5年内累积 >= 3年工作经验
C类全球百强大学毕业 + 5年内累积 < 3年工作经验年度限额10,000名
* 百强大学认定以泰晤士、QS、US News、上海交大四大排名任一为准
# QMAS 优才计划评分维度
- 年龄 (age_score): 18-39岁满分40-44岁扣分45-50岁大幅扣分51+不符合
- 学历 (education_score): 博士 > 硕士/双学位 > 学士 > 大专
- 工作经验 (experience_score): 10年+满分5-9年中等2-4年基础
- 语言能力 (language_score): 中英文流利加分,其他语言额外加分
- 年收入 (income_score): 200万+港币满分100-200万中等50-100万基础
- 家庭背景 (family_score): 配偶学历/工作,子女情况
- 其他:名企/跨国公司经验、海外工作经验、行业紧缺、持有专业资格等
- 总分达到80+为高度推荐60-79为推荐40-59为条件推荐<40不推荐
# CIES 投资移民门槛
- 最低投资门槛3000万港币净资产
- 投资于许可的金融资产和非住宅房地产
- 1000万港币额度可获考虑
- 申请前6个月需持有相关资产
# GEP 专才计划要点
- 必须有香港雇主担保
- 年薪达200万港币可属人才清单范畴
- 不限行业,但需证明职位难以在港招聘
# IANG 留学IANG要点
- 需为香港认可院校毕业生
- 毕业后可获 IANG 签证在港求职/工作
- 已扩展至大湾区校区毕业生
# TECHTAS 科技人才要点
- 需有获配额的科技公司雇主
- 从事14个科技领域之一
- 薪酬不低于市场水平
# 输出格式
必须返回 JSON包含 categories_assessment、overall_recommendation、highlights、concerns、next_steps
```
## 5. 输入/输出格式
### 输入 (Coordinator 传入)
```typescript
interface AssessmentExpertInput {
/** 用户基本信息Coordinator 从对话中收集) */
userInfo: {
age?: number;
education?: string; // 学历:博士/硕士/学士/大专
university?: string; // 毕业院校名称
major?: string; // 专业
graduationYear?: number; // 毕业年份
totalYearsOfExperience?: number;
currentJobTitle?: string;
currentIndustry?: string;
annualIncome?: number; // 年收入(人民币)
incomeCurrency?: string; // 收入货币,默认 CNY
hasHongKongEmployer?: boolean;
hasTechBackground?: boolean;
investmentAmount?: number; // 可投资金额(港币)
languageSkills?: string[]; // 语言能力
nationality?: string;
currentLocation?: string;
additionalInfo?: Record<string, unknown>;
};
/** Coordinator 建议优先评估的类别(可选) */
targetCategories?: Array<'QMAS' | 'GEP' | 'IANG' | 'TTPS' | 'CIES' | 'TECHTAS'>;
}
```
### 输出 (返回给 Coordinator)
```typescript
interface AssessmentExpertOutput {
/** 各类别评估详情 */
categories_assessment: Array<{
category: 'QMAS' | 'GEP' | 'IANG' | 'TTPS' | 'CIES' | 'TECHTAS';
category_name: string; // 中文名
eligibility_score: number; // 0-100 适配度
is_eligible: boolean; // 是否基本符合
recommendation: 'HIGHLY_RECOMMENDED' | 'RECOMMENDED' | 'CONDITIONALLY_RECOMMENDED' | 'NOT_RECOMMENDED';
sub_class?: string; // TTPS 的 A/B/C 类
scoring_breakdown: Array<{
dimension: string; // 评分维度
score: number; // 得分
max_score: number; // 满分
reason: string; // 得分理由
}>;
strengths: string[]; // 优势项
weaknesses: string[]; // 不足项
improvement_suggestions: string[]; // 改善建议
}>;
/** 综合推荐(按优先级排序的类别列表) */
overall_recommendation: Array<{
rank: number;
category: string;
category_name: string;
score: number;
summary: string; // 一句话推荐理由
}>;
/** 用户核心优势 */
highlights: string[];
/** 主要顾虑/风险 */
concerns: string[];
/** 建议的下一步行动 */
next_steps: string[];
/** 信息完整度评估 */
info_completeness: {
score: number; // 0-100
missing_fields: string[]; // 缺失的关键信息
impact: string; // 信息缺失对评估的影响说明
};
}
```
## 6. 触发时机 (When to Trigger)
Coordinator 在以下场景调用 `invoke_assessment_expert`
| 场景 | 前提条件 | Coordinator 行为 |
|------|----------|-----------------|
| 用户主动要求评估 | "帮我看看我适合哪个" | 直接调用,传入已有 userInfo |
| 信息收集达到阈值 | 已收集 3+ 关键字段 | Coordinator 判断时机合适,主动调用 |
| 用户补充了关键信息 | 用户补充了收入/学历等 | 重新调用获取更新的评估 |
| 转化阶段需要依据 | Coordinator 需要数据支撑推荐 | 调用后综合到回复中 |
**关键信息阈值**:至少需要以下 3 项中的 2 项才值得调用:
- `age``education`(基础画像)
- `totalYearsOfExperience``annualIncome`(专业能力)
- `university``currentIndustry`(细分匹配)
## 7. 内部循环 (Internal Loop)
Assessment Expert 的 agent loop 最多 **3 轮** tool 调用:
```
┌─────────────────────────────────────────────────────┐
│ Assessment Expert Internal Loop (max 3 turns) │
│ │
│ Turn 0: 分析输入信息完整度 │
│ ├── 信息充分 → 直接开始评估 │
│ └── 信息不足 → get_user_context() 补充用户记忆 │
│ │
│ Turn 1: 查询评估标准(可选) │
│ ├── search_knowledge({query: "TTPS高才通评分标准"}) │
│ └── 确保用最新的政策标准评估 │
│ │
│ Turn 2: 补充特定类别信息(如需) │
│ ├── search_knowledge({query: 细分查询}) │
│ └── 如百强大学名单确认、人才清单行业确认等 │
│ │
│ Final: 综合所有信息 │
│ ├── 对 6 个类别逐一评分 │
│ ├── 排序推荐 │
│ └── 返回结构化评估报告 │
└─────────────────────────────────────────────────────┘
```
**评估决策树**(内部推理逻辑):
```
用户信息输入
├── 年薪 >= 250万港币?
│ └── YES → TTPS A类 高度推荐 (90+分)
├── 百强大学毕业?
│ ├── YES + 工作经验 >= 3年 → TTPS B类 高度推荐 (85+分)
│ └── YES + 工作经验 < 3年 TTPS C类 推荐 (70+, 注意名额限制)
├── 可投资金额 >= 3000万港币?
│ └── YES → CIES 高度推荐 (90+分)
├── 有香港雇主?
│ ├── YES + 科技行业 → TECHTAS 高度推荐 (85+分)
│ └── YES + 其他行业 → GEP 高度推荐 (85+分)
├── 香港院校毕业?
│ └── YES → IANG 高度推荐 (90+分)
└── 综合条件评估 → QMAS
├── 12项计分
└── 按总分给出推荐等级
```
## 8. 与其他 Agent 的关系
```
┌──────────────┐ invoke_assessment_expert ┌──────────────┐
│ │ ──────────────────────────────→ │ │
│ Coordinator │ AssessmentExpertOutput │ Assessment │
│ │ ←────────────────────────────── │ Expert │
└──────┬───────┘ └──────┬───────┘
│ │
│ ┌────────┴────────┐
│ │ │
│ get_user_context search_knowledge
│ │ │
│ ▼ ▼
│ ┌────────────┐ ┌────────────┐
│ │ Knowledge │ │ Knowledge │
│ │ Service │ │ Service │
│ │ (Memory) │ │ (RAG) │
│ └────────────┘ └────────────┘
├── 评估完成后Coordinator 可能调用 Strategist 决定如何呈现结果
├── 评估结果 → Memory Manager 保存评估摘要到用户记忆
└── 如果评估结果引发用户异议 → Objection Handler
```
**与旧架构的对应关系**
| 旧架构 | 新架构 |
|--------|--------|
| `StrategyEngineService.performAssessment()` | Assessment Expert Agent |
| 硬编码的评分逻辑(`if/else` 链) | LLM + 知识库动态评估 |
| 只评估传入的单个类别 | 强制评估全部 6 个类别 |
| 简单的 `suitabilityScore` | 多维度 `scoring_breakdown` |
## 9. 示例场景
### 场景 1典型 IT 从业者评估
**Coordinator 调用**
```json
{
"tool": "invoke_assessment_expert",
"input": {
"userInfo": {
"age": 32,
"education": "硕士",
"university": "浙江大学",
"major": "计算机科学",
"totalYearsOfExperience": 8,
"currentJobTitle": "高级软件工程师",
"currentIndustry": "互联网/科技",
"annualIncome": 800000,
"incomeCurrency": "CNY",
"hasHongKongEmployer": false,
"hasTechBackground": true,
"languageSkills": ["中文", "英文"]
}
}
}
```
**返回结果**(精简展示):
```json
{
"categories_assessment": [
{
"category": "TTPS",
"category_name": "高才通计划",
"eligibility_score": 88,
"is_eligible": true,
"recommendation": "HIGHLY_RECOMMENDED",
"sub_class": "B",
"scoring_breakdown": [
{"dimension": "院校资质", "score": 30, "max_score": 30, "reason": "浙江大学位列全球百强大学名单"},
{"dimension": "工作经验", "score": 28, "max_score": 30, "reason": "8年经验满足B类3年+要求,且经验丰富"},
{"dimension": "毕业时效", "score": 20, "max_score": 20, "reason": "硕士毕业在5年内需确认具体年份"},
{"dimension": "行业匹配", "score": 10, "max_score": 20, "reason": "科技行业为香港重点发展领域"}
],
"strengths": ["百强大学硕士", "8年科技行业经验", "年龄处于黄金期"],
"weaknesses": ["年薪未达A类250万港币标准"],
"improvement_suggestions": ["确认浙江大学在当年排名中是否在列"]
},
{
"category": "QMAS",
"category_name": "优才计划",
"eligibility_score": 75,
"is_eligible": true,
"recommendation": "RECOMMENDED",
"scoring_breakdown": [
{"dimension": "年龄", "score": 30, "max_score": 30, "reason": "32岁年龄加分最高档"},
{"dimension": "学历", "score": 20, "max_score": 25, "reason": "硕士学历"},
{"dimension": "工作经验", "score": 15, "max_score": 20, "reason": "8年经验属于资深水平"},
{"dimension": "年收入", "score": 5, "max_score": 15, "reason": "80万人民币(约87万港币),中等水平"},
{"dimension": "语言能力", "score": 5, "max_score": 10, "reason": "中英双语"}
],
"strengths": ["年龄优势突出", "学历+经验组合好"],
"weaknesses": ["年收入在优才评分中属于中等"],
"improvement_suggestions": ["可准备详细的项目成果证明提升竞争力"]
},
{
"category": "GEP",
"category_name": "专才计划",
"eligibility_score": 30,
"is_eligible": false,
"recommendation": "NOT_RECOMMENDED",
"scoring_breakdown": [
{"dimension": "雇主担保", "score": 0, "max_score": 50, "reason": "目前没有香港雇主"}
],
"strengths": ["专业背景符合市场需求"],
"weaknesses": ["缺少香港雇主担保(硬性条件)"],
"improvement_suggestions": ["可先在港求职,获得 offer 后通过 GEP 申请"]
},
{
"category": "TECHTAS",
"category_name": "科技人才入境计划",
"eligibility_score": 35,
"is_eligible": false,
"recommendation": "CONDITIONALLY_RECOMMENDED",
"scoring_breakdown": [
{"dimension": "科技背景", "score": 20, "max_score": 30, "reason": "计算机专业+科技行业从业者"},
{"dimension": "雇主配额", "score": 0, "max_score": 40, "reason": "需要有获配额的香港科技公司雇佣"},
{"dimension": "领域匹配", "score": 15, "max_score": 30, "reason": "软件工程属于AI/数据分析相关领域"}
],
"strengths": ["科技背景匹配14个指定领域"],
"weaknesses": ["需有获配额的科技公司雇佣"],
"improvement_suggestions": ["可关注香港科技园、数码港入驻企业的招聘机会"]
},
{
"category": "CIES",
"category_name": "投资移民",
"eligibility_score": 5,
"is_eligible": false,
"recommendation": "NOT_RECOMMENDED",
"scoring_breakdown": [
{"dimension": "投资金额", "score": 0, "max_score": 80, "reason": "未提供投资金额信息且最低门槛为3000万港币"}
],
"strengths": [],
"weaknesses": ["投资移民门槛极高3000万港币"],
"improvement_suggestions": []
},
{
"category": "IANG",
"category_name": "留学IANG",
"eligibility_score": 0,
"is_eligible": false,
"recommendation": "NOT_RECOMMENDED",
"scoring_breakdown": [
{"dimension": "港校毕业", "score": 0, "max_score": 80, "reason": "毕业于浙江大学(内地院校),非香港认可院校毕业生"}
],
"strengths": [],
"weaknesses": ["非香港院校毕业不符合IANG基本条件"],
"improvement_suggestions": ["如有兴趣可考虑先赴港读书再走IANG路径"]
}
],
"overall_recommendation": [
{"rank": 1, "category": "TTPS", "category_name": "高才通B类", "score": 88, "summary": "百强大学硕士+8年经验高度匹配B类条件审批快、通过率高"},
{"rank": 2, "category": "QMAS", "category_name": "优才计划", "score": 75, "summary": "综合条件优秀,可作为备选方案同步申请"},
{"rank": 3, "category": "TECHTAS", "category_name": "科技人才", "score": 35, "summary": "如能获得港企配额雇佣,科技背景匹配度高"}
],
"highlights": [
"浙江大学属于全球百强TTPS B类通道畅通",
"32岁年龄在所有类别中均为加分项",
"8年科技行业经验专业背景扎实",
"中英双语能力"
],
"concerns": [
"需确认浙江大学在当年具体排名榜单中是否在列",
"年收入距离TTPS A类标准有较大差距",
"暂无香港雇主关系GEP/TECHTAS路径暂不可行"
],
"next_steps": [
"确认浙江大学硕士毕业的具体年份TTPS B类要求5年内",
"准备学位证书、工作证明等申请材料",
"建议首选TTPS B类申请同时准备QMAS作为备选",
"可预约付费详细评估,获取完整申请方案"
],
"info_completeness": {
"score": 75,
"missing_fields": ["graduationYear", "nationality", "investmentAmount"],
"impact": "毕业年份会影响TTPS时效判定国籍信息影响签证类型其他缺失字段影响较小"
}
}
```

View File

@ -0,0 +1,395 @@
# 04 - Conversation Strategist Agent (策略顾问) 设计详解
## 1. 核心职责
Strategist 是系统中的**幕后军师**。它的输出**不直接展示给用户**,而是为 Coordinator 提供对话策略建议。它替代了旧架构中 `StrategyEngineService` 的阶段判断和引导逻辑。
核心能力:
1. **情绪感知** -- 分析用户当前的情绪状态(积极/中立/犹豫/抗拒)
2. **阶段判断** -- 判断对话处于哪个咨询阶段(开场/了解需求/信息收集/评估推荐/异议处理/转化促成)
3. **转化信号检测** -- 识别用户是否释放了购买意向信号
4. **信息缺口分析** -- 判断还缺哪些关键信息,以及收集优先级
5. **策略推荐** -- 建议 Coordinator 下一步应该做什么(继续收集信息/推进评估/处理异议/促成转化)
6. **语气调节** -- 建议回复的语气和节奏
> 设计原则:**Strategist 是 Coordinator 的私人顾问,用户永远看不到它的输出。** 它帮助 Coordinator 做更好的对话决策,而不是直接生成用户回复。
## 2. 模型与参数
```typescript
{
model: 'claude-sonnet-4-20250514', // 情商和推理能力要求高
max_tokens: 1500, // 策略建议不需要太长
temperature: 0.3, // 允许少量创造性(策略建议需要灵活性)
system: [
{
type: 'text',
text: strategistPrompt,
cache_control: { type: 'ephemeral' }
}
],
}
```
选用 Sonnet 的理由:
- 需要深层次的情绪分析和对话理解
- 需要综合考虑商业目标和用户体验的平衡
- 策略建议的质量直接影响转化率
## 3. 可用工具 (Available Tools)
Strategist 只有 **1 个工具**
### 3.1 get_user_context
```typescript
{
name: 'get_user_context',
description: '获取用户的历史背景信息和过往对话记忆,用于制定更精准的对话策略。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '检索用户记忆的查询,如"用户的购买意向和顾虑"'
}
},
required: ['query']
}
}
```
> Strategist **不需要** search_knowledge因为它不回答政策问题。它只需要理解用户是谁、想什么、担心什么。
## 4. System Prompt 要点
```
# 身份
你是 iConsulting 的对话策略顾问。你的建议仅供 Coordinator 参考,不会展示给用户。
# 核心目标
在确保用户体验的前提下,最大化咨询转化率。转化路径:
1. 免费咨询 → 收集信息 → 初步评估 → 付费详细评估¥99-199
2. 付费评估 → 推荐方案 → 预约人工专家对接
# 对话阶段定义
- GREETING: 开场寒暄建立信任1-2轮
- DISCOVERY: 了解需求初步分类2-3轮
- INFO_COLLECTION: 收集评估所需信息3-5轮
- ASSESSMENT: 展示评估结果和推荐方案1-2轮
- OBJECTION_HANDLING: 处理用户顾虑和异议1-3轮
- CONVERSION: 促成付费或专家对接1-2轮
- FOLLOW_UP: 售后跟进(持续)
# 分析维度
1. 用户情绪enthusiastic / positive / neutral / hesitant / resistant / frustrated
2. 转化信号:
- 强信号:询问价格、如何付款、下一步怎么做
- 中信号:频繁追问细节、表达时间紧迫感
- 弱信号问了3个以上问题、主动分享个人信息
- 负信号:表达"太贵了""再考虑""不急"
3. 信息缺口优先级:
- P0评估必需年龄、学历、工作经验
- P1精准匹配院校、年收入、行业
- P2细化方案语言能力、家庭情况、时间规划
# 策略原则
- 不要一次问超过2个问题
- 每3轮信息收集后给一个小结或正面反馈
- 用户表现出犹豫时,先共情再提供事实
- 不要过早推销付费服务,先让用户感受到价值
- 检测到强转化信号时,建议 Coordinator 适时引入付费评估
```
## 5. 输入/输出格式
### 输入 (Coordinator 传入)
```typescript
interface StrategistInput {
/** 对话摘要:最近几轮的概括 */
conversationSummary: string;
/** Coordinator 判断的当前阶段 */
currentStage: string;
/** 用户最新一条消息 */
latestUserMessage?: string;
/** 已收集的用户信息键值对 */
collectedInfo?: Record<string, unknown>;
/** 当前对话轮次 */
turnCount?: number;
}
```
### 输出 (返回给 Coordinator)
```typescript
interface StrategistOutput {
/** 策略建议(最核心的内容) */
strategy_advice: {
/** 建议的下一步行动 */
recommended_next_action:
| 'continue_collecting_info' // 继续收集信息
| 'provide_assessment' // 给出评估结果
| 'address_objection' // 处理异议
| 'offer_paid_service' // 推荐付费服务
| 'connect_expert' // 推荐人工专家
| 'provide_policy_info' // 提供政策信息
| 'build_rapport' // 建立关系(闲聊/共情)
| 'summarize_progress'; // 小结当前进展
/** 行动的具体建议描述 */
action_detail: string;
/** 信息收集优先级(如果推荐继续收集) */
missing_info_priorities: Array<{
field: string; // 字段名
priority: 'P0' | 'P1' | 'P2';
suggested_question: string; // 建议的提问方式
}>;
};
/** 转化信号分析 */
conversion_signals: {
signal_strength: 'strong' | 'medium' | 'weak' | 'negative';
signals_detected: string[]; // 检测到的具体信号
conversion_readiness: number; // 0-100 转化就绪度
};
/** 情绪分析 */
sentiment_analysis: {
current_mood: 'enthusiastic' | 'positive' | 'neutral' | 'hesitant' | 'resistant' | 'frustrated';
mood_trend: 'improving' | 'stable' | 'declining';
key_concerns: string[]; // 用户当前主要顾虑
};
/** 语气调节建议 */
tone_adjustment: {
suggested_tone: 'professional' | 'warm' | 'encouraging' | 'empathetic' | 'urgent';
response_length: 'short' | 'medium' | 'long';
should_use_emoji: boolean;
special_notes: string; // 特殊注意事项
};
/** 阶段建议 */
stage_assessment: {
current_stage: string;
should_transition: boolean;
suggested_next_stage?: string;
transition_reason?: string;
};
}
```
## 6. 触发时机 (When to Trigger)
Coordinator 在以下场景调用 `invoke_strategist`
| 场景 | 触发条件 | 目的 |
|------|----------|------|
| 对话初期定调 | 第 2-3 轮对话时 | 获取初始策略定位 |
| 用户情绪变化 | 用户语气/态度明显变化 | 及时调整对话策略 |
| 信息收集中期 | 每收集 2-3 条信息后 | 判断是否该给评估了 |
| 转化决策点 | 评估结果呈现后 | 判断是否该推付费服务 |
| 异议出现 | 用户表达顾虑/犹豫 | 获取处理建议(配合 Objection Handler |
| 对话停滞 | 用户回复变短/间隔变长 | 获取重新激活策略 |
**不应触发的场景**
- 第一轮开场白(还没有足够信息做策略分析)
- 用户提的是纯政策问题(直接调 Policy Expert 即可)
- 对话已经进入支付流程
**调用频率控制**:建议每 2-3 轮调用一次,避免每轮都调(浪费 token
## 7. 内部循环 (Internal Loop)
Strategist 的内部循环非常简洁,通常 **1 轮**即可完成:
```
┌─────────────────────────────────────────────────┐
│ Strategist Internal Loop (max 2 turns) │
│ │
│ Turn 0: 分析输入 │
│ ├── 如果 Coordinator 传入的信息足够 │
│ │ └── 直接生成策略建议(大多数情况) │
│ │ │
│ ├── 如果需要更多用户历史 │
│ │ └── get_user_context() 获取用户记忆 │
│ │ │
│ Turn 1: 综合分析 │
│ ├── 结合用户记忆 + 对话摘要 │
│ └── 生成策略建议 │
└─────────────────────────────────────────────────┘
```
> Strategist 的设计偏向**轻量级**:快速分析、快速返回。它不需要搜索知识库,不需要多轮推理。
## 8. 与其他 Agent 的关系
```
┌──────────────┐ invoke_strategist ┌──────────────┐
│ │ ──────────────────────────────────→ │ │
│ Coordinator │ StrategistOutput │ Strategist │
│ │ ←────────────────────────────────── │ │
└──────┬───────┘ └──────┬───────┘
│ │
│ Coordinator 根据策略建议决定: │ get_user_context
│ ↓
│ ┌────────────────────────────────────┐ ┌──────────────┐
│ │ 如果 recommended = 'address_objection' │ │ Knowledge │
│ │ → invoke_objection_handler │ │ Service │
│ │ │ │ (Memory) │
│ │ 如果 recommended = 'provide_assessment' │ └──────────────┘
│ │ → invoke_assessment_expert │
│ │ │
│ │ 如果 recommended = 'provide_policy_info'│
│ │ → invoke_policy_expert │
│ │ │
│ │ 如果 recommended = 'offer_paid_service' │
│ │ → generate_payment │
│ └────────────────────────────────────┘
│ Strategist 输出 → Coordinator 的下一次决策参考
│ (不缓存,每次调用都是独立判断)
```
**与旧架构的对应关系**
| 旧架构 | 新架构 |
|--------|--------|
| `StrategyEngineService.evaluateTransition()` | Strategist 的 `stage_assessment` |
| `StrategyEngineService.buildStageGuidance()` | Strategist 的 `strategy_advice` |
| `DEFAULT_CONSULTING_STRATEGY` 中的 8 阶段状态机 | Strategist LLM 自主判断阶段 |
| 程序化的 `detectUserIntents()` | Strategist 的 `sentiment_analysis` + `conversion_signals` |
**核心区别**:旧架构用规则引擎判断阶段转移,新架构用 LLM 做整体性判断,更灵活、更能理解语境。
## 9. 示例场景
### 场景 1信息收集中期 -- 用户开始犹豫
**Coordinator 调用**
```json
{
"tool": "invoke_strategist",
"input": {
"conversationSummary": "用户是一位35岁的金融从业者已告知学历为本科非百强大学年收入约60万人民币工作经验10年。上一轮用户主动分享了这些信息。这一轮用户说'好的,其实我也不确定自己到底适不适合移民'",
"currentStage": "INFO_COLLECTION",
"latestUserMessage": "好的,其实我也不确定自己到底适不适合移民",
"collectedInfo": {
"age": 35,
"education": "本科",
"annualIncome": 600000,
"totalYearsOfExperience": 10,
"currentIndustry": "金融"
},
"turnCount": 6
}
}
```
**返回结果**
```json
{
"strategy_advice": {
"recommended_next_action": "build_rapport",
"action_detail": "用户开始出现犹豫情绪,此时不宜继续追问信息。建议先肯定用户已经分享的信息,给出一些积极反馈(如年龄和工作经验在优才计划中是加分项),让用户感受到希望。然后自然过渡到给一个初步评估,让用户看到具体可能性。",
"missing_info_priorities": [
{
"field": "university",
"priority": "P1",
"suggested_question": "暂不追问,等用户情绪稳定后再自然引出"
},
{
"field": "languageSkills",
"priority": "P2",
"suggested_question": "可在评估时顺带确认"
}
]
},
"conversion_signals": {
"signal_strength": "weak",
"signals_detected": ["主动分享了较多个人信息", "但出现了犹豫表达"],
"conversion_readiness": 35
},
"sentiment_analysis": {
"current_mood": "hesitant",
"mood_trend": "declining",
"key_concerns": ["对自身条件缺乏信心", "移民决策本身的不确定性"]
},
"tone_adjustment": {
"suggested_tone": "encouraging",
"response_length": "medium",
"should_use_emoji": false,
"special_notes": "先共情'确实移民是个大决定',然后用已有信息给出一些积极评价,避免继续追问"
},
"stage_assessment": {
"current_stage": "INFO_COLLECTION",
"should_transition": false,
"suggested_next_stage": null,
"transition_reason": "虽然信息基本够做初步评估,但用户情绪需要先稳定。建议在下一轮给出鼓励性的初步反馈后,再过渡到评估阶段。"
}
}
```
### 场景 2用户释放强转化信号
**Coordinator 调用**
```json
{
"tool": "invoke_strategist",
"input": {
"conversationSummary": "用户32岁清华大学硕士8年IT经验。已完成评估TTPS B类高度推荐88分。上一轮展示了评估结果用户回复'这个评估很详细!那如果我想正式申请,下一步应该怎么做?'",
"currentStage": "ASSESSMENT",
"latestUserMessage": "这个评估很详细!那如果我想正式申请,下一步应该怎么做?",
"collectedInfo": {
"age": 32,
"education": "硕士",
"university": "清华大学",
"totalYearsOfExperience": 8,
"currentIndustry": "IT/科技"
},
"turnCount": 8
}
}
```
**返回结果**
```json
{
"strategy_advice": {
"recommended_next_action": "offer_paid_service",
"action_detail": "用户主动询问下一步这是非常强的转化信号。建议1先肯定用户的条件确实很好TTPS B类高匹配增强信心2说明免费咨询只能给到初步方向付费详细评估可以提供完整的材料清单、时间规划、风险评估3自然引出付费评估服务¥99强调性价比。不要硬推用'如果您想更详细了解'的方式。",
"missing_info_priorities": []
},
"conversion_signals": {
"signal_strength": "strong",
"signals_detected": [
"主动询问下一步行动",
"对评估结果表达积极认可",
"使用了感叹号表达兴奋"
],
"conversion_readiness": 85
},
"sentiment_analysis": {
"current_mood": "enthusiastic",
"mood_trend": "improving",
"key_concerns": []
},
"tone_adjustment": {
"suggested_tone": "professional",
"response_length": "medium",
"should_use_emoji": false,
"special_notes": "保持专业,不要因为用户热情就过度推销。提供清晰的选项:付费评估 or 预约专家。让用户自己选择。"
},
"stage_assessment": {
"current_stage": "ASSESSMENT",
"should_transition": true,
"suggested_next_stage": "CONVERSION",
"transition_reason": "用户已认可评估结果并主动询问下一步,时机成熟,进入转化阶段。"
}
}
```

View File

@ -0,0 +1,446 @@
# 05 - Objection Handler Agent (异议处理专家) 设计详解
## 1. 核心职责
Objection Handler 是系统中的**异议化解专家**。当用户表达顾虑、犹豫或反对意见时Coordinator 调用此 Agent 获取专业的回应方案。
核心能力:
1. **异议分类** -- 识别异议类型(费用/时间/成功率/家庭/政治/信任等)
2. **共情回应** -- 先理解和认同用户的感受,建立信任
3. **事实反驳** -- 用知识库中的真实数据和政策依据消除误解
4. **案例引用** -- 援引类似背景的成功案例增强说服力
5. **替代方案** -- 当无法直接消除顾虑时,提供绕行方案
6. **话术建议** -- 为 Coordinator 提供具体的回应话术参考
> 设计原则:**先共情、再讲理、后给方案。** 永远不要否定用户的感受,即使他们的担心在事实上是错误的。
## 2. 模型与参数
```typescript
{
model: 'claude-sonnet-4-20250514', // 需要共情能力 + 逻辑推理
max_tokens: 2000,
temperature: 0.2, // 允许少量灵活性(话术需要自然)
system: [
{
type: 'text',
text: objectionHandlerPrompt,
cache_control: { type: 'ephemeral' }
}
],
}
```
选用 Sonnet 的理由:
- 异议处理需要高情商(理解用户潜台词)
- 需要平衡共情与说服,这对模型能力要求较高
- 回应中需要准确引用政策数据,不能出错
## 3. 可用工具 (Available Tools)
Objection Handler 有 **2 个工具**
### 3.1 search_knowledge
```typescript
{
name: 'search_knowledge',
description: '搜索知识库获取政策事实、成功率数据、费用标准、审批时间等信息,用于以事实回应用户异议。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询,如"高才通审批成功率数据"或"移民后就业情况"'
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '相关移民类别'
}
},
required: ['query']
}
}
```
### 3.2 get_user_context
```typescript
{
name: 'get_user_context',
description: '获取用户的历史背景和之前的顾虑记录,避免重复回应同一异议,并根据用户具体情况定制回应。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '检索用户记忆,如"用户之前表达过的顾虑和担忧"'
}
},
required: ['query']
}
}
```
## 4. System Prompt 要点
```
# 身份
你是 iConsulting 的异议处理专家。你擅长理解用户的顾虑,并用专业、温暖的方式化解。
# 回应三步法(必须遵守)
1. 共情 (Empathize): 承认用户的感受是合理的,用"确实""您的担心很正常"等语言
2. 事实 (Educate): 用知识库中的数据和政策纠正误解
3. 方案 (Empower): 给出具体的解决方案或替代路径
# 常见异议分类与策略
## COST费用顾虑
- "太贵了" / "移民中介费好高"
- 策略区分付费评估¥99vs 正式申请费用 vs 中介费;强调自助申请的可能性;对比移民后的收入提升
## TIMELINE时间顾虑
- "要等好久" / "时间太长了"
- 策略按类别给出真实审批时间TTPS 4-6周最快对比其他国家移民周期强调可以边等边准备
## SUCCESS_RATE成功率顾虑
- "能成功吗" / "通过率高吗"
- 策略引用知识库中的通过率数据分析用户条件匹配度避免承诺100%成功
## FAMILY家庭顾虑
- "家人不支持" / "孩子教育怎么办"
- 策略:提供香港教育体系信息;配偶随行政策;强调不需要立即定居
## POLITICAL政治顾虑
- "香港现在还好吗" / "安全吗"
- 策略:保持中立客观;引导关注经济和教育优势;强调"多一个选择"而非"逃离"
## TRUST信任顾虑
- "AI能给准确建议吗" / "你们靠谱吗"
- 策略强调AI只做初筛正式服务由持牌顾问提供建议验证信息来源官方网站
## READINESS准备度顾虑
- "我条件不够好" / "再等等吧"
- 策略:指出已有优势;强调"越早准备越好";政策有变化风险
# 红线
- 永远不要承诺100%成功率
- 不做任何政治立场判断
- 不贬低其他移民目的地
- 不催促用户做决定
```
## 5. 输入/输出格式
### 输入 (Coordinator 传入)
```typescript
interface ObjectionHandlerInput {
/** 用户的异议/顾虑原文 */
objection: string;
/** 用户上下文摘要Coordinator 提供的背景信息) */
userContext: string;
/** 用户关注的移民类别(如果已知) */
category?: 'QMAS' | 'GEP' | 'IANG' | 'TTPS' | 'CIES' | 'TECHTAS';
}
```
### 输出 (返回给 Coordinator)
```typescript
interface ObjectionHandlerOutput {
/** 异议分类 */
objection_category: 'COST' | 'TIMELINE' | 'SUCCESS_RATE' | 'FAMILY' | 'POLITICAL' | 'TRUST' | 'READINESS' | 'OTHER';
/** 异议严重程度 */
severity: 'low' | 'medium' | 'high';
/** 共情回应Coordinator 可直接使用或改写) */
empathy_response: string;
/** 事实反驳要点 */
factual_rebuttal: {
key_points: string[]; // 核心反驳论点
data_references: string[]; // 引用的数据/政策
sources: Array<{
title: string;
article_id?: string;
}>;
};
/** 成功案例引用(如果知识库有相关案例) */
success_story_reference?: {
scenario: string; // 案例背景简述
relevance: string; // 与当前用户的关联点
};
/** 建议的完整回应话术(供 Coordinator 参考) */
suggested_response: string;
/** 建议的后续动作 */
follow_up_action: {
action: 'continue_discussion' | 'provide_more_data' | 'offer_alternative' | 'give_space' | 'connect_expert';
detail: string;
};
}
```
## 6. 触发时机 (When to Trigger)
Coordinator 在以下场景调用 `invoke_objection_handler`
| 场景 | 用户信号 | Coordinator 判断方式 |
|------|----------|---------------------|
| 直接表达顾虑 | "太贵了" / "不确定" / "有点担心" | LLM 识别负面情绪+具体顾虑主题 |
| 犹豫不前 | "让我想想" / "再考虑一下" | LLM 识别拖延信号 |
| 质疑可行性 | "我条件不太好吧" / "能通过吗" | LLM 识别自我怀疑 |
| 对比竞品 | "其他中介说..." / "网上说..." | LLM 识别外部信息干扰 |
| 家庭反对 | "老婆/老公不同意" | LLM 识别第三方阻力 |
| 政治敏感 | "香港还安全吗" | LLM 识别政治顾虑 |
**通常与 Strategist 配合使用**
1. Strategist 检测到 `sentiment: hesitant` + `recommended: address_objection`
2. Coordinator 据此调用 Objection Handler
3. Coordinator 综合两者的建议生成最终回复
## 7. 内部循环 (Internal Loop)
Objection Handler 的 agent loop 最多 **2 轮** tool 调用:
```
┌─────────────────────────────────────────────────────┐
│ Objection Handler Internal Loop (max 2 turns) │
│ │
│ Turn 0: 分析异议 + 获取背景 │
│ ├── 同时发起两个 tool 调用(并行): │
│ │ ├── search_knowledge({query: 相关事实查询}) │
│ │ └── get_user_context({query: 用户顾虑历史}) │
│ │ │
│ Turn 1: 综合生成回应方案 │
│ ├── 如果 Turn 0 的搜索结果不够 │
│ │ └── search_knowledge({query: 补充查询}) │
│ │ │
│ └── 生成结构化输出 │
│ ├── 分类异议 │
│ ├── 构建共情 + 事实 + 方案 三段式回应 │
│ └── 返回给 Coordinator │
└─────────────────────────────────────────────────────┘
```
**并行调用优化**
```typescript
// Objection Handler 可以在第一轮同时调用两个工具
const toolCalls = [
{
name: 'search_knowledge',
input: { query: '高才通审批成功率和时间线', category: 'TTPS' }
},
{
name: 'get_user_context',
input: { query: '用户之前表达过的顾虑' }
}
];
// 两个工具并行执行,减少延迟
```
## 8. 与其他 Agent 的关系
```
┌──────────────┐
│ Strategist │
│ (检测异议 │
│ 信号) │
└──────┬───────┘
│ sentiment: hesitant
│ recommended: address_objection
┌──────────────┐ invoke_objection_handler ┌──────────────┐
│ │ ────────────────────────→ │ Objection │
│ Coordinator │ ObjectionHandlerOutput │ Handler │
│ │ ←──────────────────────── │ │
└──────┬───────┘ └──────┬───────┘
│ │
│ Coordinator 综合回应: ├── search_knowledge
│ 1. 共情语句(来自 Objection Handler ├── get_user_context
│ 2. 事实数据(来自 Objection Handler
│ 3. 对话策略(来自 Strategist
│ ┌──────────────┐
│ │ Knowledge │
├── 可能追加调用 Case Analyst │ Service │
│ (用成功案例强化说服) └──────────────┘
└── 可能追加调用 Policy Expert
(用官方政策回应质疑)
```
**协作链路示例**
1. 用户说:"高才通通过率高吗?感觉不太靠谱"
2. Coordinator 调用 Strategist → 检测到 `SUCCESS_RATE` 异议 + `hesitant` 情绪
3. Coordinator 同时调用 Objection Handler → 获取共情+数据回应
4. Coordinator 可能追加调用 Case Analyst → 获取类似背景的成功案例
5. Coordinator 综合三者结果,生成最终回复
## 9. 示例场景
### 场景 1费用顾虑
**Coordinator 调用**
```json
{
"tool": "invoke_objection_handler",
"input": {
"objection": "你们的评估服务要99块网上免费的评估工具一大把为什么要花这个钱",
"userContext": "用户35岁本科金融行业年薪60万。已初步了解优才计划。对移民有兴趣但比较谨慎。",
"category": "QMAS"
}
}
```
**返回结果**
```json
{
"objection_category": "COST",
"severity": "medium",
"empathy_response": "您说得对,网上确实有不少免费的评估工具。花钱之前多做比较是非常明智的。",
"factual_rebuttal": {
"key_points": [
"免费工具通常只做简单条件匹配如年龄、学历无法综合评估12项优才计划评分维度",
"我们的付费评估由AI深度分析+真人顾问审核双重把关覆盖全部6个移民类别",
"评估报告包含详细的材料准备清单、时间规划和风险评估,相当于一份完整的申请预案",
"¥99相比正式申请的中介费通常2-5万元只是零头但能帮您判断是否值得投入"
],
"data_references": [
"优才计划评分标准包含12项维度满足6项为门槛",
"正式申请中介费用通常为20,000-50,000人民币"
],
"sources": [
{
"title": "优才计划申请费用与流程指南",
"article_id": "qmas-fee-guide-001"
}
]
},
"success_story_reference": {
"scenario": "有位类似背景的金融从业者本科、30+岁在做完详细评估后发现自己更适合高才通B类而非最初考虑的优才计划节省了大量准备时间和费用。",
"relevance": "用户也是金融行业、本科学历,可能存在类似的类别误判风险"
},
"suggested_response": "您说得对,网上确实有不少免费的评估工具,花钱之前多做比较是非常明智的。\n\n不过实话说免费工具大多只看年龄和学历这两项很难覆盖优才计划12项评分标准的综合评估。我们的详细评估会同时分析全部6个移民类别的匹配度帮您找到最优路径。\n\n之前就有一位和您背景类似的金融从业者原本打算走优才评估后发现更适合高才通B类审批速度快了几个月。\n\n¥99 对于一个可能影响几年规划的决策来说,性价比还是很高的。当然,如果您还想多了解一些再做决定,我随时在这里为您解答。",
"follow_up_action": {
"action": "continue_discussion",
"detail": "不要强推付费先回答完这个顾虑。如果用户继续聊可在2-3轮后再自然提起。"
}
}
```
### 场景 2成功率怀疑
**Coordinator 调用**
```json
{
"tool": "invoke_objection_handler",
"input": {
"objection": "我朋友申请优才被拒了,现在通过率好像很低?",
"userContext": "用户28岁硕士非百强5年工作经验IT行业。已完成初步评估TTPS不符合非百强大学优才中等推荐。",
"category": "QMAS"
}
}
```
**返回结果**
```json
{
"objection_category": "SUCCESS_RATE",
"severity": "high",
"empathy_response": "听到朋友的经历确实会让人担心。每个人的情况不同,被拒的原因也各不相同。",
"factual_rebuttal": {
"key_points": [
"优才计划采取择优录取机制,通过率取决于申请人的综合竞争力,并非随机审批",
"影响通过率的核心因素:行业是否属于人才清单、材料准备的完整度和说服力、申请时机",
"您朋友被拒可能有具体原因(材料不完整、分数不够、不在热门行业等),不代表所有人都会被拒",
"您28岁+硕士+5年IT经验年龄和行业科技领域都是加分项"
],
"data_references": [
"IT/科技行业属于香港重点发展的14个科技领域之一",
"优才计划已取消年度配额限制,全年接受申请"
],
"sources": [
{
"title": "优才计划最新申请要求与审批标准",
"article_id": "qmas-criteria-001"
},
{
"title": "香港人才清单行业分类",
"article_id": "talent-list-001"
}
]
},
"success_story_reference": {
"scenario": "一位29岁、普通本科毕业的数据分析师凭借在AI领域的项目经验和行业论文成功通过优才计划。关键在于材料中充分展示了行业贡献。",
"relevance": "用户同样来自IT领域且学历更高硕士如果能充分展示专业能力通过可能性不低"
},
"suggested_response": "听到朋友的经历确实会让人担心,这种心情完全可以理解。\n\n不过每个申请人的情况差异很大您朋友被拒可能有材料、时机或分数等方面的具体原因。优才是择优录取关键在于怎么呈现自己的竞争力。\n\n从您的条件来看28岁年龄加分很高硕士学历是加分项IT行业属于香港重点发展领域。如果能在申请材料中突出您在科技领域的项目经验和成果竞争力是不弱的。\n\n要不要我帮您更详细地分析一下各项评分情况这样您对自己的实际竞争力会有更清晰的判断。",
"follow_up_action": {
"action": "provide_more_data",
"detail": "建议接下来调用 Assessment Expert 做一次完整评估,用具体分数让用户看到自己的竞争力。数据比口头安慰更有说服力。"
}
}
```
### 场景 3家庭顾虑
**Coordinator 调用**
```json
{
"tool": "invoke_objection_handler",
"input": {
"objection": "我倒是想去,但我老婆觉得孩子在内地读书挺好的,不想折腾",
"userContext": "用户38岁已婚有孩子年龄未知TTPS B类高度推荐。之前对话很积极。",
"category": "TTPS"
}
}
```
**返回结果**
```json
{
"objection_category": "FAMILY",
"severity": "high",
"empathy_response": "家人的意见确实很重要,毕竟移民是全家的大事。您太太关心孩子的教育稳定性,这完全可以理解。",
"factual_rebuttal": {
"key_points": [
"拿到香港身份并不意味着必须立即搬过去,很多家庭选择'先拿身份、后续安排'的方式",
"高才通获批后,孩子可以享受香港教育资源(包括国际学校、双语教育),但不强制转学",
"持港身份的孩子在内地参加联考DSE/港澳台联考)有更多升学选择",
"可以维持'一家两地'模式:一方在港工作保持身份续签,家人不一定要搬迁"
],
"data_references": [
"持香港居民身份的学生可参加港澳台联招考试",
"高才通签证持有人的受养人可申请随行签证",
"香港7年居住可申请永居但不要求连续居住"
],
"sources": [
{
"title": "香港身份与子女教育规划指南",
"article_id": "family-education-001"
}
]
},
"success_story_reference": {
"scenario": "一位类似情况的家庭,先生先通过高才通拿到香港身份,太太和孩子继续在内地。两年后孩子面临小升初时,全家利用港籍身份获得了国际学校的入学机会。",
"relevance": "用户也是已婚有孩子,可以用灵活的方式先拿身份、不急于搬迁"
},
"suggested_response": "家人的意见确实很重要,移民毕竟是全家的大事。您太太关心孩子的教育稳定性,这是非常负责任的想法。\n\n其实拿到香港身份并不意味着必须马上搬过去。很多家庭的做法是'先拿身份、灵活安排'——孩子可以继续在内地读书,等到合适的时机(比如升学节点)再考虑是否过去。\n\n而且港籍孩子还多了一条路可以参加港澳台联考升学选择更多。相当于给孩子多了一个选择而不是替他们做决定。\n\n要不和太太分享一下这些信息有时候换个角度来看'多一个身份'和'一定要搬家'是完全不同的概念。",
"follow_up_action": {
"action": "give_space",
"detail": "家庭异议需要给用户时间与家人商量。不要催促决定,可以提供一些教育方面的资料链接让用户带回去与太太讨论。"
}
}
```

View File

@ -0,0 +1,514 @@
# 06 - Case Analyst Agent (案例分析师) 设计详解
## 1. 核心职责
Case Analyst 是系统中的**案例匹配引擎**。当 Coordinator 需要用真实案例来增强说服力时(评估后展示、异议处理、犹豫用户激励),调用此 Agent 从知识库中查找并呈现最相关的成功案例。
核心能力:
1. **案例检索** -- 从知识库中搜索与用户画像最匹配的成功案例
2. **相似度评估** -- 计算用户条件与案例主角的匹配程度
3. **关键因素提取** -- 分析案例成功的关键因素,映射到当前用户的优劣势
4. **时间线参考** -- 提供案例从准备到获批的时间线供用户参考
5. **差异化呈现** -- 在多个案例之间做差异化展示,覆盖不同角度
> 设计原则:**案例必须来自知识库,不可捏造。** 如果没有匹配的案例,诚实说明而不是编故事。
## 2. 模型与参数
```typescript
{
model: 'claude-haiku-3-5-20241022', // 主要是搜索+格式化,不需要 Sonnet
max_tokens: 1500,
temperature: 0, // 案例呈现需要准确性
system: [
{
type: 'text',
text: caseAnalystPrompt,
cache_control: { type: 'ephemeral' }
}
],
}
```
选用 Haiku 的理由:
- 核心工作是搜索知识库 + 结构化整理,不涉及复杂推理
- 需要快速返回结果(用户在等待中)
- 成本敏感 -- 案例查找可能频繁触发Haiku 成本仅为 Sonnet 的 1/10
- 输入/输出格式固定Haiku 足以胜任
## 3. 可用工具 (Available Tools)
Case Analyst 有 **2 个工具**
### 3.1 search_knowledge
```typescript
{
name: 'search_knowledge',
description: '搜索知识库中的成功案例、客户故事、申请经验。搜索时应包含用户的核心特征(行业、学历、年龄段)作为关键词。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '案例搜索查询,如"IT行业硕士30岁高才通成功案例"'
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别,用于缩小案例搜索范围'
}
},
required: ['query']
}
}
```
### 3.2 get_user_context
```typescript
{
name: 'get_user_context',
description: '获取用户的详细背景信息,用于更精准地匹配案例。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '检索用户背景,如"用户的学历工作行业信息"'
}
},
required: ['query']
}
}
```
## 4. System Prompt 要点
```
# 身份
你是 iConsulting 的案例分析师。你的任务是从知识库中查找与当前用户最匹配的成功案例。
# 核心原则
1. 所有案例必须来自知识库搜索结果,绝不能编造
2. 如果没有高度匹配的案例,诚实说明并提供最接近的参考
3. 案例呈现要突出与用户的相似点,让用户产生共鸣
4. 同时指出案例与用户的差异,保持客观
5. 优先展示最近的案例(时效性更强)
# 搜索策略
- 第一轮搜索:用用户的核心特征组合(行业+学历+类别)
- 如果结果不足:放宽条件,搜索同行业或同类别的案例
- 最多搜索 2 轮
# 相似度评分标准0-100
- 同行业 +25
- 同学历层次 +20
- 年龄差在5岁内 +15
- 同移民类别 +20
- 同地区/城市 +10
- 工作年限差在3年内 +10
# 输出格式
必须返回 JSON包含 matched_cases[]、similarity_analysis、timeline_reference
```
## 5. 输入/输出格式
### 输入 (Coordinator 传入)
```typescript
interface CaseAnalystInput {
/** 用户的核心画像 */
userProfile: {
age?: number;
education?: string;
university?: string;
industry?: string;
yearsOfExperience?: number;
annualIncome?: number;
targetCategory?: string;
};
/** 目标移民类别 */
targetCategory: 'QMAS' | 'GEP' | 'IANG' | 'TTPS' | 'CIES' | 'TECHTAS';
/** 案例搜索的侧重点(可选) */
focus?: 'success_story' | 'timeline' | 'similar_background' | 'overcoming_weakness';
}
```
### 输出 (返回给 Coordinator)
```typescript
interface CaseAnalystOutput {
/** 匹配到的案例列表(按相似度排序) */
matched_cases: Array<{
/** 案例标题/代号 */
case_title: string;
/** 案例主角的背景摘要 */
profile_summary: string;
/** 申请的移民类别 */
category: string;
/** 与当前用户的相似度0-100 */
similarity_score: number;
/** 相似点列表 */
similarities: string[];
/** 差异点列表 */
differences: string[];
/** 成功的关键因素 */
key_success_factors: string[];
/** 案例时间线 */
timeline?: {
preparation: string; // 准备期
application: string; // 申请期
approval: string; // 审批期
total: string; // 总耗时
};
/** 来源引用 */
source?: {
title: string;
article_id?: string;
};
}>;
/** 综合相似度分析 */
similarity_analysis: {
best_match_score: number; // 最高匹配分数
average_score: number; // 平均匹配分数
match_quality: 'high' | 'medium' | 'low' | 'none';
summary: string; // 一句话总结匹配情况
};
/** 时间线参考(综合多个案例) */
timeline_reference: {
estimated_preparation: string; // 预计准备时间
estimated_processing: string; // 预计审批时间
estimated_total: string; // 预计总时间
factors_affecting_timeline: string[]; // 影响时间的因素
};
/** 如果无匹配案例 */
no_match_reason?: string;
}
```
## 6. 触发时机 (When to Trigger)
Coordinator 在以下场景调用 `invoke_case_analyst`
| 场景 | 目的 | 触发条件 |
|------|------|----------|
| 评估结果呈现后 | 用案例增强推荐说服力 | 评估完成,需要展示同类成功案例 |
| 用户犹豫不决 | 用案例激励用户 | Strategist 建议 build_rapport |
| 异议处理中 | 用案例回应"能成功吗"的顾虑 | Objection Handler 建议 provide_more_data |
| 用户主动要求 | "有没有类似的案例?" | 用户直接询问案例 |
| 时间线问题 | 用案例提供时间参考 | 用户问"要多久" |
**不应触发的场景**
- 对话初期,用户信息不足(无法做有效匹配)
- 用户在问纯政策问题(调 Policy Expert
- 已经展示过案例且用户没有追问
## 7. 内部循环 (Internal Loop)
Case Analyst 的 agent loop 最多 **2 轮** tool 调用,设计为**快进快出**
```
┌─────────────────────────────────────────────────────┐
│ Case Analyst Internal Loop (max 2 turns) │
│ │
│ Turn 0: 主搜索 │
│ ├── 构建搜索查询: │
│ │ "{industry} {education} {age}岁 │
│ │ {targetCategory} 成功案例" │
│ ├── search_knowledge({query, category}) │
│ │ │
│ ├── 结果分析: │
│ │ ├── 找到 2+ 相关案例 → 直接生成输出,结束 │
│ │ ├── 找到 0-1 条 → 放宽条件进入 Turn 1 │
│ │ └── 找到 0 条 → 返回 no_match_reason │
│ │ │
│ Turn 1: 补充搜索(放宽条件) │
│ ├── 放宽策略: │
│ │ ├── 去掉年龄限制 │
│ │ ├── 只保留行业 + 类别 │
│ │ └── 或搜索同类别的通用成功案例 │
│ ├── search_knowledge({query: 放宽后的查询}) │
│ │ │
│ └── 综合两轮结果生成输出 │
└─────────────────────────────────────────────────────┘
```
**搜索查询构建逻辑**
```typescript
function buildCaseSearchQuery(input: CaseAnalystInput): string {
const parts: string[] = [];
// 核心维度
if (input.userProfile.industry) parts.push(input.userProfile.industry);
if (input.userProfile.education) parts.push(input.userProfile.education);
if (input.userProfile.age) {
const ageRange = input.userProfile.age < 30 ? '20多岁' :
input.userProfile.age < 40 ? '30多岁' : '40多岁';
parts.push(ageRange);
}
// 类别
const categoryNames: Record<string, string> = {
QMAS: '优才计划', GEP: '专才计划', IANG: 'IANG',
TTPS: '高才通', CIES: '投资移民', TECHTAS: '科技人才'
};
parts.push(categoryNames[input.targetCategory] || input.targetCategory);
parts.push('成功案例');
return parts.join(' ');
}
```
## 8. 与其他 Agent 的关系
```
┌──────────────┐ invoke_case_analyst ┌──────────────┐
│ │ ─────────────────────────────→ │ │
│ Coordinator │ CaseAnalystOutput │ Case │
│ │ ←───────────────────────────── │ Analyst │
└──────┬───────┘ └──────┬───────┘
│ │
│ 常见协作模式: ├── search_knowledge
│ └── get_user_context
│ 模式 A评估 + 案例(增强说服)
│ ├── invoke_assessment_expert (并行)
│ └── invoke_case_analyst (并行)
│ → Coordinator 综合评估结果和案例一起呈现
│ 模式 B异议 + 案例(回应质疑)
│ ├── invoke_objection_handler
│ └── invoke_case_analyst
│ → Coordinator 用案例数据支撑异议回应
│ 模式 C单独使用用户主动问案例
│ └── invoke_case_analyst
│ → Coordinator 直接转述案例
```
**并行调用优化**
Case Analyst 和 Assessment Expert 可以**并行调用**,因为两者互不依赖:
```typescript
// Coordinator 同时发起两个 Agent 调用
const [assessmentResult, caseResult] = await Promise.all([
invokeAssessmentExpert({ userInfo }),
invokeCaseAnalyst({ userProfile, targetCategory: 'TTPS' }),
]);
// 综合两者结果生成回复
```
## 9. 示例场景
### 场景 1IT 从业者查找高才通案例
**Coordinator 调用**
```json
{
"tool": "invoke_case_analyst",
"input": {
"userProfile": {
"age": 32,
"education": "硕士",
"university": "浙江大学",
"industry": "IT/科技",
"yearsOfExperience": 8,
"targetCategory": "TTPS"
},
"targetCategory": "TTPS",
"focus": "success_story"
}
}
```
**内部执行**
```
Turn 0: search_knowledge({
query: "IT科技 硕士 30多岁 高才通 成功案例",
category: "TTPS"
})
→ 找到 2 条相关案例
→ 信息充足,直接生成输出
```
**返回结果**
```json
{
"matched_cases": [
{
"case_title": "资深前端工程师 高才通B类获批",
"profile_summary": "33岁华中科技大学硕士前端开发9年经验曾就职于腾讯、字节跳动",
"category": "TTPS B类",
"similarity_score": 82,
"similarities": [
"同为IT/科技行业",
"硕士学历",
"年龄接近33 vs 32",
"工作经验相近9年 vs 8年",
"均为百强大学毕业"
],
"differences": [
"案例为华中科技大学(用户为浙江大学)",
"案例有大厂经历(腾讯、字节)"
],
"key_success_factors": [
"百强大学硕士满足B类学历要求",
"9年经验远超B类最低3年要求",
"IT行业为香港重点发展领域",
"准备了详细的项目经验证明"
],
"timeline": {
"preparation": "约2周材料准备",
"application": "1天在线提交",
"approval": "5周",
"total": "约7周"
},
"source": {
"title": "IT行业高才通成功案例集",
"article_id": "case-ttps-it-001"
}
},
{
"case_title": "AI算法工程师 高才通B类获批",
"profile_summary": "29岁北京大学学士+硕士AI/机器学习方向5年经验年薪50万",
"category": "TTPS B类",
"similarity_score": 68,
"similarities": [
"IT/科技行业",
"百强大学",
"高才通B类路径"
],
"differences": [
"年龄较轻29 vs 32",
"工作经验较少5年 vs 8年",
"北大 vs 浙大"
],
"key_success_factors": [
"AI领域高度匹配香港科技发展战略",
"虽然经验年限不长但项目含金量高",
"准备了发表论文和专利清单"
],
"timeline": {
"preparation": "约1周",
"application": "1天",
"approval": "4周",
"total": "约5周"
},
"source": {
"title": "AI领域人才高才通案例",
"article_id": "case-ttps-ai-001"
}
}
],
"similarity_analysis": {
"best_match_score": 82,
"average_score": 75,
"match_quality": "high",
"summary": "找到2个高度匹配的IT行业高才通B类成功案例您的条件浙大硕士+8年经验与案例主角非常接近匹配度较高。"
},
"timeline_reference": {
"estimated_preparation": "1-2周",
"estimated_processing": "4-6周",
"estimated_total": "5-8周",
"factors_affecting_timeline": [
"材料准备的完整度直接影响审批速度",
"申请高峰期可能延长1-2周",
"百强大学认证通常不需要额外时间"
]
}
}
```
### 场景 2无高匹配案例时的处理
**Coordinator 调用**
```json
{
"tool": "invoke_case_analyst",
"input": {
"userProfile": {
"age": 45,
"education": "大专",
"industry": "传统制造业",
"yearsOfExperience": 20
},
"targetCategory": "QMAS",
"focus": "overcoming_weakness"
}
}
```
**内部执行**
```
Turn 0: search_knowledge({query: "传统制造业 大专 40多岁 优才计划 成功案例", category: "QMAS"})
→ 0 条直接匹配
Turn 1: search_knowledge({query: "大专学历 优才计划 成功案例 经验丰富"})
→ 1 条弱相关案例
→ 如实返回低匹配结果
```
**返回结果**
```json
{
"matched_cases": [
{
"case_title": "制造业高管 优才计划获批",
"profile_summary": "42岁本科学历制造业20年经验担任工厂副总经理年薪80万",
"category": "QMAS",
"similarity_score": 45,
"similarities": [
"同为制造业从业者",
"工作年限相近20年",
"年龄接近42 vs 45"
],
"differences": [
"案例为本科(用户为大专)",
"案例有高管头衔",
"案例年薪较高"
],
"key_success_factors": [
"20年行业深耕强调管理经验和行业贡献",
"持有多项行业专利和资质证书",
"在申请材料中突出了企业经营业绩"
],
"timeline": {
"preparation": "约4周需额外准备补充材料",
"application": "1天",
"approval": "约10周",
"total": "约15周"
},
"source": {
"title": "制造业人才优才申请经验分享",
"article_id": "case-qmas-mfg-001"
}
}
],
"similarity_analysis": {
"best_match_score": 45,
"average_score": 45,
"match_quality": "low",
"summary": "知识库中与您条件高度匹配的案例较少。找到1个同行业案例作为参考但该案例的学历和职位层级与您有差异。建议通过详细评估确认最优路径。"
},
"timeline_reference": {
"estimated_preparation": "4-6周可能需要额外补充材料",
"estimated_processing": "9-12周",
"estimated_total": "3-4个月",
"factors_affecting_timeline": [
"大专学历可能需要额外的经验证明材料",
"需要突出工作成果和行业贡献弥补学历短板",
"建议准备推荐信增加申请说服力"
]
}
}
```

View File

@ -0,0 +1,617 @@
# 07 - Memory Manager Agent (记忆管理) 设计详解
## 1. 核心职责
Memory Manager 是系统中的**用户信息管家**。它负责从对话中提取用户信息、组织存储到长期记忆、以及按需检索用户历史上下文。它替代了旧架构中 `StrategyEngineService.extractUserInfo()` 的手动提取逻辑。
核心能力:
1. **信息提取** -- 从对话文本中自动识别和提取用户的个人信息(年龄、学历、工作等)
2. **记忆存储** -- 将提取的信息分类标注后保存到 knowledge-service 的长期记忆
3. **上下文加载** -- 检索用户的历史记忆,为其他 Agent 提供用户背景
4. **摘要生成** -- 将散落在多次对话中的用户信息整合为结构化的用户画像
5. **去重合并** -- 避免重复存储相同信息,新信息覆盖旧信息
> 设计原则:**只提取用户明确表述的事实,不做推测。** "我大概三十多岁"提取为"30+"而非具体数字。
## 2. 模型与参数
```typescript
{
model: 'claude-haiku-3-5-20241022', // 结构化提取任务Haiku 足矣
max_tokens: 1000,
temperature: 0, // 信息提取必须确定性
system: [
{
type: 'text',
text: memoryManagerPrompt,
cache_control: { type: 'ephemeral' }
}
],
}
```
选用 Haiku 的理由:
- 信息提取是相对简单的 NLP 任务(实体识别 + 分类)
- 需要高频调用(每轮对话后都可能触发),成本敏感
- 输入输出格式高度结构化,不需要 Sonnet 的推理能力
- 速度要求高 -- 信息保存不应阻塞主对话流
## 3. 可用工具 (Available Tools)
Memory Manager 有 **2 个工具**
### 3.1 save_user_memory
```typescript
{
name: 'save_user_memory',
description: '将用户信息保存到长期记忆存储。每条记忆包含类型、内容和相关移民类别。',
input_schema: {
type: 'object',
properties: {
memoryType: {
type: 'string',
enum: ['FACT', 'PREFERENCE', 'INTENT'],
description: 'FACT=用户陈述的事实年龄、学历等PREFERENCE=用户偏好倾向的移民方式INTENT=用户意图(计划时间、预算等)'
},
content: {
type: 'string',
description: '要保存的记忆内容,用简洁的陈述句表达,如"用户32岁清华大学计算机硕士"'
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '相关的移民类别(如果有明确关联)'
}
},
required: ['memoryType', 'content']
}
}
```
**底层实现**:调用 knowledge-service 的 `POST /api/v1/memory/user`
```typescript
const response = await axios.post('http://knowledge-service:3003/api/v1/memory/user', {
userId: context.userId,
tenantId: context.tenantId,
memoryType: input.memoryType,
content: input.content,
category: input.category,
importance: IMPORTANCE_MAP[input.memoryType], // FACT:70, INTENT:80, PREFERENCE:60
source: 'conversation',
conversationId: context.conversationId,
});
```
### 3.2 get_user_context
```typescript
{
name: 'get_user_context',
description: '获取用户的历史记忆和背景信息。返回按重要性和相关性排序的记忆列表。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '检索查询,如"用户的基本信息和移民意向"'
}
},
required: ['query']
}
}
```
## 4. System Prompt 要点
```
# 身份
你是 iConsulting 的记忆管理专员。你负责从对话中提取、保存和检索用户信息。
# 核心原则
1. 只提取用户明确陈述的信息,不做推测
2. 模糊信息保留模糊性("三十多岁" → "30+岁"不要推测为35
3. 同一信息不要重复保存,检查已有记忆后再决定是否保存
4. 每条记忆用简洁的陈述句表达
5. 正确分类 memoryType
- FACT: 客观事实(年龄、学历、工作、收入等)
- PREFERENCE: 主观偏好(偏好的移民方式、时间偏好等)
- INTENT: 行动意图(打算何时申请、预算范围、已在准备等)
# 需要提取的核心信息字段
## 基础信息
- age: 年龄
- education: 最高学历(博士/硕士/学士/大专/高中)
- university: 毕业院校
- major: 专业方向
## 职业信息
- currentJobTitle: 当前职位
- currentIndustry: 所属行业
- currentCompany: 当前公司(如果提到)
- totalYearsOfExperience: 工作年限
- annualIncome: 年收入(注意币种)
## 移民相关
- targetCategory: 感兴趣的移民类别
- hasHongKongEmployer: 是否有香港雇主
- hasTechBackground: 是否有科技背景
- investmentAmount: 可投资金额
- immigrationTimeline: 计划移民时间
## 家庭信息
- maritalStatus: 婚姻状况
- hasChildren: 是否有子女
- familySupport: 家人支持程度
# 操作类型
根据 Coordinator 传入的 action 参数执行不同操作:
- load_context: 加载用户上下文(调用 get_user_context
- save_info: 从对话中提取信息并保存(调用 save_user_memory
- summarize: 加载所有记忆并生成结构化摘要
```
## 5. 输入/输出格式
Memory Manager 的输入/输出根据 `action` 不同而变化:
### Action: load_context
**输入**
```typescript
interface MemoryManagerLoadInput {
action: 'load_context';
/** 当前用户问题(用于检索相关记忆) */
query: string;
}
```
**输出**
```typescript
interface MemoryManagerLoadOutput {
action: 'load_context';
/** 检索到的用户记忆列表 */
memories: Array<{
type: 'FACT' | 'PREFERENCE' | 'INTENT';
content: string;
importance: number;
category?: string;
createdAt?: string;
}>;
/** 用户画像摘要(如果有足够信息) */
user_profile_summary?: string;
/** 记忆总数 */
total_memories: number;
}
```
### Action: save_info
**输入**
```typescript
interface MemoryManagerSaveInput {
action: 'save_info';
/** 用户最新消息 */
userMessage: string;
/** 助手最新回复 */
assistantMessage: string;
/** 已有的用户信息(避免重复提取) */
existingInfo?: Record<string, unknown>;
}
```
**输出**
```typescript
interface MemoryManagerSaveOutput {
action: 'save_info';
/** 本次提取并保存的信息 */
extracted_info: Record<string, unknown>;
/** 保存的记忆条数 */
saved_count: number;
/** 保存的记忆详情 */
saved_memories: Array<{
memoryType: 'FACT' | 'PREFERENCE' | 'INTENT';
content: string;
field: string; // 对应的字段名
}>;
/** 跳过的信息(已存在) */
skipped_fields: string[];
}
```
### Action: summarize
**输入**
```typescript
interface MemoryManagerSummarizeInput {
action: 'summarize';
}
```
**输出**
```typescript
interface MemoryManagerSummarizeOutput {
action: 'summarize';
/** 结构化用户画像 */
user_profile: {
// 基础信息
basic: {
age?: string;
education?: string;
university?: string;
major?: string;
nationality?: string;
location?: string;
};
// 职业信息
career: {
jobTitle?: string;
industry?: string;
company?: string;
yearsOfExperience?: string;
annualIncome?: string;
};
// 移民意向
immigration: {
targetCategories?: string[];
timeline?: string;
hasHKEmployer?: boolean;
investmentCapacity?: string;
};
// 家庭情况
family: {
maritalStatus?: string;
hasChildren?: boolean;
familySupport?: string;
};
};
/** 信息完整度 */
completeness: {
score: number; // 0-100
filled_fields: string[];
missing_fields: string[];
};
/** 一段话总结 */
narrative_summary: string;
}
```
## 6. 触发时机 (When to Trigger)
Coordinator 根据不同的 `action` 在不同时机调用 `invoke_memory_manager`
### load_context 触发时机
| 场景 | 触发条件 | 目的 |
|------|----------|------|
| 对话开始 | 第 1 轮对话 | 加载用户历史信息,实现跨会话记忆 |
| 需要用户背景 | Coordinator 需要参考用户画像 | 为决策提供上下文 |
| 评估前准备 | 准备调 Assessment Expert 前 | 补充已有但本次对话未提及的信息 |
### save_info 触发时机
| 场景 | 触发条件 | 目的 |
|------|----------|------|
| 用户分享个人信息 | 对话中出现年龄、学历、工作等信息 | 实时保存用户数据 |
| 每轮对话后 | 助手回复生成后 | 确保不遗漏任何用户信息 |
| 用户表达偏好/意图 | "我比较想走高才通" / "打算明年申请" | 保存用户偏好和意图 |
### summarize 触发时机
| 场景 | 触发条件 | 目的 |
|------|----------|------|
| 评估前摘要 | 准备做全面评估时 | 生成完整的用户画像供 Assessment Expert 使用 |
| 对话结束时 | 对话即将结束 | 更新用户画像摘要 |
**调用频率建议**
- `load_context`:每次新对话开始时调用 1 次
- `save_info`不需要每轮都调用Coordinator 判断用户提供了新信息时才调用
- `summarize`:整个对话过程中最多调用 1-2 次
## 7. 内部循环 (Internal Loop)
Memory Manager 的内部循环根据 action 不同而变化:
### load_context 流程
```
┌─────────────────────────────────────────────────────┐
│ load_context Loop (max 1 turn) │
│ │
│ Turn 0: │
│ ├── get_user_context({query: input.query}) │
│ ├── 整理返回的记忆列表 │
│ ├── 如果记忆足够多,生成 user_profile_summary │
│ └── 返回结构化输出 │
└─────────────────────────────────────────────────────┘
```
### save_info 流程
```
┌─────────────────────────────────────────────────────┐
│ save_info Loop (max 2 turns) │
│ │
│ Turn 0: 提取信息 │
│ ├── LLM 分析 userMessage + assistantMessage │
│ ├── 对比 existingInfo筛除已有字段 │
│ ├── 对新信息逐条调用 save_user_memory │
│ │ (可能并行保存多条) │
│ │ │
│ Turn 1: 确认保存结果(如需) │
│ ├── 检查 save 操作是否全部成功 │
│ └── 返回保存结果摘要 │
└─────────────────────────────────────────────────────┘
```
### summarize 流程
```
┌─────────────────────────────────────────────────────┐
│ summarize Loop (max 2 turns) │
│ │
│ Turn 0: 加载所有记忆 │
│ ├── get_user_context({query: "用户所有背景信息"}) │
│ │ │
│ Turn 1: 生成摘要 │
│ ├── 将散碎的记忆整合为结构化 user_profile │
│ ├── 计算 completeness score │
│ └── 生成 narrative_summary │
└─────────────────────────────────────────────────────┘
```
**save_info 的提取逻辑伪代码**
```typescript
async function extractAndSave(input: MemoryManagerSaveInput): Promise<MemoryManagerSaveOutput> {
// Step 1: LLM 提取信息Haiku 快速完成)
const extractionPrompt = `
从以下对话中提取用户信息。只提取能明确确定的信息,不要猜测。
已有信息(不要重复提取):${JSON.stringify(input.existingInfo)}
用户消息:${input.userMessage}
助手回复:${input.assistantMessage}
返回 JSON{field: value} 格式。
`;
const extracted = await llm.extract(extractionPrompt);
const newFields = filterOutExisting(extracted, input.existingInfo);
// Step 2: 逐条保存(可并行)
const savePromises = Object.entries(newFields).map(([field, value]) => {
const memoryType = inferMemoryType(field); // FACT/PREFERENCE/INTENT
return saveUserMemory({
memoryType,
content: `${FIELD_LABELS[field]}${value}`,
category: inferCategory(field, value),
});
});
const results = await Promise.all(savePromises);
return {
action: 'save_info',
extracted_info: newFields,
saved_count: results.filter(r => r.success).length,
saved_memories: results.map(r => r.detail),
skipped_fields: Object.keys(extracted).filter(k => input.existingInfo?.[k]),
};
}
```
## 8. 与其他 Agent 的关系
```
┌──────────────┐ invoke_memory_manager ┌──────────────┐
│ │ ─────────────────────────────→ │ │
│ Coordinator │ MemoryManagerOutput │ Memory │
│ │ ←───────────────────────────── │ Manager │
└──────┬───────┘ └──────┬───────┘
│ │
│ Memory Manager 是其他 Agent 的 ├── save_user_memory
│ 数据供应商 └── get_user_context
│ │
│ ▼
│ ┌──────────────┐
│ │ Knowledge │
│ │ Service │
│ │ (Memory API) │
│ └──────────────┘
│ 协作模式:
│ 1. 对话开始:
│ Coordinator → invoke_memory_manager({action: 'load_context'})
│ → 获取用户画像 → 注入到后续所有 Agent 调用的上下文中
│ 2. 信息收集轮:
│ 用户发消息 → Coordinator 回复 → Coordinator 调用
│ invoke_memory_manager({action: 'save_info', userMessage, assistantMessage})
│ → 信息自动归档(异步,不阻塞主流程)
│ 3. 评估准备:
│ Coordinator → invoke_memory_manager({action: 'summarize'})
│ → 获取完整用户画像 → 传入 Assessment Expert
│ 4. 跨会话衔接:
│ 新对话开始 → load_context → 上一次对话的信息仍然可用
```
**与旧架构的对应关系**
| 旧架构 | 新架构 |
|--------|--------|
| `StrategyEngineService.extractUserInfo()` | Memory Manager 的 `save_info` action |
| `ConsultingState.collectedInfo` | Memory Manager 从 knowledge-service 加载 |
| 手动在 System Prompt 中拼接用户信息 | Memory Manager 的 `load_context` + `summarize` |
| 信息只在单次对话中有效 | 信息持久化到 knowledge-service跨会话有效 |
**异步保存优化**
`save_info` 可以**异步执行**,不阻塞 Coordinator 的主回复流:
```typescript
// Coordinator 主流程
const reply = yield* generateReply(userMessage); // 先回复用户
// 异步保存用户信息(不阻塞)
invokeMemoryManager({
action: 'save_info',
userMessage,
assistantMessage: reply,
existingInfo: currentCollectedInfo,
}).catch(err => logger.warn('Memory save failed:', err));
```
## 9. 示例场景
### 场景 1从对话中提取信息 (save_info)
**Coordinator 调用**
```json
{
"tool": "invoke_memory_manager",
"input": {
"action": "save_info",
"userMessage": "我今年32岁浙大计算机硕士毕业的现在在杭州做前端开发工作8年了年薪大概80万",
"assistantMessage": "感谢您分享这些信息!您的背景很不错...",
"existingInfo": {}
}
}
```
**内部执行**
```
Turn 0: LLM 分析对话,提取信息
→ 识别到 5 个字段age=32, university="浙江大学", education="硕士",
major="计算机", currentJobTitle="前端开发", totalYearsOfExperience=8,
annualIncome=800000, currentLocation="杭州"
Turn 1: 并行保存 8 条记忆
→ save_user_memory({type: "FACT", content: "用户32岁"})
→ save_user_memory({type: "FACT", content: "浙江大学计算机硕士"})
→ save_user_memory({type: "FACT", content: "前端开发工程师工作8年"})
→ save_user_memory({type: "FACT", content: "年薪约80万人民币"})
→ save_user_memory({type: "FACT", content: "目前在杭州工作"})
→ ... (其他字段)
```
**返回结果**
```json
{
"action": "save_info",
"extracted_info": {
"age": 32,
"education": "硕士",
"university": "浙江大学",
"major": "计算机",
"currentJobTitle": "前端开发",
"totalYearsOfExperience": 8,
"annualIncome": 800000,
"currentLocation": "杭州"
},
"saved_count": 5,
"saved_memories": [
{"memoryType": "FACT", "content": "用户32岁", "field": "age"},
{"memoryType": "FACT", "content": "浙江大学计算机硕士毕业", "field": "education+university+major"},
{"memoryType": "FACT", "content": "从事前端开发工作8年", "field": "currentJobTitle+totalYearsOfExperience"},
{"memoryType": "FACT", "content": "年薪约80万人民币", "field": "annualIncome"},
{"memoryType": "FACT", "content": "目前在杭州工作", "field": "currentLocation"}
],
"skipped_fields": []
}
```
### 场景 2跨会话加载上下文 (load_context)
**Coordinator 调用**(新对话开始时):
```json
{
"tool": "invoke_memory_manager",
"input": {
"action": "load_context",
"query": "用户的基本背景和移民意向"
}
}
```
**返回结果**
```json
{
"action": "load_context",
"memories": [
{"type": "FACT", "content": "用户32岁", "importance": 70, "createdAt": "2026-02-05"},
{"type": "FACT", "content": "浙江大学计算机硕士毕业", "importance": 70, "createdAt": "2026-02-05"},
{"type": "FACT", "content": "从事前端开发工作8年", "importance": 70, "createdAt": "2026-02-05"},
{"type": "FACT", "content": "年薪约80万人民币", "importance": 70, "createdAt": "2026-02-05"},
{"type": "INTENT", "content": "对高才通B类非常感兴趣", "importance": 80, "category": "TTPS", "createdAt": "2026-02-05"},
{"type": "PREFERENCE", "content": "希望尽快办理,时间越短越好", "importance": 60, "createdAt": "2026-02-05"}
],
"user_profile_summary": "32岁浙大计算机硕士8年前端开发经验年薪80万。上次对话中对高才通B类表现出浓厚兴趣偏好快速通道。",
"total_memories": 6
}
```
### 场景 3生成用户画像摘要 (summarize)
**Coordinator 调用**(准备做评估前):
```json
{
"tool": "invoke_memory_manager",
"input": {
"action": "summarize"
}
}
```
**返回结果**
```json
{
"action": "summarize",
"user_profile": {
"basic": {
"age": "32岁",
"education": "硕士",
"university": "浙江大学",
"major": "计算机科学",
"location": "杭州"
},
"career": {
"jobTitle": "前端开发工程师",
"industry": "IT/互联网",
"yearsOfExperience": "8年",
"annualIncome": "约80万人民币"
},
"immigration": {
"targetCategories": ["TTPS"],
"timeline": "尽快",
"hasHKEmployer": false
},
"family": {
"maritalStatus": "未知",
"hasChildren": null,
"familySupport": "未知"
}
},
"completeness": {
"score": 65,
"filled_fields": ["age", "education", "university", "major", "location", "jobTitle", "industry", "yearsOfExperience", "annualIncome", "targetCategories", "timeline"],
"missing_fields": ["nationality", "maritalStatus", "hasChildren", "languageSkills", "hasHongKongEmployer", "hasTechBackground", "investmentAmount"]
},
"narrative_summary": "用户是一位32岁的前端开发工程师浙江大学计算机硕士毕业拥有8年IT行业经验年薪约80万人民币目前在杭州工作。用户对高才通B类最感兴趣希望尽快办理。家庭情况和语言能力等信息尚未收集。"
}
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,921 @@
# 10 - 实施计划详解 (Implementation Plan)
## 1. 总体时间线
```
Week 1 Week 2 Week 2-3 Week 3 Week 4 Week 5
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Phase 1 │ │ Phase 2 │ │ Phase 3 │ │ Phase 4 │ │ Phase 5+6│ │ Phase 7 │
│ │ │ │ │ │ │ │ │ │ │ │
│ 架构文档 │──→│ 基础设施 │──→│ 专家Agent│──→│ Coordinator│──→│ 集成+前端│──→│ 测试+优化│
│ + 类型 │ │ │ │ │ │ │ │ │ │ │
│ 定义 │ │ Queue │ │ 6个Agent │ │ 核心Loop │ │ Module │ │ 场景测试 │
│ │ │ Context │ │ 6份Prompt│ │ 主Prompt │ │ Gateway │ │ 性能优化 │
│ │ │ Base │ │ │ │ │ │ Frontend │ │ 旧代码清理│
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
```
## 2. Phase 1: 架构文档 + 类型定义 (Week 1)
### 2.1 架构文档12份
| 序号 | 文件 | 内容 | 状态 |
|------|------|------|------|
| 00 | `00-overview.md` | 总览 + 架构图 | ✅ 已完成 |
| 01 | `01-coordinator-agent.md` | Coordinator 设计 | ✅ 已完成 |
| 02 | `02-specialist-agents.md` | 6 个专家 Agent 设计 | 待完成 |
| 03 | `03-agent-communication.md` | Agent 间通信协议 | 待完成 |
| 04 | `04-streaming-protocol.md` | 流式传输协议 | 待完成 |
| 05 | `05-frontend-integration.md` | 前端集成方案 | 待完成 |
| 06 | `06-token-cost-management.md` | Token 与成本管理 | 待完成 |
| 07 | `07-testing-strategy.md` | 测试策略 | 待完成 |
| 08 | `08-context-injection.md` | 动态上下文注入 | ✅ 已完成 |
| 09 | `09-tool-execution.md` | 工具并发执行 | ✅ 已完成 |
| 10 | `10-implementation-plan.md` | 实施计划(本文档) | ✅ 已完成 |
| 11 | `11-prompt-templates.md` | 所有 Prompt 模板 | ✅ 已完成 |
### 2.2 类型定义文件
需要创建以下类型定义文件:
```
packages/services/conversation-service/src/infrastructure/agents/
├── types/
│ ├── agent.types.ts # Agent 相关类型
│ ├── stream.types.ts # 流式事件类型
│ └── context.types.ts # 上下文注入类型
```
**agent.types.ts**
```typescript
// 核心 Agent 类型定义
/** Agent 名称枚举 */
export enum AgentName {
COORDINATOR = 'coordinator',
POLICY_EXPERT = 'policy_expert',
ASSESSMENT_EXPERT = 'assessment_expert',
STRATEGIST = 'strategist',
OBJECTION_HANDLER = 'objection_handler',
CASE_ANALYST = 'case_analyst',
MEMORY_MANAGER = 'memory_manager',
}
/** Agent 使用的模型 */
export enum AgentModel {
SONNET = 'claude-sonnet-4-20250514',
HAIKU = 'claude-haiku-4-20250514',
}
/** Agent 配置 */
export interface AgentConfig {
name: AgentName;
model: AgentModel;
maxTokens: number;
maxTurns: number; // Agent mini-loop 最大轮次
timeoutMs: number; // 超时时间
tools: string[]; // 可用工具名列表
systemPromptFile: string; // Prompt 文件路径
}
/** Agent 执行上下文 */
export interface AgentExecutionContext {
userId: string;
conversationId: string;
coordinatorTurn: number; // Coordinator 当前轮次
parentToolUseId: string; // 触发此 Agent 的 tool_use ID
}
/** Agent Loop 参数 */
export interface AgentLoopParams {
messages: Anthropic.MessageParam[];
systemPrompt: string[] | Anthropic.TextBlockParam[];
tools: Anthropic.Tool[];
maxTurns: number;
maxBudgetUsd: number;
conversationId: string;
userId: string;
abortSignal?: AbortSignal;
}
/** 咨询状态(从 consulting_state XML tag 提取) */
export interface ConsultingStateReport {
currentStage: string;
collectedFields: string[];
nextAction: string;
confidenceLevel: 'low' | 'medium' | 'high';
recommendedPrograms?: string[];
}
```
**stream.types.ts**
```typescript
// 流式事件类型
/** 所有可能的 Stream 事件 */
export type StreamEvent =
| TextStreamEvent
| AgentStartEvent
| AgentProgressEvent
| AgentCompleteEvent
| ToolStartEvent
| ToolCompleteEvent
| ToolErrorEvent
| StateUpdateEvent
| UsageEvent
| ErrorEvent
| EndEvent;
export interface TextStreamEvent {
type: 'text';
content: string;
}
export interface AgentStartEvent {
type: 'agent_start';
agentName: string;
toolUseId: string;
}
export interface AgentProgressEvent {
type: 'agent_progress';
agentName: string;
turn: number;
maxTurns: number;
}
export interface AgentCompleteEvent {
type: 'agent_complete';
agentName: string;
durationMs: number;
tokensUsed?: { input: number; output: number };
}
export interface StateUpdateEvent {
type: 'state_update';
state: ConsultingStateReport;
}
export interface UsageEvent {
type: 'usage';
usage: { inputTokens: number; outputTokens: number };
totalCostUsd: number;
}
export interface ErrorEvent {
type: 'error';
code: string;
message: string;
}
export interface EndEvent {
type: 'end';
totalTokens: { input: number; output: number };
totalCostUsd: number;
turnsUsed: number;
agentsCalled: string[];
}
```
**context.types.ts**
- 详见 [08-context-injection.md](./08-context-injection.md) 第 3 节
**交付标准**
- 所有类型文件通过 `tsc --noEmit` 类型检查
- 每个 interface 有 JSDoc 注释
- 导出为 barrel file (`types/index.ts`)
## 3. Phase 2: 基础设施 (Week 2)
### 3.1 文件结构
```
packages/services/conversation-service/src/infrastructure/agents/
├── types/ # Phase 1 产物
│ ├── agent.types.ts
│ ├── stream.types.ts
│ ├── context.types.ts
│ └── index.ts
├── core/ # Phase 2 产物
│ ├── tool-execution-queue.ts # 工具并发执行队列
│ ├── context-injector.service.ts # 上下文注入器
│ ├── base-specialist.service.ts # 专家 Agent 基类
│ ├── agent-executor.ts # Agent 执行器工厂
│ └── token-tracker.ts # Token 追踪器
```
### 3.2 tool-execution-queue.ts
- 完整实现详见 [09-tool-execution.md](./09-tool-execution.md) 第 4 节
- **关键依赖**: `tool-execution.types.ts`
- **单测覆盖**:
- 纯并发批次(所有 safe 工具)
- 纯串行批次(所有 unsafe 工具)
- 混合批次safe + unsafe 交替)
- 单个工具超时
- 批次中一个工具失败
- 空请求
- 结果排序正确性
```typescript
// 单测示例
describe('ToolExecutionQueue', () => {
it('should execute concurrent-safe tools in parallel', async () => {
const queue = new ToolExecutionQueue();
const executionOrder: string[] = [];
queue.registerTool('tool_a', {
isConcurrencySafe: true,
executor: async () => {
executionOrder.push('a_start');
await sleep(100);
executionOrder.push('a_end');
return { content: 'a', success: true };
},
// ...
});
queue.registerTool('tool_b', {
isConcurrencySafe: true,
executor: async () => {
executionOrder.push('b_start');
await sleep(50);
executionOrder.push('b_end');
return { content: 'b', success: true };
},
// ...
});
const results = await queue.executeAll([
{ toolUseId: '1', toolName: 'tool_a', input: {}, originalIndex: 0 },
{ toolUseId: '2', toolName: 'tool_b', input: {}, originalIndex: 1 },
]);
// 两个工具应该几乎同时开始
expect(executionOrder[0]).toBe('a_start');
expect(executionOrder[1]).toBe('b_start');
// b 先完成50ms vs 100ms
expect(executionOrder[2]).toBe('b_end');
expect(executionOrder[3]).toBe('a_end');
// 结果按 originalIndex 排序
expect(results[0].toolName).toBe('tool_a');
expect(results[1].toolName).toBe('tool_b');
});
});
```
### 3.3 context-injector.service.ts
- 完整实现详见 [08-context-injection.md](./08-context-injection.md) 第 5 节
- **关键依赖**: `KnowledgeClientService`, `context.types.ts`
- **单测覆盖**:
- 所有 8 种上下文的独立构建
- 缓存命中/未命中
- Token 预算裁剪
- P0 上下文不被裁剪
- 缓存失效
- auto-compaction 触发条件
### 3.4 base-specialist.service.ts
- 完整实现详见 [09-tool-execution.md](./09-tool-execution.md) 第 6 节
- **关键依赖**: `@anthropic-ai/sdk`
- **单测覆盖**:
- Mini-loop 正常完成1轮无工具调用
- Mini-loop 有工具调用2轮
- 达到 maxTurns 强制终止
- API 错误处理
- Token 追踪回调
### 3.5 交付标准
- 所有基础设施组件可以独立运行单测
- 不依赖具体的 Specialist Agent 实现
- 使用 mock 的 Claude API 进行测试
## 4. Phase 3: 专家 Agent (Week 2-3)
### 4.1 文件结构
```
packages/services/conversation-service/src/infrastructure/agents/
├── specialists/
│ ├── policy-expert.service.ts # 政策专家
│ ├── assessment-expert.service.ts # 评估专家
│ ├── strategist.service.ts # 策略顾问
│ ├── objection-handler.service.ts # 异议处理
│ ├── case-analyst.service.ts # 案例分析
│ └── memory-manager.service.ts # 记忆管理
├── prompts/
│ ├── coordinator-system-prompt.ts # Phase 4
│ ├── policy-expert-prompt.ts
│ ├── assessment-expert-prompt.ts
│ ├── strategist-prompt.ts
│ ├── objection-handler-prompt.ts
│ ├── case-analyst-prompt.ts
│ └── memory-manager-prompt.ts
```
### 4.2 各 Agent 实现计划
| Agent | 继承 | Model | 工具 | 预估代码量 | 优先级 |
|-------|------|-------|------|-----------|--------|
| `PolicyExpertService` | `BaseSpecialistService` | Sonnet | `search_knowledge` | ~120 行 | P0 |
| `AssessmentExpertService` | `BaseSpecialistService` | Sonnet | `search_knowledge`, `get_user_context` | ~200 行 | P0 |
| `MemoryManagerService` | `BaseSpecialistService` | Haiku | `save_user_memory`, `get_user_context` | ~150 行 | P0 |
| `StrategistService` | `BaseSpecialistService` | Sonnet | `get_user_context` | ~100 行 | P1 |
| `ObjectionHandlerService` | `BaseSpecialistService` | Sonnet | `search_knowledge`, `get_user_context` | ~120 行 | P1 |
| `CaseAnalystService` | `BaseSpecialistService` | Haiku | `search_knowledge`, `get_user_context` | ~100 行 | P2 |
### 4.3 开发顺序
建议按以下顺序开发(优先级 + 依赖关系):
```
1. MemoryManagerService → 最简单,验证 BaseSpecialist + 工具执行
2. PolicyExpertService → 最常用,验证 knowledge-service 集成
3. AssessmentExpertService → 核心业务,需要 knowledge + 用户上下文
4. StrategistService → 辅助 Coordinator 决策
5. ObjectionHandlerService → 异议处理,依赖 knowledge
6. CaseAnalystService → 最后实现,依赖度最低
```
### 4.4 每个 Agent 的独立测试策略
```typescript
// 以 PolicyExpertService 为例的独立测试
describe('PolicyExpertService', () => {
let service: PolicyExpertService;
let mockAnthropicClient: jest.Mocked<Anthropic>;
let mockKnowledgeClient: jest.Mocked<KnowledgeClientService>;
beforeEach(() => {
mockAnthropicClient = createMockAnthropicClient();
mockKnowledgeClient = createMockKnowledgeClient();
service = new PolicyExpertService(mockAnthropicClient, mockKnowledgeClient);
});
it('should answer policy questions without tool calls', async () => {
// Mock: Claude 直接回答,不调用工具
mockAnthropicClient.messages.create.mockResolvedValue({
content: [{ type: 'text', text: '高才通B类要求...' }],
usage: { input_tokens: 500, output_tokens: 200 },
stop_reason: 'end_turn',
});
const result = await service.execute({
input: { query: '高才通B类条件是什么' },
maxTurns: 3,
});
expect(result.output).toContain('高才通');
expect(result.turnsUsed).toBe(1);
});
it('should use search_knowledge tool when needed', async () => {
// Mock: Claude 调用 search_knowledge再用结果生成回答
mockAnthropicClient.messages.create
.mockResolvedValueOnce({
content: [
{ type: 'tool_use', id: 't1', name: 'search_knowledge', input: { query: 'TTPS B类 条件' } },
],
usage: { input_tokens: 500, output_tokens: 100 },
stop_reason: 'tool_use',
})
.mockResolvedValueOnce({
content: [{ type: 'text', text: '根据查询结果高才通B类...' }],
usage: { input_tokens: 800, output_tokens: 300 },
stop_reason: 'end_turn',
});
mockKnowledgeClient.retrieveKnowledge.mockResolvedValue({
content: '高才通B类要求全球百强大学学士学位...',
sources: [{ articleId: '1', title: '高才通指南', similarity: 0.92 }],
});
const result = await service.execute({
input: { query: '高才通B类条件' },
maxTurns: 3,
onToolCall: jest.fn(),
});
expect(result.output).toContain('根据查询结果');
expect(result.turnsUsed).toBe(2);
expect(mockKnowledgeClient.retrieveKnowledge).toHaveBeenCalled();
});
});
```
### 4.5 交付标准
- 每个 Agent 有独立的单元测试mock Claude API + mock 外部服务)
- 每个 Agent 的 Prompt 是独立文件,可以独立调整
- 所有 Agent 通过 `BaseSpecialistService.execute()` 统一调用
## 5. Phase 4: Coordinator (Week 3)
### 5.1 文件结构
```
packages/services/conversation-service/src/infrastructure/agents/
├── coordinator/
│ ├── coordinator-agent.service.ts # 主服务
│ ├── agent-loop.ts # 核心递归循环
│ └── state-extractor.ts # 从回复中提取 consulting_state
├── prompts/
│ └── coordinator-system-prompt.ts # 500+ 行 Prompt
```
### 5.2 coordinator-system-prompt.ts
这是整个系统最重要的文件,详见 [11-prompt-templates.md](./11-prompt-templates.md)。
关键结构:
```typescript
export function buildCoordinatorSystemPrompt(config: {
expertContact: { wechat: string; phone: string; workingHours: string };
paidServices: { assessmentPrice: number; description: string };
currentDate: string;
}): string {
return `
# 身份定义
...(约 30 行)
# 你的专家团队
...(约 80 行)
# 六大移民类别
...(约 200 行)
# 对话策略
...(约 100 行)
# 回复规范
...(约 40 行)
# 状态报告格式
...(约 30 行)
# 业务规则
...(约 20 行)
`;
}
```
### 5.3 agent-loop.ts
核心递归循环,详见 [01-coordinator-agent.md](./01-coordinator-agent.md) 第 4 节。
关键特性:
- `async function* agentLoop()`: AsyncGenerator 模式
- 递归调用自身tool_results → 再次调用 Claude
- 集成 ContextInjector
- 集成 ToolExecutionQueue
- 集成 state extractor
- abort 信号支持
### 5.4 coordinator-agent.service.ts
NestJS 服务,封装 agent-loop对外暴露与旧 `ClaudeAgentServiceV2` 相同的接口:
```typescript
@Injectable()
export class CoordinatorAgentService {
constructor(
private contextInjector: ContextInjectorService,
private toolExecutionQueue: ToolExecutionQueue,
// ... 各 Specialist Agent
) {}
async *sendMessage(params: {
conversationContext: ConversationContext;
userMessage: string;
attachments?: FileAttachment[];
userId: string;
conversationId: string;
deviceInfo?: DeviceInfo;
}): AsyncGenerator<StreamEvent> {
// 委托给 agentLoop
yield* agentLoop({
messages: this.buildMessages(params),
systemPrompt: this.buildSystemPrompt(),
tools: this.toolExecutionQueue.getClaudeTools(),
maxTurns: 15,
maxBudgetUsd: 0.50,
conversationId: params.conversationId,
userId: params.userId,
});
}
}
```
### 5.5 交付标准
- Coordinator 可以独立运行集成测试(使用真实 Claude API
- agent-loop 有完整的单元测试mock API
- System Prompt 经过人工审查
- 通过 5 个核心对话场景的 E2E 测试
## 6. Phase 5: 集成 (Week 4 前半)
### 6.1 NestJS Module 集成
**新增 agents.module.ts**
```typescript
// packages/services/conversation-service/src/infrastructure/agents/agents.module.ts
@Module({
imports: [KnowledgeModule], // 依赖 KnowledgeClientService
providers: [
// Core
ToolExecutionQueue,
ContextInjectorService,
// Specialists
PolicyExpertService,
AssessmentExpertService,
StrategistService,
ObjectionHandlerService,
CaseAnalystService,
MemoryManagerService,
// Coordinator
CoordinatorAgentService,
],
exports: [CoordinatorAgentService],
})
export class AgentsModule {}
```
**修改 claude.module.ts**
```typescript
// 在 claude.module.ts 中导入 AgentsModule
@Module({
imports: [AgentsModule], // 新增
providers: [
ClaudeAgentServiceV2, // 保留旧服务(过渡期)
// ...
],
exports: [
ClaudeAgentServiceV2, // 保留旧导出
CoordinatorAgentService, // 新增导出
],
})
export class ClaudeModule {}
```
**修改 conversation.service.ts**
```typescript
// 切换注入(使用 feature flag
@Injectable()
export class ConversationService {
constructor(
@Inject(CONVERSATION_REPOSITORY)
private readonly conversationRepo: IConversationRepository,
@Inject(MESSAGE_REPOSITORY)
private readonly messageRepo: IMessageRepository,
// 旧服务(过渡期保留)
private readonly claudeAgentService: ClaudeAgentServiceV2,
// 新服务
private readonly coordinatorAgentService: CoordinatorAgentService,
private readonly configService: ConfigService,
) {}
private get useNewArchitecture(): boolean {
return this.configService.get<boolean>('USE_MULTI_AGENT', false);
}
async *sendMessage(params: SendMessageParams): AsyncGenerator<StreamChunk> {
if (this.useNewArchitecture) {
yield* this.sendMessageV3(params); // 新架构
} else {
yield* this.sendMessageV2(params); // 旧架构
}
}
}
```
**更新 conversation.gateway.ts**
```typescript
// 新增 Agent 相关的 WebSocket 事件
// stream_chunk 保持不变
// 新增:
// agent_start { agentName, conversationId }
// agent_progress { agentName, turn, maxTurns, conversationId }
// agent_complete { agentName, durationMs, conversationId }
```
### 6.2 Feature Flag 策略
```
环境变量: USE_MULTI_AGENT=true/false
开发环境: true (直接使用新架构)
测试环境: true (测试新架构)
生产环境: false (等全部测试通过后切换)
```
### 6.3 共享类型更新
```typescript
// packages/shared/types/src/conversation.ts
// 新增 StreamEvent 类型供前端使用
export interface AgentStatusEvent {
type: 'agent_start' | 'agent_progress' | 'agent_complete';
agentName: string;
conversationId: string;
// agent_progress only
turn?: number;
maxTurns?: number;
// agent_complete only
durationMs?: number;
}
```
### 6.4 交付标准
- Feature flag 可以在不重启服务的情况下切换
- 旧架构在 `USE_MULTI_AGENT=false` 时完全不受影响
- WebSocket 事件向后兼容(新事件是追加的,不修改已有事件)
## 7. Phase 6: 前端 (Week 4 后半)
### 7.1 前端文件修改
```
packages/clients/web-client/src/
├── stores/
│ └── chatStore.ts # 新增 agentStatus 状态
├── components/
│ ├── chat/
│ │ ├── ChatWindow.tsx # 处理新的 WebSocket 事件
│ │ ├── MessageBubble.tsx # 可能需要微调
│ │ └── AgentStatus.tsx # 新增Agent 工作状态组件
│ └── shared/
│ └── AgentBadge.tsx # 新增Agent 名称+状态徽章
```
### 7.2 chatStore 更新
```typescript
// chatStore.ts (Zustand)
interface AgentStatus {
name: string;
displayName: string; // 中文显示名
status: 'idle' | 'working' | 'completed';
startedAt?: number;
completedAt?: number;
durationMs?: number;
}
interface ChatState {
// 已有状态...
messages: Message[];
isStreaming: boolean;
// 新增Agent 状态
activeAgents: AgentStatus[];
agentHistory: AgentStatus[]; // 本次对话中所有 Agent 调用记录
}
// Agent 名称映射
const AGENT_DISPLAY_NAMES: Record<string, string> = {
policy_expert: '政策专家',
assessment_expert: '评估专家',
strategist: '策略顾问',
objection_handler: '异议处理',
case_analyst: '案例分析',
memory_manager: '记忆管理',
};
```
### 7.3 AgentStatus 组件
```tsx
// AgentStatus.tsx
// 显示在聊天窗口中,消息气泡和输入框之间
function AgentStatus({ agents }: { agents: AgentStatus[] }) {
const workingAgents = agents.filter(a => a.status === 'working');
if (workingAgents.length === 0) return null;
return (
<div className="agent-status-bar">
{workingAgents.map(agent => (
<div key={agent.name} className="agent-badge working">
<span className="agent-icon">🔄</span>
<span className="agent-name">{agent.displayName}</span>
<span className="agent-time">
{formatElapsedTime(Date.now() - (agent.startedAt || 0))}
</span>
</div>
))}
<span className="agent-hint">正在分析中...</span>
</div>
);
}
```
### 7.4 ChatWindow 事件处理
```typescript
// ChatWindow.tsx - WebSocket 事件处理更新
socket.on('agent_start', (data: { agentName: string; conversationId: string }) => {
chatStore.addActiveAgent({
name: data.agentName,
displayName: AGENT_DISPLAY_NAMES[data.agentName] || data.agentName,
status: 'working',
startedAt: Date.now(),
});
});
socket.on('agent_complete', (data: { agentName: string; durationMs: number }) => {
chatStore.completeAgent(data.agentName, data.durationMs);
});
socket.on('stream_end', () => {
chatStore.clearActiveAgents(); // 清除所有工作中的 Agent 状态
});
```
### 7.5 交付标准
- Agent 状态在 UI 中实时显示
- Agent 完成后自动消失(带淡出动画)
- 在弱网环境下不会出现 Agent 状态 "卡住" 的问题(超时自动清除)
- 旧架构模式下不显示 Agent 状态
## 8. Phase 7: 测试 + 优化 + 清理 (Week 5)
### 8.1 测试场景矩阵
| 场景编号 | 场景描述 | 涉及 Agent | 预期行为 |
|----------|---------|-----------|---------|
| S01 | 新用户首次咨询 | Coordinator + Memory | 破冰 → 收集信息 → 保存记忆 |
| S02 | 询问高才通条件 | Coordinator + Policy | 检索知识库 → 准确回答 |
| S03 | 信息收集后评估 | Coordinator + Assessment + Memory | 评估资格 → 推荐方案 |
| S04 | 用户表示犹豫 | Coordinator + Objection + Strategist | 识别异议 → 策略回应 |
| S05 | 用户要求付费评估 | Coordinator + (Payment tool) | 生成支付链接 |
| S06 | 老用户回访 | Coordinator + Memory | 加载历史记忆 → 个性化问候 |
| S07 | 并发工具调用 | Coordinator + Policy + Case | 并行执行两个 Agent |
| S08 | 超长对话 (30+ 轮) | All | Auto-compaction 触发 |
| S09 | API 错误恢复 | Coordinator | 降级到 Haiku / 友好错误 |
| S10 | 用户中途取消 | Coordinator | AbortSignal 生效 |
### 8.2 性能优化清单
| 优化项 | 目标 | 方法 |
|--------|------|------|
| 首次回复延迟 | < 3s | Prompt Caching + 预热 |
| 并发 Agent 延迟 | < 5s (3 Agent 并行) | ToolExecutionQueue 并行 |
| Context Injection 延迟 | < 200ms | 缓存 + 并行获取 |
| Auto-compaction 延迟 | < 2s | Haiku 摘要 |
| Token 成本 (每对话) | < $0.30 | Haiku for 辅助 Agent + Caching |
| 内存使用 | < 100MB per 1000 并发对话 | 对话结束清理缓存 |
### 8.3 旧代码清理计划
**阶段 A: 标记废弃 (Week 5)**
```typescript
// 不删除代码,只标记 @deprecated
/** @deprecated Use CoordinatorAgentService instead. Will be removed in v4. */
export class ClaudeAgentServiceV2 { ... }
/** @deprecated Replaced by Coordinator Prompt. */
export class StrategyEngineService { ... }
/** @deprecated Intent classification now handled by LLM. */
export const intentClassifier = { ... };
/** @deprecated Response quality now handled by Prompt. */
export const responseGate = { ... };
```
**阶段 B: 删除代码 (Week 6+, after 1 week in production)**
- 删除 `strategy-engine.service.ts`
- 删除 `intent-classifier.ts`
- 删除 `response-gate.ts`
- 删除 `default-strategy.ts`
- 删除 `claude-agent-v2.service.ts`
- 移除 feature flag
### 8.4 Rollback 计划
```
如果新架构在生产中出现严重问题:
1. 即时回滚 (< 1 分钟):
设置环境变量 USE_MULTI_AGENT=false
→ 自动切回 ClaudeAgentServiceV2
→ 不需要重新部署
2. 部署回滚 (< 5 分钟):
git revert 到 Phase 5 之前的 commit
→ 重新部署 conversation-service
3. 数据恢复:
新架构不修改数据库 schema
ConsultingState 格式向后兼容
→ 无需数据迁移
```
## 9. 组件依赖图
```
┌─────────────────┐
│ agents.module │
└────────┬────────┘
│ exports
┌────────────▼────────────┐
│ CoordinatorAgentService │
└────────────┬────────────┘
│ depends on
┌──────────────────┼──────────────────┐
│ │ │
┌────────▼───────┐ ┌────────▼────────┐ ┌───────▼────────┐
│ ContextInjector │ │ ToolExecution │ │ agentLoop() │
│ Service │ │ Queue │ │ (function) │
└────────┬───────┘ └────────┬────────┘ └───────┬────────┘
│ │ │
┌────────▼───────┐ ┌────────▼────────┐ │
│ Knowledge │ │ Agent Executors │ │
│ ClientService │ │ (factory) │ │
└────────────────┘ └────────┬────────┘ │
│ │
┌───────▼───────────────────┘
┌─────────▼─────────┐
│ BaseSpecialist │
│ Service (abstract) │
└─────────┬─────────┘
│ extends
┌───────┬───────┬───┴───┬───────┬───────┐
│ │ │ │ │ │
Policy Assess Strat Object Case Memory
Expert Expert gist Handler Analyst Manager
```
## 10. 风险与缓解
| 风险 | 概率 | 影响 | 缓解措施 |
|------|------|------|---------|
| Coordinator Prompt 效果不佳 | 高 | 高 | Phase 4 投入大量时间测试 Prompt准备多个版本 A/B 测试 |
| Token 成本超预期 | 中 | 中 | 监控每次对话成本,设置 maxBudgetUsd 硬上限 |
| 并发 Agent 导致 Rate Limit | 中 | 中 | 实现指数退避重试,降级到 Haiku |
| Context Window 不够用 | 低 | 高 | Auto-compaction + 严格的 token 预算 |
| 前端 Agent 状态同步问题 | 中 | 低 | WebSocket 事件超时自动清理 |
| 旧代码删除导致回归 | 低 | 高 | Feature flag 过渡期,完善的回滚计划 |
## 11. 交付 Checklist
### Phase 1 Checklist
- [ ] 12 份架构文档全部完成
- [ ] `agent.types.ts` 通过类型检查
- [ ] `stream.types.ts` 通过类型检查
- [ ] `context.types.ts` 通过类型检查
- [ ] 所有类型有 JSDoc 注释
### Phase 2 Checklist
- [ ] `ToolExecutionQueue` 通过所有单元测试
- [ ] `ContextInjectorService` 通过所有单元测试
- [ ] `BaseSpecialistService` 通过所有单元测试
- [ ] 所有组件可以独立测试(不依赖 Claude API
### Phase 3 Checklist
- [ ] 6 个 Specialist Agent 全部实现
- [ ] 6 份 Prompt 文件全部编写
- [ ] 每个 Agent 有独立的单元测试
- [ ] 每个 Agent 使用真实 API 的集成测试(可选,按需运行)
### Phase 4 Checklist
- [ ] Coordinator System Prompt 完成500+ 行)
- [ ] agent-loop 通过所有单元测试
- [ ] CoordinatorAgentService 通过集成测试
- [ ] 5 个核心场景 E2E 测试通过
### Phase 5 Checklist
- [ ] `agents.module.ts` 编写完成
- [ ] Feature flag 切换工作正常
- [ ] 旧架构在 flag=false 时不受影响
- [ ] WebSocket 事件向后兼容
### Phase 6 Checklist
- [ ] chatStore 更新完成
- [ ] AgentStatus 组件实现
- [ ] ChatWindow 处理新事件
- [ ] 前端无 regression
### Phase 7 Checklist
- [ ] 10 个测试场景全部通过
- [ ] 性能指标达标
- [ ] 旧代码标记 @deprecated
- [ ] 回滚计划验证通过
- [ ] 文档更新完成

File diff suppressed because it is too large Load Diff

View File

@ -116,11 +116,37 @@ export class ConversationGateway
tool: chunk.toolName,
result: chunk.toolResult,
});
} else if (chunk.type === 'agent_start') {
client.emit('agent_start', {
messageId,
conversationId,
agentType: chunk.agentType,
agentName: chunk.agentName,
description: chunk.description,
});
} else if (chunk.type === 'agent_complete') {
client.emit('agent_complete', {
messageId,
conversationId,
agentType: chunk.agentType,
agentName: chunk.agentName,
durationMs: chunk.durationMs,
success: chunk.success,
});
} else if (chunk.type === 'coordinator_thinking') {
client.emit('coordinator_thinking', {
messageId,
conversationId,
phase: chunk.phase,
message: chunk.message,
});
} else if (chunk.type === 'end') {
client.emit('stream_end', {
messageId,
conversationId,
isComplete: true,
inputTokens: chunk.inputTokens,
outputTokens: chunk.outputTokens,
});
}
}

View File

@ -19,10 +19,10 @@ import {
MESSAGE_REPOSITORY,
} from '../../domain/repositories/message.repository.interface';
import {
ClaudeAgentServiceV2,
ConversationContext,
CoordinatorAgentService,
LegacyConversationContext as ConversationContext,
StreamChunk,
} from '../../infrastructure/claude/claude-agent-v2.service';
} from '../../infrastructure/agents/coordinator/coordinator-agent.service';
export interface CreateConversationParams {
userId: string;
@ -54,7 +54,7 @@ export class ConversationService {
private readonly conversationRepo: IConversationRepository,
@Inject(MESSAGE_REPOSITORY)
private readonly messageRepo: IMessageRepository,
private readonly claudeAgentService: ClaudeAgentServiceV2,
private readonly claudeAgentService: CoordinatorAgentService,
) {}
/**

View File

@ -0,0 +1,102 @@
/**
* Agents Module
* Agent NestJS
*
* Agent
* - CoordinatorAgentService ClaudeAgentServiceV2
* - 6 Agent
* - ContextInjectorService
*/
import { Module, Global } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import Anthropic from '@anthropic-ai/sdk';
import { ConfigService } from '@nestjs/config';
// Coordinator
import { CoordinatorAgentService } from './coordinator/coordinator-agent.service';
import { ContextInjectorService } from './coordinator/context-injector.service';
// Specialists
import { PolicyExpertService } from './specialists/policy-expert.service';
import { AssessmentExpertService } from './specialists/assessment-expert.service';
import { StrategistService } from './specialists/strategist.service';
import { ObjectionHandlerService } from './specialists/objection-handler.service';
import { CaseAnalystService } from './specialists/case-analyst.service';
import { MemoryManagerService } from './specialists/memory-manager.service';
// External dependencies
import { KnowledgeModule } from '../knowledge/knowledge.module';
import { TokenUsageService } from '../claude/token-usage.service';
import { ImmigrationToolsService } from '../claude/tools/immigration-tools.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm';
/**
* Anthropic Client Provider
* Anthropic SDK Agent
*/
const AnthropicClientProvider = {
provide: Anthropic,
useFactory: (configService: ConfigService) => {
const baseUrl = configService.get<string>('ANTHROPIC_BASE_URL');
const isProxyUrl =
baseUrl &&
(baseUrl.includes('67.223.119.33') || baseUrl.match(/^\d+\.\d+\.\d+\.\d+/));
if (isProxyUrl) {
console.log(`[AgentsModule] Using Anthropic proxy: ${baseUrl}`);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
return new Anthropic({
apiKey: configService.get<string>('ANTHROPIC_API_KEY'),
baseURL: baseUrl || undefined,
});
},
inject: [ConfigService],
};
@Global()
@Module({
imports: [
ConfigModule,
KnowledgeModule,
TypeOrmModule.forFeature([TokenUsageORM]),
],
providers: [
// Shared Anthropic client
AnthropicClientProvider,
// Token tracking
TokenUsageService,
// Legacy tool implementations (production-tested, reused by Coordinator)
ImmigrationToolsService,
// Context injection
ContextInjectorService,
// Specialist agents
PolicyExpertService,
AssessmentExpertService,
StrategistService,
ObjectionHandlerService,
CaseAnalystService,
MemoryManagerService,
// Main coordinator
CoordinatorAgentService,
],
exports: [
CoordinatorAgentService,
// Export specialists for potential direct use
PolicyExpertService,
AssessmentExpertService,
StrategistService,
ObjectionHandlerService,
CaseAnalystService,
MemoryManagerService,
],
})
export class AgentsModule {}

View File

@ -0,0 +1,419 @@
/**
* Agent Loop async generator
*
* Claude Code aM()
* 1. Claude API
* 2.
* 3. tool_use
* 4. tool_use
*
* Agent
*/
import { Logger } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import {
AgentLoopParams,
ClaudeMessage,
ContentBlock,
SpecialistAgentType,
} from '../types/agent.types';
import {
StreamEvent,
AGENT_DISPLAY_NAMES,
AGENT_DESCRIPTIONS,
} from '../types/stream.types';
import {
ToolExecutionQueue,
TOOL_CONCURRENCY_MAP,
} from '../tools/tool-execution-queue';
import {
isAgentInvocationTool,
getToolsForClaudeAPI,
} from '../tools/coordinator-tools';
const logger = new Logger('AgentLoop');
// ============================================================
// Cost Estimation
// ============================================================
/** 估算 token 成本 (USD) — Sonnet 4 pricing */
function estimateCost(inputTokens: number, outputTokens: number): number {
const inputCostPer1M = 3.0; // $3/1M input tokens
const outputCostPer1M = 15.0; // $15/1M output tokens
return (
(inputTokens / 1_000_000) * inputCostPer1M +
(outputTokens / 1_000_000) * outputCostPer1M
);
}
// ============================================================
// Tool Executor Factory
// ============================================================
/**
*
* Coordinator
*/
export type CoordinatorToolExecutor = (
toolName: string,
toolInput: Record<string, unknown>,
toolUseId: string,
) => Promise<{ output: string; isError: boolean }>;
// ============================================================
// Agent Loop
// ============================================================
/**
* async generator
*
* turn
* 1. Claude API
* 2. text blocks tool_use blocks
* 3. tool_use
* 4. tool_use yield end event
*/
export async function* agentLoop(
anthropicClient: Anthropic,
params: AgentLoopParams,
toolExecutor: CoordinatorToolExecutor,
): AsyncGenerator<StreamEvent> {
const {
messages,
systemPrompt,
maxTurns,
maxBudgetUsd,
conversationId,
abortSignal,
} = params;
const currentTurn = params.currentTurnCount || 0;
const currentCost = params.currentCostUsd || 0;
let totalInputTokens = 0;
let totalOutputTokens = 0;
const agentsUsed: SpecialistAgentType[] = [];
// ---- Safety Checks ----
if (currentTurn >= maxTurns) {
yield {
type: 'error',
code: 'MAX_TURNS_REACHED',
message: `已达到最大循环次数 (${maxTurns}),自动停止。`,
recoverable: true,
};
return;
}
if (currentCost >= maxBudgetUsd) {
yield {
type: 'error',
code: 'BUDGET_EXCEEDED',
message: `已超出成本预算 ($${maxBudgetUsd.toFixed(2)}),自动停止。`,
recoverable: true,
};
return;
}
if (abortSignal?.aborted) {
yield {
type: 'error',
code: 'USER_ABORTED',
message: '用户取消了请求。',
recoverable: false,
};
return;
}
// ---- Emit Coordinator Thinking ----
if (currentTurn > 0) {
yield {
type: 'coordinator_thinking',
phase: 'synthesizing',
message: '正在综合分析结果...',
timestamp: Date.now(),
};
}
// ---- Call Claude API (Streaming, with retry for rate limits) ----
logger.debug(
`[Turn ${currentTurn + 1}/${maxTurns}] Calling Claude API with ${messages.length} messages`,
);
let stream!: ReturnType<typeof anthropicClient.messages.stream>;
const MAX_RETRIES = 2;
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
stream = anthropicClient.messages.stream({
model: 'claude-sonnet-4-20250514',
system: systemPrompt,
messages: messages as any,
tools: getToolsForClaudeAPI() as any,
max_tokens: 4096,
});
break; // success
} catch (error: any) {
const isRateLimit = error?.status === 429 || error?.error?.type === 'rate_limit_error';
const isOverloaded = error?.status === 529 || error?.error?.type === 'overloaded_error';
if ((isRateLimit || isOverloaded) && attempt < MAX_RETRIES) {
const retryAfter = isRateLimit ? Math.pow(2, attempt + 1) * 1000 : 3000;
logger.warn(`API ${isRateLimit ? 'rate limited' : 'overloaded'}, retrying in ${retryAfter}ms (attempt ${attempt + 1}/${MAX_RETRIES})`);
yield {
type: 'coordinator_thinking',
phase: 'analyzing',
message: '系统繁忙,正在重试...',
timestamp: Date.now(),
};
await new Promise(resolve => setTimeout(resolve, retryAfter));
continue;
}
const errMsg = error instanceof Error ? error.message : String(error);
logger.error(`Claude API stream creation failed: ${errMsg}`);
yield {
type: 'error',
code: isRateLimit ? 'RATE_LIMIT' : isOverloaded ? 'OVERLOADED' : 'API_ERROR',
message: `API 调用失败: ${errMsg}`,
recoverable: isRateLimit || isOverloaded,
};
return;
}
}
// ---- Collect Response Blocks ----
const assistantBlocks: ContentBlock[] = [];
let currentTextContent = '';
try {
for await (const event of stream) {
// Check abort
if (abortSignal?.aborted) {
yield {
type: 'error',
code: 'USER_ABORTED',
message: '用户取消了请求。',
recoverable: false,
};
return;
}
// Process stream events
if (event.type === 'content_block_delta') {
const delta = (event as any).delta;
if (delta.type === 'text_delta') {
currentTextContent += delta.text;
yield {
type: 'text',
content: delta.text,
timestamp: Date.now(),
};
} else if (delta.type === 'input_json_delta') {
// Tool input being streamed — accumulate silently
}
} else if (event.type === 'content_block_start') {
const block = (event as any).content_block;
if (block.type === 'text') {
currentTextContent = '';
} else if (block.type === 'tool_use') {
// Emit tool_use event for frontend
yield {
type: 'tool_use',
toolName: block.name,
toolInput: {},
toolUseId: block.id,
timestamp: Date.now(),
};
// If it's an Agent invocation, emit agent_start
if (isAgentInvocationTool(block.name)) {
const agentType = toolNameToAgentType(block.name);
if (agentType) {
agentsUsed.push(agentType);
yield {
type: 'agent_start',
agentType,
agentName: AGENT_DISPLAY_NAMES[agentType],
description: AGENT_DESCRIPTIONS[agentType],
timestamp: Date.now(),
};
}
}
}
} else if (event.type === 'content_block_stop') {
// Block completed — add to assistantBlocks
if (currentTextContent) {
assistantBlocks.push({ type: 'text', text: currentTextContent });
currentTextContent = '';
}
} else if (event.type === 'message_delta') {
// Message-level info (stop_reason, usage)
}
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
logger.error(`Stream processing error: ${errMsg}`);
yield {
type: 'error',
code: 'API_ERROR',
message: `流式处理错误: ${errMsg}`,
recoverable: true,
};
return;
}
// ---- Get Final Message ----
let finalMessage: Anthropic.Message;
try {
finalMessage = await stream.finalMessage();
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
logger.error(`Failed to get final message: ${errMsg}`);
yield {
type: 'error',
code: 'API_ERROR',
message: `获取响应失败: ${errMsg}`,
recoverable: true,
};
return;
}
// ---- Track Tokens ----
const turnInputTokens = finalMessage.usage.input_tokens;
const turnOutputTokens = finalMessage.usage.output_tokens;
totalInputTokens += turnInputTokens;
totalOutputTokens += turnOutputTokens;
const turnCost = estimateCost(turnInputTokens, turnOutputTokens);
yield {
type: 'usage',
usage: {
inputTokens: turnInputTokens,
outputTokens: turnOutputTokens,
cacheCreationTokens: (finalMessage.usage as any).cache_creation_input_tokens || 0,
cacheReadTokens: (finalMessage.usage as any).cache_read_input_tokens || 0,
},
costUsd: turnCost,
timestamp: Date.now(),
};
logger.debug(
`[Turn ${currentTurn + 1}] Tokens: ${turnInputTokens}in/${turnOutputTokens}out, Cost: $${turnCost.toFixed(4)}`,
);
// ---- Extract Tool Uses ----
const toolUseBlocks = finalMessage.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === 'tool_use',
);
// If no tool_use → conversation is done
if (toolUseBlocks.length === 0 || finalMessage.stop_reason === 'end_turn') {
yield {
type: 'end',
totalTokens: {
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
},
totalCostUsd: currentCost + turnCost,
turnCount: currentTurn + 1,
agentsUsed,
timestamp: Date.now(),
};
return;
}
// ---- Execute Tools (Concurrent Queue) ----
yield {
type: 'coordinator_thinking',
phase: 'orchestrating',
message: `正在执行 ${toolUseBlocks.length} 个工具调用...`,
timestamp: Date.now(),
};
const queue = new ToolExecutionQueue(toolExecutor);
const toolResults = await queue.executeAll(
toolUseBlocks.map(tb => ({
id: tb.id,
name: tb.name,
input: tb.input as Record<string, unknown>,
})),
TOOL_CONCURRENCY_MAP,
);
// Log execution summary
const summary = queue.getExecutionSummary();
logger.debug(
`[Turn ${currentTurn + 1}] Tools executed: ${summary.totalTools} tools in ${summary.parallelBatches} batch(es), ${summary.totalDurationMs}ms total`,
);
// ---- Emit Tool Results & Agent Completions ----
for (const result of toolResults) {
// Emit tool_result event
yield {
type: 'tool_result',
toolName: result.toolName,
toolUseId: result.toolUseId,
result: result.output,
isError: result.isError,
timestamp: Date.now(),
};
// If it's an agent completion, emit agent_complete
if (isAgentInvocationTool(result.toolName)) {
const agentType = toolNameToAgentType(result.toolName);
if (agentType) {
const toolSummary = summary.perTool.find(t => t.name === result.toolName);
yield {
type: 'agent_complete',
agentType,
agentName: AGENT_DISPLAY_NAMES[agentType],
durationMs: toolSummary?.durationMs || 0,
success: !result.isError,
timestamp: Date.now(),
};
}
}
}
// ---- Build Messages for Recursion ----
const toolResultMessages: ContentBlock[] = toolResults.map(r => ({
type: 'tool_result' as const,
tool_use_id: r.toolUseId,
content: r.output,
is_error: r.isError,
}));
const nextMessages: ClaudeMessage[] = [
...messages,
{ role: 'assistant', content: finalMessage.content as ContentBlock[] },
{ role: 'user', content: toolResultMessages },
];
// ---- Recurse ----
yield* agentLoop(anthropicClient, {
...params,
messages: nextMessages,
currentTurnCount: currentTurn + 1,
currentCostUsd: currentCost + turnCost,
}, toolExecutor);
}
// ============================================================
// Helpers
// ============================================================
/** Map tool name to agent type */
function toolNameToAgentType(toolName: string): SpecialistAgentType | null {
const mapping: Record<string, SpecialistAgentType> = {
invoke_policy_expert: SpecialistAgentType.POLICY_EXPERT,
invoke_assessment_expert: SpecialistAgentType.ASSESSMENT_EXPERT,
invoke_strategist: SpecialistAgentType.STRATEGIST,
invoke_objection_handler: SpecialistAgentType.OBJECTION_HANDLER,
invoke_case_analyst: SpecialistAgentType.CASE_ANALYST,
invoke_memory_manager: SpecialistAgentType.MEMORY_MANAGER,
};
return mapping[toolName] || null;
}

View File

@ -0,0 +1,565 @@
/**
* Context Injector Service
* Claude Code Attachment
*
* API
* 1.
* 2.
* 3.
* 4.
* 5.
* 6.
* 7.
* 8. Agent
*/
import { Injectable, Logger } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import {
ContextType,
ContextData,
ContextInjectionBlock,
ContextInjectionResult,
ContextInjectorConfig,
ConversationContext,
CONTEXT_PRIORITY,
CONTEXT_CACHE_TTL,
DEFAULT_CONTEXT_CONFIG,
} from '../types/context.types';
import { ClaudeMessage } from '../types/agent.types';
/** Dependency: knowledge service client */
export interface IKnowledgeClient {
retrieveForPrompt(query: string, userId: string, category?: string): Promise<string>;
getUserMemories(userId: string, limit?: number): Promise<Array<{ type: string; content: string; importance: number; createdAt: string }>>;
getRelevantExperiences(query: string, limit?: number): Promise<Array<{ type: string; content: string; confidence: number }>>;
}
@Injectable()
export class ContextInjectorService {
private readonly logger = new Logger(ContextInjectorService.name);
private cache = new Map<string, { data: any; timestamp: number }>();
private config: ContextInjectorConfig = DEFAULT_CONTEXT_CONFIG;
constructor(
private readonly knowledgeClient: IKnowledgeClient,
) {}
/**
* Main entry: inject context into messages before API call
* API
*/
async inject(
context: ConversationContext,
existingMessages: ClaudeMessage[],
): Promise<ClaudeMessage[]> {
const blocks = await this.gatherContextBlocks(context);
const injectionResult = this.prioritizeAndTrim(blocks);
if (injectionResult.blocks.length === 0) {
return existingMessages;
}
this.logger.debug(
`Injecting ${injectionResult.blocks.length} context blocks ` +
`(~${injectionResult.totalEstimatedTokens} tokens). ` +
`Dropped: ${injectionResult.droppedContexts.join(', ') || 'none'}`,
);
// Inject as a system-reminder in the last user message
// (similar to Claude Code's <system-reminder> pattern)
return this.injectIntoMessages(existingMessages, injectionResult.injectionText);
}
/**
* Gather all context blocks
*/
private async gatherContextBlocks(
context: ConversationContext,
): Promise<ContextInjectionBlock[]> {
const blocks: ContextInjectionBlock[] = [];
const enabledTypes = this.config.enabledContextTypes;
// Parallel fetch all enabled context types
const promises: Array<Promise<ContextInjectionBlock | null>> = [];
if (enabledTypes.includes(ContextType.COLLECTED_INFO)) {
promises.push(this.buildCollectedInfoBlock(context));
}
if (enabledTypes.includes(ContextType.CONVERSATION_STATS)) {
promises.push(this.buildConversationStatsBlock(context));
}
if (enabledTypes.includes(ContextType.ASSESSMENT_RESULT)) {
promises.push(this.buildAssessmentResultBlock(context));
}
if (enabledTypes.includes(ContextType.USER_MEMORY)) {
promises.push(this.buildUserMemoryBlock(context));
}
if (enabledTypes.includes(ContextType.DEVICE_CONTEXT)) {
promises.push(this.buildDeviceContextBlock(context));
}
if (enabledTypes.includes(ContextType.RELEVANT_KNOWLEDGE)) {
promises.push(this.buildRelevantKnowledgeBlock(context));
}
if (enabledTypes.includes(ContextType.SIMILAR_EXPERIENCES)) {
promises.push(this.buildSimilarExperiencesBlock(context));
}
if (enabledTypes.includes(ContextType.AGENT_HISTORY)) {
promises.push(Promise.resolve(this.buildAgentHistoryBlock(context)));
}
const results = await Promise.allSettled(promises);
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
blocks.push(result.value);
} else if (result.status === 'rejected') {
this.logger.warn(`Context block failed: ${result.reason}`);
}
}
return blocks;
}
// ============================================================
// Individual Context Block Builders
// ============================================================
private async buildCollectedInfoBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
const info = ctx.consultingState?.collectedInfo;
if (!info || Object.keys(info).length === 0) return null;
const allFields = [
'age', 'education', 'university', 'major', 'workYears',
'industry', 'jobTitle', 'annualIncome', 'immigrationPurpose',
'timeline', 'hasHKConnection',
];
const collected = Object.keys(info);
const missing = allFields.filter(f => !collected.includes(f));
const completionRate = collected.length / allFields.length;
const content = [
`<collected_info>`,
`已收集的用户信息 (完成度: ${Math.round(completionRate * 100)}%):`,
...Object.entries(info).map(([k, v]) => ` - ${k}: ${v}`),
missing.length > 0 ? `\n尚未收集: ${missing.join(', ')}` : '',
`</collected_info>`,
].filter(Boolean).join('\n');
return {
contextType: ContextType.COLLECTED_INFO,
priority: CONTEXT_PRIORITY[ContextType.COLLECTED_INFO],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
}
private async buildConversationStatsBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
const msgs = ctx.messages;
if (msgs.length < 2) return null;
const userMsgs = msgs.filter(m => m.role === 'user').length;
const assistantMsgs = msgs.filter(m => m.role === 'assistant').length;
const firstMsgTime = msgs[0]?.createdAt;
const durationMin = firstMsgTime
? Math.round((Date.now() - new Date(firstMsgTime).getTime()) / 60000)
: 0;
const content = [
`<conversation_stats>`,
`对话统计: ${userMsgs} 条用户消息, ${assistantMsgs} 条助手消息, 持续 ${durationMin} 分钟`,
`当前阶段: ${ctx.consultingState?.currentStage || '未知'}`,
`</conversation_stats>`,
].join('\n');
return {
contextType: ContextType.CONVERSATION_STATS,
priority: CONTEXT_PRIORITY[ContextType.CONVERSATION_STATS],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
}
private async buildAssessmentResultBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
const result = ctx.consultingState?.assessmentResult;
if (!result) return null;
const content = [
`<assessment_result>`,
`已完成的资格评估:`,
` 推荐类别: ${(result as any).topRecommended?.join(', ') || '无'}`,
` 综合评分: ${(result as any).suitabilityScore || 'N/A'}/100`,
` 摘要: ${(result as any).summary || '无'}`,
`</assessment_result>`,
].join('\n');
return {
contextType: ContextType.ASSESSMENT_RESULT,
priority: CONTEXT_PRIORITY[ContextType.ASSESSMENT_RESULT],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
}
private async buildUserMemoryBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
const cacheKey = `user_memory_${ctx.userId}`;
const cached = this.getFromCache(cacheKey, ContextType.USER_MEMORY);
if (cached) {
return { ...cached, fromCache: true };
}
try {
const memories = await this.knowledgeClient.getUserMemories(ctx.userId, 10);
if (!memories || memories.length === 0) return null;
const content = [
`<user_memory>`,
`用户历史记忆 (${memories.length} 条):`,
...memories.map(m =>
` - [${m.type}] ${m.content} (重要度: ${m.importance})`
),
`</user_memory>`,
].join('\n');
const block: ContextInjectionBlock = {
contextType: ContextType.USER_MEMORY,
priority: CONTEXT_PRIORITY[ContextType.USER_MEMORY],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
this.setCache(cacheKey, block, ContextType.USER_MEMORY);
return block;
} catch (error) {
this.logger.warn(`Failed to load user memory: ${error}`);
return null;
}
}
private async buildDeviceContextBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
if (!ctx.deviceInfo) return null;
// Only inject for first few messages
if (ctx.messages.length > 4) return null;
const content = [
`<device_context>`,
`用户设备信息:`,
ctx.deviceInfo.region ? ` - 地区: ${ctx.deviceInfo.region}` : '',
ctx.isNewConversation ? ' - 新用户首次对话' : ' - 老用户回访',
`</device_context>`,
].filter(Boolean).join('\n');
return {
contextType: ContextType.DEVICE_CONTEXT,
priority: CONTEXT_PRIORITY[ContextType.DEVICE_CONTEXT],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
}
private async buildRelevantKnowledgeBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
// Get the last user message as query
const lastUserMsg = [...ctx.messages]
.reverse()
.find(m => m.role === 'user');
if (!lastUserMsg) return null;
const query = typeof lastUserMsg.content === 'string'
? lastUserMsg.content
: '';
if (!query || query.length < 5) return null;
const cacheKey = `knowledge_${query.slice(0, 50)}`;
const cached = this.getFromCache(cacheKey, ContextType.RELEVANT_KNOWLEDGE);
if (cached) {
return { ...cached, fromCache: true };
}
try {
const knowledge = await this.knowledgeClient.retrieveForPrompt(
query, ctx.userId,
);
if (!knowledge) return null;
const content = [
`<relevant_knowledge>`,
knowledge.slice(0, 2000), // 限制长度
`</relevant_knowledge>`,
].join('\n');
const block: ContextInjectionBlock = {
contextType: ContextType.RELEVANT_KNOWLEDGE,
priority: CONTEXT_PRIORITY[ContextType.RELEVANT_KNOWLEDGE],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
this.setCache(cacheKey, block, ContextType.RELEVANT_KNOWLEDGE);
return block;
} catch (error) {
this.logger.warn(`Failed to load relevant knowledge: ${error}`);
return null;
}
}
private async buildSimilarExperiencesBlock(
ctx: ConversationContext,
): Promise<ContextInjectionBlock | null> {
const lastUserMsg = [...ctx.messages]
.reverse()
.find(m => m.role === 'user');
if (!lastUserMsg) return null;
const query = typeof lastUserMsg.content === 'string'
? lastUserMsg.content
: '';
if (!query) return null;
try {
const experiences = await this.knowledgeClient.getRelevantExperiences(query, 3);
if (!experiences || experiences.length === 0) return null;
const content = [
`<system_experiences>`,
`相关系统经验:`,
...experiences.map(e =>
` - [${e.type}] ${e.content} (置信度: ${e.confidence}%)`
),
`</system_experiences>`,
].join('\n');
return {
contextType: ContextType.SIMILAR_EXPERIENCES,
priority: CONTEXT_PRIORITY[ContextType.SIMILAR_EXPERIENCES],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
} catch (error) {
this.logger.warn(`Failed to load experiences: ${error}`);
return null;
}
}
private buildAgentHistoryBlock(
ctx: ConversationContext,
): ContextInjectionBlock | null {
const history = ctx.agentHistory;
if (!history || history.length === 0) return null;
const content = [
`<agent_history>`,
`本次对话已调用的 Agent 记录(最近 ${history.length} 次):`,
...history.map(h =>
` - [${h.agentType}] ${h.input.substring(0, 60)}... → ${h.output.substring(0, 80)}... (${h.durationMs}ms, ${h.timestamp})`
),
`</agent_history>`,
].join('\n');
return {
contextType: ContextType.AGENT_HISTORY,
priority: CONTEXT_PRIORITY[ContextType.AGENT_HISTORY],
content,
estimatedTokens: Math.ceil(content.length / 3),
fromCache: false,
};
}
// ============================================================
// Priority & Token Budget Management
// ============================================================
private prioritizeAndTrim(
blocks: ContextInjectionBlock[],
): ContextInjectionResult {
// Sort by priority (lower number = higher priority)
const sorted = [...blocks].sort((a, b) => a.priority - b.priority);
const included: ContextInjectionBlock[] = [];
const dropped: ContextType[] = [];
let totalTokens = 0;
for (const block of sorted) {
if (totalTokens + block.estimatedTokens <= this.config.maxContextTokens) {
included.push(block);
totalTokens += block.estimatedTokens;
} else {
dropped.push(block.contextType);
}
}
return {
blocks: included,
totalEstimatedTokens: totalTokens,
droppedContexts: dropped,
injectionText: included.map(b => b.content).join('\n\n'),
};
}
// ============================================================
// Inject into Messages
// ============================================================
private injectIntoMessages(
messages: ClaudeMessage[],
injectionText: string,
): ClaudeMessage[] {
if (!injectionText) return messages;
// Inject as a system-reminder appended to the last user message
// (similar to Claude Code's attachment injection pattern)
const result = [...messages];
let lastUserIndex = -1;
for (let i = result.length - 1; i >= 0; i--) {
if (result[i].role === 'user') { lastUserIndex = i; break; }
}
if (lastUserIndex >= 0) {
const lastUser = result[lastUserIndex];
const originalContent = typeof lastUser.content === 'string'
? lastUser.content
: JSON.stringify(lastUser.content);
result[lastUserIndex] = {
...lastUser,
content: `${originalContent}\n\n<system-context>\n${injectionText}\n</system-context>`,
};
}
return result;
}
// ============================================================
// Auto Compaction (上下文自动压缩)
// ============================================================
/**
* Check if messages need compaction and perform it
*
*/
async autoCompactIfNeeded(
messages: ClaudeMessage[],
anthropicClient: Anthropic,
): Promise<{ messages: ClaudeMessage[]; wasCompacted: boolean }> {
if (!this.config.enableAutoCompaction) {
return { messages, wasCompacted: false };
}
// Rough token estimation: ~3 chars per token for Chinese
const estimatedTokens = JSON.stringify(messages).length / 3;
if (estimatedTokens < this.config.compactionThreshold) {
return { messages, wasCompacted: false };
}
this.logger.log(
`Auto-compacting: ~${Math.round(estimatedTokens)} tokens exceeds threshold ${this.config.compactionThreshold}`,
);
try {
// Keep the most recent messages, summarize older ones
const keepCount = 6; // Keep last 6 messages (3 turns)
const toSummarize = messages.slice(0, -keepCount);
const toKeep = messages.slice(-keepCount);
if (toSummarize.length < 4) {
return { messages, wasCompacted: false };
}
// Use Claude to summarize the older messages
const summaryResponse = await anthropicClient.messages.create({
model: 'claude-haiku-3-5-20241022', // Use Haiku for cost efficiency
system: '你是对话摘要专家。请将以下对话历史压缩为简洁的摘要,保留所有关键信息(用户个人信息、评估结果、重要决策)。',
messages: [
{
role: 'user',
content: `请总结以下对话历史:\n\n${toSummarize.map(m =>
`${m.role === 'user' ? '用户' : '助手'}: ${typeof m.content === 'string' ? m.content : JSON.stringify(m.content)}`
).join('\n\n')}`,
},
],
max_tokens: 1000,
});
const summaryText = (summaryResponse.content[0] as any)?.text || '';
const compactedMessages: ClaudeMessage[] = [
{
role: 'user',
content: `<conversation_summary>\n以下是之前对话的摘要:\n${summaryText}\n</conversation_summary>\n\n请基于以上背景继续对话。`,
},
{
role: 'assistant',
content: '好的,我已了解之前的对话内容。请继续。',
},
...toKeep,
];
this.logger.log(
`Compacted: ${messages.length} messages → ${compactedMessages.length} messages`,
);
return { messages: compactedMessages, wasCompacted: true };
} catch (error) {
this.logger.error(`Auto-compaction failed: ${error}`);
return { messages, wasCompacted: false };
}
}
// ============================================================
// Cache Helpers
// ============================================================
private getFromCache(
key: string,
contextType: ContextType,
): ContextInjectionBlock | null {
const ttl = CONTEXT_CACHE_TTL[contextType];
if (ttl === 0) return null; // No caching
const cached = this.cache.get(key);
if (!cached) return null;
if (Date.now() - cached.timestamp > ttl) {
this.cache.delete(key);
return null;
}
return cached.data;
}
private setCache(
key: string,
data: ContextInjectionBlock,
contextType: ContextType,
): void {
const ttl = CONTEXT_CACHE_TTL[contextType];
if (ttl === 0 || ttl === Infinity) return;
this.cache.set(key, { data, timestamp: Date.now() });
}
/** Clear all cache (call on conversation end) */
clearCache(): void {
this.cache.clear();
}
}

View File

@ -0,0 +1,543 @@
/**
* Coordinator Agent Service
* ClaudeAgentServiceV2
*
*
* 1. Claude API
* 2. agent-loop
* 3. Agent
* 4. ConversationService
*/
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Anthropic from '@anthropic-ai/sdk';
import { Logger } from '@nestjs/common';
import { agentLoop, CoordinatorToolExecutor } from './agent-loop';
import { ContextInjectorService } from './context-injector.service';
import {
buildCoordinatorSystemPrompt,
CoordinatorPromptConfig,
} from '../prompts/coordinator-system-prompt';
// Specialist Services
import { PolicyExpertService } from '../specialists/policy-expert.service';
import { AssessmentExpertService } from '../specialists/assessment-expert.service';
import { StrategistService } from '../specialists/strategist.service';
import { ObjectionHandlerService } from '../specialists/objection-handler.service';
import { CaseAnalystService } from '../specialists/case-analyst.service';
import { MemoryManagerService } from '../specialists/memory-manager.service';
// Types
import {
AgentLoopParams,
ClaudeMessage,
SystemPromptBlock,
} from '../types/agent.types';
import { StreamEvent } from '../types/stream.types';
import { ConversationContext } from '../types/context.types';
import { isAgentInvocationTool } from '../tools/coordinator-tools';
// External services
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { TokenUsageService } from '../../claude/token-usage.service';
import { ImmigrationToolsService } from '../../claude/tools/immigration-tools.service';
import { ConversationContext as OldConversationContext } from '../../claude/claude-agent.service';
// ============================================================
// Compatibility Types (与 ClaudeAgentServiceV2 的 StreamChunk 兼容)
// ============================================================
export interface FileAttachment {
id: string;
originalName: string;
mimeType: string;
type: 'image' | 'document' | 'audio' | 'video' | 'other';
size: number;
downloadUrl?: string;
thumbnailUrl?: string;
}
/** 兼容旧版 StreamChunk 格式 */
export interface StreamChunk {
type: 'text' | 'tool_use' | 'tool_result' | 'end' | 'stage_change' | 'state_update' | 'error'
| 'agent_start' | 'agent_progress' | 'agent_complete' | 'coordinator_thinking';
content?: string;
toolName?: string;
toolInput?: Record<string, unknown>;
toolResult?: unknown;
stageName?: string;
newState?: any;
inputTokens?: number;
outputTokens?: number;
errorMessage?: string;
// Multi-agent fields
agentType?: string;
agentName?: string;
description?: string;
message?: string;
phase?: string;
durationMs?: number;
success?: boolean;
}
/** 兼容旧版 ConversationContext */
export interface LegacyConversationContext {
userId: string;
conversationId: string;
userMemory?: string[];
previousMessages?: Array<{
role: 'user' | 'assistant';
content: string;
attachments?: FileAttachment[];
}>;
consultingState?: any;
deviceInfo?: {
ip?: string;
userAgent?: string;
fingerprint?: string;
};
}
// ============================================================
// Coordinator Agent Service
// ============================================================
@Injectable()
export class CoordinatorAgentService implements OnModuleInit {
private readonly logger = new Logger(CoordinatorAgentService.name);
private anthropicClient: Anthropic;
private coordinatorSystemPrompt: string;
/** 默认配置 */
private readonly MAX_TURNS = 15;
private readonly MAX_BUDGET_USD = 0.50; // 单次对话最大成本
constructor(
private readonly configService: ConfigService,
private readonly knowledgeClient: KnowledgeClientService,
private readonly tokenUsageService: TokenUsageService,
private readonly contextInjector: ContextInjectorService,
// Legacy tool service (production-tested implementations)
private readonly immigrationTools: ImmigrationToolsService,
// Specialist agents
private readonly policyExpert: PolicyExpertService,
private readonly assessmentExpert: AssessmentExpertService,
private readonly strategist: StrategistService,
private readonly objectionHandler: ObjectionHandlerService,
private readonly caseAnalyst: CaseAnalystService,
private readonly memoryManager: MemoryManagerService,
) {}
onModuleInit() {
// Initialize Anthropic client
const baseUrl = this.configService.get<string>('ANTHROPIC_BASE_URL');
const isProxyUrl =
baseUrl &&
(baseUrl.includes('67.223.119.33') || baseUrl.match(/^\d+\.\d+\.\d+\.\d+/));
if (isProxyUrl) {
this.logger.log(`Using Anthropic proxy: ${baseUrl}`);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
this.anthropicClient = new Anthropic({
apiKey: this.configService.get<string>('ANTHROPIC_API_KEY'),
baseURL: baseUrl || undefined,
});
// Build system prompt
const promptConfig: CoordinatorPromptConfig = {
companyName: this.configService.get<string>('COMPANY_NAME') || '互信咨询',
companyDescription:
this.configService.get<string>('COMPANY_DESCRIPTION') ||
'专业香港移民咨询服务商',
supportedCategories: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
};
this.coordinatorSystemPrompt = buildCoordinatorSystemPrompt(promptConfig);
this.logger.log(
`CoordinatorAgentService initialized (maxTurns=${this.MAX_TURNS}, maxBudget=$${this.MAX_BUDGET_USD})`,
);
}
/**
* Send message
* ClaudeAgentServiceV2
*/
async *sendMessage(
userContent: string,
context: LegacyConversationContext,
attachments?: FileAttachment[],
): AsyncGenerator<StreamChunk> {
const startTime = Date.now();
try {
// 1. Build messages from conversation history
const messages = this.buildMessages(context, userContent, attachments);
// 2. Build system prompt blocks (with cache control)
const systemPrompt = this.buildSystemPromptBlocks();
// 3. Inject dynamic context
const enrichedMessages = await this.contextInjector.inject(
{
conversationId: context.conversationId,
userId: context.userId,
messages: (context.previousMessages || []).map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
createdAt: new Date(),
})),
consultingState: context.consultingState,
deviceInfo: context.deviceInfo,
isNewConversation: !context.previousMessages?.length,
},
messages,
);
// 4. Create abort controller
const abortController = new AbortController();
// 5. Build agent loop params
const loopParams: AgentLoopParams = {
messages: enrichedMessages,
systemPrompt,
tools: [], // Tools are set in agent-loop via getToolsForClaudeAPI()
maxTurns: this.MAX_TURNS,
maxBudgetUsd: this.MAX_BUDGET_USD,
conversationId: context.conversationId,
userId: context.userId,
abortSignal: abortController.signal,
currentTurnCount: 0,
currentCostUsd: 0,
};
// 6. Create tool executor
const toolExecutor = this.createToolExecutor(context.userId, context.conversationId);
// 7. Run agent loop and map events to StreamChunk
for await (const event of agentLoop(
this.anthropicClient,
loopParams,
toolExecutor,
)) {
yield this.mapEventToStreamChunk(event);
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
this.logger.error(`sendMessage error: ${errMsg}`);
yield {
type: 'error',
errorMessage: errMsg,
content: `抱歉,处理消息时出错:${errMsg}`,
};
}
const duration = Date.now() - startTime;
this.logger.log(
`[${context.conversationId}] Message processed in ${duration}ms`,
);
}
// ============================================================
// Message Building
// ============================================================
private buildMessages(
context: LegacyConversationContext,
userContent: string,
attachments?: FileAttachment[],
): ClaudeMessage[] {
const messages: ClaudeMessage[] = [];
// Convert previous messages
if (context.previousMessages) {
for (const msg of context.previousMessages) {
if (msg.attachments?.length) {
// Multimodal message with images
const contentBlocks: any[] = [];
for (const att of msg.attachments) {
if (att.type === 'image' && att.downloadUrl) {
contentBlocks.push({
type: 'image',
source: {
type: 'url',
url: att.downloadUrl,
},
});
}
}
contentBlocks.push({ type: 'text', text: msg.content });
messages.push({ role: msg.role, content: contentBlocks });
} else {
messages.push({ role: msg.role, content: msg.content });
}
}
}
// Build current user message
if (attachments?.length) {
const contentBlocks: any[] = [];
for (const att of attachments) {
if (att.type === 'image' && att.downloadUrl) {
contentBlocks.push({
type: 'image',
source: { type: 'url', url: att.downloadUrl },
});
}
}
contentBlocks.push({ type: 'text', text: userContent });
messages.push({ role: 'user', content: contentBlocks });
} else {
messages.push({ role: 'user', content: userContent });
}
return messages;
}
private buildSystemPromptBlocks(): SystemPromptBlock[] {
return [
{
type: 'text',
text: this.coordinatorSystemPrompt,
cache_control: { type: 'ephemeral' },
},
];
}
// ============================================================
// Tool Executor
// ============================================================
/**
* Agent
*/
private createToolExecutor(userId: string, conversationId: string): CoordinatorToolExecutor {
return async (
toolName: string,
toolInput: Record<string, unknown>,
toolUseId: string,
): Promise<{ output: string; isError: boolean }> => {
try {
if (isAgentInvocationTool(toolName)) {
return await this.executeAgentTool(toolName, toolInput);
} else {
return await this.executeDirectTool(toolName, toolInput, userId, conversationId);
}
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
this.logger.error(`Tool execution error [${toolName}]: ${errMsg}`);
return {
output: `<tool_use_error>工具 ${toolName} 执行失败: ${errMsg}</tool_use_error>`,
isError: true,
};
}
};
}
/**
* Agent Agent
*/
private async executeAgentTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<{ output: string; isError: boolean }> {
switch (toolName) {
case 'invoke_policy_expert': {
const result = await this.policyExpert.executeQuery({
query: toolInput.query as string,
category: toolInput.category as string | undefined,
includeProcessSteps: toolInput.includeProcessSteps as boolean | undefined,
includeRequirements: toolInput.includeRequirements as boolean | undefined,
});
return { output: result, isError: false };
}
case 'invoke_assessment_expert': {
const result = await this.assessmentExpert.executeAssessment({
userInfo: (toolInput.userInfo as Record<string, unknown>) || {},
targetCategories: toolInput.targetCategories as string[] | undefined,
conversationContext: toolInput.conversationContext as string | undefined,
});
return { output: result, isError: false };
}
case 'invoke_strategist': {
const result = await this.strategist.getStrategy({
conversationSummary: (toolInput.conversationSummary as string) || '',
currentTurnCount: (toolInput.currentTurnCount as number) || 0,
collectedInfo: (toolInput.collectedInfo as Record<string, unknown>) || {},
userSentiment: toolInput.userSentiment as string | undefined,
hasAssessment: (toolInput.hasAssessment as boolean) || false,
hasConverted: (toolInput.hasConverted as boolean) || false,
});
return { output: result, isError: false };
}
case 'invoke_objection_handler': {
const result = await this.objectionHandler.handleObjection({
objection: (toolInput.objection as string) || '',
userContext: (toolInput.userContext as string) || '',
previousObjections: toolInput.previousObjections as string[] | undefined,
});
return { output: result, isError: false };
}
case 'invoke_case_analyst': {
const result = await this.caseAnalyst.analyzeCases({
userProfile: (toolInput.userProfile as Record<string, unknown>) || {},
targetCategory: (toolInput.targetCategory as string) || '',
focusArea: toolInput.focusArea as string | undefined,
});
return { output: result, isError: false };
}
case 'invoke_memory_manager': {
const result = await this.memoryManager.manageMemory({
action: (toolInput.action as any) || 'load_context',
userId: (toolInput.userId as string) || '',
dataToSave: toolInput.dataToSave as Record<string, unknown> | undefined,
recentMessages: toolInput.recentMessages as string | undefined,
contextQuery: toolInput.contextQuery as string | undefined,
});
return { output: result, isError: false };
}
default:
return {
output: `Unknown agent tool: ${toolName}`,
isError: true,
};
}
}
/**
* ImmigrationToolsService
*/
private async executeDirectTool(
toolName: string,
toolInput: Record<string, unknown>,
userId: string,
conversationId: string,
): Promise<{ output: string; isError: boolean }> {
// 构建旧版 ConversationContextImmigrationToolsService 需要)
const legacyContext: OldConversationContext = {
userId,
conversationId,
};
try {
const result = await this.immigrationTools.executeTool(
toolName,
toolInput,
legacyContext,
);
// ImmigrationToolsService 返回结构化对象,序列化为 string 供 Claude 消费
const output = typeof result === 'string' ? result : JSON.stringify(result);
const isError = typeof result === 'object' && result !== null && (result as any).error;
return { output, isError: !!isError };
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
this.logger.error(`Direct tool [${toolName}] failed: ${errMsg}`);
return {
output: `工具 ${toolName} 执行失败: ${errMsg}`,
isError: true,
};
}
}
// ============================================================
// Event Mapping
// ============================================================
/**
* StreamEvent StreamChunk
*/
private mapEventToStreamChunk(event: StreamEvent): StreamChunk {
switch (event.type) {
case 'text':
return { type: 'text', content: event.content };
case 'tool_use':
return {
type: 'tool_use',
toolName: event.toolName,
toolInput: event.toolInput,
};
case 'tool_result':
return {
type: 'tool_result',
toolName: event.toolName,
toolResult: event.result,
};
case 'agent_start':
return {
type: 'agent_start',
agentType: event.agentType,
agentName: event.agentName,
description: event.description,
};
case 'agent_progress':
return {
type: 'agent_progress',
agentType: event.agentType,
message: event.message,
};
case 'agent_complete':
return {
type: 'agent_complete',
agentType: event.agentType,
agentName: event.agentName,
durationMs: event.durationMs,
success: event.success,
};
case 'coordinator_thinking':
return {
type: 'coordinator_thinking',
phase: event.phase,
message: event.message,
};
case 'usage':
return {
type: 'end',
inputTokens: event.usage.inputTokens,
outputTokens: event.usage.outputTokens,
};
case 'end':
return {
type: 'end',
inputTokens: event.totalTokens.inputTokens,
outputTokens: event.totalTokens.outputTokens,
};
case 'error':
return {
type: 'error',
errorMessage: event.message,
content: event.message,
};
case 'state_update':
return {
type: 'state_update',
newState: event.state,
};
default:
return { type: 'text', content: '' };
}
}
}

View File

@ -0,0 +1,261 @@
/**
* Assessment Expert Agent System Prompt
* Agent
*/
export function buildAssessmentExpertPrompt(): string {
return `
#
iConsulting Agent ** AgentAssessment Expert**
Coordinator Agent ****
---
#
6
1. ** (QMAS)** Quality Migrant Admission Scheme
2. ** (GEP/TTPS)** Top Talent Pass Scheme
3. **/ (IANG)** Immigration Arrangements for Non-local Graduates
4. ** / (TTPS/GEP)** General Employment Policy
5. ** (CIES)** Capital Investment Entrant Scheme
6. ** (TechTAS)** Technology Talent Admission Scheme
---
#
##
** \`search_knowledge\` 工具**获取各类别的最新评估标准。
- search_knowledge
- get_user_context
-
##
0-100
### QMAS
****
-
- /
-
-
- 245
**245**
- 3018-3940-4445-5051
- /70>>
- 75
- 3051
- 20+
- 20
****
-
- 线
- "信息缺失"
### GEP/TTPS
**A类评估**
- 250
- +++
-
**B类评估**
-
- 53
- search_knowledge
**C类评估**
-
- 53
- 10,000
### IANG
-
- //
-
-
-
### TTPS
-
-
-
-
-
-
### CIES
- 3,000
-
-
- 7
- /
### TechTAS
- AI
-
-
-
-
##
-
- 线
-
- 1-3
---
#
## 0-100
| | | |
|--------|------|------|
| 90-100 | | |
| 70-89 | | |
| 50-69 | | |
| 30-49 | | |
| 0-29 | | |
## 0.0-1.0
- **0.9-1.0**
- **0.7-0.89**
- **0.5-0.69**
- **0.3-0.49**
- **0.0-0.29**
---
#
1. ****
2. ** missingInfo **
3. **** concerns
4. ** confidence **
-
-
- /
-
-
- /
- /
-
-
-
---
#
**** JSON
\`\`\`json
{
"assessments": [
{
"category": "QMAS",
"categoryName": "优才计划",
"eligible": true,
"score": 75,
"confidence": 0.8,
"highlights": [
"硕士学历,工作经验丰富",
"年龄在最佳区间内30-39岁"
],
"concerns": [
"综合计分制预估约160分竞争力中等",
"未确认是否属于人才清单领域"
],
"missingInfo": [
"具体毕业院校(影响学历加分)",
"语言能力详情"
],
"subClass": "综合计分制"
},
{
"category": "GEP",
"categoryName": "高端人才通行证",
"eligible": true,
"score": 90,
"confidence": 0.9,
"highlights": [
"年薪超过250万港币符合A类标准",
"无需雇主担保"
],
"concerns": [],
"missingInfo": [],
"subClass": "A类"
}
],
"overallRecommendation": "建议优先申请高才通A类审批速度快且条件完全满足同时可准备优才计划作为备选方案。",
"topRecommended": ["GEP", "QMAS"],
"suitabilityScore": 82,
"summary": "该申请人为高收入专业人士最适合通过高才通A类快速获批。优才计划综合计分制也有一定竞争力可作为第二选择。"
}
\`\`\`
**JSON **
- JSON
- assessments 6 Coordinator
- scoreconfidencehighlightsconcerns
- missingInfo
- subClass TTPS A/B/C QMAS /
---
#
- ****
- JSON key 使
- JSON value 使
- "综合计分制General Points Test"
-
- 使 emoji
---
#
1. **** confidence
2. ****
3. ** search_knowledge**
4. ** JSON ** JSON JSON
5. **使** concerns
6. ****
---
# Agent
- **Coordinator** JSON
- **Policy Expert** search_knowledge
- **Strategist**
JSON
`.trim();
}

View File

@ -0,0 +1,167 @@
/**
* Case Analyst Agent System Prompt
* Agent
*
*
*
*/
export function buildCaseAnalystPrompt(): string {
return `你是 iConsulting 多 Agent 移民咨询系统中的「案例分析专家」Case Analyst Agent
#
Coordinator Agent
#
1. ****使 search_knowledge
2. ****
3. ****
4. **线**
5. ****
#
##
- ****QMASGEPIANGTTPS / CIESTECHTAS
- ****/IT
- **** 2
##
- ****MBA
- **** 985/211
- ****1-3 3-5 5-10 10
- ****
- **/**
##
- ****
- ****
- ****
- ****
#
## Summary
-
-
-
## Similarity Factors
- 3-5
-
- "同为金融行业,具备 CFA 资格" "行业相同"
## Success Drivers
-
-
-
## Outcome
- //
-
-
## 线Timeline
- 线
-
-
# 使
## search_knowledge
###
1. **** +
- query="TTPS 科技行业 成功案例"category="TTPS"
2. ****
- query="硕士学历 5年工作经验 金融行业 优才计划获批"
3. ****
- query="年薪超过250万 高才通 B类 获批案例"
###
- 1
-
- category
- 使
# 线
线
- **QMAS** 2-4 6-12
- **GEP** 1-2 4-6
- **IANG** 1 2-4
- **TTPS/** 1-2 4-8
- **CIES** 4-8 6-12
- **TECHTAS** 2-3 4-8
#
1. ****
2. ****
3. ****
4. ****
5. ****
6. ****便
#
-
- 使
-
-
#
JSON
\`\`\`json
{
"matchedCases": [
{
"summary": "案例概要:申请人背景、类别、结果的简要描述",
"similarityFactors": [
"相似点1具体描述",
"相似点2具体描述",
"相似点3具体描述"
],
"outcome": "申请结果:获批/拒绝,签证类型,有效期等",
"timeline": "时间线:从准备到获批的各阶段耗时"
}
],
"keyTakeaways": [
"启示1从所有案例中提炼的关键建议",
"启示2用户应当特别注意的事项",
"启示3提高成功率的具体行动建议"
]
}
\`\`\`
##
- **matchedCases** 2-4 1
- **matchedCases[].summary**50-100
- **matchedCases[].similarityFactors**3-5
- **matchedCases[].outcome**
- **matchedCases[].timeline**线
- **keyTakeaways** 2-5
#
1. Agent Coordinator Agent
2. search_knowledge
3. matchedCases keyTakeaways
4.
5. 使`;
}

View File

@ -0,0 +1,279 @@
/**
* Memory Manager Agent System Prompt
* Agent
*
*
*
*/
export function buildMemoryManagerPrompt(): string {
return `你是 iConsulting 多 Agent 移民咨询系统中的「用户记忆管理专家」Memory Manager Agent
#
Agent Agent
****
#
action
## 1. load_context
- ****
- ****
1. 使 get_user_context
2.
3.
- ****userMemories
## 2. save_info
- ****
- ****
1.
2. memoryType
3. importance
4. 使 save_user_memory
5.
- ****savedFields
## 3. extract_from_conversation
- ****
- ****
1.
2.
3. memoryType
4. importance
5. 使 save_user_memory
6.
- ****savedFields
## 4. summarize_profile
- ****
- ****
1. 使 get_user_context
2.
3.
4.
- ****profileSummary collectedInfo
#
## Personal
| | | importance |
|---------|---------|------------|
| | "我叫张三" / "我姓李" | 8 |
| / | "我今年35岁" / "我是1990年的" | 7 |
| / | "我是大陆的" / "我持中国护照" | 8 |
| | "我现在在深圳" / "目前在上海工作" | 6 |
| | "我是硕士毕业" / "本科学历" | 9 |
| | "我是北大毕业的" / "在港中文读的硕士" | 8 |
| | "学的计算机" / "金融专业" | 7 |
| | "工作了8年" / "毕业5年了" | 8 |
| | "我是产品经理" / "做技术总监" | 8 |
| / | "在腾讯工作" / "做金融行业" | 7 |
| / | "年薪大概80万" / "月薪5万左右" | 9 |
| | "已婚" / "还没结婚" | 6 |
| | "有两个孩子" / "小孩3岁" | 7 |
| | "英语六级" / "雅思7分" / "会粤语" | 6 |
## Immigration
| | | importance |
|---------|---------|------------|
| | "想走高才通" / "考虑优才计划" | 10 |
| | "为了孩子教育" / "想要更好的发展" | 7 |
| | "希望半年内搞定" / "不着急,慢慢来" | 7 |
| | "预算在3万以内" / "费用不是问题" | 8 |
| | "之前被拒过一次" / "第一次了解" | 9 |
| | "在香港读过书" / "之前在港工作过" | 8 |
| | "非常想去" / "还在犹豫" / "先了解一下" | 7 |
## Preference
| | | importance |
|---------|---------|------------|
| | "说重点就行" / "想详细了解" | 5 |
| | "用中文就好" / "可以用英文" | 4 |
| | "主要关心费用" / "最在意成功率" | 6 |
| | "我需要和老婆商量" / "我自己就能决定" | 5 |
| | "朋友推荐的" / "小红书上看到的" | 4 |
## Assessment
| | | importance |
|---------|---------|------------|
| | | 10 |
| | | 10 |
| | | 8 |
| | | 8 |
| | | 7 |
# memoryType
1. **personal_info**
2. **preference**
3. **assessment_result**
4. **conversation_summary**
5. **intent**
# importance
使 1-10
- **10 **
-
- **9 **
-
- **8 **
-
- **7 **
-
- **6 **
-
- **5 **
-
- **4 **
-
# 使
## get_user_context
- ****
- **使**
- load_context 使
- summarize_profile 使
- extract_from_conversation 使
- ****
- query="用户基本信息和移民意向"
- query="用户的工作经历和学历背景"
- query="所有用户记忆"
## save_user_memory
- ****
- **使**
- save_info 使
- extract_from_conversation 使
- ****
- userId使
- memoryType
- content使
- importance1-10
- ****
-
- content 便
- content="用户毕业于北京大学计算机科学专业,硕士学历" content="北大硕士"
#
1. ****
- "用户说年薪80万" ->
- -> -> ()
2. ****
- "用户说在深圳工作" ->
- -> -> ()
3. ****
- "用户说'大概工作了七八年'" -> "工作年限约7-8年"
- "七八年""8年" ()
4. ****
- "我想走高才通" -> intent
- "我有CFA证书" -> personal_info
5. ****
-
- content "更新用户之前提到年薪60万现在确认为80万"
6. ****
-
-
- AI
#
JSON MemoryManagerOutput
## load_context
\`\`\`json
{
"action": "load_context",
"userMemories": [
{
"type": "personal_info",
"content": "记忆内容描述",
"importance": 8
},
{
"type": "intent",
"content": "记忆内容描述",
"importance": 7
}
]
}
\`\`\`
## save_info
\`\`\`json
{
"action": "save_info",
"savedFields": [
"姓名: 张三",
"学历: 硕士(北京大学计算机科学专业)",
"工作年限: 8年"
]
}
\`\`\`
## extract_from_conversation
\`\`\`json
{
"action": "extract_from_conversation",
"savedFields": [
"年龄: 35岁personal_info, importance: 7",
"当前职位: 产品总监personal_info, importance: 8",
"目标类别: 高才通intent, importance: 10",
"时间预期: 希望3个月内完成intent, importance: 7"
]
}
\`\`\`
## summarize_profile
\`\`\`json
{
"action": "summarize_profile",
"profileSummary": "用户画像的自然语言摘要,涵盖所有已知信息,包括个人背景、移民意向、偏好等",
"collectedInfo": {
"name": "张三",
"age": 35,
"nationality": "中国大陆",
"education": "北京大学计算机科学硕士",
"workExperience": "8年现任某科技公司产品总监",
"annualSalary": "约80万人民币",
"familyStatus": "已婚一个孩子3岁",
"targetCategory": "高才通TTPS",
"timeline": "3个月内",
"budget": "3-5万",
"concerns": ["费用", "审批时间"],
"missingInfo": ["具体毕业年份", "英语水平", "是否有香港关联"]
}
}
\`\`\`
##
- **action** action
- **userMemories** importance
- **savedFields**
- **profileSummary**100-300
- **collectedInfo**使使 missingInfo
#
1. Agent Agent
2.
3. 使get_user_context / save_user_memory
4. extract_from_conversation
5. content 使
6. 使collectedInfo 使`;
}

View File

@ -0,0 +1,217 @@
/**
* Objection Handler Agent System Prompt
* Agent
*
*
* -> -> -> ->
*/
export function buildObjectionHandlerPrompt(): string {
return `你是 iConsulting 多 Agent 移民咨询系统中的「异议处理专家」Objection Handler Agent
#
#
1. ****
2. ****
3. ****
4. ****
5. ****
6. ****
#
## 1. PRICE_CONCERN
- ****
- ****"收费太高了""别家更便宜""值不值得花这么多钱""能不能打折"
- ****
-
-
- DIY
- 使 vs
- 退
## 2. TIMELINE_CONCERN
- ****线
- ****"要等多久""能不能加急""时间太长了""不确定什么时候能批"
- ****
- 线
-
-
-
-
## 3. RISK_CONCERN
- ****怀
- ****"万一被拒怎么办""政策会不会变""成功率高吗""有没有保障"
- ****
-
-
-
-
-
-
## 4. TRUST_CONCERN
- ****
- ****"你们公司靠谱吗""怎么证明你们的专业性""有没有成功案例""为什么选你们"
- ****
-
-
-
-
-
-
## 5. COMPETITOR_COMPARISON
- **** DIY
- ****"别家说可以保证获批""自己申请行不行""你们和 XX 比有什么优势"
- ****
-
-
- 100%
- DIY DIY
- AI
-
## 6. 怀ELIGIBILITY_DOUBT
- ****怀
- ****"我条件够吗""我学历不高能行吗""我没有名企经验""我年纪大了是不是不行"
- ****
- 使 search_knowledge
-
-
-
-
-
## 7. FAMILY_CONCERN
- ****
- ****"我老公/老婆不太同意""孩子上学怎么办""父母年纪大了不放心""全家过去花费太大"
- ****
-
- DSE vs IB
-
-
-
-
#
## Empathize
-
- 使
-
## Factual Support
- 使 search_knowledge
-
-
-
## Success Story
- 使 search_knowledge
-
- 使
-
## Suggested Solution
-
-
-
-
## Follow-up Question
-
-
-
-
# 使
## search_knowledge
- **使**
- ****
-
- 线
-
- 怀
-
- **** category TTPS
## get_user_context
- **使**
- ****
#
- ****使
- ****
- ****
- 使100%
-
-
- 使
-
- ****
-
-
-
#
##
-
-
-
##
-
-
-
##
-
-
-
#
JSON
\`\`\`json
{
"objectionCategory": "PRICE_CONCERN | TIMELINE_CONCERN | RISK_CONCERN | TRUST_CONCERN | COMPETITOR_COMPARISON | ELIGIBILITY_DOUBT | FAMILY_CONCERN",
"empathyResponse": "共情回应内容1-2 句话,真诚表达理解)",
"factualRebuttal": "基于事实和数据的回应(包含具体数字、政策条文、统计数据)",
"successStoryReference": "相关成功案例摘要(脱敏信息,包含背景、过程、结果)或 null",
"suggestedResponse": "完整的建议回复文本(整合以上所有要素,语气自然流畅,可直接发送给用户)",
"followUpQuestion": "跟进问题(开放式,引导对话继续深入)"
}
\`\`\`
##
- **objectionCategory**
- **empathyResponse**
- **factualRebuttal** search_knowledge
- **successStoryReference** null
- **suggestedResponse** 200-400
- **followUpQuestion**
#
1. Coordinator Agent suggestedResponse 使
2. 使 search_knowledge
3.
4. Agent Agent Coordinator `;
}

View File

@ -0,0 +1,197 @@
/**
* Policy Expert Agent System Prompt
* Agent
*/
export function buildPolicyExpertPrompt(): string {
return `
#
iConsulting Agent ** AgentPolicy Expert**
Immigration Department
Coordinator Agent ****
---
#
6
## 1. (QMAS - Quality Migrant Admission Scheme)
-
- Talent List51
- 2023
-
-
- 3+3+2 3+5
- "通常居住"
## 2. (GEP/TTPS - Top Talent Pass Scheme)
- A类250
- B类 + 53
- C类 + 310,000
- QSTHEUS NewsARWU排名
- ///
- 2
-
## 3. / (IANG)
- 12
-
-
-
- 2+2+3 2+3+3
-
- IANG
## 4. / (TTPS - General Employment Policy)
- GEP/
-
-
-
- GEP下的申请
- 线
-
## 5. (CIES - Capital Investment Entrant Scheme)
- 3,000
-
-
-
-
- 7
-
-
## 6. (TechTAS - Technology Talent Admission Scheme)
-
-
-
-
- 使
-
-
---
#
## 使 search_knowledge
****
******** \`search_knowledge\` 工具从知识库中检索最新信息,然后基于检索结果来组织回答。
- search_knowledge
-
- Coordinator
- 使 category
****
1. "高才通B类的学历要求是什么" search_knowledge({ query: "高才通B类 学历要求 百强大学", category: "GEP" })
2. "投资移民可以买房吗?" search_knowledge({ query: "资本投资者入境计划 许可投资 房地产 住宅物业", category: "CIES" })
****
- "据我所知高才通B类需要全球百强大学学位……"
##
- "**知识库中未找到该信息的明确条文,建议核实入境处官方最新公告**"
-
- "根据知识库中《XXX》条目"
-
##
- TTPS/GEP QMAS
- "截至知识库最新更新"
-
##
- /
-
- QMAS超过50岁的处理
-
-
-
-
---
#
Coordinator Agent
##
1. **** 2-3
2. ****使
3. ****
4. ****
5. ****
##
\`\`\`
TTPSB类面向全球百强大学的毕业生
- //
- 53
- QS/THE/US News/ARWU
-
1.
2. 线
3. 4
4. 6
-
-
- TTPS政策详解 /
\`\`\`
---
#
- ****
- ****
- QMAS, Quality Migrant Admission Scheme
- ****使"《入境条例》Cap. 115"
- ****使HKD
- 使 emoji
-
---
#
1. **** search_knowledge
2. ****"您一定能获批""成功率为XX%"
3. ****
4. ****
5. **** Assessment Expert
6. ****
---
# Agent
- **Coordinator**
- **Assessment Expert**
- **Strategist**
Agent
`.trim();
}

View File

@ -0,0 +1,214 @@
/**
* Strategist Agent System Prompt
* Agent
*/
export function buildStrategistPrompt(): string {
return `
#
iConsulting Agent ** AgentStrategist**
Coordinator Agent
**使**
---
#
## greeting
-
- "你好"访
-
## need_discovery
-
- "我想去香港""香港移民有什么方式"
-
## info_collection
-
-
- 2-3
## assessment
-
-
- Assessment Expert
## recommendation
-
-
-
## objection_handling
-
- "太贵了""我担心……""这个靠谱吗""成功率高吗"
- +
## conversion
- /
- 线
-
## post_sale
-
-
-
---
#
##
- /"你们的评估服务多少钱?"
- "怎么付费?""可以用微信支付吗?"
- 线"整个流程需要多久?""最快什么时候能办下来?"
- "我需要准备什么文件?"
- "我想开始申请""帮我办理"
##
- "有类似背景成功的案例吗?"
- "你们和XX公司比有什么优势"
- "申请费是多少?""需要公证吗?"
- "如果被拒了怎么办?"
##
-
-
- "我想尽快办""越快越好"
- "我老婆/孩子也能一起吗?"
---
#
Coordinator
| | | |
|----------|----------|----------|
| / | + | |
| / | + | |
| / | + | |
| / | + | |
| 怀/ | + | |
| / | + | |
---
#
Coordinator
## P0
- /
- /
- QMAS/GEP/IANG
- /
## P1
-
-
- /
-
## P2
- //
-
-
- /
-
## P3
-
-
-
-
---
#
- **high**
- **medium**
- **low**线
---
#
**** JSON
\`\`\`json
{
"currentStage": "info_collection",
"stageConfidence": 0.85,
"recommendedNextAction": "用户已提供年龄和学历信息,但尚未提供工作经验和年薪。建议 Coordinator 自然地追问用户的工作经历和收入情况,以便触发资格评估。",
"missingInfoPriorities": [
{
"field": "工作经验年限",
"priority": "P1",
"reason": "影响 QMAS 和 GEP 的核心评分"
},
{
"field": "当前年薪",
"priority": "P1",
"reason": "决定是否符合高才通A类标准"
},
{
"field": "语言能力",
"priority": "P2",
"reason": "QMAS 综合计分制的加分项"
}
],
"conversionSignals": [
{
"signal": "用户主动提供了学历和年龄信息",
"strength": "weak",
"interpretation": "表明用户有一定意向,愿意配合评估流程"
}
],
"toneAdjustment": "用户态度积极配合,建议保持专业+热情的语气,适时肯定用户的条件优势以增强信心。",
"urgencyLevel": "medium",
"additionalNotes": "用户提到毕业于清华大学这是百强大学高才通B/C类可能是一个强有力的方向。建议在收集到工作经验信息后优先评估该类别。"
}
\`\`\`
**JSON **
- \`currentStage\`当前咨询阶段的英文标识greeting / need_discovery / info_collection / assessment / recommendation / objection_handling / conversion / post_sale
- \`stageConfidence\`对阶段判断的置信度0.0-1.0
- \`recommendedNextAction\`:对 Coordinator 的下一步行动建议,要具体可执行
- \`missingInfoPriorities\`:按优先级排序的缺失信息列表
- \`conversionSignals\`识别到的转化信号列表每个包含信号描述、强度strong/medium/weak和解读
- \`toneAdjustment\`:语气调整建议
- \`urgencyLevel\`紧急程度low/medium/high
- \`additionalNotes\`:任何其他值得 Coordinator 注意的观察和建议(可选)
---
#
- ****
- JSON key 使
- JSON value 使
-
- 使 emoji
---
#
1. ** JSON ** JSON JSON
2. ****使"您好""感谢"
3. ****
4. ****
5. ****使 Coordinator
6. ****
`.trim();
}

View File

@ -0,0 +1,121 @@
/**
* Assessment Expert Specialist Agent
* Agent
*/
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { BaseSpecialistService } from './base-specialist.service';
import {
AGENT_CONFIGS,
SpecialistAgentType,
ToolDefinition,
AssessmentExpertInput,
} from '../types/agent.types';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { buildAssessmentExpertPrompt } from '../prompts/assessment-expert-prompt';
@Injectable()
export class AssessmentExpertService extends BaseSpecialistService {
constructor(
anthropicClient: Anthropic,
private readonly knowledgeClient: KnowledgeClientService,
) {
super(AGENT_CONFIGS[SpecialistAgentType.ASSESSMENT_EXPERT], anthropicClient);
}
protected getSystemPrompt(): string {
return buildAssessmentExpertPrompt();
}
protected getTools(): ToolDefinition[] {
return [
{
name: 'search_knowledge',
description: '搜索知识库获取各移民类别的最新申请条件和评分标准',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索查询' },
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
},
},
required: ['query'],
},
},
{
name: 'get_user_context',
description: '获取用户的历史记忆信息,补充评估所需的背景数据',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '检索相关信息的查询' },
},
required: ['query'],
},
},
];
}
protected async executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string> {
switch (toolName) {
case 'search_knowledge': {
try {
return await this.knowledgeClient.search(
toolInput.query as string,
toolInput.category as string | undefined,
) || '未找到相关内容。';
} catch (error) {
return '知识库搜索暂时不可用。';
}
}
case 'get_user_context': {
try {
return await this.knowledgeClient.getUserContext(
toolInput.query as string,
) || '无用户历史记录。';
} catch (error) {
return '用户记忆检索暂时不可用。';
}
}
default:
return `Unknown tool: ${toolName}`;
}
}
/**
* Typed execute method
*/
async executeAssessment(input: AssessmentExpertInput): Promise<string> {
const userMessage = this.buildUserMessage(input);
const { result } = await this.execute(userMessage);
return result;
}
private buildUserMessage(input: AssessmentExpertInput): string {
const infoStr = Object.entries(input.userInfo)
.map(([k, v]) => ` - ${k}: ${v}`)
.join('\n');
let message = `请对以下用户进行全面的移民资格评估:\n\n用户信息\n${infoStr}`;
if (input.targetCategories?.length) {
message += `\n\n重点评估类别${input.targetCategories.join(', ')}`;
} else {
message += '\n\n请评估所有6个移民类别QMAS, GEP, IANG, TTPS, CIES, TECHTAS的适合程度。';
}
if (input.conversationContext) {
message += `\n\n对话背景${input.conversationContext}`;
}
message += '\n\n请以 JSON 格式输出评估报告,包含每个类别的评分、推荐理由和注意事项。';
return message;
}
}

View File

@ -0,0 +1,243 @@
/**
* Base Specialist Agent Service
* Agent Claude API
*
* Agent
* 1. System Prompt
* 2.
* 3. /
*/
import { Logger } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import {
AgentConfig,
AgentModel,
ToolDefinition,
AgentExecutionRecord,
SpecialistAgentType,
} from '../types/agent.types';
/** Tool executor for specialist's own tools */
export type SpecialistToolExecutor = (
toolName: string,
toolInput: Record<string, unknown>,
) => Promise<string>;
/** Specialist execution options */
export interface SpecialistExecutionOptions {
/** Additional context to prepend to the user message */
additionalContext?: string;
/** Override max tokens for this call */
maxTokensOverride?: number;
/** Abort signal */
abortSignal?: AbortSignal;
}
export abstract class BaseSpecialistService {
protected readonly logger: Logger;
protected anthropicClient: Anthropic;
constructor(
protected readonly config: AgentConfig,
anthropicClient: Anthropic,
) {
this.logger = new Logger(this.constructor.name);
this.anthropicClient = anthropicClient;
}
/** Subclass must define the system prompt */
protected abstract getSystemPrompt(): string;
/** Subclass must define available tools */
protected abstract getTools(): ToolDefinition[];
/** Subclass must provide tool executor */
protected abstract executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string>;
/**
* Execute the specialist agent
* Returns the final text response after internal tool loops
*/
async execute(
userMessage: string,
options?: SpecialistExecutionOptions,
): Promise<{ result: string; record: AgentExecutionRecord }> {
const startedAt = new Date();
let totalInputTokens = 0;
let totalOutputTokens = 0;
let toolCallsCount = 0;
const fullMessage = options?.additionalContext
? `${options.additionalContext}\n\n${userMessage}`
: userMessage;
let messages: Array<{ role: 'user' | 'assistant'; content: any }> = [
{ role: 'user', content: fullMessage },
];
const systemPrompt = this.getSystemPrompt();
const tools = this.getTools();
const maxTokens = options?.maxTokensOverride || this.config.maxTokens;
this.logger.debug(
`[${this.config.type}] Starting execution with ${tools.length} tools, max ${this.config.maxTurns} turns`,
);
let finalText = '';
try {
// Internal tool loop (like a mini agentLoop)
for (let turn = 0; turn < this.config.maxTurns; turn++) {
// Check abort
if (options?.abortSignal?.aborted) {
this.logger.debug(`[${this.config.type}] Aborted by signal`);
break;
}
// Call Claude API (non-streaming for specialists — simpler and sufficient)
const response = await this.callClaude(
systemPrompt,
messages,
tools.length > 0 ? tools.map(t => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
})) : undefined,
maxTokens,
);
// Track tokens
totalInputTokens += response.usage.input_tokens;
totalOutputTokens += response.usage.output_tokens;
// Extract text content
const textBlocks = response.content.filter(
(b: any) => b.type === 'text',
);
const toolUseBlocks = response.content.filter(
(b: any) => b.type === 'tool_use',
);
// Collect text
for (const block of textBlocks) {
finalText += (block as any).text;
}
// If no tool use, we're done
if (toolUseBlocks.length === 0 || response.stop_reason === 'end_turn') {
this.logger.debug(
`[${this.config.type}] Completed at turn ${turn + 1} with ${toolCallsCount} tool calls`,
);
break;
}
// Process tool calls
const toolResults: Array<{
type: 'tool_result';
tool_use_id: string;
content: string;
}> = [];
for (const toolUse of toolUseBlocks) {
const tu = toolUse as any;
toolCallsCount++;
this.logger.debug(
`[${this.config.type}] Tool call: ${tu.name} (turn ${turn + 1})`,
);
try {
const result = await this.executeTool(tu.name, tu.input);
toolResults.push({
type: 'tool_result',
tool_use_id: tu.id,
content: result,
});
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
toolResults.push({
type: 'tool_result',
tool_use_id: tu.id,
content: `<tool_use_error>Error: ${errMsg}</tool_use_error>`,
});
}
}
// Append assistant message and tool results for next turn
messages = [
...messages,
{ role: 'assistant', content: response.content },
{ role: 'user', content: toolResults },
];
// Reset finalText — we want the text from the LAST response
finalText = '';
}
const record: AgentExecutionRecord = {
agentType: this.config.type,
startedAt,
completedAt: new Date(),
durationMs: Date.now() - startedAt.getTime(),
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
toolCallsCount,
success: true,
};
return { result: finalText || '(No response generated)', record };
} catch (error) {
const errMsg = error instanceof Error ? error.message : String(error);
this.logger.error(`[${this.config.type}] Execution failed: ${errMsg}`);
const record: AgentExecutionRecord = {
agentType: this.config.type,
startedAt,
completedAt: new Date(),
durationMs: Date.now() - startedAt.getTime(),
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
toolCallsCount,
success: false,
error: errMsg,
};
return {
result: `Agent ${this.config.type} encountered an error: ${errMsg}`,
record,
};
}
}
/**
* Call Claude API with timeout
*/
private async callClaude(
systemPrompt: string,
messages: Array<{ role: 'user' | 'assistant'; content: any }>,
tools: any[] | undefined,
maxTokens: number,
): Promise<Anthropic.Message> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(
() => reject(new Error(`Agent ${this.config.type} timed out after ${this.config.timeoutMs}ms`)),
this.config.timeoutMs,
);
});
const apiCall = this.anthropicClient.messages.create({
model: this.config.model,
system: [{ type: 'text' as const, text: systemPrompt, cache_control: { type: 'ephemeral' as const } }],
messages,
...(tools && tools.length > 0 ? { tools } : {}),
max_tokens: maxTokens,
...(this.config.temperature !== undefined ? { temperature: this.config.temperature } : {}),
});
return Promise.race([apiCall, timeoutPromise]);
}
}

View File

@ -0,0 +1,120 @@
/**
* Case Analyst Specialist Agent
* Agent
*
* 使 Haiku
*/
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { BaseSpecialistService } from './base-specialist.service';
import {
AGENT_CONFIGS,
SpecialistAgentType,
ToolDefinition,
CaseAnalystInput,
} from '../types/agent.types';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { buildCaseAnalystPrompt } from '../prompts/case-analyst-prompt';
@Injectable()
export class CaseAnalystService extends BaseSpecialistService {
constructor(
anthropicClient: Anthropic,
private readonly knowledgeClient: KnowledgeClientService,
) {
super(AGENT_CONFIGS[SpecialistAgentType.CASE_ANALYST], anthropicClient);
}
protected getSystemPrompt(): string {
return buildCaseAnalystPrompt();
}
protected getTools(): ToolDefinition[] {
return [
{
name: 'search_knowledge',
description: '搜索知识库获取成功案例和申请经验',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索查询' },
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
},
},
required: ['query'],
},
},
{
name: 'get_user_context',
description: '获取用户的背景信息,用于匹配相似案例',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '检索查询' },
},
required: ['query'],
},
},
];
}
protected async executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string> {
switch (toolName) {
case 'search_knowledge': {
try {
return await this.knowledgeClient.search(
toolInput.query as string,
toolInput.category as string | undefined,
) || '未找到相关案例。';
} catch (error) {
return '案例库搜索暂时不可用。';
}
}
case 'get_user_context': {
try {
return await this.knowledgeClient.getUserContext(
toolInput.query as string,
) || '无用户历史记录。';
} catch (error) {
return '用户记忆检索暂时不可用。';
}
}
default:
return `Unknown tool: ${toolName}`;
}
}
/**
* Typed execute method
*/
async analyzeCases(input: CaseAnalystInput): Promise<string> {
const userMessage = this.buildUserMessage(input);
const { result } = await this.execute(userMessage);
return result;
}
private buildUserMessage(input: CaseAnalystInput): string {
const profileStr = Object.entries(input.userProfile)
.map(([k, v]) => ` - ${k}: ${v}`)
.join('\n');
let message = `请为以下用户检索和分析相关的成功案例:\n\n用户背景\n${profileStr}`;
message += `\n\n目标移民类别${input.targetCategory}`;
if (input.focusArea) {
message += `\n\n重点关注${input.focusArea}`;
}
message += '\n\n请以 JSON 格式返回案例分析,包含:';
message += '\n- matchedCases: 匹配的案例列表(含 summary, similarityFactors, outcome, timeline';
message += '\n- keyTakeaways: 关键启示';
return message;
}
}

View File

@ -0,0 +1,154 @@
/**
* Memory Manager Specialist Agent
* Agent
*
* 使 Haiku
* isConcurrencySafe = false
*/
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { BaseSpecialistService } from './base-specialist.service';
import {
AGENT_CONFIGS,
SpecialistAgentType,
ToolDefinition,
MemoryManagerInput,
} from '../types/agent.types';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { buildMemoryManagerPrompt } from '../prompts/memory-manager-prompt';
@Injectable()
export class MemoryManagerService extends BaseSpecialistService {
constructor(
anthropicClient: Anthropic,
private readonly knowledgeClient: KnowledgeClientService,
) {
super(AGENT_CONFIGS[SpecialistAgentType.MEMORY_MANAGER], anthropicClient);
}
protected getSystemPrompt(): string {
return buildMemoryManagerPrompt();
}
protected getTools(): ToolDefinition[] {
return [
{
name: 'get_user_context',
description: '获取用户的历史记忆信息',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '检索查询' },
},
required: ['query'],
},
},
{
name: 'save_user_memory',
description: '保存用户信息到长期记忆',
input_schema: {
type: 'object',
properties: {
userId: { type: 'string', description: '用户 ID' },
memoryType: {
type: 'string',
enum: ['FACT', 'PREFERENCE', 'INTENT'],
description: '记忆类型: FACT(个人信息/事实), PREFERENCE(偏好), INTENT(意向)',
},
content: { type: 'string', description: '要保存的内容' },
importance: {
type: 'number',
description: '重要程度 1-10',
},
},
required: ['userId', 'memoryType', 'content'],
},
},
];
}
protected async executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string> {
switch (toolName) {
case 'get_user_context': {
try {
return await this.knowledgeClient.getUserContext(
toolInput.query as string,
) || '无用户历史记录。';
} catch (error) {
return '用户记忆检索暂时不可用。';
}
}
case 'save_user_memory': {
try {
await this.knowledgeClient.saveUserMemory({
userId: toolInput.userId as string,
memoryType: (toolInput.memoryType as 'FACT' | 'PREFERENCE' | 'INTENT') || 'FACT',
content: toolInput.content as string,
importance: (toolInput.importance as number) || 5,
});
return `已成功保存 ${toolInput.memoryType} 类型的记忆。`;
} catch (error) {
return '记忆保存失败,请稍后重试。';
}
}
default:
return `Unknown tool: ${toolName}`;
}
}
/**
* Typed execute method
*/
async manageMemory(input: MemoryManagerInput): Promise<string> {
const userMessage = this.buildUserMessage(input);
const { result } = await this.execute(userMessage);
return result;
}
private buildUserMessage(input: MemoryManagerInput): string {
switch (input.action) {
case 'load_context':
return [
`请加载用户 ${input.userId} 的上下文信息。`,
input.contextQuery ? `重点检索:${input.contextQuery}` : '',
'请返回 JSON 格式的用户记忆列表。',
].filter(Boolean).join('\n');
case 'save_info':
const dataStr = input.dataToSave
? Object.entries(input.dataToSave)
.map(([k, v]) => ` - ${k}: ${v}`)
.join('\n')
: ' (无数据)';
return [
`请为用户 ${input.userId} 保存以下信息:`,
dataStr,
'请依次保存每条信息,并返回保存结果。',
].join('\n');
case 'extract_from_conversation':
return [
`请从以下对话中提取用户 ${input.userId} 的关键信息并保存:`,
'',
input.recentMessages || '(无对话内容)',
'',
'请提取并保存:个人信息、偏好、意向、评估相关数据等。',
'返回 JSON 格式,列出提取并保存的字段。',
].join('\n');
case 'summarize_profile':
return [
`请为用户 ${input.userId} 生成完整的画像摘要。`,
'请先加载所有可用的用户记忆,然后整合为结构化的用户画像。',
'返回 JSON 格式,包含 profileSummary 和 collectedInfo。',
].join('\n');
default:
return `执行记忆操作:${input.action}(用户:${input.userId}`;
}
}
}

View File

@ -0,0 +1,119 @@
/**
* Objection Handler Specialist Agent
* Agent
*/
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { BaseSpecialistService } from './base-specialist.service';
import {
AGENT_CONFIGS,
SpecialistAgentType,
ToolDefinition,
ObjectionHandlerInput,
} from '../types/agent.types';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { buildObjectionHandlerPrompt } from '../prompts/objection-handler-prompt';
@Injectable()
export class ObjectionHandlerService extends BaseSpecialistService {
constructor(
anthropicClient: Anthropic,
private readonly knowledgeClient: KnowledgeClientService,
) {
super(AGENT_CONFIGS[SpecialistAgentType.OBJECTION_HANDLER], anthropicClient);
}
protected getSystemPrompt(): string {
return buildObjectionHandlerPrompt();
}
protected getTools(): ToolDefinition[] {
return [
{
name: 'search_knowledge',
description: '搜索知识库获取事实依据,用于回应用户的疑虑和反对意见',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索查询' },
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
},
},
required: ['query'],
},
},
{
name: 'get_user_context',
description: '获取用户历史信息,了解之前的疑虑和沟通记录',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '检索查询' },
},
required: ['query'],
},
},
];
}
protected async executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string> {
switch (toolName) {
case 'search_knowledge': {
try {
return await this.knowledgeClient.search(
toolInput.query as string,
toolInput.category as string | undefined,
) || '未找到相关内容。';
} catch (error) {
return '知识库搜索暂时不可用。';
}
}
case 'get_user_context': {
try {
return await this.knowledgeClient.getUserContext(
toolInput.query as string,
) || '无用户历史记录。';
} catch (error) {
return '用户记忆检索暂时不可用。';
}
}
default:
return `Unknown tool: ${toolName}`;
}
}
/**
* Typed execute method
*/
async handleObjection(input: ObjectionHandlerInput): Promise<string> {
const userMessage = this.buildUserMessage(input);
const { result } = await this.execute(userMessage);
return result;
}
private buildUserMessage(input: ObjectionHandlerInput): string {
let message = `用户提出了以下疑虑或反对意见,请帮助处理:\n\n用户疑虑${input.objection}`;
message += `\n\n用户背景${input.userContext}`;
if (input.previousObjections?.length) {
message += `\n\n之前的疑虑\n${input.previousObjections.map((o, i) => ` ${i + 1}. ${o}`).join('\n')}`;
}
message += '\n\n请以 JSON 格式返回处理建议,包含:';
message += '\n- objectionCategory: 疑虑类型(价格、时间、风险、信任、竞品等)';
message += '\n- empathyResponse: 共情回应';
message += '\n- factualRebuttal: 事实依据反驳';
message += '\n- successStoryReference: 相关成功案例(如有)';
message += '\n- suggestedResponse: 建议回复';
message += '\n- followUpQuestion: 跟进问题';
return message;
}
}

View File

@ -0,0 +1,91 @@
/**
* Policy Expert Specialist Agent
* Agent
*/
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { BaseSpecialistService } from './base-specialist.service';
import { AGENT_CONFIGS, SpecialistAgentType, ToolDefinition, PolicyExpertInput } from '../types/agent.types';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { buildPolicyExpertPrompt } from '../prompts/policy-expert-prompt';
@Injectable()
export class PolicyExpertService extends BaseSpecialistService {
constructor(
anthropicClient: Anthropic,
private readonly knowledgeClient: KnowledgeClientService,
) {
super(AGENT_CONFIGS[SpecialistAgentType.POLICY_EXPERT], anthropicClient);
}
protected getSystemPrompt(): string {
return buildPolicyExpertPrompt();
}
protected getTools(): ToolDefinition[] {
return [
{
name: 'search_knowledge',
description: '搜索香港移民知识库,获取政策条文、申请条件、流程步骤等信息。务必通过此工具查证政策信息,不要凭记忆回答。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '搜索查询内容',
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别代码(可选,缩小搜索范围)',
},
},
required: ['query'],
},
},
];
}
protected async executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string> {
if (toolName === 'search_knowledge') {
try {
const result = await this.knowledgeClient.search(
toolInput.query as string,
toolInput.category as string | undefined,
);
return result || '未找到相关知识库内容。';
} catch (error) {
this.logger.error(`Knowledge search failed: ${error}`);
return '知识库搜索暂时不可用,请基于已有知识回答。';
}
}
return `Unknown tool: ${toolName}`;
}
/**
* Typed execute method for Coordinator to call
*/
async executeQuery(input: PolicyExpertInput): Promise<string> {
const userMessage = this.buildUserMessage(input);
const { result } = await this.execute(userMessage);
return result;
}
private buildUserMessage(input: PolicyExpertInput): string {
let message = input.query;
if (input.category) {
message = `[类别: ${input.category}] ${message}`;
}
if (input.includeProcessSteps) {
message += '\n\n请包含详细的申请流程步骤。';
}
if (input.includeRequirements) {
message += '\n\n请列出所有申请条件和要求。';
}
return message;
}
}

View File

@ -0,0 +1,96 @@
/**
* Strategist Specialist Agent
* Agent Coordinator
*/
import { Injectable } from '@nestjs/common';
import Anthropic from '@anthropic-ai/sdk';
import { BaseSpecialistService } from './base-specialist.service';
import {
AGENT_CONFIGS,
SpecialistAgentType,
ToolDefinition,
StrategistInput,
} from '../types/agent.types';
import { KnowledgeClientService } from '../../knowledge/knowledge-client.service';
import { buildStrategistPrompt } from '../prompts/strategist-prompt';
@Injectable()
export class StrategistService extends BaseSpecialistService {
constructor(
anthropicClient: Anthropic,
private readonly knowledgeClient: KnowledgeClientService,
) {
super(AGENT_CONFIGS[SpecialistAgentType.STRATEGIST], anthropicClient);
}
protected getSystemPrompt(): string {
return buildStrategistPrompt();
}
protected getTools(): ToolDefinition[] {
return [
{
name: 'get_user_context',
description: '获取用户历史信息,辅助判断对话策略',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '检索查询' },
},
required: ['query'],
},
},
];
}
protected async executeTool(
toolName: string,
toolInput: Record<string, unknown>,
): Promise<string> {
if (toolName === 'get_user_context') {
try {
return await this.knowledgeClient.getUserContext(
toolInput.query as string,
) || '无用户历史记录。';
} catch (error) {
return '用户记忆检索暂时不可用。';
}
}
return `Unknown tool: ${toolName}`;
}
/**
* Typed execute method
*/
async getStrategy(input: StrategistInput): Promise<string> {
const userMessage = this.buildUserMessage(input);
const { result } = await this.execute(userMessage);
return result;
}
private buildUserMessage(input: StrategistInput): string {
const collectedInfoStr = Object.entries(input.collectedInfo)
.map(([k, v]) => ` - ${k}: ${v}`)
.join('\n') || ' (尚未收集任何信息)';
return [
'请分析当前对话状态并提供策略建议:',
'',
`对话摘要: ${input.conversationSummary}`,
`当前轮次: ${input.currentTurnCount}`,
`已收集信息:\n${collectedInfoStr}`,
`用户情绪: ${input.userSentiment || '未知'}`,
`已完成评估: ${input.hasAssessment ? '是' : '否'}`,
`已转化: ${input.hasConverted ? '是' : '否'}`,
'',
'请返回 JSON 格式的策略建议,包含:',
'- currentStage: 当前所处咨询阶段',
'- recommendedNextAction: 建议的下一步行动',
'- missingInfoPriorities: 需要优先收集的信息',
'- conversionSignals: 检测到的转化信号',
'- toneAdjustment: 语气调整建议',
'- urgencyLevel: 紧急程度 (low/medium/high)',
].join('\n');
}
}

View File

@ -0,0 +1,402 @@
/**
* Coordinator Tools
* Agent
*/
import { ToolDefinition } from '../types/agent.types';
// ============================================================
// Agent Invocation Tools (调用专家 Agent 的工具)
// ============================================================
export const AGENT_INVOCATION_TOOLS: ToolDefinition[] = [
{
name: 'invoke_policy_expert',
description:
'调用政策专家 Agent 查询和解读香港移民政策。当用户询问具体政策、申请条件、流程步骤时使用。' +
'务必通过此工具查证政策信息,不要凭记忆回答政策问题。',
input_schema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '政策查询内容,例如:"TTPS B类的申请条件和流程"',
},
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
description: '移民类别代码(可选,缩小查询范围)',
},
includeProcessSteps: {
type: 'boolean',
description: '是否需要包含详细申请流程步骤',
},
includeRequirements: {
type: 'boolean',
description: '是否需要列出所有申请条件',
},
},
required: ['query'],
},
isConcurrencySafe: true,
},
{
name: 'invoke_assessment_expert',
description:
'调用评估专家 Agent 对用户进行移民资格评估。' +
'需要至少收集到年龄、国籍、学历、工作经验等基本信息后才可调用。' +
'评估结果会包含每个类别的评分和推荐理由。',
input_schema: {
type: 'object',
properties: {
userInfo: {
type: 'object',
description:
'用户信息键值对,如 {"age": "35", "nationality": "中国大陆", "education": "硕士/计算机", "workExperience": "10年/软件工程"}',
},
targetCategories: {
type: 'array',
items: { type: 'string' },
description: '重点评估的类别列表可选不传则评估所有6个类别',
},
conversationContext: {
type: 'string',
description: '最近几轮对话的简要摘要,帮助评估专家理解背景',
},
},
required: ['userInfo'],
},
isConcurrencySafe: true,
},
{
name: 'invoke_strategist',
description:
'调用策略顾问 Agent 分析对话状态并提供策略建议。' +
'仅供协调器内部参考,用户不可见。' +
'建议每 3-5 轮调用一次,或在关键决策点调用。',
input_schema: {
type: 'object',
properties: {
conversationSummary: {
type: 'string',
description: '当前对话摘要',
},
currentTurnCount: {
type: 'number',
description: '当前对话轮次数',
},
collectedInfo: {
type: 'object',
description: '已收集的用户信息',
},
userSentiment: {
type: 'string',
description: '用户当前情绪(如:积极、犹豫、焦虑、中性)',
},
hasAssessment: {
type: 'boolean',
description: '是否已完成资格评估',
},
hasConverted: {
type: 'boolean',
description: '是否已转化(付费/预约)',
},
},
required: ['conversationSummary', 'currentTurnCount', 'collectedInfo'],
},
isConcurrencySafe: true,
},
{
name: 'invoke_objection_handler',
description:
'调用异议处理专家 Agent 处理用户的疑虑和反对意见。' +
'当用户表达犹豫、担忧、价格异议或与竞品比较时使用。',
input_schema: {
type: 'object',
properties: {
objection: {
type: 'string',
description: '用户的疑虑或反对意见原文',
},
userContext: {
type: 'string',
description: '用户背景信息摘要(已知的)',
},
previousObjections: {
type: 'array',
items: { type: 'string' },
description: '之前已处理过的疑虑列表',
},
},
required: ['objection', 'userContext'],
},
isConcurrencySafe: true,
},
{
name: 'invoke_case_analyst',
description:
'调用案例分析师 Agent 查找和分析类似的成功案例。' +
'用于建立用户信心,展示公司实力。',
input_schema: {
type: 'object',
properties: {
userProfile: {
type: 'object',
description: '用户基本信息',
},
targetCategory: {
type: 'string',
description: '目标移民类别',
},
focusArea: {
type: 'string',
description: '关注重点(如:审批速度、特殊情况处理等)',
},
},
required: ['userProfile', 'targetCategory'],
},
isConcurrencySafe: true,
},
{
name: 'invoke_memory_manager',
description:
'调用记忆管理 Agent 管理用户信息。' +
'在对话开始时加载用户上下文(load_context)' +
'在收集到新信息时保存(save_info)' +
'定期从对话中提取信息(extract_from_conversation)。',
input_schema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['load_context', 'save_info', 'extract_from_conversation', 'summarize_profile'],
description: '操作类型',
},
userId: {
type: 'string',
description: '用户 ID',
},
dataToSave: {
type: 'object',
description: '要保存的信息键值对save_info 时使用)',
},
recentMessages: {
type: 'string',
description: '最近的对话内容extract_from_conversation 时使用)',
},
contextQuery: {
type: 'string',
description: '上下文检索查询load_context 时使用)',
},
},
required: ['action', 'userId'],
},
isConcurrencySafe: false, // 有写操作
},
];
// ============================================================
// Direct Tools (协调器直接调用的工具,不经过专家 Agent)
// ============================================================
export const DIRECT_TOOLS: ToolDefinition[] = [
{
name: 'search_knowledge',
description:
'直接搜索知识库获取移民相关信息。' +
'用于快速查询事实信息,不需要完整的政策专家分析时使用。',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索查询' },
category: {
type: 'string',
enum: ['QMAS', 'GEP', 'IANG', 'TTPS', 'CIES', 'TECHTAS'],
},
},
required: ['query'],
},
isConcurrencySafe: true,
},
{
name: 'get_user_context',
description: '快速检索用户的历史记忆信息',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '检索查询' },
},
required: ['query'],
},
isConcurrencySafe: true,
},
{
name: 'check_off_topic',
description: '检查用户消息是否偏离移民咨询主题',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '用户消息内容' },
},
required: ['query'],
},
isConcurrencySafe: true,
},
{
name: 'get_current_datetime',
description: '获取当前日期和时间(用于时效性政策信息判断)',
input_schema: {
type: 'object',
properties: {},
},
isConcurrencySafe: true,
},
{
name: 'web_search',
description: '搜索互联网获取最新移民资讯、新闻或政策更新',
input_schema: {
type: 'object',
properties: {
query: { type: 'string', description: '搜索查询' },
},
required: ['query'],
},
isConcurrencySafe: true,
},
{
name: 'get_exchange_rate',
description: '获取当前汇率(讨论投资移民金额时使用)',
input_schema: {
type: 'object',
properties: {
from: { type: 'string', description: '源货币代码,如 CNY' },
to: { type: 'string', description: '目标货币代码,如 HKD' },
},
required: ['from', 'to'],
},
isConcurrencySafe: true,
},
{
name: 'fetch_immigration_news',
description: '获取最新的香港移民新闻和政策动态',
input_schema: {
type: 'object',
properties: {
category: {
type: 'string',
description: '新闻类别(可选)',
},
limit: {
type: 'number',
description: '返回条数,默认 5',
},
},
},
isConcurrencySafe: true,
},
{
name: 'save_user_memory',
description:
'直接保存用户信息到长期记忆。' +
'用于保存关键信息,无需启动完整的记忆管理 Agent。',
input_schema: {
type: 'object',
properties: {
userId: { type: 'string', description: '用户 ID' },
memoryType: {
type: 'string',
enum: ['FACT', 'PREFERENCE', 'INTENT'],
description: '记忆类型',
},
content: { type: 'string', description: '记忆内容' },
importance: {
type: 'number',
description: '重要程度 1-10',
},
},
required: ['userId', 'memoryType', 'content'],
},
isConcurrencySafe: false,
},
{
name: 'collect_assessment_info',
description:
'结构化收集评估所需信息。' +
'当对话中获得了用户的评估相关信息时,用此工具保存。',
input_schema: {
type: 'object',
properties: {
userId: { type: 'string', description: '用户 ID' },
field: {
type: 'string',
enum: [
'age', 'nationality', 'education_level', 'education_field',
'work_experience_years', 'work_experience_field', 'current_salary',
'language_ability', 'family_status', 'has_hk_connection',
'investment_capability', 'target_timeline',
],
description: '信息字段名',
},
value: { type: 'string', description: '字段值' },
},
required: ['userId', 'field', 'value'],
},
isConcurrencySafe: false,
},
{
name: 'generate_payment',
description:
'生成支付链接。仅在用户明确表示要付费或预约服务时使用。',
input_schema: {
type: 'object',
properties: {
userId: { type: 'string', description: '用户 ID' },
serviceType: {
type: 'string',
enum: ['assessment', 'consultation', 'full_service'],
description: '服务类型',
},
amount: { type: 'number', description: '金额HKD' },
description: { type: 'string', description: '服务描述' },
},
required: ['userId', 'serviceType'],
},
isConcurrencySafe: false,
},
];
// ============================================================
// All Coordinator Tools Combined
// ============================================================
/** 协调器全部可用工具 */
export const ALL_COORDINATOR_TOOLS: ToolDefinition[] = [
...AGENT_INVOCATION_TOOLS,
...DIRECT_TOOLS,
];
/**
* Get Claude API-compatible tool definitions
* (strips custom fields like isConcurrencySafe)
*/
export function getToolsForClaudeAPI(): Array<{
name: string;
description: string;
input_schema: Record<string, unknown>;
}> {
return ALL_COORDINATOR_TOOLS.map(t => ({
name: t.name,
description: t.description,
input_schema: t.input_schema,
}));
}
/** Agent 调用工具名称集合 */
export const AGENT_TOOL_NAMES = new Set(
AGENT_INVOCATION_TOOLS.map(t => t.name),
);
/** 判断是否是 Agent 调用工具 */
export function isAgentInvocationTool(toolName: string): boolean {
return AGENT_TOOL_NAMES.has(toolName);
}

View File

@ -0,0 +1,275 @@
/**
* Tool Execution Queue
* Claude Code Q80
*
*
* - search_knowledge + invoke_policy_expert
* - save_user_memory
* -
*/
import { Logger } from '@nestjs/common';
export interface QueuedTool {
id: string; // tool_use_id from Claude
name: string; // tool name
input: Record<string, unknown>;
isConcurrencySafe: boolean;
status: 'queued' | 'executing' | 'completed' | 'failed';
result?: string;
error?: string;
startedAt?: number;
completedAt?: number;
promise?: Promise<void>;
}
export type ToolExecutor = (
toolName: string,
toolInput: Record<string, unknown>,
toolUseId: string,
) => Promise<{ output: string; isError: boolean }>;
export class ToolExecutionQueue {
private readonly logger = new Logger(ToolExecutionQueue.name);
private tools: QueuedTool[] = [];
private executor: ToolExecutor;
constructor(executor: ToolExecutor) {
this.executor = executor;
}
/**
* Add a tool to the execution queue
*/
addTool(
id: string,
name: string,
input: Record<string, unknown>,
isConcurrencySafe: boolean,
): void {
this.tools.push({
id,
name,
input,
isConcurrencySafe,
status: 'queued',
});
}
/**
* Check if a tool can start executing now
*/
private canExecute(tool: QueuedTool): boolean {
const executing = this.tools.filter(t => t.status === 'executing');
// If nothing is executing, always allow
if (executing.length === 0) return true;
// If the new tool is concurrency-safe AND all executing are safe → allow parallel
if (tool.isConcurrencySafe && executing.every(t => t.isConcurrencySafe)) {
return true;
}
// Otherwise block: non-safe tool must wait for queue to drain
return false;
}
/**
* Process the queue start any tools that can run
*/
private async processQueue(): Promise<void> {
for (const tool of this.tools) {
if (tool.status !== 'queued') continue;
if (this.canExecute(tool)) {
await this.executeTool(tool);
} else if (!tool.isConcurrencySafe) {
// Non-safe tool encountered — stop processing further
// It will be picked up after current executing tools complete
break;
}
}
}
/**
* Execute a single tool
*/
private async executeTool(tool: QueuedTool): Promise<void> {
tool.status = 'executing';
tool.startedAt = Date.now();
this.logger.debug(`Executing tool: ${tool.name} (id: ${tool.id})`);
const execution = (async () => {
try {
const result = await this.executor(tool.name, tool.input, tool.id);
tool.result = result.output;
tool.status = result.isError ? 'failed' : 'completed';
if (result.isError) {
tool.error = result.output;
}
} catch (error) {
tool.status = 'failed';
tool.error = error instanceof Error ? error.message : String(error);
tool.result = `<tool_use_error>Error executing ${tool.name}: ${tool.error}</tool_use_error>`;
} finally {
tool.completedAt = Date.now();
const duration = tool.completedAt - (tool.startedAt || tool.completedAt);
this.logger.debug(
`Tool completed: ${tool.name} (${tool.status}) in ${duration}ms`,
);
}
})();
tool.promise = execution;
// After this tool completes, try to process more from the queue
execution.finally(() => {
this.processQueue();
});
}
/**
* Execute all queued tools and return results in order
* This is the main entry point
*/
async executeAll(
toolUses: Array<{
id: string;
name: string;
input: Record<string, unknown>;
}>,
concurrencyMap: Record<string, boolean>,
): Promise<Array<{ toolUseId: string; toolName: string; output: string; isError: boolean }>> {
// Reset state
this.tools = [];
// Queue all tools
for (const tu of toolUses) {
const isSafe = concurrencyMap[tu.name] ?? true; // default to safe
this.addTool(tu.id, tu.name, tu.input, isSafe);
}
// Start processing
await this.processQueue();
// Wait for all to complete
await this.waitForAll();
// Return results in original order
return this.tools.map(t => ({
toolUseId: t.id,
toolName: t.name,
output: t.result || `<tool_use_error>No result from ${t.name}</tool_use_error>`,
isError: t.status === 'failed',
}));
}
/**
* Wait for all tools to finish executing
*/
private async waitForAll(): Promise<void> {
while (this.hasUnfinishedTools()) {
const executing = this.tools
.filter(t => t.status === 'executing' && t.promise)
.map(t => t.promise!);
if (executing.length > 0) {
await Promise.race(executing);
}
// Process any newly unblocked tools
await this.processQueue();
}
}
/**
* Check if there are still tools waiting or executing
*/
private hasUnfinishedTools(): boolean {
return this.tools.some(
t => t.status === 'queued' || t.status === 'executing',
);
}
/**
* Get execution summary
*/
getExecutionSummary(): {
totalTools: number;
parallelBatches: number;
totalDurationMs: number;
perTool: Array<{ name: string; durationMs: number; status: string }>;
} {
const minStart = Math.min(
...this.tools.map(t => t.startedAt || Infinity),
);
const maxEnd = Math.max(
...this.tools.map(t => t.completedAt || 0),
);
return {
totalTools: this.tools.length,
parallelBatches: this.countParallelBatches(),
totalDurationMs: maxEnd > 0 ? maxEnd - minStart : 0,
perTool: this.tools.map(t => ({
name: t.name,
durationMs: t.completedAt && t.startedAt
? t.completedAt - t.startedAt
: 0,
status: t.status,
})),
};
}
/**
* Estimate how many parallel batches were executed
*/
private countParallelBatches(): number {
// Group tools by overlapping execution windows
let batches = 0;
let lastEndTime = 0;
for (const tool of this.tools) {
if (tool.startedAt && tool.startedAt >= lastEndTime) {
batches++;
}
if (tool.completedAt) {
lastEndTime = Math.max(lastEndTime, tool.completedAt);
}
}
return batches || 1;
}
}
// ============================================================
// Concurrency Map (工具并发安全性声明)
// ============================================================
/**
*
* true =
* false = /
*/
export const TOOL_CONCURRENCY_MAP: Record<string, boolean> = {
// Agent invocation tools — 互相独立,可并行
invoke_policy_expert: true,
invoke_assessment_expert: true,
invoke_strategist: true,
invoke_objection_handler: true,
invoke_case_analyst: true,
invoke_memory_manager: false, // 有写操作save_user_memory
// Direct tools — 按操作类型
search_knowledge: true, // 读操作
get_user_context: true, // 读操作
check_off_topic: true, // 读操作
get_current_datetime: true, // 无副作用
web_search: true, // 读操作
get_exchange_rate: true, // 读操作
fetch_immigration_news: true, // 读操作
// 有副作用的工具 — 串行
save_user_memory: false, // 写操作
generate_payment: false, // 创建支付订单
collect_assessment_info: false, // 写操作
};

View File

@ -0,0 +1,323 @@
/**
* Multi-Agent System Type Definitions
* Agent
*/
// ============================================================
// Agent Identity & Configuration
// ============================================================
/** 专家 Agent 类型枚举 */
export enum SpecialistAgentType {
POLICY_EXPERT = 'policy_expert',
ASSESSMENT_EXPERT = 'assessment_expert',
STRATEGIST = 'strategist',
OBJECTION_HANDLER = 'objection_handler',
CASE_ANALYST = 'case_analyst',
MEMORY_MANAGER = 'memory_manager',
}
/** Agent 模型选择 */
export type AgentModel = 'claude-sonnet-4-20250514' | 'claude-haiku-3-5-20241022';
/** Agent 配置 */
export interface AgentConfig {
type: SpecialistAgentType;
model: AgentModel;
maxTurns: number; // 内部工具循环最大轮次
maxTokens: number; // 单次 API 调用最大 token
temperature?: number; // API 温度参数 (0-1)undefined 使用模型默认值
timeoutMs: number; // 执行超时
isConcurrencySafe: boolean; // 是否可以与其他 Agent 并行
}
/** 所有 Agent 的默认配置 */
export const AGENT_CONFIGS: Record<SpecialistAgentType, AgentConfig> = {
[SpecialistAgentType.POLICY_EXPERT]: {
type: SpecialistAgentType.POLICY_EXPERT,
model: 'claude-sonnet-4-20250514',
maxTurns: 3,
maxTokens: 2048,
temperature: 0, // 政策信息需要确定性
timeoutMs: 30000,
isConcurrencySafe: true,
},
[SpecialistAgentType.ASSESSMENT_EXPERT]: {
type: SpecialistAgentType.ASSESSMENT_EXPERT,
model: 'claude-sonnet-4-20250514',
maxTurns: 3,
maxTokens: 3000,
temperature: 0, // 评估需要确定性
timeoutMs: 45000,
isConcurrencySafe: true,
},
[SpecialistAgentType.STRATEGIST]: {
type: SpecialistAgentType.STRATEGIST,
model: 'claude-sonnet-4-20250514',
maxTurns: 2,
maxTokens: 1500,
temperature: 0.3, // 策略建议需要一定创造性
timeoutMs: 20000,
isConcurrencySafe: true,
},
[SpecialistAgentType.OBJECTION_HANDLER]: {
type: SpecialistAgentType.OBJECTION_HANDLER,
model: 'claude-sonnet-4-20250514',
maxTurns: 3,
maxTokens: 2048,
temperature: 0.2, // 异议处理需要共情+少量变化
timeoutMs: 30000,
isConcurrencySafe: true,
},
[SpecialistAgentType.CASE_ANALYST]: {
type: SpecialistAgentType.CASE_ANALYST,
model: 'claude-haiku-3-5-20241022',
maxTurns: 2,
maxTokens: 1500,
temperature: 0, // 案例检索需要确定性
timeoutMs: 15000,
isConcurrencySafe: true,
},
[SpecialistAgentType.MEMORY_MANAGER]: {
type: SpecialistAgentType.MEMORY_MANAGER,
model: 'claude-haiku-3-5-20241022',
maxTurns: 2,
maxTokens: 1024,
temperature: 0, // 信息提取需要确定性
timeoutMs: 15000,
isConcurrencySafe: false, // 有写操作save_user_memory
},
};
// ============================================================
// Agent Invocation (Coordinator → Specialist)
// ============================================================
/** Policy Expert 输入 */
export interface PolicyExpertInput {
query: string;
category?: string; // QMAS | GEP | IANG | TTPS | CIES | TECHTAS
includeProcessSteps?: boolean;
includeRequirements?: boolean;
}
/** Policy Expert 输出 */
export interface PolicyExpertOutput {
policySummary: string;
requirements?: string[];
processSteps?: string[];
importantNotes?: string[];
sources?: string[];
}
/** Assessment Expert 输入 */
export interface AssessmentExpertInput {
userInfo: Record<string, unknown>;
targetCategories?: string[];
conversationContext?: string; // 最近几轮对话摘要
}
/** 单个类别的评估结果 */
export interface CategoryAssessment {
category: string;
categoryName: string;
eligible: boolean;
score: number; // 0-100
confidence: number; // 0-1
highlights: string[];
concerns: string[];
missingInfo?: string[];
subClass?: string; // e.g. TTPS A/B/C
}
/** Assessment Expert 输出 */
export interface AssessmentExpertOutput {
assessments: CategoryAssessment[];
overallRecommendation: string;
topRecommended: string[]; // 最推荐的类别
suitabilityScore: number; // 综合评分 0-100
summary: string;
}
/** Strategist 输入 */
export interface StrategistInput {
conversationSummary: string;
currentStage?: string; // 当前咨询阶段
latestUserMessage?: string; // 最新用户消息
currentTurnCount: number;
collectedInfo: Record<string, unknown>;
userSentiment?: string;
hasAssessment: boolean;
hasConverted: boolean;
}
/** Strategist 输出 */
export interface StrategistOutput {
currentStage: string;
recommendedNextAction: string;
missingInfoPriorities: string[];
conversionSignals: string[];
toneAdjustment?: string;
suggestedQuestions?: string[];
urgencyLevel: 'low' | 'medium' | 'high';
}
/** Objection Handler 输入 */
export interface ObjectionHandlerInput {
objection: string;
category?: string; // 相关移民类别
userContext: string;
previousObjections?: string[];
}
/** Objection Handler 输出 */
export interface ObjectionHandlerOutput {
objectionCategory: string;
empathyResponse: string;
factualRebuttal: string;
successStoryReference?: string;
suggestedResponse: string;
followUpQuestion: string;
}
/** Case Analyst 输入 */
export interface CaseAnalystInput {
userProfile: Record<string, unknown>;
targetCategory: string;
focusArea?: string;
}
/** Case Analyst 输出 */
export interface CaseAnalystOutput {
matchedCases: Array<{
summary: string;
similarityFactors: string[];
outcome: string;
timeline: string;
}>;
keyTakeaways: string[];
}
/** Memory Manager Action Types */
export type MemoryAction = 'load_context' | 'save_info' | 'extract_from_conversation' | 'summarize_profile';
/** Memory Manager 输入 */
export interface MemoryManagerInput {
action: MemoryAction;
userId: string;
/** For save_info: key-value pairs to save */
dataToSave?: Record<string, unknown>;
/** For extract_from_conversation: recent messages */
recentMessages?: string;
/** For load_context: optional query for relevance filtering */
contextQuery?: string;
}
/** Memory Manager 输出 */
export interface MemoryManagerOutput {
action: MemoryAction;
/** For load_context */
userMemories?: Array<{
type: string;
content: string;
importance: number;
}>;
/** For save_info / extract */
savedFields?: string[];
/** For summarize_profile */
profileSummary?: string;
collectedInfo?: Record<string, unknown>;
}
// ============================================================
// Agent Execution Tracking
// ============================================================
/** Agent 执行记录 */
export interface AgentExecutionRecord {
agentType: SpecialistAgentType;
startedAt: Date;
completedAt?: Date;
durationMs?: number;
inputTokens: number;
outputTokens: number;
toolCallsCount: number;
success: boolean;
error?: string;
}
/** Coordinator 单轮执行统计 */
export interface CoordinatorTurnStats {
turnNumber: number;
agentsInvoked: AgentExecutionRecord[];
directToolCalls: number;
coordinatorInputTokens: number;
coordinatorOutputTokens: number;
totalCostUsd: number;
}
// ============================================================
// Coordinator Loop Parameters
// ============================================================
/** Agent Loop 参数 */
export interface AgentLoopParams {
messages: ClaudeMessage[];
systemPrompt: SystemPromptBlock[];
tools: ToolDefinition[];
maxTurns: number;
maxBudgetUsd: number;
conversationId: string;
userId: string;
abortSignal?: AbortSignal;
/** 已经消耗的轮次(递归时累加)*/
currentTurnCount?: number;
/** 已经消耗的成本(递归时累加)*/
currentCostUsd?: number;
}
/** Claude API 消息格式 */
export interface ClaudeMessage {
role: 'user' | 'assistant';
content: string | ContentBlock[];
}
/** System Prompt Block (支持 cache_control) */
export interface SystemPromptBlock {
type: 'text';
text: string;
cache_control?: { type: 'ephemeral' };
}
/** Content Block 类型 */
export type ContentBlock =
| { type: 'text'; text: string }
| { type: 'tool_use'; id: string; name: string; input: Record<string, unknown> }
| { type: 'tool_result'; tool_use_id: string; content: string; is_error?: boolean }
| { type: 'image'; source: { type: 'base64'; media_type: string; data: string } };
/** Tool Definition for Claude API */
export interface ToolDefinition {
name: string;
description: string;
input_schema: Record<string, unknown>;
/** 是否可以与其他工具并发执行 */
isConcurrencySafe?: boolean;
}
// ============================================================
// Consulting State (LLM 自报告,非程序控制)
// ============================================================
/** 咨询状态(由 Coordinator 在回复中自行报告) */
export interface ConsultingStateReport {
currentStage: string;
collectedInfo: Record<string, unknown>;
assessmentDone: boolean;
assessmentResult?: {
topRecommended: string[];
suitabilityScore: number;
};
nextAction: string;
conversionReady: boolean;
}

View File

@ -0,0 +1,236 @@
/**
* Context Injection Type Definitions
*
*/
// ============================================================
// Context Types (每种注入上下文的类型)
// ============================================================
/** 上下文类型枚举 */
export enum ContextType {
USER_MEMORY = 'user_memory',
COLLECTED_INFO = 'collected_info',
CONVERSATION_STATS = 'conversation_stats',
ASSESSMENT_RESULT = 'assessment_result',
RELEVANT_KNOWLEDGE = 'relevant_knowledge',
SIMILAR_EXPERIENCES = 'similar_experiences',
DEVICE_CONTEXT = 'device_context',
AGENT_HISTORY = 'agent_history',
}
/** 上下文注入优先级越小越优先token 不足时从低优先级开始丢弃) */
export const CONTEXT_PRIORITY: Record<ContextType, number> = {
[ContextType.COLLECTED_INFO]: 1, // 最重要:已收集的用户信息
[ContextType.ASSESSMENT_RESULT]: 2, // 已有的评估结果
[ContextType.USER_MEMORY]: 3, // 用户历史记忆
[ContextType.CONVERSATION_STATS]: 4, // 对话统计
[ContextType.RELEVANT_KNOWLEDGE]: 5, // 相关知识
[ContextType.AGENT_HISTORY]: 6, // 最近 Agent 调用记录
[ContextType.SIMILAR_EXPERIENCES]: 7, // 系统经验
[ContextType.DEVICE_CONTEXT]: 8, // 设备信息(可丢弃)
};
/** 上下文缓存策略 */
export const CONTEXT_CACHE_TTL: Record<ContextType, number> = {
[ContextType.USER_MEMORY]: 60000, // 1 min — 可能被其他 Agent 更新
[ContextType.COLLECTED_INFO]: 0, // 不缓存 — 每轮都从对话状态读取
[ContextType.CONVERSATION_STATS]: 0, // 不缓存 — 实时计算
[ContextType.ASSESSMENT_RESULT]: 300000, // 5 min — 评估结果稳定
[ContextType.RELEVANT_KNOWLEDGE]: 30000, // 30 sec — 可能话题变化
[ContextType.SIMILAR_EXPERIENCES]: 300000, // 5 min — 系统经验稳定
[ContextType.DEVICE_CONTEXT]: Infinity, // 整个对话不变
[ContextType.AGENT_HISTORY]: 0, // 不缓存 — 每轮更新
};
// ============================================================
// Context Data Interfaces
// ============================================================
/** 用户记忆上下文 */
export interface UserMemoryContext {
type: ContextType.USER_MEMORY;
memories: Array<{
memoryType: string;
content: string;
importance: number;
createdAt: string;
}>;
profileSummary?: string;
}
/** 已收集信息上下文 */
export interface CollectedInfoContext {
type: ContextType.COLLECTED_INFO;
info: Record<string, unknown>;
completionRate: number; // 0-1, 信息收集完成度
missingFields: string[];
}
/** 对话统计上下文 */
export interface ConversationStatsContext {
type: ContextType.CONVERSATION_STATS;
totalTurns: number;
userMessageCount: number;
assistantMessageCount: number;
durationMinutes: number;
agentsInvokedThisSession: string[];
lastAgentUsed?: string;
}
/** 评估结果上下文 */
export interface AssessmentResultContext {
type: ContextType.ASSESSMENT_RESULT;
topRecommended: string[];
suitabilityScore: number;
summary: string;
completedAt: string;
}
/** 相关知识上下文 */
export interface RelevantKnowledgeContext {
type: ContextType.RELEVANT_KNOWLEDGE;
articles: Array<{
title: string;
summary: string;
category: string;
similarity: number;
}>;
}
/** 系统经验上下文 */
export interface SimilarExperiencesContext {
type: ContextType.SIMILAR_EXPERIENCES;
experiences: Array<{
experienceType: string;
content: string;
confidence: number;
}>;
}
/** 设备上下文 */
export interface DeviceContextData {
type: ContextType.DEVICE_CONTEXT;
ip?: string;
region?: string;
timezone?: string;
userAgent?: string;
deviceType?: 'mobile' | 'desktop' | 'tablet';
isNewUser: boolean;
firstMessageTime: string;
}
/** Agent 调用历史上下文 */
export interface AgentHistoryContext {
type: ContextType.AGENT_HISTORY;
recentCalls: Array<{
agentType: string;
input: string; // 简短摘要
output: string; // 简短摘要
durationMs: number;
timestamp: string;
}>;
}
/** 上下文数据联合类型 */
export type ContextData =
| UserMemoryContext
| CollectedInfoContext
| ConversationStatsContext
| AssessmentResultContext
| RelevantKnowledgeContext
| SimilarExperiencesContext
| DeviceContextData
| AgentHistoryContext;
// ============================================================
// Context Injection Result
// ============================================================
/** 单个上下文注入块 */
export interface ContextInjectionBlock {
contextType: ContextType;
priority: number;
content: string; // 格式化后的文本
estimatedTokens: number; // 估算 token 数
fromCache: boolean;
}
/** 上下文注入结果 */
export interface ContextInjectionResult {
blocks: ContextInjectionBlock[];
totalEstimatedTokens: number;
droppedContexts: ContextType[]; // 因 token 限制被丢弃的上下文
injectionText: string; // 拼接后的注入文本
}
// ============================================================
// Context Injector Configuration
// ============================================================
/** 上下文注入器配置 */
export interface ContextInjectorConfig {
/** 上下文注入的最大 token 预算(预留给上下文的 token 数)*/
maxContextTokens: number; // default: 4000
/** 是否启用自动压缩 */
enableAutoCompaction: boolean; // default: true
/** 触发压缩的 token 阈值(总消息 token 数)*/
compactionThreshold: number; // default: 80000 (80k tokens)
/** 压缩后的目标 token 数 */
compactionTarget: number; // default: 40000 (40k tokens)
/** 启用的上下文类型 */
enabledContextTypes: ContextType[];
}
/** 默认配置 */
export const DEFAULT_CONTEXT_CONFIG: ContextInjectorConfig = {
maxContextTokens: 4000,
enableAutoCompaction: true,
compactionThreshold: 80000,
compactionTarget: 40000,
enabledContextTypes: Object.values(ContextType),
};
// ============================================================
// Conversation Context (传入 Coordinator 的完整上下文)
// ============================================================
/** 对话上下文(从 ConversationService 传入) */
export interface ConversationContext {
conversationId: string;
userId: string;
messages: Array<{
role: 'user' | 'assistant' | 'system';
content: string;
metadata?: Record<string, unknown>;
createdAt: Date;
}>;
consultingState?: {
currentStage?: string;
collectedInfo?: Record<string, unknown>;
assessmentResult?: Record<string, unknown>;
stageHistory?: Array<{
stageId: string;
enteredAt: string;
turnsInStage: number;
}>;
};
deviceInfo?: {
ip?: string;
userAgent?: string;
fingerprint?: string;
region?: string;
};
agentHistory?: Array<{
agentType: string;
input: string;
output: string;
durationMs: number;
timestamp: string;
}>;
isNewConversation: boolean;
}

View File

@ -0,0 +1,185 @@
/**
* Stream Event Type Definitions
* StreamChunk Agent
*/
import { SpecialistAgentType, CoordinatorTurnStats, ConsultingStateReport } from './agent.types';
// ============================================================
// Base Stream Event
// ============================================================
/** 所有流式事件的基础类型 */
export interface BaseStreamEvent {
type: string;
timestamp?: number;
}
// ============================================================
// Text & Content Events (与现有 StreamChunk 兼容)
// ============================================================
/** 文本内容(流式增量) */
export interface TextStreamEvent extends BaseStreamEvent {
type: 'text';
content: string;
index?: number;
}
/** 工具调用事件 */
export interface ToolUseStreamEvent extends BaseStreamEvent {
type: 'tool_use';
toolName: string;
toolInput: Record<string, unknown>;
toolUseId: string;
}
/** 工具结果事件 */
export interface ToolResultStreamEvent extends BaseStreamEvent {
type: 'tool_result';
toolName: string;
toolUseId: string;
result: string;
isError: boolean;
}
// ============================================================
// Agent Events (新增:多 Agent 协作事件)
// ============================================================
/** Agent 开始工作 */
export interface AgentStartEvent extends BaseStreamEvent {
type: 'agent_start';
agentType: SpecialistAgentType;
agentName: string; // 用户友好名称,如 "政策专家"
description?: string; // 正在做什么,如 "正在查询TTPS政策..."
}
/** Agent 中间进度 */
export interface AgentProgressEvent extends BaseStreamEvent {
type: 'agent_progress';
agentType: SpecialistAgentType;
message: string; // 进度描述
}
/** Agent 完成工作 */
export interface AgentCompleteEvent extends BaseStreamEvent {
type: 'agent_complete';
agentType: SpecialistAgentType;
agentName: string;
durationMs: number;
success: boolean;
}
/** Coordinator 正在思考/编排 */
export interface CoordinatorThinkingEvent extends BaseStreamEvent {
type: 'coordinator_thinking';
phase: 'analyzing' | 'orchestrating' | 'synthesizing';
message?: string;
}
// ============================================================
// State & Control Events
// ============================================================
/** 咨询状态更新 */
export interface StateUpdateEvent extends BaseStreamEvent {
type: 'state_update';
state: ConsultingStateReport;
}
/** Token 使用情况 */
export interface UsageEvent extends BaseStreamEvent {
type: 'usage';
usage: {
inputTokens: number;
outputTokens: number;
cacheCreationTokens?: number;
cacheReadTokens?: number;
};
costUsd: number;
turnStats?: CoordinatorTurnStats;
}
/** 流结束事件 */
export interface EndStreamEvent extends BaseStreamEvent {
type: 'end';
totalTokens: {
inputTokens: number;
outputTokens: number;
};
totalCostUsd: number;
turnCount: number;
agentsUsed: SpecialistAgentType[];
}
/** 错误事件 */
export interface ErrorStreamEvent extends BaseStreamEvent {
type: 'error';
code: 'MAX_TURNS_REACHED' | 'BUDGET_EXCEEDED' | 'USER_ABORTED' | 'API_ERROR' | 'RATE_LIMIT' | 'OVERLOADED' | 'INTERNAL_ERROR';
message: string;
recoverable: boolean;
}
// ============================================================
// Union Type
// ============================================================
/** 所有可能的流式事件 */
export type StreamEvent =
| TextStreamEvent
| ToolUseStreamEvent
| ToolResultStreamEvent
| AgentStartEvent
| AgentProgressEvent
| AgentCompleteEvent
| CoordinatorThinkingEvent
| StateUpdateEvent
| UsageEvent
| EndStreamEvent
| ErrorStreamEvent;
// ============================================================
// Agent Friendly Names (用于前端展示)
// ============================================================
export const AGENT_DISPLAY_NAMES: Record<SpecialistAgentType, string> = {
[SpecialistAgentType.POLICY_EXPERT]: '政策专家',
[SpecialistAgentType.ASSESSMENT_EXPERT]: '评估专家',
[SpecialistAgentType.STRATEGIST]: '策略顾问',
[SpecialistAgentType.OBJECTION_HANDLER]: '异议处理专家',
[SpecialistAgentType.CASE_ANALYST]: '案例分析师',
[SpecialistAgentType.MEMORY_MANAGER]: '记忆管理',
};
export const AGENT_DESCRIPTIONS: Record<SpecialistAgentType, string> = {
[SpecialistAgentType.POLICY_EXPERT]: '正在查询移民政策信息...',
[SpecialistAgentType.ASSESSMENT_EXPERT]: '正在评估您的移民资格...',
[SpecialistAgentType.STRATEGIST]: '正在分析最佳咨询策略...',
[SpecialistAgentType.OBJECTION_HANDLER]: '正在准备专业解答...',
[SpecialistAgentType.CASE_ANALYST]: '正在查找类似成功案例...',
[SpecialistAgentType.MEMORY_MANAGER]: '正在整理您的信息...',
};
// ============================================================
// WebSocket Event Names (前端监听)
// ============================================================
export const WS_EVENTS = {
// 现有事件(保持兼容)
CONNECTED: 'connected',
MESSAGE: 'message',
STREAM_START: 'stream_start',
STREAM_CHUNK: 'stream_chunk',
STREAM_END: 'stream_end',
TOOL_CALL: 'tool_call',
TOOL_RESULT: 'tool_result',
ERROR: 'error',
// 新增事件(多 Agent
AGENT_START: 'agent_start',
AGENT_PROGRESS: 'agent_progress',
AGENT_COMPLETE: 'agent_complete',
COORDINATOR_THINKING: 'coordinator_thinking',
STATE_UPDATE: 'state_update',
} as const;

View File

@ -7,6 +7,10 @@
* 3.
* 4.
* 5.
*
* @deprecated CoordinatorAgentService
* 使 Agent Coordinator + 6 Agent
* infrastructure/agents/coordinator/coordinator-agent.service.ts
*/
import { Injectable, OnModuleInit } from '@nestjs/common';

View File

@ -1,3 +1,9 @@
/**
* @deprecated CoordinatorAgentService
* Agent V1使 + 线
* 使 Agent
* infrastructure/agents/coordinator/coordinator-agent.service.ts
*/
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Anthropic from '@anthropic-ai/sdk';

View File

@ -8,19 +8,23 @@ import { TokenUsageService } from './token-usage.service';
import { StrategyEngineService } from './strategy/strategy-engine.service';
import { TokenUsageORM } from '../database/postgres/entities/token-usage.orm';
import { KnowledgeModule } from '../knowledge/knowledge.module';
import { AgentsModule } from '../agents/agents.module';
@Global()
@Module({
imports: [
ConfigModule,
KnowledgeModule,
AgentsModule,
TypeOrmModule.forFeature([TokenUsageORM]),
],
providers: [
ClaudeAgentService,
/** @deprecated Use CoordinatorAgentService from AgentsModule instead */
ClaudeAgentServiceV2,
ImmigrationToolsService,
TokenUsageService,
/** @deprecated Strategy engine is now handled by Strategist Agent */
StrategyEngineService,
],
exports: [

View File

@ -1,6 +1,10 @@
/**
* -
*
*
* @deprecated Agent
* CoordinatorAgentService Claude
* infrastructure/agents/coordinator/coordinator-agent.service.ts
*/
export enum IntentType {

View File

@ -1,6 +1,10 @@
/**
* -
* AI
*
* @deprecated Agent
* Coordinator Agent
* infrastructure/agents/prompts/coordinator-system-prompt.ts
*/
import { IntentType, IntentResult } from './intent-classifier';

View File

@ -6,6 +6,10 @@
* 2.
* 3. System Prompt
* 4.
*
* @deprecated Agent
* CoordinatorAgentService + StrategistService
* infrastructure/agents/coordinator/coordinator-agent.service.ts
*/
import { Injectable } from '@nestjs/common';

View File

@ -286,4 +286,31 @@ export class KnowledgeClientService implements OnModuleInit {
return false;
}
}
// ============================================================
// Convenience Methods (for Specialist Agents)
// ============================================================
/**
* 便 Agent 使
* retrieveForPrompt
*/
async search(query: string, category?: string): Promise<string> {
const result = await this.retrieveForPrompt({ query, category });
return result || '';
}
/**
* 便 Agent 使
* userId
*/
async getUserContext(query: string, userId?: string): Promise<string> {
if (!userId) {
// 无 userId 时做通用检索
return await this.search(query);
}
const memories = await this.searchUserMemories({ userId, query, limit: 5 });
if (memories.length === 0) return '';
return memories.map(m => `[${m.memoryType}] (重要度:${m.importance}) ${m.content}`).join('\n');
}
}

View File

@ -91,8 +91,14 @@ export enum WsMessageType {
STREAM_CHUNK = 'stream_chunk',
STREAM_END = 'stream_end',
TOOL_CALL = 'tool_call',
TOOL_RESULT = 'tool_result',
ERROR = 'error',
CONNECTED = 'connected',
// Multi-Agent Events
AGENT_START = 'agent_start',
AGENT_COMPLETE = 'agent_complete',
COORDINATOR_THINKING = 'coordinator_thinking',
}
export interface SendMessageDto {

View File

@ -0,0 +1,89 @@
import { useChatStore } from '../stores/chatStore';
import { CheckCircle2, Loader2, Brain } from 'lucide-react';
import { clsx } from 'clsx';
const AGENT_DISPLAY_NAMES: Record<string, string> = {
policy: '政策专家',
assessment: '评估专家',
strategy: '策略顾问',
objection: '异议处理专家',
case_analysis: '案例分析师',
memory: '记忆管理',
};
const PHASE_DISPLAY: Record<string, string> = {
analyzing: '正在分析问题...',
orchestrating: '正在协调专家团队...',
synthesizing: '正在综合分析结果...',
};
export function AgentStatusIndicator() {
const { activeAgents, completedAgents, coordinatorPhase, coordinatorMessage } = useChatStore();
const hasActivity = activeAgents.length > 0 || coordinatorPhase !== null;
if (!hasActivity) return null;
return (
<div className="agent-status-enter max-w-3xl mx-auto mb-3">
<div className="rounded-xl border border-secondary-200 bg-secondary-50 px-4 py-3 space-y-2">
{/* Coordinator phase */}
{coordinatorPhase && (
<div className="flex items-center gap-2 text-sm text-secondary-600">
<Brain className="w-4 h-4 text-primary-500 agent-pulse" />
<span className="font-medium text-primary-600">
{PHASE_DISPLAY[coordinatorPhase] || coordinatorMessage}
</span>
</div>
)}
{/* Active agents */}
{activeAgents.length > 0 && (
<div className="space-y-1.5">
{activeAgents.map((agent) => (
<div
key={`${agent.messageId}-${agent.agentType}`}
className="agent-enter flex items-center gap-2 text-sm"
>
<Loader2 className="w-3.5 h-3.5 text-primary-500 animate-spin flex-shrink-0" />
<span className="text-secondary-700">
<span className="font-medium text-secondary-800">
{AGENT_DISPLAY_NAMES[agent.agentType] || agent.agentName}
</span>
{agent.description && (
<span className="text-secondary-500 ml-1">- {agent.description}</span>
)}
</span>
</div>
))}
</div>
)}
{/* Recently completed agents (shown inline) */}
{completedAgents.length > 0 && activeAgents.length > 0 && (
<div className="flex flex-wrap gap-2 pt-1 border-t border-secondary-200">
{completedAgents.map((agent) => (
<div
key={`${agent.messageId}-${agent.agentType}-done`}
className={clsx(
'flex items-center gap-1 text-xs px-2 py-0.5 rounded-full',
agent.success
? 'bg-green-50 text-green-600'
: 'bg-red-50 text-red-500',
)}
>
<CheckCircle2 className="w-3 h-3" />
<span>{AGENT_DISPLAY_NAMES[agent.agentType] || agent.agentName}</span>
{agent.durationMs > 0 && (
<span className="text-secondary-400 ml-0.5">
{(agent.durationMs / 1000).toFixed(1)}s
</span>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -2,6 +2,7 @@ import { useRef, useEffect } from 'react';
import { MessageBubble } from './MessageBubble';
import { InputArea } from './InputArea';
import { TypingIndicator } from './TypingIndicator';
import { AgentStatusIndicator } from './AgentStatusIndicator';
import { useChatStore } from '../stores/chatStore';
import { useChat } from '../hooks/useChat';
import { MessageSquare, Menu } from 'lucide-react';
@ -80,8 +81,9 @@ export function ChatWindow() {
)}
</div>
{/* Input area */}
{/* Agent status + Input area */}
<div className="border-t border-secondary-200 bg-white p-4">
<AgentStatusIndicator />
<div className="max-w-3xl mx-auto">
<InputArea
onSend={sendMessage}

View File

@ -27,6 +27,10 @@ export function useChat() {
addConversation,
setCurrentConversation,
setConnected,
addActiveAgent,
completeAgent,
setCoordinatorPhase,
clearAgentState,
} = useChatStore();
// 文件上传状态
@ -84,6 +88,7 @@ export function useChat() {
console.log('Stream started:', data);
setStreaming(true);
clearStreamContent();
clearAgentState();
});
socket.on('stream_chunk', (data) => {
@ -105,6 +110,30 @@ export function useChat() {
addMessage(conversationId, message);
setStreaming(false);
clearStreamContent();
clearAgentState();
});
// Multi-agent events
socket.on('agent_start', (data) => {
console.log('Agent started:', data);
addActiveAgent({
messageId: data.messageId,
conversationId: data.conversationId,
agentType: data.agentType,
agentName: data.agentName,
description: data.description,
startedAt: Date.now(),
});
});
socket.on('agent_complete', (data) => {
console.log('Agent completed:', data);
completeAgent(data.messageId, data.agentType, data.durationMs, data.success);
});
socket.on('coordinator_thinking', (data) => {
console.log('Coordinator thinking:', data);
setCoordinatorPhase(data.phase, data.message);
});
socket.on('tool_call', (data) => {

View File

@ -10,6 +10,27 @@ export interface FileAttachment {
thumbnailUrl?: string;
}
export interface ActiveAgent {
messageId: string;
conversationId: string;
agentType: string;
agentName: string;
description: string;
startedAt: number;
}
export interface CompletedAgent {
messageId: string;
conversationId: string;
agentType: string;
agentName: string;
durationMs: number;
success: boolean;
completedAt: number;
}
export type CoordinatorPhase = 'analyzing' | 'orchestrating' | 'synthesizing' | null;
export interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
@ -62,6 +83,16 @@ interface ChatState {
// WebSocket
isConnected: boolean;
setConnected: (connected: boolean) => void;
// Multi-agent
activeAgents: ActiveAgent[];
completedAgents: CompletedAgent[];
coordinatorPhase: CoordinatorPhase;
coordinatorMessage: string;
addActiveAgent: (agent: ActiveAgent) => void;
completeAgent: (messageId: string, agentType: string, durationMs: number, success: boolean) => void;
setCoordinatorPhase: (phase: CoordinatorPhase, message: string) => void;
clearAgentState: () => void;
}
export const useChatStore = create<ChatState>((set, get) => ({
@ -153,4 +184,48 @@ export const useChatStore = create<ChatState>((set, get) => ({
// WebSocket
isConnected: false,
setConnected: (connected) => set({ isConnected: connected }),
// Multi-agent
activeAgents: [],
completedAgents: [],
coordinatorPhase: null,
coordinatorMessage: '',
addActiveAgent: (agent) =>
set((state) => ({
activeAgents: [...state.activeAgents, agent],
})),
completeAgent: (messageId, agentType, durationMs, success) =>
set((state) => {
const agent = state.activeAgents.find(
(a) => a.messageId === messageId && a.agentType === agentType,
);
return {
activeAgents: state.activeAgents.filter(
(a) => !(a.messageId === messageId && a.agentType === agentType),
),
completedAgents: agent
? [
...state.completedAgents,
{
messageId: agent.messageId,
conversationId: agent.conversationId,
agentType: agent.agentType,
agentName: agent.agentName,
durationMs,
success,
completedAt: Date.now(),
},
]
: state.completedAgents,
};
}),
setCoordinatorPhase: (phase, message) =>
set({ coordinatorPhase: phase, coordinatorMessage: message }),
clearAgentState: () =>
set({
activeAgents: [],
completedAgents: [],
coordinatorPhase: null,
coordinatorMessage: '',
}),
}));

View File

@ -94,3 +94,47 @@
.typing-dot:nth-child(3) {
animation-delay: 0.4s;
}
/* Agent status indicator */
@keyframes agentPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
@keyframes agentSlideIn {
from {
opacity: 0;
transform: translateX(-8px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes agentSlideOut {
from {
opacity: 1;
transform: translateX(0);
}
to {
opacity: 0;
transform: translateX(8px);
}
}
.agent-pulse {
animation: agentPulse 1.5s ease-in-out infinite;
}
.agent-enter {
animation: agentSlideIn 0.3s ease-out;
}
.agent-status-enter {
animation: fadeIn 0.3s ease-out;
}