跳到内容

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

一个典型的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 schema,以替代工具自动推断的schema。

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 中,节点可以是函数或可运行对象。为此我们需要两个主要的节点:

  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 可运行对象相同的接口

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节点后停止了!