跳到内容

如何在多代理应用中添加多轮对话

先决条件

本指南假定您熟悉以下内容

在本操作指南中,我们将构建一个应用程序,允许最终用户与一个或多个代理进行多轮对话。我们将创建一个节点,使用 interrupt 来收集用户输入并路由回活跃代理。

代理将作为图中的节点实现,该图执行代理步骤并确定下一步操作

  1. 等待用户输入以继续对话,或
  2. 通过 移交 路由到另一个代理(或返回自身,例如在循环中)。
function human(state: typeof MessagesAnnotation.State): Command {
  const userInput: string = interrupt("Ready for user input.");

  // Determine the active agent
  const activeAgent = ...; 

  return new Command({
    update: {
      messages: [{
        role: "human",
        content: userInput,
      }]
    },
    goto: activeAgent,
  });
}

function agent(state: typeof MessagesAnnotation.State): Command {
  // The condition for routing/halting can be anything, e.g. LLM tool call / structured output, etc.
  const goto = getNextAgent(...); // 'agent' / 'anotherAgent'

  if (goto) {
    return new Command({
      goto,
      update: { myStateKey: "myStateValue" }
    });
  } else {
    return new Command({
      goto: "human"
    });
  }
}

设置

首先,让我们安装所需的软件包 npm install langchain/langgraph langchain/openai langchain/core uuid zod

// 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 = "Time Travel: LangGraphJS";
Time Travel: LangGraphJS

设置 LangSmith 用于 LangGraph 开发

注册 LangSmith 以快速发现问题并提高 LangGraph 项目的性能。LangSmith 允许您使用跟踪数据来调试、测试和监控使用 LangGraph 构建的 LLM 应用程序 — 在这里阅读更多关于如何开始的信息。

旅行推荐示例

在本示例中,我们将构建一个旅行助理代理团队,他们可以通过移交相互通信。

我们将创建 3 个代理

  • travelAdvisor:可以帮助提供一般旅行目的地推荐。可以向 sightseeingAdvisorhotelAdvisor 寻求帮助。
  • sightseeingAdvisor:可以帮助提供观光推荐。可以向 travelAdvisorhotelAdvisor 寻求帮助。
  • hotelAdvisor:可以帮助提供酒店推荐。可以向 sightseeingAdvisorhotelAdvisor 寻求帮助。

这是一个完全连接的网络 - 每个代理都可以与任何其他代理对话。

为了实现代理之间的移交,我们将使用具有结构化输出的 LLM。每个代理的 LLM 将返回一个输出,其中包含其文本响应 (response) 以及要路由到的下一个代理 (goto)。如果代理有足够的信息来响应用户,则 goto 将设置为 human 以路由返回并从人类收集信息。

现在,让我们定义我们的代理节点和图!

import { z } from "zod";
import { ChatOpenAI } from "@langchain/openai";
import { BaseMessage } from "@langchain/core/messages";
import {
  MessagesAnnotation,
  StateGraph,
  START,
  Command,
  interrupt,
  MemorySaver
} from "@langchain/langgraph";

const model = new ChatOpenAI({ model: "gpt-4o" });


/**
 * Call LLM with structured output to get a natural language response as well as a target agent (node) to go to next.
 * @param messages list of messages to pass to the LLM
 * @param targetAgentNodes list of the node names of the target agents to navigate to
 */
function callLlm(messages: BaseMessage[], targetAgentNodes: string[]) {
  // define the schema for the structured output:
  // - model's text response (`response`)
  // - name of the node to go to next (or 'finish')
  const outputSchema = z.object({
    response: z.string().describe("A human readable response to the original question. Does not need to be a final response. Will be streamed back to the user."),
    goto: z.enum(["finish", ...targetAgentNodes]).describe("The next agent to call, or 'finish' if the user's query has been resolved. Must be one of the specified values."),
  })
  return model.withStructuredOutput(outputSchema, { name: "Response" }).invoke(messages)
}

