news 2026/10/17 7:27:51

RAG 2.0实战:构建生产级AI原生投诉处理系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
RAG 2.0实战:构建生产级AI原生投诉处理系统

前阵子帮一位做跨境电商的朋友收拾他们那套客户投诉处理流程,几千条工单堆在系统里,人工回复平均要等四五个小时,用户满意度一路往下掉。他们之前也试过让大模型直接接客服,结果更惨——模型一本正经地承诺“您的包裹将在明天送达”,实际上货还在仓库里躺着。这就是典型的裸用大模型:企业的产品知识、售后政策、物流规则都在模型知识边界之外,模型不知道就是不知道,但它不会承认自己不知道。

后来我们把它改成了一套基于RAG 2.0思路的AI原生投诉系统,才算是把这事理顺了。如果你也在做客服、工单、FAQ问答或者企业知识库这类项目,这篇文章就是把我这次落地的完整思路、关键代码、踩坑记录都摊开来讲,从数据准备到检索链路,再到生成与评估,全部复刻出来。别管标题里“秒变架构师”是不是夸张,看完之后你至少能少走三步弯路。

1. 项目定位与整体设计思路

1.1 投诉系统为什么非得用RAG 2.0

先说清楚一个概念:RAG 2.0不是一个官方标准,而是工程界对第一代RAG套路的反思和升级。第一代RAG的做法很质朴——把用户问题拼上检索到的文档片段,一股脑丢给大模型,让它生成答案。这种做法在demo里看着挺像回事,但一上生产就露馅:用户问的是口语化投诉,切回来的知识片段牛头不对马嘴,模型照着片段硬答,答得越流利错得越离谱,而且整个过程没有任何反馈机制,你甚至不知道系统到底在哪一步出了问题。

RAG 2.0的核心变化是:把“匹配文档”变成了“解决问题”。检索之前做查询理解,召回之后做精排,生成之前做上下文精简,生成之后还要做事实校验。整个链路不是线性的“检索→拼装→生成”,而是一个带着多个校验点的闭环。

投诉系统恰好是检验这套思路最好的场景,原因有三。第一,用户表达极度口语化,充斥着错别字、情绪发泄、指代不清,“你们这个东西垃圾得一匹”到底说的是哪个商品、什么问题,全靠系统自己理解。第二,答案质量要求极高,投诉回复一旦出错,等于在用户的雷区上二次引爆。第三,知识来源复杂,产品手册、物流规则、售后政策、历史工单、客服话术全都要进知识库,互相之间还可能冲突。

1.2 系统整体架构与核心模块分工

我先把这套系统的架构图用文字给你画出来,你脑子里有一个总谱之后,后面看代码就不会迷路。

系统分成五层,从下往上依次是:

  • 数据层:收集投诉工单、FAQ、产品资料、物流规则、售后政策。这部分解决“企业知识从哪来”的问题。
  • 索引层:把非结构化文档切成块,做向量化,同时保留原始文本和元数据,建一个“向量索引 + 关键词索引”的双通道底座。
  • 检索层:接收用户投诉,先做查询改写和意图识别,然后并行做向量召回和关键词召回,合并后用重排序模型精排。这部分是RAG 2.0和1.0拉开差距的地方。
  • 生成层:根据重排后的知识片段,动态压缩上下文,再让大模型严格依照知识片段生成回复、投诉分类、处理建议,并附上引用来源。
  • 评估层:记录全链路的trace日志,定期用评测集跑分数,发现问题后回流到索引层或检索层调整。

模块之间是异步解耦的。有人会问,一个投诉系统而已,有必要搞得这么重吗?我的回答是:如果你只求demo,确实没必要;但如果你要面对的是每天几百上千条真实投诉,任何一环掉链子,线上就会立刻给你脸色看。尤其是重排序这一环,省掉的代价是幻觉率翻倍。

2. 数据准备与索引构建实操

2.1 数据治理:把垃圾扔进索引之前先想清楚

