跳转至

第3讲:LangChain 生态系统

上一讲RAG 核心概念深入 下一讲Milvus 索引机制与基本操作


本讲导入

LangChain 的组件很多:Runnable、Prompt、Message、Parser、History、Loader、Splitter、VectorStore 都是常见概念。学习这些组件时,最重要的不是记住每个类名,而是看清它们在企业级 RAG 项目中分别出现在什么位置、解决什么工程问题。

本讲只围绕三个问题:

  1. LangChain 在本项目里到底是什么角色?
  2. 一次在线问答中,哪些步骤用到了 LangChain?
  3. 一次离线入库中,哪些步骤用到了 LangChain?

先记住一句话:

本项目没有把 LangChain 当成“一键 RAG 框架”,而是把它当成一组可靠的工程适配器:模型适配器、消息对象、结构化输出、历史记录、文档对象、加载器、切分器和向量库封装。


本讲目标

学完本讲后,需要能说清楚:

  • 为什么项目用 ChatOpenAI 接入 DashScope,而不是直接绑定某个厂商 SDK。
  • SystemMessageHumanMessageAIMessage 在多轮对话和 Prompt 中分别承担什么角色。
  • with_structured_output() 为什么比让模型返回自由文本更适合意图识别和查询变体生成。
  • SQLChatMessageHistoryDocumentTextSplitterMilvus VectorStore 分别位于项目哪条链路。
  • 为什么本项目没有直接使用 RetrievalQAConversationalRetrievalChain 或一条 LCEL 管道完成全部 RAG。

本讲地图

本图只展示本项目真正使用的 LangChain 能力:它们是工程适配器,不替代业务编排。

图 1:第 03 讲功能闭环地图

flowchart TD
    C03_CHAT["模型适配<br/>get_chat_model()"]
    C03_MSG["对话消息<br/>format_messages()"]
    C03_STRUCT["结构化输出<br/>with_structured_output(QueryVariants)"]
    C03_DOC["文档对象<br/>load_file()"]
    C03_SPLIT["文本切分<br/>split_documents()"]
    C03_VECTOR{{"向量库封装<br/>MilvusHybridStore"}}
    C03_CHAT --> C03_MSG
    C03_MSG --> C03_STRUCT
    C03_STRUCT --> C03_DOC
    C03_DOC --> C03_SPLIT
    C03_SPLIT --> C03_VECTOR
    style C03_CHAT fill:#F8FAFC,stroke:#64748B,stroke-width:2px
    style C03_MSG fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
    style C03_STRUCT fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
    style C03_DOC fill:#FEF3C7,stroke:#D97706,stroke-width:2px
    style C03_SPLIT fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
    style C03_VECTOR fill:#DCFCE7,stroke:#16A34A,stroke-width:2px

节点与代码对齐

节点 对齐文件 函数/对象 本章职责
模型适配 qa_core/llm/client.py get_chat_model() 用 ChatOpenAI 兼容接口接入 DashScope 等模型服务。
对话消息 qa_core/memory/history.py format_messages() 用消息对象组织多轮历史和追问上下文。
结构化输出 qa_core/pipeline/query_variants.py with_structured_output(QueryVariants) 让模型输出稳定的查询变体结构。
文档对象 qa_core/indexing/document_loaders.py load_file() 把多格式资料加载成 LangChain Document。
文本切分 qa_core/indexing/chunking.py split_documents() 把长文档切成可检索 chunk。
向量库封装 qa_core/retrieval/store.py MilvusHybridStore 封装 langchain-milvus 与 PyMilvus 的检索边界。

第一部分:LangChain 在项目中的角色

1.1 LangChain 不是完整业务流程

LangChain 不是模型,也不是知识库,更不是项目的业务大脑。它更像一组标准接口:

