ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RAG 从零到生产:切块、向量化、重排序、评测,一条链路讲透(附完整代码)

RAG 从零到生产:切块、向量化、重排序、评测,一条链路讲透(附完整代码) 网上 80% 的 RAG 教程停在「连上向量库跑通问答」真到生产一检索全是噪声。这篇把从切块策略到评测指标的完整链路讲透代码可直接复制。文章目录一、先纠正一个认知RAG 的难点不在生成二、切块Chunking决定上限的一步2.1 常见策略对比2.2 直接可用的切块代码三、嵌入与向量检索四、重排序Rerank性价比最高的一步提升四点五、混合检索专有名词场景的救星四点六、查询改写用户的问题往往不适合直接检索五、生成端 Prompt防幻觉三件套六、评测不量化等于白调七、生产化检查清单八、效果调优的排查顺序卡住时按这个来一、先纠正一个认知RAG 的难点不在生成新手以为 RAG 是「检索 生成」五五开实际工程里检索质量占 80%查回来一堆无关片段再强的模型也只能编生成端只是「别乱发挥」靠 Prompt 约束就行。所以本文的重心全放在检索链路切块 → 嵌入 → 检索 → 重排序 → 生成最后补上多数教程跳过的一环——怎么量化评测你的 RAG 好不好。二、切块Chunking决定上限的一步2.1 常见策略对比策略做法适用坑固定长度切每 500 字切一刀快速原型把句子拦腰斩断召回碎片递归字符切按段落→句子→字符逐级降级切通用文档推荐起步分隔符顺序要调结构化切按 Markdown 标题/代码块切技术文档、Wiki需要解析器支持语义切用嵌入相似度找语义断点长篇报告贵且对短文档收益小2.2 直接可用的切块代码# chunking.pyfrompathlibimportPathdefrecursive_chunk(text:str,chunk_size:int500,overlap:int80)-list[str]:递归字符切块优先按大段落切超长段落再降级按句子切。 overlap 保留上下文衔接避免关键句正好被切在边界。separators[\n\n,\n,。,,,,,]defsplit_segment(seg:str,seps:list[str])-list[str]:iflen(seg)chunk_size:return[seg.strip()]ifseg.strip()else[]ifnotseps:# 无分隔符可用硬切return[seg[j:jchunk_size].strip()forjinrange(0,len(seg),chunk_size)]sep,restseps[0],seps[1:]piecesseg.split(sep)ifsepelselist(seg)out,buf[],forpinpieces:candidate(bufsepp)ifbufelsepiflen(candidate)chunk_sizeandbuf:out.extend(split_segment(buf,rest))# 超长则降级到下一层级bufpelse:bufcandidateifbuf.strip():out.extend(split_segment(buf,rest))returnout chunkssplit_segment(text,separators)# 加 overlap把上一块的尾部接到下一块开头ifoverlap0:merged,prev_tail[],forcinchunks:merged.append((prev_tail[-overlap:] c).strip()ifprev_tailelsec)prev_tailc chunksmergedreturnchunksif__name____main__:docPath(sample.txt).read_text(encodingutf-8)fori,cinenumerate(recursive_chunk(doc),1):print(f--- chunk{i}({len(c)}字) ---\n{c[:80]}...)踩坑overlap不是越大越好。设到块长的 30% 以上向量库里全是重复内容既费钱又拉低多样性。500 字块配 80 字 overlap 是个稳妥起点。三、嵌入与向量检索pipinstallsentence-transformers chromadb# rag_core.pyimportchromadbfromsentence_transformersimportSentenceTransformer modelSentenceTransformer(BAAI/bge-small-zh-v1.5)# 中文场景性价比最高的小模型clientchromadb.PersistentClient(path./chroma_db)colclient.get_or_create_collection(docs,metadata{hnsw:space:cosine})defbuild_index(chunks:list[str],source:str):embsmodel.encode(chunks,normalize_embeddingsTrue).tolist()col.add(ids[f{source}_{i}foriinrange(len(chunks))],documentschunks,embeddingsembs,metadatas[{source:source,chunk_id:i}foriinrange(len(chunks))],)defretrieve(query:str,top_k:int10)-list[dict]:q_embmodel.encode([query],normalize_embeddingsTrue).tolist()rescol.query(query_embeddingsq_emb,n_resultstop_k)return[{text:t,score:1-d,source:m[source]}fort,d,minzip(res[documents][0],res[distances][0],res[metadatas][0])]选型速查场景推荐嵌入模型维度中文、低成本BAAI/bge-small-zh-v1.5512中文、追求精度BAAI/bge-m31024支持长文本多语言intfloat/multilingual-e5-base768关键参数normalize_embeddingsTrue必须开归一化后才能用余弦相似度chroma 的 space 记得设cosine默认 L2 对归一化向量结果一致但语义容易搞混。四、重排序Rerank性价比最高的一步提升向量检索是「粗排」召回快但精度一般。加一个 Cross-Encoder 重排通常能带来最明显的一次性质量提升# rerank.pyfromsentence_transformersimportCrossEncoder rerankerCrossEncoder(BAAI/bge-reranker-base)defrerank(query:str,candidates:list[dict],top_n:int3)-list[dict]:pairs[(query,c[text])forcincandidates]scoresreranker.predict(pairs)forc,sinzip(candidates,scores):c[rerank_score]float(s)returnsorted(candidates,keylambdax:-x[rerank_score])[:top_n]流程变成向量召回 top 10 → 重排取 top 3 → 只把 3 条塞给模型。上下文更短、噪声更少、token 费用反而更低这是 RAG 里少有的「既要又要」。四点五、混合检索专有名词场景的救星纯向量检索有个盲区型号、代码名、错误码这类精确词比如 “ERR_CONN_RESET”、“RTX-5090”嵌入后容易和无关内容混在一起。加一路 BM25 关键词检索两路结果融合是提升最稳的手段# hybrid.py —— BM25 向量 双路召回 RRF 融合fromrank_bm25importBM25Okapi# pip install rank-bm25 jiebaimportjieba corpus[c[text]forcinall_chunks]# 你的全部切块tokenized[list(jieba.cut(c))forcincorpus]bm25BM25Okapi(tokenized)defhybrid_search(query:str,top_k:int10)-list[dict]:# 第一路向量召回vec_resultsretrieve(query,top_ktop_k)# 复用第三节的 retrieve# 第二路BM25 关键词召回scoresbm25.get_scores(list(jieba.cut(query)))kw_idssorted(range(len(corpus)),keylambdai:-scores[i])[:top_k]kw_results[{text:corpus[i],source:sources[i],score:scores[i]}foriinkw_idsifscores[i]0]# RRF 融合不看原始分数两路量纲不同只看排名defrrf(results:list[dict],tag:str)-dict[str,float]:return{r[text]:1.0/(60rank)forrank,rinenumerate(results)}|{}fused:dict[str,float]{}forrank,rinenumerate(vec_results):fused[r[text]]fused.get(r[text],0)1.0/(60rank)forrank,rinenumerate(kw_results):fused[r[text]]fused.get(r[text],0)1.0/(60rank)rankedsorted(fused.items(),keylambdax:-x[1])[:top_k]text2meta{r[text]:rforrinvec_resultskw_results}return[text2meta[t]|{rrf:s}fort,sinranked]RRF 的好处是不用纠结「BM25 分数和余弦相似度怎么加权」只按排名投票60 是经验常数一般不用调。专有名词查询多、或者评测里 Hit Rate 卡住不动时先上混合检索。四点六、查询改写用户的问题往往不适合直接检索两个高频问题及对策指代不清「那它多少钱」——多轮对话里的「它」检索必挂。把最近几轮对话先让模型改写成独立问题「Opus 5.5 的 API 价格是多少」再检索口语与文档措辞不一致用户问「怎么省钱」文档里写「成本优化」。让模型一次生成 2-3 个同义改写分别检索后合并去重命中率提升明显。REWRITE_PROMPT把用户的追问改写成不依赖上下文、适合检索的独立问题 并额外给出2个同义表述每行一个不要解释。 对话{history} 追问{question}改写是「花一次小模型调用换命中率」的买卖长对话场景必开。五、生成端 Prompt防幻觉三件套PROMPT你是严谨的资料问答助手。请严格遵守 1. 只依据下面的【参考资料】回答禁止使用资料外的知识 2. 每个关键结论后标注来源编号如 [1] 3. 如果资料不足以回答直接回答「资料中没有相关信息」不要猜测。 【参考资料】 {context} 【问题】 {question}三条缺一不可限定知识边界、强制引用来源方便人审、允许说不知道。幻觉大多不是模型爱编是 Prompt 没给它「不知道」这个出口。六、评测不量化等于白调改了切块策略到底有没有用必须跑评测。最小可行方案# eval.pyimportjsondefevaluate(testset_path:str,k:int3):testset 每行{question: ..., answer: ..., expected_source: xxx} 核心指标命中率Hit Rate—— top-k 里是否包含正确来源文档cases[json.loads(l)forlinopen(testset_path,encodingutf-8)]hits0forcaseincases:resultsretrieve(case[question],top_kk)got_sources{r[source]forrinresults}ifcase[expected_source]ingot_sources:hits1else:print(fMISS:{case[question]})print(fHit{k}{hits/len(cases):.1%})evaluate(testset.jsonl)指标优先级建议Hit Rate命中率先看检索有没有找对文档这是地基MRR正确答案排得越靠前分越高衡量排序质量有精力再上 LLM-as-Judge 评答案忠实度。MRR 的实现也就十行直接给出defmrr(testset_path:str,k:int10)-float:MRR正确来源在结果里排第 1 得 1.0排第 2 得 0.5以此类推cases[json.loads(l)forlinopen(testset_path,encodingutf-8)]total0.0forcaseincases:resultsretrieve(case[question],top_kk)rr0.0forrank,rinenumerate(results,1):ifr[source]case[expected_source]:rr1.0/rankbreaktotalrrreturntotal/len(cases)print(fMRR10 {mrr(testset.jsonl):.3f})判读标准Hit Rate 高但 MRR 低 → 检索找得到但排序差加重排两者都低 → 回去调切块或上混合检索别急着换模型。每次只改一个变量比如只换切块大小跑一遍评测对比 Hit Rate否则你永远不知道是哪个改动起的作用。七、生产化检查清单切块保留source元数据回答能回溯到原文档索引更新有幂等机制同 id 先删后加避免重复入库检索结果为空时的兜底话术而不是硬答多轮对话场景接入查询改写第 4.6 节专有名词密集的语料上了混合检索第 4.5 节评测集 ≥ 50 条真实用户问题随版本回归。八、效果调优的排查顺序卡住时按这个来很多人 RAG 效果差就盲目换嵌入模型其实正确排查顺序是固定的1. 肉眼看原始切块 → 切得稀碎/粘连先修切块零成本 2. 跑 Hit Rate → 检索层面的问题别碰生成端 3. Hit 低 → 混合检索 查询改写 → 还不行再试换嵌入模型 4. Hit 高但答案差 → 问题在生成端查 Prompt、上下文是否被截断 5. 排序差MRR低 → 上 Cross-Encoder 重排90% 的案例在第 1、2 步就解决了。切块质量 检索策略 模型选择这个优先级别搞反。
返回列表