第1章 为什么需要理解 LangChain
更多内容,访问 杨艺韬讲堂(www.yangyitao.com)
本章基于 LangChain 1.0.3 / langchain-core 1.2.26 源码分析。源码路径:libs/目录。
当我们站在 2025 年回望 AI 应用开发的演进历程,会发现一个有趣的规律:每一次底层模型能力的跃迁,都会催生出一个新的应用框架浪潮。从最初手写 HTTP 请求调用 OpenAI API,到使用各种轻量封装库,再到如今以 LangChain 为代表的完整应用框架生态------这个过程并非偶然,而是由真实的工程痛点驱动的必然选择。
LangChain 是当前 LLM 应用开发领域使用最广泛的框架。它的核心仓库在 GitHub 上拥有超过 10 万颗星,PyPI 周下载量常年维持在百万级。然而,对于大多数开发者而言,LangChain 仍然是一个"会用但不理解"的黑盒。本书的使命,就是带领读者打开这个黑盒,从源码层面理解其设计哲学与实现细节。
:::tip 本章要点
- AI 应用框架的三个演进阶段:从裸 API 调用到轻量封装,再到声明式框架
- LangChain 的核心创新:LCEL 表达式语言与 Runnable 统一接口协议
- 为什么要读源码:超越文档和教程的局限性,理解设计决策背后的权衡
- 本书路线图:从核心抽象到具体实现,系统掌握 LangChain 的每一层 :::
1.1 AI 应用框架的演进
1.1.1 第一阶段:裸 API 调用时代
最初的 LLM 应用开发是简单直接的。开发者直接构造 HTTP 请求,发送给 API 端点,解析返回的 JSON。这种方式的问题很快就暴露了:
python
# 2022年初期的典型写法
import requests
import json
def call_llm(prompt: str) -> str:
response = requests.post(
"https://api.openai.com/v1/completions",
headers={"Authorization": f"Bearer {api_key}"},
json={"model": "text-davinci-003", "prompt": prompt, "max_tokens": 500}
)
return response.json()["choices"][0]["text"]
# 问题1:错误处理?重试逻辑?速率限制?
# 问题2:如何组合多个调用?如何传递上下文?
# 问题3:如何追踪调试?如何衡量性能?
# 问题4:如何在不同提供商之间切换?
每个项目都在重复解决相同的问题:重试逻辑、流式输出处理、Prompt 管理、上下文拼接。更关键的是,当应用复杂度增长时,代码会迅速变成一团意大利面条------各种回调嵌套、异常处理逻辑散落各处、不同 LLM 提供商的 API 差异让代码充满条件分支。
1.1.2 第二阶段:轻量封装时代
随后出现了各种轻量封装库,它们解决了最基本的痛点:统一不同 LLM 提供商的接口、提供重试机制、简化流式输出处理。但这些库大多是"功能的集合"而非"架构的表达"------它们提供了一堆工具函数,但缺乏一个统一的组合范式。
python
# 轻量封装的典型问题:缺乏统一的组合方式
result = llm.generate(
prompt_template.format(
context=retriever.search(query),
question=query
)
)
parsed = output_parser.parse(result)
# 每一步都是命令式的,流程硬编码在业务代码中
# 想要添加流式输出?需要重写整个流程
# 想要并行检索?需要手动管理线程池
# 想要添加追踪?需要在每个调用点插入日志
1.1.3 第三阶段:声明式框架时代
LangChain 代表了第三个阶段的到来。它的核心洞察是:LLM 应用本质上是一个数据处理管道,每个组件接收输入、产生输出,组件之间通过统一的接口协议连接。 这个洞察催生了两个关键创新:
- Runnable 协议 :一个统一的接口,所有组件都实现
invoke/batch/stream方法 - LCEL(LangChain Expression Language) :一种声明式的管道组合语法,用
|操作符连接组件
python
# LangChain LCEL 的声明式写法
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 声明式地描述数据流
chain = (
RunnableParallel(
context=retriever,
question=RunnablePassthrough(),
)
| prompt
| model
| StrOutputParser()
)
# 同步调用、异步调用、批量处理、流式输出------全部自动支持
chain.invoke("什么是 LangChain?")
await chain.ainvoke("什么是 LangChain?")
chain.batch(["问题1", "问题2", "问题3"])
for chunk in chain.stream("什么是 LangChain?"):
print(chunk, end="")
这种声明式的方式带来了根本性的改变:开发者描述"数据如何流动",框架负责"如何高效执行"。
2022"] -->|痛点:重复代码| B["轻量封装时代
2023"] B -->|痛点:缺乏组合范式| C["声明式框架时代
2024-2025"] A1["手写 HTTP 请求"] --> A A2["手动错误处理"] --> A A3["硬编码流程"] --> A B1["统一 API 封装"] --> B B2["基础重试机制"] --> B B3["仍然命令式"] --> B C1["Runnable 统一协议"] --> C C2["LCEL 声明式语法"] --> C C3["自动并行/流式/追踪"] --> C style A fill:#f9d5d5 style B fill:#f9ecd5 style C fill:#d5f9d5
1.2 LangChain 的定位与竞品对比
在深入 LangChain 的核心创新之前,有必要将它放在更广阔的 AI 应用框架版图中审视。当前市场上有多个值得关注的 LLM 应用框架:
| 框架 | 核心定位 | 组合范式 | 流式支持 | 生态规模 |
|---|---|---|---|---|
| LangChain | 通用 LLM 应用框架 | LCEL 管道操作符 | 原生 transform 链 | Partner 包 15+ |
| LlamaIndex | 数据索引与检索 | Query Engine | 回调驱动 | Hub 插件 |
| Haystack | 搜索增强生成 | Pipeline DAG | 组件级 | 有限 |
| Semantic Kernel | 企业 AI 编排 | Kernel + Plugin | 事件驱动 | Microsoft 生态 |
| LangGraph | 有状态 Agent 编排 | 图状态机 | 原生 | 与 LangChain 互补 |
LangChain 的独特之处在于它选择了最通用的抽象层级。它不仅仅解决检索增强生成(RAG)或 Agent 编排这一个问题,而是提供了一套可以组合任意 LLM 操作的基础设施。这种通用性既是它最大的优势(适用场景极广),也是它最被诟病的地方(初学者容易迷失在抽象层中)。
LangGraph 是 LangChain 团队推出的另一个框架,专注于有状态的、图结构的 Agent 编排。它构建在 langchain-core 之上,复用了 Runnable 协议和 RunnableConfig 机制。这个关系本身就说明了 langchain-core 的抽象设计具有足够的通用性。
1.3 LangChain 的核心创新
1.3.1 LCEL:声明式表达语言
LCEL 并非一种独立的编程语言,而是利用 Python 的运算符重载(__or__ 和 __ror__)实现的领域特定语言(DSL)。当你写出 prompt | model | parser 时,Python 解释器实际上在构建一棵由 RunnableSequence 和 RunnableParallel 组成的执行树。
这种设计的精妙之处在于:它是惰性的 。| 操作符不会触发任何实际的计算,它只是构建了一个描述数据流的对象图。真正的计算在调用 invoke/stream 时才会发生。这使得框架可以在执行时进行优化------比如识别可以并行执行的分支、自动推断输入输出 schema、构建用于可视化的计算图。
python
# LCEL 的管道操作符本质
# 源码文件:libs/core/langchain_core/runnables/base.py
class Runnable(ABC, Generic[Input, Output]):
def __or__(self, other):
"""a | b 等价于 RunnableSequence(a, b)"""
return RunnableSequence(self, coerce_to_runnable(other))
def __ror__(self, other):
"""当左操作数不是 Runnable 时触发"""
return RunnableSequence(coerce_to_runnable(other), self)
1.3.2 统一接口协议
LangChain 最深层的架构决策是:让所有组件------从简单的字符串处理函数到复杂的 LLM 调用------都遵循同一个接口协议。 这个协议定义在 Runnable 抽象基类中:
这个设计的优势在于组合的一致性 。一个 RunnableSequence(由多个 Runnable 组成的管道)本身也是一个 Runnable,它自动拥有 invoke/batch/stream 的全部能力。这意味着你可以将一个复杂的 chain 作为另一个 chain 的组件------组合是无限递归的,且每一层都保持类型安全。
1.3.3 自动化的能力推导
统一接口带来了一个深远的好处:能力的自动传播 。当你用 | 组合两个 Runnable 时,框架会自动为组合体生成:
- 同步执行能力 (
invoke):依次调用每个步骤 - 异步执行能力 (
ainvoke):默认通过线程池执行同步版本,子类可覆写为原生异步 - 批量处理能力 (
batch):默认并行调用invoke,使用线程池执行器 - 流式输出能力 (
stream):通过transform方法将前一步的流式输出喂给下一步 - Schema 推断能力:自动从第一步推断输入类型,从最后一步推断输出类型
- 可视化能力:自动生成 Mermaid 图表描述数据流
这意味着开发者写的每一行 LCEL 代码,都自动具备了生产级别的功能完备性。
1.4 为什么要读源码
1.4.1 文档的局限性
LangChain 的官方文档是以用例驱动的------它告诉你"如何使用",但很少解释"为什么这样设计"以及"底层如何实现"。这在简单场景下不是问题,但当你遇到以下情况时,文档就力不从心了:
- 性能调优 :为什么我的 chain 比预期慢?
batch的默认并发度是多少?流式输出的缓冲策略是什么? - 调试困难:回调链(callbacks)是如何传播的?为什么我的自定义回调没有收到预期的事件?
- 扩展定制 :如何正确地子类化一个 Runnable?
_call_with_config和直接调用invoke的区别是什么? - 架构理解 :
langchain-core和langchain的边界在哪里?为什么 Partner 包要独立出去?
1.4.2 源码揭示真实的设计权衡
每一行源码背后都是一个设计决策,每一个设计决策背后都是一组权衡。通过阅读源码,你会发现:
ensure_config 为什么要检查 ContextVar? 因为 LangChain 使用 var_child_runnable_config 这个 ContextVar 来实现配置的自动传播------父 Runnable 的配置会自动继承给子 Runnable,无需显式传递。这个设计让 LCEL 的嵌套调用变得简洁,但也带来了隐式状态的复杂性。
python
# 源码文件:libs/core/langchain_core/runnables/config.py
var_child_runnable_config: ContextVar[RunnableConfig | None] = ContextVar(
"child_runnable_config", default=None
)
def ensure_config(config: RunnableConfig | None = None) -> RunnableConfig:
empty = RunnableConfig(tags=[], metadata={}, callbacks=None, ...)
# 先从 ContextVar 继承父级配置
if var_config := var_child_runnable_config.get():
empty.update({k: v.copy() if k in COPIABLE_KEYS else v ...})
# 再用显式传入的配置覆盖
if config is not None:
empty.update({k: v ...})
return empty
coerce_to_runnable 为什么要区分生成器函数和普通函数? 因为生成器函数天然支持流式输出(transform),将其包装为 RunnableGenerator 可以保留这个能力;而普通函数只能包装为 RunnableLambda,它在流式场景下必须先累积全部输入再产出输出。
python
# 源码文件:libs/core/langchain_core/runnables/base.py
def coerce_to_runnable(thing: RunnableLike) -> Runnable[Input, Output]:
if isinstance(thing, Runnable):
return thing
if is_async_generator(thing) or inspect.isgeneratorfunction(thing):
return RunnableGenerator(thing) # 保留流式能力
if callable(thing):
return RunnableLambda(cast(..., thing)) # 无原生流式能力
if isinstance(thing, dict):
return RunnableParallel(thing) # 字典 -> 并行执行
raise TypeError(...)
1.4.3 源码是最好的学习材料
LangChain 的源码有几个特点使其非常适合阅读:
- 类型标注完善:几乎所有函数都有完整的类型标注,泛型参数使用清晰
- 文档字符串详尽:每个类和方法都有详细的 docstring,包含使用示例
- 层次分明 :
langchain-core约 6000 行的base.py虽然体量大,但类之间的继承关系清晰 - 设计模式丰富:策略模式、组合模式、装饰器模式、访问者模式在源码中随处可见
1.5 LangChain 的分层架构概览
在深入任何细节之前,我们先鸟瞰 LangChain 的整体分层结构。这将帮助你在后续章节中始终保持全局视角。
这个架构有三层清晰的分界:
- langchain-core :最底层,定义了所有核心抽象。
Runnable协议、消息类型、Prompt 模板、语言模型接口、输出解析器、工具接口、回调系统------这些都在这里。这一层的设计原则是"最小化依赖、最大化抽象"。 - Partner 包 :中间层的一部分,每个 LLM 提供商或向量数据库都有自己的独立包。它们实现
langchain-core定义的接口,但彼此完全独立。这种设计避免了"安装 LangChain 就要拉取所有依赖"的问题。 - langchain (经典包):提供高级抽象,如 Chains、Agents、Memory。它组合
langchain-core的基础组件,构建常见的应用模式。
1.6 本书路线图
本书按照"从核心到外围、从抽象到具体"的路线展开,每一章都建立在前一章的基础之上。
基础篇(第 1-5 章)
- 第 1 章(本章):为什么需要理解 LangChain------建立学习动机和全局视角
- 第 2 章 架构总览 :langchain-core、langchain、Partners 的关系,
chain.invoke()的完整旅程 - 第 3 章 Runnable 与 LCEL :深入
Runnable协议,理解|操作符、RunnableSequence、RunnableParallel等核心组合原语 - 第 4 章 消息系统 :
BaseMessage家族、消息的序列化与类型安全 - 第 5 章 语言模型抽象 :
BaseLLM、BaseChatModel的设计,缓存与速率限制
进阶篇(第 6-11 章)
- 第 6 章 Prompt 工程:模板系统的设计与实现,Few-shot 选择器
- 第 7 章 输出解析器:从字符串到结构化数据的桥梁
- 第 8 章 工具系统 :
BaseTool的设计,工具调用协议 - 第 9 章 文档与检索 :
Document抽象、检索器接口 - 第 10 章 检索增强生成 :
Retriever的实现与优化策略 - 第 11 章 Chains:经典 Chain 的设计与 LCEL 的关系
高级篇(第 12-18 章)
- 第 12 章 回调与追踪:Callback 系统的观察者模式实现,LangSmith 集成
- 第 13 章 Memory 系统:对话记忆的多种策略
- 第 14 章 Agent 架构:从 ReAct 到工具调用 Agent 的演进
- 第 15 章 工具调用 Agent:现代 Agent 的实现细节
- 第 16 章 序列化 :
Serializable体系,JSON 序列化与反序列化 - 第 17 章 Partner 生态:如何构建 LangChain 集成包
- 第 18 章 设计模式总结:贯穿 LangChain 的架构智慧
1.7 阅读源码的方法论
在开始阅读 LangChain 源码之前,这里提供几个实用的方法论建议。
1.7.1 从 base.py 入手
LangChain-core 的 runnables/base.py 是整个框架的心脏,约 6200 行代码。不要被它的体量吓到。这个文件中的类遵循清晰的层次:
bash
Runnable (ABC) # 最底层的抽象,定义协议
|
+-- RunnableSerializable # 加入序列化能力
| |
| +-- RunnableSequence # 管道组合
| +-- RunnableParallel # 并行组合
| +-- RunnableBranch # 条件分支
| +-- RunnableBindingBase # 参数绑定
|
+-- RunnableLambda # 函数包装
+-- RunnableGenerator # 生成器包装
建议从 Runnable.__or__ 开始,跟踪 | 操作符如何创建 RunnableSequence;然后阅读 RunnableSequence.invoke,理解管道的执行流程。
1.7.2 跟踪一次完整调用
理解 LangChain 最有效的方式是跟踪一次完整的 chain.invoke() 调用。从用户的 chain.invoke(input) 开始,观察数据如何流经:
ensure_config初始化配置(合并 ContextVar 中的父级配置)CallbackManager启动追踪(触发on_chain_start事件)- 数据依次流过每个
step.invoke(每一步都patch_config传递子回调) - 最终结果通过
on_chain_end报告给追踪系统
1.7.3 关注 config 的流转
RunnableConfig 是 LangChain 的"血液循环系统"。几乎每一个方法都接收一个可选的 config 参数。理解 config 如何在组件之间传递、合并、转换,是理解整个框架的关键。
python
# RunnableConfig 的核心字段
class RunnableConfig(TypedDict, total=False):
tags: list[str] # 标签,用于过滤追踪事件
metadata: dict[str, Any] # 元数据,传递给回调
callbacks: Callbacks # 回调处理器链
run_name: str # 本次运行的名称
max_concurrency: int # 并行度上限
recursion_limit: int # 递归深度限制(默认25)
configurable: dict[str, Any] # 运行时可配置字段
run_id: uuid.UUID | None # 唯一运行标识
1.7.4 使用调试工具辅助阅读
LangChain 内置了丰富的调试和可视化能力,这些工具在阅读源码时是极好的辅助:
python
from langchain_core.globals import set_debug
# 开启全局调试,所有 Runnable 调用都会打印详细的输入/输出
set_debug(True)
# 可视化 chain 的结构
chain.get_graph().print_ascii()
# 生成 Mermaid 图
print(chain.get_graph().draw_mermaid())
# 检查 chain 的输入/输出 schema
print(chain.input_schema.model_json_schema())
print(chain.output_schema.model_json_schema())
# 使用 ConsoleCallbackHandler 追踪执行过程
from langchain_core.tracers import ConsoleCallbackHandler
chain.invoke(input, config={"callbacks": [ConsoleCallbackHandler()]})
这些工具不仅在生产调试中有用,在阅读源码时也能帮助你快速验证你对某段代码行为的理解是否正确。
1.7.5 推荐的阅读顺序
基于我们对 LangChain 源码的分析经验,推荐以下阅读顺序:
runnables/config.py(约 400 行):先理解RunnableConfig的结构和ensure_config/patch_config的逻辑runnables/base.py中的Runnable类 (约 2000 行):理解invoke/batch/stream的默认实现和__or__/__ror__操作符runnables/base.py中的RunnableSequence(约 600 行):理解管道的核心执行逻辑runnables/base.py中的RunnableLambda(约 500 行):理解函数包装和类型推断callbacks/manager.py:理解回调系统如何驱动追踪language_models/chat_models.py:理解 LLM 如何实现 Runnable 协议
1.8 设计决策:为什么 LangChain 选择了这条路
为什么选择运算符重载而非方法链?
LangChain 使用 | 而非 .then() 来组合 Runnable。这不是偶然的。方法链(如 a.then(b).then(c))将组合逻辑耦合在对象上;而运算符重载(a | b | c)让组合成为一个独立的操作。更重要的是,Python 的 | 操作符在视觉上暗示了"管道"的概念,与 Unix 管道和函数式编程中的管道操作符(|>)形成呼应。
为什么不用 DAG 框架如 Airflow?
LangChain 的目标场景与 Airflow 等 DAG 框架有根本区别。Airflow 针对的是长时间运行的批处理任务,强调调度和持久化;LangChain 针对的是实时交互,需要亚秒级的响应延迟和实时的流式输出。此外,LLM 应用的数据流结构通常是在运行时动态确定的(例如 Agent 根据模型输出决定下一步调用什么工具),这与 Airflow 的静态 DAG 定义截然不同。
为什么 langchain-core 要独立出来?
这是一个关于依赖管理的决策。如果所有代码都在一个包中,那么安装 langchain-openai 就意味着安装 langchain 的全部依赖。通过将核心抽象独立为 langchain-core(依赖极少),Partner 包只需依赖 langchain-core 而非整个 langchain。这大幅减少了依赖冲突的可能性。
1.9 一个完整的 LCEL 示例:从概念到源码
在结束本章之前,让我们通过一个完整的示例,将前面讨论的所有概念串联起来。这个示例展示了一个典型的 RAG(Retrieval-Augmented Generation)管道:
python
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
# 假设 retriever 是一个已经配置好的向量检索器
prompt = ChatPromptTemplate.from_template(
"根据以下上下文回答问题:\n\n上下文:{context}\n\n问题:{question}"
)
model = ChatOpenAI(model="gpt-4o-mini")
# LCEL 声明式管道
chain = (
RunnableParallel(
context=retriever,
question=RunnablePassthrough(),
)
| prompt
| model
| StrOutputParser()
)
让我们逐行分析这段代码在源码层面发生了什么:
第一行 RunnableParallel(...) :创建了一个并行执行体。retriever 如果不是 Runnable,会被 coerce_to_runnable 转换。RunnablePassthrough() 创建了一个透传原始输入的 Runnable。
第二行 | prompt :触发 RunnableParallel.__or__(prompt)。由于 RunnableParallel 继承了 RunnableSerializable 继承了 Runnable,它拥有 __or__ 方法。这创建了一个 RunnableSequence(parallel, prompt)。
第三行 | model :触发前一行结果(RunnableSequence)的 __or__。RunnableSequence 覆写了 __or__,直接将 model 追加到步骤列表中,而不是嵌套新的 RunnableSequence。
第四行 | StrOutputParser() :同理,StrOutputParser 被追加到步骤列表。最终 chain 是一个 RunnableSequence,包含 4 个步骤。
当调用 chain.invoke("什么是向量数据库?") 时:
这个例子展示了 LangChain 的核心价值:用 4 行声明式代码,构建了一个具备并行检索、Prompt 格式化、LLM 调用、输出解析、自动追踪、异步支持、批量处理和流式输出能力的生产级管道。
1.10 小结
本章建立了阅读本书的动机和全局视角。我们了解了 AI 应用框架从裸 API 调用到声明式表达语言的演进历程,理解了 LangChain 的两个核心创新------Runnable 统一协议和 LCEL 表达式语言------如何从根本上改变了 LLM 应用的开发方式。
更重要的是,我们明确了为什么要读源码:官方文档告诉你"怎么用",但只有源码能告诉你"为什么这样设计"以及"底层如何实现"。当你遇到性能问题、调试困难或需要深度定制时,源码级别的理解将是你最强大的武器。
从下一章开始,我们将正式进入 LangChain 的源码世界。第 2 章将从架构全景出发,厘清 langchain-core、langchain、Partner 包之间的关系,并跟踪一次完整的 chain.invoke() 调用,为后续的深入分析奠定基础。