index.js
c
/**
* LangGraph Multi-Agent Demo (最终修复版 - 解决 undefined 返回和工具识别问题)
*/
require("dotenv").config();
const { ChatOpenAI } = require("@langchain/openai");
const { StateGraph, START, END } = require("@langchain/langgraph");
const { tool } = require("@langchain/core/tools");
const { z } = require("zod");
const { MemorySaver } = require("@langchain/langgraph");
const {
HumanMessage,
AIMessage,
SystemMessage,
ToolMessage,
} = require("@langchain/core/messages");
// ================= 0. 获取当前时间 =================
const now = new Date();
const currentDateStr = now.toISOString().split("T")[0]; // 格式: 2026-03-19
const currentWeekDay = ["日", "一", "二", "三", "四", "五", "六"][now.getDay()];
const todayInfo = `今天是 ${currentDateStr} (星期${currentWeekDay})。当用户说"明天"时,指 ${new Date(now.getTime() + 86400000).toISOString().split("T")[0]}。`;
console.log(`系统时间已同步: ${todayInfo}`);
// ================= 1. 配置检查 =================
if (!process.env.OPENAI_API_KEY) {
console.error("错误: 未找到 OPENAI_API_KEY。请检查 .env 文件。");
process.exit(1);
}
console.log("API Key 已加载");
console.log("使用基站: https://api.acto.cn/v1");
const llm = new ChatOpenAI({
modelName: "gpt-4o-mini",
temperature: 0.7,
configuration: {
baseURL: "https://api.acto.cn/v1",
},
});
// ================= 2. 定义工具 =================
const searchFlightsTool = tool(
async ({ from, to, date }) => {
console.log(`\n[系统] 正在查询航班: ${from} -> ${to} (${date})...`);
await new Promise((r) => setTimeout(r, 800));
return `找到以下航班:\n1. CA123: ${from} -> ${to}, 08:00起飞, ¥2800\n2. MU456: ${from} -> ${to}, 14:00起飞, ¥2200`;
},
{
name: "search_flights",
description: "Search for flights between two cities.",
schema: z.object({
from: z.string().describe("Departure city"),
to: z.string().describe("Destination city"),
date: z.string().describe("Travel date"),
}),
},
);
const searchHotelsTool = tool(
async ({ city, checkIn, checkOut }) => {
console.log(
`\n[系统] 正在查询酒店: ${city} (${checkIn} 到 ${checkOut})...`,
);
await new Promise((r) => setTimeout(r, 800));
return `找到以下酒店:\n1. ${city}中心大酒店: ¥800/晚, 评分 4.8\n2. ${city}河景民宿: ¥450/晚, 评分 4.5`;
},
{
name: "search_hotels",
description: "Search for hotels in a specific city.",
schema: z.object({
city: z.string().describe("City name"),
checkIn: z.string().describe("Check-in date"),
checkOut: z.string().describe("Check-out date"),
}),
},
);
const toolsMap = {
search_flights: searchFlightsTool,
search_hotels: searchHotelsTool,
};
// ================= 3. 定义 Agent 节点逻辑 =================
function createAgentNode(agentName, systemPrompt, boundTools) {
return async function agentNode(state) {
console.log(`[${agentName}] 思考中...`);
const messages = state.messages;
const agentLLM = llm.bindTools(boundTools);
const inputMessages = [new SystemMessage(systemPrompt), ...messages];
try {
let response = await agentLLM.invoke(inputMessages);
let newMessages = [response];
if (response.tool_calls && response.tool_calls.length > 0) {
console.log(
`[${agentName}] 检测到工具调用: ${response.tool_calls.map((t) => t.name).join(", ")}`,
);
for (const toolCall of response.tool_calls) {
const toolFunc = toolsMap[toolCall.name];
if (toolFunc) {
const result = await toolFunc.invoke(toolCall.args);
// 显式构造 ToolMessage,确保类型正确
const toolMsg = new ToolMessage({
content: String(result),
tool_call_id: toolCall.id,
name: toolCall.name,
});
newMessages.push(toolMsg);
console.log(`[${agentName}] 工具执行完毕: ${toolCall.name}`);
} else {
console.warn(`未找到工具实现: ${toolCall.name}`);
}
}
console.log(`[${agentName}] 生成最终回复...`);
const finalResponse = await agentLLM.invoke([
...inputMessages,
...newMessages,
]);
newMessages.push(finalResponse);
}
return { messages: newMessages };
} catch (e) {
// 终极防御:无论 e 是什么类型,都能安全转为字符串
let errorMsg = "未知错误";
if (e === null || e === undefined) {
errorMsg = "错误对象为空 (null/undefined)";
} else if (typeof e === "string") {
errorMsg = e;
} else if (typeof e === "object") {
// 尝试读取 message, stack, 或 toString
errorMsg = e.message || e.stack || JSON.stringify(e) || String(e);
} else {
errorMsg = String(e);
}
console.error(`[${agentName}] Error:`, errorMsg);
return {
messages: [
new AIMessage(
`${agentName} 遇到内部问题: ${errorMsg}. 建议尝试简化您的请求或重新开始。`,
),
],
};
}
};
}
const flightAgentNode = createAgentNode(
"Flight Agent",
`${todayInfo}
You are a flight expert.
CRITICAL RULES:
1. Use the tool 'search_flights' ONLY when you have specific from, to, and date.
2. If the user says "tomorrow" or "next week", calculate the exact date based on TODAY (${currentDateStr}).
3. DO NOT guess round-trip flights unless explicitly asked. Only search the route mentioned in the immediate context.
4. If the user asks to "change date", keep the original cities and only update the date.`,
[searchFlightsTool],
);
const hotelAgentNode = createAgentNode(
"Hotel Agent",
`${todayInfo}
You are a hotel expert.
CRITICAL RULES:
1. Use 'search_hotels' ONLY with specific city and dates.
2. Calculate relative dates (e.g., "tomorrow") based on TODAY (${currentDateStr}).
3. Do not assume check-out dates; if missing, assume 1 night stay unless specified.`,
[searchHotelsTool],
);
const generalAgentNode = createAgentNode(
"General Assistant",
"You are a friendly travel assistant. Chat naturally. No tools needed.",
[],
);
// ================= 4. 核心修复:健壮的消息类型识别与路由 =================
/**
* 修复版:超级健壮的消息类型识别
* 兼容 LangChain 不同版本的 Message 结构
*/
function getMessageType(msg) {
if (!msg) return "unknown";
// 方法 1: 标准 _getType
if (typeof msg._getType === "function") {
try {
return msg._getType();
} catch (e) {
/* ignore */
}
}
// 方法 2: role 属性 (某些版本或自定义消息)
if (msg.role) {
if (msg.role === "assistant") return "ai";
return msg.role;
}
// 方法 3: 构造函数名称匹配 (最兜底的方法)
const cname = msg.constructor ? msg.constructor.name : "";
if (cname.includes("HumanMessage")) return "human";
if (cname.includes("AIMessage")) return "ai";
if (cname.includes("ToolMessage")) return "tool";
if (cname.includes("SystemMessage")) return "system";
// 方法 4: 检查是否有 tool_call_id (ToolMessage 的特征)
if (msg.tool_call_id) return "tool";
return "unknown";
}
/**
* 初始路由
*/
async function initialRouter(state) {
const messages = state.messages || [];
const lastHumanMsg = [...messages]
.reverse()
.find((m) => getMessageType(m) === "human");
if (!lastHumanMsg) return "general_agent";
const content = lastHumanMsg.content.toLowerCase();
// --- A. 检测明确的意图关键词 ---
const hasFlightIntent =
content.includes("机票") ||
content.includes("航班") ||
content.includes("飞") ||
content.includes("flight");
const hasHotelIntent =
content.includes("酒店") ||
content.includes("住") ||
content.includes("hotel");
if (hasFlightIntent) {
console.log(`[规则路由] 明确意图: 机票 -> Flight Agent`);
return "flight_agent";
}
if (hasHotelIntent) {
console.log(`[规则路由] 明确意图: 酒店 -> Hotel Agent`);
return "hotel_agent";
}
// --- B. 检测"参数修改"意图 (关键修复!) ---
const modificationKeywords = [
"换个",
"改成",
"改为",
"变成",
"换到",
"调整",
"修正",
"重新",
"不对",
"错了",
"不是",
"date",
"time",
"tomorrow",
"yesterday",
];
const isModificationRequest = modificationKeywords.some((k) =>
content.includes(k),
);
if (isModificationRequest) {
// 修复:先 join 成字符串,再 toLowerCase()
const allHistoryText = messages
.map((m) => String(m.content || ""))
.join(" ")
.toLowerCase();
if (
allHistoryText.includes("机票") ||
allHistoryText.includes("flight") ||
allHistoryText.includes("航班")
) {
console.log(`[上下文路由] 检测到修改请求 + 历史机票意图 -> Flight Agent`);
return "flight_agent";
}
if (
allHistoryText.includes("酒店") ||
allHistoryText.includes("hotel") ||
allHistoryText.includes("住")
) {
console.log(`[上下文路由] 检测到修改请求 + 历史酒店意图 -> Hotel Agent`);
return "hotel_agent";
}
}
// --- C. 检测纯实体补充 ---
const dateRegex = /\d{4}[-/]\d{1,2}[-/]\d{1,2}|明天|后天|今天|昨日|明日/;
const hasDateEntity = dateRegex.test(content);
const knownCities = [
"北京",
"上海",
"广州",
"深圳",
"成都",
"杭州",
"重庆",
"武汉",
"西安",
"南京",
];
const hasCityEntity = knownCities.some((c) => content.includes(c));
if (hasDateEntity || hasCityEntity) {
// 修复:先 join 成字符串,再 toLowerCase()
const allHistoryText = messages
.map((m) => String(m.content || ""))
.join(" ")
.toLowerCase();
if (allHistoryText.includes("机票") || allHistoryText.includes("flight")) {
console.log(`[上下文路由] 检测到实体补充 + 历史机票意图 -> Flight Agent`);
return "flight_agent";
}
if (allHistoryText.includes("酒店") || allHistoryText.includes("hotel")) {
console.log(`[上下文路由] 检测到实体补充 + 历史酒店意图 -> Hotel Agent`);
return "hotel_agent";
}
}
return "general_agent";
}
/**
* 终极修正版:shouldContinue
* 逻辑:
* 1. 如果检测到"新实体"(新城市、新日期)或"明确重启词",强制重置状态(开启新任务)。
* 2. 如果仅是顺序词(先/再)且无新实体,且已有工具记录,则禁止重置(防止死循环)。
*/
async function shouldContinue(state) {
const messages = state.messages || [];
if (messages.length === 0) return END;
const lastMessage = messages[messages.length - 1];
// 1. 错误检查
if (lastMessage.content && String(lastMessage.content).startsWith("Error")) {
return END;
}
// 2. 防死循环:检测最后一条消息是否是 "纯文本 AI 回复"
const lastMsgType = getMessageType(lastMessage);
if (lastMsgType === "ai" || lastMsgType === "assistant") {
const hasPendingToolCalls =
lastMessage.tool_calls && lastMessage.tool_calls.length > 0;
const recentMessages = messages.slice(-4);
const hasRecentToolExecution = recentMessages.some(
(m) => getMessageType(m) === "tool",
);
// 判据:AI 回复了,但没有 pending 工具,最近也没执行过工具 -> 必须结束,等用户说话
if (!hasPendingToolCalls && !hasRecentToolExecution) {
console.log(`[防死循环] AI 已回复且无工具动作,强制结束等待用户输入`);
return END;
}
if (hasPendingToolCalls) {
console.log(`发现未处理的 tool_calls,尝试继续`);
}
}
// 3. 获取最后一条人类消息
const lastHumanMsg = [...messages]
.reverse()
.find((m) => getMessageType(m) === "human");
if (!lastHumanMsg) return END;
const userContent = String(lastHumanMsg.content).toLowerCase();
// 4. 意图识别
const hasFlightIntent =
userContent.includes("机票") ||
userContent.includes("航班") ||
userContent.includes("飞") ||
userContent.includes("flight");
const hasHotelIntent =
userContent.includes("酒店") ||
userContent.includes("住") ||
userContent.includes("hotel");
// 如果既没机票也没酒店意图,直接结束(除非是在多步流程中,但这里简化处理)
if (!hasFlightIntent && !hasHotelIntent) {
return END;
}
// 5. 获取已执行工具记录
const executedTools = messages
.filter((m) => getMessageType(m) === "tool")
.map((m) => m.name || "unknown");
let hasCheckedFlight = executedTools.includes("search_flights");
let hasCheckedHotel = executedTools.includes("search_hotels");
// ================= 核心逻辑:智能状态重置 =================
// A. 定义城市库
const knownCities = [
"北京",
"上海",
"广州",
"深圳",
"成都",
"杭州",
"重庆",
"武汉",
"西安",
"南京",
];
// B. 提取【纯人类】的历史城市 (排除最后一条当前消息,排除 AI 的消息)
const historyHumanMessages = messages.filter(
(m, idx) => getMessageType(m) === "human" && idx !== messages.length - 1,
);
const historyText = historyHumanMessages
.map((m) => String(m.content || ""))
.join(" ")
.toLowerCase();
const historyCities = knownCities.filter((c) => historyText.includes(c));
// C. 提取当前消息的城市
const currentCities = knownCities.filter((c) => userContent.includes(c));
// D. 判断是否有新城市
const hasNewCity =
currentCities.length > 0 &&
currentCities.some((c) => !historyCities.includes(c));
// E. 判断是否有新日期
const dateRegex = /\d{4}[-/]\d{1,2}[-/]\d{1,2}|明天|后天|今天|昨日|明日|下周/;
const hasDateEntity = dateRegex.test(userContent);
// F. 判断指令类型
const hardResetWords = [
"重新",
"这次",
"现在",
"new",
"restart",
"换一个任务",
"从头开始",
"算了",
];
const isHardReset = hardResetWords.some((w) => userContent.includes(w));
const modificationWords = ["换个", "改成", "改为", "调整", "换到", "修正"];
const isModification = modificationWords.some((w) => userContent.includes(w));
let forceResetFlight = false;
let forceResetHotel = false;
if (isHardReset) {
// 彻底重来
forceResetFlight = true;
forceResetHotel = true;
console.log(`[重置] 检测到明确重启指令 -> 清空所有状态`);
} else if (isModification) {
// 修改意图:只重置涉及到的部分
if (hasFlightIntent && (hasNewCity || hasDateEntity)) {
forceResetFlight = true;
console.log(`[重置] 修改意图 + 机票参数变动 -> 仅重置机票状态`);
}
if (hasHotelIntent && (hasNewCity || hasDateEntity)) {
forceResetHotel = true;
console.log(`[重置] 修改意图 + 酒店参数变动 -> 仅重置酒店状态`);
}
} else if (hasNewCity) {
// 新城市意图(非修改词,但出现了新城市):视为新子任务
if (hasFlightIntent) {
forceResetFlight = true;
console.log(
`[重置] 发现新机票城市 (${currentCities.join(",")}) -> 重置机票状态`,
);
}
if (hasHotelIntent) {
forceResetHotel = true;
console.log(
`[重置] 发现新酒店城市 (${currentCities.join(",")}) -> 重置酒店状态`,
);
}
}
// G. 应用重置
if (forceResetFlight) hasCheckedFlight = false;
if (forceResetHotel) hasCheckedHotel = false;
// ================= 核心逻辑结束 =================
// 打印调试
console.log(
` [DEBUG] 意图: 机票=${hasFlightIntent}, 酒店=${hasHotelIntent}`,
);
console.log(
` [DEBUG] 城市: 当前=[${currentCities.join(",")}] 历史=[${historyCities.join(",")}] 新城市=${hasNewCity}`,
);
console.log(
` [DEBUG] 状态: 机票已查=${hasCheckedFlight}, 酒店已查=${hasCheckedHotel}`,
);
// 7. 决策流转
if (hasFlightIntent && hasHotelIntent) {
if (!hasCheckedFlight && !hasCheckedHotel) return "flight_agent";
if (hasCheckedFlight && !hasCheckedHotel) return "hotel_agent";
if (!hasCheckedFlight && hasCheckedHotel) return "flight_agent";
return END;
}
if (hasFlightIntent && !hasHotelIntent) {
if (!hasCheckedFlight) return "flight_agent";
return END;
}
if (!hasFlightIntent && hasHotelIntent) {
if (!hasCheckedHotel) return "hotel_agent";
return END;
}
return END;
}
// ================= 5. 构建图 =================
const workflow = new StateGraph({
channels: {
messages: {
reducer: (x, y) => x.concat(y),
default: () => [],
},
},
});
workflow.addNode("flight_agent", flightAgentNode);
workflow.addNode("hotel_agent", hotelAgentNode);
workflow.addNode("general_agent", generalAgentNode);
workflow.addConditionalEdges(START, initialRouter, [
"flight_agent",
"hotel_agent",
"general_agent",
]);
// 确保 addConditionalEdges 的 targets 包含所有可能的返回值
workflow.addConditionalEdges("flight_agent", shouldContinue, [
"hotel_agent",
"flight_agent", // 防止自我循环或重试
END,
]);
workflow.addConditionalEdges("hotel_agent", shouldContinue, [
"flight_agent",
"hotel_agent",
END,
]);
workflow.addEdge("general_agent", END);
const checkpointer = new MemorySaver();
const app = workflow.compile({ checkpointer });
console.log("Graph 构建成功!\n");
// ================= 6. 运行交互 =================
const readline = require("readline").createInterface({
input: process.stdin,
output: process.stdout,
});
const threadId = "session-" + Date.now();
const config = { configurable: { thread_id: threadId } };
console.log("提示: 试着说 '帮我查北京到上海的机票,顺便看看酒店'");
console.log("输入 'quit' 退出\n");
const askQuestion = () =>
new Promise((resolve) => readline.question("👤 你: ", resolve));
(async () => {
while (true) {
const input = await askQuestion();
if (input.toLowerCase() === "quit") break;
if (!input.trim()) continue;
try {
console.log("\n开始处理任务链...");
const result = await app.invoke(
{ messages: [new HumanMessage(input)] },
config,
);
const messages = result.messages || [];
const lastAiMsg = messages
.slice()
.reverse()
.find((m) => {
const type = getMessageType(m);
return (
(type === "ai" || type === "assistant") &&
m.content &&
String(m.content).trim() !== "" &&
!String(m.content).startsWith("Error")
);
});
if (lastAiMsg) {
console.log("\n" + "=".repeat(50));
console.log("最终方案:");
console.log("=".repeat(50));
console.log(lastAiMsg.content);
console.log("=".repeat(50) + "\n");
} else {
console.log("\n助手: (处理完成,但未生成文本回复)");
}
} catch (error) {
console.error("\n发生严重错误:", error.message);
// console.error(error.stack);
}
}
readline.close();
console.log("再见!");
})();
package.json
c
{
"name": "swarm-demo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"@langchain/core": "^1.1.32",
"@langchain/langgraph": "^1.2.2",
"@langchain/langgraph-swarm": "^1.0.1",
"@langchain/openai": "^1.2.13",
"agno": "^1.0.0",
"dotenv": "^17.3.1",
"openai": "^6.31.0",
"zod": "^4.3.6"
}
}