# LangChain RAG 应用实践:从向量库检索到可上线问答链

前面已经分别讲过 FAISS、Pinecone、TCVectorDB、Weaviate 和自定义 VectorStore。到这一步,向量数据库不再是孤立工具,而是要进入完整 RAG 应用。

一个最小 RAG 应用看起来很简单:用户提问,Retriever 找文档,Prompt 拼上下文,LLM 生成答案。真正上线后,问题会多很多:向量库连接放在哪里、检索结果怎么格式化、权限过滤怎么传入、答案如何带来源、历史消息是否参与检索、失败时怎么降级、怎么评估回答是否忠实。

所以这篇不写“能跑就行”的示例,而是按生产服务拆分来写第一个 LangChain RAG 应用。

# RAG 链路图

LangChain RAG 应用流程

# 架构协作图

LangChain RAG 应用架构

这条链路可以拆成两段。

第一段是离线入库:文档解析、文本切分、Embedding、写入向量数据库。

第二段是在线问答:用户提问、检索相关 chunk、填充 Prompt、调用模型、解析答案、返回引用和 trace。

不要把这两段混在一个接口里。入库链路是数据工程,问答链路是在线服务,它们的吞吐、错误处理、重试策略和监控指标都不一样。

# 当前推荐写法

早期 LangChain RAG 示例经常使用 RetrievalQA 或 create_retrieval_chain。这些写法在旧项目里还能见到,但新项目更建议理解 LCEL 和 Runnable,把检索、格式化、Prompt、模型、解析显式串起来。

这样做有几个好处:

  • 每个节点都能单独测试。
  • 权限过滤、历史消息、重排、引用返回更容易插入。
  • trace 更清晰。
  • 后续切换向量库或模型更容易。

最小链路可以概括为:

{
    "context": itemgetter("query") | retriever | format_documents,
    "query": itemgetter("query"),
} | prompt | llm | parser
1
2
3
4

生产版会在这条链路外面再加上权限、记忆、日志、评估、异常处理和引用结构化返回。

# 安装依赖

以 Weaviate 做示例:

pip install -U langchain-core langchain-openai langchain-weaviate weaviate-client
1

环境变量:

export OPENAI_API_KEY="your-openai-api-key"
export WEAVIATE_HOST="localhost"
export WEAVIATE_PORT="8080"
export WEAVIATE_GRPC_PORT="50051"
export WEAVIATE_COLLECTION="Dataset"
1
2
3
4
5

生产里这些配置不要散落在业务代码里,应该由配置中心、环境变量或密钥管理系统统一注入。

# 封装向量数据库服务

向量库 client 不应该每次请求都创建。它和数据库连接类似,应该在服务启动时初始化,然后复用。

import os
from dataclasses import dataclass

import weaviate
from langchain_openai import OpenAIEmbeddings
from langchain_weaviate import WeaviateVectorStore


@dataclass(frozen=True)
class VectorDatabaseSettings:
    host: str
    port: int
    grpc_port: int
    collection_name: str
    text_key: str = "text"
    embedding_model: str = "text-embedding-3-small"


class VectorDatabaseService:
    def __init__(self, settings: VectorDatabaseSettings) -> None:
        self.settings = settings
        self.client = weaviate.connect_to_local(
            host=settings.host,
            port=settings.port,
            grpc_port=settings.grpc_port,
        )
        self.embeddings = OpenAIEmbeddings(model=settings.embedding_model)
        self.vector_store = WeaviateVectorStore(
            client=self.client,
            index_name=settings.collection_name,
            text_key=settings.text_key,
            embedding=self.embeddings,
        )

    def as_retriever(self, *, k: int = 5, filters=None):
        return self.vector_store.as_retriever(
            search_kwargs={
                "k": k,
                "filters": filters,
            }
        )

    def close(self) -> None:
        self.client.close()


def build_vector_database_service() -> VectorDatabaseService:
    settings = VectorDatabaseSettings(
        host=os.environ["WEAVIATE_HOST"],
        port=int(os.environ.get("WEAVIATE_PORT", "8080")),
        grpc_port=int(os.environ.get("WEAVIATE_GRPC_PORT", "50051")),
        collection_name=os.environ["WEAVIATE_COLLECTION"],
    )
    return VectorDatabaseService(settings)
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

这层只负责连接和暴露 Retriever,不负责拼 Prompt,也不负责业务权限。边界越清晰,后面越好维护。

# 格式化检索结果

Retriever 返回的是 Document 列表。模型不能直接理解 Python 对象,需要把它格式化成上下文字符串。

from langchain_core.documents import Document


def format_documents(documents: list[Document]) -> str:
    blocks = []

    for index, document in enumerate(documents, start=1):
        source = document.metadata.get("source", "unknown")
        page = document.metadata.get("page", "unknown")
        content = document.page_content.strip()

        blocks.append(
            f"[{index}] source={source} page={page}\n{content}"
        )

    return "\n\n".join(blocks)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

