ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

可溯源RAG实战:让AI客服每句话都有据可查

可溯源RAG实战:让AI客服每句话都有据可查 我接手过一个售后客服系统那会儿线上的AI客服已经在跑但运营团队隔三差五就截一张对话截图扔到群里“这个AI又说瞎话了。”用户问退货时效它答得特别肯定“签收后7天内可以退。”结果售后政策明明写的7天无理由退货、但生鲜类目不支持。你说它错吧政策里确实有“7天”的字眼你说它对吧它把“适用类目”这个限定条件完全丢了。这种一本正经的胡说八道比直接答不上来更麻烦。后来我们把整个链路重做成了可溯源RAG核心就一句话每一条回答都能一路追回到知识库里的某一段原文追不到就不许说。这篇文章想把这段改造的经验完整写出来包括设计思路、代码级的实操、以及踩过的坑适合正在做AI客服、RAG知识库或者被“模型幻觉”折磨得头疼的读者参考。1. 先搞清楚AI客服为什么会“胡说八道”1.1 LLM不是不懂而是容易把“相关”当成“真实”互联网上关于“大模型幻觉”的解释已经很多了但真正上手做客服系统之后我的理解才变得具体。你想想大模型的本质是一个“接着往下说的机器”它根据上文去猜下一个最可能的词每一步都在做概率采样。这套机制用在写诗、写总结上很能打但用在知识问答上有个致命问题它没有“去查证”这个动作。模型在训练时见过“7天内退货”这个片段它就认为这个词串在“退货政策”语境下高度相关于是顺着往下编。它不是不知道生鲜类目不能退而是生成那一刻它根本没在“查”知识而是在“接龙”。生活里最接近的类比是一个人背书背得很熟但从不翻课本。你问他某条规定他凭着记忆里的“大概印象”就答了细节自然容易张冠李戴。所以AI客服胡说八道的第一层原因藏在生成机制里这不是靠换一个更大的模型就能根治的。1.2 传统RAG的瓶颈不只是“检索不到”很多人一听到“AI胡说八道”第一反应是“那给它接上知识库做RAG不就行了”这句话方向没错但只做了一半。我见过不少团队把文档往向量库里一灌接一个检索就往prompt里拼结果上线照样翻车。为什么传统RAG的瓶颈远不止“检索不到”这么简单。我总结下来至少有四类召回不精向量检索是“语义相似”但“语义相似”不等于“答案正确”。用户问“能退吗”知识库里有一段“退换货政策”和一段“退款到账时间”语义上都相关向量返回一堆模型可能抓了错的那一段。上下文碎片化政策文档经常是一整段话里包含适用对象、时限、例外情况。按固定字数切成若干chunk之后每个chunk都丢了上下文比如“生鲜类目除外”落在另一个chunk里模型只看眼前这段自然断章取义。知识冲突与更新滞后同一个问题旧政策说7天新政策说15天两篇文档都在知识库里模型不知道谁优先最后挑了一个“看起来更顺”的答案。检索没被“约束”传统RAG只是把检索结果当参考资料扔给模型模型可以自由发挥引用不引用全看它心情。这四点任何一个不解决AI客服都会继续“一本正经地胡说”。只追求“检索到了”毫无意义关键是“检索到的内容是否完整、准确、并且被强制约束使用”。1.3 客服场景对溯源有刚性需求你可能觉得“让回答引用来源”是个锦上添花的体验功能但在客服场景里它真的是刚需。为什么我列几条实际运营里摆在那里的痛点第一用户信任。客服回答代表官方口径用户会拿去跟平台申诉。如果AI给了一个没有出处的承诺售后和仲裁都会很难办。第二质检需求。客服团队隔三差五要抽检对话质量如果没有“索引到原文”的机制质检员只能凭感觉判断AI回答对错效率极低。第三知识更新闭环。政策总在变怎么知道AI有没有用上最新一版有溯源日志后你可以直接检索“所有引用老版本政策的会话”批量复盘而不是靠用户投诉来发现。所以所谓“可溯源RAG”不是一个锦上添花的角标而是一条从知识库到最终回答都可以回查、复核、修正的技术链路。它就是这套系统的质检证据链。2. 可溯源RAG的整体设计思路2.1 溯源不是回答里加角标而是一条完整链路先纠正一个常见误解很多人以为让模型在回答末尾加一句“以上内容来自XX文档”就是溯源了。但实际上模型自己加的“来源”可能都是编的它连见过哪几个chunk都不一定按真实引用来。我做可溯源RAG时把“溯源”拆成了五个环节缺一不可信源治理定义哪些文档可以进知识库、以哪个版本为准、冲突时怎么裁决知识切块按业务语义边界切分并给每一块打上稳定的身份标识chunk_id、来源、版本检索召回向量加关键词召回再精排确保给到模型的top几条是真正相关的生成约束在prompt里写死引用规约模型必须基于给定片段作答并且只能引用真实存在的chunk_id审计留痕每次问答都记录完整日志回答里引用了什么、检索到了什么随时可以回放。只有这五个环节都做了你才能真正说“每一条回答有据可查”。任何一个环节偷懒最后都会漏回访。2.2 用本体思维治理知识比一上来画知识图谱更现实热词里看到“ontology RAG”我多说两句。很多团队做客服知识库第一步就想着要不要上知识图谱画了一堆实体关系最后维护成本极高还不好用。我的经验是别一上来就上重武器先引入“本体思维”也就是把知识库里的概念、分类、属性、约束关系理清楚。举个例子电商售后里“退货政策”和“商品类目”是两套东西但它们之间有关系退货政策适用于某些类目不适用于另一些类目。如果知识库里只是两篇孤立的文档那么用户问“生鲜能退货吗”检索系统可能只能找到“退货政策”或“商品规格说明”的其中一个片段回答就缺了“类目限定”这个关键信息。这其实就是热词里“知识割裂”的根源文档与文档之间、条款与商品之间缺乏关联。解决方式不是画复杂图谱而是在知识切块时给chunk加上“业务属性标签”和“关联锚点”甚至做成一条条结构化条目让“退货政策-类目限制-商品类型”这几个维度可以被同时检索到。轻量级的做法也很简单给每个chunk的metadata里加上category、适用类目、生效时间、关联规则ID。检索时把分类条件也带进查询这样“生鲜能不能退”就能同时命中“退货政策”中针对生鲜的例外条款。这就是一个小成本的ontology RAG落地方式。2.3 Agentic RAG要用在刀刃上别一开始就做全自动智能体再聊“agentic RAG”。这个词最近很热但我对客服场景的建议是先用固定流程再渐进引入。客服问题里有大量“单跳问题”比如“退货时效多久”“包邮门槛是多少”一次检索就能解决。这些问题根本不需要让模型去“规划调用什么工具”给个复杂的agent架构反而会因为多步推理而放大幻觉风险你更难追踪每一条回答的依据。真正需要agentic RAG的是“多跳问题”比如用户问“我这个型号的耳机参加以旧换新吗”答案需要两步先定位用户商品型号去商品库查出适用类目然后再去活动库查这个类目是否在活动范围内。这种场景下你可以让模型做一个“意图路由”先判断需要几步检索、分别去哪个库然后按顺序执行子任务。但注意即便是agent式多步检索每一步检索的结果也得保留引用最后生成的回答依然要落到具体chunk_id上。Agent只负责调度检索不能负责“自由创作答案”。3. 核心实现把“有据可查”落实到代码和流程3.1 知识切块按业务边界切不按字数切做RAG最先碰到的问题就是切块。很多教程喜欢给你一个text_splitter按固定字符数硬切比如500字一块。这个做法在通用文档上凑合能用在客服政策文档上就是灾难。原因我刚才说了政策条款经常是一整段话包含适用条件、执行细则、例外情况硬切会拆散上下文。我在项目里用的方式是按“业务语义块”切。先人工把政策文档转成结构化条目比如一条政策拆成条款标题退换货时效适用对象普通商品生鲜、定制类目除外细则1签收后7天内可申请无理由退货细则2生鲜类商品不支持无理由退货细则3定制类商品不支持无理由退货每一条作为一个chunkmetadata里带上政策ID、标题、来源URL、版本号、生效日期。这样模型拿到chunk时上下文是完整的不会出现“生鲜除外”和“7天可退”被硬切到两块的问题。我这里给一个比较实用的切块伪代码思路是“按规则分割而不是按字符数分割”import json # 每条政策已经按人工整理好的结构存成 JSON 列表 policy_items [ { policy_id: R001, title: 退换货时效, content: 普通商品签收后7天内可申请无理由退货, scope: 普通商品生鲜、定制类目除外, source_url: https://help.example.com/policy/R001, version: 2025-01, effective_date: 2025-01-01 }, { policy_id: R002, title: 生鲜类目例外, content: 生鲜类商品不支持无理由退货, scope: 生鲜类目, source_url: https://help.example.com/policy/R002, version: 2025-01, effective_date: 2025-01-01 } ] # 每个条目直接作为一个 chunkmetadata 里保留完整溯源信息 chunks [] for idx, item in enumerate(policy_items): chunk_id fpolicy_{item[policy_id]}_{item[version]} chunks.append({ id: chunk_id, text: f{item[title]}{item[content]}。适用范围{item[scope]}。, metadata: { chunk_id: chunk_id, policy_id: item[policy_id], source_url: item[source_url], version: item[version], effective_date: item[effective_date] } }) print(f共生成 {len(chunks)} 个可溯源 chunk)这段代码很简单但关键点是chunk_id里已经绑定了policy_id和version后面模型回答里引用到的任何ID都能直接映射回一条真实政策原文。这一步是整个溯源的地基。3.2 检索链路向量召回、关键词召回、重排三段式有了高质量chunk接下来才是检索环节。我实测下来只靠向量检索是不够的。客服问题经常有精准的专有名词、型号、简称比如“某型号充电器支持PD快充吗”向量检索可能在语义上找到相关文档但关键词检索能直接命中“型号PD”。所以我的检索链路是三条腿走路第一向量检索取top20。用embedding模型把query和chunk都转成向量在向量库里找余弦相似度最高的20条。第二关键词检索BM25/全文检索取top20。适合精确匹配型号、政策编号、商品编码。第三把两组结果合并去重再用一个rerank模型也就是Cross-Encoder对这40条做精排取top3。精排的目的是用query和chunk的完整交叉注意力来判断“到底哪几条真的回答了这个问题”这一步能明显提升命中质量。这里要提醒一个关键点不要贪多。很多人喜欢把10条检索结果全部塞进prompt希望模型自己“选”。实测下来检索结果越多模型越容易被无关片段干扰最后反而编出一个融合了多个片段错误信息的答案。我一般控制在3条以内宁缺毋滥。顺带说一句“RAG hit rate”这个指标。你可以统计top5里有多少比例包含了最终被引用的chunk这能反映“检索是否够准”但单看这个数不够——还要看“检索结果没有噪音”的程度。因为top1的准往往比top5的平均命中率更重要。客服场景里把资源花在提升top1、top3的精准度上比一味把hit rate刷到95%更有价值。3.3 生成约束引用规约写进prompt不引用就不许说检索做得再好如果生成阶段没约束模型照样能自由发挥。我在prompt里用了一套比较强硬的规约把引用变成硬性要求核心几条你只能依据“知识片段”列表中的内容回答。每一条回答必须附上引用ID格式为[引用ID]引用ID必须是片段列表中真实存在的chunk_id。如果片段列表中没有能回答用户问题的信息直接说“该问题暂时无法从现有知识库确认”不要自行推断。禁止拼接多个片段中不存在的细节禁止把片段里的词语重新组合成新事实。如果多个片段内容冲突优先引用版本最新的片段并在回答中说明“这来自最新版政策”。来看一个实际用的prompt片段缩略你是电商客服助手。请基于以下知识片段回答用户问题。 知识片段 fragment idpolicy_R001_2025-01 source... 普通商品签收后7天内可申请无理由退货。适用范围普通商品生鲜、定制类目除外。 /fragment fragment idpolicy_R002_2025-01 source... 生鲜类商品不支持无理由退货。适用范围生鲜类目。 /fragment 回答要求 - 只能使用上述片段中的信息作答。 - 结尾列出你实际引用的片段ID例如引用[policy_R001_2025-01] - 片段中没有的信息不要补充多个片段冲突时以版本最新者为准。有人会问这样约束之后模型会不会变得“死板”确实会更谨慎但客服场景本来就不需要AI像个诗人一样自由创作。它的价值就是“说准确的话”哪怕话术生硬一点也远比“自信地给错误承诺”要好。3.4 前端展示与审计后台让溯源真正闭环回答里带上了引用ID这只是个中间产物还要让它变成用户和运营都能看到的东西。我做的前端效果是回答里的[引用ID]会渲染成可点击的来源卡片用户点开能看到原文标题、来源链接、生效日期和版本。用户看到“7天退货”的承诺旁边就有政策条款原文信任感完全不一样。后台那边我设计了一张“审计日志表”每次会话记录下这些字段字段说明例子request_id一次请求的唯一编号req_8f3a2cquery用户原问生鲜能退货吗retrieved_chunk_ids检索阶段召回的chunk列表policy_R002_2025-01, policy_R001_2025-01final_answer模型最终回答生鲜类商品不支持无理由退货。引用[policy_R002_2025-01]cited_chunk_ids回答中实际引用的IDpolicy_R002_2025-01version知识库版本快照2025-01有了这张表运营遇到任何“AI说错话”的投诉都可以直接拿request_id去查定位是检索没召回正确的chunk还是模型没用检索结果是知识库旧版本没下线还是新政策压根没录入每一步都能复盘这才是“有据可查”的真实含义。4. 实操过程从零搭一个可溯源RAG4.1 选型建议与准备清单先说选型目的是让你不被一堆热词吓倒。可溯源RAG的核心在于流程设计不在于某个特定框架。我的建议是图简单就用LangChain这类生态把向量化、检索、生成串起来想要更可控就自己写几十行代码组件其实就那么几块——一个embedding模型、一个本地向量库、一个LLM接口。模型线上客服我用过API模型也用过本地量化模型。两者差异在于API模型指令遵循能力更强引用规约执行得更稳本地模型成本低、数据不出内网但prompt约束要写得非常死。热词里看到“ollama 简易本地RAG”确实适合零基础先在本地跑通流程。我建议第一版先拿一个指令遵循能力强的API模型做基准验证链路再考虑换成本地模型。embedding模型中文客服场景建议用支持中文的embedding模型比如bge系列等。选型标准是在你们的FAQ与政策文本上跑一批验证集看召回top5里是否包含正确答案。不要迷信排行榜。向量库起步阶段用FAISS这类本地库就够了部署简单数据量大、需要权限控制时再上正式的向量数据库。LLM接口OpenAI兼容格式的接口都可以无痛切换方便你在API模型和本地模型之间做A/B。准备清单其实就三样东西一份整理好的政策/FAQ文档、一个Python环境、一个能跑embedding和LLM的环境。4.2 建索引把条款和FAQ变成可检索的chunk我建议把知识库分成两个库政策库和FAQ库。政策库按上一节说的结构化条目切分FAQ库则按“问-答对”切一个问答对就是一个chunkmetadata里带上问题分类、更新时间。这样每个chunk都是自包含的检索时不会出现“只看到答案一半”的情况。建索引的过程就是读取结构化数据→生成chunk文本→生成embedding向量→写入向量库同时把chunk_id和metadata存入原始数据表。这里的关键是原始数据表要和向量库一一对应不能只把向量丢进去否则后面没法审计。from sentence_transformers import SentenceTransformer import faiss import numpy as np # 用中文embedding模型 model SentenceTransformer(BAAI/bge-large-zh-v1.5) # 假设chunks是上一步生成的结构化片段列表 texts [chunk[text] for chunk in chunks] embeddings model.encode(texts, normalize_embeddingsTrue) # 建FAISS索引 dim embeddings.shape[1] index faiss.IndexFlatIP(dim) index.add(np.asarray(embeddings))这一段代码只是骨架但思路很清晰每个向量都对应一个chunk对象你在向量库的一侧存向量在数据库的一侧存chunk的完整原文与metadata。后面检索得到向量下标后第一件事就是反查出chunk_id而不是直接拿下标去拼prompt。4.3 一次完整问答的溯源响应结构搭好了索引之后我习惯把一次问答的响应做成一个结构化的JSON方便前端渲染和日志存储。大概长这样{ request_id: req_8f3a2c, query: 生鲜能退货吗, retrieved: [ { chunk_id: policy_R002_2025-01, text: 生鲜类商品不支持无理由退货。适用范围生鲜类目。, score: 0.93, source_url: https://help.example.com/policy/R002 }, { chunk_id: policy_R001_2025-01, text: 普通商品签收后7天内可申请无理由退货。适用范围普通商品生鲜、定制类目除外。, score: 0.91, source_url: https://help.example.com/policy/R001 } ], answer: 生鲜类商品不支持无理由退货。引用[policy_R002_2025-01], cited_chunk_ids: [policy_R002_2025-01] }你看这个JSON把“检索到了什么”和“最终引用了什么”分开了。运营在后台上看到的是最终引用的chunk开发在排查时看到的是完整retrieved列表——两者对不上自然就知道是哪一环节出了问题。4.4 用真实场景做一次端到端验证功能做完不能直接上线上我建议至少跑三组验证第一组是准确率测试集。从历史客服工单里挑出100条真实用户问题人工标注好标准答案和对应政策条目跑完看引用chunk与标注的匹配率。第二组是拒答率测试。专门挑50条知识库里没有答案的问题比如“你们公司什么时候上市”看系统是否正确触发“暂时无法确认”而不是强行编个答案。这组测试是很多人忽略的但它在客服场景里特别重要——不会答比乱答安全。第三组是冲突场景测试。故意在知识库里保留一份旧版本政策看系统是否能够按“版本最新优先”规则选择新政策并且把旧版本信息排除出回答。我的经验是第一版跑完准确率往往只有六成上下不用慌。把失败case拉出来逐条看是检索问题还是prompt问题调上两三轮基本能到85%以上。这个调优过程才是真正积累经验的地方。5. 常见问题与排查技巧实录5.1 检索到的chunk明明包含正确答案但AI还是答错这是最典型的“生成阶段翻车”。根因通常是prompt约束不够硬模型在生成时被“自身知识”带跑了。解决顺序先看prompt里是否明确写了“只能使用片段信息”“无信息就拒答”再看温度参数是不是设太高建议客服场景把temperature调到0.1以下。如果还不行把生成的输入输出日志翻出来确认模型是不是真的拿到了那个chunk——有时候retrieved列表里有但代码拼接时漏了。5.2 回答带上了引用ID但点开原文对不上内容这种情况一般是chunk_id写错了或者metadata里的原文和索引里的chunk文本不是同一份。我在项目里踩过这个坑向量库更新后原始数据表忘同步结果检索返回的是新chunk的ID但前端渲染时去旧表查原文自然对不上。所以“有据可查”的前提是“索引、原文、ID”三者必须原子性同步更新时要一起改不能分步操作。5.3 政策更新了AI还在引用老版本这是知识库版本管理问题。解决方案有两个一是检索阶段过滤生效日期把已过期或未生效的chunk直接从候选集里剔除二是把“版本冲突”的处理规则写进prompt明确告诉模型“有多个版本时以最新版为准”。但最保险的还是前者——别让旧版本进候选集。上线时把旧政策chunk标记为“失效”而不是从库里物理删除方便审计历史引用但检索时必须排除。5.4 结构化文档里的表格、图片检索不到客服政策里有大量表格和商品参数图embedding模型处理不了纯图片表格。我的做法是表格转成Markdown或键值对文本再入库图片里的关键信息人工或OCR转成结构化文本否则宁可不要让模型回答相关字段。这里要明白一个原则知识库的准入标准是“可检索、可引用、可版本化”达不到这个标准的原始材料先不要进库否则只会制造新的幻觉。5.5 面向客服场景的快速排查速查表我把踩过的坑整理成一张表遇到问题直接对着查现象优先排查方向建议处理检索命中但回答错误生成prompt约束、temperature强化引用规约温度降到0.1以下回答没带引用IDprompt未强制引用增加“必须列出引用ID”的硬性要求引用与原文对不上索引与原始表同步原子化更新校验chunk_id老版本回答频出检索阶段未过滤失效chunk按生效日期过滤旧版标记失效表格图片答不了原始材料未结构化表格转文本图片OCR后入库检索召回但回答还偏离rerank阶段噪音过多减少topK控制在3条以内说实话这些坑都不是特别“高深”的但它们都是真实上线时最常见的翻车点。工具在不停更新底层这些问题却很有生命力。把基础动作做扎实比追任何新词都管用。最后分享一个我个人的体会可溯源RAG说到底不是给用户看的功能而是给维护者和管理者的一条反馈闭环。以前AI说错话运营只能截图骂街现在拿着request_id追到源头一条错误能准确归类成“知识没录全”“检索没召回”“版本没过期”“模型没守规矩”四类然后逐类修补。这个过程做完你会发现自己对AI客服系统的掌控力比换什么模型都来得实在。如果你也在被“AI胡说八道”折磨别急着上更复杂的架构先把“每一条回答都能追到原始出处”这个地基打牢后续所有优化才有地方落脚。
返回列表