AI agent -demo

51 阅读7分钟
// 导入必要的模块:readline-sync 用于同步读取用户输入,确保用户交互顺畅
const readline = require("readline-sync");
// 导入 OpenAI 的配置和 API,用于调用 LLM 服务,提供强大的推理能力
const { Configuration, OpenAIApi } = require("openai");
// 导入 axios 用于进行 HTTP 请求,如调用天气 API 或其他外部服务
const axios = require("axios");
// 导入 mongoose 用于连接和操作 MongoDB 数据库,实现数据持久化和自我完善
const mongoose = require("mongoose");
// 导入 winston 用于日志记录,便于调试和监控代理行为
const winston = require("winston");
// 导入 mathjs 用于数学计算,支持预算优化
const math = require("mathjs");
// 导入 uuid 的 v4 方法,用于生成唯一会话 ID,确保每个会话独立
const { v4: uuidv4 } = require("uuid");

// 连接 MongoDB 数据库,使用本地 travel_db 数据库,设置解析选项以避免警告
mongoose.connect('mongodb://localhost/travel_db', { useNewUrlParser: true, useUnifiedTopology: true });
// 定义 TravelSchema,存储旅行会话数据,包括会话 ID、记忆、行程、创建和更新时间、反馈
const TravelSchema = new mongoose.Schema({
  sessionId: String,  // 会话唯一标识
  memory: Array,      // 存储对话记忆,用于观察和推理
  itinerary: Object,  // 存储行程计划,支持规划功能
  createdAt: { type: Date, default: Date.now },  // 创建时间,默认当前时间
  updatedAt: Date,    // 更新时间,用于跟踪变化
  feedback: String    // 用户反馈,用于自我完善
});
// 创建 TravelModel 模型,用于数据库操作
const TravelModel = mongoose.model('Travel', TravelSchema);

// 创建日志记录器,设置日志级别为 info,格式为带时间戳的 JSON,输出到文件
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' })]  // 输出到文件
});

// 定义 Tool 类,封装工具,包括名称、描述和执行函数,支持行动功能
class Tool {
  // 构造函数:初始化工具的名称、描述和执行函数
  constructor(name, description, func) {
    this.name = name;        // 工具名称
    this.description = description;  // 工具描述,用于LLM推理
    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;  // 抛出错误,支持重试
    }
  }
}

// 定义 Memory 类,管理对话记忆,支持观察和自我完善
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;  // 返回整个消息数组
  }
  // 压缩记忆:消息超 10 条时,用 LLM 总结,保留最后 5 条加总结
  async compress(llm) {
    if (this.messages.length > 10) {  // 检查消息长度
      const summary = await llm.ask(`总结历史对话,确保保留关键用户偏好、行程细节和反馈: ${this.all().map(m => m.content).join('\n')}`);  // LLM 总结
      this.messages = this.messages.slice(-5).concat([{ role: "system", content: summary }]);  // 保留最后 5 条加总结
    }
  }
  // 保存到数据库:更新或插入会话的记忆、行程和反馈
  async saveToDB(sessionId, itinerary, feedback = '') {
    await TravelModel.updateOne(
      { sessionId },  // 根据 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;  // 无历史
  }
}

// 定义 LLM 类,与 OpenAI API 交互,支持推理和规划
class LLM {
  // 构造函数:初始化 OpenAI API 和模型,默认 gpt-4o
  constructor(apiKey, model = "gpt-4o") {
    this.openai = new OpenAIApi(new Configuration({ apiKey }));  // 创建 OpenAI 实例
    this.model = model;  // 设置模型
  }
  // 询问 LLM:发送提示,获取响应,解析为 JSON,记录日志
  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}}`;  // 修复非 JSON
      logger.info(`LLM response: ${content}`);  // 记录日志
      return JSON.parse(content);  // 解析为 JSON
    } catch (e) {
      logger.error(`LLM error: ${e.message}`);  // 记录错误
      throw e;  // 抛出错误
    }
  }
}

// 定义 TravelReActAgent 类,ReAct 框架的旅行代理
class TravelReActAgent {
  // 构造函数:初始化工具、LLM、系统提示、记忆、行程等
  constructor({ tools, llm, systemPrompt }) {
    this.tools = tools;  // 工具列表,支持行动
    this.llm = llm;      // LLM 实例,支持推理
    this.memory = new Memory();  // 记忆实例,支持观察
    this.systemPrompt = systemPrompt;  // 系统提示,指导行为
    this.maxSteps = 15;  // 最大步骤数,允许复杂规划
    this.sessionId = uuidv4();  // 生成唯一会话 ID
    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}`;
  }

  // 思考步骤:压缩记忆,构建提示,询问 LLM,添加响应到记忆
  async think() {
    await this.memory.compress(this.llm);  // 压缩记忆
    const prompt = this.buildPrompt();    // 构建提示
    const response = await this.llm.ask(prompt, "你是一个旅游规划ReAct智能体,使用链式思考结合工具优化行程,确保计划完整、预算内、无冲突。输出JSON。");  // 询问 LLM
    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++) {  // 重试 3 次
      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} 重试失败`);  // 重试失败
  }

  // 检测变化:使用 LLM 动态分析最后消息,识别变化类型和内容
  async detectChanges() {
    const last = this.memory.last();  // 获取最后消息
    if (!last) return null;  // 如果无消息,返回 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);  // 返回新计划
  }

  // 解析用户偏好:解析查询为 JSON 格式的偏好
  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);  // 抛出错误
  }

  // 运行代理:处理用户查询,生成计划,循环 ReAct,处理变化
  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}`);  // 调用 API
      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";  // 获取 API Key
  const llm = new LLM(apiKey);  // 创建 LLM
  const systemPrompt = "你是一个旅游规划ReAct智能体,结合推理、行动、观察、规划、协作和自我完善生成优化行程。使用工具收集信息,逐步规划,确保完整方案。";  // 系统提示
  const agent = new TravelReActAgent({ tools, llm, systemPrompt });  // 创建代理

  const userQuery = readline.question("输入旅行需求 (e.g., 北京5天,预算3000,文化,2人,素食,高铁): ");  // 读取输入
  await agent.run(userQuery);  // 运行
})();