Skip to content

如何将运行时的值传递给工具

有时,你希望让调用工具的大语言模型(LLM)填充工具函数参数的*一部分*,并在运行时为其他参数提供值。如果你使用的是 LangChain 风格的工具,一种简单的处理方法是使用 InjectedArg 对函数参数进行注解。这种注解可以使该参数不显示给大语言模型。

在 LangGraph 应用程序中,你可能希望在运行时将图状态或共享内存(存储)传递给工具。当工具的输出受到过去代理步骤的影响时(例如,如果你将子代理用作工具,并希望将消息历史传递给子代理),或者当工具的输入需要根据过去代理步骤的上下文进行验证时,这种有状态的工具就很有用。

在本指南中,我们将演示如何使用 LangGraph 预构建的 ToolNode 来实现这一点。

前提条件

本指南针对 **LangChain 工具调用**,假设你熟悉以下内容:

你仍然可以在 LangGraph 中使用你的提供商软件开发工具包(SDK)进行工具调用,而不会失去 LangGraph 的任何核心功能。

以下示例中的核心技术是将一个参数**注解**为“注入式”,这意味着它将由你的程序注入,大语言模型不应看到或填充该参数。以下代码片段可作为简要总结:

from typing import Annotated

from langchain_core.runnables import RunnableConfig
from langchain_core.tools import InjectedToolArg
from langgraph.store.base import BaseStore

from langgraph.prebuilt import InjectedState, InjectedStore


# 可以是同步或异步的;不需要 @tool 装饰器
async def my_tool(
    # 这些参数由大语言模型填充
    some_arg: str,
    another_arg: float,
    # config: RunnableConfig 在 LangChain 调用中始终可用
    # 这不会暴露给大语言模型
    config: RunnableConfig,
    # 以下三个特定于预构建的 ToolNode
    # (以及扩展的 `create_react_agent`)。如果你在自己的节点中单独调用该工具,那么你需要自己提供这些参数。
    store: Annotated[BaseStore, InjectedStore],
    # 这会传入完整的状态。
    state: Annotated[State, InjectedState],
    # 你也可以从状态中注入单个字段
    messages: Annotated[list, InjectedState("messages")]
    # 以下内容与 create_react_agent 或 ToolNode 不兼容
    # 你也可以排除其他参数,使其不显示给模型。
    # 如果你在自己的节点中调用工具/函数,这些参数必须手动提供,并且很有用
    # some_other_arg=Annotated["MyPrivateClass", InjectedToolArg],
):
    """调用 my_tool 以对现实世界产生影响。

    参数:
        some_arg: 一个非常重要的参数
        another_arg: 大语言模型将提供的另一个参数
    """ # 文档字符串将成为工具的描述,并传递给模型
    print(some_arg, another_arg, config, store, state, messages)
    # 当传递给 bind_tools 或 with_structured_output 时,Config、some_other_rag、store 和 state 对 LangChain 模型都是“隐藏”的
    return "... 一些响应"

API Reference: RunnableConfig | InjectedToolArg


安装设置

首先,我们需要安装所需的软件包。

%%capture --no-stderr
%pip install --quiet -U langgraph langchain-openai

接下来,我们需要为 OpenAI(我们将使用的聊天模型)设置 API 密钥。

import getpass
import os


def _set_env(var: str):
    if not os.environ.get(var):
        os.environ[var] = getpass.getpass(f"{var}: ")


_set_env("OPENAI_API_KEY")
OPENAI_API_KEY:  ········

为 LangGraph 开发设置 LangSmith

注册 LangSmith 以快速发现问题并提升你的 LangGraph 项目的性能。LangSmith 允许你使用跟踪数据来调试、测试和监控使用 LangGraph 构建的大语言模型应用程序 — 点击 此处 了解更多关于如何开始使用的信息。

将图状态传递给工具

让我们首先看看如何让我们的工具能够访问图状态。我们需要定义我们的图状态:

from typing import List

