Electron 构建多店铺智能客服系统:会话隔离、消息抓取与自动回复实现
项目目前主要围绕以下几个问题展开:
这篇文章记录一下整个项目的架构和几个比较关键的实现思路。
注意:本文主要讨论 Electron、网页观察、本地数据处理和 AI API 集成等技术实现。涉及第三方平台的功能应优先使用其官方开放接口,并遵守对应平台的最新规则。
这个项目本质上需要同时具备两种能力:
第一种是浏览器能力。
需要加载现有的商家后台网页,并维护 Cookie、登录状态以及网页运行环境。
第二种是桌面程序能力。
需要:
Electron 正好把 Chromium 和 Node.js 组合在一起,因此比较适合这种“网页 + 本地应用”的场景。
项目核心依赖:
整体结构大致如下:
其中:
负责 Electron 主进程、窗口管理、账号管理、数据存储以及 AI 调用。
负责观察被加载的商家页面,并在网页环境和 Electron 主进程之间建立桥梁。
负责本地快捷回答匹配。
这种拆分方式有一个好处:
页面观察、业务逻辑和 UI 不会全部堆在一个文件里。
如果直接创建多个普通网页窗口,很容易遇到一个问题:
登录 A 店铺之后,另一个窗口也变成 A 店铺。
原因是多个页面可能共用 Chromium Session。
多店铺客服工具真正需要的是:
每个店铺必须拥有自己的浏览器会话。
Electron 的 Session/Partition 机制非常适合解决这个问题。
设计上可以为每个账号生成稳定的 partition:
然后让对应页面始终使用这个 partition。
这样:
互不影响。
而 persist: 前缀还有一个非常重要的作用:
关闭程序以后 Session 仍然可以持久化。
下一次打开程序时,只要平台本身的登录状态仍然有效,就不需要重新登录全部店铺。
对于多账号桌面应用而言,这是整个架构的基础。
项目中另外一个比较重要的设计思想是:
店铺账号是业务对象,浏览器页面只是它的一个运行载体。
本地保存类似:
用于记录:
这样以后即使重新创建窗口,也可以根据账号 ID 找回对应 Session。
整个关系变成:
而不是:
这样后期做窗口重建、页面异常恢复以及批量店铺操作会方便很多。
客服工作台的第二个核心问题是:
怎么知道客户发来了新消息?
如果没有官方消息接口,一种研究思路是在 preload 环境中观察当前页面。
项目将这一部分单独放在:
中。
页面观察层负责识别:
然后通过 IPC 将结构化数据交给主进程。
架构类似:
这里有一个非常重要的原则:
不要让 Renderer 直接负责网页采集。
Renderer 应该只关心:
而不是:
否则平台网页稍微发生变化,整个应用 UI 层也会受到影响。
网页观察有一个非常常见的问题:
同一条消息可能被观察到很多次。
例如:
如果直接保存,就可能出现:
因此消息进入本地存储前必须进行去重。
一种常见方案是给消息构造稳定标识:
然后生成 message key。
例如:
保存之前判断:
实际项目中还需要考虑:
因此不能简单地只使用:
作为唯一标识。
为了降低部署复杂度,这个项目没有一开始就引入数据库,而是使用本地 JSON 文件。
类似:
每个店铺单独保存。
这样的优点是:
但普通:
并不是非常可靠。
如果程序恰好在写文件过程中异常退出,JSON 可能只写了一半。
所以项目采用了类似:
的方式降低损坏概率。
对于 Electron 本地工具来说,这是一个很值得做的小优化。
最开始设计 AI 客服时,很容易想到:
但实际运行后会发现大量客服问题都是重复的:
这些问题调用一次大模型,本质上是在浪费:
所以项目增加了一层:
完整流程变成:
这样高频问题基本不需要调用远程模型。
最简单的快捷回答可能这样写:
但真实客户可能会说:
所以项目实现了一个简单的中文概念归一化系统。
例如:
这样:
都会被归一化为:
客服消息中还有大量没有实际语义的信息:
例如:
真正有价值的信息可能只有:
因此在计算相似度之前,会先进行文本标准化:
这里还使用:
处理 Unicode 字符形式差异。
为了保持整个本地匹配器足够轻量,项目没有引入大型 NLP 模型,而是使用字符 Bigram。
例如:
可以拆成:
然后计算两个集合之间的 Dice Similarity。
核心思路:
这样就得到一个:
之间的相似度分数。
相比简单关键词匹配,这种方法对于:
会更加友好。
这里踩过一个比较有意思的坑。
例如:
和:
文本非常相似。
但是业务含义完全不同。
一个问:
另一个问:
如果只计算字符串相似度,很容易误匹配。
因此代码中增加了概念冲突:
也就是说:
当输入和规则存在明显概念冲突时,直接降低甚至取消匹配。
最终评分不是简单的字符串相似度,而是类似:
这种方法虽然没有 embedding 那么强,但优势非常明显:
非常适合几十到几百条客服 FAQ。
有时候一条消息虽然命中了关键词,但实际上并不应该触发对应回答。
例如规则:
可能需要排除:
因此每条快捷回答规则包含:
匹配过程先检查:
只有达到阈值并且没有明显歧义,才返回快捷回答。
这个设计可以显著降低自动客服的“答非所问”。
最终形成的回复架构实际上是三级:
相比所有消息直接发送给大模型,这种架构更适合客服系统。
原因很简单:
确定性问题应该由确定性系统解决。
AI 更适合处理:
而不是用来回答每天重复几百次的:
为了避免系统和单一 AI 服务绑定,项目把 AI Provider 抽象成三种:
用户可以自行选择。
整体结构:
上层客服逻辑并不需要知道底层究竟调用了谁。
FastGPT 提供 OpenAI 风格的 Chat API,因此接入成本比较低。
请求大致可以抽象为:
其中一个很有用的字段是:
可以为:
生成稳定 ID。
例如:
这样不同客户的上下文不会混在一起。
FastGPT 官方当前文档也说明,chat/completions 推荐在请求体中传入 appId,并可通过 chatId 管理应用下的独立会话。
Dify Chatflow 同样存在会话上下文问题。
假设:
如果没有前面的聊天记录:
实际上无法判断指什么。
因此系统为:
保存独立:
结构类似:
这样每个客户都拥有自己的 AI 上下文。
Electron 桌面应用还有一个非常重要的问题:
API Key 放在哪里?
最简单的方法当然是:
但这样任何能够读取文件的人都能直接看到 Key。
项目使用 Electron:
进行本地加密:
保存时转换成 Base64:
使用时再:
这样至少不会直接把 API Key 以明文形式放在配置文件中。
当然需要说明:
safeStorage 并不能代替完整的操作系统安全措施。
如果系统账号本身已经被攻破,本地应用也无法保证绝对安全。
项目给每个店铺设计了三种模式:
其中实际更推荐:
工作流程:
原因是大模型可能出现:
客服系统一旦自动发送错误信息,影响的是实际客户。
因此“AI 生成 + 人工确认”通常是更稳妥的过渡方案。
自动客服还有一个容易被忽略的问题:
AI 回答正确,不代表消息发送就是安全的。
假设程序当前处理:
但页面因为刷新或者人工操作切换到了:
这时候如果代码直接:
就可能把客户 A 的回复发送给客户 B。
所以自动发送之前必须重新确认目标身份。
设计上应该:
这里宁可:
也不要:
这是自动客服系统非常重要的一条安全原则。
除了监听实时消息,项目还实现了历史会话批量采集。
流程大致是:
这里不能一切换会话就马上读取 DOM。
因为现代网页通常存在异步渲染。
例如:
因此需要:
而不是单纯:
把所有模块组合起来以后,大概是:
做完这个项目以后,我觉得有几个设计思路比较值得保留。
例如:
很多都是确定性问题。
使用:
通常比大模型更:
比较合理的客服链路是:
而不是:
无论做的是:
只要存在多账号,第一件事都应该考虑:
否则后期会出现大量登录状态污染问题。
比如自动发送。
真正可靠的逻辑不是:
而是:
任何一步无法确认:
这比单纯追求自动化成功率重要得多。
目前这个架构还有很多可以继续扩展的地方。
例如:
当聊天记录越来越多以后,可以把:
迁移到:
实现更好的查询、索引和数据管理。
当前快捷回答使用:
未来可以增加 embedding:
提高自然语言问题的召回率。
本地知识库目前可以进一步升级成:
从简单文本知识库变成真正的 RAG。
整个架构实际上并不一定只能处理一个电商平台。
如果把:
进一步抽象成:
理论上可以形成:
上层:
都可以继续复用。
这个项目表面上看是一个 Electron 客服工具,但真正值得研究的是背后的几个通用问题:
其中我认为最有价值的并不是“接入了 AI”,而是:
把确定性的本地规则、业务知识和不确定性的大模型组合起来。
高频问题由本地系统解决,复杂问题交给 AI,风险操作再增加人工确认。
这种架构不仅适用于电商客服。
同样可以用在:
等场景。
后续如果继续迭代,我准备重点尝试:
把现在的“AI 辅助客服”进一步变成一个更加通用的本地智能客服工作台。