const readline = require("readline-sync");
const { Configuration, OpenAIApi } = require("openai");
const axios = require("axios");
const mongoose = require("mongoose");
const winston = require("winston");
const math = require("mathjs");
const { v4: uuidv4 } = require("uuid");
mongoose.connect('mongodb://localhost/travel_db', { useNewUrlParser: true, useUnifiedTopology: true });
const TravelSchema = new mongoose.Schema({
sessionId: String,
memory: Array,
itinerary: Object,
createdAt: { type: Date, default: Date.now },
updatedAt: Date,
feedback: String
});
const TravelModel = mongoose.model('Travel', TravelSchema);
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
transports: [new winston.transports.File({ filename: 'travel-agent.log' })]
});
class Tool {
constructor(name, description, func) {
this.name = name;
this.description = description;
this.func = func;
}
async execute(args) {
try {
const result = await this.func(args);
logger.info(`Tool ${this.name} executed with args: ${args}, result: ${result}`);
return result;
} catch (e) {
logger.error(`Tool ${this.name} failed: ${e.message}`);
throw e;
}
}
}
class Memory {
constructor() {
this.messages = [];
}
add(role, content) {
this.messages.push({ role, content, timestamp: new Date() });
}
last() {
return this.messages[this.messages.length - 1];
}
all() {
return this.messages;
}
async compress(llm) {
if (this.messages.length > 10) {
const summary = await llm.ask(`总结历史对话,确保保留关键用户偏好、行程细节和反馈: ${this.all().map(m => m.content).join('\n')}`);
this.messages = this.messages.slice(-5).concat([{ role: "system", content: summary }]);
}
}
async saveToDB(sessionId, itinerary, feedback = '') {
await TravelModel.updateOne(
{ sessionId },
{ memory: this.messages, itinerary, updatedAt: new Date(), feedback },
{ upsert: true }
);
}
async loadFromDB(sessionId) {
const doc = await TravelModel.findOne({ sessionId });
if (doc) {
this.messages = doc.memory;
return { itinerary: doc.itinerary, feedback: doc.feedback };
}
return null;
}
}
class LLM {
constructor(apiKey, model = "gpt-4o") {
this.openai = new OpenAIApi(new Configuration({ apiKey }));
this.model = model;
}
async ask(prompt, systemPrompt = "你是一个旅游规划专家,输出严格的JSON格式,确保响应完整和准确。") {
try {
const res = await this.openai.createChatCompletion({
model: this.model,
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: prompt }
],
temperature: 0.3,
max_tokens: 2048
});
let content = res.data.choices[0].message.content.trim();
if (!content.startsWith('{')) content = `{${content}}`;
logger.info(`LLM response: ${content}`);
return JSON.parse(content);
} catch (e) {
logger.error(`LLM error: ${e.message}`);
throw e;
}
}
}
class TravelReActAgent {
constructor({ tools, llm, systemPrompt }) {
this.tools = tools;
this.llm = llm;
this.memory = new Memory();
this.systemPrompt = systemPrompt;
this.maxSteps = 15;
this.sessionId = uuidv4();
this.itinerary = { userPrefs: {}, itinerary: [], totalCost: 0, status: "pending" };
this.feedback = '';
}
buildPrompt() {
const toolDesc = this.tools.map(t => `工具名: ${t.name} 描述: ${t.description}`).join('\n');
const history = this.memory.all().map(m => `${m.role}: ${m.content} [${m.timestamp.toISOString()}]`).join('\n');
return `${this.systemPrompt}\n可用工具:\n${toolDesc}\n对话历史:\n${history}\n当前用户需求: ${this.memory.last().content}\n使用ReAct框架: 思考(thought), 行动(action), 最终答案(finalAnswer)。输出JSON: {thought: string, action: {tool: string, args: string} | null, finalAnswer: object | null}`;
}
async think() {
await this.memory.compress(this.llm);
const prompt = this.buildPrompt();
const response = await this.llm.ask(prompt, "你是一个旅游规划ReAct智能体,使用链式思考结合工具优化行程,确保计划完整、预算内、无冲突。输出JSON。");
this.memory.add("assistant", JSON.stringify(response));
return response;
}
async act(action) {
if (!action || !action.tool) {
return "无行动需要执行";
}
const tool = this.tools.find(t => t.name === action.tool);
if (!tool) {
const msg = `工具 ${action.tool} 不存在`;
this.memory.add("system", msg);
logger.warn(msg);
return msg;
}
for (let retry = 0; retry < 3; retry++) {
try {
const result = await tool.execute(action.args);
this.memory.add("tool", `工具 ${tool.name} 返回: ${result}`);
if (action.tool === "search_attractions") {
const attractions = result.split(', ').map(a => a.trim());
const day = this.itinerary.itinerary.length + 1;
const activities = attractions.map((a, i) => ({
name: a,
cost: Math.floor(Math.random() * 100) + 50,
time: `${String(9 + i * 3).padStart(2, '0')}:00-${String(12 + i * 3).padStart(2, '0')}:00`,
type: 'attraction'
}));
this.itinerary.itinerary.push({
day,
activities,
dailyCost: activities.reduce((sum, act) => sum + act.cost, 0)
});
this.itinerary.totalCost += this.itinerary.itinerary[day - 1].dailyCost;
} else if (action.tool === "booking") {
const [type, city, cost] = result.split(', ');
const lastDay = this.itinerary.itinerary[this.itinerary.itinerary.length - 1];
lastDay.activities.push({
name: city.trim(),
cost: parseInt(cost.split(' ')[1]),
time: type.includes('酒店') ? "20:00-08:00" : "18:00-20:00",
type: type.trim()
});
lastDay.dailyCost += parseInt(cost.split(' ')[1]);
this.itinerary.totalCost += parseInt(cost.split(' ')[1]);
} else if (action.tool === "search_transport") {
const [fromTo, mode, cost] = result.split(', ');
this.itinerary.itinerary[0].activities.unshift({
name: `交通: ${fromTo.trim()}`,
cost: parseInt(cost.split(' ')[1]),
time: "08:00-09:00",
type: 'transport'
});
this.itinerary.totalCost += parseInt(cost.split(' ')[1]);
} else if (action.tool === "search_dining") {
const dining = result.split(', ').map(d => d.trim());
const lastDay = this.itinerary.itinerary[this.itinerary.itinerary.length - 1];
lastDay.activities.push({
name: dining[0],
cost: Math.floor(Math.random() * 50) + 30,
time: "12:00-13:00",
type: 'dining'
});
lastDay.dailyCost += lastDay.activities[lastDay.activities.length - 1].cost;
this.itinerary.totalCost += lastDay.activities[lastDay.activities.length - 1].cost;
}
await this.memory.saveToDB(this.sessionId, this.itinerary, this.feedback);
return result;
} catch (e) {
this.memory.add("system", `工具 ${tool.name} 失败: ${e.message}`);
}
}
throw new Error(`工具 ${tool.name} 重试失败`);
}
async detectChanges() {
const last = this.memory.last();
if (!last) return null;
const prompt = `
分析以下消息,判断是否包含需要调整行程的变化:
- 消息角色: ${last.role}
- 消息内容: ${last.content}
- 消息时间: ${last.timestamp.toISOString()}
请识别变化类型并提取关键信息,输出JSON格式:
{
type: string, // 变化类型,如 "weather", "user", "event", "none"
value: string, // 变化的具体内容
details: object // 提取的细节,如 { condition: "雨", probability: 40 } 或 { newCity: "上海", newBudget: 5000 }
}
如果无变化,type 设置为 "none"。
`;
try {
const response = await this.llm.ask(prompt, "你是一个旅游规划专家,精确分析消息内容,识别变化类型并提取细节,输出严格的JSON格式。");
if (!response.type || !response.value) {
logger.warn(`LLM 响应不完整,返回默认值: ${JSON.stringify(response)}`);
return { type: "none", value: last.content, details: {} };
}
logger.info(`变化检测结果: ${JSON.stringify(response)}`);
if (response.type === "none") return null;
return response;
} catch (e) {
logger.error(`变化检测失败: ${e.message}`);
return { type: "none", value: last.content, details: {} };
}
}
async evaluateImpact(change, plan, prefs) {
const prompt = `
分析以下变化对旅行计划的影响:
- 变化类型: ${change.type}
- 变化内容: ${change.value}
- 变化细节: ${JSON.stringify(change.details)}
- 当前行程计划: ${JSON.stringify(plan, null, 2)}
- 用户偏好: ${JSON.stringify(prefs, null, 2)}
请评估:
1. 变化的具体影响(例如,哪些活动受影响、需要何种调整)。
2. 受影响的天数和具体活动(如果有)。
3. 推荐的调整策略(如替换活动、调整时间、重新规划)。
输出JSON格式:
{
impact: string,
affectedDays: number[],
affectedActivities: {day: number, index: number, name: string}[],
strategy: string,
newPrefs: object | null
}
`;
try {
const response = await this.llm.ask(prompt, "你是一个旅游规划专家,基于上下文动态评估变化的影响,确保输出准确、详细的JSON。");
if (!response.impact || !response.strategy) {
logger.warn(`LLM 响应不完整,返回默认值: ${JSON.stringify(response)}`);
return { impact: "无影响", affectedDays: [], affectedActivities: [], strategy: "无需调整", newPrefs: null };
}
if (change.type === "user" && !response.newPrefs) {
response.newPrefs = await this.parsePrefs(change.value);
}
logger.info(`变化评估结果: ${JSON.stringify(response)}`);
if (response.affectedDays.length > 0 && response.affectedActivities.length === 0) {
response.affectedActivities = plan
.filter(day => response.affectedDays.includes(day.day))
.flatMap(day =>
day.activities.map((act, index) => ({
day: day.day,
index,
name: act.name
}))
);
}
return response;
} catch (e) {
logger.error(`变化评估失败: ${e.message}`);
return {
impact: "无法评估影响",
affectedDays: [],
affectedActivities: [],
strategy: "保持原计划",
newPrefs: null
};
}
}
async generateNewPlan(plan, change, impact) {
const prompt = `
基于现有行程 ${JSON.stringify(plan)}、变化 ${JSON.stringify(change)} 和影响 ${JSON.stringify(impact)},
生成调整后的完整行程。重点处理受影响活动:${JSON.stringify(impact.affectedActivities)}。
调整策略:${impact.strategy}。确保预算内、无时间冲突、活动多样。
输出JSON: [{day: number, activities: [{name, cost, time, type}], dailyCost: number}]
`;
return await this.llm.ask(prompt);
}
async parsePrefs(query) {
const prompt = `解析用户旅行需求: ${query}\n输出JSON: {city: string, days: number, budget: number, interests: array, people: number, preferences: {diet: string, transport: string}}`;
return await this.llm.ask(prompt);
}
async generatePlan(prefs) {
const prompt = `基于用户偏好 ${JSON.stringify(prefs)},生成 ${prefs.days} 天详细行程计划。每一天包括2-4个活动(景点、餐饮、住宿、交通),估算每日成本,总成本不超过预算 ${prefs.budget} 的90%。兴趣点: ${prefs.interests.join(', ')}。考虑饮食 ${prefs.preferences.diet} 和交通 ${prefs.preferences.transport}。输出JSON: [{day: number, activities: [{name: string, cost: number, time: string, type: string}], dailyCost: number}]`;
return await this.llm.ask(prompt);
}
async validatePlan(plan, prefs) {
const totalCost = plan.reduce((sum, day) => sum + day.dailyCost, 0);
if (totalCost > prefs.budget) return { valid: false, reason: `预算超限: ${totalCost} > ${prefs.budget}` };
let conflicts = false;
plan.forEach(day => {
let prevEnd = '00:00';
day.activities.sort((a, b) => a.time.split('-')[0] > b.time.split('-')[0] ? 1 : -1);
day.activities.forEach(act => {
const [start, end] = act.time.split('-');
if (start < prevEnd) conflicts = true;
prevEnd = end;
});
});
if (conflicts) return { valid: false, reason: "活动时间冲突" };
return { valid: true };
}
async adjustPlan(plan, observation) {
const prompt = `基于现有行程 ${JSON.stringify(plan)} 和新观察 ${observation}(如天气变化或用户反馈),优化行程(如添加室内备选)。输出完整JSON新行程。`;
return await this.llm.ask(prompt);
}
async applyFeedback() {
const history = await this.memory.loadFromDB(this.sessionId);
if (history && history.feedback) {
this.systemPrompt += `\n基于过去反馈: ${history.feedback},优化规划以避免类似问题。`;
}
}
async finalizePlan() {
const valid = await this.validatePlan(this.itinerary.itinerary, this.itinerary.userPrefs);
if (valid.valid) {
this.itinerary.status = "completed";
await this.memory.saveToDB(this.sessionId, this.itinerary, this.feedback);
this.feedback = readline.question("请提供反馈以改进下次规划: ");
await this.memory.saveToDB(this.sessionId, this.itinerary, this.feedback);
return this.itinerary;
}
throw new Error(valid.reason);
}
async run(userQuery) {
await this.applyFeedback();
this.memory.add("user", userQuery);
this.itinerary.userPrefs = await this.parsePrefs(userQuery);
this.itinerary.itinerary = await this.generatePlan(this.itinerary.userPrefs);
await this.memory.saveToDB(this.sessionId, this.itinerary);
let step = 0;
while (step < this.maxSteps && this.itinerary.status !== "completed") {
try {
const response = await this.think();
logger.info(`Step ${step + 1}: ${JSON.stringify(response)}`);
if (response.finalAnswer) {
this.itinerary.itinerary = response.finalAnswer;
const finalItinerary = await this.finalizePlan();
console.log("行程规划完成。最终行程:");
console.log(JSON.stringify(finalItinerary, null, 2));
break;
}
if (response.action) {
const result = await this.act(response.action);
console.log(`步骤 ${step + 1} 结果: ${result}`);
}
const change = await this.detectChanges();
if (change) {
const impact = await this.evaluateImpact(change, this.itinerary.itinerary, this.itinerary.userPrefs);
if (impact.impact !== "无影响") {
const newPlan = await this.generateNewPlan(this.itinerary.itinerary, change, impact);
const validation = await this.validatePlan(newPlan, this.itinerary.userPrefs);
if (validation.valid) {
this.itinerary.itinerary = newPlan;
await this.memory.saveToDB(this.sessionId, this.itinerary);
} else {
this.itinerary.itinerary = await this.adjustPlan(newPlan, validation.reason);
await this.memory.saveToDB(this.sessionId, this.itinerary);
}
}
}
step++;
} catch (e) {
logger.error(`Step ${step + 1} failed: ${e.message}`);
this.memory.add("system", `步骤 ${step + 1} 错误: ${e.message}`);
step++;
}
}
if (this.itinerary.status !== "completed") {
console.log("达到最大步骤,行程未完成,请检查输入或工具。");
}
}
}
const tools = [
new Tool("search_attractions", "搜索旅游景点,参数: 城市,兴趣", async (args) => {
const [city, interest] = args.split(',');
return `${city} 的 ${interest} 景点: 故宫, 长城, 颐和园, 鸟巢, 水立方`;
}),
new Tool("weather", "查询天气,参数: 城市,日期", async (args) => {
const [city, date] = args.split(',');
try {
const res = await axios.get(`https://api.weatherapi.com/v1/forecast.json?key=YOUR_WEATHER_KEY&q=${city}&dt=${date}`);
const condition = res.data.forecast.forecastday[0].day.condition.text;
const probability = res.data.forecast.forecastday[0].day.daily_will_it_rain;
const temp = res.data.forecast.forecastday[0].day.avgtemp_c;
return JSON.stringify({ condition, probability, temperature: temp });
}),
new Tool("booking", "预订酒店/交通,参数: 类型(酒店/交通),城市,预算", async (args) => {
const [type, city, budget] = args.split(',');
const cost = Math.floor(parseInt(budget) / 2);
return `${type}, ${city} 预订成功, 成本 ${cost}`;
}),
new Tool("calc_budget", "计算预算,参数: 数学表达式", async (args) => {
try {
return math.evaluate(args);
} catch {
return "计算错误";
}
}),
new Tool("search_transport", "搜索交通方式,参数: 出发城市,到达城市,日期", async (args) => {
const [from, to, date] = args.split(',');
const cost = Math.floor(Math.random() * 200) + 100;
return `${from} 到 ${to} 于 ${date}, 方式: 高铁, 成本 ${cost}`;
}),
new Tool("search_dining", "搜索餐饮,参数: 城市,饮食偏好", async (args) => {
const [city, pref] = args.split(',');
return `${city} 的 ${pref} 餐饮: 当地餐厅A, 餐厅B, 街头小吃`;
})
];
(async () => {
const apiKey = process.env.OPENAI_API_KEY || "YOUR_OPENAI_API_KEY";
const llm = new LLM(apiKey);
const systemPrompt = "你是一个旅游规划ReAct智能体,结合推理、行动、观察、规划、协作和自我完善生成优化行程。使用工具收集信息,逐步规划,确保完整方案。";
const agent = new TravelReActAgent({ tools, llm, systemPrompt });
const userQuery = readline.question("输入旅行需求 (e.g., 北京5天,预算3000,文化,2人,素食,高铁): ");
await agent.run(userQuery);
})();