LangChain 表达语言(LCEL)的底层是怎么实现的

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

看langchain文档里的一段示例代码,演示了怎么把提示 + 模型 + 输出解析器链接在一起。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
model = ChatOpenAI(model="gpt-4")
output_parser = StrOutputParser()

chain = prompt | model | output_parser

chain.invoke({"topic": "ice cream"})

不知道大家有没有疑问官方说 prompt | model | output_parser 类似于unix管道操作符,但是python本身不是默认支持管道操作符的,那它是怎么实现的呢


1. 运算符重载

在 Python 中,运算符重载允许类定义特殊方法来自定义标准运算符的行为。对于管道符 |,可以通过实现 __or__ 方法来自定义其行为。

1.1 什么是运算符重载

运算符重载是通过定义特定的魔术方法(特殊方法)来实现的。以下是一些常见运算符及其对应的魔术方法:

  • + : __add__
  • - : __sub__
  • * : __mul__
  • / : __truediv__
  • | : __or__

1.2 __or__ 方法

__or__ 方法,可以让你实现自定义使用 | 运算符时的行为。通过在类中定义 __or__ 方法,可以使得两个对象之间的 | 运算具有特定的含义。

1.3. __or__ 方法的定义

__or__ 方法的定义非常简单,接受一个参数,表示运算符右侧的对象。通常,__or__ 方法会返回一个新对象,代表了两个对象通过 | 运算符结合的结果。

1.3.1 示例:使用 __or__ 实现链式调用

以下是一个简单的示例,展示如何使用 __or__ 方法来实现链式调用。

class Step:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __or__(self, other):
        def combined_func(*args, **kwargs):
            result = self.func(*args, **kwargs)
            return other(result)
        return Step(combined_func)

# 定义两个步骤
step1 = Step(lambda x: x + 1)
step2 = Step(lambda x: x * 2)

# 使用管道符将步骤链接起来
pipeline = step1 | step2

# 执行管道
result = pipeline(3)
print(result)  # 输出: 8

1.3.2 解释

1) Step 类

  • __init__ 方法接受一个函数并将其存储在实例变量 func 中。
  • __call__ 方法使得 Step 实例可以像函数一样被调用,即 step1(3) 实际上调用的是 step1.func(3)
  • __or__ 方法定义了当使用 | 运算符时的行为。它创建并返回一个新的 Step 对象,其 func 是组合了当前 Stepother 的函数。

2) 组合函数

  • __or__ 方法中,combined_func 首先调用当前 Stepfunc,然后将结果传递给 otherfunc
  • combined_func 最终被封装到一个新的 Step 对象中。

3) 使用管道符链接步骤

  • step1 | step2 创建了一个新的 Step 对象,该对象的 funcstep1step2 的组合。
  • 调用 pipeline(3) 时,先执行 step1 的函数(3 + 1),然后将结果传递给 step2 的函数(4 * 2),最终得到结果 8

通过重载 __or__ 方法,可以自定义类在使用管道符 | 时的行为。这种方法可以用于实现链式调用,使得代码更加模块化和易读。LangChain 使用类似的技术,使得不同的处理步骤可以通过管道符自然地链接在一起,从而实现复杂的语言模型应用程序的流式处理。

二、langchain里的实现

1. Runnable

代码在 langchain_core/runnables/base.py 里的

class Runnable(Generic[Input, Output], ABC):

以下是类里的说明的翻译:

一个可以被调用、批处理、流式传输、转换和组合的工作单元。

1.1 主要方法

  • invoke/ainvoke:将单个输入转换为输出。
  • batch/abatch:高效地将多个输入转换为输出。
  • stream/astream:从单个输入流式传输输出。
  • astream_log:从一个输入流式传输输出和选定的中间结果。

内置优化:

  • 批处理:默认情况下,batch 使用线程池执行器并行运行 invoke()。可以通过重载优化批处理。
  • 异步:带有“a”后缀的方法是异步的。默认情况下,它们使用 asyncio 的线程池执行同步方法。可以通过重载实现原生异步。

所有方法都接受一个可选的配置参数,可以用于配置执行、添加标签和元数据以进行追踪和调试等。

可运行对象通过 input_schema 属性、output_schema 属性和 config_schema 方法公开其输入、输出和配置的示意信息。

1.2 LCEL 和组合

====================

LangChain 表达语言(LCEL)是一种将可运行对象组合成链的声明方式。任何以这种方式构建的链都将自动支持同步、异步、批处理和流式传输。

