[LangChain之链]RunnableCallable——将“自由定义”的函数变成标准组件

在众多针对Runnable的继承类型中,个人认为最重要的莫过于RunnableCallable。有过LangChain开发经验的人都知道,LangChain的很多组件都可以写成函数的形式,比如工具、中间件、LangGraph的节点等,而且它们的签名相对"自由",其参数不仅仅用于提供外部输入,还可以用来注入一些运行时对象,比如RuntimeBaseStoreStreamWriterRunnableConfig等。它们很多都会转换成RunnableCallable对象来使用。

1. 构造函数

RunnableLambda类似,RunnableCallable用于封装提供的同步或者异步函数,将它们转换成支持LCEL链的标准组件。从如下所示的代码片段可以看出,我们提供的函数签名对输入参数和返回值都没有限制。

python 复制代码
class RunnableCallable(Runnable):
    def __init__(
        self,
        func: Callable[..., Any | Runnable] | None,
        afunc: Callable[..., Awaitable[Any | Runnable]] | None = None,
        *,
        name: str | None = None,
        tags: Sequence[str] | None = None,
        trace: bool = True,
        recurse: bool = True,
        explode_args: bool = False,
        **kwargs: Any,
    ) -> None

除了提供的用于执行目标操作的funcafunc参数外,RunnableCallable的构造函数还提供了其他的关键字参数:

  • name:给这个节点起个名,方便在跟踪记录或日志里识别。如果没有指定,func或者afunc的名称会作为其名称;
  • tags:附加到跟踪记录的标签;
  • trace:开启LangSmitch跟踪的开关。如果设为False,这个单元的执行过程将不会出现在监控链路中,适合高频、简单的逻辑以节省资源;
  • recurse: 递归开关。如果函数返回的依然是一个Runnable,是否继续自动执行它;
  • explode_args:参数解包。如果设为True,且输入是一个字典,它会将字典解包为关键字参数传给func;
  • kwargs: 预先填充的参数;

2. 针对Runtime和RunnableConfig的自动注入

如果提供的函数包含RuntimeRunnableConfig类型的参数,并且参数名分别设置为runtimeconfig,调用invoke/aninvoke方发并指定config参数为一个RunnableConfig对象,该对象将绑定为func/afunc的config参数,RunnableConfig中保存的Runtime(对应的Key为"__pregel_runtime",可以通过常量langgraph._internal._constants.CONFIG_KEY_RUNTIME得到它)将绑定为runtime参数。这也是为什么configruntime在很多地方作为保留参数的原因。如下的程序演示了针对这两个核心对象的参数注入。

python 复制代码
from langgraph._internal._runnable import RunnableCallable
from langgraph._internal._constants import  CONFIG_KEY_RUNTIME
from langchain_core.runnables import  RunnableConfig
from langgraph.runtime import Runtime

global_config: RunnableConfig = {
    "configurable": {
        CONFIG_KEY_RUNTIME: Runtime()
    }
}

def test_func(input:dict, config:RunnableConfig, runtime:Runtime)-> dict:
    assert config is global_config
    assert runtime is global_config.get("configurable", {}).get(CONFIG_KEY_RUNTIME)
    return input

runnable = RunnableCallable(func=test_func)
result = runnable.invoke(input ={"foo":"bar"},config=global_config)
assert result == {"foo":"bar"}

3. 针对其他参数的手工注入

除了针对RuntimeRunnableConfig的自动注入,封装函数的其他参数也可以通过在构造函数预填充的参数,或者调用invoke/aninvoke方法指定的关键字参数进行绑定。在如下这个演示实例中,test_func中的stream_writerstore参数就是分别通过这两种方式填充的。当我们调用StateGraph的add_conditional_edges方法添加"条件边",path参数指定的用于解析分支路径的函数中的storestream_writer参数就是采用这种方式注入的。

python 复制代码
from langgraph._internal._runnable import RunnableCallable
from langgraph._internal._constants import  CONFIG_KEY_RUNTIME
from langchain_core.runnables import  RunnableConfig
from langgraph.runtime import Runtime
from langgraph.store.base import BaseStore
from langgraph.types import StreamWriter
from typing import cast

