既然不想带情绪,我就用 RAG 做了一个永远准时的 AI 客服。今天下午客户那边又炸了,一个售后问题扯皮了三个小时,最后发现是对方自己没看更新日志。我坐在电脑前,血压有点高,脑子里就一个念头:这种重复的、基于文档的问答,能不能让机器来,而且必须比人准,比人快,还不能有情绪。
直接上 RAG。现在这玩意儿已经不是实验室概念了,是能直接上生产线的工具。我的目标很明确:把产品文档、更新日志、常见QA、甚至历史工单里的有效解决方案,全部灌进去,做成一个24小时在线的智能知识库。难点从来不是调用个 API 那么简单,是怎么让 AI 在浩如烟海的文本里,精准地找到那一两句话,并且用人类能听懂的方式组织出来。
首先就是分块策略。你不能把一整本 PDF 扔进去让大模型自己读,那召回率跟抽奖差不多。我试过按固定字符数切,结果一个问题答案刚好被切成两半,AI 返回的东西前言不搭后语。后来改用语义分块,结合标点、段落和自然停顿,确保每个“块”在语义上是相对完整的。比如一个功能的使用步骤,必须完整地放在同一个块里。这里用了 LangChain 的 RecursiveCharacterTextSplitter,但参数调了很久,chunk_size 和 chunk_overlap 的平衡是个细活儿,overlap 太小容易断掉上下文,太大又引入冗余噪音。
索引和检索是关键。我放弃了简单的余弦相似度,上了 ColBERT 或者类似的双编码器模型。为什么?因为用户的问题和文档的表述往往不是字面匹配的。“怎么付不了款”和“支付接口报错”在字面上相似度可能不高,但语义上就是一回事。传统的 BM25 在这种场景下会漏掉很多关键信息。我用的是 Sentence Transformers 生成的嵌入向量,建了个 FAISS 索引,速度快,内存也吃得消。但这里有个坑,FAQ 里有很多“如果…那么…”的假设性长句,嵌入效果并不好,后来我不得不对这部分内容做了预处理,把长句拆解成“条件-结果”的短句对,再分别嵌入,检索时用最大池化来合并分数。
最麻烦的是上下文窗口和提示工程。检索出来 top_k 个文档块,不是简单拼接起来扔给 LLM 就完事了。我设计了一个多阶段过滤:先用一个轻量级的分类模型判断用户意图是“操作指导”、“故障排查”还是“政策咨询”,根据意图对不同来源的文档块赋予不同权重。比如故障排查,就更看重历史工单和更新日志;政策咨询,就必须严格锁定在官方公告范围内。然后,在构造给 GPT-4 的最终提示时,我用了严格的指令:“你是一个客服助手,必须且只能依据以下提供的参考信息来回答问题。如果信息不足,请明确告知用户需要联系人工客服,禁止编造信息。” 同时,把检索到的片段按相关性从高到低排列,并在每个片段前标注来源和置信度分数,让模型自己决定采信哪些部分。
测试阶段更磨人。我模拟了各种刁钻问法、错别字、口语化表达。比如用户说“你们那个扫码的东西坏了”,系统得能映射到“二维码支付功能故障”。我写了一大堆测试用例,用自动化脚本跑,看召回率和准确率。中间发现一个严重问题:当文档中存在多个相似但略有冲突的解决方案时(比如不同版本的文档),AI 有时会混合在一起给出一个矛盾的答案。后来我加了版本元数据过滤,在检索时就限定只检索符合当前用户版本号的文档块,这才算解决。
现在这东西跑起来了。API 调用,毫秒级响应,答案永远基于最新的文档,不会因为今天被客户骂了十次就语气不耐烦,也不会因为凌晨三点而反应迟钝。它没有情绪,只有逻辑和准确率。这让我想起前几年带团队做交付的时候,最头疼的就是人的状态不稳定,同样的知识,不同客服回答能出三个版本。现在,我把最核心、最标准的知识固化成了代码和向量,人可以去处理更复杂、更需要共情的异常情况。
或许,这就是技术带来的某种“公平”。它不完美,会犯错,但它的错误是可追溯、可优化的,而不是一团模糊的情绪迷雾。我关掉测试界面,后台显示它已经自动处理了今天 47% 的初级咨询。窗外天色暗了,但我感觉,有些东西终于清晰了起来。














