--- 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, scenario.data-sync, scenario.customer-service-kb] 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 天后归档。”