Skip to content

第 19 章 · 实战一:生产级智能客服 Agent

本章目标:综合运用前 18 章所学,从零构建一个电商售后客服 Agent——意图识别分流、订单查询、退换货政策 RAG 问答、人工转接与会话记忆一个不少;并把成本分级路由、密钥安全、评估闭环三条生产红线在构建过程中落地,而不是事后补课。

19.1 需求分析与架构设计

先明确业务边界。这个客服 Agent 只做售后场景的四件事:

需求对应能力用到章节
闲聊/简单问题快速应答廉价小模型分流ch08 branch
查订单状态queryOrder 工具ch05 createTool
退换货政策问答RAG 检索政策文档ch12
情绪激动/超权限诉求workflow suspend 转人工ch09

整体架构如下——意图路由是成本与质量的总闸门

text
用户消息


┌──────────────────┐   简单/闲聊(~70% 流量)
│  意图路由 triage  │──────────────▶ gpt-5-mini 直接回复
│  (gpt-5-mini)   │
└───────┬──────────┘
        │ 业务问题(查单/退换货)

┌─────────────────────────────────┐
│  expertAgent(claude-sonnet)    │
│  ├─ queryOrder 工具 ──▶ 订单 DB │
│  ├─ RAG 政策检索 ──▶ 向量库      │
│  └─ Memory(按用户隔离会话)      │
└───────┬─────────────────────────┘
        │ 情绪激动 / 超出退款权限

   workflow.suspend() ──▶ 人工客服接管

设计决策只有一条主线:让便宜的模型处理能应付的问题,贵的模型只花在刀刃上

19.2 项目骨架与模型分级路由

复用第 1 章脚手架,新增目录结构:

bash
npm install @mastra/core @mastra/libsql @mastra/memory zod
mkdir -p src/mastra/{agents,tools,workflows}

先写两个档位的 Agent。注意 instructions 遵循第 4 章的三原则(定角色、划边界、给格式):

typescript
// src/mastra/agents/triage-agent.ts —— 一线分诊:便宜模型扛住大多数流量
import { Agent } from '@mastra/core/agent';

export const triageAgent = new Agent({
  id: 'triage',
  name: 'Triage',
  instructions: `
你是电商售后的第一道接待。职责:
- 判断用户意图:闲聊问候 / 订单查询 / 退换货咨询 / 投诉
- 闲聊与简单常见问题直接一句话回答
- 涉及具体订单或政策的请求,输出「转接」并附上用户的原始诉求摘要
始终使用中文,回复不超过 80 字。`,
  // 低成本档:意图识别不需要旗舰模型
  model: 'openai/gpt-5-mini',
});
typescript
// src/mastra/agents/expert-agent.ts —— 专家坐席:只在必要时消耗高能力模型
import { Agent } from '@mastra/core/agent';

export const expertAgent = new Agent({
  id: 'expert',
  name: 'Support Expert',
  instructions: `
你是资深售后专员,处理订单查询与退换货咨询。
规则:
- 回答政策问题时必须依据提供的政策资料,不得编造条款
- 查询订单前必须拿到合法订单号
- 用户情绪激动或要求超出标准政策时,回复「为您转接人工客服」
始终使用中文。`,
  // 高能力档:工具调用 + 政策推理
  model: 'anthropic/claude-sonnet-4-5',
});

用 workflow 的 branch() 把两者串成分流主干(第 8 章语法):

typescript
// src/mastra/workflows/support-router.ts —— 意图分流主干
import { createStep, createWorkflow } from '@mastra/core/workflows';
import { z } from 'zod';

const classifyStep = createStep({
  id: 'classify',
  inputSchema: z.object({ message: z.string(), userId: z.string() }),
  outputSchema: z.object({ simple: z.boolean(), message: z.string(), userId: z.string() }),
  execute: async ({ inputData, mastra }) => {
    // 让便宜的小模型判断是否为简单问题
    const res = await mastra.getAgent('triage').generate(inputData.message);
    const simple = !res.text.includes('转接');
    return { simple, message: inputData.message, userId: inputData.userId };
  },
});

export const supportFlow = createWorkflow({
  id: 'support-router',
  inputSchema: z.object({ message: z.string(), userId: z.string() }),
  outputSchema: z.object({ answer: z.string() }),
})
  .then(classifyStep)
  .branch([
    // 简单问题:直接采纳 triage 的回答(已在 classify 中缓存)
    [async ({ inputData }) => inputData.simple, answerSimpleStep],
    // 业务问题:交给专家坐席走工具 + RAG 全链路
    [async () => true, expertAnswerStep],
  ])
  .commit();

成本收益

典型电商客服流量中闲聊与常见问题占比约 70%。全量跑旗舰模型的账单,在分流后通常能降到原来的三分之一以下。

