☰
啃 LangChain 第四章 LCEL:管道符 | 背后的世界,信息的交通
2026/10/3 16:59:26 网站建设 项目流程

为什么写这篇

前三章(模型、Message、结构化输出)学完,觉得自己"入门了"。结果一翻到第四章LCEL(LangChain Expression Language),整个人比较蒙。

prompt | llm | StrOutputParser()这行代码,视频里老师敲得飞快,我跟着敲也能跑。但让我讲讲|到底干了什么、RunnablePassthrough为什么存在、assign里的lambda x到底 x 是什么——全答不上来。

这章是 LangChain 最抽象的部分,也是后续所有高级功能(RAG、Agent、记忆)的地基。我手写了三张笔记,反复啃了啃,现在可以能用人话讲清楚了。、

一、LCEL 是什么:一个符号,五种组件

LCEL = LangChain Expression Language,核心就是管道符|。

它把"数据不断被处理、传递"的过程,写成一行管道,高复用、不断输出。

数据 → 处理A → 处理B → 处理C → 输出

LangChain 里有 5 个核心组件,名字都带Runnable(可运行):

组件作用类比
Runnable所有可上链对象的基类水管接口
RunnableLambda把普通函数变成链上的一节把任意水泵接进水管
RunnablePassthrough输入原样输出,assign可加字段直通管 + 可加旁路
RunnableParallel一个输入同时分发给多个并行处理分水器
invoke/batch/stream任何链的调用方式开水龙头的三种姿势

一句话:所有能上链的东西,都实现了 Runnable 接口,所以能用|连起来。


二、基础管道:prompt | llm | StrOutputParser()

这是最常见的链,也是理解后续一切的起点。

fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParser prompt=ChatPromptTemplate.from_messages([("system","你是一个翻译。"),("human","把这句话翻译成英文:{text}"),])chain=prompt|llm|StrOutputParser()chain.invoke({"text":"你好"})

数据流转(重点)

dict → [prompt] → PromptValue → [llm] → AIMessage → [StrOutputParser] → str

每一节都是一个 Runnable,上一节的输出是下一节的输入:

  1. 你传{"text": "你好"}(dict)
  2. prompt把 dict 填进模板,产出PromptValue(填好的消息)
  3. llm调模型,产出AIMessage(模型回复)
  4. StrOutputParser把 AIMessage 里的文本抽出来,产出str

|的本质:把左边的输出,作为右边的输入。


三、RunnableLambda:把普通函数接进水管

链上的每一节必须是 Runnable。那普通函数怎么上链?用RunnableLambda包一层。

fromlangchain_core.runnablesimportRunnableLambdadef加粗(text:str)->str:returnf"**{text}**"chain=prompt|llm|StrOutputParser()|RunnableLambda(加粗)print(chain.invoke({"text":"你好"}))# 输出会被加粗

适用场景:链的中间需要自定义处理逻辑。比如对模型输出做后处理(清洗、格式化、加标签)。

我手写笔记里特别标了一句:可实现自定义的逻辑处理。这就是 RunnableLambda 存在的意义——让你不写子类也能往链里塞逻辑。


四、RunnablePassthrough:透传 + assign 加字段


这是我卡最久的一个概念。

单独的 RunnablePassthrough:价值不大

RunnablePassthrough().invoke("hello")# 原样输出 "hello"

输入啥输出啥,单独用没意义。真正的威力在.assign()。

assign:在字典上动态加字段

fromlangchain_core.runnablesimportRunnablePassthrough add=RunnablePassthrough.assign(context=lambdax:"检索到的文档",question=lambdax:"问题",)add.invoke({"question":"什么是 RAG?"})# {"question": "什么是 RAG?", "context": "检索到的文档", "question2": "问题"}

assign 的 key 是你定义的字段名,value 是函数(接收原字典,返回新值)。

为什么需要 assign?(核心问题)

我一开始最大的疑问是:prompt 里的{context}不也是变量吗,为什么必须用 assign 加字段?

