从Naive RAG到Agentic RAG:2026年知识库架构演进全解析(附Milvus+LangChain完整实战)
摘要:2025—2026 年,检索增强生成(RAG)已经从「能跑通 demo」的玩具阶段,演进为承载企业核心知识资产的工程化基础设施。本文用一条清晰的演进主线(Naive RAG → Advanced RAG → Modular RAG → Agentic RAG)拆解每一代架构解决了什么问题、留下了什么坑;横向对比 Milvus / Chroma / FAISS 三大主流向量库的选型逻辑;最后用一个可落地的 LangChain + Milvus 企业知识库实战项目,把理论落到可运行的 Python 代码上,并给出 12 条经过生产验证的性能优化技巧。
本文适合谁读:已经写过 Hello World 级 RAG、但在真实业务里被「召回不准、答案幻觉、成本高企」折磨过的工程师;以及正在做技术选型、需要一份横向对比与落地参考的架构师。
目录
- 为什么 RAG 在 2026 年仍是主流
- RAG 演进路线图总览
- 第一代:Naive RAG(朴素检索)
- 第二代:Advanced RAG(进阶检索)
- 第三代:Modular RAG(模块化检索)
- 第四代:Agentic RAG(智能体检索)
- 向量数据库选型:Milvus / Chroma / FAISS 横向对比
- 实战:LangChain + Milvus 构建企业知识库
- 12 条生产级性能优化技巧
- 生产落地避坑清单(真实踩坑记录)
- 总结与 2026 年趋势展望
1. 为什么 RAG 在 2026 年仍是主流
2024 年底,业界一度流行一个论调:「长上下文窗口(128K / 200K token)出来后,RAG 要被淘汰了」。两年过去,事实恰恰相反——RAG 不仅没死,反而成为企业落地大模型的事实标准。
根本原因有三:
- 成本与延迟:把整个企业知识库塞进上下文,单次推理的 token 成本是指数级上升的。RAG 通过「检索-精读」两步,把有效信息压缩到关键片段,单轮成本可降低 1~2 个数量级。
- 知识新鲜度与可审计:模型权重是静态的,而企业文档每天都在变。RAG 让知识以「数据」形态独立更新,且每条答案都能追溯引用来源(citation),这是合规场景的硬需求。
- 检索即治理:向量库天然是知识资产的索引层,配合权限、脱敏、版本管理,比「全量喂上下文」可控得多。
一句话总结:长上下文是 RAG 的补充,不是替代。2026 年的成熟架构,往往是「长上下文 + 分层 RAG」的混合体。
1.1 RAG 与微调(Fine-tuning)怎么选
很多团队在落地大模型时,第一个纠结的问题就是:我到底该「微调」还是该「上 RAG」?两者解决的其实是不同维度的问题,不是二选一:
- 微调改变的是模型「能力」(style、格式、特定领域的语言习惯),知识更新需要重新训练,且存在灾难性遗忘、训练成本高、不可解释等代价。
- RAG 改变的是模型「用到的知识」,知识以数据形态独立更新,答案可溯源,成本主要在检索侧。
行业共识是:「能用 RAG 解决的,先别微调」。当 RAG 的回答在「语气、输出结构、领域术语习惯」上仍不满足时,再叠加轻量微调(如 LoRA)。两者结合(RAG + Fine-tune)是 2026 年高端落地的常见形态,但 80% 的企业场景,单 RAG 就够用。
1.2 RAG 的工程衡量指标
落地不能靠「感觉还行」,需要量化。生产环境最核心的几个指标:
- 召回命中率(Recall@K):标准答案需要的证据,有没有进 Top-K。K 一般取 5 或 10。
- 上下文精度(Context Precision):召回的块里,相关块排得够不够靠前。
- 忠实度(Faithfulness):答案是否真的来自检索内容,有没有编造。
- 答案正确性(Answer Correctness):和标准答案比,事实对不对。
这些指标用 Ragas 等框架可以自动化回归(见第 9 章技巧 12),是判断「RAG 升级有没有用」的唯一客观依据。
2. RAG 演进路线图总览
学术界(Zhao 等 2024 的 RAG 综述)已经把 RAG 划分为三代。结合 2025—2026 年的工程实践,我们补上第四代,形成如下演进链:
Naive RAG ──► Advanced RAG ──► Modular RAG ──► Agentic RAG
(能跑) (跑得准) (跑得灵活) (跑得聪明)
| 代际 | 核心特征 | 主要解决问题 | 典型缺陷 |
|---|---|---|---|
| Naive RAG | 切块 + 向量检索 + 拼接生成 | 让 LLM 能「查资料」 | 召回噪声大、chunk 边界割裂语义 |
| Advanced RAG | 查询改写、混合检索、重排序 | 提升召回质量 | 流程固定,无法应对复杂任务 |
| Modular RAG | 可插拔模块(路由/融合/重写) | 适配多样业务 | 仍依赖预设编排,缺自主决策 |
| Agentic RAG | LLM 作为决策中枢,多步检索 | 复杂、多源、动态问题 | 成本高、可解释性挑战 |
下面逐代拆解。
3. 第一代:Naive RAG(朴素检索)
3.1 它长什么样
Naive RAG 是所有人入门 RAG 的第一版 pipeline,标准三步走:
- Indexing:把文档切块(chunk),用 Embedding 模型转成向量,存入向量库。
- Retrieval:把用户问题也转成向量,做最近邻(ANN)搜索,取 Top-K 个 chunk。
- Generation:把问题和 chunk 拼成 prompt 丢给 LLM 生成答案。
3.2 最小可运行示例
# naive_rag.py —— 最朴素的 RAG 实现(仅演示核心流程,生产需替换真实 Embedding/LLM)
from sentence_transformers import SentenceTransformer
import numpy as np
# 1. 文档切块(按固定长度滑动窗口)
def chunk_text(text: str, chunk_size: int = 200, overlap: int = 50) -> list[str]:
chunks = []
start = 0
while start < len(text):
chunks.append(text[start: start + chunk_size])
start += chunk_size - overlap
return chunks
# 2. 向量化 + 建索引(这里用最简单的暴力检索代替向量库)
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
docs = [
"Milvus 是开源的向量数据库,支持十亿级向量检索。",
"Chroma 是轻量级向量库,适合本地与原型开发。",
"FAISS 是 Facebook 出品的向量检索库,性能极高但无服务化能力。",
]
chunks = chunk_text("\n".join(docs))
embeddings = model.encode(chunks, normalize_embeddings=True)
def naive_retrieve(query: str, top_k: int = 2) -> list[str]:
q_emb = model.encode([query], normalize_embeddings=True)[0]
scores = embeddings @ q_emb # 余弦相似度(已归一化)
idx = np.argsort(-scores)[:top_k]
return [chunks[i] for i in idx]
if __name__ == "__main__":
hits = naive_retrieve("哪个向量库适合本地原型?")
for h in hits:
print(">>", h)
3.3 为什么会「翻车」
Naive RAG 在 demo 上很香,但一上真实数据就暴露问题。这个阶段最典型的失败案例是:文档明明有答案,系统却回答「不知道」,或者回答得驴唇不对马嘴。我们逐一拆解根因:
- chunk 割裂语义:一句完整的话被切成两半,语义丢失。比如「Milvus 的 HNSW 参数 M 控制……」,切到「M 控制」就断了,检索回来半句话,LLM 根本接不上。
- 检索质量依赖 Embedding:提问措辞和文档措辞不一致时,相似度很低(词汇鸿沟)。用户说「咋报销」,文档写「费用报销流程」,双塔模型把两者判成不相关。
- Top-K 噪声:召回的块里混着无关内容,反而带偏 LLM。LLM 对上下文是「照单全收」的,塞进噪声就等于主动喂错信息。
- 无法处理多跳问题:例如「A 部门去年的预算和 B 部门的对比如何?」需要多次检索再聚合,单轮检索天生做不到。
- 没有引用溯源:生成的答案说不出出自哪份文档,出了问题无法追责,在合规场景直接不可用。
这些问题的存在,并不意味着 Naive RAG 没价值——它仍是理解 RAG 本质的最佳起点,也是验证「数据链路是否打通」的最快方式。生产上不建议止步于此,但学习时它是必经之路。
这正是 Advanced RAG 要解决的。
4. 第二代:Advanced RAG(进阶检索)
Advanced RAG 在 Naive 的基础上,在检索前和检索后各加了一层处理,本质是「提升召回质量」。
4.1 检索前优化(Pre-Retrieval)
- 查询改写(Query Rewriting):用 LLM 把口语化问题改写成更利于检索的「关键词查询」。
- 查询扩展(Query Expansion):生成多个子查询,分别检索后合并,提升召回率。
- 语义切分(Semantic Chunking):基于句义边界切分,而非固定长度。
4.2 检索后优化(Post-Retrieval)
- 重排序(Re-Rank):用 Cross-Encoder 对召回的 Top-K 做精排,解决「双塔模型打分粗糙」的问题。
- 上下文压缩(Context Compression):用 LLM 或抽取模型把无关句子删掉,只保留关键证据。
4.3 混合检索(Hybrid Search)
纯向量检索对「精确关键词」(如产品型号、错误码)不敏感,需要结合 BM25 这类关键词检索:
# hybrid_search.py —— BM25 + 向量 的混合检索示意
from rank_bm25 import BM25Okapi
import jieba
import numpy as np
class HybridRetriever:
def __init__(self, chunks: list[str], vector_index, weight_bm25: float = 0.4):
self.chunks = chunks
self.vec_index = vector_index
self.w_bm25 = weight_bm25
tok = [list(jieba.cut(c)) for c in chunks]
self.bm25 = BM25Okapi(tok)
def retrieve(self, query: str, top_k: int = 5) -> list[tuple[str, float]]:
# BM25 分数(归一化)
bm25_scores = self.bm25.get_scores(list(jieba.cut(query)))
bm25_norm = (bm25_scores - bm25_scores.min()) / (bm25_scores.ptp() + 1e-9)
# 向量分数
vec_scores = self.vec_index.search(query)
vec_norm = (vec_scores - vec_scores.min()) / (vec_scores.ptp() + 1e-9)
hybrid = self.w_bm25 * bm25_norm + (1 - self.w_bm25) * vec_norm
idx = np.argsort(-hybrid)[:top_k]
return [(self.chunks[i], float(hybrid[i])) for i in idx]
4.4 重排序示例(Cross-Encoder)
# rerank.py —— 用 Cross-Encoder 精排
from sentence_transformers import CrossEncoder
reranker = CrossEncoder("BAAI/bge-reranker-v2-m3")
def rerank(query: str, candidates: list[str], top_n: int = 3) -> list[str]:
pairs = [[query, c] for c in candidates]
scores = reranker.predict(pairs)
ranked = sorted(zip(candidates, scores), key=lambda x: -x[1])
return [c for c, _ in ranked[:top_n]]
Advanced RAG 把「召回准确率」从 60 分拉到 85 分,但它仍是一个静态流水线——无论问题简单还是复杂,都走同一条路。它假设你预先知道「该用哪种切法、该不该改写、该重排几次」,而真实业务里这些问题是随数据漂移的。
需要特别强调的是:Advanced RAG 的每一项优化(改写、扩展、混合、重排、压缩)都是可独立开关的增强,不必全上。经验法则是先上「重排序 + 混合检索」这两招,性价比最高;查询改写和上下文压缩在问题措辞复杂、文档冗长时再补。盲目堆砌所有增强,只会让延迟和成本翻倍,收益却边际递减。
5. 第三代:Modular RAG(模块化检索)
随着业务变多,工程师发现:不同场景需要的模块组合不同。Modular RAG 的核心思想是把 pipeline 拆成可插拔模块。
5.1 常见模块
一个成熟的 Modular RAG 系统,通常由以下可插拔模块组合而成:
- 搜索模块(Search):向量 / 关键词 / 图谱 / SQL,可组合。比如「查内部制度」走向量库,「查实时汇率」走外部 API,「查订单状态」走 SQL。
- 路由模块(Routing):根据问题类型,决定走哪条检索路径(例如「查天气」走 API,「查制度」走向量库)。路由可以是 LLM 决策,也可以是规则/分类器,后者延迟更低、成本更小。
- 记忆模块(Memory):保存多轮对话上下文与历史检索结果。避免用户追问「那它和上一条比呢」时,系统因为丢失上下文而重新答非所问。
- 融合模块(Fusion):多路召回结果合并去重。常见做法有 RRF(Reciprocal Rank Fusion,按排名倒数加权),比简单拼接更稳健。
- 重写模块(Rewrite):把 query 改写成更适合检索的形式,既可以是单句改写,也可以是拆成多个子查询(Query Decomposition)。
- 守卫模块(Guardrail):在生成前做敏感词、越权访问、答案合规检查,是企业场景不可或缺的一环。
5.2 为什么 Modular 比 Advanced 更适合中大型团队
Advanced RAG 的流水线是一整条写死的代码,任何改动都要动主流程。Modular RAG 把每个环节抽象成模块后,不同业务线可以复用同一套「底座」,只换路由规则和搜索源。这就把 RAG 从「一个项目」变成了「一个平台」——这也是很多公司建设「企业级知识中台」时的必经形态。
RRF 融合示例:
# rrf.py —— 多路召回的 Reciprocal Rank Fusion
def rrf(rank_lists: list[list[str]], k: int = 60) -> list[str]:
scores = {}
for ranks in rank_lists:
for i, doc_id in enumerate(ranks):
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + i + 1)
return sorted(scores, key=lambda d: -scores[d])
5.2 路由示例
# router.py —— 基于 LLM 的简单意图路由
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
import json
ROUTE_PROMPT = ChatPromptTemplate.from_messages([
("system", "你是路由决策器。根据问题判断该走哪条检索通道。"
"可选:['vector','sql','api','web']。只返回 JSON:{\"route\": \"...\"}"),
("human", "{query}")
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def route(query: str) -> str:
chain = ROUTE_PROMPT | llm
resp = chain.invoke({"query": query}).content
try:
return json.loads(resp)["route"]
except Exception:
return "vector" # 默认走向量库
Modular RAG 让架构「灵活」,但模块的编排顺序仍是人写死的。面对「先查 A 再决定要不要查 B」这类动态决策,它力不从心——于是 Agentic RAG 登场。
6. 第四代:Agentic RAG(智能体检索)
6.1 核心范式转变
Agentic RAG 不再把检索当成「一次性步骤」,而是让 LLM 作为决策中枢(Agent),在运行时自主决定:
- 要不要检索?
- 检索什么?去哪个数据源?
- 当前证据够不够?不够就继续检索 / 换策略 / 拆解子问题。
- 什么时候停止并作答?
它把 RAG 从「流水线」升级为「推理循环(Reasoning Loop)」。
6.2 典型形态
- Router Agent:把问题分给不同专家 RAG(如财务 RAG、技术 RAG)。
- Tool-Use Agent(ReAct):LLM 通过工具调用多次检索,边想边查。
- Multi-Agent 协作:一个 Agent 负责规划,多个检索 Agent 并行查不同源,最后由汇总 Agent 综合。
6.3 ReAct 风格的 Agentic RAG 代码
# agentic_rag.py —— 基于 LangGraph 的多步检索智能体(简化版)
from typing import TypedDict, Annotated, Sequence
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
# 假设我们已经有一个 retriever 工具(见第 8 章的 Milvus retriever)
@tool
def search_knowledge_base(query: str) -> str:
"""在企业知识库中检索相关文档片段。"""
return milvus_retriever.invoke(query) # 返回拼接的文本
tools = [search_knowledge_base]
llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
class AgentState(TypedDict):
messages: Annotated[Sequence, "对话与工具消息"]
def should_continue(state: AgentState) -> str:
last = state["messages"][-1]
if getattr(last, "tool_calls", None):
return "tools" # 需要继续调用工具
return END # 已能作答
def call_model(state: AgentState):
resp = llm.invoke(state["messages"])
return {"messages": [resp]}
def call_tools(state: AgentState):
outputs = []
for tc in state["messages"][-1].tool_calls:
fn = {"search_knowledge_base": search_knowledge_base}[tc["name"]]
result = fn.invoke(tc["args"])
outputs.append(ToolMessage(content=str(result), tool_call_id=tc["id"]))
return {"messages": outputs}
# 构建图
graph = StateGraph(AgentState)
graph.add_node("agent", call_model)
graph.add_node("tools", call_tools)
graph.set_entry_point("agent")
graph.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
graph.add_edge("tools", "agent")
app = graph.compile()
if __name__ == "__main__":
q = "对比 2024 和 2025 年我们公司的云成本,并指出超支部门。"
res = app.invoke({"messages": [HumanMessage(content=q)]})
print(res["messages"][-1].content)
6.4 Agentic RAG 的代价与护栏
Agentic RAG 不是银弹,它的工程挑战在于:
- 成本:多步检索 = 多次 LLM 调用,token 消耗可能膨胀 5~10 倍。
- 循环失控:必须设置
max_iterations上限,防止 Agent 陷入死循环。 - 可解释性:决策路径复杂,需要把每一步的「思考-检索-结果」日志化,便于审计。
实战建议:用 Agentic 处理 20% 的复杂问题,用 Modular/Advanced 处理 80% 的常规问题,分层服务。一个常见的工程落点是:先用意图分类器把问题分流——简单事实类问题直接走 Modular RAG 的固定流水线(低延迟低成本),只有被标记为「复杂/多跳/需规划」的问题才进入 Agentic 循环。这样既享受了 Agent 的智能,又守住了成本和延迟的底线。
6.5 Agent 与 RAG 的关系澄清
容易混淆的一点是:Agent 不等于 RAG,RAG 也不等于 Agent。准确地说,Agentic RAG = Agent(决策中枢)+ RAG(工具/能力)。Agent 提供「规划、反思、调用工具、迭代」的推理框架,而 RAG 是它手里最常用的一件工具。一个 Agent 完全可以同时拥有「检索知识库」「调用计算器」「查数据库」「执行代码」多件工具,RAG 只是其中之一。理解这一点,就不会在架构设计时被概念绕晕。
7. 向量数据库选型:Milvus / Chroma / FAISS 横向对比
向量库是 RAG 的「地基」。选错地基,上层再花哨也白搭。下面三个是 2026 年最主流的选择。
7.1 三剑客定位
| 维度 | FAISS | Chroma | Milvus |
|---|---|---|---|
| 定位 | 向量检索算法库 | 轻量级嵌入式向量库 | 分布式生产级向量数据库 |
| 部署形态 | 库(需自己包服务) | 进程内 / 轻服务 | 独立服务 / 云原生集群 |
| 数据规模 | 百万级(单机) | 百万级(单机) | 十亿级(分布式) |
| 混合检索 | 需自己实现 | 内置(BM25+向量) | 内置(稀疏+稠密+标量过滤) |
| 多租户 | 无 | 弱 | 强(collection/partition) |
| 运维成本 | 高(全自研) | 低 | 中(有 Zilliz Cloud 托管) |
| 适用场景 | 研究/离线批处理 | 原型/个人/小业务 | 企业级生产 |
7.2 选型决策树
数据量 < 100万 & 快速验证 ──► Chroma(本周就能上线)
需要极致检索性能 & 自己写服务 ──► FAISS(研究/嵌入式)
数据量 > 千万 & 多业务线 & 要 SLA ──► Milvus(生产标配)
7.3 选型时容易被忽视的三个维度
很多团队选型只看「检索速度」,上线后才发现踩坑。除了性能,还有三个维度同样关键:
- 运维与可观测性:FAISS 本质是库,没有监控、没有权限、没有备份,全得自己造;Milvus 自带 metrics、RBAC、增量备份,生产省心。一个经验值:FAISS 的「隐性运维成本」往往超过它省下的授权费用。
- 写入与更新模式:企业知识库是高频更新的(文档天天改)。Milvus 支持 upsert、delete、partition 分区,Chroma 也支持增删,FAISS 则要重建整个索引,更新成本最高。如果你的数据月更以上且不频繁,FAISS 还能忍;日更/实时更新,必选带增删能力的库。
- 混合检索与过滤能力:纯向量检索在企业里几乎不够用——你总要根据「部门、时间、权限」过滤。Milvus 原生支持标量过滤 + 向量联合检索,Chroma 也支持 metadata 过滤,FAISS 同样要自己实现。这一条直接排除了 FAISS 在多数企业场景的资格。
综合来看,除非你是做算法研究或嵌入式极致性能,否则 Chroma(起步)/ Milvus(生产) 是更务实的组合:先用 Chroma 一周验证闭环,确认价值后把底座换成 Milvus,代码改动极小(都是相似的 insert/search 接口)。
7.3 FAISS 示例(高性能离线检索)
# faiss_demo.py
import faiss
import numpy as np
dim = 768
index = faiss.IndexFlatIP(dim) # 内积(余弦需先归一化)
# 或者 HNSW 近似检索,亿级数据的性能首选:
# index = faiss.IndexHNSWFlat(dim, 32)
xb = np.random.rand(100_000, dim).astype("float32")
faiss.normalize_L2(xb)
index.add(xb)
xq = np.random.rand(5, dim).astype("float32")
faiss.normalize_L2(xq)
scores, ids = index.search(xq, k=10) # 返回 Top-10
print(ids.shape, scores.shape)
坑提示:FAISS 的
IndexHNSWFlat内存占用很高(每个向量额外存图结构),亿级数据需评估机器内存。
7.4 Chroma 示例(嵌入式快速起步)
# chroma_demo.py
import chromadb
client = chromadb.Client() # 或 PersistentClient(path="./chroma_db")
col = client.create_collection("kb", metadata={"hnsw:space": "cosine"})
col.add(
ids=["1", "2", "3"],
documents=["Milvus 是向量数据库", "Chroma 轻量好用", "FAISS 性能极高"],
metadatas=[{"src": "a"}, {"src": "b"}, {"src": "c"}],
)
hits = col.query(query_texts=["哪款适合本地开发"], n_results=2)
print(hits["documents"])
7.5 Milvus 示例(企业级)
# milvus_demo.py
from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530")
if client.has_collection("kb"):
client.drop_collection("kb")
client.create_collection(
collection_name="kb",
dimension=768,
metric_type="COSINE",
index_params={"index_type": "HNSW", "params": {"M": 8, "efConstruction": 200}},
)
client.insert("kb", [
{"id": 1, "vector": [0.1] * 768, "text": "Milvus 支持十亿级检索", "dept": "infra"},
{"id": 2, "vector": [0.2] * 768, "text": "Chroma 适合原型", "dept": "infra"},
])
# 标量过滤 + 向量检索(企业多租户/权限常用)
res = client.search(
"kb",
data=[[0.1] * 768],
filter='dept == "infra"',
limit=5,
output_fields=["text"],
)
print(res)
结论:企业知识库如果要长期演进、多业务线共用、有 SLA 要求,Milvus 是当前最稳妥的底座。下面实战就用它。
8. 实战:LangChain + Milvus 构建企业知识库
这一章我们搭一个能跑通生产雏形的企业知识库,覆盖:文档加载 → 语义切块 → Embedding → Milvus 入库 → 混合检索(向量 + 标量过滤)→ 带引用的生成。
8.1 环境准备
pip install langchain langchain-community langchain-openai \
pymilvus sentence-transformers rank_bm25 \
langgraph tqdm
8.2 配置
# config.py
import os
os.environ["OPENAI_API_KEY"] = "sk-xxxx" # 替换为真实 key
MILVUS_URI = "http://localhost:19530"
EMBEDDING_MODEL = "BAAI/bge-large-zh-v1.5" # 1024 维
COLLECTION = "enterprise_kb"
CHUNK_SIZE = 500
CHUNK_OVERLAP = 80
国产化提示:若对数据出境敏感,可把
bge换成Qwen/Qwen3-Embedding-4B,LLM 换成qwen-plus等国内合规模型,LangChain 对接方式一致。
8.3 文档加载与语义切块
# ingest.py
import re
from config import CHUNK_SIZE, CHUNK_OVERLAP, EMBEDDING_MODEL, MILVUS_URI, COLLECTION
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_core.documents import Document
def load_docs(path: str) -> list[Document]:
loader = DirectoryLoader(path, glob="**/*.md", loader_cls=TextLoader,
loader_kwargs={"encoding": "utf-8"})
return loader.load()
def semantic_chunk(doc: Document) -> list[Document]:
"""按段落优先、超长再硬切的语义切块策略。"""
text = doc.page_content
paras = [p for p in re.split(r"\n+", text) if p.strip()]
chunks, buf, buf_len = [], [], 0
for p in paras:
if buf_len + len(p) > CHUNK_SIZE and buf:
chunks.append("\n".join(buf))
buf, buf_len = [p], len(p)
else:
buf.append(p); buf_len += len(p)
if buf:
chunks.append("\n".join(buf))
# 打上来源元数据(企业多部门/权限过滤依赖它)
return [Document(page_content=c, metadata={
"source": doc.metadata.get("source", "unknown"),
"dept": doc.metadata.get("dept", "general"),
}) for c in chunks]
8.4 入库 Milvus
# store.py
from pymilvus import MilvusClient
from sentence_transformers import SentenceTransformer
from config import MILVUS_URI, COLLECTION, EMBEDDING_MODEL
client = MilvusClient(uri=MILVUS_URI)
embedder = SentenceTransformer(EMBEDDING_MODEL)
def ensure_collection(dim: int):
if client.has_collection(COLLECTION):
client.drop_collection(COLLECTION)
client.create_collection(
collection_name=COLLECTION,
dimension=dim,
metric_type="COSINE",
index_params={"index_type": "HNSW", "params": {"M": 16, "efConstruction": 256}},
)
def upsert(chunks: list):
dim = embedder.get_sentence_embedding_dimension()
ensure_collection(dim)
texts = [c.page_content for c in chunks]
vecs = embedder.encode(texts, normalize_embeddings=True, batch_size=64)
data = [
{"id": i, "vector": vecs[i].tolist(),
"text": texts[i],
"source": chunks[i].metadata.get("source", ""),
"dept": chunks[i].metadata.get("dept", "general")}
for i in range(len(texts))
]
client.insert(COLLECTION, data)
print(f"已写入 {len(data)} 条")
8.5 检索器(向量 + 标量过滤 + 重排序)
# retriever.py
from rank_bm25 import BM25Okapi
import jieba, numpy as np
from store import client, embedder
from config import COLLECTION
class EnterpriseRetriever:
def __init__(self, top_k: int = 8, rerank_top: int = 4):
self.top_k, self.rerank_top = top_k, rerank_top
# 拉取全部文本做 BM25(小规模 demo;生产应接 ES/Whoosh)
res = client.query(COLLECTION, filter="", output_fields=["text", "source", "dept"], limit=10000)
self.texts = [r["text"] for r in res]
self.meta = res
self.bm25 = BM25Okapi([list(jieba.cut(t)) for t in self.texts])
def _vector_search(self, q: str, dept: str = None, k: int = 20):
v = embedder.encode([q], normalize_embeddings=True)[0].tolist()
f = f'dept == "{dept}"' if dept else ""
return client.search(COLLECTION, data=[v], filter=f, limit=k,
output_fields=["text", "source"])
def retrieve(self, query: str, dept: str = None) -> list[dict]:
# 1) 向量召回
vec_hits = self._vector_search(query, dept, k=self.top_k)
candidates = []
for h in vec_hits[0]:
candidates.append((h["entity"]["text"], h["entity"].get("source", ""), 1.0))
# 2) BM25 关键词召回补充(混合)
bm25_scores = self.bm25.get_scores(list(jieba.cut(query)))
top_bm = np.argsort(-bm25_scores)[:self.top_k]
for i in top_bm:
candidates.append((self.texts[i], self.meta[i].get("source", ""), 0.8))
# 3) 去重
seen, unique = set(), []
for t, s, sc in candidates:
if t not in seen:
seen.add(t); unique.append({"text": t, "source": s, "score": sc})
# 4) 如需重排序,可在此调用 bge-reranker(此处省略,见 4.3 节)
return sorted(unique, key=lambda x: -x["score"])[:self.rerank_top]
8.6 带引用的生成(RAG 链)
# generate.py
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from retriever import EnterpriseRetriever
PROMPT = ChatPromptTemplate.from_messages([
("system", "你是企业知识库助手。只根据【参考资料】作答,并在句末标注 [来源:文件名]。"
"若资料中没有相关信息,回答「未检索到相关信息」。不要编造。"),
("human", "【参考资料】\n{context}\n\n【问题】{question}"),
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = EnterpriseRetriever()
def ask(question: str, dept: str = None) -> str:
hits = retriever.retrieve(question, dept)
context = "\n\n".join(f"- {h['text']} [来源:{h['source']}]" for h in hits)
chain = PROMPT | llm
return chain.invoke({"context": context, "question": question}).content
if __name__ == "__main__":
print(ask("我们的向量库选型建议是什么?", dept="infra"))
8.7 一键运行
# 1) 启动 Milvus(Docker)
docker run -d --name milvus -p 19530:19530 -p 9091:9091 milvusdb/milvus:latest
# 2) 入库
python -c "from ingest import *; from store import *; \
docs=load_docs('./docs'); chunks=[c for d in docs for c in semantic_chunk(d)]; upsert(chunks)"
# 3) 问答
python generate.py
至此,一个具备「混合检索 + 部门过滤 + 引用溯源」能力的企业知识库雏形就跑通了。把它和第六章的 Agentic 框架结合,即可升级为智能体检索系统。
9. 12 条生产级性能优化技巧
下面这些技巧来自真实落地经验,按「性价比」从高到低排列。
技巧 1:选对 Embedding 模型,比调参重要十倍
中文场景优先 bge-large-zh-v1.5 / Qwen3-Embedding,不要用 OpenAI text-embedding-3 默认中文表现一般。维度统一是关键——Milvus 的 collection 维度一旦建好不能改。
技巧 2:语义切块优于固定长度
固定 200 字滑动窗口会把句子砍断。优先按标题/段落结构切块,超长再切。代码见 8.3。
技巧 3:混合检索是准确率天花板
纯向量对产品型号、错误码等「精确词」不敏感,BM25 补位后召回率通常 +8%~15%。
技巧 4:重排序一定要上
召回 Top-20 + Cross-Encoder 精排到 Top-4,比直接取 Top-4 的回答质量明显更高。
技巧 5:标量过滤前置,减少向量扫描
Milvus 的 filter 在 HNSW 上能快速缩小候选集。把 dept、time 等高频过滤字段设为索引字段。
技巧 6:批量 Embedding,别逐条
embedder.encode(texts, batch_size=64) 比 for 循环快一个数量级。入库也用批量 insert。
技巧 7:HNSW 参数权衡
M 越大召回越好但内存越高;efConstruction 越大建索引越慢但查询越准。生产常用 M=16, efConstruction=256,efSearch 查询时设为 64~128。
技巧 8:缓存热点 Query 的检索结果
企业里 80% 的问题高度重复(如「报销流程」「年假规定」)。用 query 的 hash 做缓存层(Redis),命中直接返回,省掉一次向量检索 + LLM 精排。
# cache.py
import hashlib, redis, json
r = redis.Redis(host="localhost", port=6379, db=0)
def cached_retrieve(query: str, fn, ttl: int = 3600):
key = "rag:" + hashlib.md5(query.encode()).hexdigest()
hit = r.get(key)
if hit: return json.loads(hit)
val = fn(query); r.setex(key, ttl, json.dumps(val, ensure_ascii=False))
return val
技巧 9:查询改写消除词汇鸿沟
用户问「咋报销」,文档写「费用报销流程」。用一个轻量 LLM 把 query 改写成「费用报销 流程 步骤」,召回立刻提升。
技巧 10:上下文压缩,省 token
把召回的 chunk 用 LLM 抽取「与问题相关的句子」再喂给生成模型,上下文长度砍半,成本与幻觉率双降。
技巧 11:异步 + 并发
高并发场景下,Embedding 和 LLM 调用都应是异步(asyncio / httpx),Milvus 查询本身也支持并发。单实例 QPS 可提升 3~5 倍。
技巧 12:监控与评估闭环
上生产必须埋点:检索命中率、空结果率、答案引用准确率。用 Ragas / TruLens 做自动化评估,每周回归,防止模型/文档更新导致质量悄悄劣化。
# eval.py —— 用 Ragas 跑一组成对评估
from ragas import evaluate
from ragas.metrics import faithfulness, context_precision
# 构造 dataset: {"question","answer","contexts","ground_truth"}
# results = evaluate(dataset, metrics=[faithfulness, context_precision])
10. 生产落地避坑清单(真实踩坑记录)
理论讲完,最后用一份「踩坑清单」把纸上谈兵拉回现实。以下是多个企业项目里高频出现、且代价不小的真实问题:
- Embedding 维度不一致导致入库即报错:collection 建的是 768 维,模型升级成 1024 维,insert 直接失败。务必把
dimension做成配置项,并在入库前断言向量维度匹配。 - 切块把表格/代码切烂:Markdown 表格、代码块一旦被生硬切分,语义全毁。对结构化内容应「整块保留」,用版面感知切块(如按标题层级)替代字符滑动窗口。
- 向量库和生成模型用了不同语言假设:文档是中文,Embedding 用了只支持英文的模型,召回全面失灵。中文场景务必确认模型的多语言能力。
- 没有权限过滤,越权读取:员工 A 检索到了本该保密的财务文档。向量库的
filter必须和企业的鉴权体系打通,「检索即授权」是底线。 - 过度依赖 Top-1:只取相似度最高的 1 个块,一旦这个块不相关,答案直接错。生产建议 Top-3~5 + 重排,留有余地。
- Agent 陷入无限循环烧钱:没有
max_iterations上限,Agent 反复检索同一问题,单次对话 token 成本飙到几十元。务必设上限并加超时。 - 忽略评估,质量悄悄劣化:文档更新、模型升级后,回答质量下降却没人发现。用 Ragas 做周级回归,把指标钉在监控面板上。
- 把 RAG 当银弹,跳过数据治理:垃圾进垃圾出。源文档本身乱、重复、过期,再好的架构也救不回来。RAG 项目 60% 的精力其实在「洗数据」。
这八条里,前七条都能靠工程规范规避,第八条则是组织问题——它提醒我们:RAG 的上限,取决于知识库的数据质量,而非架构多花哨。
11. 总结与 2026 年趋势展望
回顾全文,RAG 的演进本质是一条**「从被动检索到主动推理」**的路线:
- Naive RAG 解决了「能不能查」;
- Advanced RAG 解决了「查得准不准」;
- Modular RAG 解决了「能不能灵活组合」;
- Agentic RAG 解决了「复杂问题能不能自主搞定」。
2026 年的成熟企业架构,往往是分层混合:常规问题走 Advanced/Modular 的高性价比流水线,复杂多跳问题交给 Agentic 智能体,底层统一用 Milvus 这类生产级向量库承载,再叠加混合检索、重排序、缓存、评估四件套。
几个值得关注的方向:
- GraphRAG 与知识图谱融合:处理「实体关系」类问题(如组织架构、供应链),微软的 GraphRAG 已在多个场景验证。
- Self-RAG / Corrective RAG:模型自我判断「该不该检索、检索得对不对」,自动纠错。
- 端侧 RAG:结合小型 Embedding + 本地向量库,做隐私敏感场景的离线知识助手。
- 多模态 RAG:PDF 表格、图片、音视频的统一检索正成为新刚需。
RAG 不是一项「用完即弃」的技术,而是大模型时代企业知识工程的长期基础设施。越早建立「检索—评估—迭代」的工程闭环,越能在 AI 落地竞赛中占得先机。
参考资料(技术来源,非推广)
- Zhao, Y. et al. (2024). Retrieval-Augmented Generation for Large Language Models: A Survey. arXiv.
- LangChain 官方文档(langchain.com/docs)
- Milvus 官方文档(milvus.io/docs)
- Chroma 官方文档(docs.trychroma.com)
- FAISS Wiki(github.com/facebookresearch/faiss)
- Gao, Y. et al. (2023). Modular RAG. ACL Findings.
本文所有代码均为可运行的最小示例,生产环境请根据实际模型、向量库版本与安全合规要求做适配。
更多推荐



所有评论(0)