19.3 订单查询工具与政策 RAG

专家坐席需要两件武器:查订单的工具和检索政策的能力。

typescript
// src/mastra/tools/query-order.ts —— 订单查询工具:连模拟数据库
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

// 演示用模拟数据;生产环境替换为真实订单服务 API
const mockOrderDb: Record<string, { status: string; amount: number; item: string }> = {
  'SO-2026-0001': { status: '已发货', amount: 299, item: '无线耳机' },
  'SO-2026-0002': { status: '待付款', amount: 1299, item: '机械键盘' },
};

export const queryOrderTool = createTool({
  id: 'query-order',
  description: '根据订单号查询订单状态、金额与商品,仅在用户提供订单号时调用',
  inputSchema: z.object({
    orderId: z.string().regex(/^SO-\d{4}-\d{4}$/, '订单号格式如 SO-2026-0001'),
  }),
  outputSchema: z.object({
    found: z.boolean(),
    status: z.string().optional(),
    amount: z.number().optional(),
    item: z.string().optional(),
  }),
  execute: async ({ inputData }) => {
    // 最小权限原则:这里只读,绝不给写库凭据
    const order = mockOrderDb[inputData.orderId];
    if (!order) return { found: false };
    return { found: true, ...order };
  },
});

再把退换货政策做成可检索的 RAG 片段(简化版实现,完整向量库见第 12 章):

typescript
// src/mastra/tools/policy-rag.ts —— 退换货政策检索:关键词匹配演示版
import { createTool } from '@mastra/core/tools';
import { z } from 'zod';

// 生产环境替换为向量库语义检索(pgvector / LibSQL vector)
const policyDocs = [
  { topic: '七天无理由', text: '签收后 7 天内不影响二次销售可无理由退货,运费买家承担。' },
  { topic: '质量问题', text: '质量问题退货免运费,需提供照片凭证,审核 24 小时内完成。' },
  { topic: '退款时效', text: '退货签收后 3 个工作日内原路退回,具体以支付平台为准。' },
];

export const policyRagTool = createTool({
  id: 'search-policy',
  description: '检索退换货政策条文,回答政策相关问题前必须先调用本工具',
  inputSchema: z.object({ keyword: z.string().describe('政策主题关键词,如"退款"') }),
  outputSchema: z.object({ clauses: z.array(z.string()) }),
  execute: async ({ inputData }) => ({
    clauses: policyDocs
      .filter((d) => d.text.includes(inputData.keyword) || d.topic.includes(inputData.keyword))
      .map((d) => d.text),
  }),
});

把两个工具挂到专家坐席上:

typescript
// src/mastra/index.ts —— 注册中心组装
import { Mastra } from '@mastra/core';
import { expertAgent } from './agents/expert-agent';
import { triageAgent } from './agents/triage-agent';

// 工具通过构造参数挂载,Agent 自主决定何时调用
expertAgent.__registerTools?.({ queryOrderTool, policyRagTool });

export const mastra = new Mastra({
  agents: { triageAgent, expertAgent },
  workflows: { supportFlow },
});

19.4 会话记忆与人工转接

客服最怕"每次都要重新解释一遍"。用 Memory 按 resource/thread 隔离每个用户的会话(第 10 章):

typescript
// generate 时传入 memory 选项,同一 userId 自动延续上下文
await expertAgent.generate('我的耳机左声道没声音了', {
  memory: {
    resource: userId,     // 用户身份:跨会话保留长期信息
    thread: `after-sale-${orderId ?? 'general'}`, // 一个工单一个线程
  },
});
// 用户下一轮问"刚才说的怎么申请?"时,Agent 能理解"刚才"指什么

转人工是客服 Agent 的安全阀。用 workflow 的 suspend 实现可控中断(第 9 章):

typescript
// src/mastra/workflows/handoff-step.ts —— 人工转接步骤
import { createStep } from '@mastra/core/workflows';
import { z } from 'zod';

export const handoffStep = createStep({
  id: 'handoff',
  inputSchema: z.object({ message: z.string(), userId: z.string() }),
  outputSchema: z.object({ escalated: z.boolean(), ticketId: z.string().optional() }),
  execute: async ({ inputData, suspend }) => {
    // 触发条件:负面情绪词 或 用户明确要求人工
    const angry = /(投诉|315|工商|垃圾|骗子|人工)/.test(inputData.message);
    if (angry) {
      // 挂起并携带上下文,人工坐席在工作台看到后 resume 继续
      await suspend({
        reason: '情绪升级或用户要求人工',
        transcript: inputData.message,
        userId: inputData.userId,
      });
    }
    return { escalated: false };
  },
});

