1. 项目概述从零构建一个通用型对话机器人Keap最近在琢磨一个挺有意思的玩意儿怎么从零开始亲手搭建一个能真正理解我们说话、并且能帮我们处理点实际事情的对话机器人。这想法其实源于日常的一个小痛点——我们每天在各种App、邮件、笔记软件之间切换记录待办事项、保存一闪而过的灵感信息太分散了。能不能有个统一的“智能助手”我用最自然的语言跟它说句话它就能帮我记住、查找甚至在未来提醒我这就是“Keap”这个项目诞生的初衷。Keap我给它取这个名字是希望它能成为一个可靠的“知识管家”Keeper。在项目的这个第一阶段我们的目标很明确打造一个基于文本的对话机器人核心。它最初将通过电子邮件与我们交互就像一个永远在线的智能邮箱助手。你可以发邮件告诉它“Keap请把‘下周和客户开会’记到日程里”或者问它“Keap把我上个月记的读书清单找出来”。它需要理解这些自然语言指令背后的意图并执行相应的操作比如存储数据到数据库或者从数据库里查询并返回结果。这个架构设计是开放性的我称之为“X-Bot”架构。这里的“X”代表“任何类型”。虽然Keap的第一个化身是邮件助手但它的核心——理解语言、处理逻辑、管理数据——是通用的。未来我们可以把它的“沟通渠道”从邮件换成微信、Slack、Telegram而无需重写核心大脑。这就像给同一个大脑接上不同的耳朵和嘴巴。为了实现这个目标技术选型上我计划用Rasa NLU来处理自然语言理解用Flask或CherryPy来构建处理请求的API服务层整个系统最终要能方便地一键部署到服务器上保持24小时在线。接下来我们就深入拆解这个架构的每一层看看如何把想法变成可运行的代码。2. 核心架构设计与技术选型思路构建Keap这样的机器人不能一上来就写代码必须先理清架构。一个好的架构应该像乐高积木模块清晰、职责单一、易于替换和扩展。基于“X-Bot”的理念我将Keap初步划分为三个核心层次感知层输入、大脑层处理、执行层输出与存储。感知层负责接收用户的原始消息目前是邮件大脑层负责理解消息意图、管理对话状态并决策该做什么执行层则负责实际的数据操作存/取和生成回复。2.1 为什么选择Rasa NLU作为语言理解核心在自然语言处理NLP领域工具有很多比如微软的LUIS、Google的Dialogflow或者一些开源库如spaCy。我选择Rasa NLU主要基于以下几点考量开源与可控性Rasa是完全开源的这意味着所有数据、模型都掌握在自己手里没有隐私泄露到第三方云服务的风险。这对于处理个人任务、日程等敏感信息至关重要。你可以自己部署在服务器上完全私有化。强大的意图识别与实体提取Rasa NLU的核心任务就是把一句自然语言如“Keap把买牙刷加到购物清单里”解析成结构化的数据。它会识别出用户的“意图”add_to_list并提取出关键“实体”item: “牙刷”,list_name: “购物清单”。它的实体提取器支持多种方式包括基于规则的、基于统计模型的非常灵活。与Rasa Core的天然集成为未来铺垫Rasa其实是一个套件包含NLU理解和Core对话管理。虽然Keap第一期可能用不到复杂的多轮对话管理Core但选用Rasa NLU为未来升级留下了无缝衔接的通道。当我们需要Keap处理更复杂的、需要上下文记忆的对话时引入Rasa Core会非常顺畅。自定义能力强Rasa的pipeline处理流程可以高度定制。你可以插入自己的分词器、特征提取器、实体提取器。例如针对中文你可以轻松集成Jieba分词针对特定领域的术语可以训练自定义的实体识别模型。注意Rasa NLU在初期配置和训练数据准备上需要一些耐心。它不像云服务那样有现成的图形界面一切都需要通过配置文件config.yml和训练数据文件nlu.md来定义。但这正是其灵活性和强大之处的来源。2.2 后端API框架Flask vs. CherryPyKeap的大脑需要一个“服务器”来驱动它负责接收来自邮件渠道的请求调用Rasa NLU进行解析执行业务逻辑访问数据库然后组织回复。这个服务器本质上是一个Web API。我主要在Flask和CherryPy之间权衡。Flask这是一个极其轻量级和灵活的Python Web框架。它的哲学是“微核心”只提供最基础的路由、请求/响应处理其他功能如数据库ORM、表单验证都通过扩展来实现。对于Keap这样一个核心逻辑明确、功能相对单一的项目Flask的简洁性非常有吸引力。写一个接收JSON、处理、返回JSON的API端点用Flask可能只需要十几行代码。CherryPy这是一个更“老牌”的、面向对象的Web框架。它允许你将Web应用像普通Python对象一样构建每个URL可以映射到对象的方法。它自称是一个“支持HTTP的应用程序框架”内置了Web服务器、会话管理等功能比Flask更“全栈”一些。我的选择倾向是Flask。原因在于1)学习曲线和社区Flask的社区更大资料、解决方案更多遇到问题更容易找到答案。2)灵活性Keap的API目前可能很简单但未来如果集成认证、更复杂的中间件Flask丰富的扩展生态如Flask-RESTful, Flask-JWT能提供更多选择。3)部署友好Flask应用可以非常方便地使用Gunicorn或uWSGI配合Nginx部署这是生产环境的常见做法成熟稳定。2.3 数据存储的简单哲学Keap需要存储用户发来的信息如“买牙刷”并能按条件查询如“显示过去两天的清单”。在项目初期复杂性和稳定性是关键。因此我排除了直接使用大型关系数据库如PostgreSQL或NoSQL数据库如MongoDB的方案尽管它们功能强大。我计划采用一个更轻量、更直接的方式SQLite数据库配合Python的sqlite3库。SQLite是一个文件数据库无需安装和运行独立的数据库服务整个数据库就是一个.db文件。这对于单用户或轻量级应用的原型阶段来说简直是完美选择。它支持标准的SQL语法能很好地满足“存储”和“查询”这两个核心需求。我们可以设计一张简单的表包含字段如id主键、user_id用户标识初期可以用邮箱地址、content存储的内容、intentRasa解析出的意图、entities以JSON格式存储提取的实体、created_at创建时间。这样当用户查询“过去两天的清单”时我们只需执行一条SQL查询SELECT * FROM items WHERE user_id? AND created_at date(now, -2 days)。实操心得在原型阶段切忌过度设计。用最简单的工具SQLite快速验证核心功能自然语言理解数据存取是否跑通远比一开始就搭建一个“高大上”但复杂的架构更重要。功能验证成功后如果确有性能或扩展性需求再将数据迁移到MySQL或PostgreSQL是相对容易的。3. 系统搭建与核心模块实现详解理论说得再多不如动手搭起来。这一部分我们将一步步地构建Keap的各个模块。请确保你的开发环境已经安装了Python 3.7或以上版本。3.1 项目初始化与环境配置首先创建一个干净的项目目录并建立虚拟环境这是保证依赖隔离的好习惯。mkdir keap-bot cd keap-bot python -m venv venv # 激活虚拟环境 # 在Windows上: venv\Scripts\activate # 在Mac/Linux上: source venv/bin/activate接下来创建我们的依赖文件requirements.txt。这里列出了第一阶段所需的核心库。rasa3.6.15 flask2.3.3 python-dotenv1.0.0 sqlite3 # 通常Python标准库自带无需单独安装使用pip安装它们pip install -r requirements.txt同时创建我们的项目结构。一个清晰的结构能让后续开发维护事半功倍。keap-bot/ ├── venv/ # 虚拟环境目录 ├── requirements.txt # 项目依赖 ├── .env # 环境变量配置文件如邮箱密码切勿提交到Git ├── app.py # Flask主应用文件 ├── rasa_nlu/ # Rasa NLU相关文件 │ ├── config.yml # Rasa NLU配置文件 │ ├── data/ │ │ └── nlu.yml # NLU训练数据 │ └── models/ # 训练好的模型存放处 ├── database.py # 数据库操作封装 └── email_handler.py # 邮件收发处理模块未来实现3.2 训练Rasa NLU模型教Keap理解人话这是Keap的“大脑皮层”负责理解指令。我们在rasa_nlu/config.yml中定义NLU管道。一个适用于初学者、效果不错的配置如下version: 3.1 language: en # 初始使用英文后续可扩展中文 pipeline: - name: WhitespaceTokenizer - name: RegexFeaturizer - name: LexicalSyntacticFeaturizer - name: CountVectorsFeaturizer - name: CountVectorsFeaturizer analyzer: char_wb min_ngram: 1 max_ngram: 4 - name: DIETClassifier epochs: 100 constrain_similarities: true - name: EntitySynonymMapper - name: ResponseSelector epochs: 100 - name: FallbackClassifier threshold: 0.7这个管道包含了分词、特征提取、意图和实体分类DIETClassifier等组件。DIETClassifier是Rasa的一个强大分类器能同时处理意图分类和实体识别。接下来我们需要准备训练数据。在rasa_nlu/data/nlu.yml中我们定义一些示例语句和对应的意图、实体。version: 3.1 nlu: - intent: greet examples: | - hey - hello - hi - good morning - good evening - intent: add_item examples: | - please add [toothbrush](item) to my [shopping](list_name) list - store [buy milk](item) in the [grocery](list_name) list - Keap, remember to [call John tomorrow](item) - add [read AI paper](item) to [tasks](list_name) - intent: query_items examples: | - show me my list from the [last 2 days](time_range) - what did I store [yesterday](time_range)? - get my [shopping](list_name) list - Keap, fetch all my tasks - intent: goodbye examples: | - bye - goodbye - see you - have a nice day - synonym: shopping examples: | - shopping list - buy list - stuff to buy - synonym: tasks examples: | - todo - things to do - work items这里定义了四个意图问候greet、添加项目add_item、查询项目query_items和告别goodbye。在add_item的例子中我们用方括号[]标注了实体如[toothbrush](item)表示文本“toothbrush”是一个类型为item的实体。底部的synonym部分定义了同义词确保“shopping list”和“buy list”能被归一化处理。现在进入rasa_nlu目录开始训练模型cd rasa_nlu rasa train nlu训练完成后模型会保存在rasa_nlu/models目录下。你可以使用以下命令在命令行快速测试一下模型的理解能力rasa shell nlu输入“add toothbrush to shopping list”看看它是否能正确识别出意图add_item和实体item: toothbrush,list_name: shopping。3.3 构建Flask APIKeap的神经中枢有了能理解语言的模型我们需要一个API来调用它并处理业务逻辑。回到项目根目录创建app.py。from flask import Flask, request, jsonify import sqlite3 from datetime import datetime, timedelta import json import os from rasa.shared.nlu.training_data.message import Message from rasa.nlu.model import Interpreter app Flask(__name__) # 加载训练好的Rasa NLU模型 MODEL_PATH ./rasa_nlu/models/nlu-20240315-103045.tar.gz # 请替换为你的实际模型路径 interpreter Interpreter.load(MODEL_PATH) # 初始化数据库 def init_db(): conn sqlite3.connect(keap.db) c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, content TEXT NOT NULL, intent TEXT, entities TEXT, -- 存储为JSON字符串 list_name TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() conn.close() app.route(/parse, methods[POST]) def parse_message(): 解析用户消息的端点 data request.json message_text data.get(text, ) if not message_text: return jsonify({error: No text provided}), 400 # 使用Rasa NLU解析消息 result interpreter.parse(message_text) return jsonify(result) app.route(/process, methods[POST]) def process_command(): 处理用户命令的核心端点模拟邮件处理后的调用 data request.json user_id data.get(user_id, default_user) # 实际应从邮件发件人获取 message_text data.get(text, ) # 1. 解析意图和实体 nlu_result interpreter.parse(message_text) intent nlu_result[intent][name] entities {entity[entity]: entity[value] for entity in nlu_result[entities]} # 2. 根据意图执行逻辑 response_text conn sqlite3.connect(keap.db) c conn.cursor() if intent add_item: item_content entities.get(item, message_text) # 如果没有提取到实体则用全文 list_name entities.get(list_name, default) # 将实体字典转为JSON字符串存储 entities_json json.dumps(entities) c.execute( INSERT INTO items (user_id, content, intent, entities, list_name) VALUES (?, ?, ?, ?, ?) , (user_id, item_content, intent, entities_json, list_name)) conn.commit() response_text fGot it. Ive added {item_content} to your {list_name} list. elif intent query_items: list_name entities.get(list_name) time_range entities.get(time_range) # 构建查询条件 query SELECT content, list_name, created_at FROM items WHERE user_id ? params [user_id] if list_name: query AND list_name ? params.append(list_name) if time_range: # 这里需要解析time_range实体例如“last 2 days” # 这是一个简化版实际需要更复杂的自然语言时间解析可考虑用dateparser库 if last in time_range and day in time_range: try: days int(time_range.split()[1]) # 非常简单的提取 start_date (datetime.now() - timedelta(daysdays)).strftime(%Y-%m-%d %H:%M:%S) query AND created_at ? params.append(start_date) except: pass # 解析失败忽略时间条件 query ORDER BY created_at DESC c.execute(query, params) items c.fetchall() if items: item_list \n.join([f- {row[0]} ({row[1]}, {row[2]}) for row in items]) response_text fHere are your items:\n{item_list} else: response_text You have no items in your list for the given criteria. elif intent greet: response_text Hello! Im Keap, your personal assistant. How can I help you today? elif intent goodbye: response_text Goodbye! Have a great day. else: response_text Im not sure how to handle that yet. You can ask me to add things to a list or query your saved items. conn.close() return jsonify({ user_id: user_id, original_text: message_text, intent: intent, entities: entities, response: response_text }) if __name__ __main__: init_db() # 启动时初始化数据库 app.run(debugTrue, port5000)这个Flask应用提供了两个关键端点/parse纯粹测试NLU解析输入文本返回Rasa的解析结果。/process这是Keap的核心处理逻辑。它接收用户ID和文本先调用NLU解析然后根据意图intent执行相应的数据库操作插入或查询最后组织一个友好的文本回复。运行python app.pyFlask服务将在http://127.0.0.1:5000启动。你可以使用Postman或curl进行测试curl -X POST http://127.0.0.1:5000/process \ -H Content-Type: application/json \ -d {user_id: aliceexample.com, text: Keap, add buy toothpaste to my shopping list}预期会返回一个JSON包含解析结果和操作响应。3.4 数据层封装与未来邮件集成展望为了代码更清晰我们将数据库操作单独封装。创建database.pyimport sqlite3 import json from contextlib import contextmanager DB_PATH keap.db contextmanager def get_db_connection(): 上下文管理器确保数据库连接正确关闭 conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 使返回的行像字典一样访问 try: yield conn finally: conn.close() def add_item(user_id, content, intent, entities, list_namedefault): with get_db_connection() as conn: c conn.cursor() entities_json json.dumps(entities) c.execute( INSERT INTO items (user_id, content, intent, entities, list_name) VALUES (?, ?, ?, ?, ?) , (user_id, content, intent, entities_json, list_name)) conn.commit() return c.lastrowid def query_items(user_id, list_nameNone, start_dateNone): with get_db_connection() as conn: c conn.cursor() query SELECT * FROM items WHERE user_id ? params [user_id] if list_name: query AND list_name ? params.append(list_name) if start_date: query AND created_at ? params.append(start_date) query ORDER BY created_at DESC c.execute(query, params) return [dict(row) for row in c.fetchall()] # 返回字典列表然后在app.py中就可以导入并使用这些函数使视图函数更简洁。关于邮件集成这是将Keap变为“24小时在线助手”的关键一步。思路是为Keap创建一个专属邮箱如keap.assistantyourdomain.com。使用Python的imaplib库定期或通过服务器钩子检查收件箱。当收到新邮件时提取发件人作为user_id和邮件正文作为message_text。调用我们写好的/processAPI或者直接导入处理函数。将API返回的response文本通过smtplib库以回复邮件的形式发送给用户。由于邮件协议处理、安全认证OAuth2等内容较多这将是下一阶段Part 2的重点。目前我们已经拥有了Keap最核心的“大脑”和“神经反射弧”API。4. 开发中的常见问题与调试技巧在搭建Keap的过程中我踩过不少坑。这里把一些典型问题和解决方法记录下来希望能帮你节省时间。4.1 Rasa NLU训练与解析问题问题1训练时警告“No training data found”或模型效果极差。排查首先检查nlu.yml文件的格式是否正确特别是缩进。YAML对缩进非常敏感。确保version: “3.1”和nlu:在顶层每个intent下的examples:使用正确的缩进。可以使用在线YAML验证器检查。解决确保示例数量足够。每个意图至少提供15-30个不同表达方式的例子覆盖同义词、不同句式。实体标注要一致。问题2实体识别不准特别是复合实体或长句子。排查检查训练数据中是否包含了足够多带有该实体的、句式多样的例子。例如对于item实体不仅要标注“买 牙刷 ”还要有“记得 下班后去超市买牛奶和面包 ”。解决在config.yml的pipeline中可以调整DIETClassifier的epochs如增加到200或embedding_dimension。考虑使用RegexFeaturizer和RegexEntityExtractor来辅助识别有固定模式的内容如日期、时间、产品编号。增加训练数据是最根本的方法。问题3意图混淆比如把“show me my tasks”识别为greet。排查检查greet意图的示例是否过于宽泛或者query_items的示例是否不足。使用rasa test nlu命令可以生成详细的混淆矩阵直观看到哪些意图容易被混淆。解决为容易混淆的意图增加更多具有区分度的示例。例如在query_items中增加“what’s on my list”、“display my stored items”等。也可以考虑在config.yml中启用FallbackClassifier并设置一个合适的threshold如0.7当最高置信度低于阈值时归类为nlu_fallback意图你可以让Keap回复“我没太听懂你能换种说法吗”。4.2 Flask API开发与集成问题问题1跨域请求CORS错误当从前端页面调用API时。现象浏览器控制台报错“Access-Control-Allow-Origin”。解决使用Flask-CORS扩展。安装pip install flask-cors然后在app.py中初始化from flask_cors import CORS; CORS(app)。在生产环境中需要配置更严格的CORS策略。问题2SQLite数据库并发写入冲突。现象在多线程环境下如Flask开发服务器处理多个并发请求可能遇到sqlite3.OperationalError: database is locked。解决使用连接池或确保单线程连接在上面的get_db_connection上下文管理器中每次请求都创建新连接并由上下文管理器确保关闭这在轻量并发下通常可行。更严谨的做法是使用SQLAlchemy等ORM它内置了连接池管理。设置超时在连接时设置timeout参数sqlite3.connect(DB_PATH, timeout10)。这会让SQLite在遇到锁时等待最多10秒。考虑升级数据库如果并发量真的很大应考虑迁移到PostgreSQL或MySQL。问题3处理自然语言时间表达式如“last 2 days”挑战我们在NLU中提取了time_range实体但它的值是字符串“last 2 days”我们需要将其转换为程序可理解的日期时间范围。解决方案不要自己写复杂的解析器。使用第三方库如dateparserpip install dateparser。它非常强大import dateparser time_text entities.get(time_range) if time_text: parsed_date dateparser.parse(time_text, settings{RELATIVE_BASE: datetime.now()}) if parsed_date: start_date parsed_date.strftime(%Y-%m-%d %H:%M:%S) # 使用start_date进行数据库查询这能处理“yesterday”, “2 hours ago”, “next Monday”等多种表达。4.3 项目部署与持续运行的考量问题如何让Keap 7x24小时运行本地运行不可靠你的电脑不能一直开着且IP可能变化。解决方案部署到云服务器。步骤如下选择服务器购买一台最基础的Linux云服务器如1核1G。上传代码使用Git或SFTP将项目代码上传到服务器。安装依赖在服务器上同样创建虚拟环境并安装requirements.txt。使用生产级WSGI服务器不要直接用flask run。使用Gunicornpip install gunicorn然后运行gunicorn -w 4 -b 0.0.0.0:5000 app:app。-w 4表示启动4个工作进程处理并发。设置反向代理使用Nginx作为反向代理处理静态文件、SSL加密HTTPS并将请求转发给Gunicorn。这能提升安全性和性能。进程守护使用Systemd或Supervisor来管理Gunicorn进程确保它崩溃后能自动重启。邮件检查守护进程未来实现邮件处理器email_handler.py后也需要用Systemd/Supervisor将其作为一个常驻服务运行定期检查邮箱。核心避坑技巧在开发过程中务必编写日志。在app.py开头配置import logging; logging.basicConfig(levellogging.DEBUG)。在所有关键步骤收到请求、解析结果、数据库操作、发送回复都记录日志。这样当线上出现问题时查看日志文件能快速定位问题根源远比盲目猜测高效。5. 性能优化与后续迭代方向目前Keap已经具备了最核心的“理解-执行-回复”闭环。但作为一个有追求的项目我们当然不满足于此。以下是一些让Keap变得更聪明、更健壮、更实用的优化和扩展思路。5.1 提升NLU理解能力的进阶策略当前的NLU模型基于有限的示例数据。要提升其泛化能力数据增强手动编写所有示例费时费力。可以使用同义词替换、句式变换等工具自动生成更多训练数据。例如将“add milk ”自动扩展为“please add milk ”、“could you add milk ”、“I need to add milk ”等。引入预训练词向量在config.yml的pipeline中可以在CountVectorsFeaturizer之前或之后加入LanguageModelFeaturizer并配置如HuggingFace的预训练模型例如bert。这能让模型更好地理解词汇在上下文中的语义显著提升对未在训练数据中出现过的表达方式的理解能力。自定义组件如果Keap需要处理非常专业的领域术语比如医学、法律可以编写自定义的Rasa NLU组件集成领域特定的词典或规则。5.2 架构解耦与消息队列引入目前的架构中Flask API同步处理所有请求接收、NLU解析、数据库操作、生成回复。如果NLU解析或数据库查询很慢会阻塞整个请求影响用户体验。引入异步处理与消息队列场景当用户通过邮件发送一个复杂查询如“找出我所有关于‘机器学习’的记录”时数据库全文搜索可能需要几秒钟。方案我们可以引入一个消息队列如Redis或RabbitMQ。流程变为Flask API接收到请求后立即返回一个“已收到正在处理”的响应。同时将处理任务包含用户ID、消息文本放入消息队列。一个独立的“工作进程”Worker从队列中取出任务执行耗时的NLU解析和数据库查询。工作进程完成后通过邮件或未来的其他渠道将结果异步发送给用户。好处API响应极快用户体验好耗时任务被分流系统更稳定工作进程可以水平扩展应对高负载。5.3 多模态与多渠道扩展这是“X-Bot”理念的终极体现。我们需要设计一个“适配器”Adapter模式。统一输入/输出接口定义一个抽象的Channel类包含receive()和send(user_id, message)方法。具体实现EmailChannel通过IMAP/SMTP协议收发邮件。WebhookChannel接收来自Slack、Discord、微信公众平台通过服务器配置的HTTP回调Webhook。TelegramChannel使用Telegram Bot API。核心路由一个主调度器Dispatcher负责接收来自各个Channel的消息统一调用Keap的process_command核心逻辑获取响应后再通过对应的Channel发送回去。状态管理当引入多轮对话Rasa Core后需要为每个用户在每个频道维护独立的对话状态。这通常需要一个中央化的对话状态存储如Redis键可以是f”{channel_id}:{user_id}”。5.4 安全性与隐私加固当Keap开始处理真实个人数据时安全至关重要。认证与授权目前我们用邮箱地址作为user_id。但这并不安全。未来需要引入真正的用户系统OAuth 2.0登录和API密钥认证。每个请求必须携带有效的TokenFlask API需要验证Token后才能处理。数据加密敏感信息如密码、API密钥绝不能硬编码在代码中。必须使用环境变量.env文件管理并且.env文件要加入.gitignore。数据库连接字符串、邮箱密码等都从这里读取。输入净化与防注入虽然我们使用了参数化查询?占位符来防止SQL注入但对于从NLU解析出的、最终要展示或存储的内容也要进行适当的HTML转义或过滤防止跨站脚本XSS攻击。通信加密确保所有API端点都通过HTTPSSSL/TLS提供服务。在Nginx配置中强制将HTTP重定向到HTTPS并配置安全的SSL证书。走到这一步Keap已经从最初的一个想法变成了一个拥有基本智能、可扩展架构的原型。它虽然简单但完整地展示了构建一个对话式AI助手的核心路径从自然语言理解到业务逻辑执行再到响应生成。最重要的是这个架构是面向未来设计的无论是增加新的技能如“提醒我”还是接入新的沟通渠道都有了清晰的扩展点。在下一阶段我们将真正实现邮件收发功能让Keap“活”起来并开始探索更复杂的对话管理。
从零构建通用对话机器人:Rasa NLU与Flask架构实践
发布时间:2026/6/13 5:13:45
1. 项目概述从零构建一个通用型对话机器人Keap最近在琢磨一个挺有意思的玩意儿怎么从零开始亲手搭建一个能真正理解我们说话、并且能帮我们处理点实际事情的对话机器人。这想法其实源于日常的一个小痛点——我们每天在各种App、邮件、笔记软件之间切换记录待办事项、保存一闪而过的灵感信息太分散了。能不能有个统一的“智能助手”我用最自然的语言跟它说句话它就能帮我记住、查找甚至在未来提醒我这就是“Keap”这个项目诞生的初衷。Keap我给它取这个名字是希望它能成为一个可靠的“知识管家”Keeper。在项目的这个第一阶段我们的目标很明确打造一个基于文本的对话机器人核心。它最初将通过电子邮件与我们交互就像一个永远在线的智能邮箱助手。你可以发邮件告诉它“Keap请把‘下周和客户开会’记到日程里”或者问它“Keap把我上个月记的读书清单找出来”。它需要理解这些自然语言指令背后的意图并执行相应的操作比如存储数据到数据库或者从数据库里查询并返回结果。这个架构设计是开放性的我称之为“X-Bot”架构。这里的“X”代表“任何类型”。虽然Keap的第一个化身是邮件助手但它的核心——理解语言、处理逻辑、管理数据——是通用的。未来我们可以把它的“沟通渠道”从邮件换成微信、Slack、Telegram而无需重写核心大脑。这就像给同一个大脑接上不同的耳朵和嘴巴。为了实现这个目标技术选型上我计划用Rasa NLU来处理自然语言理解用Flask或CherryPy来构建处理请求的API服务层整个系统最终要能方便地一键部署到服务器上保持24小时在线。接下来我们就深入拆解这个架构的每一层看看如何把想法变成可运行的代码。2. 核心架构设计与技术选型思路构建Keap这样的机器人不能一上来就写代码必须先理清架构。一个好的架构应该像乐高积木模块清晰、职责单一、易于替换和扩展。基于“X-Bot”的理念我将Keap初步划分为三个核心层次感知层输入、大脑层处理、执行层输出与存储。感知层负责接收用户的原始消息目前是邮件大脑层负责理解消息意图、管理对话状态并决策该做什么执行层则负责实际的数据操作存/取和生成回复。2.1 为什么选择Rasa NLU作为语言理解核心在自然语言处理NLP领域工具有很多比如微软的LUIS、Google的Dialogflow或者一些开源库如spaCy。我选择Rasa NLU主要基于以下几点考量开源与可控性Rasa是完全开源的这意味着所有数据、模型都掌握在自己手里没有隐私泄露到第三方云服务的风险。这对于处理个人任务、日程等敏感信息至关重要。你可以自己部署在服务器上完全私有化。强大的意图识别与实体提取Rasa NLU的核心任务就是把一句自然语言如“Keap把买牙刷加到购物清单里”解析成结构化的数据。它会识别出用户的“意图”add_to_list并提取出关键“实体”item: “牙刷”,list_name: “购物清单”。它的实体提取器支持多种方式包括基于规则的、基于统计模型的非常灵活。与Rasa Core的天然集成为未来铺垫Rasa其实是一个套件包含NLU理解和Core对话管理。虽然Keap第一期可能用不到复杂的多轮对话管理Core但选用Rasa NLU为未来升级留下了无缝衔接的通道。当我们需要Keap处理更复杂的、需要上下文记忆的对话时引入Rasa Core会非常顺畅。自定义能力强Rasa的pipeline处理流程可以高度定制。你可以插入自己的分词器、特征提取器、实体提取器。例如针对中文你可以轻松集成Jieba分词针对特定领域的术语可以训练自定义的实体识别模型。注意Rasa NLU在初期配置和训练数据准备上需要一些耐心。它不像云服务那样有现成的图形界面一切都需要通过配置文件config.yml和训练数据文件nlu.md来定义。但这正是其灵活性和强大之处的来源。2.2 后端API框架Flask vs. CherryPyKeap的大脑需要一个“服务器”来驱动它负责接收来自邮件渠道的请求调用Rasa NLU进行解析执行业务逻辑访问数据库然后组织回复。这个服务器本质上是一个Web API。我主要在Flask和CherryPy之间权衡。Flask这是一个极其轻量级和灵活的Python Web框架。它的哲学是“微核心”只提供最基础的路由、请求/响应处理其他功能如数据库ORM、表单验证都通过扩展来实现。对于Keap这样一个核心逻辑明确、功能相对单一的项目Flask的简洁性非常有吸引力。写一个接收JSON、处理、返回JSON的API端点用Flask可能只需要十几行代码。CherryPy这是一个更“老牌”的、面向对象的Web框架。它允许你将Web应用像普通Python对象一样构建每个URL可以映射到对象的方法。它自称是一个“支持HTTP的应用程序框架”内置了Web服务器、会话管理等功能比Flask更“全栈”一些。我的选择倾向是Flask。原因在于1)学习曲线和社区Flask的社区更大资料、解决方案更多遇到问题更容易找到答案。2)灵活性Keap的API目前可能很简单但未来如果集成认证、更复杂的中间件Flask丰富的扩展生态如Flask-RESTful, Flask-JWT能提供更多选择。3)部署友好Flask应用可以非常方便地使用Gunicorn或uWSGI配合Nginx部署这是生产环境的常见做法成熟稳定。2.3 数据存储的简单哲学Keap需要存储用户发来的信息如“买牙刷”并能按条件查询如“显示过去两天的清单”。在项目初期复杂性和稳定性是关键。因此我排除了直接使用大型关系数据库如PostgreSQL或NoSQL数据库如MongoDB的方案尽管它们功能强大。我计划采用一个更轻量、更直接的方式SQLite数据库配合Python的sqlite3库。SQLite是一个文件数据库无需安装和运行独立的数据库服务整个数据库就是一个.db文件。这对于单用户或轻量级应用的原型阶段来说简直是完美选择。它支持标准的SQL语法能很好地满足“存储”和“查询”这两个核心需求。我们可以设计一张简单的表包含字段如id主键、user_id用户标识初期可以用邮箱地址、content存储的内容、intentRasa解析出的意图、entities以JSON格式存储提取的实体、created_at创建时间。这样当用户查询“过去两天的清单”时我们只需执行一条SQL查询SELECT * FROM items WHERE user_id? AND created_at date(now, -2 days)。实操心得在原型阶段切忌过度设计。用最简单的工具SQLite快速验证核心功能自然语言理解数据存取是否跑通远比一开始就搭建一个“高大上”但复杂的架构更重要。功能验证成功后如果确有性能或扩展性需求再将数据迁移到MySQL或PostgreSQL是相对容易的。3. 系统搭建与核心模块实现详解理论说得再多不如动手搭起来。这一部分我们将一步步地构建Keap的各个模块。请确保你的开发环境已经安装了Python 3.7或以上版本。3.1 项目初始化与环境配置首先创建一个干净的项目目录并建立虚拟环境这是保证依赖隔离的好习惯。mkdir keap-bot cd keap-bot python -m venv venv # 激活虚拟环境 # 在Windows上: venv\Scripts\activate # 在Mac/Linux上: source venv/bin/activate接下来创建我们的依赖文件requirements.txt。这里列出了第一阶段所需的核心库。rasa3.6.15 flask2.3.3 python-dotenv1.0.0 sqlite3 # 通常Python标准库自带无需单独安装使用pip安装它们pip install -r requirements.txt同时创建我们的项目结构。一个清晰的结构能让后续开发维护事半功倍。keap-bot/ ├── venv/ # 虚拟环境目录 ├── requirements.txt # 项目依赖 ├── .env # 环境变量配置文件如邮箱密码切勿提交到Git ├── app.py # Flask主应用文件 ├── rasa_nlu/ # Rasa NLU相关文件 │ ├── config.yml # Rasa NLU配置文件 │ ├── data/ │ │ └── nlu.yml # NLU训练数据 │ └── models/ # 训练好的模型存放处 ├── database.py # 数据库操作封装 └── email_handler.py # 邮件收发处理模块未来实现3.2 训练Rasa NLU模型教Keap理解人话这是Keap的“大脑皮层”负责理解指令。我们在rasa_nlu/config.yml中定义NLU管道。一个适用于初学者、效果不错的配置如下version: 3.1 language: en # 初始使用英文后续可扩展中文 pipeline: - name: WhitespaceTokenizer - name: RegexFeaturizer - name: LexicalSyntacticFeaturizer - name: CountVectorsFeaturizer - name: CountVectorsFeaturizer analyzer: char_wb min_ngram: 1 max_ngram: 4 - name: DIETClassifier epochs: 100 constrain_similarities: true - name: EntitySynonymMapper - name: ResponseSelector epochs: 100 - name: FallbackClassifier threshold: 0.7这个管道包含了分词、特征提取、意图和实体分类DIETClassifier等组件。DIETClassifier是Rasa的一个强大分类器能同时处理意图分类和实体识别。接下来我们需要准备训练数据。在rasa_nlu/data/nlu.yml中我们定义一些示例语句和对应的意图、实体。version: 3.1 nlu: - intent: greet examples: | - hey - hello - hi - good morning - good evening - intent: add_item examples: | - please add [toothbrush](item) to my [shopping](list_name) list - store [buy milk](item) in the [grocery](list_name) list - Keap, remember to [call John tomorrow](item) - add [read AI paper](item) to [tasks](list_name) - intent: query_items examples: | - show me my list from the [last 2 days](time_range) - what did I store [yesterday](time_range)? - get my [shopping](list_name) list - Keap, fetch all my tasks - intent: goodbye examples: | - bye - goodbye - see you - have a nice day - synonym: shopping examples: | - shopping list - buy list - stuff to buy - synonym: tasks examples: | - todo - things to do - work items这里定义了四个意图问候greet、添加项目add_item、查询项目query_items和告别goodbye。在add_item的例子中我们用方括号[]标注了实体如[toothbrush](item)表示文本“toothbrush”是一个类型为item的实体。底部的synonym部分定义了同义词确保“shopping list”和“buy list”能被归一化处理。现在进入rasa_nlu目录开始训练模型cd rasa_nlu rasa train nlu训练完成后模型会保存在rasa_nlu/models目录下。你可以使用以下命令在命令行快速测试一下模型的理解能力rasa shell nlu输入“add toothbrush to shopping list”看看它是否能正确识别出意图add_item和实体item: toothbrush,list_name: shopping。3.3 构建Flask APIKeap的神经中枢有了能理解语言的模型我们需要一个API来调用它并处理业务逻辑。回到项目根目录创建app.py。from flask import Flask, request, jsonify import sqlite3 from datetime import datetime, timedelta import json import os from rasa.shared.nlu.training_data.message import Message from rasa.nlu.model import Interpreter app Flask(__name__) # 加载训练好的Rasa NLU模型 MODEL_PATH ./rasa_nlu/models/nlu-20240315-103045.tar.gz # 请替换为你的实际模型路径 interpreter Interpreter.load(MODEL_PATH) # 初始化数据库 def init_db(): conn sqlite3.connect(keap.db) c conn.cursor() c.execute( CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, content TEXT NOT NULL, intent TEXT, entities TEXT, -- 存储为JSON字符串 list_name TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.commit() conn.close() app.route(/parse, methods[POST]) def parse_message(): 解析用户消息的端点 data request.json message_text data.get(text, ) if not message_text: return jsonify({error: No text provided}), 400 # 使用Rasa NLU解析消息 result interpreter.parse(message_text) return jsonify(result) app.route(/process, methods[POST]) def process_command(): 处理用户命令的核心端点模拟邮件处理后的调用 data request.json user_id data.get(user_id, default_user) # 实际应从邮件发件人获取 message_text data.get(text, ) # 1. 解析意图和实体 nlu_result interpreter.parse(message_text) intent nlu_result[intent][name] entities {entity[entity]: entity[value] for entity in nlu_result[entities]} # 2. 根据意图执行逻辑 response_text conn sqlite3.connect(keap.db) c conn.cursor() if intent add_item: item_content entities.get(item, message_text) # 如果没有提取到实体则用全文 list_name entities.get(list_name, default) # 将实体字典转为JSON字符串存储 entities_json json.dumps(entities) c.execute( INSERT INTO items (user_id, content, intent, entities, list_name) VALUES (?, ?, ?, ?, ?) , (user_id, item_content, intent, entities_json, list_name)) conn.commit() response_text fGot it. Ive added {item_content} to your {list_name} list. elif intent query_items: list_name entities.get(list_name) time_range entities.get(time_range) # 构建查询条件 query SELECT content, list_name, created_at FROM items WHERE user_id ? params [user_id] if list_name: query AND list_name ? params.append(list_name) if time_range: # 这里需要解析time_range实体例如“last 2 days” # 这是一个简化版实际需要更复杂的自然语言时间解析可考虑用dateparser库 if last in time_range and day in time_range: try: days int(time_range.split()[1]) # 非常简单的提取 start_date (datetime.now() - timedelta(daysdays)).strftime(%Y-%m-%d %H:%M:%S) query AND created_at ? params.append(start_date) except: pass # 解析失败忽略时间条件 query ORDER BY created_at DESC c.execute(query, params) items c.fetchall() if items: item_list \n.join([f- {row[0]} ({row[1]}, {row[2]}) for row in items]) response_text fHere are your items:\n{item_list} else: response_text You have no items in your list for the given criteria. elif intent greet: response_text Hello! Im Keap, your personal assistant. How can I help you today? elif intent goodbye: response_text Goodbye! Have a great day. else: response_text Im not sure how to handle that yet. You can ask me to add things to a list or query your saved items. conn.close() return jsonify({ user_id: user_id, original_text: message_text, intent: intent, entities: entities, response: response_text }) if __name__ __main__: init_db() # 启动时初始化数据库 app.run(debugTrue, port5000)这个Flask应用提供了两个关键端点/parse纯粹测试NLU解析输入文本返回Rasa的解析结果。/process这是Keap的核心处理逻辑。它接收用户ID和文本先调用NLU解析然后根据意图intent执行相应的数据库操作插入或查询最后组织一个友好的文本回复。运行python app.pyFlask服务将在http://127.0.0.1:5000启动。你可以使用Postman或curl进行测试curl -X POST http://127.0.0.1:5000/process \ -H Content-Type: application/json \ -d {user_id: aliceexample.com, text: Keap, add buy toothpaste to my shopping list}预期会返回一个JSON包含解析结果和操作响应。3.4 数据层封装与未来邮件集成展望为了代码更清晰我们将数据库操作单独封装。创建database.pyimport sqlite3 import json from contextlib import contextmanager DB_PATH keap.db contextmanager def get_db_connection(): 上下文管理器确保数据库连接正确关闭 conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row # 使返回的行像字典一样访问 try: yield conn finally: conn.close() def add_item(user_id, content, intent, entities, list_namedefault): with get_db_connection() as conn: c conn.cursor() entities_json json.dumps(entities) c.execute( INSERT INTO items (user_id, content, intent, entities, list_name) VALUES (?, ?, ?, ?, ?) , (user_id, content, intent, entities_json, list_name)) conn.commit() return c.lastrowid def query_items(user_id, list_nameNone, start_dateNone): with get_db_connection() as conn: c conn.cursor() query SELECT * FROM items WHERE user_id ? params [user_id] if list_name: query AND list_name ? params.append(list_name) if start_date: query AND created_at ? params.append(start_date) query ORDER BY created_at DESC c.execute(query, params) return [dict(row) for row in c.fetchall()] # 返回字典列表然后在app.py中就可以导入并使用这些函数使视图函数更简洁。关于邮件集成这是将Keap变为“24小时在线助手”的关键一步。思路是为Keap创建一个专属邮箱如keap.assistantyourdomain.com。使用Python的imaplib库定期或通过服务器钩子检查收件箱。当收到新邮件时提取发件人作为user_id和邮件正文作为message_text。调用我们写好的/processAPI或者直接导入处理函数。将API返回的response文本通过smtplib库以回复邮件的形式发送给用户。由于邮件协议处理、安全认证OAuth2等内容较多这将是下一阶段Part 2的重点。目前我们已经拥有了Keap最核心的“大脑”和“神经反射弧”API。4. 开发中的常见问题与调试技巧在搭建Keap的过程中我踩过不少坑。这里把一些典型问题和解决方法记录下来希望能帮你节省时间。4.1 Rasa NLU训练与解析问题问题1训练时警告“No training data found”或模型效果极差。排查首先检查nlu.yml文件的格式是否正确特别是缩进。YAML对缩进非常敏感。确保version: “3.1”和nlu:在顶层每个intent下的examples:使用正确的缩进。可以使用在线YAML验证器检查。解决确保示例数量足够。每个意图至少提供15-30个不同表达方式的例子覆盖同义词、不同句式。实体标注要一致。问题2实体识别不准特别是复合实体或长句子。排查检查训练数据中是否包含了足够多带有该实体的、句式多样的例子。例如对于item实体不仅要标注“买 牙刷 ”还要有“记得 下班后去超市买牛奶和面包 ”。解决在config.yml的pipeline中可以调整DIETClassifier的epochs如增加到200或embedding_dimension。考虑使用RegexFeaturizer和RegexEntityExtractor来辅助识别有固定模式的内容如日期、时间、产品编号。增加训练数据是最根本的方法。问题3意图混淆比如把“show me my tasks”识别为greet。排查检查greet意图的示例是否过于宽泛或者query_items的示例是否不足。使用rasa test nlu命令可以生成详细的混淆矩阵直观看到哪些意图容易被混淆。解决为容易混淆的意图增加更多具有区分度的示例。例如在query_items中增加“what’s on my list”、“display my stored items”等。也可以考虑在config.yml中启用FallbackClassifier并设置一个合适的threshold如0.7当最高置信度低于阈值时归类为nlu_fallback意图你可以让Keap回复“我没太听懂你能换种说法吗”。4.2 Flask API开发与集成问题问题1跨域请求CORS错误当从前端页面调用API时。现象浏览器控制台报错“Access-Control-Allow-Origin”。解决使用Flask-CORS扩展。安装pip install flask-cors然后在app.py中初始化from flask_cors import CORS; CORS(app)。在生产环境中需要配置更严格的CORS策略。问题2SQLite数据库并发写入冲突。现象在多线程环境下如Flask开发服务器处理多个并发请求可能遇到sqlite3.OperationalError: database is locked。解决使用连接池或确保单线程连接在上面的get_db_connection上下文管理器中每次请求都创建新连接并由上下文管理器确保关闭这在轻量并发下通常可行。更严谨的做法是使用SQLAlchemy等ORM它内置了连接池管理。设置超时在连接时设置timeout参数sqlite3.connect(DB_PATH, timeout10)。这会让SQLite在遇到锁时等待最多10秒。考虑升级数据库如果并发量真的很大应考虑迁移到PostgreSQL或MySQL。问题3处理自然语言时间表达式如“last 2 days”挑战我们在NLU中提取了time_range实体但它的值是字符串“last 2 days”我们需要将其转换为程序可理解的日期时间范围。解决方案不要自己写复杂的解析器。使用第三方库如dateparserpip install dateparser。它非常强大import dateparser time_text entities.get(time_range) if time_text: parsed_date dateparser.parse(time_text, settings{RELATIVE_BASE: datetime.now()}) if parsed_date: start_date parsed_date.strftime(%Y-%m-%d %H:%M:%S) # 使用start_date进行数据库查询这能处理“yesterday”, “2 hours ago”, “next Monday”等多种表达。4.3 项目部署与持续运行的考量问题如何让Keap 7x24小时运行本地运行不可靠你的电脑不能一直开着且IP可能变化。解决方案部署到云服务器。步骤如下选择服务器购买一台最基础的Linux云服务器如1核1G。上传代码使用Git或SFTP将项目代码上传到服务器。安装依赖在服务器上同样创建虚拟环境并安装requirements.txt。使用生产级WSGI服务器不要直接用flask run。使用Gunicornpip install gunicorn然后运行gunicorn -w 4 -b 0.0.0.0:5000 app:app。-w 4表示启动4个工作进程处理并发。设置反向代理使用Nginx作为反向代理处理静态文件、SSL加密HTTPS并将请求转发给Gunicorn。这能提升安全性和性能。进程守护使用Systemd或Supervisor来管理Gunicorn进程确保它崩溃后能自动重启。邮件检查守护进程未来实现邮件处理器email_handler.py后也需要用Systemd/Supervisor将其作为一个常驻服务运行定期检查邮箱。核心避坑技巧在开发过程中务必编写日志。在app.py开头配置import logging; logging.basicConfig(levellogging.DEBUG)。在所有关键步骤收到请求、解析结果、数据库操作、发送回复都记录日志。这样当线上出现问题时查看日志文件能快速定位问题根源远比盲目猜测高效。5. 性能优化与后续迭代方向目前Keap已经具备了最核心的“理解-执行-回复”闭环。但作为一个有追求的项目我们当然不满足于此。以下是一些让Keap变得更聪明、更健壮、更实用的优化和扩展思路。5.1 提升NLU理解能力的进阶策略当前的NLU模型基于有限的示例数据。要提升其泛化能力数据增强手动编写所有示例费时费力。可以使用同义词替换、句式变换等工具自动生成更多训练数据。例如将“add milk ”自动扩展为“please add milk ”、“could you add milk ”、“I need to add milk ”等。引入预训练词向量在config.yml的pipeline中可以在CountVectorsFeaturizer之前或之后加入LanguageModelFeaturizer并配置如HuggingFace的预训练模型例如bert。这能让模型更好地理解词汇在上下文中的语义显著提升对未在训练数据中出现过的表达方式的理解能力。自定义组件如果Keap需要处理非常专业的领域术语比如医学、法律可以编写自定义的Rasa NLU组件集成领域特定的词典或规则。5.2 架构解耦与消息队列引入目前的架构中Flask API同步处理所有请求接收、NLU解析、数据库操作、生成回复。如果NLU解析或数据库查询很慢会阻塞整个请求影响用户体验。引入异步处理与消息队列场景当用户通过邮件发送一个复杂查询如“找出我所有关于‘机器学习’的记录”时数据库全文搜索可能需要几秒钟。方案我们可以引入一个消息队列如Redis或RabbitMQ。流程变为Flask API接收到请求后立即返回一个“已收到正在处理”的响应。同时将处理任务包含用户ID、消息文本放入消息队列。一个独立的“工作进程”Worker从队列中取出任务执行耗时的NLU解析和数据库查询。工作进程完成后通过邮件或未来的其他渠道将结果异步发送给用户。好处API响应极快用户体验好耗时任务被分流系统更稳定工作进程可以水平扩展应对高负载。5.3 多模态与多渠道扩展这是“X-Bot”理念的终极体现。我们需要设计一个“适配器”Adapter模式。统一输入/输出接口定义一个抽象的Channel类包含receive()和send(user_id, message)方法。具体实现EmailChannel通过IMAP/SMTP协议收发邮件。WebhookChannel接收来自Slack、Discord、微信公众平台通过服务器配置的HTTP回调Webhook。TelegramChannel使用Telegram Bot API。核心路由一个主调度器Dispatcher负责接收来自各个Channel的消息统一调用Keap的process_command核心逻辑获取响应后再通过对应的Channel发送回去。状态管理当引入多轮对话Rasa Core后需要为每个用户在每个频道维护独立的对话状态。这通常需要一个中央化的对话状态存储如Redis键可以是f”{channel_id}:{user_id}”。5.4 安全性与隐私加固当Keap开始处理真实个人数据时安全至关重要。认证与授权目前我们用邮箱地址作为user_id。但这并不安全。未来需要引入真正的用户系统OAuth 2.0登录和API密钥认证。每个请求必须携带有效的TokenFlask API需要验证Token后才能处理。数据加密敏感信息如密码、API密钥绝不能硬编码在代码中。必须使用环境变量.env文件管理并且.env文件要加入.gitignore。数据库连接字符串、邮箱密码等都从这里读取。输入净化与防注入虽然我们使用了参数化查询?占位符来防止SQL注入但对于从NLU解析出的、最终要展示或存储的内容也要进行适当的HTML转义或过滤防止跨站脚本XSS攻击。通信加密确保所有API端点都通过HTTPSSSL/TLS提供服务。在Nginx配置中强制将HTTP重定向到HTTPS并配置安全的SSL证书。走到这一步Keap已经从最初的一个想法变成了一个拥有基本智能、可扩展架构的原型。它虽然简单但完整地展示了构建一个对话式AI助手的核心路径从自然语言理解到业务逻辑执行再到响应生成。最重要的是这个架构是面向未来设计的无论是增加新的技能如“提醒我”还是接入新的沟通渠道都有了清晰的扩展点。在下一阶段我们将真正实现邮件收发功能让Keap“活”起来并开始探索更复杂的对话管理。