项目问题 LangChain 提供的抽象
不同 LLM 厂商 API 不一致 ChatOpenAI 等 ChatModel 统一接口
多轮对话消息结构容易混乱 SystemMessage / HumanMessage / AIMessage
LLM 输出格式不稳定 with_structured_output() + Pydantic
历史消息要持久化 SQLChatMessageHistory
文件格式不同 Document Loader
大文档需要切块 Text Splitter
向量库写入和检索 API 复杂 VectorStore

也就是说,LangChain 解决的是接口标准化工程胶水问题。本项目真正的业务流程仍然由 qa_core 自己编排。

1.2 本项目的使用边界

flowchart TB
    subgraph Use["本项目重点使用"]
        A["ChatOpenAI<br/>统一 LLM 调用"]
        B["Message Types<br/>多轮对话消息"]
        C["Structured Output<br/>意图/变体结构化"]
        D["SQLChatMessageHistory<br/>MySQL 历史"]
        E["Document / Loader / Splitter<br/>离线入库"]
        F["Milvus VectorStore<br/>向量库封装"]
    end

    subgraph Light["本项目只讲清楚,不作为主链路"]
        G["Runnable<br/>invoke / stream / batch"]
        H["LCEL<br/>prompt | model | parser"]
    end

    subgraph Avoid["本项目不直接采用"]
        I["RetrievalQA"]
        J["ConversationalRetrievalChain"]
        K["Agent 自动决策"]
    end

    Use --> Light --> Avoid

    style Use fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style Light fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
    style Avoid fill:#FEF2F2,stroke:#DC2626,stroke-width:2px

这里要特别区分:不用高层 Chain,不代表不用 LangChain。本项目用的是 LangChain 的底层稳定组件,把分支、阈值、追问改写、FAQ 直出、文档检索、Prompt 档位这些业务决策留在项目代码里。


第二部分:一张图看清两条主线

本项目中 LangChain 的使用分成两条线:

  1. 在线问答链路:用户提问 -> 意图识别 -> 检索准备 -> Prompt -> LLM 流式生成。
  2. 离线入库链路:业务文件 -> Document -> 切分 -> Milvus VectorStore 写入。
flowchart LR
    subgraph Online["在线问答链路"]
        Q["用户问题"]
        M1["Message<br/>历史上下文"]
        I["Structured Output<br/>意图/查询变体"]
        P["Prompt Profile<br/>System + Human"]
        L["ChatOpenAI.stream()<br/>流式生成"]
    end

    subgraph Offline["离线入库链路"]
        F["PDF / Word / MD / Excel"]
        D["Document Loader<br/>统一成 Document"]
        S["Text Splitter<br/>父子块切分"]
        V["Milvus VectorStore<br/>add_documents"]
    end

    subgraph Store["Milvus"]
        FAQ[("FAQ 集合")]
        DOC[("文档集合")]
    end

    Q --> M1 --> I --> P --> L
    F --> D --> S --> V --> DOC
    I --> FAQ
    P --> DOC

    style Online fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
    style Offline fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style Store fill:#FFFBEB,stroke:#D97706,stroke-width:2px

这张图就是本讲主线。后面所有组件都放回这两条线里讲。


第三部分:在线问答链路中的 LangChain

3.1 ChatOpenAI:统一模型调用入口

项目文件:qa_core/llm/client.py

本项目实际使用 DashScope 的 OpenAI-compatible 接口,但代码里不直接写 DashScope SDK,而是统一用 LangChain 的 ChatOpenAI

from functools import lru_cache

from langchain_core.messages import HumanMessage
from langchain_openai import ChatOpenAI

from qa_core.config.settings import get_settings


@lru_cache(maxsize=2)
def get_chat_model(streaming: bool = False) -> ChatOpenAI:
    settings = get_settings()
    return ChatOpenAI(
        model=settings.llm_model,
        api_key=settings.llm_api_key,
        base_url=settings.llm_base_url,
        temperature=settings.llm_temperature,
        timeout=settings.llm_timeout,
        streaming=streaming,
    )

