为了能够在LangGraph中更有效的利用ToolNode,我们对它的定义做一个详细的介绍。ToolNode继承了RunnableCallable,其name字段表示节点名称,上面我们将它称为tools节点就是因为它的默认值为tools。
1. ToolNode的构建
根据如下所示的针对__init__方法的定义,可用看出创建ToolNode时除了需要提供注册的工具序列之外,还可以初始化其他一些字段成员用于对工具的执行做相应的控制。
class ToolNode(RunnableCallable):
name: str = "tools"
def __init__(
self,
tools: Sequence[BaseTool | Callable],
*,
name: str = "tools",
tags: list[str] | None = None,
handle_tool_errors: bool
| str
| Callable[..., str]
| type[Exception]
| tuple[type[Exception], ...] = _default_handle_tool_errors,
messages_key: str = "messages",
wrap_tool_call: ToolCallWrapper | None = None,
awrap_tool_call: AsyncToolCallWrapper | None = None,
) -> None:
super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False)
self._tools_by_name: dict[str, BaseTool] = {}
self._injected_args: dict[str, _InjectedArgs] = {}
self._handle_tool_errors = handle_tool_errors
self._messages_key = messages_key
self._wrap_tool_call = wrap_tool_call
self._awrap_tool_call = awrap_tool_call
for tool in tools:
if not isinstance(tool, BaseTool):
tool_ = create_tool(cast("type[BaseTool]", tool))
else:
tool_ = tool
self._tools_by_name[tool_.name] = tool_
self._injected_args[tool_.name] = _get_all_injected_args(tool_)
ToolNode的__init__方法具有如下参数:
- tools:注册的工具,如果执行的是一个
Callable对象,将会转换成一个BaseTool对象,通常是一个StucturedTool对象。关于BaseTool以及从Callable向BaseTool的转换规则在“赋予Agent执行力的工具是个什么东西?”一文中具有详细介绍。所有表示工具的BaseTool对象会保存在_tools_by_name字段对应的字典中,Key为工具的名称; - name:当前节点的名称,默认为
tools; - handle_tool_errors:用于指定异常处理的策略或者异常处理器;
- messages_key:由于
ToolNode的执行都是围绕着代表对话历史的消息列表进行的,所以它能工作的前提是具有如此一个表示消息历史的状态成员。该参数表示此状态成员的名称,默认值为messages; - wrap_tool_call/awrap_tool_call:包装工具调用(同步和异步)的包装器,利用它可以实现针对工具调用的动态拦截;
__init__方法处理将指定的参数赋值给对应的字段外,还完成了注入参数的提取,具体体现在针对_get_all_injected_args方法的调用上。该方法返回一个_InjectedArgs对象,用来表示工具函数中是否具有针对状态、表示长期存储的BaseStore和表示工具运行时的ToolRuntime的参数注入。因为ToolCall提供的参数列表不会包含这些自动注入的参数,但是在调用工具函数时却必须指定。我的文章“如何在工具函数中注入各种运行时对象?”具有对这个问题的详细介绍。
def _get_all_injected_args(tool: BaseTool) -> _InjectedArgs:
@dataclass
class _InjectedArgs:
state: dict[str, str | None]
store: str | None
runtime: str | None
2. 工具调用的包装
我们知道表示中间件的AgentMiddleware定义了wrap_tool_call和awrap_tool_call方法用来工具调用的包装,我们会发现着两个方法的签名和ToolNode的wrap_tool_call/awrap_tool_call参数的声明的可执行对象具有一致的签名。当create_agent方法在创建ToolNode的时候,会提取所有注册的中间件,并创建两个Callable对象来表示体现针对它们的wrap_tool_call和awrap_tool_call方法的按序调用,这两个Callable对象会作为调用__init__方法的wrap_tool_call/awrap_tool_call参数。
ToolCallWrapper = Callable[
[ToolCallRequest, Callable[[ToolCallRequest], ToolMessage | Command]],
ToolMessage | Command,
]
AsyncToolCallWrapper = Callable[
[ToolCallRequest, Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]]],
Awaitable[ToolMessage | Command],
]
class AgentMiddleware(Generic[StateT, ContextT]):
def wrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]
async def awrap_tool_call(
self,
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command[Any]]],
) -> ToolMessage | Command[Any]
如果ToolNode在LangGraph中使用,意味着我们得手工创建它。如果具有针对工具调用进行包装的需求,可以显式提供这两个参数。在如下的演示程序中,我们根据定义的工具get_weather创建了一个ToolNode,针对同步调用的包装器被设置成log函数,我们利用它输出工具调用得输入和输出。
from typing import Callable, Any
from langchain_core.tools import tool
from langgraph.types import Command
from langchain_core.messages import AIMessage,ToolCall,ToolMessage
from langgraph.prebuilt import ToolNode
from langchain.agents.middleware import ToolCallRequest
from langchain_core.runnables import RunnableConfig
from langgraph.runtime import Runtime
from langgraph._internal._constants import CONFIG_KEY_RUNTIME
@tool
def get_weather(city: str) -> str:
"""A tool to get weather information for a given city."""
return f"It's sunny today in {city}."
def log(
request: ToolCallRequest,
handler: Callable[[ToolCallRequest], ToolMessage | Command[Any]],
) -> ToolMessage | Command[Any]:
print(f"Tool call '{request.tool_call['name']}' started with args: {request.tool_call['args']}")
result = handler(request)
print(f"Tool call '{request.tool_call['name']}' finished with result: {result}")
return result
tool_node = ToolNode(tools=[get_weather], wrap_tool_call=log)
tool_call = ToolCall(name="get_weather", args={"city": "Suzhou"}, id="tool_call_001")
messages = [AIMessage(content="", tool_calls=[tool_call])]
config ={
"configurable": {
CONFIG_KEY_RUNTIME: Runtime(),
}
}
tool_message:ToolMessage = tool_node.invoke(messages, config=config)[0]
assert tool_message.name == "get_weather"
assert tool_message.content == "It's sunny today in Suzhou."
assert tool_message.tool_call_id == "tool_call_001"
我们提供一个AIMessage列表作为调用ToolNode的输入,此AIMessage中携带一个用于描述工具调用ToolCall对象。作为参数的RunnableConfig中添加了一个必需的Runtime对象。调用ToolNode返回一个包含一个ToolMessage对象的列表,此ToolMessage正是对get_weather工具函数返回值的封装。包装器log输出如下所示:
Tool call 'get_weather' started with args: {'city': 'Suzhou'}
Tool call 'get_weather' finished with result: content="It's sunny today in Suzhou." name='get_weather' tool_call_id='tool_call_001'
3. 异常处理
ToolNode``__init__方法的handle_tool_errors参数决定了当工具执行抛出异常时,ToolNode如何捕获并将其转化为发送回LLM的消息,而不是让整个程序直接崩溃。它具有如下几种定义方法:
- 布尔值:
- True: 捕获所有异常,并将错误堆栈信息作为字符串返回作为
ToolMessage的内容返回给LLM; - False (默认): 不捕获异常。如果工具报错,程序直接抛出异常并停止运行;
- True: 捕获所有异常,并将错误堆栈信息作为字符串返回作为
- 字符串:无论发生什么错误,都返回这段固定的字符串给LLLM,可用隐藏具体报错细节(出于安全考虑),给模型一个通用的重试建议;
- 异常类型:定向捕获指定的异常。如果抛出的是指定异常,则将其转化为消息返回;如果抛出的是其他异常,程序依然会崩溃;
- 自定义函数 (
Callable[..., str]):异常处理函数。
该参数默认指定如下一个异常处理函数,意味着在默认情况下只处理ToolInvocationError。
def _default_handle_tool_errors(e: Exception) -> str:
if isinstance(e, ToolInvocationError):
return e.message
raise e
如下的程序演示了几种异常异常处理方式,包括将参数handle_tool_errors指定为True、错误消息和异常处理函数。
from typing import Callable
from langchain_core.tools import tool
from langgraph.types import Command
from langchain_core.messages import AIMessage,ToolCall
from langgraph.prebuilt import ToolNode
from langchain_core.runnables import RunnableConfig
from langgraph.runtime import Runtime
from langgraph._internal._constants import CONFIG_KEY_RUNTIME
@tool
def fake_tool():
"""A tool that should not be called."""
raise ValueError("This tool should not be called.")
tool_call = ToolCall(name="fake_tool", args={}, id="tool_call_001")
messages = [AIMessage(content="", tool_calls=[tool_call])]
config ={
"configurable": {
CONFIG_KEY_RUNTIME: Runtime(),
}
}
def invoke(handle_tool_errors: bool
| str
| Callable[..., str]
| type[Exception]
| tuple[type[Exception], ...]) -> str:
tool_node = ToolNode(tools=[fake_tool], handle_tool_errors=handle_tool_errors)
print(tool_node.invoke(messages, config=config)[0].content)
print("=== handle_tool_errors=True ===")
invoke(handle_tool_errors=True)
print("\n=== handle_tool_errors=custom message ===")
invoke(handle_tool_errors="Error happened while calling tool.")
print("\n=== handle_tool_errors=handler function ===")
invoke(handle_tool_errors=lambda exc: f"Error happened while calling tool: {str(exc)}")
输出:
Error: ValueError('This tool should not be called.')
Please fix your mistakes.
=== handle_tool_errors=custom message ===
Error happened while calling tool.
=== handle_tool_errors=handler function ===
Error happened while calling tool: This tool should not be called.
478



被折叠的 条评论
为什么被折叠?