答案是:prompt 的{}只是空位,它自己不会产生内容,必须有人把值塞进去。

  • 如果值是用户手动提供的 → 不需要 assign,直接chain.invoke({"context": "...", "question": "..."})也没问题
  • 如果值需要系统自动生成(比如检索文档)→ 必须用 assign,在管道运行时自动算出来,在我看来,有一种一个内部消息内部消化的感觉

这就是 RAG 的核心:用户只问问题,系统自动检索文档,assign 负责把检索结果塞进字典,prompt 才能填空。

chain=(RunnablePassthrough.assign(context=lambdax:retriever.invoke(x["question"])# 自动检索)|prompt|llm)chain.invoke({"question":"什么是 RAG?"})# context 由管道自动检索,不用用户手动给

易错点(我亲手踩过)

  1. assign 的 key 必须和 prompt 的{变量}对上——少了报错,多了忽略
  2. value 必须是函数(lambda),不是写死的字符串——写死字符串是退化用法,真实场景是动态计算
  3. lambda 的输入 x 是原字典——你可以用x["question"]取到用户传的问题

五、RunnableParallel:并行分支

基本用法

fromlangchain_core.runnablesimportRunnableParallel parallel=RunnableParallel(joke=joke_chain,poem=poem_chain)out=parallel.invoke({"question":"月亮"})# {"joke": "笑话内容", "poem": "诗的内容"}

invoke 的输入字典会完整传给每一条链,每条链各取所需:

  • joke_chain 收到完整字典,只用question
  • poem_chain 同样收到完整字典,只用question
  • 多余的字段忽略,缺少的字段报错(那条链报错,整个 invoke 失败)

字典简写(等价写法)

# 写法1:RunnableParallel 类parallel1=RunnableParallel(joke=joke_chain,poem=poem_chain)# 写法2:直接写字典(语法糖,完全等价)parallel2={"joke":joke_chain,"poem":poem_chain}

LangChain 内部会自动把字典转成 RunnableParallel。日常基本都用字典简写。

并行结果汇总

并行返回的是字典,key 是你起的子链名。要把结果拼成一段文字,用 RunnableLambda:

defcombine(d:dict)->str:returnf"笑话:{d['joke']}\n诗:{d['poem']}"final=parallel|RunnableLambda(combine)print(final.invoke({"question":"月亮"}))

并行结果是字典,用自己定义的键名取出来。


六、本章总结:一张图理清

RunnableLambda → 普通函数上链 RunnablePassthrough.assign → 字典动态加字段(RAG 核心) RunnableParallel → 一个输入走多条链,返回字典

三者组合就能写出复杂的链:

chain=({"context":retriever,"question":RunnablePassthrough()}# 并行:检索 + 透传|prompt|llm|StrOutputParser())

这行代码是 RAG 的经典写法,拆开看就是 RunnableParallel + RunnablePassthrough + 基础管道的组合。


写在最后

学完这章最大的感受:抽象的东西一定要配具体场景理解,而且自己用笔去引导自己的思路也会更好

RunnablePassthrough.assign单看定义,我完全不知道它有什么用。直到写到 RAG 的代码——“用户只问问题,系统自动检索文档塞给模型”——才突然明白:哦,assign 就是干这个的。

如果你也在卡这章,我的建议是:别死磕概念,先把prompt | llm | parser跑通,然后去看 RAG 的最小实现,回来再看 assign 和 parallel,很多地方会突然通。

以及自己感觉现在的自学和高中有不一样的感受,高中方向太明确,也可以说太窄了,现在自己学的内容很适合主动发散,不必追求太耀眼的结果,思维在颅骨内一次次反射,也是一种各位可以尝试的感受,不稳会断,但没有失控感。

抽象概念理解起来费力是正常的,硬啃效率低。把它放到具体场景里,成本会降一半。


关于作者:平顶山学院 2026 级计算机专业新生,AI 工程方向自学者。爬虫起步,正在啃 LangChain,目标做出能落地的 AI 系统。

我的代码仓库:Gitee | 个人博客:small-mo.netlify.app

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询