ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Codex 100个真实案例 - 用AI做智能客服聊天机器人(意图识别+多轮对话)

Codex 100个真实案例 - 用AI做智能客服聊天机器人(意图识别+多轮对话) 1. 为什么客服机器人总是“听不懂人话”做智能客服聊天机器人最让人头疼的不是接不上大模型而是用户一句“我上周买的那单怎么还没到”系统直接回一句“请问您要咨询什么业务”。意图识别没命中多轮对话上下文丢了槽位提取全靠正则硬扛最后用户骂骂咧咧点了转人工。我试过把意图识别、槽位提取、多轮对话状态管理拆成三个独立模块每个模块单独调优效果比塞进一个大 prompt 里稳定得多。这套思路在电商客服场景里尤其明显查订单、退换货、产品咨询、投诉建议、打招呼、告别六类意图覆盖了 80% 的常见问题。剩下的长尾问题交给 FAQ 知识库做模糊匹配匹配不上再转人工。这篇文章要交付的是一条完整链路用 Codex 生成意图识别引擎TF-IDF 贝叶斯分类器 关键词精确匹配三重评分用槽位提取器从用户输入里抓订单号、手机号、退货原因用对话状态机维护多轮上下文最后通过 TaoToken 统一 Key 和 API 通道把模型调用接进来。适合正在做客服自动化、想跑通意图分类到上下文续接完整流程的开发者。整个项目骨架用 Node.js Express Socket.IO 做后端Vue 3 做 Web WidgetSQLite 存对话日志。下面从环境准备开始一步步把可复制的配置和验证动作给出来。2. TaoToken 前置统一 Key 与 API 通道配置在写业务代码之前先把模型调用通道配好。TaoToken 的作用是提供一个统一的 API 入口你不用在代码里硬编码多个厂商的 Key也不用为每个模型单独写适配层。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 。2.1 获取 API Key登录后进入控制台在 API Keys 页面创建一个新 Key。建议按项目命名比如smart-chatbot-dev方便后续区分环境。创建后立即复制保存页面刷新后不会再显示完整 Key。2.2 config.toml 骨架Codex 项目通常用config.toml管理模型配置。下面是一个可复制的骨架把api_key替换成你刚创建的值# config.toml - TaoToken 统一通道配置 [model] provider taotoken base_url https://taotoken.net/api api_key sk-你的TaoTokenKey default_model gpt-4o-mini timeout 30 [model.params] temperature 0.3 max_tokens 1024 top_p 0.9 [intent] confidence_threshold 0.3 unknown_retry_limit 3 [dialog] session_timeout_minutes 5 max_turns 20temperature设 0.3 是因为客服场景需要稳定输出不需要太多创造性。confidence_threshold设 0.3 是意图识别的置信度阈值低于这个值就返回 unknown触发转人工建议。2.3 环境变量注入不要把 Key 直接写进代码提交到仓库。用.env文件管理# .env TAOTOKEN_API_KEYsk-你的TaoTokenKey TAOTOKEN_BASE_URLhttps://taotoken.net/api PORT3200然后在server/src/app.js里通过dotenv加载require(dotenv).config(); const apiKey process.env.TAOTOKEN_API_KEY; const baseUrl process.env.TAOTOKEN_BASE_URL;这样本地开发和线上部署可以用不同的 Key互不干扰。3. 可复制配置意图识别引擎与槽位提取器3.1 意图定义文件 intents.json先让 Codex 生成意图定义。每个意图包含tag、patterns训练样本、responses回复模板、required_slots必需槽位。下面是六类核心意图的配置{ intents: [ { tag: greeting, patterns: [你好, 您好, 在吗, 有人吗, 客服在吗, 请问有人吗], responses: [您好我是智能客服小助手请问有什么可以帮您的], required_slots: [] }, { tag: query_order, patterns: [查订单, 我的订单, 订单状态, 物流信息, 快递到哪了, 什么时候发货, 包裹在哪], responses: [好的请提供您的订单号我来帮您查询。], required_slots: [order_id] }, { tag: return_goods, patterns: [退货, 换货, 退款, 我要退, 申请退货, 东西有问题想退, 七天无理由退货], responses: [了解请问您要退货的订单号是多少退货原因是什么], required_slots: [order_id, reason] }, { tag: product_inquiry, patterns: [这个产品怎么样, 有什么推荐, 产品介绍, 多少钱, 价格, 有没有优惠, 库存], responses: [请问您想了解哪款产品可以告诉我产品名称或型号。], required_slots: [product_name] }, { tag: complaint, patterns: [投诉, 我要投诉, 差评, 服务太差, 态度不好, 太慢了, 不满意], responses: [非常抱歉给您带来不好的体验请详细描述您遇到的问题。], required_slots: [description] }, { tag: farewell, patterns: [谢谢, 感谢, 拜拜, 再见, 没有问题了, 好的谢谢], responses: [不客气祝您生活愉快有问题随时找我。], required_slots: [] } ] }每个意图至少 10 条训练样本样本越贴近真实用户表达识别率越高。3.2 意图识别引擎核心逻辑意图识别用三重评分关键词精确匹配权重 0.2TF-IDF 相似度权重 0.4贝叶斯分类器权重 0.4。综合得分低于阈值返回 unknown。// server/src/intent-engine.js const natural require(natural); const fs require(fs); const path require(path); class IntentEngine { constructor() { this.tfidf new natural.TfIdf(); this.classifier new natural.BayesClassifier(); this.intents []; this.confidenceThreshold 0.3; this.initialized false; } async init() { const dataPath path.join(__dirname, ../../data/intents.json); const rawData fs.readFileSync(dataPath, utf-8); const { intents } JSON.parse(rawData); this.intents intents; intents.forEach((intent) { const document intent.patterns.join( ); this.tfidf.addDocument(document); intent.patterns.forEach(pattern { this.classifier.addDocument(pattern, intent.tag); }); }); this.classifier.train(); this.initialized true; console.log(意图引擎初始化完成已加载 ${intents.length} 个意图); } recognize(userInput) { if (!this.initialized) throw new Error(意图引擎未初始化); const input userInput.trim().toLowerCase(); const exactMatch this._exactMatch(input); if (exactMatch exactMatch.confidence 0.8) return exactMatch; const tfidfResult this._tfidfMatch(input); const bayesResult this._bayesMatch(input); const combinedScores this._combineScores(exactMatch, tfidfResult, bayesResult); let bestIntent null; let bestScore 0; for (const [tag, score] of Object.entries(combinedScores)) { if (score bestScore) { bestScore score; bestIntent tag; } } if (bestScore this.confidenceThreshold) { return { intent: unknown, confidence: bestScore, responses: [抱歉我没有理解您的意思。您可以换个说法试试或者输入转人工联系人工客服。], required_slots: [] }; } const intentData this.intents.find(i i.tag bestIntent); return { intent: bestIntent, confidence: bestScore, responses: intentData.responses, required_slots: intentData.required_slots || [] }; } _exactMatch(input) { let bestMatch null; let bestScore 0; for (const intent of this.intents) { for (const pattern of intent.patterns) { if (input.includes(pattern.toLowerCase())) { const score pattern.length / Math.max(input.length, 1); if (score bestScore) { bestScore Math.min(score * 1.5, 1.0); bestMatch { intent: intent.tag, confidence: bestScore, responses: intent.responses, required_slots: intent.required_slots || [] }; } } } } return bestMatch; } _tfidfMatch(input) { const scores {}; this.tfidf.tfidfs(input, (index, measure) { const tag this.intents[index].tag; scores[tag] measure; }); const maxScore Math.max(...Object.values(scores), 1); for (const tag of Object.keys(scores)) { scores[tag] scores[tag] / maxScore; } return scores; } _bayesMatch(input) { const classifications this.classifier.getClassifications(input); const scores {}; classifications.forEach(c { scores[c.label] c.value; }); const maxScore Math.max(...Object.values(scores), 0.001); for (const tag of Object.keys(scores)) { scores[tag] scores[tag] / maxScore; } return scores; } _combineScores(exactMatch, tfidfScores, bayesScores) { const combined {}; const allTags new Set([...Object.keys(tfidfScores), ...Object.keys(bayesScores)]); for (const tag of allTags) { const tfidfScore tfidfScores[tag] || 0; const bayesScore bayesScores[tag] || 0; const exactScore (exactMatch exactMatch.intent tag) ? exactMatch.confidence : 0; combined[tag] exactScore * 0.2 tfidfScore * 0.4 bayesScore * 0.4; } return combined; } } module.exports IntentEngine;3.3 槽位提取器槽位提取用正则表达式 枚举值匹配 自由文本三种方式。订单号匹配ORD开头的编号手机号匹配 11 位数字退货原因从预定义枚举里找。// server/src/slot-extractor.js const fs require(fs); const path require(path); class SlotExtractor { constructor() { const dataPath path.join(__dirname, ../../data/slots.json); const rawData fs.readFileSync(dataPath, utf-8); this.slotDefinitions JSON.parse(rawData).slots; } extract(input, requiredSlots) { const extracted {}; const missing []; for (const slotName of requiredSlots) { const slotDef this.slotDefinitions[slotName]; if (!slotDef) continue; const value this._extractSlot(input, slotDef); if (value ! null) { extracted[slotName] value; } else { missing.push(slotName); } } return { extracted, missing }; } _extractSlot(input, slotDef) { switch (slotDef.type) { case regex: return this._extractByRegex(input, slotDef.pattern); case enum: return this._extractByEnum(input, slotDef.values); case free_text: return this._extractFreeText(input, slotDef.min_length || 2); default: return null; } } _extractByRegex(input, pattern) { const regex new RegExp(pattern, i); const match input.match(regex); return match ? match[0] : null; } _extractByEnum(input, values) { for (const value of values) { if (input.includes(value)) return value; } return null; } _extractFreeText(input, minLength) { const cleaned input.replace(/你好|请问|帮我|我想|我要|麻烦/g, ).trim(); return cleaned.length minLength ? cleaned : null; } getPrompt(slotName) { const slotDef this.slotDefinitions[slotName]; return slotDef ? slotDef.prompt : 请提供更多信息。; } getSlotDisplayName(slotName) { const slotDef this.slotDefinitions[slotName]; return slotDef ? slotDef.name : slotName; } } module.exports SlotExtractor;对应的slots.json定义{ slots: { order_id: { name: 订单号, type: regex, pattern: ORD[0-9]{8,12}, prompt: 请提供您的订单号ORD开头的编号, examples: [ORD20250001, ORD202501150023] }, phone: { name: 手机号, type: regex, pattern: 1[3-9]\\d{9}, prompt: 请提供您的手机号码, examples: [13800138000] }, product_name: { name: 产品名称, type: enum, values: [蓝牙耳机, 充电宝, 手机壳, 数据线, 键盘, 鼠标], prompt: 请问您想了解哪款产品, examples: [蓝牙耳机, 充电宝] }, reason: { name: 退货原因, type: enum, values: [质量问题, 尺寸不合适, 不喜欢, 发错货, 商品损坏, 描述不符, 其他], prompt: 请问退货原因是什么, examples: [质量问题, 不喜欢] }, description: { name: 问题描述, type: free_text, min_length: 5, prompt: 请详细描述您遇到的问题, examples: [快递太慢了等了一周还没到] } } }4. 多轮对话状态机与验证请求4.1 对话状态流转多轮对话的核心是状态机。四个状态IDLE空闲等待用户输入、COLLECTING正在收集槽位、CONFIRMING等待用户确认、TRANSFERRING正在转人工。用户说“我要退货”但没给订单号时状态从 IDLE 切到 COLLECTING机器人主动追问订单号订单号和退货原因都拿到后切到 CONFIRMING 让用户确认用户确认后执行退货逻辑状态回到 IDLE。// server/src/dialog-manager.js const { v4: uuidv4 } require(uuid); const IntentEngine require(./intent-engine); const SlotExtractor require(./slot-extractor); const DialogState { IDLE: idle, COLLECTING: collecting, CONFIRMING: confirming, TRANSFERRING: transferring }; class DialogManager { constructor() { this.intentEngine new IntentEngine(); this.slotExtractor new SlotExtractor(); this.sessions new Map(); this.sessionTimeout 5 * 60 * 1000; this._startCleanupTimer(); } async init() { await this.intentEngine.init(); console.log(对话管理器初始化完成); } async processMessage(sessionId, userMessage) { let session this.sessions.get(sessionId); if (!session) session this._createSession(sessionId); session.lastActive Date.now(); session.history.push({ role: user, content: userMessage, timestamp: Date.now() }); if (this._isTransferRequest(userMessage)) { return this._handleTransfer(session); } let reply; switch (session.state) { case DialogState.IDLE: reply await this._handleIdleState(session, userMessage); break; case DialogState.COLLECTING: reply await this._handleCollectingState(session, userMessage); break; case DialogState.CONFIRMING: reply await this._handleConfirmingState(session, userMessage); break; default: reply 系统异常请重新开始对话。; session.state DialogState.IDLE; } session.history.push({ role: bot, content: reply, timestamp: Date.now() }); return { reply, state: session.state, sessionId: session.id, shouldTransfer: session.state DialogState.TRANSFERRING, context: { intent: session.currentIntent, slots: session.filledSlots, turn: session.turnCount } }; } async _handleIdleState(session, userMessage) { const result this.intentEngine.recognize(userMessage); session.currentIntent result.intent; session.turnCount; if (result.intent unknown) { session.unknownCount; if (session.unknownCount 3) { return 看起来我暂时无法理解您的问题要帮您转人工客服吗输入转人工即可。; } return this._randomPick(result.responses); } session.unknownCount 0; if (result.required_slots result.required_slots.length 0) { session.requiredSlots [...result.required_slots]; session.filledSlots {}; const extraction this.slotExtractor.extract(userMessage, session.requiredSlots); Object.assign(session.filledSlots, extraction.extracted); if (extraction.missing.length 0) { session.state DialogState.COLLECTING; session.pendingSlots extraction.missing; const nextSlot session.pendingSlots[0]; const prompt this.slotExtractor.getPrompt(nextSlot); return ${this._randomPick(result.responses)}\n${prompt}; } else { return this._generateConfirmation(session); } } return this._randomPick(result.responses); } async _handleCollectingState(session, userMessage) { session.turnCount; const intentCheck this.intentEngine.recognize(userMessage); if (intentCheck.intent ! unknown intentCheck.intent ! session.currentIntent intentCheck.confidence 0.7) { session.state DialogState.IDLE; return this._handleIdleState(session, userMessage); } const extraction this.slotExtractor.extract(userMessage, session.pendingSlots); Object.assign(session.filledSlots, extraction.extracted); if (extraction.missing.length 0) { session.pendingSlots extraction.missing; const nextSlot session.pendingSlots[0]; const slotName this.slotExtractor.getSlotDisplayName(nextSlot); const prompt this.slotExtractor.getPrompt(nextSlot); if (session.lastAskedSlot nextSlot) { session.slotRetryCount; if (session.slotRetryCount 3) { return 看起来我们在${slotName}这里遇到了困难要转人工客服帮您处理吗输入转人工即可。; } return 抱歉我没能识别出您的${slotName}。${prompt}; } session.lastAskedSlot nextSlot; session.slotRetryCount 0; return prompt; } return this._generateConfirmation(session); } async _handleConfirmingState(session, userMessage) { const positive [是, 对, 确认, 没错, 好的, 可以, yes]; const negative [不是, 不对, 取消, 重新, 不, no]; const input userMessage.trim().toLowerCase(); if (positive.some(w input.includes(w))) { const result this._executeBusiness(session); session.state DialogState.IDLE; session.currentIntent null; session.filledSlots {}; return result; } if (negative.some(w input.includes(w))) { session.state DialogState.IDLE; session.filledSlots {}; return 好的已取消。请问还有什么可以帮您的; } return 请确认以上信息是否正确回复确认继续或回复取消重新输入。; } _generateConfirmation(session) { session.state DialogState.CONFIRMING; let confirmMsg 请确认以下信息\n; for (const [slot, value] of Object.entries(session.filledSlots)) { const displayName this.slotExtractor.getSlotDisplayName(slot); confirmMsg - ${displayName}${value}\n; } confirmMsg \n信息正确请回复确认需要修改请回复取消。; return confirmMsg; } _executeBusiness(session) { const intent session.currentIntent; const slots session.filledSlots; switch (intent) { case query_order: return 订单 ${slots.order_id} 的状态\n- 状态已发货\n- 快递公司顺丰速运\n- 预计送达明天下午; case return_goods: return 退货申请已提交\n- 订单号${slots.order_id}\n- 退货原因${slots.reason}\n- 处理时间1-3个工作日; case product_inquiry: return ${slots.product_name} 产品信息\n- 价格¥199.00\n- 库存充足\n- 评分4.8/5.0; case complaint: return 投诉已记录工单号TK${Date.now().toString().slice(-8)}\n- 问题描述${slots.description}\n- 处理时限24小时内回复; default: return 处理完成还有其他可以帮您的吗; } } _isTransferRequest(message) { const keywords [转人工, 人工客服, 真人客服, 找人工, 活人]; return keywords.some(k message.includes(k)); } _handleTransfer(session) { session.state DialogState.TRANSFERRING; return { reply: 好的正在为您转接人工客服请稍等...\n预计等待时间1-3分钟, state: DialogState.TRANSFERRING, sessionId: session.id, shouldTransfer: true, context: { intent: session.currentIntent, slots: session.filledSlots, history: session.history } }; } _createSession(sessionId) { const session { id: sessionId || uuidv4(), state: DialogState.IDLE, currentIntent: null, requiredSlots: [], pendingSlots: [], filledSlots: {}, history: [], turnCount: 0, unknownCount: 0, slotRetryCount: 0, lastAskedSlot: null, createdAt: Date.now(), lastActive: Date.now() }; this.sessions.set(session.id, session); return session; } _randomPick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } _startCleanupTimer() { setInterval(() { const now Date.now(); for (const [id, session] of this.sessions) { if (now - session.lastActive this.sessionTimeout) { this.sessions.delete(id); } } }, 60 * 1000); } } module.exports { DialogManager, DialogState };4.2 验证请求跑通多轮对话启动服务后用 Socket.IO 客户端模拟一次完整的退货对话。先安装依赖cd server npm install express socket.io natural better-sqlite3 uuid cors dotenv启动服务node src/app.js看到智能客服服务已启动: http://localhost:3200说明服务正常。然后用一个简单的测试脚本验证多轮对话// test-dialog.js const { io } require(socket.io-client); const socket io(http://localhost:3200); socket.on(connect, () { console.log(已连接到客服服务); socket.emit(chat:join, { sessionId: test-session-001 }); }); socket.on(chat:message, (data) { console.log([${data.role}] ${data.content}); }); // 模拟多轮对话 setTimeout(() { console.log(\n--- 第1轮用户说退货 ---); socket.emit(chat:message, { message: 我要退货, sessionId: test-session-001 }); }, 1000); setTimeout(() { console.log(\n--- 第2轮用户提供订单号 ---); socket.emit(chat:message, { message: 订单号是 ORD202501150023, sessionId: test-session-001 }); }, 3000); setTimeout(() { console.log(\n--- 第3轮用户提供退货原因 ---); socket.emit(chat:message, { message: 质量问题, sessionId: test-session-001 }); }, 5000); setTimeout(() { console.log(\n--- 第4轮用户确认 ---); socket.emit(chat:message, { message: 确认, sessionId: test-session-001 }); }, 7000); setTimeout(() { socket.disconnect(); process.exit(0); }, 9000);预期输出[bot] 了解请问您要退货的订单号是多少退货原因是什么 [bot] 请提供您的订单号ORD开头的编号 [bot] 请确认以下信息 - 订单号ORD202501150023 - 退货原因质量问题 信息正确请回复确认需要修改请回复取消。 [bot] 退货申请已提交 - 订单号ORD202501150023 - 退货原因质量问题 - 处理时间1-3个工作日如果第 2 轮没有正确提取订单号检查slots.json里的正则ORD[0-9]{8,12}是否匹配你的订单号格式。如果第 3 轮没有识别“质量问题”检查reason枚举值里是否包含这个词。5. 本篇常见错排查5.1 意图识别总是返回 unknown先看置信度阈值。confidenceThreshold默认 0.3如果训练样本太少或用户表达差异大综合得分可能低于 0.3。把阈值调到 0.2 试试但不要低于 0.15否则容易误判。再看训练样本。每个意图至少 10 条样本且要覆盖不同说法。比如“查订单”这个意图除了“查订单”“我的订单”还要加“快递到哪了”“包裹在哪”“什么时候发货”这类口语化表达。5.2 槽位提取失败订单号提取失败最常见的原因是正则不匹配。如果你的订单号格式是ORD-2025-001正则要改成ORD-?[0-9]{4}-?[0-9]{3,}。手机号提取失败检查是否带了国家码86如果有正则要加(\86)?前缀。枚举值提取失败通常是用户说了同义词。比如用户说“东西坏了”但枚举里只有“质量问题”。解决办法是在枚举值里加同义词或者在提取前做一次同义词映射。5.3 多轮对话上下文丢失如果用户在第 2 轮提供了订单号但机器人还在问订单号说明session.pendingSlots没有正确更新。检查_handleCollectingState里extraction.missing的逻辑如果missing为空应该进入确认状态而不是继续追问。另一个常见问题是会话超时。默认 5 分钟无操作会清理会话如果测试时间隔太久会话被清理了上下文自然丢失。测试时把sessionTimeout临时调大或者确保在 5 分钟内完成对话。5.4 Socket.IO 连接失败检查app.js里的 CORS 配置。开发环境用origin: *生产环境要限制具体域名。如果前端报Access-Control-Allow-Origin错误确认后端 CORS 中间件在 Socket.IO 之前加载。端口冲突也是常见问题。3200 端口被占用时改PORT环境变量同时前端 Socket.IO 连接地址也要同步修改。6. 接入文档与后续调优意图识别和槽位提取跑通后下一步是把 FAQ 知识库接进来。FAQ 用 TF-IDF 做模糊匹配相似度超过 0.7 直接返回答案不用走意图识别。这样常见问题比如“怎么查物流”“支持哪些支付方式”可以秒回只有复杂问题才走多轮对话。模型调用通道方面TaoToken 的 API 端点https://taotoken.net/api兼容 OpenAI 格式你可以直接把base_url指向它用统一的 Key 调用不同模型。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有完整的请求示例和参数说明。如果你要长期跑编码任务或 Agent 流程Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 有更详细的配额和模型选择说明。API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 建议按环境创建不同的 Key方便排查问题时定位是哪个环境的调用。调优方向有三个一是扩充意图训练样本把线上真实用户问法加进去二是调整三重评分的权重如果关键词匹配准确率高可以把exactScore权重从 0.2 提到 0.3三是给 FAQ 知识库加分类标签用户问“退货”时优先匹配退货类 FAQ减少无关答案的干扰。
返回列表