别再只会 pip install langchain 了——框架底层到底发生了什么

原理拆解、六大核心模块,以及一次 LLM 调用到底经历了什么

一、先不说定义,我们打个比方

想象你要开一家餐厅。

厨房里有各种厨具——炒锅、蒸笼、烤箱、搅拌机。每种厨具擅长做不同的事,但它们的开关方式、操作界面、输出格式都不一样。你不可能让厨师每次换一口锅都得重新学一遍操作方法。

LangChain 干的就是这件事:给所有”锅”统一配一个标准接口。

不管底层是 GPT-4o、Claude、通义千问还是本地部署的 Llama,在 LangChain 眼里它们都是同一类东西——ChatModel。你只需要关心”我要问什么”,不需要操心每个厂商的 SDK 怎么写、API 参数怎么拼。

一句话版本: LangChain = 一套标准化的”乐高积木”,让 LLM 拥有记忆、能查资料、会用工具、能自主决策,开发者用管道符 | 就能把它们串联成完整的应用。


二、两个核心设计哲学

哲学 1:万物皆 Runnable——统一接口

LangChain 里几乎所有组件都实现了同一个接口——Runnable。不管是一个 Prompt 模板、一个 LLM 调用、还是一条完整的检索链,它们都支持完全一样的一组操作:

# 三种调用方式,所有组件通用

result = chain.invoke({"question": "什么是 RAG?"})
# 同步调用,返回完整结果

async for chunk in chain.astream({"question": "什么是 RAG?"}):
    print(chunk, end="")
# 异步流式输出,边生成边返回

results = chain.batch([{"question": q1}, {"question": q2}])
# 批量处理,一次塞多条进去

这种设计的好处很直白:你学会了 invoke / stream / batch 三件套,LangChain 生态里的任何组件都能拿来就用。切换底层模型?改一行代码。换向量数据库?也就改一行。

哲学 2:LCEL——用管道符搭应用

LangChain Expression Language(LCEL)是整个框架的灵魂。它的核心语法就一个符号:|(管道符),类似 Unix 管道。数据从左到右流动,前一个组件的输出自动成为后一个组件的输入。

from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

prompt = ChatPromptTemplate.from_template("用一句话解释:{concept}")
llm = ChatOpenAI(model="gpt-4o")

# 用 | 串联三个组件,一条流水线搞定
chain = prompt | llm | StrOutputParser()

result = chain.invoke({"concept": "量子纠缠"})
print(result)  # 量子纠缠是...

上面这四行代码干了三件事:把用户输入填到提示词模板里 → 发给 GPT-4o → 把模型返回的结构化消息拆成纯字符串。没有 glue code,没有手动拼接,管道符把一切都串起来了。


三、包的”拆家”——v1.0 的模块化分层

2025 年 LangChain v1.0 做了一件早就该做的事:把原来一个臃肿的大包拆成好几层。目前的包结构是:

  • langchain-core — 框架基石。定义所有抽象协议:Runnable 接口、BaseChatModel、BaseMessage、PromptTemplate、Tools 等。零第三方依赖,超级轻量。你可以把它理解成整个生态的”宪法”——所有上层组件都得遵守这里的接口约定。
  • langchain — 业务层。封装了 Chains、Retrievers、基础 Agent 等高层抽象。它不包含任何具体模型的实现,只定义”一个链该怎么跑””一个 Agent 该怎么决策”这些通用逻辑。
  • langchain-openai / langchain-anthropic / … — 各厂商的专用适配包。你需要哪个模型就装哪个,不需要的不占磁盘。切换模型就是换一个包、改一行类名的事。
  • langchain-community — 社区维护的第三方集成大杂烩:Chroma、Pinecone、各种文档加载器、小众模型。可以理解为”插件市场”。

为什么要这么拆? 举个真实场景:你只想用 LangChain 写一个调用 OpenAI 的脚本。在老版本里,你得 pip install langchain 然后拉下来一整个几百 MB 的包,里面塞满了你根本用不着的 Chroma、Pinecone、各种 loader。目前你只需要 pip install langchain-core langchain-openai,干净利落。


四、六大核心模块,逐个拆开看

如果 LangChain 是一台精密的机器,下面这六个模块就是它的核心齿轮。它们各自独立,又能任意组合。

