先看效果:输入预算和兴趣,AI给你1小时车程内的目的地

阵亡将士纪念日旅游人数下降,不是因为不想出去,而是因为机票酒店太贵。Nerwallet旅游作家Sally French在CBS News里说得直白——人们开始搜索离家1-2小时的短途目的地,省下交通和住宿的大头。

这对开发者是个机会:旅游产品可以降维打本地微度假,而AI能帮用户做“个性化的短途决策”。

所以,我花了一个周末写了个Demo:本地旅行推荐器(Local Trip Recommender)。输入你的位置、预算、兴趣(比如“徒步”、“亲子”、“美食”),AI会调用地图API找到周边车程1小时内的地点,然后结合你的偏好生成一份带预算、路线、小贴士的旅行计划。

Demo截图:输入框和推荐结果卡片

演示地址(已部署在Vercel+Render,安全无毒):[暂不公开,可自建]

读完本文你得到的

  1. 一个可直接运行的短途旅行推荐器代码(React前端 + Node.js后端 + OpenAI API + Google Maps API)。
  2. 从技术选型到上线的完整链路,尤其是流式输出和API计费控制的实战经验。
  3. 对“低成本旅游+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端点。

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
// 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直接推送给前端,前端用EventSourcefetch + ReadableStream接收。
  • 预算budget在这里只是传递给LLM做参考,没有真正做费用计算。后续可以接入Uber/滴滴API估算交通费。

2. 前端:流式接收并渲染AI推荐

React组件精简如下(/frontend/src/App.jsx):

jsx
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 58 59 60 61 62 63 64 65 66 67 68 69 70 71
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_KEYGOOGLE_PLACES_API_KEY

注意:Render免费版15分钟无请求会休眠,第一次请求会冷启动慢5-10秒。对于演示来说可以接受,生产环境建议换成Railway或Fly.io带always-on的付费方案。


项目结构和配置

text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
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配置需加代理(本地开发):

js
1 2 3 4 5 6 7 8 9
// 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查询节点。

javascript
1
const overpassQuery = `[out:json];node["tourism"="attraction"](around:50000,${lat},${lon});out 5;`;

不需要Key,防限流控制好即可。

4. 用户体验:空结果处理

如果用户输入的经纬度附近没有景点(比如农村),Google Places返回空数组。LLM prompt里没有传入POI,模型可能编造景点。一定做兜底逻辑:如果places不足3个,返回提示“附近暂无景点,试试扩大范围或输入市区坐标”。

5. 流式中断与重试

前端流式读取时,如果网络断开或后端报错(比如OpenAI超时),用户看到的全文会中断。可以加个超时控制和错误提示。

javascript
1 2
const controller = new AbortController();
setTimeout(() => controller.abort(), 30000); // 30秒超时

同时捕获异常后显示“请求超时,请重试”。


延伸思考:这个Demo值不值得继续做?

我个人的判断:值得做,但需要找到差异化切入点

理由:

  1. 趋势明确——短途高性价比旅游需求在上升(不仅美国,中国国内“周末周边游”也是热点)。
  2. 现有OTA(携程、TripAdvisor等)的推荐算法多基于大数据协同过滤,缺少“个性化+实时预算+车程”的组合搜索。
  3. 成本极低:用GPT-4o-mini可以替代大量的规则引擎,快速覆盖数万种兴趣组合。

但风险也明显:

  • Google Maps API和LLM调用成本加起来,如果用户量大后可能不划算。后期可以考虑自建推荐模型(轻量BERT)减少LLM调用量。
  • 用户对“AI推荐”的信任度不如人工攻略,需要加入用户评价或真实照片。

如果现在要做,我建议先做一个微信小程序Telegram Bot,降低开发成本,快速验证“推荐准确性”这个核心指标。等日活过千再考虑做App。


总结

原文讲的是#高成本旅游#导致短途旅行兴起,我从开发者视角做了一款AI短途旅行推荐器。全文代码可运行,技术栈简单,部署成本低。

你学到的:

  • 如何用Google Places API + OpenAI流式输出构建一个生成型推荐应用。
  • 前端流式渲染的Fetch API实现。
  • 部署和成本控制的坑。

下次遇到类似的“生活方式变化”新闻,试着想:这个变化有没有一个可以用AI解决的交互场景? 如果有,24小时能不能写一个Demo跑起来?能的话,你就走在了快速迭代的产品路上。

GitHub repository tree structure showing frontend and backend folders

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