async function travelAdvisor(
  state: typeof MessagesAnnotation.State
): Promise<Command> {
  const systemPrompt = 
      "You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc). " +
      "If you need specific sightseeing recommendations, ask 'sightseeingAdvisor' for help. " +
      "If you need hotel recommendations, ask 'hotelAdvisor' for help. " +
      "If you have enough information to respond to the user, return 'finish'. " +
      "Never mention other agents by name.";

  const messages = [{"role": "system", "content": systemPrompt}, ...state.messages] as BaseMessage[];
  const targetAgentNodes = ["sightseeingAdvisor", "hotelAdvisor"];
  const response = await callLlm(messages, targetAgentNodes);
  const aiMsg = {"role": "ai", "content": response.response, "name": "travelAdvisor"};

  let goto = response.goto;
  if (goto === "finish") {
      goto = "human";
  }

  return new Command({goto, update: { "messages": [aiMsg] } });
}

async function sightseeingAdvisor(
  state: typeof MessagesAnnotation.State
): Promise<Command> {
  const systemPrompt = 
      "You are a travel expert that can provide specific sightseeing recommendations for a given destination. " +
      "If you need general travel help, go to 'travelAdvisor' for help. " +
      "If you need hotel recommendations, go to 'hotelAdvisor' for help. " +
      "If you have enough information to respond to the user, return 'finish'. " +
      "Never mention other agents by name.";

  const messages = [{"role": "system", "content": systemPrompt}, ...state.messages] as BaseMessage[];
  const targetAgentNodes = ["travelAdvisor", "hotelAdvisor"];
  const response = await callLlm(messages, targetAgentNodes);
  const aiMsg = {"role": "ai", "content": response.response, "name": "sightseeingAdvisor"};

  let goto = response.goto;
  if (goto === "finish") {
      goto = "human";
  }

  return new Command({ goto, update: {"messages": [aiMsg] } });
}

async function hotelAdvisor(
  state: typeof MessagesAnnotation.State
): Promise<Command> {
  const systemPrompt = 
      "You are a travel expert that can provide hotel recommendations for a given destination. " +
      "If you need general travel help, ask 'travelAdvisor' for help. " +
      "If you need specific sightseeing recommendations, ask 'sightseeingAdvisor' for help. " +
      "If you have enough information to respond to the user, return 'finish'. " +
      "Never mention other agents by name.";

  const messages = [{"role": "system", "content": systemPrompt}, ...state.messages] as BaseMessage[];
  const targetAgentNodes = ["travelAdvisor", "sightseeingAdvisor"];
  const response = await callLlm(messages, targetAgentNodes);
  const aiMsg = {"role": "ai", "content": response.response, "name": "hotelAdvisor"};

  let goto = response.goto;
  if (goto === "finish") {
      goto = "human";
  }

  return new Command({ goto, update: {"messages": [aiMsg] } });
}

function humanNode(
  state: typeof MessagesAnnotation.State
): Command {
  const userInput: string = interrupt("Ready for user input.");

  let activeAgent: string | undefined = undefined;

  // Look up the active agent
  for (let i = state.messages.length - 1; i >= 0; i--) {
      if (state.messages[i].name) {
          activeAgent = state.messages[i].name;
          break;
      }
  }

  if (!activeAgent) {
      throw new Error("Could not determine the active agent.");
  }

  return new Command({
      goto: activeAgent,
      update: {
        "messages": [
            {
                "role": "human",
                "content": userInput,
            }
        ]
      }
  });
}

const builder = new StateGraph(MessagesAnnotation)
  .addNode("travelAdvisor", travelAdvisor, {
    ends: ["sightseeingAdvisor", "hotelAdvisor"]
  })
  .addNode("sightseeingAdvisor", sightseeingAdvisor, {
    ends: ["human", "travelAdvisor", "hotelAdvisor"]
  })
  .addNode("hotelAdvisor", hotelAdvisor, {
    ends: ["human", "travelAdvisor", "sightseeingAdvisor"]
  })
  // This adds a node to collect human input, which will route
  // back to the active agent.
  .addNode("human", humanNode, {
    ends: ["hotelAdvisor", "sightseeingAdvisor", "travelAdvisor", "human"]
  })
  // We'll always start with a general travel advisor.
  .addEdge(START, "travelAdvisor")

const checkpointer = new MemorySaver()
const graph = builder.compile({ checkpointer })
import * as tslab from "tslab";

const drawableGraph = graph.getGraph();
const image = await drawableGraph.drawMermaidPng();
const arrayBuffer = await image.arrayBuffer();

await tslab.display.png(new Uint8Array(arrayBuffer));

测试多轮对话

让我们使用此应用程序测试多轮对话。