模块一:Models——统一的大脑接口

这是 LangChain 最底层也最重大的一层。它把所有 LLM 抽象成三类标准接口,抹平了各家 API 的差异:

类型

说明

ChatModel

对话模型。输入输出都是 Message 对象,支持多轮对话。GPT-4o、Claude、Gemini、Qwen 都属于这一类。这是目前最主流的使用方式。

LLM

传统的文本补全模型。输入一个字符串,输出一个字符串。早期 GPT-3 的 text-davinci 就属于这类,目前用得越来越少了。

Embeddings

嵌入模型。把一段文本转换成一串浮点数向量。语义检索、RAG 的基石——”类似度比较”靠的就是它。

from langchain_openai import ChatOpenAI

llm = ChatOpenAI(
    model="gpt-4o",
    temperature=0.7       # 控制随机性,0 = 确定,1 = 放飞
)

# invoke 接收一个 Message 列表
from langchain_core.messages import HumanMessage, SystemMessage

response = llm.invoke([
    SystemMessage(content="你是一个毒舌但专业的代码审查员。"),
    HumanMessage(content="这段代码怎么样:print('hello world')")
])
print(response.content)  # 模型返回的文本

模块二:Prompts——提示词的”填空题”系统

直接在代码里硬编码 Prompt 字符串是很糟糕的实践——改一个字就得重新部署,而且 prompt 里往往有大量重复的模板结构。

LangChain 的 Prompts 模块把提示词变成了参数化的模板。你定义好结构,变量部分用 {} 占位,运行时动态填入:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一位{role},风格:{style}。"),
    ("user", "{question}")
])

# 运行时填入变量,生成完整消息列表
messages = prompt.invoke({
    "role": "历史老师",
    "style": "讲段子",
    "question": "安史之乱是怎么回事?"
})
# 输出:
# [
#   SystemMessage("你是一位历史老师,风格:讲段子。"),
#   HumanMessage("安史之乱是怎么回事?")
# ]

这样做的好处:Prompt 是一个独立对象,可以版本管理、A/B 测试、存数据库,而不是散落在代码各处。

模块三:Chains——流水线,把组件串起来

单个 LLM 调用能做的事很有限。真正的应用一般需要:查资料 → 拼提示词 → 调模型 → 解析输出 → 再做判断 → 可能再调一次模型。Chain 就是把这些步骤打包成一个整体。

在 LCEL 出现之前,Chain 是一个独立的类(各种 LLMChain、SequentialChain),定义步骤还得写不少胶水代码。LCEL 之后,Chain 本质上就是一组用 | 连接起来的 Runnable

# 一条典型的 RAG 问答链
from langchain_core.runnables import RunnablePassthrough

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 四步一气呵成:
# 1. 从向量库检索相关文档
# 2. 拼到提示词模板里
# 3. 发给 LLM
# 4. 解析输出
answer = chain.invoke("公司的年假政策是什么?")

注意 RunnablePassthrough()——它是个”透明人”,直接把用户的原始输入原封不动传给下一个环节,省去了手动拆解输入参数。

模块四:Memory——给 LLM 装上”记忆力”

LLM 本身是无状态的——每次调用都是独立的一问一答,上一轮聊过什么它根本不记得。Memory 模块解决的就是这个问题。

核心思路实则很朴素:把对话历史存下来,下次调用时塞进 Prompt 里。但实现方式可以很花哨:

类型

说明

BufferMemory

最简单粗暴的方式——把全部对话历史原样保存,每次调用都塞进去。优点是信息完整,缺点是 token 消耗随对话长度线性增长,聊久了钱包先受不了。

BufferWindowMemory

只保留最近 K 轮对话。相当于一个”滑动窗口”,老对话自动丢弃。简单有效,但会丢掉重大的早期信息。

SummaryMemory

每轮对话后用 LLM 生成一条摘要,只保留摘要而不是原文。省 token,但摘要可能丢失细节。有点像”记笔记”而不是”背原话”。

VectorStoreMemory

把对话内容向量化存进向量库,每次新对话时检索最相关的历史片段。适合长期记忆、跨会话场景——列如你跟 AI 说”上次聊的那个方案”,它能从几个月前的对话里找到上下文。