这里的设计点:

  • base_url 可以指向 DashScope、DeepSeek 或其他 OpenAI-compatible 服务。
  • streaming=False 用于意图识别、查询改写、结构化输出。
  • streaming=True 用于最终答案生成,方便 WebSocket 逐 token 推送。
  • @lru_cache(maxsize=2) 只缓存两个客户端实例,不缓存模型答案。

3.2 Message:让多轮对话结构稳定

LangChain 的消息对象对应 OpenAI API 的角色格式:

LangChain 对象 API role 项目用途
SystemMessage system 设定助手身份、回答边界、风险约束
HumanMessage user 用户问题、改写请求、检索上下文问题
AIMessage assistant 历史回答,进入多轮上下文

项目文件:qa_core/memory/history.py

from langchain_core.messages import AIMessage, HumanMessage, SystemMessage


def add_turn(self, session_id: str, question: str, answer: str) -> None:
    history = self.for_session(session_id)
    history.add_messages([
        HumanMessage(content=question),
        AIMessage(content=answer),
    ])


def get_context_messages(self, session_id: str):
    recent = self.get_messages(session_id, limit=self.settings.history_recent_messages)
    summary = self.get_summary(session_id)
    if summary:
        return [SystemMessage(content=f"历史摘要:{summary}")] + recent
    return recent

需要理解:LLM 本身没有会话记忆。所谓“多轮对话”,本质是每次调用模型时,把必要的历史消息重新发给模型。

sequenceDiagram
    participant U as 用户
    participant API as QAService
    participant H as SQLChatMessageHistory
    participant LLM as ChatOpenAI

    U->>API: 入职流程有哪些步骤?
    API->>LLM: [System, Human]
    LLM-->>API: AIMessage
    API->>H: 保存 Human + AI

    U->>API: 那审批需要多久?
    API->>H: 读取最近历史
    API->>LLM: [System, Human, AI, Human]
    LLM-->>API: 能理解“审批”指入职审批

3.3 Structured Output:把 LLM 输出变成业务对象

项目文件:qa_core/intent/classifier.py

意图识别不能让模型自由发挥。自由文本会出现这些情况:

1
2
3
这是一个知识库查询问题。
用户应该是在问 FAQ。
INTENT = KNOWLEDGE_QUERY, confidence is high.

这些格式都不稳定。项目里使用 Pydantic 结构约束:

from pydantic import BaseModel, Field


class QueryVariants(BaseModel):
    variants: list[str] = Field(description="查询变体列表")


model = get_chat_model(streaming=False).with_structured_output(QueryVariants)
decision = model.invoke([
    SystemMessage(content="请为用户问题生成 2-3 个等价查询变体。"),
    HumanMessage(content="用户问题:入职流程有哪些步骤?"),
])

返回值不是字符串,而是一个 Pydantic 对象:

decision.variants         # ["入职流程有哪些步骤?", "新人入职流程", ...]

这一步是 LangChain 在项目中非常关键的价值:让 LLM 的输出进入可校验、可分支、可记录的工程世界

同样的方式也用于查询变体生成。

3.4 Prompt Profile:不是一个 Prompt 走天下

项目文件:

  • qa_core/prompts/profiles.py
  • qa_core/prompts/selector.py

项目没有把所有问题都塞进同一个 Prompt,而是按意图和风险类别选择不同模板:

PROMPT_PROFILES = {
    "FAQ_QUERY": PromptProfile(
        name="faq_answer",
        system_template=FAQ_ANSWER_SYSTEM_PROMPT,
        user_template=FAQ_ANSWER_USER_TEMPLATE,
        reason="FAQ 类问题优先复用标准答案,控制回答长度和业务口径。",
    ),
    "KNOWLEDGE_QUERY": PromptProfile(
        name="knowledge_answer",
        system_template=KNOWLEDGE_ANSWER_SYSTEM_PROMPT,
        user_template=KNOWLEDGE_ANSWER_USER_TEMPLATE,
        reason="业务知识咨询需要整合文档资料。",
    ),
    "FOLLOW_UP": PromptProfile(
        name="follow_up",
        system_template=FOLLOW_UP_ANSWER_SYSTEM_PROMPT,
        user_template=FOLLOW_UP_ANSWER_USER_TEMPLATE,
        reason="追问需要结合历史理解指代。",
    ),
}

