[LangChain之链-06]配置绑定、失败重试、降级和成员过滤

接下来介绍三个以代理形式存在的Runnable类型,其中RunnableBinding使Runnable对象可以绑定配置和参数,RunnableRetry则实现了针对失败重试策略的绑定。RunnableWithFallbacks可以在被代理Runnable对象执行失败后,利用一组Runnable对象作为后备。RunnablePick虽然不属于代理,但是使用起来类似于代理,因为它从被代理Runnable的输出中摘取指定的成员。

1. RunnableBinding

为了避免在每次在调用Runnable对象的时候重复指定相同的配置和参数,我们可以这些配置和参数直接绑定在Runnable对象上。比如在如下这个演示实例中,我们自定义了一个RunnableFoobar类型,并在重写的invoke方法中传入的RunnableConfig和关键字参数打印出来。我们随后创建了一个RunnableBinding对象,并利用构造函数的参数提供了绑定的RunnableFoobar对象、RunnableConfig配置和关键字参数。

from langchain_core.runnables import Runnable, RunnableBinding, RunnableConfig
from typing import Any

class RunnableFoobar(Runnable[Any, Any]):
   def invoke(
        self,
        input: Any,
        config: RunnableConfig | None = None,
        **kwargs: Any,
    ) -> Any:
        print(f"""\
config: {config}
kwargs: {kwargs}
            """)
        return input

runnable = RunnableBinding(
    bound= RunnableFoobar (),
    config={"configurable": {"baz": "ccc"}},
    kwargs={
        "foo":"aaa",
        "bar":"bbb"
    }
)

runnable.invoke(input={},config ={"configurable":{"qux":"ddd"}},quux ="eee")

我们调用最终RunnableBinding对象的invoke方法,并同时指定了配置和关键字参数。从如下的输出结果可以看出,绑定的配置和参数与调用时指定的配置和参数合并后的结果就是最终使用的配置和参数。

config: {'tags': [], 'metadata': {'baz': 'ccc', 'qux': 'ddd'}, 'configurable': {'baz': 'ccc', 'qux': 'ddd'}}
kwargs: {'foo': 'aaa', 'bar': 'bbb', 'quux': 'eee'}

Runnablebindwith_config方法都会创建一个RunnableBinding对象。RunnableBindingBaseRunnableBinding的基类,在调用构造函数创建RunnableBinding对象的时候,我们不仅可以利用boundkwargsconfig参数绑定的Runnable对象、关键字参数和配置,还可以利用config_factories指定一组创建RunnableConfig配置的工厂,甚至可以利用custom_input_typecustom_output_type指定绑定的输入输出类型。与这些绑定内容相关的属性和方法在RunnableBindingBase中都会进行相应的改写。

class RunnableBinding(RunnableBindingBase[Input, Output])

class RunnableBindingBase(RunnableSerializable[Input, Output]):
    bound: Runnable[Input, Output]
    kwargs: Mapping[str, Any] = Field(default_factory=dict)
    config: RunnableConfig = Field(default_factory=RunnableConfig)
    config_factories: list[Callable[[RunnableConfig], RunnableConfig]] = Field(
        default_factory=list
    )
    custom_input_type: Any | None = None
    custom_output_type: Any | None = None
    def __init__(
        self,
        *,
        bound: Runnable[Input, Output],
        kwargs: Mapping[str, Any] | None = None,
        config: RunnableConfig | None = None,
        config_factories: list[Callable[[RunnableConfig], RunnableConfig]]
        | None = None,
        custom_input_type: type[Input] | BaseModel | None = None,
        custom_output_type: type[Output] | BaseModel | None = None,
        **other_kwargs: Any,
    ) -> None:
        super().__init__(
            bound=bound,
            kwargs=kwargs or {},
            config=config or {},
            config_factories=config_factories or [],
            custom_input_type=custom_input_type,
            custom_output_type=custom_output_type,
            **other_kwargs,
        )

    @override
    def invoke(
        self,
        input: Input,
        config: RunnableConfig | None = None,
        **kwargs: Any | None,
    ) -> Output:
        return self.bound.invoke(
            input,
            self._merge_configs(config),
            **{**self.kwargs, **kwargs},
        )

   def _merge_configs(self, *configs: RunnableConfig | None) -> RunnableConfig:
        config = merge_configs(self.config, *configs)
        return merge_configs(config, *(f(config) for f in self.config_factories))

