在众多针对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'"