可以这样理解:

  • Prompt 不是一段固定文案,而是回答策略配置
  • system_template 控制助手身份、边界、风险口径。
  • user_template 注入历史、检索上下文、用户问题。
  • reason 进入调试信息,帮助解释为什么选择这个模板。

3.5 最终答案:ChatOpenAI.stream() 推给前端

项目文件:qa_core/pipeline/steps.py

1
2
3
4
5
6
7
8
9
from langchain_core.messages import HumanMessage, SystemMessage


def stream_llm_answer(system_prompt: str, user_prompt: str):
    llm = get_chat_model(streaming=True)
    return llm.stream([
        SystemMessage(content=system_prompt),
        HumanMessage(content=user_prompt),
    ])

项目主流程会把每个 chunk 转成 WebSocket token 事件:

1
2
3
4
5
for chunk in stream_llm_answer(answer_prepared.system_prompt, answer_prepared.user_prompt):
    token = str(getattr(chunk, "content", "") or "")
    if not token:
        continue
    yield build_token_event(token, context.session_id)

注意:这里不是 LangChain 替我们完成整个 RAG。LangChain 只负责模型流式调用;FAQ 直出、文档检索、上下文筛选、引用补强、写历史这些仍由项目代码控制。


第四部分:离线入库链路中的 LangChain

在线问答依赖的是“可检索的知识库”。这个知识库来自离线入库链路:

flowchart LR
    A["业务文件<br/>PDF/Word/MD/Excel"] --> B["Loader<br/>转成 Document"]
    B --> C["Normalizer<br/>补齐 metadata"]
    C --> D["Splitter<br/>父子块切分"]
    D --> E["Milvus VectorStore<br/>add_documents"]
    E --> F[("Milvus<br/>dense + sparse")]

    style A fill:#EFF6FF,stroke:#2563EB
    style D fill:#ECFDF5,stroke:#059669
    style F fill:#FFFBEB,stroke:#D97706

4.1 Document:入库链路的统一数据结构

LangChain 的 Document 很简单,只有两个核心字段:

from langchain_core.documents import Document

doc = Document(
    page_content="入职流程包括提交材料、签订劳动合同、部门审批和账号开通。",
    metadata={
        "source": "hr",
        "file_name": "入职制度.md",
        "page": 1,
    },
)

本项目的约定:

  • page_content 存正文,用于 embedding、BM25 和最终上下文。
  • metadata 存来源、场景、权限、知识库版本、文件名、行号等治理字段。

需要记住:RAG 入库不是只存文本,还要存 metadata。没有 metadata,就无法做来源引用、权限过滤、版本隔离和质量追踪。

4.2 Document Loader:不同文件格式统一成 Document

项目文件:qa_core/indexing/document_loaders.py

项目用注册表模式管理 Loader,而不是在主流程里写一堆 if/elif

DOCUMENT_LOADER_SPECS = (
    DocumentLoaderSpec(
        suffixes=(".txt", ".md"),
        factory=_utf8_text_loader,
        description="UTF-8 文本/Markdown",
    ),
    DocumentLoaderSpec(
        suffixes=(".pdf",),
        factory=_pdf_loader,
        description="PDF 文档",
    ),
    DocumentLoaderSpec(
        suffixes=(".docx",),
        factory=_word_loader,
        description="Word 文档",
    ),
    DocumentLoaderSpec(
        suffixes=(".csv", ".xlsx", ".xls"),
        factory=_table_loader,
        description="表格文件",
    ),
)

重点不是背 Loader 名称,而是理解模式:

文件格式不同,但进入后续入库链路之前,都要统一成 list[Document]

4.3 Text Splitter:为什么默认使用 RecursiveCharacterTextSplitter

项目文件:qa_core/indexing/chunking.py

