LangChain Expression Language (LCEL) 速查表:从基础操作到高级组合的全面指南

在使用 LangChain 构建智能应用时,我们常常会遇到这样的场景:需要将多个组件灵活组合,实现复杂的逻辑流程。这时候,LangChain Expression Language (LCEL) 就成为了我们手中的利器。但面对众多的 LCEL 原语,你是否也曾感到困惑?今天,我们就来系统整理 LCEL 的核心操作,打造一份实用的速查表,帮助你在开发中快速查阅、灵活运用。

一、LCEL 基础操作:从单个调用到批量处理

1. 调用可运行对象

在 LCEL 中,调用可运行对象是最基础的操作。我们可以通过Runnable.invoke()同步调用,或使用Runnable.ainvoke()异步调用:

python

from langchain_core.runnables import RunnableLambda

# 定义一个将输入转为字符串的可运行对象
runnable = RunnableLambda(lambda x: str(x))

# 同步调用
result = runnable.invoke(5)
print(result)  # 输出: '5'

# 异步调用(需配合async/await)
# result = await runnable.ainvoke(5)

2. 批量处理操作

当需要处理多个输入时,批量操作能大大提高效率:

python

from langchain_core.runnables import RunnableLambda

runnable = RunnableLambda(lambda x: str(x))

# 同步批量处理
results = runnable.batch([7, 8, 9])
print(results)  # 输出: ['7', '8', '9']

# 异步批量处理
# results = await runnable.abatch([7, 8, 9])

3. 流化处理

对于大型数据或需要实时处理的场景,流化操作是更好的选择:

python

from langchain_core.runnables import RunnableLambda

# 定义一个生成器函数,将输入逐个转为字符串
def func(x):
    for y in x:
        yield str(y)

runnable = RunnableLambda(func)

# 流化处理
for chunk in runnable.stream(range(5)):
    print(chunk)  # 依次输出: '0', '1', '2', '3', '4'

# 异步流化处理
# async for chunk in await runnable.astream(range(5)):
#     print(chunk)

二、组合与并行:构建复杂逻辑流程

1. 管道操作符:顺序组合

LCEL 的管道操作符|让我们可以轻松构建顺序执行的流程:

python

from langchain_core.runnables import RunnableLambda

# 第一个可运行对象:将输入包装为字典
runnable1 = RunnableLambda(lambda x: {"foo": x})
# 第二个可运行对象:将输入重复两次
runnable2 = RunnableLambda(lambda x: [x] * 2)

# 组合成链
chain = runnable1 | runnable2

# 调用链
result = chain.invoke(2)
print(result)  # 输出: [{'foo': 2}, {'foo': 2}]

2. 并行调用:同时执行多个任务

当多个任务可以并行处理时,RunnableParallel能显著提高效率:

python

from langchain_core.runnables import RunnableLambda, RunnableParallel

runnable1 = RunnableLambda(lambda x: {"foo": x})
runnable2 = RunnableLambda(lambda x: [x] * 2)

# 并行组合
chain = RunnableParallel(first=runnable1, second=runnable2)

# 调用并行链
result = chain.invoke(2)
print(result)  # 输出: {'first': {'foo': 2}, 'second': [2, 2]}

三、数据处理与转换:灵活操作输入输出

1. 将函数转换为可运行对象

通过RunnableLambda,我们可以将任意函数转换为可运行对象:

python

from langchain_core.runnables import RunnableLambda

# 定义一个简单函数
def func(x):
    return x + 5

# 转换为可运行对象
runnable = RunnableLambda(func)

# 调用
result = runnable.invoke(2)
print(result)  # 输出: 7

2. 合并输入输出字典

RunnablePassthrough.assign允许我们在保留原始输入的同时添加新的输出字段:

python

from langchain_core.runnables import RunnableLambda, RunnablePassthrough

# 定义一个计算函数
runnable1 = RunnableLambda(lambda x: x["foo"] + 7)

# 合并字典
chain = RunnablePassthrough.assign(bar=runnable1)

# 调用
result = chain.invoke({"foo": 10})
print(result)  # 输出: {'foo': 10, 'bar': 17}

3. 保留原始输入

RunnablePassthrough可以确保原始输入包含在输出中:

python

from langchain_core.runnables import RunnableLambda, RunnableParallel, RunnablePassthrough

runnable1 = RunnableLambda(lambda x: x["foo"] + 7)

# 并行组合,其中一个分支保留原始输入
chain = RunnableParallel(bar=runnable1, baz=RunnablePassthrough())

# 调用
result = chain.invoke({"foo": 10})
print(result)  # 输出: {'bar': 17, 'baz': {'foo': 10}}

四、高级功能:错误处理与配置优化

1. 添加默认参数

Runnable.bind让我们可以为可运行对象绑定默认参数:

python

from typing import Optional
from langchain_core.runnables import RunnableLambda