重写的invoke方法会直接调用绑定Runnable对象的invoke方法,当前调用提供的配置和关键字参数会与绑定的配置和参数进行合并后作为其参数。在合并配置的时候,不仅会使用绑定的配置,还用使用由配置工厂创建的配置。其他的ainvokestream/astreamtransform/atransform方法也做了类似的重写。

class Runnable(ABC, Generic[Input, Output]):
     def bind(self, **kwargs: Any) -> Runnable[Input, Output]:
        return RunnableBinding(bound=self, kwargs=kwargs, config={})

    def with_config(
        self,
        config: RunnableConfig | None = None,
        # Sadly Unpack is not well-supported by mypy so this will have to be untyped
        **kwargs: Any,
    ) -> Runnable[Input, Output]:
        return RunnableBinding(
            bound=self,
            config=cast(
                "RunnableConfig",
                {**(config or {}), **kwargs},
            ),
            kwargs={},
        )
    def with_types(
        self,
        *,
        input_type: type[Input] | None = None,
        output_type: type[Output] | None = None,
    ) -> Runnable[Input, Output]:
        return RunnableBinding(
            bound=self,
            custom_input_type=input_type,
            custom_output_type=output_type,
            kwargs={},
        )
    def with_listeners(
        self,
        *,
        on_start: Callable[[Run], None]
        | Callable[[Run, RunnableConfig], None]
        | None = None,
        on_end: Callable[[Run], None]
        | Callable[[Run, RunnableConfig], None]
        | None = None,
        on_error: Callable[[Run], None]
        | Callable[[Run, RunnableConfig], None]
        | None = None,
    ) -> Runnable[Input, Output]:
        return RunnableBinding(
            bound=self,
            config_factories=[
                lambda config: {
                    "callbacks": [
                        RootListenersTracer(
                            config=config,
                            on_start=on_start,
                            on_end=on_end,
                            on_error=on_error,
                        )
                    ],
                }
            ],
        )

    def with_alisteners(
        self,
        *,
        on_start: AsyncListener | None = None,
        on_end: AsyncListener | None = None,
        on_error: AsyncListener | None = None,
    ) -> Runnable[Input, Output]:
        return RunnableBinding(
            bound=self,
            config_factories=[
                lambda config: {
                    "callbacks": [
                        AsyncRootListenersTracer(
                            config=config,
                            on_start=on_start,
                            on_end=on_end,
                            on_error=on_error,
                        )
                    ],
                }
            ],
        )  

Runnablebindwith_configwith_typeswith_listenerswith_alisteners方法以如上的方式创建RunnableBinding对象,所以上面的例子可以简写成如下的形式。

runnable = (RunnableFoo()
    .bind(
        foo = "aaa",
        bar = "bbb")
    .with_config(config={"configurable": {"baz": "ccc"}}))

runnable.invoke(input={},config ={"configurable":{"qux":"ddd"}},quux ="eee")

2.RunnableRetry

Runnable 添加自动重试策略的RunnableRetry是提升应用可靠性的关键组件。它专门用于处理因网络波动、模型限流或临时服务器错误而导致的瞬时失败。RunnableRetry是一个包装器,它包裹一个现有的Runnable。当被包裹的组件抛出指定的异常时,它会根据预设的策略自动重新尝试执行,而无需在业务逻辑中手动编写try-exceptwhile循环。