from langchain_text_splitters import RecursiveCharacterTextSplitter


CHINESE_SEPARATORS = [
    "\n\n",
    "\n",
    "。", "!", "?", ";",
    ";", ".", "!", "?",
    ",", ",",
    " ",
    "",
]

parent_splitter = RecursiveCharacterTextSplitter(
    chunk_size=settings.parent_chunk_size,
    chunk_overlap=settings.parent_overlap,
    separators=CHINESE_SEPARATORS,
)

child_splitter = RecursiveCharacterTextSplitter(
    chunk_size=settings.child_chunk_size,
    chunk_overlap=settings.child_overlap,
    separators=CHINESE_SEPARATORS,
)

为什么用它作为默认方案:

  • 它快、稳定、便宜,不依赖 embedding 模型。
  • chunk 大小可控,适合向量库检索。
  • 对制度、流程、手册、FAQ、Markdown 这类企业资料足够可靠。
  • 参数容易解释,适合学习、排查和生产调优。

SemanticChunker 不是不能用,但不适合作为默认切分器。它更适合无明显结构、话题自然漂移的长文,例如访谈、会议纪要、研究报告。企业 RAG 的主链路更需要稳定、可控、可复现。

4.4 MarkdownHeaderTextSplitter:先保留章节结构

项目中 Markdown 文件会先按标题拆分,再进入递归切分:

1
2
3
4
5
from langchain_text_splitters import MarkdownHeaderTextSplitter

markdown_headers = [("#", "h1"), ("##", "h2"), ("###", "h3")]
header_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=markdown_headers)
header_docs = header_splitter.split_text(doc.page_content)

这样做的好处是:标题层级会进入 metadata,后续回答可以显示更清晰的来源,例如:

来源:入职制度.md > 入职管理 > 入职流程

4.5 父子块策略:检索要精确,回答要完整

flowchart TD
    Doc["原始文档"] --> Parent["父块<br/>较大,保留完整上下文"]
    Parent --> Child["子块<br/>较小,用于精确召回"]
    Child --> VectorDB[("Milvus<br/>page_content = 子块")]
    Parent --> Meta["metadata.parent_content<br/>保存父块全文"]
    VectorDB --> Hit["检索命中子块"]
    Meta --> Context["回答时补充父块上下文"]

    style VectorDB fill:#ECFDF5,stroke:#059669
    style Context fill:#EFF6FF,stroke:#2563EB

一句话解释:

子块负责“找得准”,父块负责“答得完整”。

更深入的 chunk size、overlap 和质量检查放到 附录 G:文档切分策略 和第 16 讲展开。本讲只建立在 LangChain 生态中的位置感。

4.6 VectorStore:把 Document 写入 Milvus

项目文件:qa_core/retrieval/store.py

from langchain_milvus import Milvus


self._store = Milvus(
    embedding_function=get_embeddings(),
    builtin_function=bm25_function(),
    collection_name=self.collection_name,
    connection_args=connection_args,
    vector_field=["dense", "sparse"],
    text_field="text",
    primary_field="pk",
    auto_id=False,
    enable_dynamic_field=True,
    consistency_level="Session",
    drop_old=False,
)

这里先只讲抽象:

  • embedding_function 生成 dense 向量。
  • builtin_function=bm25_function() 让 Milvus 服务端生成 sparse 向量。
  • add_documents() 写入 Document
  • similarity_search_with_score() 检索并返回 Document + score

下一讲再进入 Milvus 自身:Collection、Schema、Index、Load、Insert、Search。


第五部分:Runnable 和 LCEL 只作为统一接口理解

5.1 Runnable 的意义

Runnable 是 LangChain 的统一调用协议。无论 Prompt、Model、Parser,核心调用都收敛到三个方法:

方法 语义 本项目对应
invoke() 一个输入,返回完整结果 意图识别、查询改写、结构化输出
stream() 一个输入,持续返回片段 最终答案逐 token 输出
batch() 多个输入,批量处理 批量测试、批量解析、离线评测可用
1
2
3
4
5
6
response = model.invoke([HumanMessage(content="入职流程有哪些步骤?")])

