当前时间: 2026-06-21 19:07:21
分类:办公文件
评论(0)
【小庄的AI踩坑笔记06】RAG 教做人架构三原则推行之后,团队的 AI 成本降下来了。但新的问题出现了。客服系统接入 LLM 之后,用户问:"你们的智能手表怎么连接 WiFi?" LLM 回答:"请在手表设置中找到网络选项,选择 WiFi 并输入密码。" 回答听起来很专业。但实际上——他们家的智能手表根本不支持 WiFi 连接。LLM 用自己的训练数据回答了问题,而不是用产品手册。小庄做了两件事:第一,在 prompt 里加了 2000 字的产品知识库摘要。第二,把模型从 7B 升级到了 72B。两个都试了。两个都没用。原因不复杂。2000 字的知识塞进 prompt,模型看到中间就忘了开头。LLM 有个通病叫 "lost in the middle"——夹在 prompt 中间的信息最容易被忽略。升级模型就更没用了,72B 的模型一样会编。小庄意识到,问题不在模型。LLM 的上下文窗口不是数据库,你不能把一堆资料全塞进去指望它自己找。上下文窗口更像人的工作记忆——你给它看 50 页产品手册,它只记得住开头和结尾,中间全忘了。于是小庄想到了RAG(Retrieval-Augmented Generation,检索增强生成):用户问什么,先去知识库里找到相关内容,再把找到的内容喂给 LLM 回答。简单说就是三步:先把文档拆成小块,再把每块转成向量存起来,最后用户提问的时候,拿问题去向量库里找最相关的内容,找到之后塞进 prompt 让 LLM 回答。这是 RAG 最关键的一步。拆得不好,后面全废。小庄试了四种策略:每 500 字一刀。最简单,问题也最明显——一句话可能被拦腰切断,语义不完整。比如"请在设置中找到网络选项"这句话,前半段在 chunk A,后半段在 chunk B,检索的时候两边各缺一半,意思就不对了。遇到空行就切一刀。比固定字数好,但仍是"一刀切"。一个长段落可能包含多个主题,强行塞进一个 chunk 也不合适。策略三:语义切分(Semantic Splitting)先把文档按句子拆开,然后把相邻的句子转成向量算余弦相似度。相似度骤降的地方就是切分点——说明话题变了。这种方法语义最完整,但计算量巨大:10 万字文档要调几千次 embedding 模型,成本高到不现实。小庄最终选了这个。产品手册是 Markdown 写的,有 H1、H2、H3 标题。按标题层级切分,每个 chunk 自然就是一个完整的小节。再给每个 chunk 贴标签:来自哪篇文档、哪个章节、什么时候更新的。检索的时候可以用标签过滤,比如只搜最新版本、只搜某个产品线。关键细节:chunk 之间需要保留 10%-20% 的 overlap(重叠)。比如每个 chunk 500 字,和前一个 chunk 重叠 50-100 字。这样边界信息不会丢失,否则一句完整的句子刚好被两刀切断,检索的时候两边各缺一半。效果比固定切分好了一大截,几乎不增加计算成本。因为文档本身就有结构,你只是利用它而已。文档拆好之后,每个 chunk 转成一个 1536 维的向量。语义相似的内容,坐标位置就靠近。这就是 Embedding 的核心思想。但纯向量检索有致命弱点:只看语义相似,不认精确关键词。用户问"WiFi",可能召回"蓝牙"的内容——都是网络相关,坐标很近,但用户要的不是蓝牙。把问题转成向量,去库里找余弦相似度最高的 chunk。适合找"意思相近但用词不同"的内容。比如用户问"连不上网",能召回"WiFi 连接失败"的文档。BM25 的核心逻辑:一个词在文档中出现次数越多越相关——但不能无限叠加,到一定次数后权重不再增长(词频饱和)。同时,如果一个词在所有文档里都出现(比如"的"、"是"),权重会被 IDF(逆文档频率)压低。你搜 "WiFi",它就找出现 "WiFi" 最多的那些 chunk。合并排名:RRF(Reciprocal Rank Fusion)两路检索各召回 20 条,加起来 30-35 条。但两套分数完全不一样,怎么合并?RRF 公式:RRF 分数 = 1 / (k + 排名),k 通常取 60。它只看排名不看原始分数。比如 chunk A 在向量检索排第 2,BM25 排第 8,RRF = 1/62 + 1/68 = 0.0308。按这个重新排序就行。好处是不用归一化两套分数,天然支持任意检索源合并。混合检索得到 30 来个候选 chunk 后,还需要更精细的排序。小庄引入了两阶段排序:Embedding 模型叫 Bi-encoder(双编码器):问题和 chunk 各自独立编码成向量,算余弦距离。速度极快,一次推理几毫秒,适合从几千个 chunk 里海选出几十个。缺点:问题和 chunk 的 token 之间没有交互,模型不知道"WiFi 密码忘了怎么办"里的"忘了"和 chunk 里的"重置"是对应的。只能算粗略的语义距离。阶段二:精排(Cross-encoder / Reranker)Reranker 叫 Cross-encoder(交叉编码器):把问题和 chunk 拼接在一起(格式 "[CLS] 问题 [SEP] chunk [SEP]"),两者的 token 在 Transformer 每一层注意力机制里交叉计算,逐词分析相关性,给出精确分数。比方:Bi-encoder 是先各自打分再配对,Cross-encoder 是面对面面试。前者效率高适合海选,后者精度高适合决赛。代价是慢 100 倍——每次推理几百毫秒。所以只能对 30 个候选精排,不能对 2000 个全做。这就是为什么需要先有粗筛。加了 Reranker 之后,检索准确率从勉强及格直接飙到了优秀水平。而之前花大力气升级 Embedding 模型,只提升了几个百分点。结论:精排的投入产出比远高于升级粗筛模型。优化 RAG 最难的不是技术,是怎么判断好不好。小庄引入了四个维度:维度一:Context Precision(检索精度)召回的 chunk 里面有多少是真正相关的?目标 80% 以上。衡量"命中率"——你给 LLM 的材料里,多少是有用的。维度二:Context Recall(检索召回率)正确答案需要的信息,有多少在检索结果里?目标 75% 以上。衡量"有没有漏掉关键信息"——答案需要的知识点不在检索结果里,LLM 再聪明也答不对。维度三:Answer Faithfulness(回答忠实度)回答里的每一句话,都能在检索内容里找到依据吗?目标 90% 以上。衡量"LLM 有没有编"——回答里出现了检索内容里没有的信息,就是幻觉。维度四:Answer Relevance(回答相关度)回答有没有直接解决用户的问题?目标 85% 以上。衡量"答非所问"——有时候 LLM 给出的内容忠实于检索结果,但和问题不匹配。这四个指标怎么算?小庄用了 RAGAS 开源框架。核心思路是用 LLM 当裁判:给 LLM 一个问题、标准答案和检索结果,让它判断检索内容是否相关、回答是否忠实。当然,当裁判的 LLM 要比被评测的更强,否则就是小学生批小学生作业。有了四个指标,优化就不再是凭感觉了。每周跑一次 200 条标准测试题(涵盖拼写错误、多轮追问、中英文混合等边缘 case),四个指标画趋势图,哪个掉了超过 5% 就报警。做完这轮,小庄总结了一份优化清单,按"花最少力气获得最大效果"排序:- Chunk 策略:按文档结构切分,每个 chunk 200-800 字,chunk 之间保留 10%-20% 重叠,附带标题、来源、更新时间等标签。效果最大。
- 混合检索 + 精排:向量检索 + BM25 双路召回,用 RRF 合并排名,再加 Cross-encoder reranker 精排。能提升 20%-30% 的准确率。
- 知识库质量:定期更新内容,删除过时的,标注置信度。垃圾内容进垃圾回答。
- Prompt 工程:明确告诉 LLM"请基于以下检索内容回答",加上"不知道就说不知道"的约束。
- 升级 Embedding 模型:影响中等。换一个更好的编码模型能提升 5%-10%,但边际效益递减很快。
- 升级 LLM:影响最小。检索质量达标之后,从 7B 升到 72B 对 RAG 效果的提升微乎其微。钱花在刀刃上,先把检索做好。
"AI 行业有个误区——大家总觉得更好的模型能解决一切。但 RAG 告诉我们,信息检索的质量比模型的能力重要得多。再聪明的学生,如果你给他一本错的参考书,他也会答错题。"菜不好,厨师手艺再好也炒不出好菜。模型是厨师,检索是买菜。RAG 不是给 LLM 加一个知识库那么简单,它是一个完整的工程体系:文档解析、文本清洗、chunk 切分、向量编码、索引构建、混合检索、结果合并、精排重排序、prompt 组装、评估监控。每一个环节都得做好。任何一个偷懒,最终都会体现在回答质量上。
基本
文件
流程
错误
SQL
调试
- 请求信息 : 2026-06-22 19:14:01 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/779736.html
- 运行时间 : 0.141665s [ 吞吐率:7.06req/s ] 内存消耗:4,673.92kb 文件加载:145
- 缓存信息 : 0 reads,0 writes
- 会话信息 : SESSION_ID=a99d75014c839a9134d4e23af10b80f9
- CONNECT:[ UseTime:0.000618s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
- SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000778s ]
- SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000311s ]
- SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000248s ]
- SHOW FULL COLUMNS FROM `set` [ RunTime:0.000469s ]
- SELECT * FROM `set` [ RunTime:0.000195s ]
- SHOW FULL COLUMNS FROM `article` [ RunTime:0.000530s ]
- SELECT * FROM `article` WHERE `id` = 779736 LIMIT 1 [ RunTime:0.000410s ]
- UPDATE `article` SET `lasttime` = 1782126841 WHERE `id` = 779736 [ RunTime:0.028385s ]
- SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000409s ]
- SELECT * FROM `article` WHERE `id` < 779736 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000582s ]
- SELECT * FROM `article` WHERE `id` > 779736 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000866s ]
- SELECT * FROM `article` WHERE `id` < 779736 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000782s ]
- SELECT * FROM `article` WHERE `id` < 779736 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.004577s ]
- SELECT * FROM `article` WHERE `id` < 779736 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.000656s ]
0.143285s