# 01 - Coordinator Agent (主协调器) 设计详解

## 1. 核心职责

Coordinator 是唯一直接面对用户的 Agent。它的职责是：

1. **理解用户意图** — 无需额外的 IntentClassifier，LLM 自行判断
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 };
  }
}
```