# 定义一个需要多个参数的函数
def func(main_arg: dict, other_arg: Optional[str] = None) -> dict:
    if other_arg:
        return {**main_arg, **{"foo": other_arg}}
    return main_arg

runnable1 = RunnableLambda(func)

# 绑定默认参数
bound_runnable1 = runnable1.bind(other_arg="bye")

# 调用
result = bound_runnable1.invoke({"bar": "hello"})
print(result)  # 输出: {'bar': 'hello', 'foo': 'bye'}

2. 错误 fallback 机制

Runnable.with_fallbacks可以为可运行对象添加备用方案:

python

from langchain_core.runnables import RunnableLambda

# 第一个可运行对象:尝试将输入加字符串"foo"
runnable1 = RunnableLambda(lambda x: x + "foo")
# 第二个可运行对象:作为 fallback
runnable2 = RunnableLambda(lambda x: str(x) + "foo")

# 添加 fallback 机制
chain = runnable1.with_fallbacks([runnable2])

# 调用(当runnable1失败时,自动使用runnable2)
result = chain.invoke(5)
print(result)  # 输出: '5foo'

3. 重试机制

Runnable.with_retry可以为可运行对象添加重试功能:

python

from langchain_core.runnables import RunnableLambda

counter = -1

# 定义一个可能失败的函数
def func(x):
    global counter
    counter += 1
    print(f"attempt with {counter=}")
    return x / counter

# 添加重试机制,最多尝试2次
chain = RunnableLambda(func).with_retry(stop_after_attempt=2)

# 调用
result = chain.invoke(2)
print(result)  # 输出: 2.0(第二次尝试成功)

五、配置与可定制化:提升灵活性

1. 配置可运行对象

RunnableConfig允许我们在调用时配置可运行对象的执行参数,以下代码在invoke方法中传入config参数,使用RunnableConfig来设置最大并发数:

python

from langchain_core.runnables import RunnableLambda, RunnableParallel, RunnableConfig

runnable1 = RunnableLambda(lambda x: {"foo": x})
runnable2 = RunnableLambda(lambda x: [x] * 2)
runnable3 = RunnableLambda(lambda x: str(x))

# 并行组合三个可运行对象
chain = RunnableParallel(first=runnable1, second=runnable2, third=runnable3)

# 调用时配置最大并发数为2
result = chain.invoke(7, config=RunnableConfig(max_concurrency=2))
print(result)  # 输出: {'first': {'foo': 7}, 'second': [7, 7], 'third': '7'}

2. 添加默认配置

Runnable.with_config可以为可运行对象设置默认配置:

python

from langchain_core.runnables import RunnableLambda, RunnableParallel

runnable1 = RunnableLambda(lambda x: {"foo": x})
runnable2 = RunnableLambda(lambda x: [x] * 2)
runnable3 = RunnableLambda(lambda x: str(x))

# 并行组合
chain = RunnableParallel(first=runnable1, second=runnable2, third=runnable3)

# 添加默认配置
configured_chain = chain.with_config(RunnableConfig(max_concurrency=2))

# 调用
result = configured_chain.invoke(7)
print(result)  # 输出: {'first': {'foo': 7}, 'second': [7, 7], 'third': '7'}

3. 使属性可配置

Runnable.configurable_fields让我们可以在运行时配置可运行对象的属性,以下示例定义了FooRunnable类,通过configurable_fields方法,使output_key属性在运行时可配置:

python

from typing import Any, Optional
from langchain_core.runnables import ConfigurableField, RunnableConfig, RunnableSerializable

# 定义一个可配置输出键的可运行对象
class FooRunnable(RunnableSerializable[dict, dict]):
    output_key: str

    def invoke(
        self, input: Any, config: Optional[RunnableConfig] = None, **kwargs: Any
    ) -> list:
        return self._call_with_config(self.subtract_seven, input, config, **kwargs)

    def subtract_seven(self, input: dict) -> dict:
        return {self.output_key: input["foo"] - 7}

# 创建实例
runnable1 = FooRunnable(output_key="bar")

# 使output_key可配置
configurable_runnable1 = runnable1.configurable_fields(
    output_key=ConfigurableField(id="output_key")
)

# 运行时配置output_key
result1 = configurable_runnable1.invoke(
    {"foo": 10}, config={"configurable": {"output_key": "not bar"}}
)
print(result1)  # 输出: {'not bar': 3}

# 使用默认配置
result2 = configurable_runnable1.invoke({"foo": 10})
print(result2)  # 输出: {'bar': 3}

六、动态与高级场景:应对复杂需求

1. 动态构建链

根据输入动态选择不同的可运行对象:

python

from langchain_core.runnables import RunnableLambda, RunnableParallel

runnable1 = RunnableLambda(lambda x: {"foo": x})
runnable2 = RunnableLambda(lambda x: [x] * 2)

# 根据输入值动态选择链
chain = RunnableLambda(lambda x: runnable1 if x > 6 else runnable2)

# 调用
result1 = chain.invoke(7)
print(result1)  # 输出: {'foo': 7}

