79 lines
3.8 KiB
Markdown
79 lines
3.8 KiB
Markdown
---
|
||
id: backend.mongodb.scenario.conversation-store
|
||
title: AI 客服会话存储:海量对话记录的建模、查询、归档和同步
|
||
type: scenario
|
||
domain: backend
|
||
stack: [mongodb, postgresql, elasticsearch]
|
||
tags: [对话记录, 文档建模, TTL, 归档, 变更流, 多租户]
|
||
level: intermediate
|
||
prerequisites: [backend.mongodb.document-model]
|
||
est_hours: 2
|
||
related: [backend.mongodb.interview]
|
||
week: [14]
|
||
---
|
||
|
||
# AI 客服会话存储:海量对话记录的建模、查询、归档和同步
|
||
|
||
## 业务背景与问题
|
||
|
||
AI 客服上线后,每天产生大量对话(用户消息、模型回复、工具调用记录、评分),原先存在 PostgreSQL 的一张 JSONB 大表中:
|
||
|
||
1. 消息结构经常变化(新增工具调用、图片、引用来源),频繁改表;
|
||
2. 坐席后台按租户查“待处理会话”越来越慢;
|
||
3. 需要按关键词搜索历史对话;
|
||
4. 数据只需要在线保留 180 天,之后归档。
|
||
|
||
## 约束
|
||
|
||
- 订单、退款等核心交易数据仍在 PostgreSQL,强一致。
|
||
- 对话数据允许最终一致的检索。
|
||
- 多租户隔离。
|
||
|
||
## 方案设计
|
||
|
||
1. **职责划分**:交易数据留在 PostgreSQL;对话记录迁移到 MongoDB(结构灵活、写入量大);全文 / 语义检索交给 Elasticsearch。
|
||
2. **建模**:
|
||
- `conversations`:会话元数据 + 最近 N 条消息嵌入(列表页和详情首屏一次读取);
|
||
- `messages`:完整消息单独集合,按 `conversation_id + seq` 引用(避免无限数组);
|
||
- 每个文档都带 `tenant_id`。
|
||
3. **索引**:`{tenant_id, status, updated_at: -1}`(坐席列表,ESR)、`{conversation_id, seq}` 唯一(幂等写入和顺序读取)、`messages.created_at` 上的 TTL 索引或按月归档任务。
|
||
4. **同步检索**:变更流 → 消费者 → 写入 ES;保存 resume token,断线后续传;消费者幂等(以消息 id 为 ES 文档 id)。
|
||
5. **一致性**:转人工(改会话 + 建工单)用事务,或者把工单放在 PostgreSQL 并通过 outbox 模式保证一致。
|
||
|
||
## 关键代码
|
||
|
||
```python
|
||
# 追加消息:唯一索引保证重复投递不重复写入;同时更新会话的最近消息(保留最后 20 条)
|
||
messages.update_one(
|
||
{"conversation_id": cid, "seq": seq},
|
||
{"$setOnInsert": {"tenant_id": tenant, "role": role, "text": text, "created_at": now}},
|
||
upsert=True,
|
||
)
|
||
conversations.update_one(
|
||
{"_id": cid, "tenant_id": tenant},
|
||
{"$push": {"recent": {"$each": [{"seq": seq, "role": role, "text": text}], "$slice": -20}},
|
||
"$set": {"updated_at": now}},
|
||
)
|
||
```
|
||
|
||
可运行的基础示例:[examples/conversations](../examples/conversations/)。
|
||
|
||
## 取舍
|
||
|
||
| 选择 | 优点 | 缺点 |
|
||
| --- | --- | --- |
|
||
| **MongoDB 存对话 + PG 存交易(本方案)** | 各取所长,结构灵活 | 多一种数据库要运维,跨库一致性要设计 |
|
||
| 全部留在 PostgreSQL JSONB | 技术栈简单,事务方便 | 写入量大时表膨胀,结构变化仍需维护索引 |
|
||
| 对话直接写 ES | 检索方便 | ES 不适合作为主存储(更新代价、一致性) |
|
||
|
||
## 易错点
|
||
|
||
- 消息全部嵌入会话文档,长会话超过 16MB。
|
||
- 查询忘记带 `tenant_id`,越权读取其他租户数据。
|
||
- 变更流消费者没有保存 resume token,重启后丢事件或全量重放。
|
||
- 归档前没有确认合规要求(对话内容可能包含个人信息,需要脱敏或加密)。
|
||
|
||
## 面试怎么讲
|
||
|
||
> “对话记录结构多变、写入量大,我把它从 PG 的 JSONB 表迁到了 MongoDB,交易数据留在 PG。会话文档嵌入最近 20 条消息,完整消息单独集合用 conversation_id+seq 唯一索引,既避免无限数组又保证幂等。坐席列表按 ESR 原则建复合索引。检索通过变更流同步到 ES,保存 resume token 断点续传。180 天后归档。”
|