跳到内容

如何让代理直接返回工具结果

一个典型的 ReAct 循环遵循 用户 -> 助手 -> 工具 -> 助手 ... -> 用户。在某些情况下,工具完成后您无需再次调用 LLM,用户可以直接自行查看结果。

在此示例中,我们将构建一个对话式 ReAct 代理,其中 LLM 可以选择决定将工具调用的结果作为最终答案返回。这在某些情况下非常有用,例如您有一些工具,它们有时可以生成可接受的最终答案,并且您希望使用 LLM 来确定何时应该如此。

设置

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

yarn add @langchain/langgraph @langchain/openai @langchain/core

接下来,我们需要为 OpenAI(我们将使用的 LLM)设置 API 密钥。您可以选择设置 LangSmith 跟踪 的 API 密钥,这将提供一流的可观察性。

// process.env.OPENAI_API_KEY = "sk_...";

// Optional, add tracing in LangSmith
// process.env.LANGCHAIN_API_KEY = "ls__..."
process.env.LANGCHAIN_CALLBACKS_BACKGROUND = "true";
process.env.LANGCHAIN_TRACING_V2 = "true";
process.env.LANGCHAIN_PROJECT = "Direct Return: LangGraphJS";
Direct Return: LangGraphJS

设置工具

我们首先定义要使用的工具。对于这个简单的示例,我们将使用一个简单的占位符“搜索引擎”。但是,创建您自己的工具非常容易 - 请参阅 此处 关于如何操作的文档。

为了添加 'return_direct' 选项,我们将创建一个自定义的 zod 模式,以替代工具自动推断的模式。

import { DynamicStructuredTool } from "@langchain/core/tools";
import { z } from "zod";

const SearchTool = z.object({
  query: z.string().describe("query to look up online"),
  // **IMPORTANT** We are adding an **extra** field here
  // that isn't used directly by the tool - it's used by our
  // graph instead to determine whether or not to return the
  // result directly to the user
  return_direct: z.boolean()
    .describe(
      "Whether or not the result of this should be returned directly to the user without you seeing what it is",
    )
    .default(false),
});

const searchTool = new DynamicStructuredTool({
  name: "search",
  description: "Call to surf the web.",
  // We are overriding the default schema here to
  // add an extra field
  schema: SearchTool,
  func: async ({}: { query: string }) => {
    // This is a placeholder for the actual implementation
    // Don't let the LLM know this though 😊
    return "It's sunny in San Francisco, but you better look out if you're a Gemini 😈.";
  },
});

const tools = [searchTool];

现在我们可以将这些工具封装在 ToolNode 中。这是一个预构建的节点,它接收 LangChain 聊天模型生成的工具调用并调用该工具,然后返回输出。

import { ToolNode } from "@langchain/langgraph/prebuilt";

const toolNode = new ToolNode(tools);

设置模型

现在我们需要加载要使用的聊天模型。重要的是,这应该满足两个条件:

  1. 它应该能处理消息。我们将所有代理状态表示为消息形式,因此它需要能很好地处理它们。
  2. 它应该支持 工具调用

注意:这些模型要求并非使用 LangGraph 的必要条件 - 它们仅是此示例的要求。

import { ChatOpenAI } from "@langchain/openai";

const model = new ChatOpenAI({
  temperature: 0,
  model: "gpt-3.5-turbo",
});
// This formats the tools as json schema for the model API.
// The model then uses this like a system prompt.
const boundModel = model.bindTools(tools);

定义代理状态

langgraph 中主要的图类型是 StateGraph

这个图通过一个状态对象进行参数化,该对象在每个节点之间传递。然后每个节点返回更新状态的操作。这些操作可以是 SET 状态上的特定属性(例如覆盖现有值)或 ADD 到现有属性。设置或添加的决定由构建图时使用的状态对象指定。

对于此示例,我们将跟踪的状态仅是消息列表。我们希望每个节点只向该列表添加消息。因此,我们将状态定义如下:

import { Annotation } from "@langchain/langgraph";
import { BaseMessage } from "@langchain/core/messages";

const AgentState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
  }),
});

定义节点

现在我们需要在图中定义几个不同的节点。在 langgraph 中,节点可以是一个函数或一个 runnable。为此,我们需要两个主要节点:

  1. 代理:负责决定采取哪些行动(如果需要)。
  2. 调用工具的函数:如果代理决定采取行动,该节点将执行该行动。

