1. 项目概述:一个面向开发者的对话机器人构建框架
最近在折腾对话机器人(Chatbot)项目时,发现了一个挺有意思的开源项目,叫bobbylkchao/chatbotBuilder。乍一看名字,你可能会觉得这又是一个类似Rasa或Microsoft Bot Framework的庞然大物。但实际深入后,我发现它的定位非常精准:一个轻量级、模块化、面向开发者的对话机器人构建框架。它不是要做一个开箱即用、拖拽配置的傻瓜式平台,而是提供了一套核心的“骨架”和“工具”,让你能基于自己的业务逻辑和技术栈,快速搭建起一个可维护、可扩展的对话系统。
简单来说,如果你厌倦了在现有框架里“戴着镣铐跳舞”,或者觉得从零开始写一个对话引擎太繁琐,那么这个项目可能正对你的胃口。它帮你处理了对话状态管理、意图识别与实体抽取的流程编排、多轮对话上下文维护这些脏活累活,让你能更专注于定义你的业务对话逻辑本身。无论是想做一个智能客服助手、一个内部工具查询机器人,还是一个带有复杂流程的互动游戏,这个框架都提供了一个不错的起点。
2. 核心架构与设计哲学拆解
2.1 为什么是“构建器”(Builder)而非“平台”?
这是理解这个项目价值的关键。市面上的对话机器人解决方案大致分两类:一类是云服务平台,提供了从 NLP 到部署的全套能力,但定制化深度和成本是问题;另一类是开源框架,功能强大但学习曲线陡峭,且往往与特定技术栈(如 Python)深度绑定。
chatbotBuilder选择了第三条路:它不提供现成的 NLP 模型(虽然可以轻松集成),也不强制你使用某种特定的后端语言或数据库。它的核心是一个架构模式和一组接口定义。你可以把它想象成乐高积木的基础板,上面有标准的插孔(接口)。你需要自己准备或制作积木块(具体的意图识别器、实体抽取器、对话处理器),然后按照规则插到板上,就能组合出你想要的形状。
这种设计带来了几个显著优势:
- 技术栈自由:核心框架与具体实现解耦。理论上,你可以用任何语言编写你的“积木块”(对话处理器),只要它们能通过 HTTP、gRPC 或消息队列与框架的核心“对话引擎”通信。
- 高度可定制:对话流程、状态管理策略、甚至整个对话引擎的某些组件,都可以被替换或扩展。你不会被框架的设计者限制死。
- 便于测试与维护:由于模块间通过清晰定义的接口交互,每个模块都可以独立进行单元测试。业务逻辑(对话处理器)与底层引擎的分离,也使得代码更清晰,维护性更高。
2.2 核心组件与数据流
框架的核心围绕着几个关键概念运转,理解它们就理解了整个系统的工作方式。
对话会话(Session):这是对话的上下文环境。每个用户(或对话线程)都有一个唯一的 Session,它保存了当前对话的状态(State)、历史消息、以及任何自定义的上下文数据。框架负责 Session 的创建、持久化(通常到 Redis 或数据库)和生命周期管理。
消息(Message):用户输入和机器人回复的基本单位。除了文本内容,消息还可以携带结构化数据(如用户选择的按钮、卡片信息等)。
意图(Intent)与实体(Entity):这是理解用户输入的关键。框架本身不实现 NLP,但它定义了一个标准的流程:当用户消息到来时,框架会调用你配置的NLU(自然语言理解)处理器。这个处理器负责分析消息,返回识别出的意图(比如“查询天气”、“预订餐厅”)和提取的实体(比如“时间:明天下午”、“地点:北京”)。框架关心的是这个接口和返回的数据结构。
对话处理器(Dialog Handler):这是你编写业务逻辑的地方。每个意图(或一组相关意图)可以关联一个或多个 Dialog Handler。框架根据 NLU 处理器返回的意图,找到对应的 Handler,并将当前 Session、用户消息、识别出的实体等上下文信息传递给它。Handler 执行你的业务代码(比如调用天气 API、查询数据库),然后决定如何回复用户,以及如何更新对话状态。
状态机(State Machine):这是管理多轮对话复杂性的核心。框架内置了一个轻量级的状态机。每个 Session 都有一个当前状态(例如:“等待用户输入目的地”、“确认预订信息”)。Dialog Handler 在处理完当前轮次后,可以指定下一个状态。这样,当用户下一条消息到来时,框架可以根据当前状态,决定由哪个 Handler 来处理(即使识别出的意图相同,在不同状态下也可能触发不同的处理逻辑),从而轻松实现复杂的、有状态的对话流程。
数据流简化视图:
- 用户发送消息 -> 框架接收,关联到对应的 Session。
- 框架调用NLU 处理器分析消息,得到意图和实体。
- 框架根据 Session 的当前状态和识别出的意图,路由到对应的Dialog Handler。
- Dialog Handler执行业务逻辑,生成回复消息,并可能更新 Session 的状态和上下文数据。
- 框架将回复发送给用户,并持久化更新后的 Session。
- 等待下一条用户消息,回到步骤1。
3. 快速上手:构建你的第一个天气查询机器人
理论说再多不如动手试一下。我们假设一个简单场景:构建一个能查询城市天气的机器人。用户可以说“北京天气怎么样?”或“上海明天天气”。
3.1 环境准备与项目初始化
首先,你需要一个基本的 Node.js 环境(项目示例通常基于 JavaScript/TypeScript,因其轻量和异步友好)。克隆仓库后,安装依赖。
git clone https://github.com/bobbylkchao/chatbotBuilder.git cd chatbotBuilder/examples # 通常示例在 examples 目录 npm install查看项目结构,你会发现核心源码在../src下,而examples/目录下有最简单的示例。我们从一个最基础的例子开始改造。
3.2 定义意图与实体
框架不关心你怎么做 NLP,但你需要告诉它可能的意图和实体。我们通常在一个配置文件(比如intents.json)或直接在代码里定义。
// 定义我们的意图枚举 const Intent = { WEATHER_QUERY: 'WEATHER_QUERY', GREETING: 'GREETING', GOODBYE: 'GOODBYE' }; // 定义实体类型 const EntityType = { CITY: 'city', DATE: 'date' };3.3 实现一个简单的 NLU 处理器
由于是示例,我们实现一个基于规则匹配的极简 NLU,实际项目中你会接入 Rasa NLU、Dialogflow 或训练自己的模型。
// simpleNLU.js class SimpleNLU { async process(text) { const result = { intent: Intent.GREETING, // 默认意图 entities: [], confidence: 1.0 }; // 极其简单的关键词匹配 if (text.includes('天气')) { result.intent = Intent.WEATHER_QUERY; // 简单提取城市(实际应用需要用更复杂的方法,如正则或模型) const cityMatch = text.match(/(北京|上海|广州|深圳)/); if (cityMatch) { result.entities.push({ type: EntityType.CITY, value: cityMatch[1] }); } if (text.includes('明天')) { result.entities.push({ type: EntityType.DATE, value: 'tomorrow' }); } } else if (text.includes('你好') || text.includes('嗨')) { result.intent = Intent.GREETING; } else if (text.includes('再见') || text.includes('拜拜')) { result.intent = Intent.GOODBYE; } return result; } }3.4 编写核心业务逻辑(Dialog Handler)
这是重头戏。我们需要为WEATHER_QUERY意图编写处理器。
// weatherDialogHandler.js const { BaseDialogHandler } = require('../src/core/handler'); // 假设框架提供了基类 class WeatherDialogHandler extends BaseDialogHandler { // 指定这个处理器能处理的意图 getIntent() { return Intent.WEATHER_QUERY; } // 核心处理方法 async handle(context) { const { message, session, nluResult } = context; const entities = nluResult.entities; // 1. 从实体中提取城市和日期 let city = '北京'; // 默认城市 let date = 'today'; // 默认今天 const cityEntity = entities.find(e => e.type === EntityType.CITY); const dateEntity = entities.find(e => e.type === EntityType.DATE); if (cityEntity) { city = cityEntity.value; } else { // 如果没有识别出城市,需要追问。这里更新状态,进入“等待城市”状态。 session.setState('AWAITING_CITY'); return { replies: [{ type: 'text', content: '请问您想查询哪个城市的天气呢?' }], session: session // 返回更新后的session }; } if (dateEntity) { date = dateEntity.value; } // 2. 调用外部天气API(模拟) const weatherInfo = await this.fetchWeather(city, date); // 3. 组织回复 const replyText = `${city}${date === 'tomorrow' ? '明天' : '今天'}的天气是:${weatherInfo}`; // 4. 重置状态(如果需要),并返回回复 session.setState('IDLE'); // 对话完成,回到空闲状态 return { replies: [{ type: 'text', content: replyText }], session: session }; } async fetchWeather(city, date) { // 这里应该是真实的API调用,例如调用和风天气、OpenWeatherMap等 // 为示例简单,我们返回模拟数据 const weatherMap = { '北京': { today: '晴,15-25°C', tomorrow: '多云,16-26°C' }, '上海': { today: '小雨,18-22°C', tomorrow: '阴,19-23°C' }, }; const key = date === 'tomorrow' ? 'tomorrow' : 'today'; return weatherMap[city]?.[key] || '暂无天气信息'; } }关键点解析:
handle方法是业务逻辑入口,接收完整的上下文context。- 我们通过
session.setState()来管理对话状态。例如,当用户没说城市时,我们转入AWAITING_CITY状态,并发送追问。 - 你需要为
AWAITING_CITY状态编写另一个专门的 Handler,来捕获用户下一次输入的城市名。这就是状态机的威力。 - 回复
replies是一个数组,支持多种类型(文本、图片、按钮等),框架会负责渲染到对应渠道(如网页、微信、Slack)。
3.5 装配与运行
最后,我们需要创建一个应用,将 NLU 处理器、Dialog Handler 和框架引擎组装起来。
// app.js const { ChatBotEngine } = require('../src/core/engine'); const SimpleNLU = require('./simpleNLU'); const WeatherDialogHandler = require('./weatherDialogHandler'); const GreetingHandler = require('./greetingHandler'); // 假设已实现 const GoodbyeHandler = require('./goodbyeHandler'); // 假设已实现 async function main() { // 1. 初始化引擎 const engine = new ChatBotEngine({ nluProcessor: new SimpleNLU(), sessionStore: new MemorySessionStore(), // 示例使用内存存储,生产环境需用Redis等 }); // 2. 注册对话处理器 engine.registerHandler(new WeatherDialogHandler()); engine.registerHandler(new GreetingHandler()); engine.registerHandler(new GoodbyeHandler()); // 3. 启动服务器(假设框架提供了简单的HTTP适配器) const server = engine.createServer(); server.listen(3000, () => { console.log('Chatbot server is running on port 3000'); }); // 4. 模拟用户请求 const testSessionId = 'user-123'; const testMessage = { text: '上海明天天气怎么样?' }; const response = await engine.processMessage(testSessionId, testMessage); console.log('Bot Reply:', response.replies[0].content); } main().catch(console.error);运行node app.js,你的第一个基于chatbotBuilder的机器人就跑起来了。它现在能理解简单的天气查询问候和告别,并且能通过状态机处理不完整的查询(比如用户先说“查天气”,再回答“上海”)。
4. 深入核心:状态管理与上下文设计实战
多轮对话的复杂性几乎都来自于状态和上下文。chatbotBuilder在这方面提供了灵活但需要精心设计的机制。
4.1 状态机的进阶用法
上面的例子展示了基本的状态跳转。但实际业务中,状态可能更复杂,比如一个订餐机器人:
IDLE->SELECTING_FOOD(用户说“我要订餐”)SELECTING_FOOD->CONFIRMING_ORDER(用户选好食物)CONFIRMING_ORDER->AWAITING_ADDRESS(用户确认订单)AWAITING_ADDRESS->ORDER_COMPLETE(用户提供地址)
你可以为每个状态编写独立的 Handler。但更优雅的方式是利用框架的状态路由特性:一个 Handler 可以声明自己处理某个意图,但仅在特定状态下生效。
class ConfirmOrderHandler extends BaseDialogHandler { getIntent() { return Intent.CONFIRM; } // 只有当前会话状态是 CONFIRMING_ORDER 时,此处理器才被触发 getRequiredState() { return 'CONFIRMING_ORDER'; } async handle(context) { // 处理确认逻辑 } }这样,即使用户在确认订单状态时说了一句“你好”(GREETING意图),只要没有为CONFIRMING_ORDER状态下的GREETING意图注册处理器,框架就会 fallback 到默认处理器或提示用户“请先确认订单”。
4.2 上下文数据的存储与使用
Session对象不仅存储状态,还有一个contextData属性(或类似名称),用于存放任意业务数据。这是实现连贯对话的关键。
例如,在订餐流程中:
// 在 SELECTING_FOOD 状态的处理器中 async handle(context) { const selectedFood = extractFoodFromMessage(context.message); // 将用户选择的食物暂存到会话上下文中 context.session.contextData.selectedFood = selectedFood; context.session.setState('CONFIRMING_ORDER'); return { replies: [{ content: `您选择了${selectedFood.name},确认吗?` }], session: context.session }; } // 在 CONFIRMING_ORDER 状态的处理器中 async handle(context) { // 直接从上下文中取出之前存储的食物信息,无需用户再次输入 const food = context.session.contextData.selectedFood; if (userConfirmed(context.message)) { // 创建订单,使用 food 信息 createOrder(food, context.userId); context.session.contextData.orderId = orderId; // 存储订单ID,可能用于后续查询 context.session.setState('ORDER_COMPLETE'); return { replies: [{ content: `订单已生成,编号${orderId}` }], ... }; } }注意事项:
- 上下文数据的清理:务必在对话流程结束或会话超时时,清理不必要的上下文数据,防止数据泄露和内存浪费。可以在状态跳转到初始状态(如
IDLE)时,清空contextData。 - 序列化:
contextData会被持久化到存储(如 Redis),因此里面存储的数据必须是可序列化的(JSON 兼容)。避免存放函数、循环引用的对象等。 - 命名空间:对于复杂应用,建议为不同模块的上下文数据使用前缀或嵌套对象,避免键名冲突。例如
contextData.ordering.food和contextData.payment.method。
5. 集成真实 NLP 服务与多渠道适配
5.1 接入专业的 NLU 引擎
前面的SimpleNLU只是个玩具。生产环境需要接入更强大的 NLP 服务。框架的优势在于,更换 NLU 就像换一个插件一样简单。
以接入微软的 Azure CLU(Conversational Language Understanding)为例:
// azureCLUProcessor.js const { CognitiveServicesCredentials } = require('ms-rest-azure'); const { LUISRuntimeClient } = require('azure-cognitiveservices-luis-runtime'); class AzureCLUProcessor { constructor(appId, endpointKey, endpoint) { const credentials = new CognitiveServicesCredentials(endpointKey); this.client = new LUISRuntimeClient(credentials, { endpoint }); this.appId = appId; } async process(text, sessionId) { try { const predictionRequest = { query: text }; // 调用 Azure CLU 预测端点 const prediction = await this.client.prediction.getSlotPrediction( this.appId, 'production', // 部署槽位 predictionRequest, { verbose: true, showAllIntents: true } ); const topIntent = prediction.prediction.topIntent; const entities = prediction.prediction.entities.map(e => ({ type: e.type, value: e.entity, // 可能还有额外的解析值,如日期时间 resolution: e.resolution })); return { intent: topIntent, entities: entities, confidence: prediction.prediction.intents[topIntent]?.score || 0, rawResponse: prediction // 原始响应,便于调试 }; } catch (error) { console.error('CLU processing error:', error); // 降级策略:返回一个默认意图或使用规则后备 return { intent: 'None', entities: [], confidence: 0 }; } } }然后在初始化引擎时,替换掉原来的SimpleNLU:
const cluProcessor = new AzureCLUProcessor(YOUR_APP_ID, YOUR_KEY, YOUR_ENDPOINT); const engine = new ChatBotEngine({ nluProcessor: cluProcessor, // ... 其他配置 });5.2 实现消息渠道适配器
框架核心不关心消息来自哪里(网站、微信、Telegram、Slack)。你需要为每个渠道编写一个适配器(Adapter)。适配器的职责是:
- 接收渠道特定的原始请求(如 HTTP POST 请求)。
- 将其转化为框架标准的
Message格式。 - 调用
engine.processMessage(sessionId, message)。 - 将框架返回的标准
Reply格式,转化为渠道特定的响应格式并发送回去。
一个简单的 HTTP Webhook 适配器示例:
// httpAdapter.js const express = require('express'); const bodyParser = require('body-parser'); function createHttpAdapter(engine, path = '/webhook') { const router = express.Router(); router.use(bodyParser.json()); router.post(path, async (req, res) => { const channel = 'web'; // 渠道标识 const userId = req.body.userId || `web_${req.ip}`; // 从请求中提取用户ID const sessionId = `${channel}_${userId}`; // 构造全局唯一的会话ID // 将渠道原始消息转换为框架标准消息 const standardMessage = { text: req.body.text, rawData: req.body, // 保留原始数据,可能包含附件、位置等信息 channel: channel }; try { const result = await engine.processMessage(sessionId, standardMessage); // 将框架的标准回复转换为渠道响应 const channelResponse = { replies: result.replies.map(reply => { if (reply.type === 'text') { return { type: 'text', content: reply.content }; } // 处理其他类型回复,如图片、按钮等 return reply; }) }; res.json(channelResponse); } catch (error) { console.error('Error processing message:', error); res.status(500).json({ error: 'Internal server error' }); } }); return router; } // 在 app.js 中使用 const app = express(); app.use(createHttpAdapter(engine)); app.listen(3000);对于微信、Telegram 等渠道,原理类似,只是认证、消息解析和响应格式更复杂。你可以为每个渠道创建一个独立的适配器模块,使核心业务逻辑与渠道细节完全解耦。
6. 生产环境部署与性能优化考量
当你的机器人从 demo 走向真实用户时,以下几个方面的考量至关重要。
6.1 会话存储的选择与配置
内存存储 (MemorySessionStore) 只适用于开发和测试。生产环境必须使用外部持久化存储,以保证服务重启后会话不丢失,并支持多实例部署。
Redis 存储实现示例:
// redisSessionStore.js const Redis = require('ioredis'); class RedisSessionStore { constructor(redisOptions, ttl = 1800) { // 默认会话过期时间30分钟 this.client = new Redis(redisOptions); this.ttl = ttl; } async get(sessionId) { const data = await this.client.get(`session:${sessionId}`); return data ? JSON.parse(data) : null; } async set(sessionId, sessionData) { const key = `session:${sessionId}`; // 使用 SETEX 设置键值对和过期时间 await this.client.setex(key, this.ttl, JSON.stringify(sessionData)); } async delete(sessionId) { await this.client.del(`session:${sessionId}`); } } // 使用 const sessionStore = new RedisSessionStore({ host: 'your-redis-host', port: 6379, password: 'your-password' }, 3600); // 1小时过期选择建议:
- Redis:首选。性能极高,支持丰富的数据结构,天然支持过期时间,是会话存储的事实标准。
- 数据库(如 PostgreSQL, MongoDB):如果会话数据非常复杂,或需要做复杂的关联查询,可以考虑。但性能通常不如 Redis,需要自己管理过期清理。
- TTL(生存时间)设置:根据你的业务场景设置。客服机器人可能设置几小时,而临时查询机器人可能只需几分钟。太短会打断用户长对话,太长会浪费存储资源。
6.2 处理并发与异步操作
对话机器人是典型的 I/O 密集型应用(等待 NLU 结果、调用外部 API、读写数据库)。必须充分利用 Node.js 的异步非阻塞特性。
- 避免阻塞事件循环:在 Dialog Handler 中,所有可能耗时的操作(网络请求、复杂计算、大量数据库查询)都必须使用
async/await或返回 Promise。绝对不要使用同步函数(如fs.readFileSync)或执行 CPU 密集型任务而不释放事件循环。 - 设置超时与重试:对外部服务(如 NLU API、天气 API)的调用必须设置超时。使用像
axios这样的库可以方便地配置timeout。对于可重试的错误(如网络抖动),实现简单的重试逻辑。 - 使用连接池:对于数据库、Redis 等,确保使用连接池,而不是为每个请求创建新连接。
6.3 日志、监控与错误处理
- 结构化日志:使用
winston或pino等库记录结构化日志。记录关键信息:会话 ID、用户 ID、意图、处理状态、耗时、错误堆栈。这便于后续调试和数据分析。 - 错误分类处理:
- NLU 识别失败:降级到规则匹配或返回友好提示“我没听明白”。
- 业务逻辑错误(如 API 调用失败):记录错误,并给用户一个友好的失败提示,如“服务暂时不可用,请稍后再试”。
- 框架内部错误:捕获并记录,返回通用错误信息,避免泄露内部细节。
- 健康检查与监控:为你的机器人服务添加
/health端点,检查其依赖(Redis、外部 API)的健康状况。使用 APM 工具(如 Prometheus, New Relic)监控服务的 QPS、响应时间、错误率。
7. 扩展框架:自定义中间件与插件机制
chatbotBuilder的威力在于其可扩展性。除了替换 NLU 和存储,你还可以通过中间件(Middleware)在消息处理流水线上插入自定义逻辑。
典型的中间件应用场景:
- 用户身份验证与授权:在 NLU 处理之前,验证用户 token,并将用户信息注入到会话上下文中。
- 输入预处理:过滤敏感词、纠正拼写、标准化文本(如全角转半角)。
- 对话记录与审计:将每轮对话(用户输入、机器人回复、识别出的意图实体)记录到审计日志或数据仓库。
- 限流与防刷:基于用户 ID 或 IP 限制请求频率。
- 情感分析:在 NLU 之后,对用户消息进行情感分析,并将结果注入上下文,供后续 Handler 做出更人性化的回应。
实现一个简单的日志中间件:
// loggingMiddleware.js class LoggingMiddleware { async execute(context, next) { const startTime = Date.now(); const { sessionId, message } = context; console.log(`[${new Date().toISOString()}] Session ${sessionId} received: ${message.text}`); // 调用流水线上的下一个处理器(可能是下一个中间件,也可能是核心的NLU+Handler流程) await next(); const endTime = Date.now(); console.log(`[${new Date().toISOString()}] Session ${sessionId} processed in ${endTime - startTime}ms`); // 注意:next() 执行后,context 中可能已经包含了处理结果(如回复) if (context.result && context.result.replies) { console.log(`Bot replied: ${context.result.replies.map(r => r.content).join('; ')}`); } } } // 在引擎初始化时注册中间件 const engine = new ChatBotEngine({ // ... 其他配置 middlewares: [ new LoggingMiddleware(), new AuthMiddleware(), // 假设有认证中间件 // ... 其他中间件 ] });中间件的执行顺序就是注册的顺序。它们构成了一个“洋葱模型”,可以对请求和响应进行全方位的拦截和处理。
8. 测试策略:如何保证你的机器人质量
对话系统的测试比传统软件更复杂,因为它涉及自然语言理解和状态流转。
8.1 单元测试:测试独立的 Dialog Handler
这是最直接的部分。你可以模拟输入上下文,断言 Handler 的输出(回复和状态变更)。
// weatherDialogHandler.test.js const WeatherDialogHandler = require('./weatherDialogHandler'); const { Session } = require('../src/core/session'); describe('WeatherDialogHandler', () => { let handler; let mockSession; beforeEach(() => { handler = new WeatherDialogHandler(); mockSession = new Session('test-session'); mockSession.setState('IDLE'); }); it('应该正确回复已知城市的天气', async () => { const context = { message: { text: '北京天气' }, session: mockSession, nluResult: { intent: 'WEATHER_QUERY', entities: [{ type: 'city', value: '北京' }], confidence: 0.9 } }; const result = await handler.handle(context); expect(result.replies[0].content).toContain('北京'); expect(result.replies[0].content).toContain('天气'); expect(result.session.state).toBe('IDLE'); // 处理完后应回到空闲状态 }); it('当未识别城市时应追问并进入等待状态', async () => { const context = { message: { text: '天气怎么样' }, session: mockSession, nluResult: { intent: 'WEATHER_QUERY', entities: [], // 没有识别出城市实体 confidence: 0.8 } }; const result = await handler.handle(context); expect(result.replies[0].content).toMatch(/哪个城市/); // 回复包含追问 expect(result.session.state).toBe('AWAITING_CITY'); // 状态已变更 }); });8.2 集成测试:测试完整的对话流
模拟用户与机器人的多轮对话,验证整个状态机的流转是否符合预期。你可以编写一个测试脚本,按顺序发送一系列消息,并检查每次的回复和最终状态。
// conversationFlow.test.js async function testWeatherFlow(engine) { const sessionId = 'test-flow-1'; // 第一轮:用户查询天气但没说城市 let response = await engine.processMessage(sessionId, { text: '我想查天气' }); expect(response.replies[0].content).toMatch(/哪个城市/); // 这里可以检查 session 状态是否为 AWAITING_CITY (需要从存储中读取) // 第二轮:用户提供城市 response = await engine.processMessage(sessionId, { text: '北京' }); // 这里需要有一个专门处理 AWAITING_CITY 状态下文本的 Handler expect(response.replies[0].content).toMatch(/北京.*天气/); // 检查状态是否回到 IDLE }8.3 端到端(E2E)测试与回归测试
对于核心对话流程,可以录制“黄金对话”用例。每次代码更新后,自动运行这些用例,确保机器人对相同输入的关键回复没有退化。这可以使用任何 E2E 测试框架(如 Jest, Mocha)配合 HTTP 客户端来模拟请求。
最重要的测试建议:Mock 所有外部依赖。在单元测试和集成测试中,NLU 服务、天气 API、数据库都应该被 Mock 或使用测试替身。测试应该快速、稳定、不依赖网络和外部服务状态。