for chunk in model.stream([HumanMessage(content="入职流程有哪些步骤?")]):
    print(chunk.content, end="")

results = parser.batch([message_a, message_b, message_c])

5.2 LCEL 是线性链路语法,不是项目主流程

LCEL 可以把 Prompt、Model、Parser 串成线性管道:

1
2
3
4
5
6
7
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("用一句话回答:{question}")
chain = prompt | model | StrOutputParser()

answer = chain.invoke({"question": "入职流程有哪些步骤?"})

它适合简单线性任务:

输入变量 -> Prompt -> Model -> Parser -> 输出

但本项目的 RAG 主流程不是一条直线:

flowchart TD
    Q["用户问题"] --> R["Stage 1 查询路由"]
    R -->|问候/越界/转人工| Direct["直接返回"]
    R -->|FAQ 精确命中| FAQDirect["FAQ 标准答案直出"]
    R -->|需要检索| Prep["意图识别 + 改写 + 检索计划"]
    Prep --> FAQ["FAQ 检索"]
    FAQ -->|高分命中| FAQAnswer["FAQ 直出"]
    FAQ -->|未直出| Doc["文档检索"]
    Doc --> Context["上下文构建"]
    Context -->|无可靠上下文| Insufficient["信息不足"]
    Context -->|有上下文| LLM["ChatOpenAI.stream"]
    LLM --> Save["写历史 + Trace"]

    style Direct fill:#FEF2F2,stroke:#DC2626
    style FAQAnswer fill:#ECFDF5,stroke:#059669
    style LLM fill:#EFF6FF,stroke:#2563EB

所以本项目选择显式编排,而不是高层 Chain:

route = decide_route(context)
if route.answer:
    return direct_answer

prepared = prepare_retrieval(context)
faq_result = search_faq(context, prepared)
doc_result = search_doc(context, prepared)
answer_prepared = prepare_answer(context, prepared, faq_result, doc_result)

for chunk in stream_llm_answer(system_prompt, user_prompt):
    yield token_event(chunk)

可以这样总结:

LCEL 很适合教“组件怎么串起来”,但企业 RAG 主链路有大量分支、阈值、提前退出和诊断信息,所以本项目不用一条 LCEL 链包到底。


第六部分:自研与生态的分工

6.1 交给 LangChain 的部分

能力 原因
LLM 客户端 多厂商 OpenAI-compatible 统一接口
Message 类型 对话历史结构稳定
结构化输出 Pydantic 约束,减少自由文本解析
SQLChatMessageHistory 省掉历史消息 CRUD
Document / Loader 文件解析后统一结构
Text Splitter 成熟切分策略,减少手写边界问题
VectorStore 统一向量库写入和检索入口

6.2 项目自己实现的部分

能力 为什么自己做
查询路由 要优先处理问候、转人工、越界、FAQ 精确命中
意图规则 高频确定场景不应都调用 LLM
检索计划 不同意图、风险类别、source 需要不同参数
FAQ 直出 标准答案命中后不需要 LLM 生成
上下文筛选 要做分数阈值、重排、来源整理
Prompt Profile 不同业务类别要有不同口径
引用补强 企业 RAG 必须给出可追溯来源
Trace 和诊断 调试、验收和生产排障都需要可解释过程

这就是本项目的工程取舍:底层组件用生态,业务编排自己掌控


第七部分:学习路径建议

为了避免把 LangChain 学成零散组件,建议按下面顺序学习,而不是按组件名孤立学习:

顺序 学什么 目标
1 LangChain 在项目里的边界 先防止误解成“一键 RAG”
2 在线问答主线 看清 Message、ChatOpenAI、结构化输出在哪里用
3 离线入库主线 看清 Document、Loader、Splitter、VectorStore 在哪里用
4 Runnable / LCEL 解释统一接口,但不把主项目讲成 LCEL 链
5 自研 vs 生态 回答“为什么不用 RetrievalQA”
6 进入 demo 用小 demo 验证每个组件,不把 demo 当项目主线

