RAG进阶实战:从检索优化到Agentic架构的专栏设计

发布时间:2026/10/5 8:47:33
RAG进阶实战:从检索优化到Agentic架构的专栏设计
1. 为什么我要做这个RAG进阶专栏过去大半年我几乎把市面上能跑通的RAG方案都折腾了一遍。从最朴素的“文档切块向量检索拼Prompt”三件套到后来引入重排序、混合检索、知识图谱增强再到把Agent和RAG揉在一起做多轮工具调用踩过的坑比写过的代码还多。这个专栏的策划本质上是我自己学习路径的一次系统化整理——把那些散落在笔记、Issue、聊天记录里的碎片经验重新组织成一条能让人少走弯路的进阶路线。RAG这个词现在热得发烫但真正落地过的人都知道Demo和生产的距离大概有从地球到月球那么远。检索召回率上不去、切块策略一刀切、向量库选型纠结、多路召回融合困难、评测体系缺失、Agent调用时上下文爆炸……每一个问题单拎出来都够写一篇长文。而市面上大部分教程还停留在“用LangChain搭一个问答机器人”的阶段对进阶问题要么一笔带过要么语焉不详。这个专栏面向的是已经跑通过基础RAG、但被各种瓶颈卡住的开发者。我会围绕检索质量优化、向量库选型与调参、知识图谱融合、Agentic RAG架构、评测与可观测性这几条主线展开每一篇都尽量给出可复现的代码、可量化的对比、以及我在实际项目中验证过的取舍逻辑。不会只讲“怎么做”更会讲“为什么这么做”以及“什么情况下别这么做”。专栏的定位是“进阶实战”所以默认读者已经了解Embedding、余弦相似度、Prompt Engineering这些基础概念。如果你还没跑通过一个最小的RAG流程建议先去找个零基础教程把链路跑通再回来读这个专栏收获会更大。接下来我会从整体设计思路开始拆解把专栏的骨架和背后的考量讲清楚。2. 专栏整体设计与内容架构拆解2.1 从“能跑”到“好用”的能力跃迁路径基础RAG的链路很短文档加载→切块→向量化→存入向量库→查询向量化→相似度检索→拼Prompt→LLM生成。这条链路能跑通但跑通和好用之间隔着好几层能力跃迁。第一层跃迁是检索质量。基础方案里切块大小固定、检索只走向量相似度、TopK拍脑袋定结果就是该召回的没召回、不该召回的混进来。进阶要解决的是切块策略怎么根据文档类型动态调整、混合检索怎么融合稀疏和稠密信号、重排序模型怎么选和怎么用。第二层跃迁是知识组织。纯向量检索把文档当成扁平的文本块丢失了结构信息。当用户问“A和B的关系是什么”时向量检索可能召回两段分别提到A和B的文本但无法直接回答关系。这时候就需要引入知识图谱或结构化知识库把实体和关系显式建模和向量检索形成互补。第三层跃迁是系统架构。单轮RAG只能回答简单事实性问题面对多跳推理、需要外部工具、需要多轮澄清的场景就力不从心。Agentic RAG把检索作为Agent的一个工具让模型自己决定什么时候检索、检索什么、要不要追问这才是更接近生产需求的形态。第四层跃迁是评测与迭代。没有评测就没有优化方向。进阶玩家必须建立自己的评测集和指标看板用LLM as Judge做自动化评估用A/B测试验证每次改动的实际收益。专栏的章节就是按照这四层跃迁来组织的每一层都会拆成若干篇从原理到实操到避坑尽量讲透。2.2 技术选型的取舍逻辑与版本锁定策略RAG生态的工具链更新极快LangChain、LlamaIndex、Haystack这些框架几个月就一个大版本向量库也是百花齐放。专栏如果追新读者跟着跑很容易因为版本差异翻车如果锁死旧版又很快过时。我的策略是核心原理讲透框架代码给两个版本稳定版最新版向量库选型给对比矩阵而非单一推荐。具体来说向量库这块我会重点覆盖pgvector和Milvus。pgvector适合已经在用PostgreSQL、数据量在百万级以内、不想引入新组件的场景优势是运维简单、事务一致性好Milvus适合亿级向量、需要分布式和高可用、对检索性能有极致要求的场景但运维复杂度高standalone模式虽然能单机跑生产环境还是建议集群。专栏里会给出两者的性能对比测试和迁移方案。框架层面LangChain生态最全但抽象层厚、调试困难LlamaIndex在索引和检索抽象上更专注Haystack偏生产级Pipeline。我会以LangChain为主讲因为社区资源最多但关键环节会给出不依赖框架的原生实现方便读者理解底层。Embedding模型的选择也是重点。OpenAI的text-embedding-3系列效果好但成本高且有网络依赖开源的BGE、M3E、GTE系列在中文场景表现不错本地部署用Ollama跑nomic-embed-text也很方便。专栏会给出不同模型在中文检索任务上的对比数据以及维度、归一化、指令前缀这些细节对检索效果的影响。2.3 读者画像与前置知识清单这个专栏不是零基础教程。我假设读者已经用Python写过至少一个LLM应用知道怎么调API理解Embedding的基本概念知道余弦相似度怎么算跑通过一个最小的RAG流程哪怕只是复制粘贴的会用Docker和基本的Linux命令因为向量库部署绕不开对Prompt Engineering有基本认知知道Few-shot和CoT是什么如果你还不满足这些建议先补一下。专栏里不会花篇幅解释什么是Token、什么是向量这些基础概念默认你已经掌握。但如果你已经满足上述条件专栏里的进阶内容应该能让你少走很多弯路。3. 核心细节解析与实操要点3.1 切块策略别再用固定大小一刀切了切块是RAG里最容易被忽视但影响巨大的环节。我见过太多项目用RecursiveCharacterTextSplitter配个chunk_size1000、overlap200就完事结果检索效果一塌糊涂。固定大小切块的问题在于它假设所有文档的结构和语义密度是一样的。但实际文档里一段代码和一段叙述性文字的最佳切块大小完全不同一个表格和一段列表也不一样。更糟糕的是固定切块经常把一句完整的话从中间切断导致Embedding语义不完整。进阶做法是按文档结构切块。Markdown按标题层级切HTML按DOM树切PDF按段落和表格切代码按函数和类切。LangChain的MarkdownHeaderTextSplitter和LlamaIndex的SentenceSplitter都支持这种结构化切块。如果文档没有明显结构可以用语义切块——先按句子切然后计算相邻句子的Embedding相似度相似度低于阈值的地方作为切分点。还有一个容易被忽略的点是父子块索引。检索时用小块比如256 token做向量匹配保证精度返回时用大块比如1024 token给LLM保证上下文完整。LlamaIndex的ParentDocumentRetriever和LangChain的ParentDocumentRetriever都实现了这个模式。实测下来父子块索引在问答任务上的召回率和答案质量都比单层切块有明显提升。注意切块大小没有万能值。中文场景下256-512 token的小块适合精确检索1024-2048 token的大块适合做上下文。建议用你的实际数据做A/B测试别抄别人的参数。3.2 混合检索与重排序让召回率再上一个台阶纯向量检索的短板很明显对关键词精确匹配不敏感、对罕见实体召回差、对否定和数值条件处理弱。比如用户问“2023年Q4的营收是多少”向量检索可能召回一堆讲营收的段落但未必能精确匹配到“2023年Q4”这个时间条件。混合检索的思路是向量检索关键词检索BM25双路召回然后融合排序。融合算法常用RRFReciprocal Rank Fusion它不需要归一化分数直接按排名融合简单且鲁棒。Milvus 2.4之后原生支持稀疏向量和稠密向量的混合检索pgvector则需要自己用tsvector做全文检索再融合。重排序是第二道保险。召回阶段追求高召回率可能返回几十上百个候选重排序阶段用Cross-Encoder模型对候选逐一打分追求高精度。常用的重排序模型有BGE-Reranker、Cohere Rerank、Jina Reranker。实测下来BGE-Reranker-v2-m3在中文场景表现很好而且可以本地部署成本可控。重排序的代价是延迟。Cross-Encoder要对每个候选做一次前向计算候选多了延迟线性增长。我的经验是召回Top50重排序后取Top5延迟增加200-500ms但答案质量提升明显。如果延迟敏感可以用ColBERT这类后期交互模型做折中。3.3 向量库选型pgvector还是Milvus这是被问得最多的问题之一。我的答案永远是看你的数据量、运维能力和查询模式。pgvector的优势在于它就是一个PostgreSQL扩展你不需要引入新的数据库组件。如果你的业务数据已经在PostgreSQL里用pgvector可以做到向量和业务数据在同一事务里操作JOIN查询也很方便。百万级向量、QPS在几十到几百的场景pgvector完全够用。HNSW索引建好之后查询延迟可以稳定在10ms以内。Milvus的优势在于它是专为向量检索设计的分布式系统。亿级向量、高并发、需要水平扩展的场景Milvus是更合适的选择。它支持多种索引类型IVF、HNSW、DiskANN支持标量过滤和向量检索的混合查询支持多租户。但Milvus的运维复杂度明显更高standalone模式虽然能单机跑但生产环境要考虑etcd、MinIO、Pulsar这些依赖组件。专栏里我会给出一个选型决策表从数据量、QPS、延迟要求、运维成本、生态集成几个维度对比。还会给出pgvector到Milvus的迁移脚本方便业务增长后平滑切换。维度pgvectorMilvus数据量百万级以内亿级运维复杂度低复用PG高多组件查询延迟10ms级10ms级混合查询SQL原生标量过滤向量分布式依赖PG原生支持生态集成PG生态独立生态3.4 知识图谱融合什么时候需要Ontology RAG纯向量RAG在处理关系型问题时很吃力。用户问“张三和李四是什么关系”向量检索可能召回分别提到张三和李四的段落但无法直接给出关系。这时候知识图谱就派上用场了。Ontology RAG的思路是先用LLM从文档里抽取实体和关系构建知识图谱查询时一路走向量检索一路走图谱查询两路结果融合后给LLM。图谱查询可以处理多跳关系、实体消歧、关系推理这些向量检索不擅长的任务。但知识图谱不是银弹。构建和维护图谱的成本很高实体抽取的准确率直接影响图谱质量图谱schema的设计也需要领域知识。我的建议是只有当你的业务里关系型查询占比超过30%时才值得引入图谱。否则用混合检索重排序先把向量检索的潜力榨干。如果决定上图谱Neo4j是最成熟的选择和LangChain、LlamaIndex都有集成。轻量级场景也可以用NetworkX在内存里建图但持久化和查询能力有限。4. 实操过程与核心环节实现4.1 用Ollamapgvector搭一个本地可跑的进阶RAG这一节我给出一个完整的、可复现的本地RAG实现用Ollama跑LLM和Embedding用pgvector做向量存储包含混合检索和重排序。所有代码都可以直接复制运行。首先用Docker起一个带pgvector的PostgreSQLdocker run -d --name pgvector \ -e POSTGRES_PASSWORDrag123 \ -e POSTGRES_DBragdb \ -p 5432:5432 \ pgvector/pgvector:pg16然后安装Python依赖pip install ollama psycopg2-binary pgvector numpy rank_bm25初始化数据库表import psycopg2 from pgvector.psycopg2 import register_vector conn psycopg2.connect( hostlocalhost, port5432, dbnameragdb, userpostgres, passwordrag123 ) register_vector(conn) cur conn.cursor() cur.execute(CREATE EXTENSION IF NOT EXISTS vector) cur.execute( CREATE TABLE IF NOT EXISTS documents ( id SERIAL PRIMARY KEY, content TEXT, embedding vector(768), tsv tsvector ) ) cur.execute(CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)) cur.execute(CREATE INDEX ON documents USING gin (tsv)) conn.commit()用Ollama拉取模型ollama pull nomic-embed-text ollama pull qwen2.5:7b文档入库和混合检索的核心逻辑import ollama import numpy as np from rank_bm25 import BM25Okapi def embed(text): resp ollama.embeddings(modelnomic-embed-text, prompttext) return np.array(resp[embedding]) def insert_doc(content): emb embed(content) cur.execute( INSERT INTO documents (content, embedding, tsv) VALUES (%s, %s, to_tsvector(simple, %s)), (content, emb, content) ) conn.commit() def hybrid_search(query, top_k5): # 向量检索 q_emb embed(query) cur.execute( SELECT id, content, 1 - (embedding %s) AS score FROM documents ORDER BY embedding %s LIMIT %s, (q_emb, q_emb, top_k * 2) ) vec_results {row[0]: row[2] for row in cur.fetchall()} # 关键词检索 cur.execute( SELECT id, content, ts_rank(tsv, plainto_tsquery(simple, %s)) AS score FROM documents WHERE tsv plainto_tsquery(simple, %s) ORDER BY score DESC LIMIT %s, (query, query, top_k * 2) ) kw_results {row[0]: row[2] for row in cur.fetchall()} # RRF融合 fused {} for rank, doc_id in enumerate(sorted(vec_results, keyvec_results.get, reverseTrue)): fused[doc_id] fused.get(doc_id, 0) 1 / (60 rank 1) for rank, doc_id in enumerate(sorted(kw_results, keykw_results.get, reverseTrue)): fused[doc_id] fused.get(doc_id, 0) 1 / (60 rank 1) top_ids sorted(fused, keyfused.get, reverseTrue)[:top_k] cur.execute(SELECT id, content FROM documents WHERE id ANY(%s), (top_ids,)) return cur.fetchall()生成答案def rag_answer(query): docs hybrid_search(query, top_k5) context \n\n.join([d[1] for d in docs]) prompt f基于以下上下文回答问题。如果上下文没有相关信息直接说不知道。 上下文 {context} 问题{query} 答案 resp ollama.chat(modelqwen2.5:7b, messages[{role: user, content: prompt}]) return resp[message][content]这套代码跑通之后你就有了一个支持混合检索的本地RAG。接下来可以逐步加入重排序、父子块索引、查询改写这些进阶模块。4.2 重排序模块的接入与参数调优在上面的基础上接入BGE-Reranker。先安装依赖pip install FlagEmbedding重排序逻辑from FlagEmbedding import FlagReranker reranker FlagReranker(BAAI/bge-reranker-v2-m3, use_fp16True) def rerank(query, candidates, top_k3): pairs [[query, c[1]] for c in candidates] scores reranker.compute_score(pairs, normalizeTrue) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [r[0] for r in ranked[:top_k]]把hybrid_search的返回结果传给rerank再拼Prompt。实测下来重排序在中文问答任务上能把Top3命中率提升15-25个百分点代价是增加约300ms延迟。参数调优方面normalizeTrue让分数落在0-1之间方便设阈值use_fp16True在GPU上能提速近一倍候选数量建议是最终TopK的5-10倍太少重排序没意义太多延迟吃不消。4.3 查询改写与多路召回的实现用户的问题往往表述模糊直接拿去检索效果不好。查询改写用LLM把原始问题改写成多个不同角度的查询分别检索后融合结果。def rewrite_query(query): prompt f把下面的问题改写成3个不同角度的检索查询每行一个不要编号 问题{query} resp ollama.chat(modelqwen2.5:7b, messages[{role: user, content: prompt}]) return [q.strip() for q in resp[message][content].strip().split(\n) if q.strip()] def multi_query_search(query, top_k5): queries [query] rewrite_query(query) all_results {} for q in queries: for doc_id, content in hybrid_search(q, top_ktop_k): all_results[doc_id] content return list(all_results.items())[:top_k * 2]多路召回的结果去重后再走重排序能显著提升召回覆盖率。代价是LLM调用次数增加延迟上升。如果延迟敏感可以用小模型做查询改写或者缓存常见查询的改写结果。5. 常见问题与排查技巧实录5.1 检索效果差的排查清单检索效果差是最常见的问题但原因可能有很多层。我整理了一个排查清单按优先级从高到低排查项常见问题解决方法切块策略块太大或太小语义不完整按结构切块用父子块索引Embedding模型模型不适合中文/领域换BGE、GTE等中文模型检索方式纯向量检索召回不足加BM25混合检索重排序没有重排序或模型不合适接入BGE-Reranker查询表述用户问题模糊查询改写多路召回索引参数HNSW参数不合理调ef_search和M数据质量文档本身噪声大清洗数据去重去噪排查时建议从切块和Embedding开始这两项影响最大。切块可以用可视化工具把块打印出来人工检查Embedding可以拿几个典型查询看召回结果是否合理。5.2 向量库部署与连接的高频坑Milvus standalone模式在Mac上用Docker安装最常见的坑是内存不够。Milvus默认配置需要至少8GB内存Mac上Docker Desktop默认可能只分配了2GB导致启动失败。解决方法是调高Docker Desktop的内存限制到8GB以上。另一个坑是milvus_uri的配置。本地文件模式用./data/milvus.db但要注意路径权限和并发访问。如果多个进程同时访问同一个本地文件可能出问题。生产环境还是用服务端模式URI写成http://localhost:19530。pgvector的坑主要在索引上。HNSW索引建好之后查询时要设置hnsw.ef_search参数默认值40可能不够调到100-200能提升召回率但增加延迟。另外pgvector的向量维度必须和Embedding模型一致建表时写错维度后面改起来很麻烦。5.3 LLM调用失败的典型错误与处理llm request failed: provider rejected the request schema or tool payload这个错误我遇到过好几次通常是因为Prompt里包含了模型不支持的格式或者工具调用的JSON schema不合法。排查方法是把请求体打印出来逐字段检查。常见原因包括消息角色不对比如用了system但模型不支持、工具定义的参数类型不匹配、上下文超长被截断。Ollama本地调用时如果模型没拉取会报模型不存在如果显存不够会报OOM。Mac上Ollama用Metal加速内存不够时会自动降级到CPU速度会慢很多。建议7B模型至少16GB内存13B模型至少32GB。还有一个坑是Embedding模型的维度。nomic-embed-text是768维BGE-M3是1024维建表时维度写错会导致插入失败。建议在代码里加一个维度校验插入前先检查向量长度。5.4 Agentic RAG的并发与安全考量把RAG包装成Agent工具后并发问题就来了。Agent可能在一轮对话里多次调用检索工具每次调用都要查向量库QPS压力成倍增加。我的做法是在Agent层加一个检索结果缓存相同查询在短时间内直接返回缓存向量库层用连接池避免每次查询都新建连接如果QPS还是扛不住考虑用Milvus集群或者加一层Redis做结果缓存。Agent安全方面AgentPoison这类攻击通过污染知识库来操纵Agent行为值得警惕。防御措施包括知识库写入做权限控制检索结果做来源校验Agent输出做敏感词过滤。如果Agent有写操作权限一定要加人工确认环节。提示Agentic RAG的调试比单轮RAG复杂得多。建议先用LangSmith或LangFuse做全链路追踪把每次检索的查询、召回结果、重排序分数、最终Prompt都记录下来出问题时能快速定位是哪一环出了问题。6. 评测体系与持续迭代6.1 用LLM as Judge搭建自动化评测没有评测就没有优化。RAG的评测指标分两层检索层看召回率、精确率、MRR、NDCG生成层看答案相关性、忠实度、完整性。人工评测成本高用LLM as Judge做自动化评测是更实际的选择。具体做法是准备一个评测集每个样本包含问题、标准答案、相关文档ID。然后用LLM对生成答案打分评分维度包括答案是否基于上下文忠实度、是否回答了问题相关性、是否完整完整性。评分Prompt要给出明确的评分标准和示例减少LLM评分的随机性。def judge_answer(query, context, answer, reference): prompt f你是一个严格的评测员。根据以下标准给答案打分1-5分 问题{query} 上下文{context} 参考答案{reference} 待评答案{answer} 评分标准 5分完全正确基于上下文无幻觉 4分基本正确有小瑕疵 3分部分正确有遗漏或轻微幻觉 2分大部分错误 1分完全错误或严重幻觉 只输出分数不要解释。 resp ollama.chat(modelqwen2.5:7b, messages[{role: user, content: prompt}]) return int(resp[message][content].strip())评测集建议至少100条覆盖不同类型的问题事实型、关系型、多跳型、否定型。每次改动后跑一遍评测对比指标变化避免“感觉变好了”这种主观判断。6.2 可观测性把RAG的每一步都记录下来生产环境的RAG必须可观测。我推荐用LangFuse做全链路追踪它开源、可自部署、和LangChain/LlamaIndex集成好。每次查询记录原始问题、改写后的查询、召回的文档ID和分数、重排序后的顺序、最终Prompt、LLM输出、延迟、Token消耗。这些数据不仅能用来排查问题还能用来做数据飞轮把用户反馈好的问答对加入评测集把bad case分析后针对性优化。我自己的项目里每周会review一次bad case找出共性问题然后决定下一步优化方向。6.3 从评测结果到优化动作的闭环评测不是目的优化才是。拿到评测结果后按以下优先级行动如果检索召回率低先查切块和Embedding再查混合检索和重排序。如果生成忠实度低查Prompt里上下文是否太长导致LLM忽略中间部分Lost in the Middle现象可以试试把最相关的文档放在上下文开头和结尾。如果答案完整性差查召回文档是否覆盖了问题的所有方面可能需要多路召回或查询分解。每次只改一个变量改完跑评测确认有效再改下一个。我见过太多人一次性改五个地方结果效果变差了都不知道是哪个改动导致的。慢就是快在RAG优化上尤其如此。7. 专栏更新节奏与配套资源专栏计划更新20篇左右每周2-3篇持续两个月。每篇配套可运行的代码仓库代码会打Tag对应文章版本避免后续更新导致读者跑不通。代码仓库里还会包含评测集、Docker Compose文件、常见问题排查脚本。配套资源包括一个完整的本地RAG项目模板Ollamapgvector混合检索重排序评测一个Milvus集群部署的Docker Compose配置一个评测集构建指南以及一份RAG优化checklist。这些资源会随专栏逐步放出订阅后即可获取。我个人的体会是RAG的进阶没有捷径就是不断踩坑、不断评测、不断迭代。这个专栏能帮你少踩一些我已经踩过的坑但最终的效果还是取决于你对业务数据的理解和持续优化的耐心。希望这个专栏能成为你RAG进阶路上的一个靠谱参考。