global_config: RunnableConfig = {
    "configurable": {
        CONFIG_KEY_RUNTIME: Runtime()
    }
}

def test_func(input:dict, config:RunnableConfig, stream_writer: StreamWriter, store: BaseStore)-> dict:
    runtime = cast(Runtime, config.get("configurable", {}).get(CONFIG_KEY_RUNTIME))
    assert stream_writer is runtime.stream_writer
    assert store is runtime.store
    return input

runtime = cast(Runtime, global_config.get("configurable", {}).get(CONFIG_KEY_RUNTIME))
runnable = RunnableCallable(func=test_func, stream_writer=runtime.stream_writer) 
result = runnable.invoke(input ={"foo":"bar"},config=global_config,store=runtime.store)
assert result == {"foo":"bar"}

4. 递归执行

在默认情况下,如果指定函数返回一个Runnable,执行RunnableCallable时会以递归的防止执行它。我们可以在创建RunnableCallable时利用recurse参数关闭这一特性。RunnableCallable递归执行Runnable的能力体现在如下的演示程序中。

python 复制代码
from langgraph._internal._runnable import RunnableCallable
from langchain_core.runnables import  Runnable

log:list[str] = []

def foo(input:dict)->Runnable:
    log.append("foo")
    return RunnableCallable(bar)

def bar(input:dict)->Runnable:
    log.append("bar")
    return RunnableCallable(baz)

def baz(input:dict)->dict:
    log.append("baz")
    return input

runnable = RunnableCallable(func=foo) 
result = runnable.invoke(input ={"foo":"bar"})
assert result == {"foo":"bar"}
assert log == ["foo", "bar", "baz"]

log.clear()
runnable = RunnableCallable(func=foo, recurse = False)
result = runnable.invoke(input ={"foo":"bar"})
assert isinstance(result,Runnable)
assert log == ["foo"]

5. 参数拆包

如果调用构造函数时将explode_args参数设置为True,意味着调用invoke/ainvoke时指定的输入参数在传入封装的函数前,会先进行拆包。如下的程序演示了这一点:

python 复制代码
from langgraph._internal._runnable import RunnableCallable

def foobar(data: str, *, foo: str, bar: str) -> dict:
    return {"data":data,"foo": foo, "bar": bar}
input = (("foobar",), {"foo": "123", "bar": "456"})

runnable = RunnableCallable(func=foobar, explode_args=True)
result = runnable.invoke(input)
assert result == {"data": "foobar", "foo": "123", "bar": "456"}

runnable = RunnableCallable(foobar)
try:
    result = runnable.invoke(input)
    assert False, "Expected TypeError due to missing arguments"
except Exception as e:
    assert isinstance(e, TypeError)
    assert str(e) == "foobar() missing 2 required keyword-only arguments: 'foo' and 'bar'"
相关推荐
GinoWi2 小时前
Chapter 3 - Python列表
python
云起SAAS2 小时前
B2B 木材行业供需对接平台微信小程序开源
微信小程序·小程序·ai编程·看广告变现轻·b2b 木材行业供需对接平台
海上日出2 小时前
2026 Multi-Agent 框架终极对比:LangGraph、CrewAI、AutoGen 谁才是真·编排之王?
ai编程
姚生2 小时前
Tushare全解析:金融量化分析的数据基石
大数据·python
qq_364371722 小时前
AI Agent 概念
ai·langchain·agent·langgraph
Hi202402172 小时前
如何从互联网上免费下载歌曲
python·自动化
爱吃的小肥羊2 小时前
刚刚!Google突然宣布:Gemini正式进香港,免魔法使用!
aigc·ai编程
Ferries2 小时前
工作五年前端,终于靠OpenClaw拥有了专属个人网站
前端·ai编程
2401_898075122 小时前
Python在金融科技(FinTech)中的应用
jvm·数据库·python