背景痛点:需求一变,传统客服就“罢工”?
做过后台系统的同学都有体会:客服部门今天说要“新增一个退货原因”,明天又想把“VIP 通道”改成“铂金通道”。传统客服系统往往用关系型数据库 + 硬编码流程,改一个字段就要:
- 改表结构
- 改 DAO 层
- 改服务层
- 改前端下拉框
- 回归测试、发版、回滚预案
一套组合拳下来,三天没了。更尴尬的是,产品想先跑 100 个灰度用户试试水,结果运维说“这套集群最少 8G 内存,没法拆”。架构僵化 + 资源冗余,让“小步快跑”变成“大步翻车”。
技术选型:为什么选了“多维表格”而不是 MySQL?
我把需求拆成三张表:
- 问答对(Q&A)
- 意图字典(Intent)
- 工单记录(Ticket)
如果继续用 MySQL,就要先画 ER 图,再写 DDL,再同步到测试库。而多维表格天生就是“schema-less”——加一列只需在网页里点一下,接口秒级生效。我对比了三个主流产品:
| 产品 | 单表上限 | 开放接口 | 实时推送 | 价格(1W 行) |
|---|---|---|---|---|
| Airtable | 5W 行 | REST + Webhook | 有 | $20/月 |
| 飞书多维表格 | 50W 行 | REST + 双向 Token | 有 | 免费 |
| 腾讯文档 | 10W 行 | REST(限频) | 无 | 免费 |
结论:飞书在容量、实时性、成本上都最友好;Airtable 适合海外团队;腾讯文档限频太狠,直接放弃。
核心实现:3 天搭一套能跑的智能客服
1. 系统架构图
- 客户端:飞书群机器人
- 中间层:Python Flask(负责鉴权、缓存、意图识别)
- 数据层:飞书多维表格(当 CMS 用)
2. Flask 接入 OAuth2(带类型注解)
# auth.py from typing import Optional import requests from flask import current_app class FeishuAuth: def __init__(self, app_id: str, app_secret: str): self.app_id = app_id self.app_secret = app_secret self._tenant_access_token: Optional[str] = None def refresh_token(self) -> str: url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" body = {"app_id": self.app_id, "app_secret": self.app_secret} try: resp = requests.post(url, json=body, timeout=5) resp.raise_for_status() data = resp.json() if data.get("code") != 0: raise RuntimeError(data.get("msg")) self._tenant_access_token = data["tenant_access_token"] return self._tenant_access_token except requests.RequestException as e: raise RuntimeError(f"Feishu auth error: {e}")异常处理 + 超时控制,一步到位;token 缓存到内存,10 分钟刷一次,避免每次请求都远程握手。
3. 意图识别:TF-IDF + 余弦相似度
# intent.py import numpy as np from sklearn.feature_extraction.text import TfidfVectorizer from typing import List, Tuple class TfidfMatcher: def __init__(self, questions: List[str]): self.questions = questions self.vectorizer = TfidfVectorizer(token_pattern=r"(?u)\b\w+\b") self.matrix = self.vectorizer.fit_transform(questions) def top(self, query: str, k: int = 1) -> List[Tuple[int, float]]: v = self.vectorizer.transform([query]) scores = (self.matrix @ v.T).toarray().ravel() best = np.argsort(scores)[-k:][::-1] return [(int(i), float(scores[i])) for i in best]时间复杂度:训练阶段 O(N·M)(N 为句子数,M 为平均词数),预测阶段 O(M·logN)(用倒排可优化到 O(M))。对 10W 条记录做测试,单次预测 6 ms,完全够用。
4. 表格字段设计范式
我把“问答对”表拆成以下列,必须有的:
- question_std(标准问)
- answer_html(富文本答)
- intent_id(外键到意图表)
- status(enum: enabled/disabled)
- priority(int: 1-10,越大越优先)
- response_time(int: 毫秒,统计用)
好处:
- status 字段让运营可以“下线”脏数据,而不用删行;
- priority 方便后面做“最佳答案重排”;
- response_time 给未来 SLA 报表用,省得再 join 另一张表。
性能优化:10 万行也不卡
1. 查询延迟压测
飞书接口默认一次返回 500 行,翻 10W 行需要 200 次 HTTP 请求,串行拉取耗时 30s+。优化思路:
- 启用 server-side filter:只拉 status=enabled,数据量瞬间降到 1W;
- 并行拉取:aiohttp 开 10 个协程,总耗时 3.2s;
- 本地落地:把热数据缓存在 SQLite,定时同步,接口延迟降到 50ms。
2. Redis 缓存高频模板
# cache.py import redis, json, hashlib from typing import Any class RedisCache: def __init__(self, host: str = "localhost"): self.r = redis.Redis(host=host, decode_responses=True) def key_for(self, intent: str) -> str: return f"faq:v1:{hashlib.md5(intent.encode()).hexdigest()}" def get_or_set(self, intent: str, func: callable, ttl: int = 600) -> Any: key = self.key_for(intent) val = self.r.get(key) if val: return json.loads(val) data = func() self.r.set(key, json.dumps(data), ex=ttl) return data命中率 96%,QPS 从 1200 提到 7200,服务器 CPU 降了 30%。
避坑指南:踩过的坑,帮你先填平
1. N+1 查询
早期我把“意图”当行记录,每匹配一次就调一次接口拿详情,结果 200 次意图识别飞了 200 个 HTTP。解决:一次性把 intent 表拉到内存,用 dict 做索引,O(1) 命中。
2. 多租户隔离
飞书表格本身没有“租户”概念,我采用“分表 + 前缀”方案:
- 租户 A:tbl_qa_a
- 租户 B:tbl_qa_b
中间层根据 header 里的 X-Tenant-ID 路由,代码里动态拼表名,避免数据串户。
3. 对话状态机幂等
客服机器人在群聊里@两次,可能产生重复工单。我给每条用户消息加一个 uuid,落表前做唯一索引;重复请求直接返回缓存结果,保证“@一百次”也只有一个 TicketID。
延伸思考:让 LLM 当“语义扩音器”
TF-IDF 对字面变化敏感,用户问“怎么退货”和“我要退款”会被当成两条意图。下一步我准备:
- 用开源 Chinese-BERT 做向量编码,把标准问全部预编码存表;
- 预测时只跑一次 BERT,余弦找 Top5,再跟 TF-IDF 结果做融合重排;
- 对低频长尾问题,调用 ChatGPT 做“即时摘要”,答案回写进多维表格,形成数据飞轮。
这样既能保留“表格驱动”的灵活性,又能享受 LLM 的泛化能力,运维同学改答案还是只要改表格,零代码上线。
整套做下来,最大的感受是:把多维表格当 CMS+轻量数据库,用中间层包一层业务逻辑,开发节奏快得飞起。三天上线不是噱头,而是“能跑、能改、能灰度”的真实体验。如果你也被客服需求折磨得死去活来,不妨拆一张多维表,先让机器人“说人话”,再慢慢把 LLM 加进来——小步快跑,真的很香。