第 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. ToolLoopAgent 在没有显式配置 stopWhen 时,默认的最大步数是多少?
2. 把子代理包装成工具时,为什么必须在 execute 中透传 abortSignal?
3. 本章中 consultExpert 工具的 execute 返回给主代理的是什么?
4. 关于 stopWhen: [isStepCount(6), hasToolCall("createTicket")] 的行为,正确的说法是?
🛠️ 动手实践
- 给系统增加一个
escalateTicket工具(升级工单优先级),并在 instructions 中约定何时使用它。 - 把
consultExpert改造为「流式进度版」:子代理执行期间先返回初步提示(参考官方 Streaming Subagent Progress 思路)。 - 将
stopWhen改为数组[isStepCount(6), hasToolCall('createTicket')],观察「创建完工单立即停止」的行为差异。