如何在多智能体应用中添加多轮对话(函数式 API)¶
在本操作指南中,我们将构建一个应用程序,允许最终用户与一个或多个智能体进行多轮对话。我们将创建一个节点,该节点使用 interrupt
来收集用户输入,并路由回活动智能体。
智能体将作为工作流程中的任务来实现,该工作流程执行智能体步骤并确定下一步操作
- 等待用户输入以继续对话,或者
- 通过 移交 路由到另一个智能体(或返回自身,例如在循环中)。
注意
本指南需要 @langchain/langgraph>=0.2.42
和 @langchain/core>=0.3.36
。
设置¶
首先,安装此示例所需的依赖项
接下来,我们需要为 Anthropic(我们将使用的 LLM)设置 API 密钥
设置 LangSmith 用于 LangGraph 开发
注册 LangSmith 以快速发现问题并提高 LangGraph 项目的性能。LangSmith 允许您使用跟踪数据来调试、测试和监控使用 LangGraph 构建的 LLM 应用程序——阅读此处了解更多关于如何开始的信息
在本示例中,我们将构建一个旅行助手智能体团队,他们可以相互交流。
我们将创建 2 个智能体
travelAdvisor
:可以帮助提供旅行目的地建议。可以向hotelAdvisor
寻求帮助。hotelAdvisor
:可以帮助提供酒店建议。可以向travelAdvisor
寻求帮助。
这是一个完全连接的网络 - 每个智能体都可以与其他任何智能体对话。
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// Tool for getting travel recommendations
const getTravelRecommendations = tool(async () => {
const destinations = ["aruba", "turks and caicos"];
return destinations[Math.floor(Math.random() * destinations.length)];
}, {
name: "getTravelRecommendations",
description: "Get recommendation for travel destinations",
schema: z.object({}),
});
// Tool for getting hotel recommendations
const getHotelRecommendations = tool(async (input: { location: "aruba" | "turks and caicos" }) => {
const recommendations = {
"aruba": [
"The Ritz-Carlton, Aruba (Palm Beach)",
"Bucuti & Tara Beach Resort (Eagle Beach)"
],
"turks and caicos": ["Grace Bay Club", "COMO Parrot Cay"]
};
return recommendations[input.location];
}, {
name: "getHotelRecommendations",
description: "Get hotel recommendations for a given destination.",
schema: z.object({
location: z.enum(["aruba", "turks and caicos"])
}),
});
// Define a tool to signal intent to hand off to a different agent
// Note: this is not using Command(goto) syntax for navigating to different agents:
// `workflow()` below handles the handoffs explicitly
const transferToHotelAdvisor = tool(async () => {
return "Successfully transferred to hotel advisor";
}, {
name: "transferToHotelAdvisor",
description: "Ask hotel advisor agent for help.",
schema: z.object({}),
// Hint to our agent implementation that it should stop
// immediately after invoking this tool
returnDirect: true,
});
const transferToTravelAdvisor = tool(async () => {
return "Successfully transferred to travel advisor";
}, {
name: "transferToTravelAdvisor",
description: "Ask travel advisor agent for help.",
schema: z.object({}),
// Hint to our agent implementation that it should stop
// immediately after invoking this tool
returnDirect: true,
});
转移工具
您可能已经注意到我们在转移工具中使用了 tool(... { returnDirect: true })
。这样做是为了让各个智能体(例如,travelAdvisor
)在调用这些工具后可以提前退出 ReAct 循环,而无需最后一次调用模型来处理工具调用的结果。这是期望的行为,因为我们希望检测到智能体何时调用此工具并立即将控制权移交给不同的智能体。
注意:这旨在与预构建的 createReactAgent
一起使用 - 如果您正在构建自定义智能体,请确保手动添加逻辑来处理标记为 returnDirect
的工具的提前退出。
现在让我们使用预构建的 createReactAgent
和我们的多智能体工作流程来创建我们的智能体。请注意,每次在从每个智能体获得最终响应后,我们都会调用 interrupt
。
import {
AIMessage,
type BaseMessage,
type BaseMessageLike
} from "@langchain/core/messages";
import { ChatAnthropic } from "@langchain/anthropic";
import { createReactAgent } from "@langchain/langgraph/prebuilt";
import {
addMessages,
entrypoint,
task,
MemorySaver,
interrupt,
} from "@langchain/langgraph";
const model = new ChatAnthropic({
model: "claude-3-5-sonnet-latest",
});
const travelAdvisorTools = [
getTravelRecommendations,
transferToHotelAdvisor,
];
// Define travel advisor ReAct agent
const travelAdvisor = createReactAgent({
llm: model,
tools: travelAdvisorTools,
stateModifier: [
"You are a general travel expert that can recommend travel destinations (e.g. countries, cities, etc).",
"If you need hotel recommendations, ask 'hotel_advisor' for help.",
"You MUST include human-readable response before transferring to another agent.",
].join(" "),
});
// You can also add additional logic like changing the input to the agent / output from the agent, etc.
// NOTE: we're invoking the ReAct agent with the full history of messages in the state
const callTravelAdvisor = task("callTravelAdvisor", async (messages: BaseMessageLike[]) => {
const response = await travelAdvisor.invoke({ messages });
return response.messages;
});
const hotelAdvisorTools = [
getHotelRecommendations,
transferToTravelAdvisor,
];
// Define hotel advisor ReAct agent
const hotelAdvisor = createReactAgent({
llm: model,
tools: hotelAdvisorTools,
stateModifier: [
"You are a hotel expert that can provide hotel recommendations for a given destination.",
"If you need help picking travel destinations, ask 'travel_advisor' for help.",
"You MUST include a human-readable response before transferring to another agent."
].join(" "),
});
// Add task for hotel advisor
const callHotelAdvisor = task("callHotelAdvisor", async (messages: BaseMessageLike[]) => {
const response = await hotelAdvisor.invoke({ messages });
return response.messages;
});
const checkpointer = new MemorySaver();
const multiTurnGraph = entrypoint({
name: "multiTurnGraph",
checkpointer,
}, async (messages: BaseMessageLike[]) => {
let callActiveAgent = callTravelAdvisor;
let agentMessages: BaseMessage[];
let currentMessages = messages;
while (true) {
agentMessages = await callActiveAgent(currentMessages);
// Find the last AI message
// If one of the handoff tools is called, the last message returned
// by the agent will be a ToolMessages because we set them to have
// "returnDirect: true". This means that the last AIMessage will
// have tool calls.
// Otherwise, the last returned message will be an AIMessage with
// no tool calls, which means we are ready for new input.
const reversedMessages = [...agentMessages].reverse();
const aiMsgIndex = reversedMessages
.findIndex((m): m is AIMessage => m.getType() === "ai");
const aiMsg: AIMessage = reversedMessages[aiMsgIndex];
// We append all messages up to the last AI message to the current messages.
// This may include ToolMessages (if the handoff tool was called)
const messagesToAdd = reversedMessages.slice(0, aiMsgIndex + 1).reverse();
// Add the agent's responses
currentMessages = addMessages(currentMessages, messagesToAdd);
if (!aiMsg?.tool_calls?.length) {
const userInput = await interrupt("Ready for user input.");
if (typeof userInput !== "string") {
throw new Error("User input must be a string.");
}
if (userInput.toLowerCase() === "done") {
break;
}
currentMessages = addMessages(currentMessages, [{
role: "human",
content: userInput,
}]);
continue;
}
const toolCall = aiMsg.tool_calls.at(-1)!;
if (toolCall.name === "transferToHotelAdvisor") {
callActiveAgent = callHotelAdvisor;
} else if (toolCall.name === "transferToTravelAdvisor") {
callActiveAgent = callTravelAdvisor;
} else {
throw new Error(`Expected transfer tool, got '${toolCall.name}'`);
}
}
return entrypoint.final({
value: agentMessages[agentMessages.length - 1],
save: currentMessages,
});
});
我们使用 while 循环来启用智能体和用户之间的持续对话。循环允许
- 获取智能体响应
- 处理智能体到智能体的转移
- 通过中断收集用户输入
- 使用特殊输入恢复(请参阅下面的
Command
)
测试多轮对话¶
让我们使用此应用程序测试多轮对话。
import { v4 as uuidv4 } from 'uuid';
import { Command } from "@langchain/langgraph";
import { isBaseMessage } from "@langchain/core/messages";
const threadConfig = {
configurable: {
thread_id: uuidv4()
},
streamMode: "updates" as const,
};
const inputs = [
// 1st round of conversation
[{ 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."
}),
// 3rd round of conversation
new Command({
resume: "i like the first one. could you recommend something to do near the hotel?"
})
];
const runConversation = async () => {
for (const [idx, userInput] of inputs.entries()) {
console.log();
console.log(`--- Conversation Turn ${idx + 1} ---`);
console.log();
console.log(`User: ${JSON.stringify(userInput, null, 2)}`);
console.log();
const stream = await multiTurnGraph.stream(
userInput as any,
threadConfig,
);
for await (const update of stream) {
if (update.__metadata__?.cached) {
continue;
}
for (const [nodeId, value] of Object.entries(update)) {
if (Array.isArray(value) && value.length > 0) {
const lastMessage = value.at(-1);
if (isBaseMessage(lastMessage) && lastMessage?.getType() === "ai") {
console.log(`${nodeId}: ${lastMessage.content}`);
}
}
}
}
}
};
// Execute the conversation
try {
await runConversation();
} catch (e) {
console.error(e);
}
--- Conversation Turn 1 ---
User: [
{
"role": "user",
"content": "i wanna go somewhere warm in the caribbean"
}
]
callTravelAdvisor: Based on the recommendations, Turks and Caicos would be an excellent choice for your Caribbean getaway! This British Overseas Territory is known for its stunning white-sand beaches, crystal-clear turquoise waters, and year-round warm weather. Grace Bay Beach in Providenciales is consistently rated as one of the world's best beaches.
You can enjoy:
- World-class snorkeling and diving
- Luxury resorts and spas
- Fresh seafood cuisine
- Water sports like kayaking and paddleboarding
- Beautiful coral reefs
- Average temperatures between 75-85°F (24-29°C) year-round
Would you like me to connect you with our hotel advisor to help you find the perfect place to stay in Turks and Caicos?
--- Conversation Turn 2 ---
User: {
"resume": "could you recommend a nice hotel in one of the areas and tell me which area it is.",
"goto": []
}
callHotelAdvisor: I can recommend two excellent options in Turks and Caicos:
1. Grace Bay Club - This luxury resort is located on the world-famous Grace Bay Beach in Providenciales (often called "Provo"). This area is the most developed and popular island in Turks and Caicos, known for its 12-mile stretch of pristine beach, excellent restaurants, and shopping. The resort offers all-oceanfront suites and is perfect if you want to be close to amenities while enjoying luxury beachfront accommodations.
2. COMO Parrot Cay - This is an exclusive private island resort located on Parrot Cay, a secluded island accessible by boat from Providenciales. This is the ultimate luxury escape if you're looking for privacy and seclusion. The resort is set on 1,000 unspoiled acres with pristine white beaches. This location is perfect for those who want to truly get away from it all while enjoying world-class service and amenities.
Would you like more specific information about either of these properties or their locations?
--- Conversation Turn 3 ---
User: {
"resume": "i like the first one. could you recommend something to do near the hotel?",
"goto": []
}
callHotelAdvisor: Grace Bay Club is perfectly situated to enjoy many activities in Providenciales! Since the hotel is located on Grace Bay Beach in Provo, here are some excellent activities nearby:
1. Beach Activities (right at your doorstep):
- Swimming and sunbathing on Grace Bay Beach
- Snorkeling right off the beach
- Beach walks along the pristine 12-mile stretch
2. Within Walking Distance:
- Salt Mills Plaza (shopping center with local boutiques and restaurants)
- Graceway Gourmet (upscale grocery store)
- Several beachfront restaurants and bars
3. Very Close By (5-10 minute drive):
- Princess Alexandra National Park (great for snorkeling)
- Leeward Marina (for boat tours and fishing trips)
- Provo Golf Club (18-hole championship golf course)
- Thursday Night Fish Fry at Bight Park (local culture and food)
4. Water Activities (operators will pick you up):
- Snorkeling or diving trips to the barrier reef
- Sunset sailing cruises
- Half-day trips to Iguana Island
- Whale watching (in season - January to April)
Would you like me to connect you with our travel advisor for more specific activity recommendations or help with booking any excursions?