比如在如下这个演示程序中,我们利用函数create_runnable创建了一个RunnableLambda对象,处理函数利用一个闭包使它会在前两次调用中抛出异常。我们以此RunnableLambda创建两个RunnableRetry,并将重试次数(包含最初的调用)分别设置为2和3,执行的结果表明前者会失败,后者会成功。

from langchain_core.runnables import Runnable, RunnableLambda
from langchain_core.runnables.retry import RunnableRetry
from  typing import Any

def create_runnable()-> Runnable[Any, str]:
    counter = 0
    def handle(_:Any) -> str:
        nonlocal counter
        counter += 1
        if counter < 3:
            raise Exception(f"manually thrown exception")
        return "SUCCESS"
    return RunnableLambda(handle)

runnable = RunnableRetry(
    bound=create_runnable(), 
    max_attempt_number =2)

try:
    runnable.invoke(None)
    assert False, "Expected an exception to be raised"
except Exception as e:
    assert str(e) == "manually thrown exception"

runnable = RunnableRetry(
    bound=create_runnable(),
    max_attempt_number =3)
result = runnable.invoke(None)
assert result == "SUCCESS"

RunnableRetryRunnableBinding一样同属RunnableBindingBase的子类。从它的构造含函数可以看出,它在现有可绑定元素基础上添加了针对重试策略的绑定,具体包括异常类型、最大重新次数以及与基于指数的退避重试(通过ExponentialJitterParams类型表示)和防抖开关。在重写的invoke/ainvokestream/astreamtransform/atransform方法中,这些绑定的重试策略会被合并到指定的RunnableConfig中被应用。

class RunnableRetry(RunnableBindingBase[Input, Output]):
    retry_exception_types: tuple[type[BaseException], ...] = (Exception,)
    wait_exponential_jitter: bool = True
    exponential_jitter_params: ExponentialJitterParams | None = None
    max_attempt_number: int = 3

    def __init__(
        self,
        *,
        name: str | None = None,
        bound: Runnable[Input@Runnable, Output@Runnable],
        kwargs: Mapping[str, Any] = dict,
        config: RunnableConfig = RunnableConfig,
        config_factories: list[(RunnableConfig) -> RunnableConfig] = list,
        custom_input_type: Any | None = None,
        custom_output_type: Any | None = None,
        retry_exception_types: tuple[type[BaseException], ...] = (Exception, ),
        wait_exponential_jitter: bool = True,
        exponential_jitter_params: ExponentialJitterParams | None = None,
        max_attempt_number: int = 3
    ) -> None

class ExponentialJitterParams(TypedDict, total=False):
    initial: float
    max: float
    exp_base: float
    jitter: float

class Runnable(ABC, Generic[Input, Output]):
    def with_retry(
        self,
        *,
        retry_if_exception_type: tuple[type[BaseException], ...] = (Exception,),
        wait_exponential_jitter: bool = True,
        exponential_jitter_params: ExponentialJitterParams | None = None,
        stop_after_attempt: int = 3,
    ) -> Runnable[Input, Output]:
        return RunnableRetry(
            bound=self,
            kwargs={},
            config={},
            retry_exception_types=retry_if_exception_type,
            wait_exponential_jitter=wait_exponential_jitter,
            max_attempt_number=stop_after_attempt,
            exponential_jitter_params=exponential_jitter_params,
        )	

Runnable定义的with_retry方法以如上的形式创建了RunnableRetry,所以上面的演示程序可以简写成如下的形式。顺便说一下,表示重试次数的参数以stop_after_attempt命名,但是构造函数对应的参数名是max_attempt_number

try:
    create_runnable().with_retry(stop_after_attempt=2).invoke(None)
    assert False, "Expected an exception to be raised"
except Exception as e:
    assert str(e) == "manually thrown exception"

runnable = create_runnable().with_retry(stop_after_attempt=3)
result = runnable.invoke(None)
assert result == "SUCCESS"