投诉系统最容易被忽视的一块,恰恰是数据侧。很多团队拿到一堆PDF和Word就往向量库里塞,塞完发现检索结果一塌糊涂,然后回头怪Embedding模型不行。其实多半是数据本身的问题。

我这次的做法是先把数据分成三大类:

  • 结构化数据:订单表、物流状态、售后记录,这类数据不要走RAG,直接查数据库,结果确定性最强。
  • 半结构化数据:FAQ表单、客服话术、政策条款,可以直接切片后进知识库。
  • 非结构化数据:产品手册、历史工单描述、投诉录音转写文本,这类必须做清洗、切片、元数据标注。

清洗这一步的重点是去噪。投诉工单里有大量“尊敬的客户您好”“请问还有什么可以帮您”这类客服模板文本,还有用户发的表情符号、乱码、重复字符。我的建议是能吃规则就用规则,正则表达式批量处理掉,不要什么都交给大模型,成本高且不稳定。我自己写了一套清洗管线,按顺序做以下几件事:去除HTML标签、去除重复标点、把全角符号转半角、去掉客服固话模板段落、保留时间戳和工单编号等关键信息。

清洗之后还有一个很多人忽略的动作——脱敏。真实投诉里会有手机号、地址、姓名、订单号,直接进向量库等于裸奔。我的习惯是用一个统一的规则引擎做实体识别和替换,手机号中间四位打码,地址只保留市级,人名替换成占位符。这一点在合规上不是可选项,是必选项。

2.2 分块参数怎么定?512字符不是万能答案

分块(chunking)是整个索引构建中最容易翻车的地方。分块太小,语义被切断,检索时匹配不上;分块太大,噪声太多,重排的效率会大幅下降,生成阶段也容易把不相关内容卷进来。网上很多人直接抄一个512字符的参数就跑了,我认为这是把复杂问题过度简化了。

我这次用的策略是“分隔符优先、长度兜底”,用一句话讲就是:能按语义结构切就先按语义结构切,只有在结构不明确时才退回到固定长度。

下面是我实际用的分块函数,核心逻辑是优先按段落切,再按句子切,最后用固定窗口兜底:

import re from typing import List def smart_chunk( text: str, max_len: int = 600, overlap: int = 80, ) -> List[str]: """ 分隔符优先的分块器: 1. 先按段落 \n\n 切 2. 段落太长就按句子(。!?;)切 3. 还是太长就用窗口滑切 4. 相邻块之间保留 overlap 字符,避免切断语义 """ blocks: List[str] = [] # 第一层:按段落切 paragraphs = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()] for para in paragraphs: if len(para) <= max_len: blocks.append(para) continue # 第二层:按句子切 sentences = re.split(r"(?<=[。!?;])", para) buf = "" for sent in sentences: if not sent.strip(): continue # 当前缓冲 + 新句子超长,先把缓冲落库 if len(buf) + len(sent) > max_len: if buf.strip(): blocks.append(buf.strip()) buf = sent else: buf += sent if buf.strip(): blocks.append(buf.strip()) # 重叠处理:给相邻块补上上一块的尾部,提升召回连续性 final_blocks: List[str] = [] for i, blk in enumerate(blocks): if i > 0 and overlap > 0: prefix = blocks[i - 1][-overlap:] blk = prefix + blk final_blocks.append(blk) return final_blocks

这套逻辑在投诉工单上跑下来,效果比固定512字符好不少,检索命中率大概提升了两三个点。一个重要的经验是:overlap不是越大越好,过大的overlap会让相近的块在向量空间里过于相似,导致召回结果大量重复,浪费重排资源,80到100个字符是一个比较稳的区间。

2.3 元数据设计:检索的隐形导航系统

很多人建完索引就忘了元数据这回事,但这恰恰是投诉系统能落地的关键。投诉工单天然带业务属性,比如产品线、投诉渠道、处理状态、紧急程度、发生时间段。把这些字段和文本块一起存进索引,检索时就可以做前置过滤,从源头砍掉一大批不相关的候选。

