LangChain 06:RAG 概要

前言

模型参数里没有你们的内部文档、工单与产品说明;硬塞进提示词又容易超上下文、难更新。
RAG(Retrieval-Augmented Generation) 先按问题检索相关片段,再让模型据此生成,适合私有知识库问答。
本文给 LangChain 1.x 下的 RAG 概要:流水线阶段、哪一步要用哪种模型、重排序插在何处,以及最小 LCEL 链与 Agent 检索 Tool。
加载、切分、嵌入、入库、重排的细节分别见后续专文;更短的入门片段见《LangChain 02:入门教程》。
聊天模型继续对接 火山方舟 Coding Planark-code-latest);嵌入演示用 DeterministicFakeEmbedding,避免未开通嵌入模型时脚本跑不通。
下文需要 Python 3.12+,依赖用 uv 管理。
各示例都是完整可运行脚本:复制到项目根目录(与 .env 同级)后执行 uv run python xxx.py 即可。

依赖

建议使用 Python 3.12 及以上。
uv 初始化工程并声明基础依赖(版本请按项目实际调整)。
演示向量库用 langchain-core 自带的 InMemoryVectorStore,无需再装 langchain-community(该包已日落)。

1
2
3
4
uv init langchain-rag-overview
cd langchain-rag-overview
uv venv --python 3.12
uv add "langchain>=1.0,<2.0" langchain-openai langchain-core langchain-text-splitters python-dotenv rich

若已在空目录内初始化,也可写 uv init --name langchain-rag-overview,再执行 uv venvuv add

后续运行示例脚本时,可直接 uv run python demo.py,一般不必手动 activate 虚拟环境。
若习惯激活,Windows 使用 .venv\Scripts\activate,macOS / Linux 使用 source .venv/bin/activate

rich 用于查看检索命中与最终答案。
示例里常用 from rich import print as rprint

在项目根目录创建 .env,写入 Coding Plan 的 API Key 与专用 Base URL。
不要把 .env 提交进 Git。

1
2
OPENAI_API_KEY=你的火山方舟 API Key
OPENAI_BASE_URL=https://ark.cn-beijing.volces.com/api/coding/v3

示例脚本开头用 load_dotenv() 加载该文件。
请勿把 Base URL 写成普通方舟 .../api/v3,以免无法抵扣 Coding Plan 额度。

实现

流程概览

RAG 可拆成 离线索引在线问答 两段。

离线(语料变更时重跑或增量更新):

  1. 加载:文件/网页 → Document
  2. 切分:长文 → 带 metadata 的小块。
  3. 嵌入:文本 → 向量。
  4. 入库:写入向量库(演示用内存库;生产可用 Milvus、云厂商库等)。

在线(每次用户提问):

  1. 检索:问题向量(或关键词)→ Top-K 相关块。
  2. 重排(可选):对候选块精排并截断到 Top-N。
  3. 增强:把块拼进提示词上下文。
  4. 生成:聊天模型据上下文作答;要求「无依据则说不知道」。

索引质量大致决定「找不找得到」;重排与提示词、生成模型一起决定「找得到之后说得对不对」。

各环节与模型

不是每一步都要调模型。
聊天、嵌入、重排是三类不同能力,不要混用同一个模型名