# this is the state schema used by the prebuilt create_react_agent we'll be using below
from langgraph.prebuilt.chat_agent_executor import AgentState
from langchain_core.documents import Document


class State(AgentState):
    docs: List[str]

API Reference: Document

定义工具

我们希望我们的工具将图状态作为输入,但我们不希望模型在调用工具时尝试生成此输入。我们可以使用 InjectedState 注解将参数标记为所需的图状态(或图状态的某个字段)。这些参数不会由模型生成。使用 ToolNode 时,图状态将自动传递给相关的工具和参数。

在这个示例中,我们将创建一个返回文档的工具,然后再创建一个工具,该工具实际引用支持某个主张的文档。

在 LangChain 中使用 Pydantic

本笔记本使用 Pydantic v2 的 BaseModel,这需要 langchain-core >= 0.3。使用 langchain-core < 0.3 会因混用 Pydantic v1 和 v2 的 BaseModel 而导致错误。

from typing import List, Tuple
from typing_extensions import Annotated

from langchain_core.messages import ToolMessage
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState


@tool
def get_context(question: str, state: Annotated[dict, InjectedState]):
    """Get relevant context for answering the question."""
    return "\n\n".join(doc for doc in state["docs"])

API Reference: ToolMessage | tool

如果我们查看这些工具的输入模式,会发现 state 仍然被列出:

get_context.get_input_schema().schema()
{'description': 'Get relevant context for answering the question.',
 'properties': {'question': {'title': 'Question', 'type': 'string'},
  'state': {'title': 'State', 'type': 'object'}},
 'required': ['question', 'state'],
 'title': 'get_context',
 'type': 'object'}

但如果我们查看工具调用模式(即传递给模型用于工具调用的内容),会发现 state 已被移除:

get_context.tool_call_schema.schema()
{'description': 'Get relevant context for answering the question.',
 'properties': {'question': {'title': 'Question', 'type': 'string'}},
 'required': ['question'],
 'title': 'get_context',
 'type': 'object'}

定义图

在这个示例中,我们将使用一个预构建的 ReAct 代理。我们首先需要定义我们的模型和一个工具调用节点(ToolNode):

from langchain_openai import ChatOpenAI
from langgraph.prebuilt import ToolNode, create_react_agent
from langgraph.checkpoint.memory import MemorySaver

model = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [get_context]

# ToolNode will automatically take care of injecting state into tools
tool_node = ToolNode(tools)

checkpointer = MemorySaver()
graph = create_react_agent(model, tools, state_schema=State, checkpointer=checkpointer)

使用它!

docs = [
    "FooBar company just raised 1 Billion dollars!",
    "FooBar company was founded in 2019",
]

inputs = {
    "messages": [{"type": "user", "content": "what's the latest news about FooBar"}],
    "docs": docs,
}
config = {"configurable": {"thread_id": "1"}}
for chunk in graph.stream(inputs, config, stream_mode="values"):
    chunk["messages"][-1].pretty_print()
================================ Human Message =================================

what's the latest news about FooBar
================================== Ai Message ==================================
Tool Calls:
  get_context (call_UkqfR7z2cLJQjhatUpDeEa5H)
 Call ID: call_UkqfR7z2cLJQjhatUpDeEa5H
  Args:
    question: latest news about FooBar
================================= Tool Message =================================
Name: get_context

FooBar company just raised 1 Billion dollars!

FooBar company was founded in 2019
================================== Ai Message ==================================

The latest news about FooBar is that the company has just raised 1 billion dollars.

将共享内存(存储)传递给图

你可能还希望让工具能够访问在多个对话或用户之间共享的内存。我们可以通过使用不同的注解 InjectedStore,将 LangGraph 存储 传递给工具来实现这一点。

让我们修改我们的示例,将文档保存在内存存储中,并使用 get_context 工具检索它们。我们还将根据用户 ID 使文档可访问,这样某些文档就只能被特定用户看到。然后,该工具将使用 配置 中提供的 user_id 来检索正确的文档集。

