☰
RAG进阶实战:从检索优化到Agentic架构的专栏设计
2026/10/5 8:47:18 网站建设 项目流程

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_size=1000、overlap=200就完事,结果检索效果一塌糊涂。

固定大小切块的问题在于:它假设所有文档的结构和语义密度是一样的。但实际文档里,一段代码和一段叙述性文字的最佳切块大小完全不同;一个表格和一段列表也不一样。更糟糕的是,固定切块经常把一句完整的话从中间切断,导致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)双路召回,然后融合排序。融合算法常用RRF(Reciprocal 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 用Ollama+pgvector搭一个本地可跑的进阶RAG

这一节我给出一个完整的、可复现的本地RAG实现,用Ollama跑LLM和Embedding,用pgvector做向量存储,包含混合检索和重排序。所有代码都可以直接复制运行。

首先用Docker起一个带pgvector的PostgreSQL:

docker run -d --name pgvector \ -e POSTGRES_PASSWORD=rag123 \ -e POSTGRES_DB=ragdb \ -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( host="localhost", port=5432, dbname="ragdb", user="postgres", password="rag123" ) 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(model="nomic-embed-text", prompt=text) 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_k=5): # 向量检索 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, key=vec_results.get, reverse=True)): fused[doc_id] = fused.get(doc_id, 0) + 1 / (60 + rank + 1) for rank, doc_id in enumerate(sorted(kw_results, key=kw_results.get, reverse=True)): fused[doc_id] = fused.get(doc_id, 0) + 1 / (60 + rank + 1) top_ids = sorted(fused, key=fused.get, reverse=True)[: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_k=5) context = "\n\n".join([d[1] for d in docs]) prompt = f"""基于以下上下文回答问题。如果上下文没有相关信息,直接说不知道。 上下文: {context} 问题:{query} 答案:""" resp = ollama.chat(model="qwen2.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_fp16=True) def rerank(query, candidates, top_k=3): pairs = [[query, c[1]] for c in candidates] scores = reranker.compute_score(pairs, normalize=True) ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True) return [r[0] for r in ranked[:top_k]]

把hybrid_search的返回结果传给rerank,再拼Prompt。实测下来,重排序在中文问答任务上能把Top3命中率提升15-25个百分点,代价是增加约300ms延迟。

参数调优方面,normalize=True让分数落在0-1之间方便设阈值;use_fp16=True在GPU上能提速近一倍;候选数量建议是最终TopK的5-10倍,太少重排序没意义,太多延迟吃不消。

4.3 查询改写与多路召回的实现

用户的问题往往表述模糊,直接拿去检索效果不好。查询改写用LLM把原始问题改写成多个不同角度的查询,分别检索后融合结果。

def rewrite_query(query): prompt = f"""把下面的问题改写成3个不同角度的检索查询,每行一个,不要编号: 问题:{query}""" resp = ollama.chat(model="qwen2.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_k=5): queries = [query] + rewrite_query(query) all_results = {} for q in queries: for doc_id, content in hybrid_search(q, top_k=top_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(model="qwen2.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项目模板(Ollama+pgvector+混合检索+重排序+评测),一个Milvus集群部署的Docker Compose配置,一个评测集构建指南,以及一份RAG优化checklist。这些资源会随专栏逐步放出,订阅后即可获取。

我个人的体会是,RAG的进阶没有捷径,就是不断踩坑、不断评测、不断迭代。这个专栏能帮你少踩一些我已经踩过的坑,但最终的效果还是取决于你对业务数据的理解和持续优化的耐心。希望这个专栏能成为你RAG进阶路上的一个靠谱参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询