跳到内容

如何管理代理步骤

在此示例中,我们将构建一个明确管理中间步骤的 ReAct 代理。

之前的示例只是将所有消息放入模型中,但额外的上下文可能会分散代理的注意力,并增加 API 调用的延迟。在此示例中,我们将仅在聊天历史记录中包含最近的 N 条消息。请注意,这旨在说明通用的状态管理。

设置

首先我们需要安装所需的包

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

接下来,我们需要为 Anthropic(我们将使用的 LLM)设置 API 密钥。

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

(可选)我们可以设置 LangSmith 追踪的 API 密钥,这将为我们提供一流的可观察性。

// 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 = "Managing Agent Steps: LangGraphJS";
Managing Agent Steps: LangGraphJS

设置状态

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),
  }),
});

设置工具

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

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

const searchTool = new DynamicStructuredTool({
  name: "search",
  description: "Call to surf the web.",
  schema: z.object({
    query: z.string().describe("The query to use in your search."),
  }),
  func: async ({}: { query: string }) => {
    // This is a placeholder, but don't tell the LLM that...
    return "Try again in a few seconds! Checking with the weathermen... Call be again next.";
  },
});

const tools = [searchTool];

我们现在可以将这些工具包装在一个简单的 ToolNode 中。这是一个简单的类,它接收一个包含 带有 tool_calls 的 AIMessages 的消息列表,运行工具,并将输出作为 ToolMessages 返回。

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

const toolNode = new ToolNode<typeof AgentState.State>(tools);

设置模型

现在我们需要加载我们想使用的聊天模型。这应该满足两个标准:

  1. 它应该能够处理消息,因为我们的状态主要是消息列表(聊天历史)。
  2. 它应该能够进行工具调用,因为我们正在使用预构建的 ToolNode

注意:这些模型要求并非使用 LangGraph 的强制要求——它们仅是此特定示例的要求。

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

const model = new ChatOpenAI({
  model: "gpt-4o",
  temperature: 0,
});
// After we've done this, we should make sure the model knows that it has these tools available to call.
// We can do this by binding the tools to the model class.
const boundModel = model.bindTools(tools);

定义节点

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

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

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

  1. 条件边:在调用代理之后,我们应该:a. 如果代理表示要采取行动,则应该调用用于调用工具的函数\ b. 如果代理表示已完成,则应该结束
  2. 普通边:在工具被调用后,它应该总是返回到代理,由代理决定接下来做什么。

让我们定义节点,以及一个决定采取何种条件边的函数。

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

// 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 || lastMessage.tool_calls.length === 0) {
    return END;
  }
  // Otherwise if there is, we continue
  return "tools";
};

// **MODIFICATION**
//
// Here we don't pass all messages to the model but rather only pass the `N` most recent. Note that this is a terribly simplistic way to handle messages meant as an illustration, and there may be other methods you may want to look into depending on your use case. We also have to make sure we don't truncate the chat history to include the tool message first, as this would cause an API error.
const callModel = async (
  state: typeof AgentState.State,
  config?: RunnableConfig,
) => {
  let modelMessages = [];
  for (let i = state.messages.length - 1; i >= 0; i--) {
    modelMessages.push(state.messages[i]);
    if (modelMessages.length >= 5) {
      if (!ToolMessage.isInstance(modelMessages[modelMessages.length - 1])) {
        break;
      }
    }
  }
  modelMessages.reverse();

  const response = await boundModel.invoke(modelMessages, 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)
  .addNode("agent", callModel)
  .addNode("tools", toolNode)
  .addEdge(START, "agent")
  .addConditionalEdges(
    "agent",
    shouldContinue,
  )
  .addEdge("tools", "agent");

// Finally, we compile it!
// This compiles it into a LangChain Runnable,
// meaning you can use it as you would any other runnable
const app = workflow.compile();

使用它!

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

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

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? Don't give up! Keep using your tools.",
    ),
  ],
};
// Setting the recursionLimit will set a max number of steps. We expect this to endlessly loop :)
try {
  for await (
    const output of await app.stream(inputs, {
      streamMode: "values",
      recursionLimit: 10,
    })
  ) {
    const lastMessage = output.messages[output.messages.length - 1];
    prettyPrint(lastMessage);
    console.log("-----\n");
  }
} catch (e) {
  // Since we are truncating the chat history, the agent never gets the chance
  // to see enough information to know to stop, so it will keep looping until we hit the
  // maximum recursion limit.
  if ((e as GraphRecursionError).name === "GraphRecursionError") {
    console.log("As expected, maximum steps reached. Exiting.");
  } else {
    console.error(e);
  }
}
[human]: what is the weather in sf? Don't give up! Keep using your tools.
-----

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

[tool]: Try again in a few seconds! Checking with the weathermen... Call be again next.
-----

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

[tool]: Try again in a few seconds! Checking with the weathermen... Call be again next.
-----

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

[tool]: Try again in a few seconds! Checking with the weathermen... Call be again next.
-----

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

[tool]: Try again in a few seconds! Checking with the weathermen... Call be again next.
-----

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

As expected, maximum steps reached. Exiting.