我这次给每个文本块设计了这么一组元数据字段:

字段说明示例
biz_line业务线跨境物流 / 国内电商 / 售后维修
doc_type文档类型工单 / 政策 / 产品手册 / FAQ
product_id关联产品P10023
source_time内容时间2024-06-01
urgency投诉紧急度high / medium / low
permission权限等级internal / partner / public

这样设计有几个好处。第一,用户投诉里如果提到“跨境”“物流”,检索时可以直接限定biz_line=跨境物流,大幅缩小搜索范围。第二,权限隔离可以做到索引层面,什么角色能看什么数据,在召回环节就挡住,而不是等到生成环节才发现泄露了不该泄露的信息。第三,产品对比类投诉要查特定产品资料时,product_id字段能帮上大忙。

向量化这一层我选的是开源的Embedding模型,针对中文效果不错,上下文窗口是512个token,刚好和我600字符的分块长度匹配。Embedding模型的选择上没有太多的玄学,核心就看两点:中文效果是否够稳、推理延迟是否能扛住线上QPS。至于行业领域的微调,除非你有几十万条标注数据,否则我不建议一上来就做,先跑通用模型拿到基线再说。

3. 检索链路:从暴力搜索到精细化召回

3.1 查询改写:把“你们东西真垃圾”变成检索能懂的句子

投诉文本和普通搜索query有个本质区别:普通搜索query通常是“一词一义”的,用户想查什么就直接打什么;投诉则是裹着一大堆情绪、背景和指代词。比如“我上周下的那个单,到现在还没有任何动静,你们客服也联系不上”——真正要检索的信息其实是“订单物流延迟”和“客服无法联系”,但直接把这整句话丢给向量检索,Embedding模型会把情绪词和事实词混在一起,召回的准确度可想而知。

所以RAG 2.0链路的第一步就是查询改写。我用大模型做改写,给它一个明确的指令:从投诉中抽取出结构化的检索条件。这样用户投诉进去,出来的是一个干净的检索query,而原始文本则被保留在上下文里供生成阶段使用。

QUERY_REWRITE_PROMPT = """你是一个投诉系统的查询理解引擎。 输入是一条用户投诉,请完成以下任务: 1. 识别投诉涉及的【业务域】(如:物流、商品质量、售后、退款) 2. 提取投诉中的【关键事实】(订单号、产品名、问题现象、发生时间) 3. 将投诉改写成2-3条适合搜索的【检索query】,必须去掉情绪词,只保留事实信息 4. 判断投诉的【紧急程度】:high(退款失败/人身安全)、medium(商品故障/发货延迟)、low(咨询/建议) 输出要求:严格输出JSON格式,不要输出任何其他文字。 { "biz_domain": "", "key_facts": [], "queries": [], "urgency": "" } """ def rewrite_query(user_complaint: str, llm_client) -> dict: resp = llm_client.chat( messages=[ {"role": "system", "content": QUERY_REWRITE_PROMPT}, {"role": "user", "content": user_complaint}, ], temperature=0.1, response_format={"type": "json_object"}, ) return json.loads(resp)

这里有个细节必须强调:改写的temperature一定要压低,我设置在0.1左右。查询改写这个任务要的是确定性,不是创造性。温度太高,同一个投诉两次改写出完全不同的query,召回结果不稳定,问题排查的时候会很崩溃。

3.2 混合检索:向量召回和关键词召回谁都不能少

向量召回的优势是语义理解,能处理“同义不同形”的表述,比如“东西坏了”和“产品故障”能匹配上。但向量召回也有两个明显的短板:第一,对专有名词、型号、编号这类精确匹配非常迟钝,你让它匹配一个订单号“SO20240618”,嵌入后的向量和“SO20240618”这个字符串的向量距离可能并不近;第二,冷门词在Embedding模型里被压缩成稠密向量之后,区分度会下降。

