Skip to content

第 21 章 · 实战二:多代理客服工单系统

本章目标:

  • ToolLoopAgent 构建一个可查单、创建工单、转接专家的多代理客服系统
  • 掌握 tool + zod 定义业务工具的工程化写法
  • stopWhen: isStepCount() 控制多步循环成本
  • 学会「子代理包装成工具」的委派(delegation)模式与取消信号传递

21.1 场景与架构设计

我们要实现一个客服系统,主代理(Triage Agent)负责理解用户诉求并调度能力:

用户 ──▶ 主代理 TriageAgent
           │  tools:
           ├─ searchTicket   查询工单
           ├─ createTicket   创建工单
           └─ consultExpert ──▶ 技术支持子代理 ExpertAgent
                                  (独立 instructions + 独立工具)

设计原则:

  • 主代理只做路由与汇总,不直接处理技术细节;
  • 专业能力下沉到工具:数据操作是普通工具,深度分析交给子代理;
  • 循环必须有上限:防止模型反复调用工具导致费用失控。

21.2 模拟工单数据与基础工具

先准备内存数据源,再用 tool + zod 定义两个业务工具:

ts
import { tool } from 'ai';
import { z } from 'zod';

// 内存「数据库」
const tickets = [
  { id: 'T-1001', title: '无法登录', status: 'open', priority: 'high' },
  { id: 'T-1002', title: '导出报表报错', status: 'resolved', priority: 'medium' },
];

export const searchTicket = tool({
  description: '按工单号或状态查询工单列表',
  inputSchema: z.object({
    ticketId: z.string().optional().describe('工单号,如 T-1001'),
    status: z.enum(['open', 'resolved']).optional().describe('按状态过滤'),
  }),
  execute: async ({ ticketId, status }) => {
    return tickets.filter(
      (t) =>
        (!ticketId || t.id === ticketId) && (!status || t.status === status),
    );
  },
});

export const createTicket = tool({
  description: '创建新的客服工单',
  inputSchema: z.object({
    title: z.string().describe('问题标题'),
    priority: z.enum(['low', 'medium', 'high']).describe('优先级'),
  }),
  execute: async ({ title, priority }) => {
    const id = `T-${1000 + tickets.length + 1}`;
    tickets.push({ id, title, status: 'open', priority });
    return { id, message: '工单已创建' };
  },
});

注意 inputSchema 里用 .describe() 给每个字段写了用途说明——这些描述会进入模型的工具元数据,直接影响调用准确率。

21.3 技术支持子代理

子代理拥有自己的 instructions 与专属工具,独立成一个「专家」:

ts
import { ToolLoopAgent } from 'ai';
import { z } from 'zod';
import { getLanguageModel } from './provider';

// 模拟的文档检索工具(真实场景接 RAG,见第 20 章)
const techDocs = [
  '无法登录排查:1. 确认账号未锁定;2. 清理浏览器缓存;3. 重置密码。',
  '导出报表报错:多为权限缺失,需管理员在后台授予 export 权限。',
];

export const expertAgent = new ToolLoopAgent({
  model: getLanguageModel(),
  instructions: `你是资深技术支持工程师。
收到问题时:先分析可能原因,给出分步排查方案。
回答务必精炼,控制在 200 字以内。`,
  tools: {
    searchDocs: {
      description: '在技术文档库中搜索解决方案',
      inputSchema: z.object({
        query: z.string().describe('搜索关键词'),
      }),
      execute: async ({ query }) =>
        techDocs.filter((doc) => doc.includes(query.slice(0, 4))),
    },
  },
});

子代理与主代理可以使用不同的 model——比如主代理用便宜的快速模型做路由,专家代理用更强的模型做诊断。

21.4 委派模式:把子代理包装成工具

这是 AI SDK 官方推荐的多代理编排方式:用一个普通 tool 封装子代理调用:

ts
import { tool } from 'ai';
import { z } from 'zod';
import { expertAgent } from './expert-agent';

export const consultExpert = tool({
  description: '将复杂技术问题转给资深技术支持专家深入分析',
  inputSchema: z.object({
    question: z.string().describe('需要专家分析的技术问题描述'),
  }),
  execute: async ({ question }, { abortSignal }) => {
    const result = await expertAgent.generate({
      prompt: question,
      abortSignal, // 用户取消时同步终止子代理
    });
    return { expertOpinion: result.text };
  },
});

两个关键点:

  • abortSignal 必须透传:用户取消请求时,取消信号沿工具 → 子代理一路传播,避免僵尸任务;
  • 工具阻塞到子代理返回为止,主代理拿到的是最终结论文本。

21.5 主代理:多步循环与步数上限

ts
import { ToolLoopAgent, isStepCount } from 'ai';
import { searchTicket, createTicket } from './ticket-tools';
import { consultExpert } from './expert-tool';
import { getLanguageModel } from './provider'; // 内部兼容 AI Gateway 与自定义 OpenAI 兼容 Provider,见第 20 章

export const triageAgent = new ToolLoopAgent({
  model: getLanguageModel(),
  instructions: `你是客服分诊助手:
1. 用户查询订单/工单 → 调用 searchTicket;
2. 需要新建工单 → 先确认标题与优先级,再调用 createTicket;
3. 复杂技术问题 → 调用 consultExpert 转接专家;
4. 最后用中文向用户简洁汇总结果。`,
  tools: {
    searchTicket,
    createTicket,
    consultExpert,
  },
  stopWhen: isStepCount(8), // 默认上限是 20 步,这里收紧控制成本
});

stopWhen 是循环的「刹车」:每次工具调用产生结果后检查一次条件,满足即停止。内置条件还有 hasToolCall(...toolNames)(某工具被调用即停)与数组组合(任一满足即停)。

21.6 运行完整系统

ts
import 'dotenv/config';
import * as readline from 'node:readline/promises';
import { triageAgent } from './triage-agent';

const terminal = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

while (true) {
  const userInput = await terminal.question('用户: ');
  if (!userInput.trim() || userInput === 'exit') break;

  const result = await triageAgent.generate({
    prompt: userInput,
  });

  console.log('客服:', result.text);
  console.log(`(共 ${result.steps.length} 步)`);
}

terminal.close();

对话示例:

text
用户: 帮我查一下 T-1001 的进度,另外这个问题一直没解决,帮我建个高优先级工单
客服: 已为您查询:T-1001「无法登录」当前为 open 状态。
      已创建高优先级工单 T-1003,技术人员将尽快跟进。(共 4 步)

本章小结

  • 多代理系统 = 主代理路由 + 业务工具 + 子代理委派,职责分离让每个环节都简单可控;
  • tool({ description, inputSchema, execute }) 配合 zod.describe() 提供高质量工具元数据;
  • 「子代理包装成工具」是官方推荐的委派模式,abortSignal 透传保证取消语义正确;
  • stopWhen: isStepCount(n) 为多步循环设置硬性成本上限(默认 20 步);
  • agent.generate() 返回 steps 可用于观测每次调用的实际开销。

🛠️ 动手实践

  1. 给系统增加一个 escalateTicket 工具(升级工单优先级),并在 instructions 中约定何时使用它。
  2. consultExpert 改造为「流式进度版」:子代理执行期间先返回初步提示(参考官方 Streaming Subagent Progress 思路)。
  3. stopWhen 改为数组 [isStepCount(6), hasToolCall('createTicket')],观察「创建完工单立即停止」的行为差异。