result2 = chain.invoke(5)
print(result2)  # 输出: [5, 5]

2. 生成事件流

Runnable.astream_events允许我们获取可运行对象执行过程中的事件流:

python

import nest_asyncio
nest_asyncio.apply()
from langchain_core.runnables import RunnableLambda, RunnableParallel

runnable1 = RunnableLambda(lambda x: {"foo": x}, name="first")

# 定义一个生成事件的异步函数
async def func(x):
    for _ in range(5):
        yield x

runnable2 = RunnableLambda(func, name="second")

# 组合链
chain = runnable1 | runnable2

# 异步获取事件流
import asyncio

async def main():
    async for event in chain.astream_events("bar", version="v2"):
        print(f"event={event['event']} | name={event['name']} | data={event['data']}")

asyncio.run(main())

3. 按完成顺序获取批处理结果

Runnable.batch_as_completed让我们可以按任务完成的顺序获取结果:

python

import time
from langchain_core.runnables import RunnableLambda, RunnableParallel

# 定义一个带延迟的可运行对象
runnable1 = RunnableLambda(lambda x: time.sleep(x) or print(f"slept {x}"))

# 按完成顺序获取结果
for idx, result in runnable1.batch_as_completed([5, 1]):
    print(idx, result)  # 先输出1 None,再输出0 None

七、结果处理与可视化:提升开发效率

1. 返回输出字典的子集

Runnable.pick允许我们只保留输出字典中的指定字段:

python

from langchain_core.runnables import RunnableLambda, RunnablePassthrough

runnable1 = RunnableLambda(lambda x: x["baz"] + 5)

# 组合链:先计算,再挑选字段
chain = RunnablePassthrough.assign(foo=runnable1).pick(["foo", "bar"])

# 调用
result = chain.invoke({"bar": "hi", "baz": 2})
print(result)  # 输出: {'foo': 7, 'bar': 'hi'}

2. 声明式批处理

Runnable.map可以声明式地创建批处理版本:

python

from langchain_core.runnables import RunnableLambda

runnable1 = RunnableLambda(lambda x: list(range(x)))
runnable2 = RunnableLambda(lambda x: x + 5)

# 组合链:先生成范围,再对每个元素加5
chain = runnable1 | runnable2.map()

# 调用
result = chain.invoke(3)
print(result)  # 输出: [5, 6, 7]

3. 获取可运行对象的图形表示

Runnable.get_graph可以帮助我们可视化可运行对象的结构:

python

from langchain_core.runnables import RunnableLambda, RunnableParallel

runnable1 = RunnableLambda(lambda x: {"foo": x})
runnable2 = RunnableLambda(lambda x: [x] * 2)
runnable3 = RunnableLambda(lambda x: str(x))

# 组合复杂链
chain = runnable1 | RunnableParallel(second=runnable2, third=runnable3)

# 获取图形表示并打印
graph = chain.get_graph()
graph.print_ascii()

4. 提取链中的所有提示

Runnable.get_prompts可以帮助我们获取链中包含的所有提示模板:

python

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableLambda

# 定义提示模板
prompt1 = ChatPromptTemplate.from_messages(
    [("system", "good ai"), ("human", "{input}")]
)
prompt2 = ChatPromptTemplate.from_messages(
    [
        ("system", "really good ai"),
        ("human", "{input}"),
        ("ai", "{ai_output}"),
        ("human", "{input2}"),
    ]
)

# 定义假的LLM
fake_llm = RunnableLambda(lambda prompt: "i am good ai")

# 组合链
chain = prompt1.assign(ai_output=fake_llm) | prompt2 | fake_llm

# 提取并打印所有提示
for i, prompt in enumerate(chain.get_prompts()):
    print(f"**prompt {i=}**\n")
    print(prompt.pretty_repr())
    print("\n" * 3)

5. 添加生命周期监听器

Runnable.with_listeners可以让我们在可运行对象的生命周期中添加监听器:

python

import time
from langchain_core.runnables import RunnableLambda
from langchain_core.tracers.schemas import Run

# 定义开始和结束监听器
def on_start(run_obj: Run):
    print("start_time:", run_obj.start_time)

def on_end(run_obj: Run):
    print("end_time:", run_obj.end_time)

runnable1 = RunnableLambda(lambda x: time.sleep(x))

# 添加监听器
chain = runnable1.with_listeners(on_start=on_start, on_end=on_end)

# 调用
chain.invoke(2)

八、总结与实践建议

通过这份速查表,我们系统整理了 LCEL 的核心原语和用法,从基础的调用、批量处理,到复杂的组合、动态构建,再到高级的配置、事件流处理。LCEL 的强大之处在于它提供了一种统一的方式来表达和组合各种可运行对象,让我们可以轻松构建复杂的逻辑流程。

如果本文对你有帮助,别忘了点赞收藏,关注我,一起探索更高效的开发方式~

你可能感兴趣的:(LangChain,langchain,python)