配套 demo 可以按这个顺序练习:

demo 建议放在
demo02_runnable_interface.py Runnable 统一协议
demo03_chatopenai_structured_output.py ChatOpenAI + 结构化输出
demo04_message_types.py Message 类型
demo05_prompt_templates.py Prompt 模板
demo09_sql_chat_history.py 历史持久化
demo10_document_loaders_splitters.py Loader + Splitter
demo11_loader_registry.py 注册表模式
demo14_vectorstore_milvus.py VectorStore + Milvus

第八部分:常见误区

8.1 误区一:用了 LangChain 就应该用 RetrievalQA

不对。RetrievalQA 适合快速 demo,但企业级 RAG 需要:

  • FAQ 标准答案直出
  • 意图识别
  • 追问改写
  • 多场景 source 过滤
  • 知识库版本隔离
  • 风险 Prompt Profile
  • 引用来源补强
  • Trace 和诊断面板

这些都很难塞进一个黑盒 Chain 里。

8.2 误区二:LCEL 越多越工程化

LCEL 适合表达线性链路,但不是所有流程都应该写成管道。复杂业务分支用显式函数更清楚,也更容易调试。

8.3 误区三:SemanticChunker 一定比 RecursiveCharacterTextSplitter 更好

不一定。企业 RAG 里的切分要可控、稳定、便宜、可复现。RecursiveCharacterTextSplitter 更适合作为默认方案;SemanticChunker 可以作为特殊文档的增强策略,而不是主链路默认切分器。

8.4 误区四:VectorStore 就是 Milvus

不是。VectorStore 是 LangChain 的抽象,Milvus 是具体后端。本项目选择 Milvus,是因为需要服务端 BM25、混合检索、多集合、多版本和企业级部署能力。


本讲实践闭环

项目 内容
本讲类型 项目地图 + 组件定位
主线 在线问答链路、离线入库链路
核心产物 能画出 LangChain 在项目中的使用位置
是否进入最终项目 是,多个组件已在 qa_core 中使用
验收方式 能解释为什么项目“用 LangChain 组件,但不用高层 Chain 包办 RAG”
后续落点 第 4 讲 Milvus、第 5 讲意图识别、第 8 讲混合检索、第 10 讲 RAG 主流程、第 16 讲入库

重点掌握

优先级 内容 要求
必会 LangChain 在本项目中的定位 生态适配器,不是业务编排核心
必会 ChatOpenAI 工厂 知道 streaming=True/False 分别用于哪里
必会 Message 类型 能解释多轮对话为什么要带历史消息
必会 with_structured_output() 能解释它如何让 LLM 输出变成业务对象
必会 Document / Loader / Splitter / VectorStore 能放回离线入库链路
理解 Runnable 的 invoke/stream/batch 知道统一调用协议
理解 LCEL 的适用边界 适合线性任务,不适合本项目完整 RAG 主链路
了解 SQLChatMessageHistory 知道它负责历史持久化,不负责检索

本讲小结

  • LangChain 在本项目里不是“一键 RAG 框架”,而是一组工程适配器。
  • 在线问答链路中主要用到 ChatOpenAI、Message、结构化输出、Prompt Profile 和流式生成。
  • 离线入库链路中主要用到 Document、Loader、Splitter 和 Milvus VectorStore。
  • Runnable 统一了 invoke / stream / batch,LCEL 统一了简单线性链路写法。
  • 本项目没有使用高层 Chain 包办 RAG,是因为企业级链路有分支、阈值、直出、过滤、追问、版本、引用和诊断。
  • 先看项目主线,再看组件 API,会更容易建立整体感。

下一讲Milvus 索引机制与基本操作 — 进入向量库底层:Collection、Schema、Index、Insert、Search,以及 LangChain Milvus 封装隐藏了哪些操作。