用途
该技能用于将 Podium 组织的历史会话、评价和联系人数据批量导出,构建适合嵌入向量库的语料。适用于客户拥有多年知识积累、AI 团队希望所有会话线程、评价和联系人备注都能按相似度检索的场景。它不是一次性脚本,而是“全量导出 + 增量 CDC”的完整管道:先一次性摄取历史积压数据,再通过每晚的 updated_at 水位扫描保持语料最新。
主要能力
该技能针对六类生产环境故障提供防护:
- 游标分页漂移:Podium 的 next_cursor 是基于排序键和位置的服务器端状态。若导出过程中会话被创建、更新或删除,朴素游标遍历会重复或跳过记录。正确做法是将排序固定到稳定的单调字段,并按 id 去重。
- 增量 CDC 缺口:朴素的 updated_at > $watermark 查询会遗漏恰好发生在水位秒的写入。正确 CDC 使用 >= 并带显式重叠余量,在加载器中按 (id, updated_at) 去重。
- 附件 URL 中途过期:Podium 附件 URL 是预签名的 S3 风格 URL,约 15 分钟过期。正确的下载器检测 403,按 attachment_id 获取新的签名 URL 并恢复。
- 超大线程分块失败:4000 条消息的会话线程若朴素拼接会超出典型 8K token 嵌入预算。分块需按语义边界(轮次边界、日期边界、空闲间隔)窗口化,并输出重叠块以支持跨窗口检索。
- 嵌入前未脱敏 PII:向量库实际上永久保存,一旦嵌入客户 SSN、信用卡号或地址便无法撤销。需在块生成时、计算任何向量之前,使用与 podium-call-transcript-pipeline 相同的 PII 模式集进行脱敏。
- 长线程导出内存溢出:朴素地将 4000 条消息全部加载到内存再分块会耗尽堆内存。正确做法是逐条流式写入 JSONL,再单独一遍将 JSONL 流式分块,内存开销保持 O(window-size) 而非 O(thread-size)。
使用方式
前置条件
- Python 3.10+
- podium-auth(假定已有 PodiumAuth 实例,不在此重新实现 OAuth)
- podium-rate-limit-survival(假定调用层遵守各端点配额)
- 持久化 CDC 水位存储——默认 SQLite,随附 cdc_watermark.py
- 本地磁盘用于流式 JSONL 输出,gzip 压缩(典型两年组织:原始约 1–10 GB,压缩后 200 MB–2 GB)
- 附件下载目标目录(S3 桶、GCS 桶或本地路径)
- 来自 podium-call-transcript-pipeline 的 PII 脱敏模式集(复用,不要分叉)
认证
该技能不实现 OAuth。所有 HTTP 调用通过注入的 podium_get() 依赖进行,该依赖持有来自 podium-auth 技能的 PodiumAuth 实例,负责令牌缓存、80%-TTL 刷新、单飞锁和范围验证。通过读取 refresh-token 文件引导,实例化 PodiumAuth(client_id, client_secret, refresh_token),然后通过 --refresh-token-file 和环境变量凭据标志将实例传递给每个导出脚本。该技能的五个脚本从不在进程内构造凭据,而是委托。
步骤
步骤 1:游标分页全量爬取(消除游标漂移) 将排序固定到稳定单调字段(created_at 升序),在加载器中按 id 去重。每成功一页后持久化游标,使崩溃后从最后成功页边界恢复。seen_ids 集合限制在 5 万条,防止数百万行导出时内存无限增长。
步骤 2:带重叠余量水位的增量 CDC(消除边界缺口) 使用 >= 并在加载器中去重;仅在一页完全持久化后推进水位。overlap_margin_s = 60 是有意为之:Podium 的 updated_at 粒度为 1 秒,60 秒重叠可吸收时钟偏移和同秒写入。水位仅在整个遍历成功后推进,而非每页推进。
步骤 3:403 刷新式附件下载(消除预签名 URL 过期) 多小时的批量导出必须在下载返回 403 时按附件 ID 重新获取签名 URL。concurrency=8 是安全默认值。
步骤 4:带语义边界的窗口化线程分块(消除超大线程) 长线程必须在自然边界分块——会话内轮次边界、超过 24 小时的空闲间隔、每块 token 硬上限。分块器流式读取 JSONL 导出,内存中不超过 window_size 条消息。24 小时空闲间隔断点对跨月支持型线程至关重要。
步骤 5:嵌入前 PII 脱敏(消除永久 PII 合规失败) 在块生成时、任何文本离开导出进程前往嵌入 API 之前,运行与 podium-call-transcript-pipeline 相同的脱敏模式集。脱敏不可恢复。对块正文、引用消息中的客户显示名以及块中复现的附件文件名运行脱敏。不要脱敏 message id 或 created_at。
步骤 6:流式 JSONL 输出(消除导出内存溢出) 每行一条记录,gzip 压缩,无内存聚合。分块器逐行读取 JSONL 并输出块;嵌入器逐行读取块并输出向量。每个阶段内存开销为 O(一条记录)。separators=(",", ":") 可节省约 5% 磁盘;每 1000 行 f.flush() 将崩溃数据丢失窗口限制为一个刷新间隔。
错误处理
- 403 Forbidden / SignatureDoesNotMatch(附件):预签名 URL 过期,通过 GET /v4/attachments/:attachment_id 重新获取,重试一次
- 404 Not Found / conversation_not_found:会话在导出中途被删除,跳过并记录 id,不重试
- 409 Conflict / cursor_invalid:上次导出运行的游标不再有效,丢弃游标,从当前水位重启
- 429 Too Many Requests / rate_limited:超出组织配额,遵守 Retry-After,委托给 podium-rate-limit-survival
- 500/502/503 / server_error:Podium 端瞬时错误,带抖动的指数退避,最多 4 次尝试
- 本地 watermark_drift:部分运行中 CDC 水位推进超过 max_seen,通过 cdc_watermark.py --reset 重置到最后一个良好水位
示例
全量历史导出(一次性):
python3 scripts/export_conversations.py \
--location-uid "{your-location-uid}" \
--mode full \
--out ./exports/conversations.jsonl.gz \
--attachments-dir ./exports/attachments
增量夜间同步:
# 通过 cron 每晚运行。成功后水位推进;部分运行会重新拉取。
python3 scripts/export_conversations.py \
--location-uid "{your-location-uid}" \
--mode incremental \
--watermark-db ./watermarks.sqlite \
--overlap-margin-seconds 60 \
--out ./exports/conversations.$(date +%F).jsonl.gz
检查 / 重置 / 推进 CDC 水位:
# 检查
python3 scripts/cdc_watermark.py --resource conversations --db ./watermarks.sqlite
# 重置(从 epoch 重新拉取全部)
python3 scripts/cdc_watermark.py --resource conversations --db ./watermarks.sqlite --reset
# 手动推进(例如通过其他工具手动回填后)
python3 scripts/cdc_watermark.py --resource conversations --db ./watermarks.sqlite --advance 1715212800
适用场景
适用于需要将 Podium 中多年积累的会话、评价和联系人数据批量导出并嵌入向量库以支持相似度检索的场景,尤其是数据量大、需要增量同步、包含附件、线程超长且涉及 PII 合规的组织。
限制
该技能不实现 OAuth,依赖 podium-auth 提供认证实例;不实现速率限制逻辑,依赖 podium-rate-limit-survival;PII 脱敏模式集复用自 podium-call-transcript-pipeline,不应分叉。分块 token 估算使用 OpenAI 经验规则(len//4),生产环境应替换为 tiktoken。附件并发默认 8,仅在观察集成余量后提高。