主要的组合原语是 RunnableSequence 和 RunnableParallel。

RunnableSequence 顺序调用一系列可运行对象,一个可运行对象的输出作为下一个的输入。可以使用 | 操作符或通过将一系列可运行对象传递给 RunnableSequence 来构建。

RunnableParallel 并发调用可运行对象,为每个可运行对象提供相同的输入。可以使用序列中的字典字面量或通过将字典传递给 RunnableParallel 来构建。

例如:

… code-block:: python

from langchain_core.runnables import RunnableLambda

# 使用 `|` 操作符构建的 RunnableSequence
sequence = RunnableLambda(lambda x: x + 1) | RunnableLambda(lambda x: x * 2)
sequence.invoke(1) # 4
sequence.batch([1, 2, 3]) # [4, 6, 8]


# 包含使用字典字面量构建的 RunnableParallel 的序列
sequence = RunnableLambda(lambda x: x + 1) | {
    'mul_2': RunnableLambda(lambda x: x * 2),
    'mul_5': RunnableLambda(lambda x: x * 5)
}
sequence.invoke(1) # {'mul_2': 4, 'mul_5': 10}

1.3 标准方法

所有可运行对象都提供其他方法,可用于修改其行为(例如,添加重试策略、添加生命周期监听器、使其可配置等)。

这些方法适用于任何可运行对象,包括通过组合其他可运行对象构建的可运行链。有关详细信息,请参阅各个方法。

例如:

… code-block:: python

from langchain_core.runnables import RunnableLambda

import random

def add_one(x: int) -> int:
    return x + 1


def buggy_double(y: int) -> int:
    '''有缺陷的代码,失败率为 70%'''
    if random.random() > 0.3:
        print('此代码失败,可能会被重试!')  # noqa: T201
        raise ValueError('触发了有缺陷的代码')
    return y * 2

sequence = (
    RunnableLambda(add_one) |
    RunnableLambda(buggy_double).with_retry( # 失败时重试
        stop_after_attempt=10,
        wait_exponential_jitter=False
    )
)

print(sequence.input_schema.schema()) # 显示推断的输入模式
print(sequence.output_schema.schema()) # 显示推断的输出模式
print(sequence.invoke(2)) # 调用序列(注意上面的重试!)

1.4 调试和追踪

随着链的增长,能够看到中间结果以调试和追踪链条是很有用的。

你可以将全局调试标志设置为 True,以启用所有链的调试输出:

.. code-block:: python

    from langchain_core.globals import set_debug
    set_debug(True)

或者,你可以将现有的或自定义的回调传递给任何给定的链:

.. code-block:: python

    from langchain_core.tracers import ConsoleCallbackHandler

    chain.invoke(
        ...,
        config={'callbacks': [ConsoleCallbackHandler()]}
    )

有关 UI(及更多功能),请查看 LangSmith:https://docs.smith.langchain.com/

2. Runnable 里的 OR

代码如下(示例):

def __or__(
    self,
    other: Union[
        Runnable[Any, Other],  # 接受一个 Runnable 对象
        Callable[[Any], Other],  # 接受一个函数,该函数将一个输入转换为输出
        Callable[[Iterator[Any]], Iterator[Other]],  # 接受一个生成器函数,将一组输入转换为一组输出
        Mapping[str, Union[Runnable[Any, Other], Callable[[Any], Other], Any]],  # 接受一个字典,该字典的值可以是 Runnable 对象、函数或任意其他值
    ],
) -> RunnableSerializable[Input, Other]:  # 返回一个新的 RunnableSerializable 对象
    """将此 runnable 与另一个对象组合以创建一个 RunnableSequence。"""
    
    # 将当前对象 self 与通过 coerce_to_runnable 函数转换的 other 对象组合成一个 RunnableSequence 并返回
    return RunnableSequence(self, coerce_to_runnable(other))

3. 回到上面的代码

我们翻看一下ChatPromptTemplate 和另外两个 跟踪一下代码,都会发现,它们都通过多层级 最终继承了Runnable

   prompt = ChatPromptTemplate.from_template("Tell me a short joke about{topic}")
   model = ChatOpenAI()
   output_parser = StrOutputParser()
   runnable = prompt | model | output_parser

总结

学习了 or 的特性之后,就可以理解langchain模拟unix管道符的链式处理的原理是什么了。后面我们会分别介绍 这里的prompt 、model 、output_parser的知识

  • 35
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值