为什么写这篇
前三章(模型、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,上一节的输出是下一节的输入:
- 你传
{"text": "你好"}(dict) prompt把 dict 填进模板,产出PromptValue(填好的消息)llm调模型,产出AIMessage(模型回复)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 由管道自动检索,不用用户手动给易错点(我亲手踩过)
- assign 的 key 必须和 prompt 的
{变量}对上——少了报错,多了忽略 - value 必须是函数(lambda),不是写死的字符串——写死字符串是退化用法,真实场景是动态计算
- 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