所以我把“向量召回”和“BM25关键词召回”做成双通道,然后再合并。这是一种非常经典并且可靠的混合检索方案,尤其在投诉这种“口语化+专有名词密集”的场景,两条腿走路比一条腿稳得多。

BM25用Elasticsearch做最省事,向量检索我用的是独立的向量库。两者并行查询后,分数需要归一化才能放在一起比。我用的方法是给每个来源的分数做min-max缩放,再分别乘上权重,最后合并取top N。

def hybrid_search( rewrite_result: dict, embed_fn, bm25_search_fn, vector_search_fn, top_k: int = 20, alpha: float = 0.6, ): """ alpha: 向量分数权重,0.6表示向量召回相对更重要 """ queries = rewrite_result["queries"] biz_domain = rewrite_result["biz_domain"] mms_pool = {} # 用MMS(Min-Max Scaling)做零基归一化,避免不同检索模式分数不可比 for q in queries: # 通道一:向量召回 vec_hits = vector_search_fn( query=embed_fn(q), top_k=top_k, filters={"biz_domain": biz_domain} if biz_domain else None, ) # 通道二:关键词召回 kw_hits = bm25_search_fn(query=q, top_k=top_k) for hit in vec_hits: _accumulate_hit(mms_pool, hit, source="vector") for hit in kw_hits: _accumulate_hit(mms_pool, hit, source="keyword") # 归一化 + 加权合并 scores = [] for chunk_id, entries in mms_pool.items(): vec_scores = [e["raw_score"] for e in entries if e["source"] == "vector"] kw_scores = [e["raw_score"] for e in entries if e["source"] == "keyword"] combined = 0.0 if vec_scores: # 取向量通道内的最大值再归一化 v_max = max(vec_scores) combined += alpha * (v_max - min_v) / (max_v - min_v) if kw_scores: k_max = max(kw_scores) combined += (1 - alpha) * (k_max - min_k) / (max_k - min_k) scores.append((chunk_id, combined)) scores.sort(key=lambda x: x[1], reverse=True) return [chunk_id for chunk_id, _ in scores[:top_k]]

这段代码里min_v、max_v、min_k、max_k我简写了,实际运行时要先扫一遍池子里的分数拿到全局最小最大值,再回来归一化。alpha这个权重需要根据业务调:如果你的投诉里大量出现订单号、型号这类精确信息,alpha可以调到0.4让关键词通道权重更高;如果投诉以描述性语义为主,alpha调到0.6甚至0.7更合理。我上线前会拿一批验证集扫描alpha从0.3到0.7的效果,然后选最优值。

3.3 精排(Rerank):召回管数量,精排管质量

召回阶段我会放行top 20到top 30个候选片段,但生成阶段不可能把这些全部塞给大模型,所以中间必须有一道精排,把最有可能真正解决用户问题的3到5个片段挑出来。

召回阶段用的是双塔结构的Embedding模型,query和document各自编码成向量,用向量距离衡量相关性。这种结构为了性能牺牲了一些精度,它本质上是“粗筛”。而精排阶段我用的是Cross-Encoder结构的重排模型,query和每个document是拼在一起过模型的,交互充分,精度明显更高,代价是慢。

如果你没听说过这两个结构的区别,用一个比喻就通了:双塔就是两个人在走廊两头各写了一张纸条,然后拿到中间比对;跨编码器是两个人坐在一张桌子上面对面聊。后者理解得更深,但一次只能聊一个人。

我在代码里用重排模型的方式很直接:

def rerank(query: str, candidate_chunks: list, rerank_fn, top_n: int = 5): pairs = [(query, chunk["content"]) for chunk in candidate_chunks] scores = rerank_fn(pairs) # 重排模型返回每组的相似度分数 scored = sorted( zip(candidate_chunks, scores), key=lambda x: x[1], reverse=True, ) return [chunk for chunk, _ in scored[:top_n]]