格式化时建议保留来源、页码、文档 id、chunk id。RAG 不是只要答案,还要能追溯答案来自哪里。

# Prompt 设计

一个朴素但可上线前继续扩展的 Prompt:

from langchain_core.prompts import ChatPromptTemplate


prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            """你是一个严谨的知识库问答助手。
只能根据给定上下文回答问题。
如果上下文不足以回答,直接说明无法从当前知识库确认。
回答后给出使用到的来源编号。""",
        ),
        (
            "human",
            """问题:
{query}

上下文:
{context}

请输出:
1. 答案
2. 来源编号""",
        ),
    ]
)
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

这里最重要的是约束模型不要自由发挥。RAG 的目标不是让模型“更会编”,而是让答案被可检索证据约束。

# 构建 LCEL 问答链

使用 LCEL 把检索、格式化、Prompt、模型和解析串起来:

from operator import itemgetter

from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser


llm = init_chat_model("gpt-5-mini", model_provider="openai")
parser = StrOutputParser()


def build_generation_chain():
    return prompt | llm | parser


def build_rag_chain(retriever):
    return (
        {
            "context": itemgetter("query") | retriever | format_documents,
            "query": itemgetter("query"),
        }
        | prompt
        | llm
        | parser
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

调用:

vector_database_service = build_vector_database_service()
retriever = vector_database_service.as_retriever(k=5)
chain = build_rag_chain(retriever)

answer = chain.invoke(
    {
        "query": "企业用户如何申请发票?",
    }
)
1
2
3
4
5
6
7
8
9

这就是最小 RAG 闭环:query 进入 Retriever,检索结果变成 context,context 和 query 填入 Prompt,模型生成答案。

# 加入权限过滤

生产 RAG 必须考虑权限。不能只根据语义相似度召回,也要保证用户只能看到自己有权限的文档。

以 Weaviate filter 为例:

from weaviate.classes.query import Filter


def build_acl_filter(auth_context, knowledge_base_id: str):
    if knowledge_base_id not in auth_context.allowed_knowledge_base_ids:
        raise PermissionError("knowledge base not allowed")

    return (
        Filter.by_property("tenant_id").equal(auth_context.tenant_id)
        & Filter.by_property("knowledge_base_id").equal(knowledge_base_id)
        & Filter.by_property("status").equal("published")
    )
1
2
3
4
5
6
7
8
9
10
11
12

查询时创建带权限过滤的 retriever:

filters = build_acl_filter(auth_context, knowledge_base_id="kb-help-center")
retriever = vector_database_service.as_retriever(k=5, filters=filters)
chain = build_rag_chain(retriever)
1
2
3

注意,权限过滤必须由服务端根据认证态生成。不要把客户端传来的过滤条件直接交给向量库。

# 返回答案和引用

只返回字符串不利于排查。生产接口建议返回结构化结果:

from dataclasses import dataclass


@dataclass(frozen=True)
class RagAnswer:
    answer: str
    sources: list[dict]
    trace_id: str
1
2
3
4
5
6
7
8

为了返回引用,可以把“检索”和“生成”拆开执行:

import uuid


def answer_question(query: str, retriever, generation_chain) -> RagAnswer:
    trace_id = str(uuid.uuid4())
    documents = retriever.invoke(query)
    context = format_documents(documents)

    answer = generation_chain.invoke(
        {
            "query": query,
            "context": context,
        }
    )

    sources = [
        {
            "source": doc.metadata.get("source"),
            "page": doc.metadata.get("page"),
            "document_id": doc.metadata.get("document_id"),
            "chunk_id": doc.metadata.get("id"),
        }
        for doc in documents
    ]

    return RagAnswer(answer=answer, sources=sources, trace_id=trace_id)
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

这段结构比“一条 LCEL 到底”更啰嗦,但生产里经常更好用,因为你能明确拿到检索文档、引用和 trace。

# 和记忆结合

聊天机器人通常还需要历史消息。这里要分清两件事:

  • 历史消息用于理解用户当前问题。
  • 检索结果用于提供事实依据。

不要把所有历史消息都塞进检索 query,也不要把历史消息直接当知识库证据。

更稳的结构是:

  1. 根据历史消息和当前问题改写一个独立 query。
  2. 用改写后的 query 检索知识库。
  3. 把检索结果和必要历史摘要一起放入 Prompt。
  4. 生成答案并保存当前轮对话。

如果只是第一版应用,可以先不做问题改写,直接用当前用户问题检索。等评估发现多轮追问召回差,再加 query rewrite。

# 常见失败链路

RAG 失败通常不是“模型不行”四个字能解释的。

现象 优先排查
完全答不上 文档是否入库、切分是否合理、Embedding 是否一致
答案像编的 Prompt 是否要求基于上下文、上下文是否为空
有正确文档但没召回 chunk 粒度、top_k、filter、Embedding 模型
召回了但答案错 上下文太长、噪声太多、模型未被约束
用户看到无权限内容 filter 是否由服务端生成,metadata 是否完整
偶发延迟高 向量库延迟、模型延迟、重排延迟、网络抖动

生产排障一定要保留 trace:原始 query、改写 query、filter、retrieved docs、score、prompt、model、answer、latency。

# 评估第一版 RAG

上线前至少准备一组评估问题。

指标 看什么
Recall@K 正确 chunk 是否被召回
MRR 正确 chunk 排名是否靠前
Groundedness 答案是否被上下文支持
Answer correctness 答案是否正确
Citation accuracy 引用是否真的支持答案
Latency 检索和生成耗时

没有评估集时,RAG 优化很容易变成“试几个问题感觉还行”。生产里每次调整 chunk、Embedding、top_k、filter、Prompt、模型都应该跑一次回归。

# 上线前检查清单

  • 向量库 client 是否复用,而不是每次请求创建。
  • Retriever 是否带权限过滤。
  • metadata 是否包含租户、知识库、文档 id、chunk id、状态、来源。
  • Prompt 是否明确要求基于上下文回答。
  • 空检索结果是否有兜底回复。
  • 是否返回 sources 和 trace_id。
  • 是否记录 query、filter、k、score、latency、answer。
  • 是否有固定评估集。
  • 是否区分离线入库任务和在线问答请求。
  • 是否有索引版本切换和回滚策略。

# 问题

第一个 RAG 应用最容易犯的错误有几个。

第一,把向量库连接写在请求函数里。每次请求重新连接会带来延迟和资源浪费。

第二,只返回答案,不返回来源。短期看省事,长期无法排查和建立信任。

第三,权限过滤靠前端传参。真正的权限条件必须由服务端认证态生成。

第四,把历史消息和知识库上下文混在一起。历史消息帮助理解问题,知识库上下文提供事实依据。

第五,不做评估集。没有评估集,就不知道一次改动是提升还是退化。

# 拓展

第一,加入 query rewrite,让多轮追问能变成独立检索问题。

第二,加入 reranker,对向量召回结果重排,提高上下文质量。

第三,加入混合检索,把关键词、向量和业务规则结合起来。

第四,加入引用校验,确认答案中的关键结论确实被来源支持。

第五,加入 LangSmith 或自研 trace,把检索和生成过程完整记录下来。

# 实际生产是否使用

会使用。

这种 2-step RAG 是企业知识库、智能客服、文档问答、内部助手最常见的生产形态。它执行路径固定、延迟可控、排查简单,适合第一版上线。

但生产里不会只写一条 retriever | prompt | llm。真正可用的系统还要有入库流水线、权限过滤、metadata schema、引用返回、trace、评估集、灰度发布和回滚。

# 现在是否抛弃

没有抛弃。

RAG 仍然是解决模型静态知识、私有知识和可追溯回答的重要方案。需要更新的是实现方式:旧的 RetrievalQA 类和一些 langchain.chains 写法已经不适合作为新项目主路径。

新项目更推荐使用当前 LangChain 的 Runnable、LCEL、VectorStore、Retriever 和显式服务拆分。如果要做复杂工具调用和多步推理,再考虑 Agentic RAG 或 LangGraph。

# 最新生产如何实现

当前更推荐这样落地:

第一,离线入库和在线问答分离。入库负责解析、切分、Embedding、写向量库、评估和发布。

第二,在线服务启动时初始化向量库 client 和 VectorStore,按请求构造带权限 filter 的 Retriever。

第三,RAG 链路使用 LCEL 显式组合:query、retriever、format_documents、prompt、model、parser。

第四,答案接口返回 answer、sources、trace_id,而不是只返回字符串。

第五,保留空检索兜底:没有上下文时不要硬答。

第六,所有检索和生成过程进入 trace,并用评估集做回归。

一个更接近生产的入口:

def run_rag_query(query: str, auth_context, knowledge_base_id: str):
    filters = build_acl_filter(auth_context, knowledge_base_id)
    retriever = vector_database_service.as_retriever(k=5, filters=filters)

    documents = retriever.invoke(query)
    if not documents:
        return RagAnswer(
            answer="当前知识库中没有检索到足够信息,无法确认答案。",
            sources=[],
            trace_id=str(uuid.uuid4()),
        )

    generation_chain = build_generation_chain()
    return answer_question(
        query=query,
        retriever=retriever,
        generation_chain=generation_chain,
    )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这段代码只是入口骨架。真实上线时,还要把 vector_database_service 做成应用级单例,把 trace 写入日志系统,把异常降级和评估回放接好。

# 总结

第一个 LangChain RAG 应用的重点不是“把几个组件串起来”,而是把链路边界想清楚:入库归入库,检索归检索,生成归生成,权限和观测贯穿全程。

能跑的 RAG 很快就能写出来;能维护、能排查、能评估、能回滚的 RAG,才是真正能进生产的版本。

参考: