先看效果:输入预算和兴趣,AI给你1小时车程内的目的地
阵亡将士纪念日旅游人数下降,不是因为不想出去,而是因为机票酒店太贵。Nerwallet旅游作家Sally French在CBS News里说得直白——人们开始搜索离家1-2小时的短途目的地,省下交通和住宿的大头。
这对开发者是个机会:旅游产品可以降维打本地微度假,而AI能帮用户做“个性化的短途决策”。
所以,我花了一个周末写了个Demo:本地旅行推荐器(Local Trip Recommender)。输入你的位置、预算、兴趣(比如“徒步”、“亲子”、“美食”),AI会调用地图API找到周边车程1小时内的地点,然后结合你的偏好生成一份带预算、路线、小贴士的旅行计划。
演示地址(已部署在Vercel+Render,安全无毒):[暂不公开,可自建]
读完本文你得到的:
- 一个可直接运行的短途旅行推荐器代码(React前端 + Node.js后端 + OpenAI API + Google Maps API)。
- 从技术选型到上线的完整链路,尤其是流式输出和API计费控制的实战经验。
- 对“低成本旅游+AI”这个产品方向的判断:为什么值得做,以及当下开发者应该切入什么位置。
技术选型:追求最低成本上线验证
核心需求:用户输入 -> 查询POI -> LLM生成推荐文本 -> 前端展示。
| 层 | 选型 | 理由 |
|---|---|---|
| 前端 | React + Vite + Tailwind CSS | 快速原型,组件化,SSR不需要 |
| 后端 | Node.js + Express | 简单,同构,部署成本低 |
| 地图API | Google Maps Places API | 数据全,但考虑费用可用OpenStreetMap的Overpass API替代 |
| LLM | OpenAI GPT-4o-mini(或GPT-4o) | 性价比高,支持流式输出,能力够用 |
| 部署 | Vercel(前端)+ Render(后端) | 免费层足够demo,Vercel serverless也支持Node |
为什么不选更火的技术?因为小产品(单日调用量<1000次)追求“24小时内从0到能跑”,React+Express+OpenAI是最短路径。如果你对AI应用开发还不熟,这套栈上手最快。
核心代码实现:三个关键片段
1. 后端:获取附近POI + 调用LLM生成推荐
Node.js/Express,代码放在/backend/src/server.js。我们只暴露一个POST /api/recommend端点。
// backend/src/server.js (核心片段)
const express = require('express');
const axios = require('axios');
const { OpenAI } = require('openai');
const app = express();
app.use(express.json());
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const GOOGLE_PLACES_API_KEY = process.env.GOOGLE_PLACES_API_KEY;
app.post('/api/recommend', async (req, res) => {
try {
const { location, budget, interests } = req.body;
// 1. 获取附近POI(使用Google Places Nearby Search)
const placesResponse = await axios.get(
'https://maps.googleapis.com/maps/api/place/nearbysearch/json',
{
params: {
location: location, // 如 "40.7128,-74.0060"
radius: 50000, // 50km ≈ 1小时车程(城市道路)
type: 'tourist_attraction',
key: GOOGLE_PLACES_API_KEY,
},
}
);
const places = placesResponse.data.results.slice(0, 5); // 取前5个
// 2. 用LLM生成个性化推荐
const prompt = `你是一个短途旅行规划师。用户位置:${location},预算:${budget}元,兴趣:${interests}。以下是附近景点(名称+评分+地址):\n${places.map(p => `${p.name} (评分${p.rating}, ${p.vicinity})`).join('\n')}\n请推荐1-2个最适合的去处,包含:总花费估算、路线建议、时间段分配。输出格式:用markdown列表,每一项以短横线开头。`;
const stream = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
res.setHeader('Content-Type', 'text/plain');
for await (const chunk of stream) {
res.write(chunk.choices[0]?.delta?.content || '');
}
res.end();
} catch (error) {
console.error(error);
res.status(500).send('出错了,稍后再试');
}
});
app.listen(process.env.PORT || 3001);
几个注意点:
- Google Places API 的
radius单位是米,50km对应大概1小时高速/1.5小时普通道路。按原文“one or two hours from where I live”可调整到100km。但需注意城市拥堵,实际车程可以之后用Directions API精确计算。 - 流式输出(stream: true)让用户等待时间缩短到1-2秒,体验远好于等全部生成。
res.write直接推送给前端,前端用EventSource或fetch + ReadableStream接收。 - 预算
budget在这里只是传递给LLM做参考,没有真正做费用计算。后续可以接入Uber/滴滴API估算交通费。
2. 前端:流式接收并渲染AI推荐
React组件精简如下(/frontend/src/App.jsx):
import { useState } from 'react';
function App() {
const [location, setLocation] = useState('');
const [budget, setBudget] = useState('200');
const [interests, setInterests] = useState('');
const [output, setOutput] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setOutput('');
setLoading(true);
// 用 fetch 读取流
const response = await fetch('/api/recommend', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
location: location || '40.7128,-74.0060', // 默认纽约
budget,
interests,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
setOutput((prev) => prev + decoder.decode(value, { stream: true }));
}
setLoading(false);
};
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">本地短途旅行推荐</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<input
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder="地理位置 (如 40.7128,-74.0060)"
className="w-full border p-2"
/>
<input
value={budget}
onChange={(e) => setBudget(e.target.value)}
placeholder="预算 (元)"
className="w-full border p-2"
/>
<input
value={interests}
onChange={(e) => setInterests(e.target.value)}
placeholder="兴趣 (如徒步, 亲子, 美食)"
className="w-full border p-2"
/>
<button
type="submit"
disabled={loading}
className="bg-blue-500 text-white px-4 py-2 rounded disabled:opacity-50"
>
{loading ? '生成中...' : '获取推荐'}
</button>
</form>
<div className="mt-6 whitespace-pre-wrap">{output}</div>
</div>
);
}
export default App;
关键点:response.body.getReader() 是浏览器的流读取API,配合ReadableStream,一行行拿到AI吐出的文字,实时追加到页面。无需EventSource也能实现流式效果,且不占用额外连接。
3. 部署配置:Vercel前端 + Render后端
前端:在Vercel新建项目,连接GitHub仓库,根目录选frontend,框架选Vite。环境变量加VITE_API_BASE_URL=https://your-render-backend.onrender.com。前端fetch时用这个URL。
后端:用Render Web Service,启动命令node src/server.js。公开端口3001。环境变量填OPENAI_API_KEY和GOOGLE_PLACES_API_KEY。
注意:Render免费版15分钟无请求会休眠,第一次请求会冷启动慢5-10秒。对于演示来说可以接受,生产环境建议换成Railway或Fly.io带always-on的付费方案。
项目结构和配置
local-trip-recommender/
├── frontend/
│ ├── public/
│ ├── src/
│ │ ├── App.jsx
│ │ ├── main.jsx
│ │ └── index.css
│ ├── package.json
│ ├── vite.config.js
│ └── .env (VITE_API_BASE_URL=http://localhost:3001)
├── backend/
│ ├── src/
│ │ └── server.js
│ ├── package.json
│ ├── .env (OPENAI_API_KEY=..., GOOGLE_PLACES_API_KEY=...)
│ └── .gitignore
├── README.md
└── .gitignore
Vite配置需加代理(本地开发):
// vite.config.js
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:3001',
},
},
});
这样前端开发时fetch('/api/recommend')自动转发。
上线要注意的坑
1. API Keys 泄露
Google Places API和OpenAI的Key直接在.env里,但部署到公开仓库会被爬走。
- 后端.env不要提交到GitHub,用仓库的
Settings > Secrets添加。 - 前端VITE_开头的变量会被打包进JS,所有前端Key都不安全。所以地图API请求应该由后端代理,不要在前端直接调用。上面代码已经这么做,前端只请求自己的后端。
2. OpenAI计费陷阱
GPT-4o-mini 输入$0.15/M tokens,输出$0.60/M tokens。一次请求大约300 tokens输入+200 tokens输出,成本约$0.00015。但如果你写死流式并且用户不断刷新,一天1000次也才0.15美元。但要注意:
- 如果Prompt里放了5个POI的描述(名称+地址+评分),tokens会更长,成本上升。
- 建议在代码里限制POI数量(比如最多5个),并做
max_tokens硬上限。 - 增加用户速率限制(比如每分钟5次),用简单内存计数即可。
3. 地图API配额
Google Places Nearby Search 免费版每天无限制(但超过$200/月后收费)。每个POI搜索请求约0.01美元。如果你一天1000次搜索,就是10美元。可以考虑切换到OpenStreetMap的Overpass API完全免费,但数据准确度稍差。
替换方案:用Overpass API查询节点。
const overpassQuery = `[out:json];node["tourism"="attraction"](around:50000,${lat},${lon});out 5;`;
不需要Key,防限流控制好即可。
4. 用户体验:空结果处理
如果用户输入的经纬度附近没有景点(比如农村),Google Places返回空数组。LLM prompt里没有传入POI,模型可能编造景点。一定做兜底逻辑:如果places不足3个,返回提示“附近暂无景点,试试扩大范围或输入市区坐标”。
5. 流式中断与重试
前端流式读取时,如果网络断开或后端报错(比如OpenAI超时),用户看到的全文会中断。可以加个超时控制和错误提示。
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000); // 30秒超时
同时捕获异常后显示“请求超时,请重试”。
延伸思考:这个Demo值不值得继续做?
我个人的判断:值得做,但需要找到差异化切入点。
理由:
- 趋势明确——短途高性价比旅游需求在上升(不仅美国,中国国内“周末周边游”也是热点)。
- 现有OTA(携程、TripAdvisor等)的推荐算法多基于大数据协同过滤,缺少“个性化+实时预算+车程”的组合搜索。
- 成本极低:用GPT-4o-mini可以替代大量的规则引擎,快速覆盖数万种兴趣组合。
但风险也明显:
- Google Maps API和LLM调用成本加起来,如果用户量大后可能不划算。后期可以考虑自建推荐模型(轻量BERT)减少LLM调用量。
- 用户对“AI推荐”的信任度不如人工攻略,需要加入用户评价或真实照片。
如果现在要做,我建议先做一个微信小程序或Telegram Bot,降低开发成本,快速验证“推荐准确性”这个核心指标。等日活过千再考虑做App。
总结
原文讲的是#高成本旅游#导致短途旅行兴起,我从开发者视角做了一款AI短途旅行推荐器。全文代码可运行,技术栈简单,部署成本低。
你学到的:
- 如何用Google Places API + OpenAI流式输出构建一个生成型推荐应用。
- 前端流式渲染的Fetch API实现。
- 部署和成本控制的坑。
下次遇到类似的“生活方式变化”新闻,试着想:这个变化有没有一个可以用AI解决的交互场景? 如果有,24小时能不能写一个Demo跑起来?能的话,你就走在了快速迭代的产品路上。

完整代码已开源:https://github.com/yellowdew/local-trip-recommender 欢迎顺手点星。有什么问题评论区见。