LangChain文本向量与检索器实践

让大模型真正懂你的数据: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 的基本流程可以概括为:

  1. 用查询生成向量并召回相关文档。
  2. 把文档内容拼接成上下文。
  3. 将原始问题和上下文一起交给聊天模型。
  4. 通过输出解析器得到最终答案。

下面是一条完整但精简的 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 更有效。

让元数据可用。 来源、租户、权限、版本、更新时间等信息应在入库时写入,并在检索前过滤。多租户系统尤其不能只依赖向量相似度来隔离数据。

区分召回和生成。 检索器返回的是证据片段,不是最终答案。提示词要明确要求模型只使用给定上下文,并在证据不足时拒答,避免把"相似"误当成"事实"。

用评测而不是感觉调参。 记录问题、召回文档、相似度分数和最终回答,分别评估召回率、上下文相关性、答案准确性和延迟。kfetch_k、过滤条件和切分参数都应通过一组固定问题对比验证。

结语

文本向量把"含义"变成了可以计算的坐标,向量存储把这些坐标组织成可扩展的索引,检索器则把不同后端统一成简单的查询接口。再借助 LCEL,将检索结果与原始问题交给聊天模型,便形成了一条清晰的 RAG 数据通路。

真正可靠的应用并不取决于某个单独组件,而取决于整条链路是否闭环:数据切分合理、嵌入空间稳定、检索结果相关且多样、元数据过滤正确,最后让模型在有证据的范围内生成答案。掌握这套组件化思路后,无论后端使用内存存储、Redis 还是托管向量数据库,都可以在同一个编程模型下逐步演进。

相关推荐
larance19 小时前
机器学习特征预处理之特征选择
人工智能·机器学习
lisw0519 小时前
AI在高端制造中的具体技术瓶颈是什么?预计何时能突破?
人工智能·机器学习·制造
画中有画19 小时前
边缘计算技术的设计与实现
人工智能·边缘计算
Cosolar19 小时前
深入理解 AI Agent:设计原理与工程实践
人工智能·面试·github
LitchiCheng19 小时前
轻量化微调 Qwen2.5 LoRA 训练全流程详解
大数据·人工智能·spark
糖果店的幽灵20 小时前
【langgraph 从入门到精通graphApi 篇】Memory 与长期记忆
运维·服务器·人工智能·langgraph
火山引擎开发者社区20 小时前
一句话上线 AI Agent 应用:火山 Supabase + IGA Pages 全栈部署实践
人工智能
火山引擎开发者社区20 小时前
AI Agent 真正进入工作流:AgentKit CLI 打造云端沙箱开发环境
人工智能
蓝速科技20 小时前
蓝速 AI 双屏翻译机:实体商铺跨境沟通落地指南
人工智能