[LangChain之链]将可执行对象和生成器转换成标准组件

0 阅读3分钟

在 LangChain 的 LCEL (LangChain Expression Language) 设计体系中,这两个组件是实现自定义逻辑流式传输的核心桥梁。它们允许你将普通的 Python 函数包装成具备invokebatchstream能力的标准Runnable节点。

RunnableLambda

RunnableLambda将一个可调用对象转换为LangChain的Runnable 协议对象。RunnableLambda的·方法定义如下,我们可以指定同步和异步版本的可执行对象作为其参数,该可执行对象的其返回类型可以是单纯的输出(Output),也可以是输出迭代器(Iterator[Output]或者AsyncIterator[Output])或者是 “可等待” 的对象(Awaitable[Output]), 甚至是另一个Runnable对象。作为可执行对象的输入,除了原始的输入(Input)和执行配置(RunnableConfig)之外,还可以包含一个AsyncCallbackManagerForChainRun对象,后者提供一系列在相应事件执行的回调。

class RunnableLambda(Runnable[Input, Output]):
    def __init__(
        self,
        func: Callable[[Input], Iterator[Output]]
        | Callable[[Input], Runnable[Input, Output]]
        | Callable[[Input], Output]
        | Callable[[Input, RunnableConfig], Output]
        | Callable[[Input, CallbackManagerForChainRun], Output]
        | Callable[[Input, CallbackManagerForChainRun, RunnableConfig], Output]
        | Callable[[Input], Awaitable[Output]]
        | Callable[[Input], AsyncIterator[Output]]
        | Callable[[Input, RunnableConfig], Awaitable[Output]]
        | Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
        | Callable[
            [Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
        ],
        afunc: Callable[[Input], Awaitable[Output]]
        | Callable[[Input], AsyncIterator[Output]]
        | Callable[[Input, RunnableConfig], Awaitable[Output]]
        | Callable[[Input, AsyncCallbackManagerForChainRun], Awaitable[Output]]
        | Callable[
            [Input, AsyncCallbackManagerForChainRun, RunnableConfig], Awaitable[Output]
        ]
        | None = None,
        name: str | None = None,
    ) -> None

RunnableLambda通过重写的invoke/ainvoke方法完成了针对指定的Callable对象的调用。transform/atransformstream/astream方法在进行类似的重写。除此之外,那些用于提供输入输出Schema的方法也被重写,转变成分析Callable对象的输入和输出类型。

RunnableGenerator

RunnableGenerator是一种专门用于处理流式数据的可运行对象。它通常包装一个 迭代器,使得该函数能无缝集成到LangChain的链式调用中,并支持真正的增量输出。与普通的RunnableLambda不同,RunnableGenerator的设计初衷是为了解决的中断问题:

  • 保持流式:如果你在链的中间使用普通函数,流可能会在这一步被阻塞,直到函数执行完才输出;
  • 增量处理:RunnableGenerator允许你接收上游传来的每一个数据块,对其进行实时处理(如过滤、修饰),然后立即将其yield给下游;

从下面给出的__init__方法可以看出,指定的作为处理器的可执行对象的输入和输出均是迭代器(Iterator[T]或者AsyncIterator)。

class RunnableGenerator(Runnable[Input, Output]):
    def __init__(
        self,
        transform: Callable[[Iterator[Input]], Iterator[Output]]
        | Callable[[AsyncIterator[Input]], AsyncIterator[Output]],
        atransform: Callable[[AsyncIterator[Input]], AsyncIterator[Output]]
        | None = None,
        *,
        name: str | None = None,
    ) -> None

在如下的演示程序中,我们定义了upper_case将输入的字符串流进行大写,并同样以字符串流的形式返回。我们根据它创建了一个RunnableGenerator对象,并调用其transform方法流式处理指定的字符串列表。

from langchain_core.runnables import RunnableGenerator,RunnableLambda
from typing import Iterator

log:list[str] = []
def generate_inputs() -> Iterator[str]:
    for word in ["foo", "bar", "baz"]:
        log.append(word)
        yield word
def upper_case(input: Iterator[str]) -> Iterator[str]:
    for word in input:
        log.append(word.upper())
        yield word.upper()

words = []
runnable = RunnableGenerator(upper_case)
for word in runnable.transform(generate_inputs()):
    words.append(word)
assert words == ["FOO", "BAR", "BAZ"]
assert log == ["foo", "FOO", "bar", "BAR", "baz", "BAZ"]

需要强调的是,RunnableGeneratortransform/atransformstream/stream方法最终都会返回一个迭代器,但是它们有着本质的区别。只有transform/atransform会将输入参数视为迭代器,stream/stream只会将其视为单个输入。由于处理函数只接受迭代器作为参数,所以stream/stream方法会将输入封装成单元素列表提交给处理函数,如下这个演示程序证明了这一点。

from langchain_core.runnables import RunnableGenerator,RunnableLambda
from typing import Iterator

def transform(input:Iterator[str])-> Iterator[str]:
    for item in input:
        yield item
runnable = RunnableGenerator(transform)

result = runnable.transform(["foo","bar","baz"])
assert isinstance(result,Iterator)
assert "foo" == next(result)
assert "bar" == next(result)
assert "baz" == next(result)

result = runnable.stream(["foo","bar","baz"])
assert isinstance(result,Iterator)
assert ["foo","bar","baz"] == next(result)