去年我用 LangChain 搭了个代码审查 Agent。写到一半我突然意识到:我明明可以直接调 OpenAI API,3 行代码就完事,为什么非要套一层 LangChain?
这个问题我问了自己三个月,终于在看源码的时候想明白了。
这篇文章就是我的答案。
项目简介
LangChain(GitHub 95k+ Stars)是目前最流行的 LLM 应用开发框架。它的核心定位不是"实现 AI 能力",而是给所有 AI 能力提供统一接口------不管底层是 OpenAI 还是 Anthropic、Pinecone 还是 Chroma,上层代码不需要改。这就是"胶水框架"的含义:它不生产能力,它连接能力。
架构全景
把 LangChain 拆开,就三层东西:
┌──────────────────────────────────────────────────┐
│ Agent 层 │
│ "让 LLM 自己决定:下一步该调哪个 Tool?" │
│ AgentExecutor → 思考 → 选 Tool → 执行 → 再思考 │
├──────────────────────────────────────────────────┤
│ Chain 层 │
│ "把多个步骤串成一条流水线" │
│ prompt │ llm │ output_parser │ tool │
├──────────────────────────────────────────────────┤
│ Tool 层 │
│ "把任意函数包装成 LLM 能调用的工具" │
│ 搜索引擎 · 数据库查询 · 代码执行 · 文件读写 │
├──────────────────────────────────────────────────┤
│ 底层能力(LLM / 向量库 / 文档加载器) │
│ OpenAI · Anthropic · Chroma · Pinecone · ... │
└──────────────────────────────────────────────────┘
每一层解决一个问题。逐层拆开看。
第一层:Tool------把函数变成 LLM 认识的工具
LLM 不认识你的函数签名,它只认识一种东西:函数描述 + 参数 Schema。
Tool 层干的就是这个转化。
from langchain.tools import tool
@tool
def search_knowledge_base(query: str) -> str:
"""搜索团队内部知识库,返回相关文档内容。
Args:
query: 搜索关键词,支持自然语言
"""
# 实际调用你的搜索逻辑
return kb.search(query)
加上 @tool 装饰器之后,LangChain 在背后做了什么?看源码:
# langchain/tools/base.py ------ 简化后的核心逻辑
class BaseTool:
name: str # 工具名,LLM 用它来标识
description: str # 工具描述,LLM 用它来理解"什么时候该用我"
args_schema: Type[BaseModel] # 参数的 JSON Schema
def _run(self, **kwargs):
"""子类实现具体的执行逻辑"""
raise NotImplementedError
关键设计:Tool 不关心 LLM 是谁,LLM 不关心 Tool 怎么实现 。它们之间只通过 name + description + JSON Schema 耦合。这意味着你随时可以把 OpenAI 换成 DeepSeek,Tool 代码一行不用改。
这层抽象对应一个设计原则:依赖倒置。高层模块(Agent)不依赖低层模块(具体函数),两者都依赖抽象(Tool 接口)。
第二层:Chain------用管道把组件串起来
有了 Tool 之后,下一个问题:怎么把 prompt、LLM 调用、结果解析这几个步骤串起来?
最笨的办法是写一堆 if-else 和临时变量。LangChain 的答案是 LCEL(LangChain Expression Language)------用 | 管道符串联组件。
from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
from langchain.schema import StrOutputParser
prompt = ChatPromptTemplate.from_template("用{language}语言解释:{concept}")
llm = ChatOpenAI(model="gpt-4")
chain = prompt | llm | StrOutputParser()
# 一行调用
result = chain.invoke({"language": "Go", "concept": "goroutine"})
这行 prompt | llm | StrOutputParser() 看起来像魔法。拆开源码看:
# langchain/runnables/base.py ------ 核心就在这里
class Runnable:
def __or__(self, other: "Runnable") -> "RunnableSequence":
"""管道操作符:self | other → RunnableSequence"""
return RunnableSequence(self, other)
def invoke(self, input, config=None):
"""同步调用"""
...
async def ainvoke(self, input, config=None):
"""异步调用"""
...
def stream(self, input, config=None):
"""流式输出"""
...
RunnableSequence 的 invoke() 本质上就是:
class RunnableSequence:
def __init__(self, *steps):
self.steps = steps
def invoke(self, input, config=None):
for step in self.steps:
input = step.invoke(input, config)
return input
每个 step 的输出变成下一个 step 的输入。就是 Unix 管道的面向对象版本。
这层抽象的价值:把"编排逻辑"和"执行逻辑"分离。你写 chain 的时候只关心数据怎么流,不关心每个组件内部怎么实现。写法从"命令式"变成了"声明式"。
第三层:Agent------让 LLM 自己决定调用顺序
Chain 的问题是:你必须提前知道步骤。但真实的场景是------用户说"帮我查一下上周的代码审查报告",你没法提前知道需要几步:先查知识库?还是直接调 Git API?还是两个都要?
Agent 层解决的就是这个:让 LLM 成为流程的决策者。
from langchain.agents import AgentExecutor, create_openai_functions_agent
tools = [search_knowledge_base, query_git_log, send_dingtalk_message]
agent = create_openai_functions_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
executor.invoke({"input": "查一下 task-api 仓库上周的代码审查报告并发到钉钉"})
Agent 的执行流程是一个循环:
用户输入 → LLM 思考 → 需要调 Tool?
├─ 是 → 调 Tool → 拿到结果 → 回到"LLM 思考"
└─ 否 → 输出最终答案
看核心源码:
# langchain/agents/agent.py ------ AgentExecutor 的简化核心循环
class AgentExecutor:
def _call(self, inputs):
intermediate_steps = []
while True:
# 让 LLM 决定下一步
output = self.agent.plan(
intermediate_steps,
callbacks=...,
**inputs,
)
# 如果 LLM 说"完成了",返回最终答案
if isinstance(output, AgentFinish):
return output.return_values
# 否则 LLM 说"调这个 Tool",执行它
tool_name = output.tool
tool_input = output.tool_input
observation = self.tools[tool_name].run(tool_input)
# 把执行结果记录到中间步骤,下一轮 LLM 能看到
intermediate_steps.append((output, observation))
这个 while True 循环就是整个 Agent 的心脏。intermediate_steps 是 LLM 的"记忆"------它看到自己刚才调了什么 Tool、拿到了什么结果,才能决定下一步。
三个关键设计决策
看完三层抽象,回头看 LangChain 做的最好的三个设计决策:
决策 1:Runnable 统一接口
问题:框架里有几十种组件------LLM、Prompt、Tool、Retriever、OutputParser。每个都不同,怎么让它们能自由组合?
决策 :所有组件都实现同一个 Runnable 接口。invoke()、stream()、batch()、ainvoke() 四种调用方式覆盖了所有场景。不管你前面接的是 LLM 还是 Retriever,对后面的组件来说都一样------上一个 Runnable 的输出,就是我的输入。
决策 2:管道语法 | 而不是 Builder 模式
问题:怎么表达"A → B → C"这样的组件链?
决策 :重载 __or__ 操作符实现 | 管道。对比 Builder 模式:
# Builder 模式(Java 风格)
chain = ChainBuilder().add(prompt).add(llm).add(parser).build()
# LCEL 管道(Python 风格)
chain = prompt | llm | parser
管道语法胜在视觉上就是数据流向。从左到右,一目了然。这是 API 设计的品味问题------好的 API 让正确的写法"看起来就是对的"。
决策 3:用 Pydantic 做 Tool 的 Schema 生成
问题:怎么把 Python 函数的参数告诉 LLM?手写 JSON Schema 又丑又容易出错。
决策 :用 Pydantic 的 BaseModel 自动生成 JSON Schema。你定义 Python 类型,框架自动转成 LLM 能理解的参数格式。类型注解既是类型检查,也是 API 文档,还是 LLM 的 function calling schema------一次定义,三处受益。
核心代码拆解:__or__ 是怎么工作的
管道是 LangChain 最核心的语法,值得单独拆开看:
# 当你写 chain = prompt | llm | parser
# Python 实际执行的是:
# Step 1: temp = prompt.__or__(llm)
# 返回 RunnableSequence(prompt, llm)
# Step 2: temp.__or__(parser)
# 返回 RunnableSequence(prompt, llm, parser)
RunnableSequence 在执行时不只是简单的 for 循环。它还要处理:
1. 输入映射------前一个输出可能是个对象,后一个需要的是 dict 的某个字段:
# 只把 LLM 输出的 "text" 字段传给下一个组件
chain = prompt | llm | {"summary": itemgetter("text")} | summary_parser
2. 并行执行------管道里某个环节可以并行调多个组件:
# 同时查知识库和搜索网页,两个结果合并后给 LLM
chain = prompt | {
"kb_result": kb_retriever,
"web_result": web_search,
} | llm
3. 流式传递 ------stream() 不是等上一个组件全部输出完才传给下一个,而是逐 token 传递。这要求每个 Runnable 的 stream() 返回的都是迭代器,RunnableSequence 的 stream() 本质是把多个迭代器串联起来。
这三种能力加起来,让 | 不只是一个语法糖------它背后藏着一整套数据流编排引擎。
你可以抄的作业
LangChain 的三层抽象不只适用于 AI 框架。任何需要"编排多个外部服务"的系统,都能用同样的思路:
1. 用统一接口隔离变化
你的系统如果依赖多个外部 API(短信、邮件、推送),给它们套一层统一接口。以后换供应商只改适配器,业务代码不动。LangChain 的 Runnable 就是你的 Notifier。
2. 管道优于 Builder
Python 里做链式调用,考虑重载 __or__ 而不是写 .add().add().build()。管道语法读起来像数据流向图,代码自己就是文档。
3. 类型注解驱动元数据
Pydantic 这套"用类型定义自动生成 Schema"的思路可以用在任何需要"描述接口给外部系统"的场景。比如你要做一个插件系统------让插件作者用类型注解声明参数,你的框架自动生成配置 UI。
4. 循环 + 中间状态 = 通用 Agent 模式
while True + intermediate_steps 这个结构不只是给 LLM 用的。任何"不确定步骤数、每步根据上一步结果动态决策"的场景都能用------比如 CI/CD 管道的自动回滚、智能爬虫的下一页决策。
最后
LangChain 被很多人吐槽"过度封装"、"抽象泄漏"。这些批评有道理------当你用 AgentExecutor 跑了一个小时发现 LLM 在死循环调同一个 Tool,你会想把电脑砸了。
但抛开使用体验,它的架构设计是这个行业最好的教材之一。Runnable 接口的统一定义、LCEL 管道的声明式编排、Agent 循环的 ReAct 模式------这三个东西是Agent 框架的设计范式,LangChain 之后的框架(LlamaIndex、Semantic Kernel、Dify)都在不同程度上复用了这些范式。
看完源码再写 Agent,你就不是"调 API"了------你知道每一层在干什么,知道为什么 prompt 要这样写、Tool 要这样定义、Chain 要从这个方向串。
下一讲拆 LlamaIndex。它是怎么把"非结构化数据 → 向量索引 → 语义查询"这个流程抽象成框架的?跟 LangChain 的设计思路有什么不同?
本文拆解的 LangChain 版本:v0.3.x。源码地址:github.com/langchain-ai/langchain
本文由mdnice多平台发布