新闻速读: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效果
用户输入:
帮我订下周五从北京到曼谷的航班,经济舱,两个人,回程周日。再订靠近大皇宫的酒店,三晚。
系统输出(流式展示):
- 理解意图:
search_flights北京→曼谷,下周五去,周日回,2人经济舱 - 查询结果:国航CA979,09:00-14:30,¥1280/人;泰航TG613,另框
- 酒店搜索:
search_hotels大皇宫附近,6月14-17日,三晚 - 推荐选项:用户确认后,调用
book_flights和book_hotel完成预订 - 返回订单号和行程摘要
所有对话都在一个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工具(函数签名)
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. 主对话处理函数
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)
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,让其生成最终回答。
项目结构
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主要代码:
<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:
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,花一周就能上一个类似功能。以下是我认为值得立即做的:
- 把API包装成OpenAI工具格式,但别急着接LLM,先写死Mock测试流程。
- 设计好确认/取消的交互:LLM负责理解意图、收集信息,但最终下单必须由用户显式确认。
- 处理边缘场景:用户说“随便”,LLM不能真的随便,应该引导用户选择。
- 监控Token消耗:一次多工具调用可能吃掉几千token,设置每日预算。
如果你现在负责一个旅行类产品,不要等Expedia开放接口,直接用它们的公共API(Expedia Rapid API)就能实现差不多的体验——区别只是需要自己写LLM编排。这篇DEMO的完整代码放在 GitHub Gist(虚构链接),欢迎去fork修改。