v1.0 的变化: 传统的 ConversationBufferMemory 在复杂 LCEL 链中集成起来比较别扭。新版本推荐用
RunnableWithMessageHistory + 外部存储(Redis、Postgres),通过 session_id 区分不同用户。记忆不再是链的属性,而是一个独立的消息仓库。

模块五:Retrieval——RAG 的核心引擎

这是 LangChain 在企业级落地中最常用的模块,没有之一。LLM 有两个天生的短板:知识截止日期、幻觉。Retrieval(检索)就是给 LLM 配了一个”外接硬盘”——回答问题之前,先去知识库里翻翻有没有相关资料。

一条完整的 RAG 流水线长这样:

 加载 ──→ ② 分割 ──→ ③ 向量化 ──→ ④ 存储 ──→ ⑤ 检索 ──→ ⑥ 生成
DocumentLoader   TextSplitter   Embeddings    VectorStore   Retriever     LLM
读取PDF/网页     切成小块       文本→向量    Chroma/FAISS   类似度Top-K   基于结果回答
# 构建一个最小 RAG 系统
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS

# ① 加载文档
loader = TextLoader("公司规章制度.txt")
docs = loader.load()

# ② 按 500 字符切块,块之间重叠 50 字符
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50
)
chunks = text_splitter.split_documents(docs)

# ③④ 向量化 + 存入 FAISS
vectorstore = FAISS.from_documents(chunks, OpenAIEmbeddings())

# ⑤ 转成检索器
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# ⑥ 接到 Chain 里——用户问一个问题,先检索,再生成答案
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

注意 chunk_overlap=50 这个参数——块之间保留一点重叠,防止关键信息刚好被切在两块的边界处。这个细节在生产环境里超级重大。

模块六:Agents——让 LLM 自己决定该干什么

Chain 是固定流水线:步骤 A → 步骤 B → 步骤 C,每次都一样。Agent 不一样——LLM 自己判断目前该做什么、用什么工具、什么时候该停

举个直观的对比:

Chain:固定流水线

Agent:自主决策

“查资料 → 拼提示词 → 生成答案”,每一步是写死的。适合翻译、摘要、标准问答这类任务——你知道正确答案的长相,只是需要 LLM 来填空。

“这个问题我需要先搜一下,不对,还是先算一遍,嗯,算出来的结果跟搜索到的不一致,我再搜一遍确认。”——LLM 在运行时自己规划路径,循环调用工具直到满意。

Agent 的核心是一个循环——思考 → 行动 → 观察 → 再思考(也叫 ReAct 模式):

┌──────────┐     ┌──────────┐     ┌──────────┐     ┌──────────┐
│   思考 │ ──→ │   行动 │ ──→ │   观察 │ ──→ │   完成 │
│ 分析任务  │     │ 调用工具  │     │ 评估结果  │     │ 输出答案  │
│ 决定下一步 │     │ 搜索/计算 │     │ 够了吗?  │     │          │
└──────────┘     └──────────┘     └────┬─────┘     └──────────┘
       ↑                               │ 不够就回到"思考"
       └───────────────────────────────┘
from langchain.agents import create_agent

# v1.0 新 API:一行创建 Agent
agent = create_agent(
    llm="gpt-4o",
    tools=[search_tool, calculator_tool, database_query_tool],
    system_prompt="你是一个数据分析助手,可以用搜索、计算、查数据库来回答问题。"
)

# 用户问什么你就自己想办法
result = agent.invoke({
    "messages": [{"role": "user", "content": "今年Q2的销售额跟去年同期比涨了多少?"}]
})

把 Agent 想象成一个实习生。 你给 TA 配了一台能上网的电脑(搜索)、一个计算器、一份公司数据库的访问权限,然后说”帮我把这个查清楚”。TA 自己去想:先查数据库拿原始数据,用计算器算增幅,如果数据对不上就再搜一下确认。整个过程你不需要告知 TA 每一步该怎么做——这就是 Agent 跟 Chain 的本质区别。

附加模块:Callbacks——应用的”监控探头”

虽然不是六大核心之一,但 Callbacks 在生产环境中必不可少。它让你能监听 LangChain 运行过程中的每一个事件:模型调用了多少次、每次花了多少 token、哪个环节最慢、什么报错了。