判断逻辑放代码而不是提示词

"什么时候转人工"是业务规则,写进代码正则/阈值可以测试和审计;只靠提示词约束的转人工在压测下必然漏判。

19.5 上线前的安全与评估

密钥:零硬编码

所有 Provider 密钥只从环境变量读取——Mastra 的模型路由会自动读取 OPENAI_API_KEY / ANTHROPIC_API_KEY

bash
# .env(已加入 .gitignore,绝不提交)
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
LIBSQL_URL=file:memory.db
typescript
// ❌ 错误示范:任何形式的硬编码密钥迟早随仓库泄漏
// model: { apiKey: 'sk-abc123' }

// ✅ 正确:声明 provider/model 字符串即可,密钥由运行时环境注入
model: 'openai/gpt-5-mini'

再补一道 PII 脱敏保险——用户消息进 prompt 前打码手机号:

typescript
// src/mastra/processors/mask-pii.ts —— 输入脱敏 Processor
export const maskPii = {
  name: 'maskPii',
  processInput: ({ messages }) =>
    messages.map((m) => ({
      ...m,
      content:
        typeof m.content === 'string'
          ? // 中国大陆手机号脱敏:保留前三后四
            m.content.replace(/(1[3-9]\d)\d{4}(\d{4})/g, '$1****$2')
          : m.content,
    })),
};

质量:给客服配一个"阅卷老师"

上真实用户之前,先用 Scorer 对固定样本集跑准确率评估(第 15 章):

typescript
// src/mastra/scorers/policy-accuracy.ts —— 用廉价模型当阅卷老师
import { Faithfulness } from '@mastra/evals/nlp';

// 评估集:政策问答的标准答案对(来自真实客服记录)
const cases = [
  { query: '七天无理由谁出运费?', expectContains: '买家承担' },
  { query: '质量问题退货运费呢?', expectContains: '免运费' },
];

for (const c of cases) {
  const res = await mastra.getAgent('expert').generate(c.query);
  const pass = res.text.includes(c.expectContains);
  console.log(pass ? '✅' : '❌', c.query);
}

// Faithfulness scorer 衡量回答是否忠实于检索到的政策原文
const score = await Faithfulness.score({ input: cases[0].query });

把这段脚本挂进 CI:每次改 instructions 或换模型都必须重跑,通过率低于阈值直接阻断合并——这就是第 15 章说的评估闭环在本项目里的落点。

跑通一次完整对话

bash
npx mastra dev   # 启动开发服务器,Studio 打开 http://localhost:4111

在 Studio 的 Playground 里依次发送:「在吗」→「帮我查下订单 SO-2026-0001」→「耳机有质量问题怎么退货?」→「你们就是骗子!」,验证四条链路分别命中:小模型直答 → 工具查单 → 政策 RAG → suspend 转人工。

19.6 生产检查单

上线前逐项打勾,别靠运气:

  • [ ] 密钥全部来自环境变量/密管系统,git log -p 扫描无泄漏历史
  • [ ] 模型分级路由生效:统计确认多数流量走小模型
  • [ ] PII 脱敏 Processor 已启用,日志抽样复核无手机号明文
  • [ ] 工具最小权限:查询工具无写库凭据
  • [ ] CI 评估套件接入,通过率阈值已配置
  • [ ] suspend 转人工链路有人工坐席真的在接收端值守
  • [ ] Storage 外置且有备份策略(LibSQL 文件库仅限本地开发)
  • [ ] 关键指标有观测:P95 延迟、错误率、转人工率

本章小结

  • 架构主线一条:意图分流是小模型与大模型之间的总闸门;
  • 工具给行动力(查订单),RAG 给知识(政策问答),Memory 给连续性(按 resource/thread 隔离);
  • 转人工是安全阀:触发条件写在代码里而非提示词里;
  • 密钥零硬编码、PII 脱敏、CI 评估闭环——三条红线都在构建过程中落地。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 客服 Agent 采用模型分级路由的主要目的是?

2. "什么时候转人工"的逻辑应该写在哪里?

3. Memory 中 resource 与 thread 的正确搭配是?

4. 关于 API Key 的做法,正确的是?

🛠️ 动手实践

  1. queryOrderTool 增加 cancelOrder 写操作工具,但故意不给它配置生产数据库凭据,体会"最小权限"如何在代码结构层面强制实现。
  2. 在 Studio 中构造一条"我要投诉到 315"的消息,观察 suspend 是否触发,并手动 resume 该 workflow 完成人工处理闭环。
  3. 向评估集中补充 5 条你所在业务的真实客服问题,运行评估脚本,记录通过率并分析失败用例的共性原因。

下一章:第 20 章 · 综合实战:全栈 AI 应用