注意重排时的query不能再拿原始投诉文本,而是用改写后的关键query,最好是把改写出来的多条query拼成一段话再拿去重排,这样重排模型能看到意图侧的重点。我在实际跑的时候发现,直接用原始投诉去重排,效果比分词后的query重排差不少,因为情绪词极大地干扰了重排模型的判断。

精排的top_n取值也不是越大越好。我试过top_n=3、5、8三档,结论是top_n=5在召回率和生成质量之间最平衡。取8的话,生成阶段上下文会臃肿,模型容易被边缘片段带偏;取3的话,偶尔会漏掉关键支撑信息,模型只能硬着头皮答,幻觉率上升。

4. 生成层与可信应答:RAG 2.0的关键一跃

4.1 上下文精化:别把重排结果一股脑塞给大模型

把重排后的5个片段直接拼进提示词,这件事看起来没什么问题,但我实际观察下来,大模型在长上下文里会“注意力稀释”——排在后面的关键信息经常被忽略,排在前面的次要信息反而主导了生成。这个现象在长上下文中非常明显。

RAG 2.0的解法是增加一层上下文精化。在把重排结果交给生成模型之前,我先做一次片段级压缩:每一段只保留和用户问题最相关的2-3句话,去掉那些“尊敬的客户”“感谢您的反馈”之类的套话。压缩的活儿我倾向于用大模型做,但显式要求它原样保留关键事实,不得改写,只做删减。

CONTEXT_COMPRESS_PROMPT = """给定用户问题和候选资料片段,请从每个片段中压缩出与问题最相关的2-3句话。 要求: - 保留实体、数字、日期、政策条款原文,不得改写 - 删除客套语、背景介绍、无信息量的句子 - 输出JSON:{"compressed": ["片段1压缩结果", "片段2压缩结果"]} """ def compress_context(user_question: str, chunks: list, llm_client) -> list: joined = "\n\n".join( f"[片段{i+1}] {chunk['content']}" for i, chunk in enumerate(chunks) ) resp = llm_client.chat( messages=[ {"role": "system", "content": CONTEXT_COMPRESS_PROMPT}, {"role": "user", "content": f"问题:{user_question}\n\n资料片段:\n{joined}"}, ], temperature=0.0, response_format={"type": "json_object"}, ) return json.loads(resp)["compressed"]

这一轮压缩看似多调了一次大模型,增加了延迟,但效果非常明显。我在离线评测集上对比过,压缩后的faithfulness分数比直接拼接高了接近五个百分点。如果你对延迟极其敏感,可以考虑用固定规则替代这步——比如按句号切句、只保留包含关键词的句子——但效果会打折,属于性能换延迟的妥协方案。

4.2 生成提示词:输出格式和引用溯源一起定

生成阶段是整个RAG系统对外的脸面,我把它拆成了两小步:先生成结构化回复框架,再填充成完整回复文本。回复框架包括投诉分类、紧急等级、给用户的回复话术、需要人工介入的标记、以及支撑这份话术的知识引用。把结构化输出固定下来,后续再做质量评估和人工复核都会方便很多。

GEN_PROMPT = """你是某企业的智能投诉处理助手。请根据【参考材料】回复用户投诉。 硬性要求: 1. 只能依据参考材料中的信息作答,严禁编造不存在的政策或事实 2. 回复要覆盖用户的核心诉求,给出明确的解决方案或下一步指引 3. 如果参考材料不足以回答,必须明说“当前资料无法确认”,并引导转人工 4. 输出格式严格为JSON: { "category": "物流延迟/商品质量/退款售后/其他", "urgency": "high/medium/low", "reply": "给用户的完整回复话术", "need_human": true/false, "references": ["片段1", "片段2"] } 参考材料: {compressed_context} 用户投诉: {user_complaint} """ def generate_answer(user_complaint: str, compressed_context: list, llm_client): resp = llm_client.chat( messages=[ {"role": "system", "content": GEN_PROMPT.format( compressed_context="\n\n".join(compressed_context), user_complaint=user_complaint, )}, ], temperature=0.2, response_format={"type": "json_object"}, ) return json.loads(resp)