最常见的用法是集成 LangSmith(官方可观测性平台),一行环境变量就能把全链路追踪打开:

# 设置这两个环境变量,所有调用自动上报 LangSmith
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=ls__...

调用链上的每一步——Prompt 的完整内容、LLM 的输入输出、每个工具调用的耗时——全都能可视化追踪。调试复杂 Agent 的时候,这个功能是救命稻草。


五、重头戏:一次 LLM 调用到底经历了什么?

前面拆解了六大模块各自干什么,目前把它们串起来,看看一个完整的请求在 LangChain 内部是怎么流动的。

假设我们搭建了一个带记忆的 RAG 问答系统,用户输入:“我上次问的那个方案的预算批下来了吗?”

第 1 步:Memory 检索历史上下文

用户说”上次问的那个方案”,系统通过 session_id 从 Memory 中检索之前的对话记录,找到三个月前用户问”XX项目方案什么时候提交”的上下文,注入到当前消息里。

第 2 步:Retrieval 检索知识库

问题”预算批下来了吗”被 Embedding 模型向量化,在向量库中搜索最相关的 Top-5 文档块,返回公司内部的审批流程文档和预算表片段。

第 3 步:Prompt 组装最终提示词

ChatPromptTemplate 把系统指令 + 历史对话 + 检索到的文档 + 用户当前问题拼成一条完整的多角色消息列表。这时候发给 LLM 的内容远比用户输入的一句话要丰富得多。

第 4 步:LLM 调用 & 流式返回

ChatOpenAI(底层调 OpenAI SDK)发送 HTTP 请求,设置 stream=True。模型一边生成 token 一边返回,LangChain 通过 LCEL 的内置流式支持把每个 token 实时推给前端——用户看到的是打字机效果。

第 5 步:Output Parser 解析 & Callbacks 记录

StrOutputParser 把 AIMessage 对象提取为纯文本。同时 Callbacks 系统把整条链的调用日志(耗时、token 数、中间结果)上报到 LangSmith,出问题时可以回溯每一步。

第 6 步:Memory 写入新的对话记录

本轮对话被追加到 Memory 中,下次用户说”上次的结果呢”时,系统知道 TA 指的是这次的结果。


从代码层面看,这整条流水线可能就几行:

# 这就是上面六个步骤对应的完整代码
chain = (
    {"context": retriever, "history": memory_retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

# 流式输出
for chunk in chain.stream("我上次问的那个方案的预算批下来了吗?"):
    print(chunk, end="", flush=True)

看起来很简洁,但背后 LangChain 帮你处理了:消息格式标准化、Prompt 模板变量替换、流式 token 拆解、输出解析、回调传播——这些如果从头手写,少说几百行。


六、一句话收尾

如果要用一句话记住 LangChain 的架构,我会这么说:

Prompts 管”怎么问” → Models 管”谁来答” → Chains 管”按什么流程走” → Memory 管”记住了什么” → Retrieval 管”去哪查资料” → Agents 管”要不要自己决定下一步”

六个模块各司其职,通过 Runnable 接口和 | 管道符自由组合。你不需要把所有模块都用上——最简单的应用可以只有一个 Prompt + 一个 LLM,最复杂的则可以把所有模块串成一个能自主推理、会查资料、有记忆的智能体。

核心要点回顾

  • 统一抽象: 所有组件实现 Runnable 接口,invoke / stream / batch 三件套通吃
  • LCEL: 用 | 串联组件,声明式编程,告别胶水代码
  • 模型无关: 切换 GPT-4o 到 Claude 到 Qwen,只需改一行 import 和类名
  • RAG 是杀手级场景: DocumentLoader → TextSplitter → Embeddings → VectorStore → Retriever 这条流水线是 LangChain 在企业里落地最广的模式
  • Agent ≠ Chain: Chain 是固定路线,Agent 是 LLM 自己选路
  • Memory 远不止”存对话”: 摘要记忆、向量记忆、实体记忆——不同场景选不同策略,直接影响成本和效果
  • LangGraph 是 Agent 的未来: 复杂多分支、带循环的 Agent 目前都推荐用 LangGraph 做图编排,LangChain 的 create_agent 底层就是 LangGraph
© 版权声明

相关文章

1 条评论

none
暂无评论...