环节 是否用模型 模型类型 用什么
加载 Loader / 解析库读文件
切分 嵌入模型 字符/递归等规则切分;语义切分才要嵌入模型
嵌入(离线) 嵌入模型 文本 → 向量(如 bge-m3
入库 向量库写接口
检索 嵌入模型 向量检索要把问题向量化后才能与库中向量比相似度(须与入库同一嵌入模型);纯关键词检索可不用
重排 Rerank 模型 对候选块精排并截断(如 bge-reranker-v2-m3),不是聊天模型
增强 拼字符串 / 提示词模板
生成 聊天模型 据上下文作答(如 ark-code-latest

说明: 表示该环节需要模型; 表示仅特定做法需要(语义切分、向量检索要嵌入模型)。
普通字符/递归切分、纯关键词检索不用模型;重排为可选环节,启用时才调用 Rerank。

可记三条边界:

  1. ark-code-latest 这类聊天模型 不能 当嵌入,也 不能 当 Rerank。
  2. 嵌入模型负责「向量相似度召回」;Rerank 负责「问题与文档成对打分」。
  3. 语义切分、向量检索、离线入库,共用同一嵌入模型与同一向量维度。

本地还是在线

嵌入与 Rerank 多数情况优先在线 API(如硅基流动);只有隐私、内网或大规模成本有硬约束时再上本地。

场景 更合适
学习、原型、流量不大 在线
语料/问题敏感、不能出内网 本地或私有化部署
QPS 很高、长期 API 更贵 本地(最好有 GPU)
本机只有 CPU、要快速跑通 在线

嵌入和 Rerank 都是短文本、可批量的推理:在线开通快,模型名与维数也好对齐。
本地要扛模型体积、显存或内存与运维,收益主要在隐私与高流量时的单位成本。

常见开源模型体量(半精度权重粗算):

类型 例子 参数量 磁盘约
嵌入(小) bge-smallall-MiniLM-L6-v2 几千万级 几十~200 MB
嵌入(常用) BAAI/bge-m3 约 0.57B 约 2 GB
Rerank(小) ms-marco-MiniLM-L6-v2 约 22M 约 100 MB
Rerank(常用) BAAI/bge-reranker-v2-m3 约 0.57B 约 2 GB
Rerank(更大) Qwen3-Reranker-4B/8B 数 B 数 GB~十余 GB

推理显存通常大于权重体积(还与 batch、序列长度有关);仅 CPU 也能跑中等模型,但延迟往往明显高于 GPU 或在线 API。
本地至少按「模型体积 + 余量」准备内存或显存。

聊天模型可继续用在线(如 Coding Plan);嵌入 / Rerank 不必与聊天同一家服务。
无论在线还是本地,入库与检索必须同一套嵌入模型
本系列演示默认在线;生产再按上表选型即可。

重排序位置

向量召回追求覆盖,Top-K 里常有「语义近但答非所问」的块。
重排序接在 检索之后、拼上下文之前:先取较大 k(如 10~20),再用 Rerank 压到 top_n(如 3~5)再生成。
概要阶段代码仍可只做「检索 → 生成」;上线噪声大时再加精排。
实现见《LangChain 11:检索重排序》。

索引入库

概要阶段用内存语料即可;真实项目把切分结果换成 Loader + Splitter 的产物。
本地演示用 InMemoryVectorStore + DeterministicFakeEmbedding(相同文本得到相同向量,便于抽查检索)。

下面完成切分、嵌入与建库,并抽查一次相似度检索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from rich import print as rprint

raw_texts = [
"LangChain 1.x 推荐用 create_agent 构建 Agent。",
"RAG 流程一般是:加载、切分、向量化、检索,可选重排,再生成答案。",
"InMemoryVectorStore 适合本地原型;生产可换托管向量库。",
]

splitter = RecursiveCharacterTextSplitter(chunk_size=80, chunk_overlap=10)
chunks = splitter.create_documents(raw_texts)

# 演示用 DeterministicFakeEmbedding;上线改为 init_embeddings(...)
embeddings = DeterministicFakeEmbedding(size=384)
vectorstore = InMemoryVectorStore.from_documents(chunks, embeddings)

hits = vectorstore.similarity_search("RAG 有哪些步骤?", k=2)
for d in hits:
rprint(d.page_content)

DeterministicFakeEmbedding 只保证链路与「同文同向量」可测,不保证真实语义检索质量。
上线请换成真实嵌入模型(见下文「嵌入注意」)。

最小问答链

固定「先检索再生成」时,用 LCEL 把 retriever、提示词与模型串成链即可。
此处只用到 聊天模型;检索侧演示仍用假嵌入。
as_retriever 把向量库变成可 invoke(问题) 的检索器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import os

from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from rich import print as rprint

load_dotenv()

api_key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]

raw_texts = [
"LangChain 1.x 推荐用 create_agent 构建 Agent。",
"RAG 流程一般是:加载、切分、向量化、检索,可选重排,再生成答案。",
"InMemoryVectorStore 适合本地原型;生产可换托管向量库。",
]
splitter = RecursiveCharacterTextSplitter(chunk_size=80, chunk_overlap=10)
chunks = splitter.create_documents(raw_texts)
embeddings = DeterministicFakeEmbedding(size=384)
vectorstore = InMemoryVectorStore.from_documents(chunks, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})


def format_docs(docs):
return "\n\n".join(d.page_content for d in docs)


prompt = ChatPromptTemplate.from_template(
"仅根据下列上下文作答;不知道就说不知道。\n\n"
"上下文:\n{context}\n\n问题:{question}"
)

model = init_chat_model(
"openai:ark-code-latest",
temperature=0,
api_key=api_key,
base_url=base_url,
)

rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)

answer = rag_chain.invoke("RAG 的典型步骤有哪些?")
rprint(answer)

提示词里写清「仅根据上下文」,可降低模型脱离语料胡编的概率(不能完全消除)。
若要在链中插入重排,把 retriever | format_docs 换成「召回 → Rerank → format」即可。

