
LCEL表达式语言LangChain的管道式组合让代码更灵活上一篇用LCEL写了三种链管道符一路接到底代码跑得也顺。但有个问题一直悬在那管道符到底干了什么RunnablePassthrough和RunnableLambda这些组件什么时候该用itemgetter和bind又是怎么回事。这篇就把LCEL表达式语言掰开揉碎讲一遍把这些零件的脾气摸清楚。LCEL是什么LCEL全称LangChain表达式语言英文叫LangChain Expression Language。它是一套写法约定让你用管道符把Runnable组件串起来像搭积木一样拼出完整流程。为什么要搞这么一套东西。早期LangChain写链要继承一堆类LLMChain、SequentialChain、RouterChain每个类参数不同组合起来代码又长又乱。LCEL把这些全统一成一个接口所有组件都是Runnable都能用竖线连接。你只要记住一个符号就是竖线。竖线左边组件的输出自动变成竖线右边组件的输入。和Unix管道一个道理cat file | grep keyword数据从左往右流。这种写法组合性强想加一步就在中间插一个管道想换掉某一步就替换那一段其余代码一行不动。管道符背后发生了什么每个Runnable组件都实现了__or__方法这是管道符的底层机制。你写a | bPython实际调用的是a.__or__(b)返回一个新的Runnable内部记录了a和b的顺序。invoke的时候先跑a把结果喂给b。这样设计有个好处整条链本身也是一个Runnable对象你能对它做任何Runnable能做的事。invoke同步调用stream流式输出batch批量处理异步版ainvoke、astream、abatch都有。拼好一条链之后同步切异步只要换个方法名链本身一行不用动。我第一次发现这事的时候挺惊喜之前为了上异步还重写过整条流程。stream这个方法做聊天界面时特别实用。用户等不及整段输出stream能把模型生成的字一个一个往外吐前端接上就是打字机效果等待感弱很多。batch也顺手一坨输入丢进去批量跑比循环调invoke快不少。RunnablePassthrough原样传递RunnablePassthrough干的事情很简单把输入原封不动传给输出。听着像废话拼链的时候却特别有用。RAG场景里检索器需要用户的原始问题提示词模板又需要检索到的文档两个地方都要拿到输入。这时候就得让原始问题一路透传下去RunnablePassthrough就是干这个的。fromlangchain_core.runnablesimportRunnablePassthrough# 输入什么就输出什么print(RunnablePassthrough().invoke({question:什么是LCEL}))# {question: 什么是LCEL}它还有个assign方法能在保留原输入的基础上追加新字段。下面RAG示例里你会看到它的用法拿来做字段拼接很顺手。RunnableLambda把普通函数塞进流水线RunnableLambda把任意Python函数包装成Runnable这样你的自定义逻辑也能用管道符串起来。fromlangchain_core.runnablesimportRunnableLambdadefto_upper(text):returntext.upper()chainRunnableLambda(to_upper)print(chain.invoke(hello))# HELLO我踩过一个坑。函数接收的参数得和上一步输出对得上。上一步输出字符串你的函数就得接收字符串上一步输出字典你的函数就得接收字典。类型不匹配报错信息有时候挺绕的新手容易卡半天。建议写函数前先想清楚上一步吐出来的是什么。其实新版本LCEL有个贴心设计你直接把普通函数写进管道里它会自动包成RunnableLambdaretriever | format_docs这种写法能直接跑不用显式套一层。RunnableParallel和itemgetter并行与取值RunnableParallel让多个组件同时跑结果按key拼成一个字典返回。RAG里检索和问题改写可以并行能省时间。fromlangchain_core.runnablesimportRunnableParallel parallelRunnableParallel({original:RunnablePassthrough(),upper:RunnableLambda(lambdax:x.upper()),})print(parallel.invoke(hi))# {original: hi, upper: HI}LCEL还有个偷懒写法直接传字典就等于RunnableParallel不用显式写类名。上一篇顺序链里就用了这个{outline: outline_chain}字典本身就是并行执行。刚开始我看到字典出现在管道左边还愣了一下搞懂之后就觉得很自然。itemgetter是Python标准库operator里的函数经常和RunnableParallel配合从字典里取某个字段比写lambda简洁。fromoperatorimportitemgetter promptChatPromptTemplate.from_template(回答问题{question})chain({question:itemgetter(topic),context:RunnablePassthrough()}|prompt|model)itemgetter(“topic”)会从输入字典里取出topic字段省得写lambda x: x[topic]。字段一多itemgetter写法干净不少。bind给组件绑定参数bind方法给Runnable预先绑定一些参数调用时自动带上。最常见的用途是给模型绑定工具或者绑定停止词。fromlangchain_core.toolsimporttooltooldefsearch(query:str)-str:搜索网络内容returnf搜索结果{query}modelChatOpenAI(modelgpt-4o-mini)# 把工具绑定到模型上model_with_toolsmodel.bind_tools([search])chainprompt|model_with_tools|StrOutputParser()bind之后这个模型对象就带着工具了每次调用都会把工具信息发给模型。你也可以用bind绑定temperature这类参数model.bind(temperature0.7)同一段代码里同一个模型就能有不同配置不用为每个配置新建实例。还有个常见用法是绑定停止词model.bind(stop[\\n\\n])让模型生成到指定符号就停。做结构化输出时挺管用能挡住模型在结果后面啰嗦一大段。完整RAG示例把上面的东西凑一块写个完整的RAG链。检索相关文档和原始问题一起塞进提示词让模型回答。fromlangchain_openaiimportChatOpenAI,OpenAIEmbeddingsfromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParserfromlangchain_core.runnablesimportRunnablePassthroughfromlangchain_community.vectorstoresimportChromafromoperatorimportitemgetter embeddingsOpenAIEmbeddings()storeChroma(embedding_functionembeddings)store.add_texts([LCEL用管道符组合组件,RunnablePassthrough原样传递输入,RunnableParallel并行执行多个组件,])retrieverstore.as_retriever(search_kwargs{k:2})promptChatPromptTemplate.from_template(根据以下背景回答问题\n背景{context}\n问题{question})defformat_docs(docs):return\n.join(doc.page_contentfordocindocs)rag_chain({context:retriever|format_docs,question:itemgetter(question)}|prompt|ChatOpenAI(modelgpt-4o-mini)|StrOutputParser())print(rag_chain.invoke({question:LCEL怎么组合组件}))这段代码值得拆开看。字典里context走检索器再格式化成字符串question用itemgetter从输入取出来两路并行执行。然后塞进提示词过模型解析输出。一个小小的字典加几个管道符就把检索、格式化、拼提示词、调模型、解析输出全串好了。调试这种链有个习惯把链断成几段分别invoke确认每一段输出对得上再接起来。RAG最容易出问题的是检索这一步context拿不到东西模型就开始瞎编。建议先把retriever单独跑一遍看返回的文档质量没问题再接进链里。还有一个排查手段把prompt单独invoke一下看填充后的提示词长什么样。有时候变量没传进去模板里留着空占位符模型自然答非所问这种问题瞄一眼渲染结果就暴露了。这篇把LCEL的几个核心组件过了一遍管道符的底层机制、RunnablePassthrough的透传、RunnableLambda的函数包装、RunnableParallel的并行、itemgetter的取值、bind的参数绑定最后拼了个RAG链。你会发现LCEL的套路就这些再复杂的流程也是这些零件搭出来的多练几次手感就有了。下一篇聊回调与监控看看链跑起来之后怎么追踪每一步的执行情况token花在哪里耗时多少出问题怎么定位到具体那一段。