# 拆解 LangChain:为什么说它是“胶水框架“?


去年我用 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):
        """流式输出"""
        ...

RunnableSequenceinvoke() 本质上就是:

复制代码
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() 返回的都是迭代器,RunnableSequencestream() 本质是把多个迭代器串联起来。

这三种能力加起来,让 | 不只是一个语法糖------它背后藏着一整套数据流编排引擎


你可以抄的作业

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多平台发布

相关推荐
humors22110 小时前
突破学习瓶颈:十个需要克服的障碍
大数据·学习·程序人生
郝学胜-神的一滴15 小时前
Qt 高级开发 011: 跨线程信号槽实战
开发语言·c++·qt·程序人生·开源软件·用户界面
郝学胜-神的一滴17 小时前
干货版《算法导论》05:从集合接口到排序
开发语言·数据结构·c++·程序人生·算法·排序
神午侠意17 小时前
电池 AI 云平台搭建
程序人生
humors2211 天前
从数据到决策:汽车使用成本的精细计算指南
大数据·程序人生
郝学胜-神的一滴1 天前
Qt 高级开发 010: 从跨界面传值到自定义信号
开发语言·c++·qt·程序人生·用户界面
婷婷_1722 天前
JTAG (IEEE 1149.1)学习记录
学习·程序人生·debug·芯片·jtag·phy·eth/pcie
humors2212 天前
听劝和辨劝
大数据·程序人生
这个DBA有点耶2 天前
COUNT进阶:超大表的近似计数与HyperLogLog
数据库·sql·程序人生·学习方法·dba·改行学it