生成温度我控制在0.2左右。投诉回复这种场景,创造性和温度成反比,温度越低越安全。但也不能设到0,因为纯0会让模型倾向于输出最常见、最保守的句子模板,反而显得像机器人。

还有一个细节:references字段必须映射回原始文档ID,而不是只填压缩后的片段文本。这样当用户对回复有异议时,运营人员可以一键定位到原始知识来源,这一点在很多企业里直接决定了AI系统的可落地性。

4.3 自我校验:让模型检查自己有没有胡说

RAG 2.0和1.0最明显的一个区别,是在生成之后多了一道校验关。大模型的幻觉问题无法根除,但可以在出口处加一道闸门把它拦住。我在生成阶段之后加了一个轻量级校验步骤:把用户问题和生成回复一起发回模型,让它扮演“事实核查员”,逐条检查回复中的每一个事实是否都能在参考材料中找到依据。

这个校验器不需要很强,我用的是同一个模型,只是换了一个system提示词。它输出一个JSON,里面包含“claim_list”和“verdict”。如果判定为“unsupported”,整条回复就不直接推给用户,而是降级走人工。

VERIFY_PROMPT = """你是一名事实核查员。请逐条检查【回复内容】中的每一个事实断言是否能在【参考材料】中找到依据。 输出JSON: { "claim_list": ["回复中的事实1", "事实2"], "supported": [true, false], "overall": "supported" / "unsupported" / "partially_supported" } 参考材料: {compressed_context} 回复内容: {generated_reply} """ def verify_answer(reply: dict, compressed_context: list, llm_client) -> str: resp = llm_client.chat( messages=[ {"role": "system", "content": VERIFY_PROMPT.format( compressed_context="\n\n".join(compressed_context), generated_reply=reply["reply"], )}, ], temperature=0.0, response_format={"type": "json_object"}, ) result = json.loads(resp) return result["overall"]

整体状态是“unsupported”或者“partially_supported”时,我的策略是这样:如果need_human本来就是true,那就照常转人工,不影响流程;如果need_human是false但校验不过,改成转人工并附上AI草稿,让人工快速编辑后发出。这套兜底逻辑上线之后,严重不实回复的比例降到了千分之几的水平,比我预期好很多。

5. 系统评估与线上调优闭环

5.1 评估体系:检索质量、生成质量、业务质量三层拆解

很多做RAG的人上来就只看“回答得对不对”,这是个大坑。RAG系统的质量问题会发生在任何一层——可能是文档压根没被检索出来,也可能是检索出来了但生成的时候没用上,还可能是生成了但和用户问的驴唇不对马嘴。不拆层评估,出了问题你根本不知道该调哪一环。

我搭的评估体系分成三层:

第一层是检索质量。用到的指标包括Recall@k(前k个结果里有多少是相关片段)、MRR(第一个相关片段排多靠前)、Precision@k(前k个结果里相关片段占比)。评估方式是准备一批“用户投诉→应命中的知识片段ID”的标注对,跑一遍检索链路,看指标。

第二层是生成质量。核心指标是faithfulness(生成内容是否忠实于参考材料)和answer_relevancy(生成内容是否命中用户问题)。这两个指标的判定我建议用大模型做裁判,但要用多模型交叉投票,避免单一模型的自带偏好。

第三层是业务质量。这个得靠业务侧定义,比如投诉一次解决率、人工转接率、用户满意度评分、平均处理时长。业务指标在在线阶段才能统计,但必须在一开始就埋好数据埋点。

我整理了一张评估对照表,上线前每个版本都会跑一遍:

层级指标评测方式调优方向
检索质量Recall@5 / MRR标注集离线跑分块、Embedding、混合检索权重、Rerank
生成质量Faithfulness / RelevancyLLM裁判+人工抽检上下文压缩、提示词、温度
业务质量解决率 / 转人工率 / 满意度线上埋点统计知识更新、兜底策略