我们还需要定义一些边。其中一些边可能是条件性的。它们之所以是条件性的,是因为根据节点的输出,可能会选择几条路径中的一条。直到该节点运行(LLM 做出决定)后,才知道将选择哪条路径。

  1. 条件边:在调用代理后,我们应该:a. 如果代理表示要采取行动,则应调用调用工具的函数;b. 如果代理表示已完成,则应终止。
  2. 普通边:调用工具后,应始终返回代理以决定下一步操作。

让我们定义节点,以及一个决定如何选择条件边的函数。

import { RunnableConfig } from "@langchain/core/runnables";
import { END } from "@langchain/langgraph";
import { AIMessage } from "@langchain/core/messages";

// Define the function that determines whether to continue or not
const shouldContinue = (state: typeof AgentState.State) => {
  const { messages } = state;
  const lastMessage = messages[messages.length - 1] as AIMessage;
  // If there is no function call, then we finish
  if (!lastMessage?.tool_calls?.length) {
    return END;
  } // Otherwise if there is, we check if it's suppose to return direct
  else {
    const args = lastMessage.tool_calls[0].args;
    if (args?.return_direct) {
      return "final";
    } else {
      return "tools";
    }
  }
};

// Define the function that calls the model
const callModel = async (state: typeof AgentState.State, config?: RunnableConfig) => {
  const messages = state.messages;
  const response = await boundModel.invoke(messages, config);
  // We return an object, because this will get added to the existing list
  return { messages: [response] };
};

定义图

现在我们可以将所有内容组合起来并定义图了!

import { START, StateGraph } from "@langchain/langgraph";

// Define a new graph
const workflow = new StateGraph(AgentState)
  // Define the two nodes we will cycle between
  .addNode("agent", callModel)
  // Note the "action" and "final" nodes are identical!
  .addNode("tools", toolNode)
  .addNode("final", toolNode)
  // Set the entrypoint as `agent`
  .addEdge(START, "agent")
  // We now add a conditional edge
  .addConditionalEdges(
    // First, we define the start node. We use `agent`.
    "agent",
    // Next, we pass in the function that will determine which node is called next.
    shouldContinue,
  )
  // We now add a normal edge from `tools` to `agent`.
  .addEdge("tools", "agent")
  .addEdge("final", END);

// Finally, we compile it!
const app = workflow.compile();

使用它!

现在我们可以使用它了!这暴露了与所有其他 LangChain runnable 相同的接口

import { HumanMessage, isAIMessage } from "@langchain/core/messages";

const prettyPrint = (message: BaseMessage) => {
  let txt = `[${message._getType()}]: ${message.content}`;
  if (
    isAIMessage(message) && (message as AIMessage)?.tool_calls?.length || 0 > 0
  ) {
    const tool_calls = (message as AIMessage)?.tool_calls
      ?.map((tc) => `- ${tc.name}(${JSON.stringify(tc.args)})`)
      .join("\n");
    txt += ` \nTools: \n${tool_calls}`;
  }
  console.log(txt);
};

const inputs = { messages: [new HumanMessage("what is the weather in sf")] };
for await (const output of await app.stream(inputs, { streamMode: "values" })) {
  const lastMessage = output.messages[output.messages.length - 1];
  prettyPrint(lastMessage);
  console.log("-----\n");
}
[human]: what is the weather in sf
-----

[ai]:  
Tools: 
- search({"query":"weather in San Francisco"})
-----

[tool]: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.
-----

[ai]: The weather in San Francisco is sunny.
-----

const inputs2 = {
  messages: [
    new HumanMessage(
      "what is the weather in sf? return this result directly by setting return_direct = True",
    ),
  ],
};
for await (
  const output of await app.stream(inputs2, { streamMode: "values" })
) {
  const lastMessage = output.messages[output.messages.length - 1];
  prettyPrint(lastMessage);
  console.log("-----\n");
}
[human]: what is the weather in sf? return this result directly by setting return_direct = True
-----

[ai]:  
Tools: 
- search({"query":"weather in San Francisco","return_direct":true})
-----

[tool]: It's sunny in San Francisco, but you better look out if you're a Gemini 😈.
-----
完成!图在运行 tools 节点后停止了!