跳到内容

如何让 Agent 直接返回工具结果

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

在本示例中,我们将构建一个对话式 ReAct Agent,其中 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. 它应该与消息一起使用。 我们将以消息的形式表示所有 Agent 状态,因此它需要能够很好地与它们一起工作。
  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);

定义 Agent 状态

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. Agent:负责决定要采取什么(如果有)操作。
  2. 调用工具的函数:如果 Agent 决定采取操作,则此节点将执行该操作。

我们还需要定义一些边。 其中一些边可能是条件性的。 它们是条件性的原因是,基于节点的输出,可能会采取几种路径之一。 采取的路径在运行该节点(LLM 决定)之前是未知的。

  1. 条件边:在调用 Agent 后,我们应该:a. 如果 Agent 说要采取行动,则应调用调用工具的函数 b. 如果 Agent 说它已完成,则应完成
  2. 正常边:在调用工具后,它应始终返回到 Agent 以决定下一步做什么

让我们定义节点,以及一个函数来决定如何采取什么条件边。

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