如何在多智能体应用中添加多轮对话¶
在本操作指南中,我们将构建一个应用程序,允许终端用户与一个或多个智能体进行多轮对话。我们将创建一个使用 interrupt
来收集用户输入并路由回活跃智能体的节点。
智能体将作为图中的节点实现,执行智能体步骤并确定下一步行动
- 等待用户输入以继续对话,或
- 通过 移交 路由到另一个智能体(或路由回自身,例如在循环中)。
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";
设置 LangSmith 进行 LangGraph 开发
注册 LangSmith 以快速发现问题并提高 LangGraph 项目的性能。LangSmith 允许您使用跟踪数据来调试、测试和监控您使用 LangGraph 构建的 LLM 应用——在此处了解更多入门信息 这里。
旅行推荐示例¶
在此示例中,我们将构建一个旅行助手智能体团队,它们可以通过移交相互通信。
我们将创建 3 个智能体
travelAdvisor
:可以提供一般旅行目的地推荐。可以向sightseeingAdvisor
和hotelAdvisor
求助。sightseeingAdvisor
:可以提供观光推荐。可以向travelAdvisor
和hotelAdvisor
求助。hotelAdvisor
:可以提供酒店推荐。可以向sightseeingAdvisor
和hotelAdvisor
求助。
这是一个全连接网络——每个智能体都可以与其他任何智能体对话。
为了实现智能体之间的移交,我们将使用具有结构化输出的 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.