工作台那边转过来一句抱怨:接过来一片空白,不知道前面聊了啥。
那个会话里,用户最后一句是"那还是让人工看看吧",上面孤零零挂着一条提问。中间没有任何东西。可我按会话 id 去查消息表,明明有九条:用户七轮,AI 六轮,加上那句转人工的话术。
消息都在,只是没显示。
这事得倒回写代码的时候。AI 回复是这么处理的:
ai_msg = _maybe_ai_reply(request, session, content, history) if is_offline else None
# 注意:AI 回复不计 user_unread,因为它已经随本次响应直接返回给用户看了;
# 再计一次未读会出现"明明已读却顶着红点"的误报。
这句注释是我写的,当时挺得意。AI 那句话确实当场就随 chat_send 的响应返回给浏览器了,用户看见了,不该算未读——逻辑没毛病。
问题是工作台的会话列表默认按未读筛。AI 轮次不计未读,那些消息就安安静静躺在详情里,列表上一点痕迹都没有。客服上线,看见一个"有 1 条未读"的会话,点进去只看见用户那句提问,自然会以为人刚开口。
更早的时候还有个 sender_type='ai' 的迁移,本意是把机器人和真人区分开,结果前端模板当时只渲染 user 和 staff 两种,第三种直接被 if/else 吞掉了。这个 bug 藏了挺久,因为客服离线时段本来也没人盯着看。
两处加起来,效果是:AI 干了不少活,人工接手时一点都不知道它干过什么。
第一件是把 AI 轮次在会话详情里显出来,给个灰底和「自动回复」小标,跟真人消息分开:
SENDER_LABELS = {
'user': '用户',
'staff': '客服',
'ai': '自动回复',
'system': '系统',
}
第二件是在列表上加来源标记:这个会话有过 AI 参与、一共几轮、最后是不是转人工。客服不用点进去就能判断"这是个简单的,还是已经聊了半天的"。
第三件最有用,也最土:转人工那一刻,把用户最初那句提问单独拎出来置顶。因为客服真正需要的往往不是全过程,是"他到底想解决什么"。聊到第七轮的时候,最初那句早被刷上去了。
这三件都不涉及模型,全是展示层。回头看,前面几篇里我们花了很大力气调提示词、调拒答边界,落到客服工作台上的体验,却卡在一个未读计数和一条 if 上。
有人提过,把 AI 那七轮压成两三句附在会话顶部:"用户 Windows 环境,问客户端安装,前面建议过清缓存和换网络,均未解决,最后要求转人工"。这个东西要是做好了,客服扫一眼就能接上。
卡住的原因有三个,一个比一个现实。
一是压缩本身要再调一次模型,转人工那一刻多一次外网调用,延迟和钱都得算;而且这条链路本来就是因为上游慢才降级的,再挂一个模型上去不太说得通。
二是摘要会丢东西。前面 78 篇里那个"改三个字导致乱报价"的教训还热着,把一个会编东西的模型放在交接环节,它编一句"用户已确认是会员",客服就照着这个前提往下答了。
三是没人验收。客服说"两三句挺好",可到底该保留哪些信息,得他们一条条标出来,这个工作量比写代码大得多。
所以到现在还是置顶最初那句提问,加上全量消息折叠展示。够用,谈不上好。
还有个更别扭的:AI 回复刻意不加 user_unread 这条注释,现在读起来像在给自己开脱。它当时是对的,只是没人想到列表默认按未读筛。一个局部正确的决定,在另一处变成了问题,这类事在客服这块遇到过不止一次。
VidDown 客服工作台那点改动就是为这类抱怨做的。
客服后来自己养成了习惯,接手先往上翻两屏。比我们做的任何东西都管用。