5.2 评测集构建与线上监控

评测集的质量决定了你调优的方向对不对。我建评测集的方法是:从历史工单里按业务域分层抽样200条真实投诉,每一条手工标注“标准答案要点”和“应命中的知识片段”。做标注的时候要注意覆盖边界case,比如“投诉里包含多个问题”“用户情绪激烈但诉求模糊”“投诉涉及多个产品线”。这些边界case在真实流量里占比不高,恰恰是系统最容易翻车的地方。

线上监控我盯三个东西:检索链条的trace日志、生成结果的校验通过率、以及转人工率的变化。trace日志里记录每一次查询改写的结果、各通道召回的候选数、重排后的排序、最终用了哪几个片段。这样一旦线上有人投诉“AI答得不对”,我可以顺着trace一步步回放,定位问题出在哪个环节。

很多人问RAG项目怎么迭代。我的节奏是每周跑一次离线评测,每两周根据评测结果做一次版本更新。更新内容可能是调整混合检索的alpha权重、改压缩提示词、或者新增一批知识文档。版本更新不追求大改,追求小步快跑,效果验证清楚再放量。

6. 落地避坑指南与常见问题速查

6.1 成本与延迟的取舍

RAG链路比裸调大模型多出了查询改写、混合检索、重排、上下文压缩、自我校验五个环节,每个环节都在加延迟。我线上实测的p95延迟数据是:

环节平均耗时
查询改写(LLM)400ms
混合检索(向量+BM25)250ms
重排(Cross-Encoder)180ms
上下文压缩(LLM)500ms
生成(LLM)900ms
合计约2.3s

如果业务对延迟有硬性要求,砍步骤时要按影响程度排序:先砍上下文压缩(用规则代替),再砍查询改写(改用规则抽取),最后才能砍自我校验。向量检索、BM25、重排这三样在投诉场景下基本是底线配置,砍哪个都会明显掉效果。

成本上,LLM调用次数是大头。一次完整投诉处理调用大模型至少四次:改写一次、压缩一次、生成一次、校验一次。想要省钱,可以在低峰时段对高频问题做缓存,把重复投诉的回复直接命中缓存,省掉整条链路。

6.2 敏感数据隔离与权限控制

投诉系统涉及用户隐私和企业内部政策,敏感数据隔离是我从一开始就死盯的点。我的做法有三层:第一层,数据入库前脱敏;第二层,索引里的文档带permission标签,检索时根据调用方身份过滤,做到不同角色的知识可见范围不同;第三层,日志系统里对原始投诉文本做掩码,避免员工在处理工单时看到无关敏感信息。

这里特别提醒一句:千万不要把脱敏后的数据和原始数据混在一个知识库里。如果业务上必须同时支持“AI用脱敏数据”和“人工看原始数据”,宁可做两套存储,一套给检索用,一套给工单系统用,也不要图省事混在一起。

6.3 高频故障与排查速查表

最后把我这次踩过的坑整理成一张速查表,遇到问题可以直接对着查:

现象可能原因排查方向
检索结果全是废话分块过大/过小,或向量召回权重过高检查分块长度、查trace里召回的片段、调低alpha
答案看起来顺畅但事实错漏生成阶段没参考材料或上下文被截断看重排后的片段、查引用ID是否为空
用户问A,系统答B查询改写把原始意图改偏了打印改写后的query,人工对比一致性
同一个问题,两次回答不一致改写或生成的temperature过高把temperature降到0.2以下
答案频繁触发转人工兜底知识库覆盖不足,或校验器过于严格扩充知识库,分析校验器误判样本
专有名词/型号匹配不到向量召回对精确词失效提高BM25权重,或给专有名词建别名映射
上下文片段秩次靠后但很重要重排模型没识别出关键信息给重要知识片段加元数据标记,重排前做偏置