import { Command } from "@langchain/langgraph";
import { v4 as uuidv4 } from "uuid";

const threadConfig = { configurable: { thread_id: uuidv4() }, streamMode: "values" as const };

const inputs = [
  // 1st round of conversation
  {
    messages: [
      { role: "user", content: "i wanna go somewhere warm in the caribbean" }
    ]
  },
  // Since we're using `interrupt`, we'll need to resume using the Command primitive.
  // 2nd round of conversation
  new Command({
    resume: "could you recommend a nice hotel in one of the areas and tell me which area it is."
  }),
  // Third round of conversation
  new Command({ resume: "could you recommend something to do near the hotel?" }),
]

let iter = 0;
for await (const userInput of inputs) {
  iter += 1;
  console.log(`\n--- Conversation Turn ${iter} ---\n`);
  console.log(`User: ${JSON.stringify(userInput)}\n`);

  for await (const update of await graph.stream(userInput, threadConfig)) {
    const lastMessage = update.messages ? update.messages[update.messages.length - 1] : undefined;
    if (lastMessage && lastMessage._getType() === "ai") {
      console.log(`${lastMessage.name}: ${lastMessage.content}`)
    }
  }
}
--- Conversation Turn 1 ---

User: {"messages":[{"role":"user","content":"i wanna go somewhere warm in the caribbean"}]}

travelAdvisor: The Caribbean is a fantastic choice for a warm getaway! Some popular destinations you might consider include Jamaica, the Dominican Republic, and the Bahamas. Each destination offers beautiful beaches, warm weather, and a plethora of activities to enjoy in a tropical setting. Aruba and Barbados are also great choices if you prefer lively beach towns with vibrant nightlife and cultural richness.

Would you like recommendations on sightseeing or places to stay in any of these Caribbean destinations?

--- Conversation Turn 2 ---

User: {"lg_name":"Command","lc_direct_tool_output":true,"resume":"could you recommend a nice hotel in one of the areas and tell me which area it is.","goto":[]}

travelAdvisor: The Caribbean is a fantastic choice for a warm getaway! Some popular destinations you might consider include Jamaica, the Dominican Republic, and the Bahamas. Each destination offers beautiful beaches, warm weather, and a plethora of activities to enjoy in a tropical setting. Aruba and Barbados are also great choices if you prefer lively beach towns with vibrant nightlife and cultural richness.

Would you like recommendations on sightseeing or places to stay in any of these Caribbean destinations?
travelAdvisor: Let's focus on Jamaica, known for its beautiful beaches and vibrant culture, perfect for a warm Caribbean escape. I'll find a nice hotel for you there.
hotelAdvisor: In Jamaica, consider staying at the "Round Hill Hotel and Villas" located in Montego Bay. It's a luxurious resort offering a private beach, beautiful villas, and a spa. Montego Bay is known for its stunning beaches, lively nightlife, and rich history with plantations and cultural sites to explore.

--- Conversation Turn 3 ---

User: {"lg_name":"Command","lc_direct_tool_output":true,"resume":"could you recommend something to do near the hotel?","goto":[]}

hotelAdvisor: In Jamaica, consider staying at the "Round Hill Hotel and Villas" located in Montego Bay. It's a luxurious resort offering a private beach, beautiful villas, and a spa. Montego Bay is known for its stunning beaches, lively nightlife, and rich history with plantations and cultural sites to explore.
hotelAdvisor: Let's find some sightseeing recommendations or activities around Round Hill Hotel and Villas in Montego Bay, Jamaica for you.
sightseeingAdvisor: While staying at the Round Hill Hotel and Villas in Montego Bay, you can explore a variety of activities nearby:

1. **Doctor’s Cave Beach**: One of Montego Bay’s most famous beaches, it’s perfect for swimming and enjoying the sun.

2. **Rose Hall Great House**: Visit this historic plantation house, rumored to be haunted, for a tour of the beautiful grounds and a taste of Jamaican history.

3. **Martha Brae River**: Enjoy rafting on this beautiful river, surrounded by lush Jamaican flora. It's a peaceful and scenic way to experience the natural beauty of the area.

4. **Dunn’s River Falls**: Although a bit farther than the other attractions, these stunning waterfalls in Ocho Rios are worth the visit for a unique climbing experience.

5. **Montego Bay Marine Park**: Explore the coral reefs and marine life through snorkeling or diving adventures.