3. RunnableWithFallbacks

RunnableWithFallbacks同样是另一个Runnable对象的代理,针对它的调用会被它转发给被代理的对象。但是它会捕获调用异常,并根据设置的降级策略按照顺序调用预先提供的后备Runnable直至成功。比如在如下这个演示程序中,我们创建了三个RunnableLambda,并人为命名为foobarbazRunnableWithFallbacks是对foo的代理,barbaz按序作为后备。被代理和第一个后备对应的Runnablefoobar)总是会抛出异常,但第二个后备baz会成功。所以针对RunnableWithFallbacks的调用最终会成功,而通过log列表的记录表明三个Runnable均参与进来。

from langchain_core.runnables import RunnableLambda
from langchain_core.runnables.fallbacks import RunnableWithFallbacks
from  typing import Any
from functools import partial

log= []

def handle(name:str, throw_exception:bool,_:Any)-> str:
    log.append(name)
    if throw_exception:
        raise Exception(f"manually thrown exception in {name}")
    return "SUCCESS"

runnable = RunnableWithFallbacks(
    runnable=RunnableLambda(partial(handle, "foo", True)),
    fallbacks=[
        RunnableLambda(partial(handle, "bar", True)),
        RunnableLambda(partial(handle, "baz", False)),
    ])
    
result = runnable.invoke(None)
assert result == "SUCCESS"
assert log== ["foo", "bar", "baz"]

RunnableWithFallbacks派生于RunnableSerializable,它的runnablefallbacks字段分别表示被代理的Runnable和后备Runnable列表。它还有两个额外的字段,其中exceptions_to_handle表示实施降级策略的异常类型。如果链以字典作为输入和输出数据,被代理或者前一个Runnable抛出的异常会写入作为输出的字典,以此传递给后面执行的Runnable。这个存储存储异常的Key通过exception_key进行设置。后备策略的实施实现在重写的invoke/ainvokestream/astreamtransform/atransform方法中。

class RunnableWithFallbacks(RunnableSerializable[Input, Output]):
    runnable: Runnable[Input, Output]
    fallbacks: Sequence[Runnable[Input, Output]]
    exceptions_to_handle: tuple[type[BaseException], ...] = (Exception,)
    exception_key: str | None = None

我们对前面略加改动来演示异常在处理流程中的传递。我们首先将三个RunnableLambda的输入、输出以及log变量均改写成字典,并在没有抛出异常的情况下在其中添加状态("status": "SUCCESS")表示成功执行。我们在创建RunnableWithFallbacks时将exception_key设置为“error”。每个Runnable在执行的时候,我们会利用这个Key从输入字典中提取前面抛出的异常,并添加到log中。程序给出的断言表明:barbaz在执行的时候均能从输入字典中得到上一个Runnable抛出的异常。

from langchain_core.runnables import RunnableLambda
from langchain_core.runnables.fallbacks import RunnableWithFallbacks
from functools import partial

log: dict[str, Exception|None] = {}

def handle(name:str, throw_exception:bool,input:dict)-> dict:
    log[name] = input.get("error", None)
    if throw_exception:
        raise Exception(f"manually thrown exception in {name}")
    return {**input, "status": "SUCCESS"}

runnable = RunnableWithFallbacks(
    runnable=RunnableLambda(partial(handle, "foo", True)),
    fallbacks=[
        RunnableLambda(partial(handle, "bar", True)),
        RunnableLambda(partial(handle, "baz", False)),
    ],
    exception_key="error")
    
result = runnable.invoke({})
assert result["status"] == "SUCCESS"
assert list(log.keys()) == ["foo", "bar", "baz"]
assert log["foo"] is None
assert str(log["bar"]) == "manually thrown exception in foo"
assert str(log["baz"]) == "manually thrown exception in bar"

Runnable定义的with_fallbacks方法会以如下的方式创建RunnableWithFallbacks对象。

