
别再让RAG变"瞎检索"!一文拆解索引、检索、生成三大Pipeline,让你的Agent真正"长脑子"——从Chunk切分到重排序,从向量召回到大模型生成,手把手教你搭建工业级RAG检索流水线,告别"答非所问"的尴尬现场。
#mermaid-svg-9V0LLbUVqqEO1mbe{font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9V0LLbUVqqEO1mbe .error-icon{fill:#96CEB4;}#mermaid-svg-9V0LLbUVqqEO1mbe .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9V0LLbUVqqEO1mbe .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9V0LLbUVqqEO1mbe .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-9V0LLbUVqqEO1mbe .marker.cross{stroke:#4ECDC4;}#mermaid-svg-9V0LLbUVqqEO1mbe svg{font-size:16px;}#mermaid-svg-9V0LLbUVqqEO1mbe p{margin:0;}#mermaid-svg-9V0LLbUVqqEO1mbe .label{color:#1A1A2E;}#mermaid-svg-9V0LLbUVqqEO1mbe .cluster-label text{fill:#69314b;}#mermaid-svg-9V0LLbUVqqEO1mbe .cluster-label span{color:#69314b;}#mermaid-svg-9V0LLbUVqqEO1mbe .cluster-label span p{background-color:transparent;}#mermaid-svg-9V0LLbUVqqEO1mbe .label text,#mermaid-svg-9V0LLbUVqqEO1mbe span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-9V0LLbUVqqEO1mbe .node rect,#mermaid-svg-9V0LLbUVqqEO1mbe .node circle,#mermaid-svg-9V0LLbUVqqEO1mbe .node ellipse,#mermaid-svg-9V0LLbUVqqEO1mbe .node polygon,#mermaid-svg-9V0LLbUVqqEO1mbe .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-9V0LLbUVqqEO1mbe .rough-node .label text,#mermaid-svg-9V0LLbUVqqEO1mbe .node .label text,#mermaid-svg-9V0LLbUVqqEO1mbe .image-shape .label,#mermaid-svg-9V0LLbUVqqEO1mbe .icon-shape .label{text-anchor:middle;}#mermaid-svg-9V0LLbUVqqEO1mbe .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9V0LLbUVqqEO1mbe .rough-node .label,#mermaid-svg-9V0LLbUVqqEO1mbe .node .label,#mermaid-svg-9V0LLbUVqqEO1mbe .image-shape .label,#mermaid-svg-9V0LLbUVqqEO1mbe .icon-shape .label{text-align:center;}#mermaid-svg-9V0LLbUVqqEO1mbe .node.clickable{cursor:pointer;}#mermaid-svg-9V0LLbUVqqEO1mbe .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-9V0LLbUVqqEO1mbe .arrowheadPath{fill:#080603;}#mermaid-svg-9V0LLbUVqqEO1mbe .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-9V0LLbUVqqEO1mbe .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-9V0LLbUVqqEO1mbe .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-9V0LLbUVqqEO1mbe .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-9V0LLbUVqqEO1mbe .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-9V0LLbUVqqEO1mbe .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-9V0LLbUVqqEO1mbe .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-9V0LLbUVqqEO1mbe .cluster text{fill:#69314b;}#mermaid-svg-9V0LLbUVqqEO1mbe .cluster span{color:#69314b;}#mermaid-svg-9V0LLbUVqqEO1mbe div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9V0LLbUVqqEO1mbe .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-9V0LLbUVqqEO1mbe rect.text{fill:none;stroke-width:0;}#mermaid-svg-9V0LLbUVqqEO1mbe .icon-shape,#mermaid-svg-9V0LLbUVqqEO1mbe .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-9V0LLbUVqqEO1mbe .icon-shape p,#mermaid-svg-9V0LLbUVqqEO1mbe .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-9V0LLbUVqqEO1mbe .icon-shape .label rect,#mermaid-svg-9V0LLbUVqqEO1mbe .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-9V0LLbUVqqEO1mbe .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9V0LLbUVqqEO1mbe .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9V0LLbUVqqEO1mbe :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
RAG检索Pipeline全景
索引阶段Indexing
检索阶段Retrieval
生成阶段Generation
文档解析
Chunk切分策略
向量化与存储
查询改写
向量召回
重排序优化
上下文组装
Prompt工程
结果输出
目录速览:
嗨,大家好呀,我是你的老朋友精通代码大仙。接下来我们一起学习 《大模型应用开发_动手做AI_Agent》,震撼你的学习轨迹!
“磨刀不误砍柴工,但磨错了刀,连柴都砍不动。”
这话放在RAG(Retrieval-Augmented Generation,检索增强生成)上,简直不要太贴切。我见过太多小伙伴,一上来就调大模型参数,折腾Prompt工程,结果Agent回答得驴唇不对马嘴——不是模型不行,是你的检索Pipeline从根上就歪了。
你是不是也这样?好不容易搭了个RAG demo,问"公司的年假政策",它给你返回"员工食堂菜单";问"Python的GIL机制",它扯半天"Java的内存模型"。然后你怀疑人生:明明用了最先进的GPT-4,为啥还是这么"智障"?
真相很扎心:RAG的效果,70%取决于检索质量,30%才看生成能力。 检索Pipeline没打通,大模型就是"巧妇难为无米之炊"。今天咱们就掰开了揉碎了,从技术角度完整拆解RAG的三大核心阶段——索引、检索、生成。不搞虚的,全是能落地的干货。
一、索引阶段(Indexing)—— 打好地基,别让知识"碎"得莫名其妙
点题:索引是RAG的"原材料加工厂"
索引阶段解决一个核心问题:怎么把原始文档,变成大模型能"看懂"、检索系统能"找到"的知识单元?
这个过程就像图书馆编目。你总不能把整本书塞进书架吧?得拆成章节、标上标签、建立索引卡。RAG的索引阶段,就是干这个活儿的。
#mermaid-svg-YX3ktCAtLvNbDFQO{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YX3ktCAtLvNbDFQO .error-icon{fill:#96CEB4;}#mermaid-svg-YX3ktCAtLvNbDFQO .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YX3ktCAtLvNbDFQO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YX3ktCAtLvNbDFQO .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-YX3ktCAtLvNbDFQO .marker.cross{stroke:#4ECDC4;}#mermaid-svg-YX3ktCAtLvNbDFQO svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YX3ktCAtLvNbDFQO p{margin:0;}#mermaid-svg-YX3ktCAtLvNbDFQO .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#1A1A2E;}#mermaid-svg-YX3ktCAtLvNbDFQO .cluster-label text{fill:#69314b;}#mermaid-svg-YX3ktCAtLvNbDFQO .cluster-label span{color:#69314b;}#mermaid-svg-YX3ktCAtLvNbDFQO .cluster-label span p{background-color:transparent;}#mermaid-svg-YX3ktCAtLvNbDFQO .label text,#mermaid-svg-YX3ktCAtLvNbDFQO span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-YX3ktCAtLvNbDFQO .node rect,#mermaid-svg-YX3ktCAtLvNbDFQO .node circle,#mermaid-svg-YX3ktCAtLvNbDFQO .node ellipse,#mermaid-svg-YX3ktCAtLvNbDFQO .node polygon,#mermaid-svg-YX3ktCAtLvNbDFQO .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-YX3ktCAtLvNbDFQO .rough-node .label text,#mermaid-svg-YX3ktCAtLvNbDFQO .node .label text,#mermaid-svg-YX3ktCAtLvNbDFQO .image-shape .label,#mermaid-svg-YX3ktCAtLvNbDFQO .icon-shape .label{text-anchor:middle;}#mermaid-svg-YX3ktCAtLvNbDFQO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YX3ktCAtLvNbDFQO .rough-node .label,#mermaid-svg-YX3ktCAtLvNbDFQO .node .label,#mermaid-svg-YX3ktCAtLvNbDFQO .image-shape .label,#mermaid-svg-YX3ktCAtLvNbDFQO .icon-shape .label{text-align:center;}#mermaid-svg-YX3ktCAtLvNbDFQO .node.clickable{cursor:pointer;}#mermaid-svg-YX3ktCAtLvNbDFQO .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-YX3ktCAtLvNbDFQO .arrowheadPath{fill:#080603;}#mermaid-svg-YX3ktCAtLvNbDFQO .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-YX3ktCAtLvNbDFQO .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-YX3ktCAtLvNbDFQO .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-YX3ktCAtLvNbDFQO .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-YX3ktCAtLvNbDFQO .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-YX3ktCAtLvNbDFQO .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-YX3ktCAtLvNbDFQO .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-YX3ktCAtLvNbDFQO .cluster text{fill:#69314b;}#mermaid-svg-YX3ktCAtLvNbDFQO .cluster span{color:#69314b;}#mermaid-svg-YX3ktCAtLvNbDFQO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YX3ktCAtLvNbDFQO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-YX3ktCAtLvNbDFQO rect.text{fill:none;stroke-width:0;}#mermaid-svg-YX3ktCAtLvNbDFQO .icon-shape,#mermaid-svg-YX3ktCAtLvNbDFQO .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-YX3ktCAtLvNbDFQO .icon-shape p,#mermaid-svg-YX3ktCAtLvNbDFQO .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-YX3ktCAtLvNbDFQO .icon-shape .label rect,#mermaid-svg-YX3ktCAtLvNbDFQO .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-YX3ktCAtLvNbDFQO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YX3ktCAtLvNbDFQO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YX3ktCAtLvNbDFQO :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
索引Pipeline
原始文档PDF/Word/网页
文档解析
清洗与标准化
Chunk切分
向量化Embedding
向量数据库存储
痛点分析:Chunk切分,切多少、怎么切,全是坑
新手最容易在这里栽跟头。我见过最典型的错误,就是一刀切地按固定字符数切分。
比如这段代码:
# 错误示范:简单粗暴的固定长度切分
def bad_chunk(text, chunk_size=500):
return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
# 结果:一句话被拦腰斩断
# "公司的年假政策是:工作满1年享5天,满10年" → Chunk 1
# "享10天,满20年享15天。申请流程如下…" → Chunk 2
用户问"工作满10年有多少天年假",检索系统可能只召回Chunk 1(里面有"满10年"但没结果),或者Chunk 2(有"10天"但不知道条件)。语义完整性被破坏了,大模型拿到手的是残缺的上下文,能答对才怪。
另一个误区是忽视文档结构。PDF里的表格、代码块、多级标题,这些结构信息如果直接当成纯文本处理,会丢失大量语义。比如:
原始表格:
| 职级 | 年假天数 | 备注 |
| P5 | 5天 | 基础 |
| P6 | 10天 | 资深 |
错误解析后变成纯文本:
"P5 5天 基础 P6 10天 资深"
这还怎么检索?
解决方案:结构化切分 + 语义边界保护
第一,按语义单元切分,而非固定长度。
# 正确示范:基于语义边界的智能切分
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 优先按段落、句子切分,再考虑长度限制
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=500, # 目标长度
chunk_overlap=50, # 重叠区域,保证上下文连贯
separators=["\\n\\n", "\\n", ".", "!", "?", " ", ""], # 优先级顺序
length_function=len
)
chunks = text_splitter.split_text(long_document)
关键技巧:设置合理的overlap(重叠区域)。比如前后Chunk重叠50-100字符,确保跨边界的语义不被切断。
第二,保留文档结构元数据。
# 为每个Chunk打上结构标签
chunk_metadata = {
"source": "员工手册2024.pdf",
"page": 15,
"section": "3.2 休假制度", # 章节信息
"doc_type": "table", # 文档类型:正文/表格/代码
"parent_title": "第三章 员工福利" # 父级标题,用于上下文重建
}
# 存储时带上metadata
vector_store.add_texts(
texts=chunks,
metadatas=[chunk_metadata] * len(chunks)
)
第三,针对特殊内容优化解析策略。
| PDF扫描件 | OCR + 版面分析 | PaddleOCR, Unstructured |
| PDF表格 | 保留表格结构,转为Markdown | Camelot, Tabula |
| 代码文档 | 按函数/类为单位切分 | Tree-sitter |
| 网页HTML | 提取正文,去除导航/广告 | BeautifulSoup, Readability |
这样做的好处:检索时能根据metadata过滤(比如"只看第三章的内容"),还能在生成阶段重建完整的上下文层次。
小结
索引阶段的核心原则是——“切得准,才能找得回”。宁可多花时间在Chunk策略上,也别让残缺的语义流入下游。记住:Garbage in, garbage out,索引质量决定了RAG的天花板。
二、检索阶段(Retrieval)—— 从"大海捞针"到"精准定位"
点题:检索是RAG的"搜索引擎"
用户提问 → 系统找相关知识 → 返回最相关的Chunk。听起来简单?但**"相关"二字,藏着大学问。**
检索阶段通常包含三个子环节:查询改写(Query Rewriting)、向量召回(Vector Retrieval)、重排序(Reranking)。就像快递分拣:先理解你要什么(改写),再从仓库找一批候选(召回),最后精选出最匹配的(重排)。
#mermaid-svg-ww5MB9WEtikV1UnV{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ww5MB9WEtikV1UnV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ww5MB9WEtikV1UnV .error-icon{fill:#96CEB4;}#mermaid-svg-ww5MB9WEtikV1UnV .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ww5MB9WEtikV1UnV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ww5MB9WEtikV1UnV .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-ww5MB9WEtikV1UnV .marker.cross{stroke:#4ECDC4;}#mermaid-svg-ww5MB9WEtikV1UnV svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ww5MB9WEtikV1UnV p{margin:0;}#mermaid-svg-ww5MB9WEtikV1UnV .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#1A1A2E;}#mermaid-svg-ww5MB9WEtikV1UnV .cluster-label text{fill:#69314b;}#mermaid-svg-ww5MB9WEtikV1UnV .cluster-label span{color:#69314b;}#mermaid-svg-ww5MB9WEtikV1UnV .cluster-label span p{background-color:transparent;}#mermaid-svg-ww5MB9WEtikV1UnV .label text,#mermaid-svg-ww5MB9WEtikV1UnV span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-ww5MB9WEtikV1UnV .node rect,#mermaid-svg-ww5MB9WEtikV1UnV .node circle,#mermaid-svg-ww5MB9WEtikV1UnV .node ellipse,#mermaid-svg-ww5MB9WEtikV1UnV .node polygon,#mermaid-svg-ww5MB9WEtikV1UnV .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-ww5MB9WEtikV1UnV .rough-node .label text,#mermaid-svg-ww5MB9WEtikV1UnV .node .label text,#mermaid-svg-ww5MB9WEtikV1UnV .image-shape .label,#mermaid-svg-ww5MB9WEtikV1UnV .icon-shape .label{text-anchor:middle;}#mermaid-svg-ww5MB9WEtikV1UnV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ww5MB9WEtikV1UnV .rough-node .label,#mermaid-svg-ww5MB9WEtikV1UnV .node .label,#mermaid-svg-ww5MB9WEtikV1UnV .image-shape .label,#mermaid-svg-ww5MB9WEtikV1UnV .icon-shape .label{text-align:center;}#mermaid-svg-ww5MB9WEtikV1UnV .node.clickable{cursor:pointer;}#mermaid-svg-ww5MB9WEtikV1UnV .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-ww5MB9WEtikV1UnV .arrowheadPath{fill:#080603;}#mermaid-svg-ww5MB9WEtikV1UnV .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-ww5MB9WEtikV1UnV .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-ww5MB9WEtikV1UnV .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-ww5MB9WEtikV1UnV .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-ww5MB9WEtikV1UnV .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-ww5MB9WEtikV1UnV .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-ww5MB9WEtikV1UnV .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-ww5MB9WEtikV1UnV .cluster text{fill:#69314b;}#mermaid-svg-ww5MB9WEtikV1UnV .cluster span{color:#69314b;}#mermaid-svg-ww5MB9WEtikV1UnV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ww5MB9WEtikV1UnV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-ww5MB9WEtikV1UnV rect.text{fill:none;stroke-width:0;}#mermaid-svg-ww5MB9WEtikV1UnV .icon-shape,#mermaid-svg-ww5MB9WEtikV1UnV .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-ww5MB9WEtikV1UnV .icon-shape p,#mermaid-svg-ww5MB9WEtikV1UnV .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-ww5MB9WEtikV1UnV .icon-shape .label rect,#mermaid-svg-ww5MB9WEtikV1UnV .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-ww5MB9WEtikV1UnV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ww5MB9WEtikV1UnV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ww5MB9WEtikV1UnV :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
检索Pipeline
用户原始查询
查询改写Query Rewriting
多路召回Vector + Keyword
候选池Top-K*3
重排序Reranking
精排结果Top-K
痛点分析:用户问的和系统想的,根本不在一个频道
痛点一:查询理解偏差。
用户问:“这玩意儿怎么用?”——"这玩意儿"指啥?系统一脸懵。或者用户用口语、方言、错别字:“年假咋请?”,向量检索直接失效。
痛点二:向量检索的"语义鸿沟"。
向量相似度 ≠ 真实相关性。经典案例:
用户查询:"怎么离职"(实际想了解离职流程)
召回结果Top1:"如何防止员工离职"(HR管理策略)
召回结果Top2:"离职证明开具规范"(真正想要的)
为啥?因为"离职"这个词向量相近,但意图完全相反。
痛点三:忽视关键词精确匹配。
纯向量检索对专有名词很弱。用户问"Python 3.11的PEP 695",向量召回可能带回"Python 3.10的新特性",因为语义相近,但版本号这个关键信息丢了。
解决方案:多路召回 + 智能重排
第一,查询改写:让系统"听懂"用户。
# 查询改写策略:扩展、澄清、结构化
# 策略1:同义词扩展(解决口语化)
def expand_query(original_query):
expansions = {
"咋": "怎么",
"啥": "什么",
"这玩意儿": "这个产品/功能",
"年假": "年休假|带薪休假|annual leave"
}
# 使用LLM进行意图识别和改写
rewritten = llm_rewrite(
prompt=f"""将用户查询改写成适合检索的标准形式。
用户查询:{original_query}
要求:1) 消除歧义 2) 补充隐含条件 3) 拆分多意图"""
)
return rewritten
# 示例:
# "年假咋请" → "员工年休假的申请流程和审批步骤"
# "这玩意儿报错" → "产品X的错误排查和解决方法"
第二,多路召回:向量 + 关键词 + 知识图谱。
# 多路召回融合
def hybrid_retrieval(query, top_k=10):
# 路1:向量召回(语义相似)
vector_results = vector_store.similarity_search(query, k=top_k*2)
# 路2:BM25关键词召回(精确匹配)
keyword_results = bm25_search(query, k=top_k*2)
# 路3:结构化过滤(利用metadata)
if "年假" in query:
filter_results = vector_store.search(
query,
filter={"section": {"$in": ["休假制度", "员工福利"]}}
)
# 融合排序:RRF(Reciprocal Rank Fusion)
final_results = reciprocal_rank_fusion([
vector_results,
keyword_results,
filter_results
], k=top_k)
return final_results
RRF公式简单有效:Score = Σ(1/(k+rank)),不同来源的结果互相补强。
第三,重排序:用更聪明的模型精选。
# 使用Cross-Encoder进行精排
from sentence_transformers import CrossEncoder
reranker = CrossEncoder('BAAI/bge-reranker-large')
def rerank(query, candidates):
# 构造query-chunk对
pairs = [[query, doc.page_content] for doc in candidates]
# 预测相关性分数
scores = reranker.predict(pairs)
# 按分数重排
ranked = sorted(
zip(candidates, scores),
key=lambda x: x[1],
reverse=True
)
return [doc for doc, score in ranked[:5]] # 取Top-5
Cross-Encoder比双塔模型(Bi-Encoder)慢,但精度高得多——因为它能"同时看"查询和文档,捕捉细粒度交互。
第四,反馈迭代:让系统越用越准。
# 记录用户点击/反馈,优化后续检索
def log_feedback(query, retrieved_docs, user_clicked_index):
# 用户点了第3个结果 → 说明前2个不够准
if user_clicked_index > 0:
# 提升被点击文档的权重
fine_tune_data.append({
"query": query,
"positive": retrieved_docs[user_clicked_index],
"negatives": retrieved_docs[:user_clicked_index]
})
小结
检索阶段的核心是——“找得全,更要找得准”。多路召回保证覆盖率,重排序保证精确度,查询改写打通用户意图。别指望单一向量检索解决所有问题,组合拳才是王道。
三、生成阶段(Generation)—— 大模型不是万能背锅侠
点题:生成是RAG的"最终组装车间"
终于到大模型出场了!但别高兴太早——检索结果再准,组装不好也是白搭。 生成阶段要解决:怎么把检索到的Chunk,有效地塞进Prompt,让大模型给出准确、连贯、有依据的回答。
#mermaid-svg-FxxDc6V97aBiBgm4{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FxxDc6V97aBiBgm4 .error-icon{fill:#96CEB4;}#mermaid-svg-FxxDc6V97aBiBgm4 .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FxxDc6V97aBiBgm4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FxxDc6V97aBiBgm4 .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-FxxDc6V97aBiBgm4 .marker.cross{stroke:#4ECDC4;}#mermaid-svg-FxxDc6V97aBiBgm4 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FxxDc6V97aBiBgm4 p{margin:0;}#mermaid-svg-FxxDc6V97aBiBgm4 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#1A1A2E;}#mermaid-svg-FxxDc6V97aBiBgm4 .cluster-label text{fill:#69314b;}#mermaid-svg-FxxDc6V97aBiBgm4 .cluster-label span{color:#69314b;}#mermaid-svg-FxxDc6V97aBiBgm4 .cluster-label span p{background-color:transparent;}#mermaid-svg-FxxDc6V97aBiBgm4 .label text,#mermaid-svg-FxxDc6V97aBiBgm4 span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-FxxDc6V97aBiBgm4 .node rect,#mermaid-svg-FxxDc6V97aBiBgm4 .node circle,#mermaid-svg-FxxDc6V97aBiBgm4 .node ellipse,#mermaid-svg-FxxDc6V97aBiBgm4 .node polygon,#mermaid-svg-FxxDc6V97aBiBgm4 .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-FxxDc6V97aBiBgm4 .rough-node .label text,#mermaid-svg-FxxDc6V97aBiBgm4 .node .label text,#mermaid-svg-FxxDc6V97aBiBgm4 .image-shape .label,#mermaid-svg-FxxDc6V97aBiBgm4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-FxxDc6V97aBiBgm4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-FxxDc6V97aBiBgm4 .rough-node .label,#mermaid-svg-FxxDc6V97aBiBgm4 .node .label,#mermaid-svg-FxxDc6V97aBiBgm4 .image-shape .label,#mermaid-svg-FxxDc6V97aBiBgm4 .icon-shape .label{text-align:center;}#mermaid-svg-FxxDc6V97aBiBgm4 .node.clickable{cursor:pointer;}#mermaid-svg-FxxDc6V97aBiBgm4 .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-FxxDc6V97aBiBgm4 .arrowheadPath{fill:#080603;}#mermaid-svg-FxxDc6V97aBiBgm4 .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-FxxDc6V97aBiBgm4 .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-FxxDc6V97aBiBgm4 .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-FxxDc6V97aBiBgm4 .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-FxxDc6V97aBiBgm4 .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-FxxDc6V97aBiBgm4 .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-FxxDc6V97aBiBgm4 .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-FxxDc6V97aBiBgm4 .cluster text{fill:#69314b;}#mermaid-svg-FxxDc6V97aBiBgm4 .cluster span{color:#69314b;}#mermaid-svg-FxxDc6V97aBiBgm4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-FxxDc6V97aBiBgm4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-FxxDc6V97aBiBgm4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-FxxDc6V97aBiBgm4 .icon-shape,#mermaid-svg-FxxDc6V97aBiBgm4 .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-FxxDc6V97aBiBgm4 .icon-shape p,#mermaid-svg-FxxDc6V97aBiBgm4 .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-FxxDc6V97aBiBgm4 .icon-shape .label rect,#mermaid-svg-FxxDc6V97aBiBgm4 .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-FxxDc6V97aBiBgm4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-FxxDc6V97aBiBgm4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-FxxDc6V97aBiBgm4 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
生成Pipeline
检索结果Top-K Chunks
上下文组装
Prompt构建
大模型生成
后处理溯源/格式化
去重
排序
截断
系统指令
参考文档
用户问题
痛点分析:Prompt塞太满,模型直接"失忆"
痛点一:上下文窗口爆炸。
检索回10个Chunk,每个500字,加起来5000字。加上系统指令、用户问题,轻松突破4K token限制。然后你换更大的模型(32K、128K),发现长上下文≠有效利用——模型会"迷失在中间",对上下文首尾记得清,中间的内容视而不见。
痛点二:信息冲突与噪声。
多个Chunk可能包含矛盾信息(比如不同版本的政策),或者无关信息(检索误召回)。模型不会自动判断哪个更可信,可能综合出一个"四不像"答案。
痛点三:缺乏溯源,"幻觉"难防。
模型回答得头头是道,但用户问"这是哪条政策规定的?",系统哑火。没有溯源能力,RAG就失去了"可验证性"这个核心优势。
解决方案:结构化Prompt + 动态上下文管理
第一,智能截断与优先级排序。
def build_context(retrieved_chunks, max_tokens=3000):
# 按相关性分数排序(已重排过)
sorted_chunks = sorted(retrieved_chunks, key=lambda x: x.score, reverse=True)
context_parts = []
current_tokens = 0
for chunk in sorted_chunks:
chunk_tokens = estimate_tokens(chunk.content)
# 优先保留高相关性内容
if current_tokens + chunk_tokens <= max_tokens * 0.8: # 留20%给指令和输出
context_parts.append({
"content": chunk.content,
"source": chunk.metadata["source"],
"relevance": chunk.score
})
current_tokens += chunk_tokens
else:
# 低相关性内容摘要或丢弃
break
return context_parts
第二,结构化Prompt模板。
RAG_PROMPT_TEMPLATE = """你是一个专业的企业知识助手。请基于以下参考文档回答问题。
【回答要求】
1. 只使用参考文档中的信息,不要编造
2. 如果文档信息不足,明确说明"根据现有资料无法确定"
3. 引用来源时使用[来源: 文档名, 章节]
【参考文档】
{context}
【历史对话】
{chat_history}
【用户问题】
{question}
请逐步思考:
1. 用户问题的核心意图是什么?
2. 参考文档中哪些部分直接相关?
3. 如何组织答案最清晰?
最终回答:"""
# 组装上下文
context_str = "\\n\\n—\\n\\n".join([
f"[来源: {c['source']}]\\n{c['content']}"
for c in context_parts
])
第三,动态Few-shot示例。
# 根据问题类型,动态选择示例
def select_few_shot_examples(query_type):
examples = {
"流程咨询": [
{"Q": "怎么申请报销?", "A": "根据《财务制度》第三章,需… [来源: 财务制度, 3.1]"},
],
"政策解释": [
{"Q": "年假可以累积吗?", "A": "根据《员工手册》第15页,年假当年有效… [来源: 员工手册, 3.2]"},
],
"故障排查": [
{"Q": "系统登录失败", "A": "可能原因:1) 密码错误… [来源: IT运维手册, 2.3]"},
]
}
return examples.get(query_type, [])
第四,后处理:溯源与格式化。
def post_process(answer, context_parts):
# 提取引用标记
citations = extract_citations(answer) # 正则匹配 [来源: …]
# 验证引用真实性
verified_citations = []
for cite in citations:
source_doc = find_in_context(cite, context_parts)
if source_doc:
verified_citations.append({
"quote": cite,
"verified": True,
"page": source_doc.get("page")
})
else:
# 标记为"未验证",提示可能幻觉
verified_citations.append({
"quote": cite,
"verified": False,
"warning": "该引用未在检索结果中找到"
})
# 格式化最终输出
return {
"answer": answer,
"citations": verified_citations,
"confidence": calculate_confidence(answer, verified_citations),
"suggested_followup": generate_followup(answer)
}
小结
生成阶段的核心是——“给模型恰到好处的上下文,而不是越多越好”。结构化Prompt、动态示例、溯源验证,三者缺一不可。记住:大模型是"演员",检索结果是"剧本",Prompt是"导演指令",配合好了才能出好戏。
四、三大阶段的协同与调优 —— Pipeline不是拼积木
点题:RAG是系统工程,局部最优≠全局最优
很多新手容易犯的错误:孤立优化每个阶段。索引切得细,检索却召回不全;检索精度高,生成时上下文又塞不下。三个阶段必须联动调优。
#mermaid-svg-SSqn83BYmy4erPCi{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SSqn83BYmy4erPCi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SSqn83BYmy4erPCi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SSqn83BYmy4erPCi .error-icon{fill:#96CEB4;}#mermaid-svg-SSqn83BYmy4erPCi .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-SSqn83BYmy4erPCi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SSqn83BYmy4erPCi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SSqn83BYmy4erPCi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SSqn83BYmy4erPCi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SSqn83BYmy4erPCi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SSqn83BYmy4erPCi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SSqn83BYmy4erPCi .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-SSqn83BYmy4erPCi .marker.cross{stroke:#4ECDC4;}#mermaid-svg-SSqn83BYmy4erPCi svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SSqn83BYmy4erPCi p{margin:0;}#mermaid-svg-SSqn83BYmy4erPCi .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#1A1A2E;}#mermaid-svg-SSqn83BYmy4erPCi .cluster-label text{fill:#69314b;}#mermaid-svg-SSqn83BYmy4erPCi .cluster-label span{color:#69314b;}#mermaid-svg-SSqn83BYmy4erPCi .cluster-label span p{background-color:transparent;}#mermaid-svg-SSqn83BYmy4erPCi .label text,#mermaid-svg-SSqn83BYmy4erPCi span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-SSqn83BYmy4erPCi .node rect,#mermaid-svg-SSqn83BYmy4erPCi .node circle,#mermaid-svg-SSqn83BYmy4erPCi .node ellipse,#mermaid-svg-SSqn83BYmy4erPCi .node polygon,#mermaid-svg-SSqn83BYmy4erPCi .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-SSqn83BYmy4erPCi .rough-node .label text,#mermaid-svg-SSqn83BYmy4erPCi .node .label text,#mermaid-svg-SSqn83BYmy4erPCi .image-shape .label,#mermaid-svg-SSqn83BYmy4erPCi .icon-shape .label{text-anchor:middle;}#mermaid-svg-SSqn83BYmy4erPCi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SSqn83BYmy4erPCi .rough-node .label,#mermaid-svg-SSqn83BYmy4erPCi .node .label,#mermaid-svg-SSqn83BYmy4erPCi .image-shape .label,#mermaid-svg-SSqn83BYmy4erPCi .icon-shape .label{text-align:center;}#mermaid-svg-SSqn83BYmy4erPCi .node.clickable{cursor:pointer;}#mermaid-svg-SSqn83BYmy4erPCi .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-SSqn83BYmy4erPCi .arrowheadPath{fill:#080603;}#mermaid-svg-SSqn83BYmy4erPCi .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-SSqn83BYmy4erPCi .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-SSqn83BYmy4erPCi .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-SSqn83BYmy4erPCi .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-SSqn83BYmy4erPCi .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-SSqn83BYmy4erPCi .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-SSqn83BYmy4erPCi .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-SSqn83BYmy4erPCi .cluster text{fill:#69314b;}#mermaid-svg-SSqn83BYmy4erPCi .cluster span{color:#69314b;}#mermaid-svg-SSqn83BYmy4erPCi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SSqn83BYmy4erPCi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-SSqn83BYmy4erPCi rect.text{fill:none;stroke-width:0;}#mermaid-svg-SSqn83BYmy4erPCi .icon-shape,#mermaid-svg-SSqn83BYmy4erPCi .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-SSqn83BYmy4erPCi .icon-shape p,#mermaid-svg-SSqn83BYmy4erPCi .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-SSqn83BYmy4erPCi .icon-shape .label rect,#mermaid-svg-SSqn83BYmy4erPCi .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-SSqn83BYmy4erPCi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SSqn83BYmy4erPCi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SSqn83BYmy4erPCi :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
协同调优循环
索引策略Chunk大小/重叠
检索策略Top-K数量
生成策略上下文窗口
评估指标
问题诊断
针对性优化
Chunk小→语义准但数量多
Top-K大→召回全但噪声多
窗口大→信息全但注意力分散
痛点分析:调了A坏B,按下葫芦浮起瓢
典型场景:
| Chunk从500降到200字 | 单Chunk语义更准 | 需要更大Top-K才能覆盖完整信息,生成时上下文爆炸 |
| 向量召回Top-K从5提到20 | 召回率提升 | 噪声增加,重排序压力变大,生成质量下降 |
| 换更强的重排序模型 | 精度提升 | 延迟增加,用户体验变差 |
| 换更大上下文窗口的模型 | 能塞更多Chunk | 成本飙升,且长上下文利用率未必高 |
根本问题:缺乏系统性的评估和诊断。
解决方案:建立端到端的评估体系
第一步:定义分层评估指标。
# 索引质量指标
index_metrics = {
"chunk_semantic_coherence": 0.85, # Chunk内语义连贯性(人工抽样)
"structure_preservation": 0.90, # 结构信息保留率
"metadata_coverage": 0.95 # 元数据完整度
}
# 检索质量指标
retrieval_metrics = {
"recall@5": 0.78, # 正确答案在前5中的比例
"mrr": 0.65, # 平均倒数排名
"ndcg@10": 0.72 # 考虑相关度加权的排名质量
}
# 生成质量指标
generation_metrics = {
"faithfulness": 0.80, # 答案忠实于检索内容(非幻觉)
"answer_relevance": 0.85, # 答案与问题相关度
"citation_accuracy": 0.90 # 引用准确性
}
# 综合指标
end_to_end_metrics = {
"user_satisfaction": 4.2/5, # 用户满意度
"latency_p99": 2.3, # P99延迟(秒)
"cost_per_query": 0.05 # 单次查询成本(美元)
}
第二步:问题诊断流程。
def diagnose_rag_issue(query, expected_answer, actual_output):
issues = []
# 检查1:检索阶段
retrieved = actual_output["retrieved_chunks"]
if expected_answer not in " ".join([c.content for c in retrieved]):
issues.append("检索失败:正确答案未召回")
# 进一步诊断:是索引问题(没切到)还是检索问题(向量不准)?
if check_index_coverage(expected_answer):
issues.append("→ 索引已覆盖,需优化检索策略")
else:
issues.append("→ 索引未覆盖,需调整Chunk策略")
# 检查2:生成阶段
elif expected_answer not in actual_output["answer"]:
issues.append("生成失败:检索到但未正确利用")
# 诊断:上下文组装问题?Prompt问题?模型能力问题?
if len(actual_output["context_used"]) < len(retrieved):
issues.append("→ 上下文被截断,需调整窗口分配")
return issues
第三步:参数联动调优。
# 网格搜索找到最优参数组合
param_grid = {
"chunk_size": [200, 300, 500, 800],
"chunk_overlap": [0, 50, 100, 200],
"retrieval_top_k": [5, 10, 15, 20],
"rerank_top_k": [3, 5, 7],
"context_max_tokens": [1500, 2000, 3000, 4000]
}
best_score = 0
best_params = None
for params in grid_search(param_grid):
pipeline = build_pipeline(params)
score = evaluate_on_test_set(pipeline, test_queries)
if score > best_score:
best_score = score
best_params = params
print(f"最优参数: {best_params}, 综合得分: {best_score}")
小结
RAG优化的核心心法——“度量驱动,全局最优”。别凭感觉调参,要建立完整的评估体系,定位瓶颈环节,针对性优化。记住:Pipeline的短板决定了整体水位,找到瓶颈比盲目堆料更重要。
五、实战:搭建一个可落地的RAG检索系统
点题:从理论到代码,走通完整链路
光说不练假把式。这一节给一个最小可运行的完整实现,覆盖索引、检索、生成三大阶段。
#mermaid-svg-9OC7lzJ65EGPe2tY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-9OC7lzJ65EGPe2tY .error-icon{fill:#96CEB4;}#mermaid-svg-9OC7lzJ65EGPe2tY .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-9OC7lzJ65EGPe2tY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-9OC7lzJ65EGPe2tY .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-9OC7lzJ65EGPe2tY .marker.cross{stroke:#4ECDC4;}#mermaid-svg-9OC7lzJ65EGPe2tY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-9OC7lzJ65EGPe2tY p{margin:0;}#mermaid-svg-9OC7lzJ65EGPe2tY .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#1A1A2E;}#mermaid-svg-9OC7lzJ65EGPe2tY .cluster-label text{fill:#69314b;}#mermaid-svg-9OC7lzJ65EGPe2tY .cluster-label span{color:#69314b;}#mermaid-svg-9OC7lzJ65EGPe2tY .cluster-label span p{background-color:transparent;}#mermaid-svg-9OC7lzJ65EGPe2tY .label text,#mermaid-svg-9OC7lzJ65EGPe2tY span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-9OC7lzJ65EGPe2tY .node rect,#mermaid-svg-9OC7lzJ65EGPe2tY .node circle,#mermaid-svg-9OC7lzJ65EGPe2tY .node ellipse,#mermaid-svg-9OC7lzJ65EGPe2tY .node polygon,#mermaid-svg-9OC7lzJ65EGPe2tY .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-9OC7lzJ65EGPe2tY .rough-node .label text,#mermaid-svg-9OC7lzJ65EGPe2tY .node .label text,#mermaid-svg-9OC7lzJ65EGPe2tY .image-shape .label,#mermaid-svg-9OC7lzJ65EGPe2tY .icon-shape .label{text-anchor:middle;}#mermaid-svg-9OC7lzJ65EGPe2tY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-9OC7lzJ65EGPe2tY .rough-node .label,#mermaid-svg-9OC7lzJ65EGPe2tY .node .label,#mermaid-svg-9OC7lzJ65EGPe2tY .image-shape .label,#mermaid-svg-9OC7lzJ65EGPe2tY .icon-shape .label{text-align:center;}#mermaid-svg-9OC7lzJ65EGPe2tY .node.clickable{cursor:pointer;}#mermaid-svg-9OC7lzJ65EGPe2tY .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-9OC7lzJ65EGPe2tY .arrowheadPath{fill:#080603;}#mermaid-svg-9OC7lzJ65EGPe2tY .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-9OC7lzJ65EGPe2tY .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-9OC7lzJ65EGPe2tY .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-9OC7lzJ65EGPe2tY .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-9OC7lzJ65EGPe2tY .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-9OC7lzJ65EGPe2tY .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-9OC7lzJ65EGPe2tY .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-9OC7lzJ65EGPe2tY .cluster text{fill:#69314b;}#mermaid-svg-9OC7lzJ65EGPe2tY .cluster span{color:#69314b;}#mermaid-svg-9OC7lzJ65EGPe2tY div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-9OC7lzJ65EGPe2tY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-9OC7lzJ65EGPe2tY rect.text{fill:none;stroke-width:0;}#mermaid-svg-9OC7lzJ65EGPe2tY .icon-shape,#mermaid-svg-9OC7lzJ65EGPe2tY .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-9OC7lzJ65EGPe2tY .icon-shape p,#mermaid-svg-9OC7lzJ65EGPe2tY .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-9OC7lzJ65EGPe2tY .icon-shape .label rect,#mermaid-svg-9OC7lzJ65EGPe2tY .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-9OC7lzJ65EGPe2tY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-9OC7lzJ65EGPe2tY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-9OC7lzJ65EGPe2tY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
完整RAG系统架构
文档上传
解析服务Unstructured
切分服务LangChain
向量化服务BGE-M3
向量数据库Milvus
用户查询
API网关
查询改写服务
检索服务Hybrid Search
重排序服务BGE-Reranker
生成服务GPT-4
结果返回
完整代码实现
"""
RAG检索Pipeline完整实现
技术栈:LangChain + BGE-M3 + Milvus + OpenAI
"""
import os
from typing import List, Dict, Any
from dataclasses import dataclass
from langchain.schema import Document
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import Milvus
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
from sentence_transformers import CrossEncoder
import numpy as np
# ============ 配置 ============
@dataclass
class RAGConfig:
"""RAG系统配置"""
# 索引配置
chunk_size: int = 500
chunk_overlap: int = 100
# 检索配置
vector_top_k: int = 15 # 向量召回数量
rerank_top_k: int = 5 # 精排后数量
# 生成配置
context_max_tokens: int = 2500
temperature: float = 0.3
# 模型配置
embedding_model: str = "BAAI/bge-m3"
reranker_model: str = "BAAI/bge-reranker-large"
llm_model: str = "gpt-4-turbo-preview"
# ============ 第一阶段:索引 ============
class IndexingPipeline:
"""文档索引Pipeline"""
def __init__(self, config: RAGConfig):
self.config = config
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=config.chunk_size,
chunk_overlap=config.chunk_overlap,
separators=["\\n\\n", "\\n", "。", "!", "?", ".", "!", "?", " ", ""],
length_function=len
)
self.embeddings = HuggingFaceEmbeddings(
model_name=config.embedding_model,
model_kwargs={'device': 'cuda'},
encode_kwargs={'normalize_embeddings': True}
)
def parse_document(self, file_path: str) –> str:
"""文档解析(简化版,实际可用Unstructured)"""
# 根据扩展名选择解析器
ext = os.path.splitext(file_path)[1].lower()
if ext == '.pdf':
# 使用PyPDF或Unstructured
from langchain.document_loaders import PyPDFLoader
loader = PyPDFLoader(file_path)
docs = loader.load()
return "\\n\\n".join([d.page_content for d in docs])
elif ext in ['.docx', '.doc']:
from docx import Document as DocxDocument
doc = DocxDocument(file_path)
return "\\n".join([p.text for p in doc.paragraphs])
elif ext in ['.md', '.txt']:
with open(file_path, 'r', encoding='utf-8') as f:
return f.read()
else:
raise ValueError(f"不支持的文件类型: {ext}")
def extract_structure(self, text: str, file_path: str) –> List[Dict]:
"""提取文档结构信息"""
# 简化实现:基于标题模式识别
import re
lines = text.split('\\n')
sections = []
current_section = {"title": "正文", "level": 0, "content": []}
for line in lines:
# 识别Markdown标题
if match := re.match(r'^(#{1,6})\\s+(.+)$', line):
level = len(match.group(1))
title = match.group(2)
# 保存上一个section
if current_section["content"]:
sections.append(current_section)
current_section = {
"title": title,
"level": level,
"content": []
}
else:
current_section["content"].append(line)
sections.append(current_section)
# 转换为chunk列表
chunks = []
for sec in sections:
content = "\\n".join(sec["content"]).strip()
if len(content) < 50: # 过滤太短的片段
continue
chunks.append({
"content": content,
"metadata": {
"source": os.path.basename(file_path),
"section": sec["title"],
"level": sec["level"],
"doc_type": self._detect_doc_type(content)
}
})
return chunks
def _detect_doc_type(self, content: str) –> str:
"""检测内容类型"""
if '|' in content and '\\n' in content:
lines = content.split('\\n')
if all('|' in l for l in lines[:3]):
return "table"
if '```' in content:
return "code"
if re.search(r'^\\d+\\.\\s', content, re.MULTILINE):
return "list"
return "text"
def create_chunks(self, raw_text: str, structure_info: List[Dict]) –> List[Document]:
"""智能切分,保持语义边界"""
documents = []
for item in structure_info:
content = item["content"]
metadata = item["metadata"]
# 如果内容本身不长,直接作为一个chunk
if len(content) <= self.config.chunk_size * 1.5:
documents.append(Document(
page_content=content,
metadata=metadata
))
else:
# 需要进一步切分
sub_chunks = self.text_splitter.split_text(content)
for i, chunk in enumerate(sub_chunks):
chunk_meta = metadata.copy()
chunk_meta["chunk_index"] = i
chunk_meta["total_chunks"] = len(sub_chunks)
documents.append(Document(
page_content=chunk,
metadata=chunk_meta
))
return documents
def build_index(self, file_paths: List[str], collection_name: str = "rag_docs"):
"""构建向量索引"""
all_documents = []
for fp in file_paths:
print(f"处理文档: {fp}")
raw_text = self.parse_document(fp)
structure = self.extract_structure(raw_text, fp)
chunks = self.create_chunks(raw_text, structure)
all_documents.extend(chunks)
print(f" 生成 {len(chunks)} 个chunks")
# 存入向量数据库
vector_store = Milvus.from_documents(
documents=all_documents,
embedding=self.embeddings,
collection_name=collection_name,
connection_args={"host": "localhost", "port": "19530"}
)
print(f"索引构建完成,共 {len(all_documents)} 个文档片段")
return vector_store
# ============ 第二阶段:检索 ============
class RetrievalPipeline:
"""检索Pipeline:改写 + 多路召回 + 重排序"""
def __init__(self, config: RAGConfig, vector_store: Milvus):
self.config = config
self.vector_store = vector_store
# 初始化重排序模型
self.reranker = CrossEncoder(
config.reranker_model,
max_length=512
)
# 初始化查询改写LLM(可用轻量级模型)
self.rewrite_llm = ChatOpenAI(
model="gpt-3.5-turbo",
temperature=0.1
)
def rewrite_query(self, original_query: str, chat_history: List = None) –> Dict[str, str]:
"""查询改写与扩展"""
rewrite_prompt = f"""分析用户查询,进行改写优化。
用户原始查询:{original_query}
请输出JSON格式:
{{
"rewritten_query": "消除歧义后的标准查询",
"search_keywords": "关键词列表,用|分隔",
"query_type": "流程咨询|政策解释|故障排查|其他",
"needs_historical": true/false,
"clarification": "如查询不明确,需要澄清的问题,否则留空"
}}
注意:保持原意,但使其更适合检索系统理解。"""
response = self.rewrite_llm.predict(rewrite_prompt)
try:
import json
result = json.loads(response)
except:
# 解析失败,使用原始查询
result = {
"rewritten_query": original_query,
"search_keywords": original_query,
"query_type": "其他",
"needs_historical": False,
"clarification": ""
}
return result
def hybrid_search(self, query_info: Dict, top_k: int = None) –> List[Document]:
"""混合检索:向量 + 关键词 + 结构化过滤"""
if top_k is None:
top_k = self.config.vector_top_k
rewritten = query_info["rewritten_query"]
keywords = query_info["search_keywords"].split("|")
results = []
# 路1:向量检索
vector_results = self.vector_store.similarity_search(
rewritten,
k=top_k
)
results.extend([(doc, "vector", i+1) for i, doc in enumerate(vector_results)])
# 路2:关键词检索(简化实现,可用BM25)
# 实际可用whoosh、elasticsearch等
keyword_query = " OR ".join([f'"{k.strip()}"' for k in keywords if k.strip()])
# 这里简化处理,实际应接入专用搜索引擎
# 路3:结构化过滤(根据query_type)
if query_info["query_type"] == "政策解释":
# 优先搜索政策相关章节
filtered = self.vector_store.similarity_search(
rewritten,
k=top_k,
filter={"doc_type": {"$in": ["text", "table"]}}
)
results.extend([(doc, "filtered", i+1) for i, doc in enumerate(filtered)])
# 去重(同一文档可能多路召回)
seen = set()
unique_results = []
for doc, source, rank in results:
doc_id = f"{doc.metadata.get('source')}:{doc.metadata.get('chunk_index', 0)}"
if doc_id not in seen:
seen.add(doc_id)
unique_results.append((doc, source, rank))
return [r[0] for r in unique_results[:top_k*2]] # 留足候选给重排
def rerank(self, query: str, candidates: List[Document]) –> List[tuple]:
"""Cross-Encoder重排序"""
if not candidates:
return []
# 构造query-doc对
pairs = [[query, doc.page_content] for doc in candidates]
# 预测相关性分数
scores = self.reranker.predict(pairs)
# 打包结果
ranked = list(zip(candidates, scores))
ranked.sort(key=lambda x: x[1], reverse=True)
return ranked[:self.config.rerank_top_k]
def retrieve(self, query: str, chat_history: List = None) –> Dict:
"""完整检索流程"""
# 1. 查询改写
query_info = self.rewrite_query(query, chat_history)
# 2. 多路召回
candidates = self.hybrid_search(query_info)
# 3. 精排
ranked_results = self.rerank(query_info["rewritten_query"], candidates)
return {
"original_query": query,
"rewritten": query_info,
"retrieved_chunks": [
{
"content": doc.page_content,
"metadata": doc.metadata,
"relevance_score": float(score)
}
for doc, score in ranked_results
],
"total_candidates": len(candidates)
}
# ============ 第三阶段:生成 ============
class GenerationPipeline:
"""生成Pipeline:上下文组装 + Prompt工程 + 结果输出"""
def __init__(self, config: RAGConfig):
self.config = config
self.llm = ChatOpenAI(
model=config.llm_model,
temperature=config.temperature,
max_tokens=1500
)
self.prompt_template = ChatPromptTemplate.from_messages([
("system", """你是企业知识助手,基于参考文档回答问题。
核心原则:
1. 忠实性:只使用参考文档信息,绝不编造
2. 准确性:数字、日期、流程步骤必须精确
3. 可追溯:每个关键事实标注来源[来源: 文档名, 章节]
4. 诚实性:信息不足时明确说明,不猜测
回答结构:
– 直接回答(2-3句话总结)
– 详细说明(分点阐述,带引用)
– 相关来源(列出参考文档)"""),
("human", """参考文档:
{context}
用户问题:{question}
请用中文回答:""")
])
def build_context(self, retrieved_chunks: List[Dict]) –> str:
"""智能组装上下文"""
# 按相关性排序(已排好)
sorted_chunks = sorted(
retrieved_chunks,
key=lambda x: x["relevance_score"],
reverse=True
)
context_parts = []
total_tokens = 0
for chunk in sorted_chunks:
# 估算token数(中文约1token/字,英文约0.3token/词)
content = chunk["content"]
estimated_tokens = len(content) # 简化估算
if total_tokens + estimated_tokens > self.config.context_max_tokens:
# 尝试摘要或截断
remaining = self.config.context_max_tokens – total_tokens
if remaining > 200:
truncated = content[:remaining*2] + "…"
content = truncated
else:
break
# 格式化带来源标记
source = chunk["metadata"].get("source", "未知")
section = chunk["metadata"].get("section", "正文")
formatted = f"""[来源: {source}, {section}]
{content}
—"""
context_parts.append(formatted)
total_tokens += estimated_tokens
return "\\n".join(context_parts)
def generate(self, query: str, retrieval_result: Dict) –> Dict:
"""生成回答"""
# 组装上下文
context = self.build_context(retrieval_result["retrieved_chunks"])
# 构建Prompt
messages = self.prompt_template.format_messages(
context=context,
question=query
)
# 调用LLM
response = self.llm.predict_messages(messages)
answer = response.content
# 后处理:提取引用、验证
processed = self.post_process(answer, retrieval_result["retrieved_chunks"])
return {
"answer": answer,
"processed_answer": processed["formatted"],
"citations": processed["citations"],
"confidence": processed["confidence"],
"context_used": len(context),
"latency_ms": processed.get("latency_ms", 0)
}
def post_process(self, answer: str, source_chunks: List[Dict]) –> Dict:
"""后处理:格式化、溯源、置信度评估"""
import re
# 提取引用标记
citation_pattern = r'\\[来源:\\s*([^,\\]]+),\\s*([^\\]]+)\\]'
citations_found = re.findall(citation_pattern, answer)
# 验证引用
verified_citations = []
for doc_name, section in citations_found:
# 在source_chunks中查找
found = any(
doc_name in c["metadata"].get("source", "") and
section in c["metadata"].get("section", "")
for c in source_chunks
)
verified_citations.append({
"doc": doc_name,
"section": section,
"verified": found
})
# 计算置信度
if not citations_found:
confidence = 0.5 # 无引用,置信度中等
else:
verified_ratio = sum(1 for c in verified_citations if c["verified"]) / len(citations_found)
confidence = 0.3 + 0.7 * verified_ratio # 基础分0.3,引用准确加分
# 格式化输出
formatted = f"""{answer}
—
**参考来源**
"""
for i, c in enumerate(verified_citations[:5], 1):
status = "✓" if c["verified"] else "?"
formatted += f"{i}. [{status}] {c['doc']} – {c['section']}\\n"
return {
"formatted": formatted,
"citations": verified_citations,
"confidence": round(confidence, 2),
"citation_count": len(citations_found),
"verified_count": sum(1 for c in verified_citations if c["verified"])
}
# ============ 主流程:RAG系统 ============
class RAGSystem:
"""完整的RAG系统"""
def __init__(self, config: RAGConfig = None):
self.config = config or RAGConfig()
self.indexing = None
self.retrieval = None
self.generation = GenerationPipeline(self.config)
self.vector_store = None
def build_index(self, documents: List[str]):
"""构建索引"""
self.indexing = IndexingPipeline(self.config)
self.vector_store = self.indexing.build_index(documents)
self.retrieval = RetrievalPipeline(self.config, self.vector_store)
print("索引构建完成,系统就绪")
def query(self, question: str, chat_history: List = None) –> Dict:
"""端到端查询"""
if not self.retrieval:
raise RuntimeError("请先构建索引")
# 检索阶段
retrieval_result = self.retrieval.retrieve(question, chat_history)
# 生成阶段
generation_result = self.generation.generate(question, retrieval_result)
return {
"question": question,
"retrieval": retrieval_result,
"generation": generation_result,
"final_answer": generation_result["processed_answer"]
}
# ============ 使用示例 ============
if __name__ == "__main__":
# 初始化系统
config = RAGConfig(
chunk_size=400,
chunk_overlap=80,
vector_top_k=12,
rerank_top_k=4
)
rag = RAGSystem(config)
# 构建索引(示例文档)
rag.build_index([
"./docs/员工手册2024.pdf",
"./docs/财务报销制度.docx",
"./docs/IT运维手册.md"
])
# 查询
result = rag.query("工作满3年的员工有多少天年假?")
print(result["final_answer"])
关键设计说明
| 文档解析 | 按类型选择解析器,保留结构 | Unstructured(更强大) |
| Chunk切分 | 语义边界优先 + 智能重叠 | 语义切分模型(如LangChain SemanticChunker) |
| 向量化 | BGE-M3(多语言、长文本) | OpenAI text-embedding-3, E5系列 |
| 向量数据库 | Milvus(高性能、可扩展) | Pinecone, Weaviate, Qdrant |
| 重排序 | Cross-Encoder精排 | LLM作为重排序器(更慢但更准) |
| 大模型 | GPT-4(平衡能力与成本) | Claude, 本地部署模型 |
小结
实战环节的核心是——“先跑通,再优化”。这个实现覆盖了完整Pipeline,但每个环节都有提升空间。建议先基于自己的数据验证效果,再针对性调优。记住:没有银弹,只有适合当前场景的trade-off。
六、常见陷阱与避坑指南 —— 前人踩过的雷你别踩
点题:RAG路上全是坑,这份地图帮你绕
最后总结我见过的典型踩坑场景,帮你少走弯路。
#mermaid-svg-ebNzL7WIwR1hu8pb{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#1A1A2E;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ebNzL7WIwR1hu8pb .error-icon{fill:#96CEB4;}#mermaid-svg-ebNzL7WIwR1hu8pb .error-text{fill:#69314b;stroke:#69314b;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ebNzL7WIwR1hu8pb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ebNzL7WIwR1hu8pb .marker{fill:#4ECDC4;stroke:#4ECDC4;}#mermaid-svg-ebNzL7WIwR1hu8pb .marker.cross{stroke:#4ECDC4;}#mermaid-svg-ebNzL7WIwR1hu8pb svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ebNzL7WIwR1hu8pb p{margin:0;}#mermaid-svg-ebNzL7WIwR1hu8pb .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#1A1A2E;}#mermaid-svg-ebNzL7WIwR1hu8pb .cluster-label text{fill:#69314b;}#mermaid-svg-ebNzL7WIwR1hu8pb .cluster-label span{color:#69314b;}#mermaid-svg-ebNzL7WIwR1hu8pb .cluster-label span p{background-color:transparent;}#mermaid-svg-ebNzL7WIwR1hu8pb .label text,#mermaid-svg-ebNzL7WIwR1hu8pb span{fill:#1A1A2E;color:#1A1A2E;}#mermaid-svg-ebNzL7WIwR1hu8pb .node rect,#mermaid-svg-ebNzL7WIwR1hu8pb .node circle,#mermaid-svg-ebNzL7WIwR1hu8pb .node ellipse,#mermaid-svg-ebNzL7WIwR1hu8pb .node polygon,#mermaid-svg-ebNzL7WIwR1hu8pb .node path{fill:#FF6B6B;stroke:#FF6B6B;stroke-width:1px;}#mermaid-svg-ebNzL7WIwR1hu8pb .rough-node .label text,#mermaid-svg-ebNzL7WIwR1hu8pb .node .label text,#mermaid-svg-ebNzL7WIwR1hu8pb .image-shape .label,#mermaid-svg-ebNzL7WIwR1hu8pb .icon-shape .label{text-anchor:middle;}#mermaid-svg-ebNzL7WIwR1hu8pb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ebNzL7WIwR1hu8pb .rough-node .label,#mermaid-svg-ebNzL7WIwR1hu8pb .node .label,#mermaid-svg-ebNzL7WIwR1hu8pb .image-shape .label,#mermaid-svg-ebNzL7WIwR1hu8pb .icon-shape .label{text-align:center;}#mermaid-svg-ebNzL7WIwR1hu8pb .node.clickable{cursor:pointer;}#mermaid-svg-ebNzL7WIwR1hu8pb .root .anchor path{fill:#4ECDC4!important;stroke-width:0;stroke:#4ECDC4;}#mermaid-svg-ebNzL7WIwR1hu8pb .arrowheadPath{fill:#080603;}#mermaid-svg-ebNzL7WIwR1hu8pb .edgePath .path{stroke:#4ECDC4;stroke-width:2.0px;}#mermaid-svg-ebNzL7WIwR1hu8pb .flowchart-link{stroke:#4ECDC4;fill:none;}#mermaid-svg-ebNzL7WIwR1hu8pb .edgeLabel{background-color:#45B7D1;text-align:center;}#mermaid-svg-ebNzL7WIwR1hu8pb .edgeLabel p{background-color:#45B7D1;}#mermaid-svg-ebNzL7WIwR1hu8pb .edgeLabel rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-ebNzL7WIwR1hu8pb .labelBkg{background-color:rgba(69, 183, 209, 0.5);}#mermaid-svg-ebNzL7WIwR1hu8pb .cluster rect{fill:#96CEB4;stroke:hsl(152.1428571429, 0%, 59.8039215686%);stroke-width:1px;}#mermaid-svg-ebNzL7WIwR1hu8pb .cluster text{fill:#69314b;}#mermaid-svg-ebNzL7WIwR1hu8pb .cluster span{color:#69314b;}#mermaid-svg-ebNzL7WIwR1hu8pb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:#96CEB4;border:1px solid hsl(152.1428571429, 0%, 59.8039215686%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ebNzL7WIwR1hu8pb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#1A1A2E;}#mermaid-svg-ebNzL7WIwR1hu8pb rect.text{fill:none;stroke-width:0;}#mermaid-svg-ebNzL7WIwR1hu8pb .icon-shape,#mermaid-svg-ebNzL7WIwR1hu8pb .image-shape{background-color:#45B7D1;text-align:center;}#mermaid-svg-ebNzL7WIwR1hu8pb .icon-shape p,#mermaid-svg-ebNzL7WIwR1hu8pb .image-shape p{background-color:#45B7D1;padding:2px;}#mermaid-svg-ebNzL7WIwR1hu8pb .icon-shape .label rect,#mermaid-svg-ebNzL7WIwR1hu8pb .image-shape .label rect{opacity:0.5;background-color:#45B7D1;fill:#45B7D1;}#mermaid-svg-ebNzL7WIwR1hu8pb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ebNzL7WIwR1hu8pb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ebNzL7WIwR1hu8pb :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
RAG常见陷阱
索引阶段陷阱
Chunk太小→语义断裂
忽视版本→新旧混淆
元数据缺失→无法过滤
检索阶段陷阱
纯向量→专有名词失效
Top-K固定→召回不足/过剩
无重排→精度天花板低
生成阶段陷阱
Prompt太长→注意力分散
无引用约束→幻觉频发
忽视延迟→用户体验差
系统性陷阱
无评估→盲目优化
数据泄露→测试集污染
忽视安全→敏感信息暴露
陷阱详解与避坑方案
陷阱1:Chunk切分过细,语义支离破碎
症状:用户问"年假申请流程",回答只说了"第一步填表",后面步骤丢失
原因:Chunk 200字,流程被切成5段,只召回前2段
避坑:关键流程类内容,优先保持完整性,宁可Chunk稍大
陷阱2:忽视文档版本管理
症状:2023年和2024年政策混用,回答自相矛盾
原因:索引时未标记版本,检索无法区分
避坑:metadata必含version字段,检索时优先最新版本
陷阱3:纯向量检索搞不定专有名词
症状:查"GPT-4 Turbo",召回"GPT-4"、"GPT-3.5"
原因:向量相似但非目标
避坑:专有名词必须走关键词精确匹配,或加同义词扩展
陷阱4:Prompt塞满上下文,模型"失忆"
症状:回答前半部分准确,后半部分胡编
原因:长上下文中间部分被模型忽略
避坑:关键信息放Prompt首尾,中间放次要细节;或分层生成
陷阱5:没有端到端评估,优化靠猜
症状:改了Chunk策略,不知道整体变好变坏
原因:只看单环节指标,忽视最终回答质量
避坑:建立完整评估集,每次改动跑全链路测试
陷阱6:测试集泄露到训练/索引中
症状:测试效果奇好,上线后拉胯
原因:测试文档提前进了索引,或评估问题与训练数据重复
避坑:严格划分数据,测试集文档索引前隔离
避坑检查清单
上线前逐项确认:
- Chunk策略经过人工抽样检验,关键内容未被切断
- 元数据完整,支持按时间/来源/类型过滤
- 混合检索(向量+关键词)已启用
- 重排序模型已加载,非直接返回向量Top-K
- Prompt长度控制在模型有效上下文范围内
- 强制引用约束已加入系统指令
- 端到端评估集已建立,基线分数已记录
- P99延迟满足产品要求(通常<3秒)
- 敏感信息过滤机制已部署
小结
避坑的核心是——“敬畏复杂性,保持怀疑”。RAG看似简单,实则处处是细节魔鬼。多测试、多观察bad case、多问自己"这个环节失败会怎样",才能构建可靠的系统。
写在最后
聊到这里,RAG检索Pipeline的三大核心阶段——索引、检索、生成,你应该有了系统性的理解。
说实话,RAG这个领域发展太快了。半年前的主流方案,现在可能已经被迭代。但万变不离其宗:高质量的索引是基础,精准的检索是桥梁,可控的生成是目标。 抓住这三根主线,新技术出来你也能快速判断价值。
我见过太多同学,一上来就追最新的模型、最炫的框架,结果基础Pipeline漏洞百出。也见过踏实打磨每个环节的团队,用相对"朴素"的技术栈,做出体验惊艳的产品。
编程之路不易,但每一步成长都算数。RAG的学习曲线确实有点陡,你可能会在Chunk大小上纠结很久,会在检索召回率和精确率之间反复权衡,会被某个bad case搞得怀疑人生——这都很正常。
保持好奇,持续学习,多动手实验,多分析case。当你能对着一个错误回答,快速定位是索引、检索还是生成环节的问题时,你就真正"通关"了。
Agent的时代才刚开始,RAG作为知识增强的核心技术,值得你投入时间深耕。期待看到你用这些知识,做出有用的产品。
咱们下篇见!
关注私信备注:“资料代找获取”,全网计算机学习资料代找:例如: 《课程:2026 年多模态大模型实战训练营》 《课程:AI 大模型工程师系统课程 (22 章完整版 持续更新)》 《课程:AI 大模型系统实战课第四期 (2026 年开课 持续更新)》 《课程:2026 年 AGI 大模型系统课 23 期》 《课程:2026 年 AGI 大模型系统课 21 期》 《课程:AI 大模型实战课 8 期 (2026 年 2 月最新完结版)》 《课程:AI 大模型系统实战课三期》 《课程:AI 大模型系统课程 (2026 年 2 月开课 持续更新)》 《课程:AI 大模型全阶课程 (2025 年 12 月开课 2026 年 6 月结课)》 《课程:AI 大模型工程师全阶课程 (2025 年 10 月开课 2026 年 4 月结课)》 《课程:2026 年最新大模型 Agent 开发系统课 (持续更新)》 《课程:LLM 多模态视觉大模型系统课》 《课程:大模型 AI 应用开发企业级项目实战课 (2026 年 1 月开课)》 《课程:大模型智能体线上速成班 V2.0》 《课程:Java+AI 大模型智能应用开发全阶课》 《课程:Python+AI 大模型实战视频教程》 《书籍:软件工程 3.0: 大模型驱动的研发新范式.pdf》 《课程:人工智能大模型系统课 (2026 年 1 月底完结版)》 《课程:AI 大模型零基础到商业实战全栈课第五期》 《课程:Vue3.5+Electron + 大模型跨平台 AI 桌面聊天应用实战 (2025)》 《课程:AI 大模型实战训练营 从入门到实战轻松上手》 《课程:2026 年 AI 大模型 RAG 与 Agent 智能体项目实战开发课》 《课程:大模型训练营配套补充资料》




