Repository navigation
【2026飞猪AI旅行创新大赛】童行 · 带娃旅游规划器 #202
Description
参赛项目名称
童行 · 带娃旅游规划器(仓库:kidtrip-planner)
和 AI 聊几句,它去飞猪查真实的景点、酒店和城际车次,然后排出一份带娃真能跑得完的行程。
👉 在线试用:http://114.132.233.6/(一次生成 30~60 秒,因为它真的在查)
团队 / 作者与奖品领取人
- 团队名称:童行
- 主要提交人 GitHub ID(仅 1 位,必填):heyan0805
- 其他成员(可选,仅用于署名):无(个人参赛)
旅行场景与目标用户
场景:带娃出行的行前行程规划。不是"帮我推荐去哪儿玩",而是用户已经把目的地和日期定了,需要有人把"每天几点去哪、两个点之间怎么走、住哪、回程车几点"排出来。
目标用户:带 1-2 个孩子的家庭,孩子集中在 3-10 岁,一趟 2-7天,以周边游和国内中短途为主;出行时通常推着婴儿车、背着一天的口粮,对"临时改主意"的容错很低。](url)
用户真正的痛点不是订票,是"排"。 带娃出门贵的不是机票,是时间:每天去哪儿、几点去、两个点之间多远、孩子会不会走到崩溃、酒店要不要换、回程车几点——这些排起来比订票费神得多。而市面上的 AI 旅行助手大多在编:编一个不存在的营业时间,编一句"步行可达"其实隔着 40 公里。
带娃场景对这类错误的容忍度极低:一个排错的下午,就是孩子在陌生城市的街头哭。所以这个场景有三条硬约束,直接决定了产品必须长成什么样:
- 每天能跑的景点有限 —— 3 个是带娃的现实上限,而同行有 3 岁以下幼儿时压到 2 个;
- 必须考虑到达时刻 —— 去程 14:16 才到,上午 09:30 就不该排景点;
- 距离和换乘是硬门槛,不是"建议" —— 推着车走两公里和空手走两公里不是一件事。
我做了什么
一句话:对话攒需求 → 真实数据检索 → 规则层校验 → 模型排程 → 事实回填,最后交出一份每个数字都能核对的行程。
核心功能
- 对话式需求采集:不填表。用自然语言聊出出发地、目的地、天数、日期、孩子年龄、是否带推车等必填项,缺什么追问什么,已确认的不会被覆盖。
- 支持多个孩子:收的是每个孩子的年龄(不是"几个孩子"),排程按最小的那个算体力上限——"5 岁 + 10 岁"不是"7.5 岁",两个孩子也不会变成"能多排两个景点"。
- 画像(推论层):从用户的表达里推出"怕走路 / 怕人多 / 需要午休 / 预算敏感"这类偏好,每一条都必须附上用户自己的原话作为证据,在界面上与推论并列显示。
- 真实数据检索:经飞猪 FlyAI CLI 查景点(35 个类别)、酒店(按星级档)、城际班次(火车 / 机票),带真实的名称、价格、经纬度、图片和购票链接。
- 行程组装:每天输出主题、景点、城际交通、备选、酒店、酒店备选、以及一句"为什么这么排"。呈现上做成一天一个节点的时间轴,景点可"换一个(还剩 N 个)"。
- 可溯源:页面底部"展开溯源"能看到这一趟真实发出的查询命令,以及被规则层筛掉的候选。成本是可见的。
创新点(全部有自动化测试钉住,不是设计口号)
① 模型不做事实,只做选择与表达。
名称、价格、坐标、图片、购票链接全部由后端从飞猪返回值回填;模型拿到的是一份带 poiId 的候选清单,它只能从里面挑,不能凭空造。模型只出现在三个位置:画像抽取、检索计划、行程组装。这一条把"幻觉"从"可能发生"变成结构上不可能——它没有可编的字段。
② 事实与推论分两份数据,合并规则故意相反。
TravelParams(用户说过的,事实)与 Persona(模型推的,推论)永不混存。前者"没答就保留旧值",后者"同 key 覆盖、没提保留"。推论入档要过两道闸门:维度闭集(只允许 4 个 key)+ evidence 逐字回查(证据必须在用户真实消息里出现过,防止模型自说自话补证据)。为什么"孩子的兴趣"不进这个闭集:兴趣是多值的,而闭集是单值结构——设计新维度前先问"它一次会不会取多个值",会就不该进来。
③ 节奏由"生理事实"决定,不由"偏好"决定。
每天排几个点看两个变量:pace(用户选的快/慢,属偏好)与最小孩子年龄(属生理事实)。同行有 3 岁及以下时,即使选了"快",每天也压到 2 个点——偏好可以让行程放松,但不能让孩子走不动。同理,人再多也不加景点:多一个大人在交通上省的是钱,不是时间。
④ 兜底三级,且"不知道"≠"没有"。
本地重算(免费)> 再问模型 > 报错。查询失败与查询为空严格区分:某天航班这次没查成,界面会明说"这次没查成,不代表这条线没有合适的航班";而上游明确说没有,就原样转述、不加这句。把不确定性如实交还用户,而不是沉默或假装没这回事。
⑤ 每天那句"为什么这么排"是模板拼的,不是模型写的。
里面的数字(当天跨度、空档时长、车次时刻)必须来自本地已算出的事实。凡是能被用户核对的句子,就不交给模型自由发挥——与"价格由后端回填"是同一条原则。这一条还挡住了最隐蔽一种错:用户说过"体力很好",这句话绝不能被引用成"今天排得轻松"的理由。
⑥ 犯错时人一定看得见。
模型偶尔仍会排出 24 公里的跨度。兜底校验不假装没发生,而是在界面上写一句人话告诉用户。不指望它不犯错,但犯错时必须可见。
⑦ 限流是设计约束,不是事后补丁。
飞猪 CLI 是外部限流接口(并发 3~6 个即 429,密集串行会 451 风控),因此查询做了串行锁 + 间隔 + 缓存 TTL + 退避,并优先用"宽查"一次覆盖多个类别。产品允许慢,但不允许"时好时坏"。
工程质量
- 后端 378 个单元测试 / 26 个测试类全绿;前端 mock 全链路验收 49 项、端到端(真实模型 + 真实上游)30 项通过。
- 关键断言做过注入验证:把被测代码故意改错,确认测试真的会失败,再恢复——避免"测试写了但从来没红过"的假绿。这一轮多孩子功能就做了 2 组(把
≤3 岁改成≤0、把"整体替换"改成"永不替换",都如期变红)。 - 已部署上线并端到端实测:一次真实生成 34~45 秒返回完整行程(真实景点、图片、坐标、购票链接)。架构为 nginx 反代 + systemd 守护,后端只监听
127.0.0.1,不直接暴露公网;配置与密钥全部走环境变量。
百炼使用说明(必填)
-
百炼能力 / 模型:通义千问
qwen3.8-flash(百炼工作区专属网关,北京地域)。只使用 Chat Completions(文本对话 + JSON 结构化输出)这一项能力,未使用向量检索、微调、Agent 编排等其它能力。 -
调用方式:百炼 API(OpenAI 兼容模式)。
基址https://ws-<workspace-id>.cn-beijing.maas.aliyuncs.com/compatible-mode/v1,请求路径/chat/completions;客户端为 LangChain4j 1.18 的OpenAiChatModel(换 baseUrl + apiKey 即可对接,不绑定厂商 SDK)。
未使用 DashScope SDK、百炼 CLI(bl)、OpenWork。 -
使用环节与输入输出:完全位于运行时链路(不在开发链路;代码与 prompt 都是自己写的,模型只在服务运行时被调用)。一次"生成行程"共 3 个环节:
环节 输入 输出 调用次数 对话攒需求( ChatComposer)对话消息 + 已确认参数 + 已有画像 一句回复 + 更新后的参数(含 childAges数组)+ 新增/修正的画像条目每轮对话 1 次 检索计划( RetrievalPlanner)已齐参数 + 今天的日期 + 35 个 POI 类别清单 + 城市名白名单 结构化 JSON 检索骨架(城市、日期区间、各类别查询词、酒店星级档) 1 次(JSON 解析失败带提示重试 1 次) 行程组装( PlanComposer)参数 + 画像 + 真实候选清单(景点/酒店/车次,含 poiId、价格、坐标、发到时刻)+ 规则约束(点数上限由节奏与最小孩子年龄共同决定) 每天的主题、所选 poiId 及顺序、时段、注意事项 1~2 次(校验不过则回退重排) 即:总调用次数 = 对话轮数 + 2~3 次。全部调用走同一条 OpenAI 兼容通道,模型只负责"选哪个、怎么说",不产出任何事实字段。模型输出还会经过一层结构强校验:字段名逐字对齐、编号对不上直接丢弃并记入"被筛掉的候选"、非法 JSON 带提示重试。
-
Skill 名称:无。未使用百炼 Skill / 插件,全部为后端自建 prompt + 输出结构强校验。
-
其他工具 / 技术栈:
- 飞猪 FlyAI CLI(
@fly-ai/flyai-cli):唯一的真实数据源,取 POI、酒店、城际班次;串行调用 + 缓存 + 退避以规避限流。 - 后端:Spring Boot 4.1 + Java 21 + LangChain4j 1.18(Jackson 3)。
- 前端:React 19 + TypeScript + Vite 8 + Tailwind 3。
- 部署:腾讯云轻量服务器(4C4G),nginx 反代静态页与
/api,systemd 守护后端(开机自启、崩溃自动拉起),配置与密钥全部走环境变量,与代码分离。
- 飞猪 FlyAI CLI(
效果展示
- 首页
- 聊天界面
- 生成的行程tips
- 行程时间线
- 可定火车,机票,酒店
- 可查看备选景点
- 景点图片可查看(景点图片数据来自飞猪)
项目链接与复现方式
| 入口 | 地址 | 说明 |
|---|---|---|
| GitHub 仓库 | https://github.com/heyan0805/kidtrip-planner | public,MIT。仓库里没有任何真实密钥,所有密钥位置都是 ${...} 占位符 |
| 在线 Demo | http://114.132.233.6/ | 打开就能用,不用注册、不用装环境。同一台 4C4G 轻量服务器上跑 nginx + 后端 |
| 演示视频 | 暂无 |
最快体验(不用装任何东西,约 1 分钟)
打开 http://114.132.233.6/ → 输入「想带孩子去珠海玩 3 天,孩子 6 岁」→ 补答它追问的出发城市和日期 → 点生成。
一次 30~60 秒(它不是装慢,是真在查飞猪的景点、酒店和车次)。
几个值得留意的地方:每天的时间轴(点位、耗时、距离);景点卡右上角「换一个(还剩 N 个)」;点了缩略图能看大图,一天有多个场景时可左右翻;页面底部「展开溯源」能看到这一趟真实发出的查询命令和被规则筛掉的候选。
只跑前端(不装 Java、不配密钥,约 2 分钟)
git clone https://github.com/heyan0805/kidtrip-planner.git
cd kidtrip-planner/frontend
npm install
VITE_USE_MOCK=true npm run dev # → http://localhost:5173Windows PowerShell 里第一句换成:$env:VITE_USE_MOCK="true"; npm run dev
假数据能跑通全流程。降级刻意做得很显眼:界面上有黄色横幅、行程里的名字都带「(假数据)」——静默降级会让人拿着一份编出来的行程去订酒店。
完整复现(前后端 + 真实数据)
前置:Node ≥ 20.19 或 ≥ 22.12(Vite 8 硬要求)· JDK 21 · Maven 3.9+ · npm i -g @fly-ai/flyai-cli · 一份 OpenAI 兼容的模型 Key(本项目实测百炼 · 通义千问)· 黑客松发放的飞猪 FlyAI 凭据(~/.flyai/config.json 与 device-id,按 FlyAI CLI 的说明放置)。
# 1) 环境变量(都不是可选项:仓库里故意不留默认值)
export KIDTRIP_LLM_API_KEY=sk-xxxx
export KIDTRIP_LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
export KIDTRIP_LLM_MODEL_NAME=qwen-plus
export KIDTRIP_LLM_ENABLE_THINKING=false # 用千问 3.x 必填,否则非流式请求会被网关掐断
export FLYAI_CLI_PATH="$(npm root -g)/@fly-ai/flyai-cli/dist/flyai-bundle.cjs"
# 2) 后端 → http://localhost:8003(首次启动会联网拉 Maven 依赖)
cd backend && mvn spring-boot:run
# 3) 前端 → http://localhost:5173(另开一个终端)
cd frontend && npm install && npm run dev启动自检:curl http://localhost:8003/api/health 应返回 {"code":0,"message":"ok",...}。
缺密钥时后端会在启动阶段直接报「缺少配置 kidtrip.llm.api-key」,不会等到你点生成才失败——失败趁早,且带着"该怎么修"一起失败。
跑测试:cd backend && mvn test → 378 个用例全绿,不需要任何密钥(模型用 FakeChatModel 替身,测试不碰网络)。
没有飞猪凭据怎么办:后端能起来,但检索层查不到数据,生成会如实报"这次没查成"(不做假数据冒充)。这种情况下建议直接用上面的 mock 模式体验界面与交互。
关于密钥(模板要求,明确回应)
- 仓库里 没有 提交任何 API Key、密码、token、私钥;模型密钥、飞猪凭据全部走环境变量注入。
- 唯一需要说明的是飞猪凭据不能共享:它绑设备标识,且上游对访问来源有风控(机房 IP 实测可用,但会波动)。所以在线 Demo 才是最省事的体验入口。
踩坑记录(可选)
千问 3.x 默认开启 thinking。思考期间链路上零字节流动,而百炼专属网关会把长时间无字节的非流式请求当空闲连接掐掉(客户端只看到 EOF reached while reading,看起来像网络故障)。因此请求体显式带 enable_thinking: false。
**奖品说明:**每个团队最多发放 1 份奖品。同一团队即使提交多个参赛作品,也只登记 1 位奖品领取人;其他团队成员可以共同署名,但不重复发放奖品。
Activity