提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
看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
是组合了当前Step
和other
的函数。
2) 组合函数:
- 在
__or__
方法中,combined_func
首先调用当前Step
的func
,然后将结果传递给other
的func
。 combined_func
最终被封装到一个新的Step
对象中。
3) 使用管道符链接步骤:
step1 | step2
创建了一个新的Step
对象,该对象的func
是step1
和step2
的组合。- 调用
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的知识