原理拆解、六大核心模块,以及一次 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