Agent 检索

需要聊天模型自行决定「要不要查库、查什么」时,把检索封成 @tool,交给 create_agent
这是常见的 Agentic RAG 形态:多工具、多轮追问时更灵活。
这里工具内部做向量检索;需要精排时在 search_knowledge 里先召回再 Rerank。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import os

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool
from langchain_core.embeddings import DeterministicFakeEmbedding
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from rich import print as rprint

load_dotenv()

api_key = os.environ["OPENAI_API_KEY"]
base_url = os.environ["OPENAI_BASE_URL"]

raw_texts = [
"LangChain 1.x 推荐用 create_agent 构建 Agent。",
"RAG 流程一般是:加载、切分、向量化、检索,可选重排,再生成答案。",
"InMemoryVectorStore 适合本地原型;生产可换托管向量库。",
]
chunks = RecursiveCharacterTextSplitter(
chunk_size=80, chunk_overlap=10
).create_documents(raw_texts)
vectorstore = InMemoryVectorStore.from_documents(
chunks, DeterministicFakeEmbedding(size=384)
)


@tool
def search_knowledge(query: str) -> str:
"""在知识库中检索与问题相关的片段。涉及产品或内部文档时请调用。"""
docs = vectorstore.similarity_search(query, k=2)
return "\n\n".join(d.page_content for d in docs)


model = init_chat_model(
"openai:ark-code-latest",
temperature=0,
api_key=api_key,
base_url=base_url,
)

agent = create_agent(
model=model,
tools=[search_knowledge],
system_prompt=(
"你是中文助手。回答知识库相关问题时先调用 search_knowledge,"
"再根据工具结果作答;没有依据就说不知道。"
),
)

result = agent.invoke(
{"messages": [{"role": "user", "content": "RAG 一般包含哪些步骤?"}]}
)
rprint(result["messages"][-1])

LCEL 链适合固定流水线;Agent 适合「检索 + 计算 + 其它 API」混用。
工具 docstring 要写清楚何时调用,否则模型可能跳过检索。

嵌入注意

聊天模型与嵌入模型是两套能力:ark-code-latest 不能当嵌入用。
生产环境用 init_embeddings 按厂商文档选择嵌入模型名,并继续走兼容的 api_key / base_url(若该端点支持嵌入)。

示意(模型名请换成你账号已开通的嵌入模型):

1
2
3
4
5
6
7
8
9
10
11
12
import os

from dotenv import load_dotenv
from langchain.embeddings import init_embeddings

load_dotenv()

embeddings = init_embeddings(
"openai:你的嵌入模型名",
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ["OPENAI_BASE_URL"],
)

若 Coding Plan 端点暂不提供嵌入,可改用其它已开通的嵌入服务,或本地 sentence-transformers 等方案;向量维度须与库中已有索引一致。

扩展

上线前可按优先级补强,本文不展开实现。

  1. 语料:Loader 读真实文件;切分参数按召回效果调(见《LangChain 07:文档加载器》《LangChain 08:文本切分器》)。
  2. 元数据:保留 source / 页码,答案中引用出处。
  3. 检索与重排k、相似度阈值、混合检索,以及 Rerank 精排(见《LangChain 11:检索重排序》)。
  4. 评估:固定问答集看命中率与幻觉率,而不是只看单次 demo。
  5. 更新:文档变更后的重建或增量索引策略。

验证

按下面顺序确认概要链路可用。

  1. 配置 .env,跑「索引入库」,应打印与 RAG 步骤相关的片段。
  2. 跑「最小问答链」,答案应提到加载、切分、向量化、检索等要点;若提示词或语料写了重排,也可出现。
  3. 跑「Agent 检索」,消息历史中应出现工具调用,最终回答仍依据语料。

总结

  1. RAG = 离线(加载 → 切分 → 嵌入 → 入库)+ 在线(检索 → 可选重排 → 增强 → 生成)。
  2. 必用模型:离线/检索侧的嵌入、在线的聊天;可选 Rerank;加载、普通切分、入库、拼提示词一般不用模型。
  3. 嵌入与 Rerank 多数优先在线 API;隐私/内网或高 QPS 再上本地;入库与检索须同一套嵌入模型。
  4. 固定流程用 LCEL + retriever;要模型自主查库用 create_agent + 检索 Tool。
  5. 演示可用假嵌入 + 内存库;上线换真实嵌入,并与聊天、Rerank 分开配置。
  6. 下一步按序看《07 文档加载器》《08 文本切分器》,再进入嵌入、Milvus 与重排专文。