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

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

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

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

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

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

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. 针对其他参数的手工注入

除了针对Runtime和RunnableConfig的自动注入,封装函数的其他参数也可以通过在构造函数预填充的参数,或者调用invoke/aninvoke方法指定的关键字参数进行绑定。在如下这个演示实例中,test_func中的stream_writer和store参数就是分别通过这两种方式填充的。当我们调用StateGraph的add_conditional_edges方法添加"条件边",path参数指定的用于解析分支路径的函数中的store和stream_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'"
相关推荐
懂点Tech6 小时前
ctfshow misc28:GIF 高度修改解题与脚本实现
开发语言·windows·python
comedate7 小时前
SMLib 的 Examples 导读:PyAPI / SM_API / SMLib 三组示例怎么读
python·cad·smlib·examples·sm_api
云和数据.ChenGuang7 小时前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
ZhengEnCi7 小时前
如何审核 AI 生成的代码:一份可落地的 6 层检查清单
ai编程·代码规范
川石课堂软件测试7 小时前
学习软件测试需掌握的一些知识
数据库·python·sql·功能测试·网络协议·mysql·http
万化冥合8 小时前
PyBox屏幕键盘如何辅助演示键盘操作?
python·计算机外设
车软学堂8 小时前
从测试转开发,到底要补什么
网络·人工智能·python·功能测试·单片机·车载系统·自动化
天空依然蔚蓝8 小时前
分享一个自动检测网站在线状态的python程序
python·运维开发·1024程序员节
岁月宁静8 小时前
一个 AI 项目从本地到上线,要经过哪些步骤?全流程讲清楚
python·docker·agent
张晓宇 .9 小时前
Codex 不显示新模型怎么办?GPT-6.1 Terra 上线后的排查教程:版本、自定义模型目录与 API 支持
人工智能·gpt·大模型·ai编程·codex·deepseek