class Runnable(ABC, Generic[Input, Output]):
    def with_fallbacks(
        self,
        fallbacks: Sequence[Runnable[Input, Output]],
        *,
        exceptions_to_handle: tuple[type[BaseException], ...] = (Exception,),
        exception_key: str | None = None,
    ) -> RunnableWithFallbacksT[Input, Output]:
        return RunnableWithFallbacks(
            runnable=self,
            fallbacks=fallbacks,
            exceptions_to_handle=exceptions_to_handle,
            exception_key=exception_key,
        )

所以上面的例子针对RunnableWithFallbacks的创建可以改写成如下的形式。

runnable =RunnableLambda(partial(handle, "foo", True)).with_fallbacks(  
    fallbacks=[
        RunnableLambda(partial(handle, "bar", True)),
        RunnableLambda(partial(handle, "baz", False)),
    ],
    exception_key="error")    

4.RunnablePick

RunnablePick是一个用于字段提取的Runnable类型。如果说RunnableAssign的作用是加法,那么RunnablePick的作用就是减法RunnablePick的主要任务是从上游传来的字典中,根据指定的Key,提取出对应的值并将其作为下一步的输入。指定的Key对应keys字段,如果只提取单个成员,它可以是一个字符串,否则就是一个列表。具体的提取逻辑实现在_pick方法中,它对输入的验证表明RunnablePick只支持类型为字典的输入。RunnablePick重写的invoke/ainvokestream/astreamtransform/atransform方法会调用此方法生成所需输出。

class RunnablePick(RunnableSerializable[dict[str, Any], Any]):
    keys: str | list[str]
    def __init__(self, keys: str | list[str], **kwargs: Any) -> None:
        super().__init__(keys=keys, **kwargs)

    def _pick(self, value: dict[str, Any]) -> Any:
        if not isinstance(value, dict):
            msg = "The input to RunnablePassthrough.assign() must be a dict."
            raise ValueError(msg)  # noqa: TRY004

        if isinstance(self.keys, str):
            return value.get(self.keys)
        picked = {k: value.get(k) for k in self.keys if k in value}
        if picked:
            return AddableDict(picked)
        return None	

从定义可以看出,RunnablePick是一个独立处理字典数据的Runnable对象,并不是一个代理。但是我们一般不会直接创建它,而是调用某个Runnable对象如下所示的pick方法。这个方法会创建一个RunnablePick对象,并返回它与当前Runnable对象构建的管道。

class Runnable(ABC, Generic[Input, Output]):
    def pick(self, keys: str | list[str]) -> RunnableSerializable[Any, Any]:
        return self | RunnablePick(keys)

RunnablePick虽然简单,但也隐藏一些容易忽略的细节:

  • 如果针对单个成员的提取,并以字符串指定数据成员,会返回数据成员的值;否则返回的是字典;
  • 如果指定成员部分不存在,不会有异常抛出,但是不存在的成员不会包含在返回的字典中;
  • 如果指定的成员全部不存在,返回的是None,而不是一个空字典。

上面的这些规则体现在如下所示的演示程序中:

from langchain_core.runnables import RunnablePassthrough
runnable = RunnablePassthrough().pick(["foo", "bar"])
input = {"foo": "123", "bar": "456", "baz": "789"}
result = runnable.invoke(input)
assert result == {"foo": "123", "bar": "456"}

runnable = RunnablePassthrough().pick("foo")
result = runnable.invoke(input)
assert result == "123"

runnable = RunnablePassthrough().pick(["foo"])
result = runnable.invoke(input)
assert result == {"foo": "123"}

runnable = RunnablePassthrough().pick(["foo","qux"])
result = runnable.invoke(input)
assert result == {"foo": "123"}

runnable = RunnablePassthrough().pick(["qux","quux"])
result = runnable.invoke(input)
assert result == None
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值