注意

  • LangGraph v0.2.34 版本开始支持本笔记本中使用的 Store API 和 InjectedStore
  • InjectedStore 注解需要 langchain-core >= 0.3.8
  • from langgraph.store.memory import InMemoryStore
    
    doc_store = InMemoryStore()
    
    namespace = ("documents", "1")  # user ID
    doc_store.put(
        namespace, "doc_0", {"doc": "FooBar company just raised 1 Billion dollars!"}
    )
    namespace = ("documents", "2")  # user ID
    doc_store.put(namespace, "doc_1", {"doc": "FooBar company was founded in 2019"})
    

    定义工具

    from langgraph.store.base import BaseStore
    from langchain_core.runnables import RunnableConfig
    from langgraph.prebuilt import InjectedStore
    
    
    @tool
    def get_context(
        question: str,
        config: RunnableConfig,
        store: Annotated[BaseStore, InjectedStore()],
    ) -> Tuple[str, List[Document]]:
        """Get relevant context for answering the question."""
        user_id = config.get("configurable", {}).get("user_id")
        docs = [item.value["doc"] for item in store.search(("documents", user_id))]
        return "\n\n".join(doc for doc in docs)
    

    API Reference: RunnableConfig

    我们还可以验证工具调用模型是否会忽略 get_context 工具的 store 参数:

    get_context.tool_call_schema.schema()
    
    {'description': 'Get relevant context for answering the question.',
     'properties': {'question': {'title': 'Question', 'type': 'string'}},
     'required': ['question'],
     'title': 'get_context',
     'type': 'object'}
    

    定义图

    让我们更新我们的 ReAct 智能体:

    tools = [get_context]
    
    # ToolNode will automatically take care of injecting Store into tools
    tool_node = ToolNode(tools)
    
    checkpointer = MemorySaver()
    # NOTE: we need to pass our store to `create_react_agent` to make sure our graph is aware of it
    graph = create_react_agent(model, tools, checkpointer=checkpointer, store=doc_store)
    

    使用它!

    让我们尝试在配置中使用 "user_id" 来运行我们的图。

    messages = [{"type": "user", "content": "what's the latest news about FooBar"}]
    config = {"configurable": {"thread_id": "1", "user_id": "1"}}
    for chunk in graph.stream({"messages": messages}, config, stream_mode="values"):
        chunk["messages"][-1].pretty_print()
    
    ================================ Human Message =================================
    
    what's the latest news about FooBar
    ================================== Ai Message ==================================
    Tool Calls:
      get_context (call_ocyHBpGgF3LPFOgRKURBfkGG)
     Call ID: call_ocyHBpGgF3LPFOgRKURBfkGG
      Args:
        question: latest news about FooBar
    ================================= Tool Message =================================
    Name: get_context
    
    FooBar company just raised 1 Billion dollars!
    ================================== Ai Message ==================================
    
    The latest news about FooBar is that the company has just raised 1 billion dollars.
    
    我们可以看到,该工具在存储中查找信息时,仅为用户“1”检索到了正确的文档。现在,让我们为另一个用户再次尝试一下:

    messages = [{"type": "user", "content": "what's the latest news about FooBar"}]
    config = {"configurable": {"thread_id": "2", "user_id": "2"}}
    for chunk in graph.stream({"messages": messages}, config, stream_mode="values"):
        chunk["messages"][-1].pretty_print()
    
    ================================ Human Message =================================
    
    what's the latest news about FooBar
    ================================== Ai Message ==================================
    Tool Calls:
      get_context (call_zxO9KVlL8UxFQUMb8ETeHNvs)
     Call ID: call_zxO9KVlL8UxFQUMb8ETeHNvs
      Args:
        question: latest news about FooBar
    ================================= Tool Message =================================
    Name: get_context
    
    FooBar company was founded in 2019
    ================================== Ai Message ==================================
    
    FooBar company was founded in 2019. If you need more specific or recent news, please let me know!
    
    我们可以看到,这次该工具引入了一份不同的文档。

    Comments