新闻速读:Expedia 又往前迈了一步,但技术本身不新

2026年5月,Expedia在Explore 2026上宣布了三项AI新体验:

  • Meta信息流内旅行规划(in-feed planning):用户直接在Meta的Feed里用自然语言规划行程,无需跳转App。
  • 自然语言旅行规划:对话式交互,一句“帮我订下周六去京都的机+酒”就能触发完整预订流程。
  • 智能预订工具:基于30年第一方数据,优化价控、退改、行程合并。

对开发者来说,这三件事的核心就两个词:LLM + API。Expedia没有自研大模型,而是将已有的预订API包装成LLM可调用的工具。Meta Feed里的AI本质是外部渠道的插件化——LLM在第三方界面里作为前端,Expedia的API作为后端。

下面我们不做新闻复述,而是写一份能跑的代码:用LLM函数调用 + Expedia风格API,实现一个“一句话订机票”的DEMO。读完你能直接落地到自己的旅行或预订类产品。

产品Demo效果

用户输入:

帮我订下周五从北京到曼谷的航班,经济舱,两个人,回程周日。再订靠近大皇宫的酒店,三晚。

系统输出(流式展示):

  1. 理解意图:search_flights 北京→曼谷,下周五去,周日回,2人经济舱
  2. 查询结果:国航CA979,09:00-14:30,¥1280/人;泰航TG613,另框
  3. 酒店搜索:search_hotels 大皇宫附近,6月14-17日,三晚
  4. 推荐选项:用户确认后,调用book_flightsbook_hotel完成预订
  5. 返回订单号和行程摘要

所有对话都在一个Web聊天界面里完成。下面前端我们用纯HTML+JS,后端Node.js + OpenAI函数调用。

技术选型(为什么这么选)

模块 选型 理由
LLM OpenAI GPT-4o(函数调用模式) 稳定,函数调用成熟,支持tools参数直接定义API签名
后端 Node.js + Express 轻量,方便集成SSE流式输出
前端 原生HTML + EventSource 0依赖,读者直接打开就能跑
API模拟 本地mock航班/酒店数据 真实Expedia API需要企业账户,但结构完全一致
会话状态 内存Map(生产用Redis) DEMO简化,但代码预留接口

为什么不用LangChain? 对于这种简单工具调用场景,原生的函数调用更直接,省去一层抽象,调试也更容易。只有当你需要多步推理、记忆复杂上下文时才上LangChain。

核心代码实现

1. 定义OpenAI工具(函数签名)

javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
const tools = [
  {
    type: "function",
    function: {
      name: "search_flights",
      description: "搜索航班,支持出发地、目的地、日期、人数、舱位",
      parameters: {
        type: "object",
        properties: {
          origin: { type: "string", description: "机场三字码或城市名" },
          destination: { type: "string" },
          depart_date: { type: "string", description: "出发日期 YYYY-MM-DD" },
          return_date: { type: "string", description: "回程日期(可选)" },
          passengers: { type: "integer", default: 1 },
          cabin: { type: "string", enum: ["economy","business","first"] }
        },
        required: ["origin","destination","depart_date"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "search_hotels",
      description: "搜索酒店,支持位置、入住日期、退房日期、人数",
      parameters: {
        type: "object",
        properties: {
          location: { type: "string" },
          checkin: { type: "string" },
          checkout: { type: "string" },
          rooms: { type: "integer", default: 1 }
        },
        required: ["location","checkin","checkout"]
      }
    }
  },
  {
    type: "function",
    function: {
      name: "book_flights",
      description: "预订航班,需要flight_id",
      parameters: {
        type: "object",
        properties: {
          flight_id: { type: "string" },
          passengers: { type: "array", items: { type: "object", properties: {
            first_name: { type: "string" },
            last_name: { type: "string" },
            dob: { type: "string" }
          }}}
        },
        required: ["flight_id","passengers"]
      }
    }
  }
];

2. 主对话处理函数

javascript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
async function handleUserMessage(userInput, sessionId) {
  const session = sessions.get(sessionId) || { messages: [] };
  session.messages.push({ role: "user", content: userInput });

  // 调用OpenAI
  const response = await openai.chat.completions.create({
    model: "gpt-4o",
    messages: session.messages,
    tools: tools,
    tool_choice: "auto"
  });

  const choice = response.choices[0];
  session.messages.push(choice.message);

  // 处理函数调用
  if (choice.finish_reason === "tool_calls") {
    for (const toolCall of choice.message.tool_calls) {
      const args = JSON.parse(toolCall.function.arguments);
      let result;
      if (toolCall.function.name === "search_flights") {
        result = await mockSearchFlights(args);
      } else if (toolCall.function.name === "search_hotels") {
        result = await mockSearchHotels(args);
      } else if (toolCall.function.name === "book_flights") {
        result = await mockBookFlights(args);
      }
      session.messages.push({
        role: "tool",
        tool_call_id: toolCall.id,
        content: JSON.stringify(result)
      });
    }
    // 递归调用一次 让LLM根据工具结果生成回复
    return await handleUserMessage("", sessionId);
  }

  // 返回最终回复
  return choice.message.content;
}

3. Mock API(真实场景需要换成Expedia API)

javascript
1 2 3 4 5 6 7 8 9
function mockSearchFlights({origin, destination, depart_date, return_date, passengers, cabin}) {
  // 返回模拟数据
  return {
    flights: [
      { flight_id: "CA979", airline: "国航", depart: "09:00", arrive: "14:30", price: 1280 * passengers, seats: passengers },
      { flight_id: "TG613", airline: "泰航", depart: "10:30", arrive: "15:45", price: 1450 * passengers, seats: passengers }
    ]
  };
}

这里的关键是递归调用:LLM可能一次请求需要调用多个工具(先搜航班,再搜酒店)。我们循环执行所有工具调用,把结果塞回会话,然后递归再调用一次LLM,让其生成最终回答。

项目结构

text
1 2 3 4 5 6 7 8
travel-ai-agent/
├── index.js              # Express服务器,处理/api/chat和SSE
├── tools.js              # 函数定义 + mock实现
├── session.js            # 会话管理(Map)
├── openai-client.js      # OpenAI 初始化
├── .env                  # OPENAI_API_KEY
└── public/
    └── index.html        # 聊天界面

public/index.html主要代码:

html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
<form id="chat-form">
  <input type="text" id="input" placeholder="说句话订行程..." />
  <button>发送</button>
</form>
<div id="messages"></div>

<script>
  const form = document.getElementById('chat-form');
  form.onsubmit = async (e) => {
    e.preventDefault();
    const text = document.getElementById('input').value;
    // 发送到后端 /api/chat,使用SSE流式接收
    const es = new EventSource(`/api/chat?input=${encodeURIComponent(text)}&session_id=${sessionId}`);
    es.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      // 追加到界面
    };
  };
</script>

上线要注意的坑(非理论,全实战)

坑1:价格实时性与库存一致性

Expedia的库存和价格是实时变动的。LLM返回的结果可能已经过时。你的处理流程必须是:

用户确认 -> 再次调用API检查价格和库存 -> 锁定库存 -> 让用户二次确认 -> 下单

绝对不能LLM给出结果后直接下单。我们DEMO中book_flights函数应该先做check availability,返回当前价格。如果价格变动,LLM要重新提示用户。

坑2:延迟过大(流式输出是必须的)

一次函数调用+递归可能耗时5-10秒(如果LLM多次调用)。前端必须用SSE流式输出每个步骤,不能等全部完成才显示。否则用户会以为系统假死。

我们的代码中,后端/api/chat可以这样实现SSE:

javascript
1 2 3 4 5 6 7 8 9 10 11 12 13
app.get('/api/chat', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  (async () => {
    const stream = await runLLMStream(req.query.input, req.query.session_id);
    for await (const chunk of stream) {
      res.write(`data: ${JSON.stringify(chunk)}\n\n`);
    }
    res.end();
  })();
});

坑3:LLM乱调用API(成本和安全)

用户可能说“帮我取消所有订单”,如果LLM调用了cancel API就麻烦。你的函数工具应该做权限分级:只暴露search和book(给用户确认后调用)。取消、退款等敏感操作必须走单独的高权限接口,且需要用户二次验证。

坑4:会话过期与重试

LLM对话是有状态的。如果用户刷新页面,会话丢失,之前搜到的航班结果就作废。生产环境应该用Redis存储会话,并设置TTL(15分钟)。同时,每次调用LLM时,把之前工具调用的结果截断(超过一定轮数就压缩或丢弃),以免上下文过长导致成本飙升和延迟。

对开发者的建议

Expedia这次发布验证了一个趋势:传统OTA API + LLM函数调用 = 新一代旅行助手。如果你手上有一个住宿/机票/酒店类的API,花一周就能上一个类似功能。以下是我认为值得立即做的:

  1. 把API包装成OpenAI工具格式,但别急着接LLM,先写死Mock测试流程。
  2. 设计好确认/取消的交互:LLM负责理解意图、收集信息,但最终下单必须由用户显式确认。
  3. 处理边缘场景:用户说“随便”,LLM不能真的随便,应该引导用户选择。
  4. 监控Token消耗:一次多工具调用可能吃掉几千token,设置每日预算。

如果你现在负责一个旅行类产品,不要等Expedia开放接口,直接用它们的公共API(Expedia Rapid API)就能实现差不多的体验——区别只是需要自己写LLM编排。这篇DEMO的完整代码放在 GitHub Gist(虚构链接),欢迎去fork修改。

expedia AI travel booking interface with chat and flight results