让大模型真正懂你的数据:LangChain 文本向量、检索器与 RAG 实践
大语言模型擅长理解和生成语言,却不会自动知道企业内部的制度、项目文档或昨天刚更新的数据。要让模型回答这些问题,关键不是继续堆叠提示词,而是建立一条稳定的数据通路:把文档变成可比较的向量,存入适合相似度检索的存储,再把检索结果交给模型生成答案。
LangChain 将这条通路拆成了几个清晰的组件:嵌入模型负责把文本转换为向量,向量存储负责索引和搜索,检索器负责提供统一的查询接口,最后由链把上下文和问题组装后交给聊天模型。理解这些组件的边界,才能把 RAG 应用从"能跑"做成"可维护、可扩展"。
一、文本为什么要变成向量
计算机可以高效处理数字,却不能直接根据一段文字的含义判断两篇文档是否相近。嵌入(Embedding)做的事情,就是把单词、句子、商品、用户或图片转换为一个固定长度的数字列表,并尽量保留它们的语义关系。
例如,一段文本经过模型处理后可能得到这样的结果:
text
[0.023, 0.487, -0.129, ..., 0.325]
这个列表的长度叫向量维度。维度越高,通常能表达更细的语义差异,但计算、存储和索引成本也会增加。向量可以看作高维空间中的一个点,文本含义相近时,它们在这个空间里往往更接近。
常见的相似度度量包括:
- 欧氏距离:两点之间的直线距离,距离越小通常越相似。
- 余弦相似度:比较两个向量的方向,弱化文本长度带来的影响。语义检索中更常用这种方式,因为它更关注"表达的含义是否一致"。
向量检索解决的是传统数据库不擅长的问题。SQL 的等值查询适合查找明确的字段值,而向量检索可以找到"关于数据库分表的内容",即使原文没有出现完全相同的关键词。
二、嵌入模型的职责与调用方式
嵌入模型是表示型模型,目标是生成语义表示,而不是直接写出一段回答。聊天模型负责"生成",嵌入模型负责"表示",二者承担的任务不同,不能互相替代。
LangChain 为不同模型提供方提供了独立的集成包。以 OpenAI 为例:
bash
pip install -U langchain-openai
初始化模型时,密钥应放在环境变量中:
python
import os
from langchain_openai import OpenAIEmbeddings
os.environ["OPENAI_API_KEY"] = "your-api-key"
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
基础嵌入接口有两个重要方法:
python
documents_vector = embeddings.embed_documents([
"向量数据库用于语义检索",
"检索器把查询转换为文档列表",
])
query_vector = embeddings.embed_query("如何做语义检索?")
embed_documents 接收多个文档,返回二维列表,适合离线建立索引;embed_query 接收一个查询字符串,返回一维向量,适合用户提问时实时调用。模型提供方可能对文档和查询使用不同的预处理策略,因此这两个接口不应混用。
三、从原始文档到可搜索索引
一个可复用的索引流程通常是:加载文档、切分文本、生成嵌入、写入向量存储。
python
from langchain_community.document_loaders import UnstructuredMarkdownLoader
from langchain_text_splitters import CharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
loader = UnstructuredMarkdownLoader("docs/knowledge.md")
raw_documents = loader.load()
splitter = CharacterTextSplitter.from_tiktoken_encoder(
encoding_name="cl100k_base",
chunk_size=500,
chunk_overlap=80,
)
documents = splitter.split_documents(raw_documents)
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
texts = [doc.page_content for doc in documents]
vectors = embeddings.embed_documents(texts)
print(len(documents), len(vectors[0]))
切分参数直接影响检索质量。块太大,检索结果会夹带大量无关内容,增加上下文成本;块太小,语义可能被截断。适度重叠可以减少关键信息刚好落在边界两侧的情况。代码、表格和标题明显的文档,还可以使用针对结构的分割器,尽量保持一个逻辑单元的完整性。
索引是离线、批量的准备过程。生产系统通常会在文档新增或更新时增量处理,而不是每次用户提问都重新计算全部向量。
四、向量存储:把向量变成可用的数据服务
手动保存向量并逐个计算距离很快就会遇到性能和管理问题。向量存储一般会提供:
- 专用索引:使用近似最近邻等算法缩小候选范围,在规模和精度之间取得平衡。
- 高效计算:利用底层向量库、SIMD 或 GPU 并行执行相似度计算。
- 数据管理:支持新增、获取、删除、持久化、分布式扩展和元数据过滤。
LangChain 把不同后端统一成向量存储接口。开发阶段可以先使用内存存储验证链路:
python
from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embedding=embeddings)
ids = vector_store.add_documents(documents)
found = vector_store.get_by_ids(ids[:2])
vector_store.delete(ids=ids[:1])
内存存储适合测试和小规模实验,进程重启后数据会消失。需要持久化或多实例部署时,可以接入 Redis、Pinecone、Chroma、Qdrant、Milvus 等后端。选择后端时,应同时考虑数据规模、延迟、过滤需求、运维方式和成本,而不是只看向量检索速度。
元数据决定检索是否可控
每个 LangChain Document 至少包含文本内容和可选的元数据:
python
from langchain_core.documents import Document
doc = Document(
page_content="向量检索根据语义返回相关文档",
metadata={"source": "handbook.md", "category": "engineering", "version": 3},
)
元数据可以先于向量相似度做过滤,例如只搜索某个产品、某个版本或某个时间范围的资料。它能缩小候选集,也能避免把不同业务域的内容混在一起。使用 Redis 等后端时,应在初始化配置中声明字段类型,例如标签字段用 tag,数值字段用 numeric,否则后续过滤可能无法建立正确的索引。
五、相似性搜索与 MMR
最基本的搜索方式是 similarity_search:把查询嵌入成向量,在向量库中找到最相近的 k 个文档。
python
docs = vector_store.similarity_search(
query="如何设计数据库表?",
k=4,
)
需要调试召回质量时,可以同时返回分数:
python
scored_docs = vector_store.similarity_search_with_score(
query="如何设计数据库表?",
k=4,
)
for doc, score in scored_docs:
print(score, doc.metadata)
不同后端的分数定义可能不同,必须先确认"分数越高越相似"还是"分数越低越相似",不能直接跨数据库比较阈值。
单纯按相似度取前几名,可能得到内容高度重复的片段。最大边际相关性(MMR)会先取一个较大的候选集,再兼顾查询相关性和结果之间的差异性:
python
docs = vector_store.max_marginal_relevance_search(
query="如何提升服务性能?",
k=4,
fetch_k=16,
)
fetch_k 是第一阶段候选池大小,k 是最终返回数量。候选池太小,MMR 没有足够空间做多样化;候选池太大,则会增加计算量。MMR 特别适合摘要、推荐和 RAG,因为它能减少上下文重复,让模型看到更多互补信息。
六、检索器:统一不同数据源的查询入口
信息检索系统的目标,是从大规模数据中快速找到与用户需求相关的内容。关系数据库擅长结构化条件和关联查询,倒排索引擅长词法匹配,向量数据库擅长语义相似度。它们都可以被包装成检索器。
LangChain 检索器的契约很简单:输入查询字符串,输出标准化的 Document 列表。向量存储可以直接转换为检索器:
python
retriever = vector_store.as_retriever(
search_type="similarity",
search_kwargs={"k": 4},
)
docs = retriever.invoke("如何设计数据库表?")
还可以切换搜索策略:
python
retriever = vector_store.as_retriever(
search_type="mmr",
search_kwargs={"k": 4, "fetch_k": 16},
)
检索器本身是一个 Runnable,因此可以使用 invoke,也能参与表达式组合。不过检索通常是同步、阻塞的,retriever.stream() 不会把检索过程拆成逐块输出;真正可流式传输的通常是后续聊天模型生成的答案。
当内置检索器不够灵活时,也可以用 @chain 把自定义查询逻辑包装成相同的输入输出形态:
python
from typing import List
from langchain_core.documents import Document
from langchain_core.runnables import chain
@chain
def custom_retriever(query: str) -> List[Document]:
return vector_store.similarity_search(query, k=4)
这样可以把元数据筛选、权限判断或多路召回写进函数,同时继续复用 LangChain 的链式组合能力。
七、把检索器接入 RAG
RAG 的基本流程可以概括为:
- 用查询生成向量并召回相关文档。
- 把文档内容拼接成上下文。
- 将原始问题和上下文一起交给聊天模型。
- 通过输出解析器得到最终答案。
下面是一条完整但精简的 LCEL 链:
python
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
model = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_messages([
("human", """
你是一个严谨的问答助手。只根据上下文回答问题;上下文没有答案时,直接说不知道。
问题:{question}
上下文:{context}
回答:
"""),
])
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{
"context": retriever | format_docs,
"question": RunnablePassthrough(),
}
| prompt
| model
| StrOutputParser()
)
for chunk in rag_chain.stream("如何设计数据库表?"):
print(chunk, end="", flush=True)
RunnablePassthrough 会把原始问题原样传给提示词模板,同时让另一条分支负责检索和格式化上下文。这样,问题与上下文可以在同一个模板中汇合。需要注意,检索阶段仍然是准备工作,最终看到的流式输出来自模型生成阶段,而不是检索器本身。
八、让结果稳定的工程要点
保证维度一致。 创建向量索引时的维度必须与嵌入模型输出维度一致;更换模型时,通常需要重建索引,不能把不同维度或不同语义空间的向量混在一起。
固定模型与版本。 文档索引和在线查询必须使用同一套嵌入模型及兼容配置。模型升级后应通过离线评测确认召回质量,再决定是否迁移全部数据。
把切分当成数据建模。 先确定文档的逻辑结构,再选择块大小、重叠长度和分割器。对标题、代码、表格进行特殊处理,往往比盲目增大 k 更有效。
让元数据可用。 来源、租户、权限、版本、更新时间等信息应在入库时写入,并在检索前过滤。多租户系统尤其不能只依赖向量相似度来隔离数据。
区分召回和生成。 检索器返回的是证据片段,不是最终答案。提示词要明确要求模型只使用给定上下文,并在证据不足时拒答,避免把"相似"误当成"事实"。
用评测而不是感觉调参。 记录问题、召回文档、相似度分数和最终回答,分别评估召回率、上下文相关性、答案准确性和延迟。k、fetch_k、过滤条件和切分参数都应通过一组固定问题对比验证。
结语
文本向量把"含义"变成了可以计算的坐标,向量存储把这些坐标组织成可扩展的索引,检索器则把不同后端统一成简单的查询接口。再借助 LCEL,将检索结果与原始问题交给聊天模型,便形成了一条清晰的 RAG 数据通路。
真正可靠的应用并不取决于某个单独组件,而取决于整条链路是否闭环:数据切分合理、嵌入空间稳定、检索结果相关且多样、元数据过滤正确,最后让模型在有证据的范围内生成答案。掌握这套组件化思路后,无论后端使用内存存储、Redis 还是托管向量数据库,都可以在同一个编程模型下逐步演进。