问题是客服主管在群里问的:九月中旬有个用户投诉,说自动回复给了一个不对的说法,想看看当时到底回了什么。
我说我查一下,然后打开日志,翻了十分钟,脸色不太好看。
那会儿 AI 调用只记两种东西:成功、失败。logger.info('AI 回复生成成功') 一行,异常时一行 logger.exception。回复正文一个字都没留。用户原话存在 ChatMessage 表里,AI 那句回复是随 chat_send 的响应直接返回给浏览器的,服务端只在内存里过了一遍就没了。
也就是说,除了那个用户的浏览器,世界上没有第二份。
补存储之前先吵了一架
第一反应是"把回复存下来"。真要落库,马上冒出四个问题,每个都不太好答。
存原文还是脱敏后的?原文对复盘最有用,但用户可能贴过手机号、兑换码;脱敏后的看不出当时到底说了什么,等于白存。
存多久?没人能给个准数。客服说投诉追溯一般一两个月,法务那边没给过明确口径,我最后拍了个七天,理由很朴素:先别留太多。
谁能看?回复里可能带用户信息,总不能整个后台都能翻。可我们当时没有细粒度权限,只有"是不是 staff"这一档。
要不要进备份?进了备份,"删除"就不再是真删除,七天保留期形同虚设。不进备份,数据库一恢复,日志整段消失。
| 要定的事 | 当时的选择 | 剩下的别扭 |
|---|---|---|
| 存原文还是脱敏 | 脱敏后截断 200 字 | 复盘时看不出用户原话 |
| 保留多久 | 七天 | 三个月前的投诉查不到 |
| 谁能看 | staff 一档 | 没有更细的权限可分 |
| 是否进备份 | 不进 | 恢复数据库会整段消失 |
吵到最后定的是一套很将就的方案:
AIAudit.objects.create(
session_id=session.id,
prompt_version=SYSTEM_PROMPT_VERSION,
model=settings.AI_CHAT_MODEL,
reply=masked_text[:200],
usage_prompt=usage.prompt_tokens,
usage_completion=usage.completion_tokens,
verdict=verdict, # answer / handoff / failed
created_at=timezone.now(),
)
脱敏后截断 200 字,按会话 id 索引,保留七天,只有 staff 能看,不进常规备份。七天一到,定时任务批量删。
后来发现最有用的其实不是 reply,是那几个元数据:提示词版本、模型名、token 数、判定类别。主管问"当时为什么这么答",看 prompt_version 和 verdict 比看那 200 字更快定位。文本反而是最容易看走眼的部分。
三个月前那条
当然还是查不到。补存储是十月的事,九月的记录早就不存在了。
最后靠客服自己手机里的聊天截图答的——她当时顺手截了一张发群里吐槽,比我们整套日志都全。这件事我记了挺久:一个花了几周设计的审计方案,被一张截图比下去了。
补完之后倒是赶上过一次用场。十一月有个用户说 AI 让他"清缓存重试",我们按会话 id 查到那条记录,看见 verdict=answer、prompt_version=v3,再对照那版提示词,发现"清缓存"是模型自己编的,提示词里从没写过。要是还只有成功失败两行日志,这事只能不了了之。
对不上的三处
现在数据在三个地方:客服工作台的会话记录、AI 审计表、服务商账单。三边各有各的主键,靠时间勉强能对上,真要追一条完整链路得手工拼。
有次想统计"转人工的会话里有多少最后真被人工回了",分别在两个表里查,数字差了十几个。查了半天发现一边按会话创建时间、一边按审计记录时间,跨天的会话就被算到了不同日期。不是数据错,是口径没统一。
定时任务那个七日清理也出过状况。有回任务卡了两天没跑,表里攒了九天的量,磁盘倒没什么,主要是"我们说保留七天"这句话就不成立了。后来加了个清理延迟告警,才算能自圆其说。
真要做得像样,得给每次 AI 调用发一个贯穿三处的 trace id,工作台、审计、日志全带上。这个改动牵的地方不少,一直排在后面。
VidDown 技术博客里日志相关的还有几篇,口径各有各的乱。
那张九月截图现在还挂在那条群消息里,没人删。我偶尔点开看一眼,主要看当时自己回的那句"我查一下"。