做一个客服 bot
本教程带你从零搭建一个能处理 80% 常见咨询、20% 升级转人工的客服 Agent。 完整代码可直接 deploy。
用户故事
“我们 SaaS 产品每天 200+ 咨询,重复问题多,但有些需要查内部知识库 + 转给真人。 想要:常规问题自动答;查询订单 / 账单走 tool call 经用户授权拉数据; 复杂 case 一键 Approval 推给人工 oncall。“
架构
用户消息 │ ▼DemoBot (HasheeAgent, WebSocket) │ ├─ 意图识别 (LLM intent classifier) │ │ │ ├─ FAQ 类 → 知识库检索 (RAG) → 直接回复 │ ├─ 订单查询 → tool_call (filesystem / external API) → 回复 │ ├─ 投诉 / 退款 → Approval artifact → oncall 接管 │ └─ 闲聊 / 不清楚 → 让 LLM 安全回复 + 主动澄清 │ └─ 全程会话归档(业务侧 Postgres,不依赖 Hashee 后端)完整代码
import { HasheeAgent } from "@hasheeai/agent-sdk-ts";import OpenAI from "openai";import { Pool } from "pg";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });const db = new Pool({ connectionString: process.env.DATABASE_URL });
const agent = await HasheeAgent.init({ agentId: process.env.HASHEE_AGENT_ID!, token: process.env.HASHEE_AGENT_TOKEN!, baseUrl: "https://api.hashee.ai", connectionMode: "websocket",});
agent.addMessageHandler(async (msg) => { if (msg.payload?.type !== "text") return; await agent.typing(msg.conversation_id);
// 1. 意图识别 const intent = await classifyIntent(msg.payload.text); console.log(`[intent] ${msg.sender_id}: ${intent}`);
// 2. 路由 switch (intent) { case "faq": await handleFaq(msg); break; case "order_query": await handleOrderQuery(msg); break; case "complaint": await handleComplaint(msg); break; default: await handleGeneral(msg); }
// 3. 归档 await archiveMessage(msg, intent);});
// === 1. 意图识别 ===
async function classifyIntent(text: string): Promise<string> { const r = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: "把用户问题分类:faq / order_query / complaint / general。只回类别词。" }, { role: "user", content: text }, ], temperature: 0, }); return r.choices[0]?.message?.content?.trim().toLowerCase() ?? "general";}
// === 2. FAQ 类(RAG)===
async function handleFaq(msg: any) { // 简化:业务侧已有 FAQ embedding 索引 const matches = await searchKnowledgeBase(msg.payload.text, 3); const context = matches.map((m) => `Q: ${m.question}\nA: ${m.answer}`).join("\n\n");
const r = await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: `你是客服。基于以下 FAQ 回答用户:\n\n${context}` }, { role: "user", content: msg.payload.text }, ], });
await agent.send(msg.conversation_id, { type: "text", text: r.choices[0]?.message?.content ?? "(空回复)", });}
async function searchKnowledgeBase(query: string, topK: number) { // 简化:用 PG 全文检索;生产环境用 pgvector / Weaviate / Qdrant const r = await db.query(` SELECT question, answer FROM faq_entries WHERE to_tsvector('simple', question) @@ plainto_tsquery('simple', $1) ORDER BY ts_rank(to_tsvector('simple', question), plainto_tsquery('simple', $1)) DESC LIMIT $2 `, [query, topK]); return r.rows;}
// === 3. 订单查询(Tool Call)===
async function handleOrderQuery(msg: any) { // 让客户端通过 tool call 提供订单号(如果对话历史里没有) const orderId = extractOrderId(msg.payload.text); if (!orderId) { await agent.send(msg.conversation_id, { type: "text", text: "麻烦提供下订单号(13 位数字)。", }); return; } const order = await fetchOrderFromBackend(orderId); await agent.sendArtifact(msg.conversation_id, { artifact: { subtype: "info", title: `订单 #${orderId}`, payload: { body: `**状态**:${order.status}\n**金额**:$${order.amount}\n**预计送达**:${order.eta}`, actions: [{ id: "track", label: "查看物流", url: order.tracking_url }], }, }, });}
// === 4. 投诉转人工(Approval)===
async function handleComplaint(msg: any) { await agent.send(msg.conversation_id, { type: "text", text: "我理解你的不满。让我把你的问题转给人工同事处理。", });
// 发 approval 卡给 oncall 群 const oncallConvId = process.env.ONCALL_GROUP_CONV_ID!; await agent.sendArtifact(oncallConvId, { artifact: { artifact_id: `escalation-${msg.message_id}`, subtype: "approval", title: "客户投诉转人工", payload: { request: { from_user: msg.sender_id, conversation_id: msg.conversation_id, issue: msg.payload.text.slice(0, 500), time: msg.created_at, }, approve_label: "我来接 (claim)", reject_label: "驳回", timeout_s: 600, }, }, });}
// === 5. 一般回复 / 兜底 ===
async function handleGeneral(msg: any) { await agent.send(msg.conversation_id, { type: "text", text: "抱歉,我没完全理解你的问题。是想:(a) 查询订单 (b) 报告问题 (c) 看常见问题?", });}
// === 工具 ===
function extractOrderId(text: string): string | null { return text.match(/\b\d{13}\b/)?.[0] ?? null;}
async function fetchOrderFromBackend(orderId: string) { // 替换为你的真实 API return { status: "已发货", amount: 99.99, eta: "2026-05-18", tracking_url: `https://my-shop.com/track/${orderId}`, };}
async function archiveMessage(msg: any, intent: string) { await db.query( "INSERT INTO cs_messages (msg_id, user_id, conv_id, content, intent, ts) VALUES ($1, $2, $3, $4, $5, NOW())", [msg.message_id, msg.sender_id, msg.conversation_id, msg.payload.text, intent], );}
// === 接收人工 oncall 的响应 ===
agent.addEventHandler(async (event) => { if (event.type !== "artifact_response") return; const ref = event.payload.ref_artifact; if (!ref?.startsWith("escalation-")) return;
if (event.payload.action === "approve") { // oncall claimed — 业务侧把用户对话路由给那个人工 const claimerName = event.sender_id; const customerConv = event.payload.payload.conversation_id; await agent.send(customerConv, { type: "text", text: `已为你接通 @<U:${claimerName}>,稍后他会跟进。`, }); }});
agent.addStatusHandler((s) => console.log(`[status] ${s}`));console.log("[cs-bot] up");部署
1. 环境变量
HASHEE_AGENT_ID=01906abc-...HASHEE_AGENT_TOKEN=hsk_...HASHEE_X25519_PRIVATE_BASE64=...HASHEE_ED25519_PRIVATE_BASE64=...DATABASE_URL=postgres://...OPENAI_API_KEY=sk-...ONCALL_GROUP_CONV_ID=01HZ...2. DB 表
CREATE TABLE faq_entries ( id SERIAL PRIMARY KEY, question TEXT NOT NULL, answer TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());CREATE INDEX ON faq_entries USING gin(to_tsvector('simple', question));
CREATE TABLE cs_messages ( msg_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, conv_id TEXT NOT NULL, content TEXT NOT NULL, intent TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL);3. 部署到 VPS(systemd)
详见 自托管 VPS。
升级路径
| 想做 | 改哪 |
|---|---|
| 加多语言(用户语言决定回复语言) | msg.payload.text 先 detect language → 把 system prompt 替换 |
| 提升 RAG 召回率 | 用 pgvector + embedding model 替代 PG 全文搜 |
| 自动摘要对话历史 | 加 summarizeConversation() 每天 cron 跑 |
| 多 LLM 切换(GPT / Claude / 本地) | 抽象 chatComplete() 接 multiple providers |
| 加情绪识别报警 | classifyIntent 加一类 “high_emotion”,触发优先转人工 |
| 自动评级 / NPS | 对话结束发 artifact form 收 1-5 星评分 |
监控指标
| 指标 | 目标 |
|---|---|
平均响应时长 (agent.typing → agent.send) | < 3 秒 (FAQ); < 8 秒 (订单查询) |
| 意图识别准确率 | > 85% (业务侧 sampling) |
| 转人工率 | 10-20% (低于 10% 可能漏识别 complaint;高于 20% 检查 LLM prompt) |
| 用户满意度 (CSAT) | > 4.0 / 5 |