[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'"
相关推荐
程序员黑豆4 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
小虎AI生活6 小时前
WorkBuddy 资料库架构解析:MD + CSV + HTML 三种载体的"流水线"设计
ai编程
2603_965148116 小时前
如何解析JSON数据?API返回的商品信息处理教程
开发语言·数据库·python·自动化·json·api
咖啡星人k7 小时前
想私有化部署 AI 开发平台?MonkeyCode 给出的答案是开源 + 离线
人工智能·大模型·ai编程·monkeycode
jufeng13077 小时前
【系列:手搓自主 AI Agent:Hermes 架构原理剖析 · 第 8 篇】
python·ai agent·配置系统
circuitsosk7 小时前
跨境电商智能化实战:AI如何赋能客服自动回复、广告智能投放与供应链预测
大数据·人工智能·python·langchain·智能客服
2601_956319888 小时前
2026年零基础学量化:从看懂示例到写清条件和动作
人工智能·python
过期的秋刀鱼!9 小时前
LangChain-D1-模型的工作流程
人工智能·python·langchain
萤火工厂目视化设计9 小时前
智能制造与企业文化目视化浪潮下:中小工厂的机遇与挑战
python·制造
青山是哪个青山9 小时前
LangChain 学习笔记(三):LangSmith 调试与监控
笔记·学习·langchain