这些坑大多数不是单个环节的问题,而是链路各环节叠加放大的结果。所以排查时我强烈建议你先把全链路trace日志打出来,从检索到生成一帧一帧看,定位到具体坏在哪一环再动手改,切忌凭感觉乱调参数。

我个人的经验是,做RAG系统最大的瓶颈往往不是模型能力,而是数据质量和链路设计的细致程度。同一个模型,数据治理做得好的团队能跑出90分的成绩,数据随便糊弄的团队只能跑出60分。如果你现在正准备上手一个知识库问答或投诉处理项目,建议先从数据清洗和索引构建做起,把地基打扎实了再谈模型和算法。这套RAG 2.0的打法,后续完全可以扩展到质检工单分析、智能售后助手、法律法规问答这些相似场景,核心链路都是通的,只是换数据和换提示词的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 7:27:09

建筑生转AI产品经理:两周上岸实战指南

1. 转行这件事&#xff0c;先想清楚再动手前两天有个学建筑的学弟找我聊&#xff0c;说看到网上有人“0基础建筑生2周上岸AI产品经理”&#xff0c;问我是不是真的。我听完笑了笑&#xff0c;告诉他&#xff1a;这事儿是真的&#xff0c;但“0基础”和“2周上岸”这几个字&…

作者头像 李华
网站建设 2026/10/17 7:25:32

Qoder本地AI编程工作台:重构IDE开发范式的核心原理与实战

1. 项目概述&#xff1a;这不是“平替”&#xff0c;而是重新定义本地AI编程工作流Qoder 这个名字最近在开发者圈子里冒得很快&#xff0c;尤其当大家搜“Codex 平替”时&#xff0c;它几乎必然出现在前三条结果里。但说实话&#xff0c;我第一次看到这个标题时心里是皱眉的——…

作者头像 李华
网站建设 2026/10/17 7:25:26

石头科技AWE2026全场景清洁矩阵,智能清洁技术升级解析

如果你关注家电和智能清洁这个赛道&#xff0c;那今年这份从AWE2026展场传出来的信号&#xff0c;确实值得坐下来认真聊一聊。石头科技这次把扫地机器人、洗地机和商用清洁三大品类的新品放到同一场展会里集中亮相&#xff0c;整体看下来&#xff0c;不只是“发了几台新机器”这…

作者头像 李华
网站建设 2026/10/17 7:24:10

用 VS Code 开发 Spring Boot 实战指南:环境配置、热重载与调试技巧

这两年我写 Spring Boot 的方式&#xff0c;从商业 IDE 换成了 VS Code&#xff0c;而且越用越顺手。刚开始纯粹觉得它轻&#xff0c;打开快&#xff0c;后来把 Java 扩展包和 Spring Boot 扩展包配齐之后发现&#xff0c;日常写接口、调依赖、断点调试、跑测试&#xff0c;这一…

作者头像 李华
网站建设 2026/10/17 7:23:54

OpenCode 终端优先 AI 编程代理:安装配置、使用技巧与避坑指南

1. 为什么终端优先的AI编程代理值得关注第一次看到 OpenCode 这个项目的时候&#xff0c;我正处在对 IDE 插件式 AI 助手审美疲劳的阶段。Cursor、Copilot、Windsurf 这些工具确实好用&#xff0c;但它们都有一个共同点——你必须待在某个特定的编辑器里&#xff0c;接受它给你…

作者头像 李华
网站建设 2026/10/17 7:23:37

从YOLOv5到YOLOv10:农作物病虫害识别系统完整实现指南

1. 为什么总有人把YOLO病虫害识别做成“PPT项目”&#xff1f;我先说个现象。很多开发者一看到“农作物病虫害识别”这个题目&#xff0c;第一反应就是去网上下一个公开数据集&#xff0c;跑通YOLOv5训练脚本&#xff0c;loss降到0.05&#xff0c;mAP打印出来50多&#xff0c;然…

作者头像 李华