1. 项目概述:RouteMind 到底要解决什么问题
1.1 核心需求解析
先聊聊这个项目的起点。RouteMind 是一款面向网页端的 AI 行程规划助手,核心目标是用自然语言对话的方式,帮用户在几分钟内生成一份可执行的行程方案。传统行程规划工具的痛点很明显:用户要手动切换地图、攻略、订票平台,反复比对时间、距离、预算,一趟旅行光做计划就能耗掉大半天。RouteMind 想做的事情很简单——让用户像跟朋友聊天一样说出需求,剩下的路线、时间、预算、备选方案全部交给 AI 去算。
这个“网页端”的定位不是随手选的。我调研过移动端和小程序端的开发成本与分发逻辑,网页端有三大优势:一是无需安装,用户从浏览器点开即用,传播链路最短;二是跨平台,Windows、macOS、Linux 甚至平板都能跑同一套代码;三是迭代速度,网页端发版不需要过应用商店审核,出问题随时回滚。对于一款需要频繁调整 AI 交互逻辑的工具型产品,网页端是验证核心价值的最快路径。
1.2 目标用户与典型使用场景
RouteMind 面向三类用户:自由行游客、商务出差人士、以及本地生活探索者。自由行游客的需求是“把攻略变成路线”,往往拿着一堆收藏笔记却排不出顺序;商务人士需要的是“会议间隙的精确时间窗”,对地铁换乘、步行距离、预留缓冲时间有硬性要求;本地生活探索者则更随性,可能只说了“周末想找个能看书又能喝咖啡的地方”,AI 也能给出合理建议。
我拆解了这三个群体后发现,他们共同的底层需求不是“生成一个路线”,而是“消除决策焦虑”。行程规划本质是一个多约束条件下的组合优化问题:景点开放时间、交通衔接、用餐时段、体力消耗、天气变化,变量太多,人脑处理不过来。RouteMind 的价值不是替用户做决定,而是把约束条件摊开、算出可行解,让用户只需要做“选 A 还是选 B”这种轻量决策。
2. 技术架构与方案选型思路
2.1 为什么选纯前端 + AI API 的轻架构
RouteMind 的技术栈选择遵循一个原则:能用现成的就不自己造,能跑在浏览器里的就不上服务器。所以整体架构是纯前端 SPA(React + TypeScript)加后端 AI 代理层,AI 能力通过大模型 API 提供,地图数据和 POI(兴趣点)信息通过开放平台 API 拉取。
这里有一个关键决策:为什么 AI 调用不直接从浏览器发?
原因是安全与成本控制。大模型 API 的密钥一旦暴露在浏览器端,等于把钱包交给别人。我在第一版确实试过直连,前端打包后密钥被扒了个精光,当天就被盗刷了几十块钱。后来改成代理层方案——前端只发请求到自己的轻量后端(一个 Node.js 服务),后端持有 API 密钥,做请求转发、结果缓存和限流。这个代理层同时承担了“格式化 Prompt”和“解析模型输出”的职责,前端永远只拿到干净的 JSON 结构。
2.2 从零搭建的最小可行架构(MVDemo)
如果你也想做类似的 AI 应用,可以参考这个最小架构,四层就够:
- 前端层:React + Vite,负责聊天界面、路线卡片渲染、地图交互;
- 代理层:Node.js + Express,两个核心接口
/api/chat和/api/route,前者处理对话补全,后者处理行程生成; - 数据层:用 Redis 做会话状态缓存,存
session_id -> 用户上下文,设置 24 小时过期; - 模型层:大模型 API(对话生成路线)+ 地图服务 API(算距离、算时长、取 POI)。
这套架构跑通的时间大概三天。第一天搭前端骨架和聊天 UI,第二天写代理层的 Prompt 工程和 JSON 输出解析,第三天接地图 API 串起端到端流程。最大的坑不在技术,而在“AI 输出格式不稳定”——模型经常给出结构完整的文本但稍微不合法,比如多一个尾逗号、把数字字段写成字符串,所以解析层必须做容错,不能直接用JSON.parse一把梭。
2.3 为什么用 React 而不是 Vue 或其他框架
选 React 不纯是个人偏好,有实际考量。第一,React 生态对“文档协作类应用”的支持更成熟,行程规划界面本质上是一个不断追加、编辑、重新排序的列表状态机,React 的不可变数据流在这种场景下更容易保证状态一致;第二,团队里现有成员对 React Hooks 的熟练度最高,减少学习成本;第三,地图库和 AI 相关的第三方组件(比如流式输出渲染、Markdown 渲染)在 React 生态里的维护活跃度更高。
如果你是单人开发,选 Vue 也完全没问题,这个层面的选择不影响核心逻辑。真正影响体验的是你如何处理“AI 流式回复”和“地图联动刷新”这两个环节——前者决定用户等待时的体感,后者决定路线调整时的流畅度。
3. 核心功能拆解与实现细节
3.1 自然语言解析:从用户闲聊到结构化行程
RouteMind 的第一步能力是把“我周五下午从杭州东出发,想先逛西湖再去灵隐寺,晚上住市区,预算 500”这样的口语输入转换成结构化行程参数。这里我没有选择训练一个专门的小模型,而是靠 Prompt 约束大模型输出 JSON,再在后端做一层校验和补全。
Prompt 核心模板大概是这个思路:
你是一个行程规划助手。请根据用户的出行需求,输出严格的 JSON 格式结果,包含: - start_point(出发地) - start_time(出发时间,ISO 格式) - destinations(目的地数组,每项包含 name、lat、lng、suggested_duration) - constraints(约束条件,包括预算、交通偏好等) - output_format 必须为 json 规则: 1. 如果用户没有提供的信息,用 null 填充,不要猜。 2. 如果用户提到“下午”“傍晚”等模糊时间,推算具体时间范围并填入 start_time_range。 3. 最多输出 8 个目的地,超出时按用户优先级排序。这里有个很实际的问题:模型输出有时候会“自作主张”。用户没说预算,它给填了个“3000 元”进去,这会导致后续的路线推荐全部跑偏。所以 Proxy 层必须做“幻觉校验”——对模型返回的 JSON 里的每个字段,检查是否在用户原话中被提及,如果是模型补的但用户没说过,统一置为 null。这个校验逻辑用正则加关键词匹配就能覆盖大部分情况。
3.2 行程生成引擎:多约束条件下的路线排序
拿到结构化的目的地列表后,RouteMind 要解决一个真正的算法问题:怎么排游览顺序最合理?这个问题的输入包括各 POI 的地理坐标、预估游玩时长、景点之间的通勤时长(步行或公共交通)、每个景点的开放时间窗口,以及用户设置的总预算和节奏偏好(紧凑/宽松)。
我第一版想得很天真,让 AI 直接输出“第几天去哪去哪”,结果经常出现两个问题:一是回头路太多,上午在城东下午蹦到城西又折回城东;二是时间冲突,明明景点 A 下午四点半就关门还排在五点。后来我改成“两阶段”:第一阶段先用地图 API 把 POI 两两之间的通勤时长全部算出来,生成一个距离矩阵;第二阶段给大模型提供一个“行程模板骨架”——从距离矩阵里挑选最优路径,用户地点确定在公司附近,中午吃饭不超过 60 分钟,下午要预留 15 分钟 buffer。这个模板骨架本质上把组合优化问题变成了条件过滤。
具体排法是这样的:采用贪心加局部回溯,从起点开始,每步从剩余 POI 里选“当前时间可达 + 距离最近 + 开放时间满足”的那个;如果某一步发现可选集合为空,回溯到上一个节点换一个选择。这种算法在 POI 数量小于 10 时效率极高,基本毫秒级出结果。数量再多就要上类似 TSP(旅行商问题)的近似算法了,但现实里用户一次旅行通常不会规划十几个点,贪心加回溯已经够用。
3.3 地图联动与实时调整:让路线看得见改得动
行程生成完成后,前端渲染一个时间线卡片列表和一张地图。地图上按顺序连线标记 POI,点击某个卡片时地图对应点闪烁高亮,拖动卡片调整顺序时地图上连线同步更新。这里我用了 MapLibre GL 作为地图渲染引擎,原因之前提过——Leaflet 的精准纠偏能力某种程度上也体现在这里,交互逻辑简单但反馈直接。还有个细节:地图 POI 卡片的赞和踩、编辑和删除,全部在卡片上完成,地图只是展示,不承载编辑功能,避免两套交互规则打架。这个决定帮我砍掉了大量地图 API 调试时间,地图组件稳定到几乎没有 Bug。
调整顺序这个交互,技术上比听起来复杂一点。卡片拖拽排序我用的是 react-beautiful-dnd,拖动结束后拿到新的顺序数组,要重新请求一次地图 API 计算每段通勤时长,再更新总时长和总预算的头部汇总。这里有一个性能优化点:拖拽过程中不要触发 API 请求,只有onDrop之后才发一次,并且用debounce500 毫秒防抖。否则用户拖一下发一次请求,地图接口的配额会很快被打满,而且响应顺序错乱会导致路线连线乱跳。
3.4 预算与时间双引擎校验
RouteMind 的一个重要功能是“预算超不超、时间够不够”的实时提示。每个 POI 预估游玩时长从大模型生成,但预算怎么估?交通费用靠地图 API 回传的出行方式均价估算,门票费用不是我编的——我接了一个景区门票数据服务,标好每个 POI 的门票参考价;餐饮费用则按用户选定的消费档次乘以餐标。所有费用在行程卡片底部单独显示,分交通、门票、餐饮、住宿四类。
行程头部是一个概览条:总天数、每天几点开始几点结束、每天步行距离预估、总预算、剩余可调预算。每当用户增删或调整顺序,概览条都会实时重算。这个重算在前端本地完成,不开新的 AI 请求,因此要有一个“费用模型”在前端同步一份 POI 基础数据。这样一来,用户看到的开销预估只是“粗粒度参考”,真正下单前我会在界面上明确标注“费用为预估值,请以实际消费为准”。
这里是我踩过的一个比较大的坑:最初我把预算计算也交给大模型,让它根据经验“猜”大概花费,结果同一个路线同一家餐厅,两次生成的预算能差一倍。后来改成“前端的费用模型 + 模型只做语义注释”,才算稳定下来。稳定之后用户体验完全不同——用户不是需要 AI 替他决定一顿饭多少钱,而是需要 AI 提供一个可置信的分项数字,让他自己判断。
4. 实际开发日志与踩坑记录
4.1 开发第 1 天:从聊天页面到第一个 AI 回复
开发第一天主要搭 React 前端框架,核心是聊天界面。聊天 UI 看起来简单,其实有几个细节特别影响体验:一是 AI 回复时打字机式的流式渲染,配合逐字出现的效果比一次性吐出整段文字更让人有“在对话”的实感;二是用户消息和 AI 消息的气泡区隔要清晰,但不要过度“对话框化”,否则多轮上下文一长页面就不像工具而像聊天软件。
流式输出我用的是 WebSocket 推送代理层,模型返回 token 逐个推给前端。这里第一版犯了不设缓冲的错——每收到一个 token 就触发一次 React 状态更新,结果页面频繁重渲染,稍微长一点的回复就直接卡成 PPT。后来改成在组件内部攒 300 毫秒的 token 批量再提交一次状态更新,体感顺畅很多。如果你也做类似功能,强烈建议加上这个小缓冲。
4.2 开发第 2 天:JSON 输出容错和幻觉校验
第二天最耗时间,但不是写功能,是处理大模型的“花式输出”。我打印了 100 次模型返回,统计了一下问题类型:未闭合大括号约占总数的 8%、数字字段带单位字符串约 6%、POI 名称自带“景区”“公园”后缀导致地图匹配失败约 15%、凭空捏造开放时间约 4%。
针对这些问题,我在代理层写了一个“清洗管道”:
function sanitizeModelOutput(raw: string): RoutePlan { // 1. 用正则抽取大括号包裹的 JSON 片段,去掉前后解释性文字 const jsonMatch = raw.match(/\{[\s\S]*\}/); if (!jsonMatch) throw new Error('No JSON found in model output'); // 2. 尝试解析,失败时捕获具体报错并做修复 let parsed: any; try { parsed = JSON.parse(jsonMatch[0]); } catch (e) { parsed = repairBrokenJson(jsonMatch[0]); // 修复未闭合括号、多余逗号等 } // 3. 字段级清洗 const cleaned = { ...parsed, // 数字字段统一转 number,带单位就剥离 budget: parseNumeric(parsed.budget), // POI 名称去掉冗余后缀,便于地图匹配 destinations: (parsed.destinations || []).map(cleanDestination), }; return validateAndFill(cleaned); // 检查关键字段是否齐全,缺失填 null }这个清洗管道在后续接入其他 AI 功能时也复用了,它让我意识到一件事:大模型输出的稳定性不是靠提升模型能力解决的,而是靠工程手段兜底。不管用哪家模型,输出格式都不可能 100% 稳定,提前设计容错层是 AI 应用开发的基本功。
4.3 开发第 3 天:地图 API 配额与费用估算
第三方 API 的配额是最容易被低估的坑。地图服务免费版额度看着不少,但你算一笔账:一次行程规划,光计算距离矩阵就要对 POI 两两配对发请求,5 个点就要 20 次距离计算,每天都搞几个名额,月初就弹配额不足。
我的应对措施是加缓存。以出发地 + 目的地 + 出行方式为 key 把距离时长结果缓存进 Redis,设置 7 天过期。这么一改,同一个城市内高频出现的地点点对直接命中缓存,地图 API 调用量直接下降七成。另外在代理层加了“请求合并”:同一次规划里所有的 POI 距离计算合成一个批量接口调用,而不是循环发单点请求。不同地图 API 的批量能力不同,但通常都支持一次请求带多个 destinations,只是要仔细阅读文档,不要想当然。
费用估算这一块前面说过,全部从前端本地模型计算。还有一个经验值得分享:门票数据不要自己在代码里维护,改成发布时拉一次远程 JSON 配置,因为门票调价是常态,写死在代码里会过时。这个配置我放在了对象存储上,改价格不需要重新发版,刷新页面即生效。
4.4 开发第 4 天:网页端与小程序同步登录
用户反馈里提得很多的一个需求是“网页端做了一半,想在小程序里继续”,这就是网页端同步小程序的登录场景。实现方案不复杂,核心是统一用户身份。RouteMind 使用微信扫码登录网页端,拿到openid作为用户唯一标识,后端签一个自己的 token(JWT),存进 Cookie 或者 LocalStorage。
小程序端通过wx.login拿到临时 code,后端用这个 code 调微信接口换openid,然后签同样的 JWT。两端的 JWT 是同一签发体系,所以会话天然互通。用户在小程序里收藏的行程,网页端登录后自动同步。这里唯一要小心的是 token 过期策略,我用的是双 token 方案——短期 access token(2 小时)加长期 refresh token(7 天),刷新接口自动续期,避免用户频繁重新扫码。
如果单纯做网页端不用小程序,这段可以不看。但如果未来有移动端计划,建议提前把用户体系做成通行证式设计,而不是等需要的时候再改,不然后面要迁移数据很痛苦。
4.5 开发第 5 天:收尾、部署与性能优化
最后一天做的是收尾工作。性能方面做了一次 Chrome Performance 面板分析,发现主要瓶颈在大模型回复渲染:长行程方案生成出来的 JSON 被渲染成卡片列表,一次性创建几百个 DOM 节点,在低端安卓机上明显卡顿。优化方案是列表虚拟化,只用 react-window 渲染可视区域内的行程卡片,滚动时动态替换。首屏渲染时间从 2200ms 降到 700ms,效果显著。
部署方面我选的是 Cloudflare Pages 加 Cloudflare Workers。Pages 托管前端静态资源,Workers 跑代理层,好处是边缘节点就近响应,国内访问速度也还不错。域名加 SSL 证书都是平台自动处理,省了很多运维功夫。如果你有自己的云服务器,用 Nginx 反代 Node.js 服务也是标准做法。实际生产部署还做了一件事:所有 AI 请求在代理层加了简单的用户级限流,每个用户每分钟最多 10 次,超过直接返回 429 提示,防止有人用脚本刷接口。
5. 常见问题与排查技巧
5.1 AI 输出内容与用户需求不匹配怎么处理
这是使用中最常见的挫败点。用户说“我想逛西湖,但不想走路太多”,模型给了个全程步行 8 公里的方案。问题出在 Prompt 没有把“步行偏好”这个约束显式传给行程生成步骤。
排查思路是回溯 Prompt 链条:用户输入的偏好有没有被转换成语义标签(比如“不累”被解析成偏好transport_preference: taxi)?这个标签有没有进入行程生成的 Prompt 模板?如果没有进入,就是这个环节断了。我后来做了一个“偏好提取”的独立步骤:每次用户输入先过一遍小模型,把偏好抽成语义标签,再和目的地列表一起拼进行程生成模板。这一步把“用户口语”和“算法参数”解耦,后续改算法不影响偏好理解,改 Prompt 也不影响用户输入。
5.2 地图点位解析失败:口语名词和实际地标对不上
POI 名称匹配是另一个高频问题。模型输出的是“灵隐寺”,地图 API 能搜到;但模型输出“飞来峰景区入口”这种非标准名,地图 API 可能返回空。插件研发阶段我的解决方案是建立一层“别名映射”:遇到解析不到的点,走一次地名归一化服务,把玩家口中的简称、旧称、俗称映射回官方 POI 名称;映射不到就推荐用户手动在地图上选点。
但最终产品不能老让用户手动选点,所以我又加了一个“模型自查”环节:生成路线后,代理层把所有 POI 名称发给地图 API 做一次批量查询,有任何一个解析失败就回传给模型,让它换个说法重新生成。这个循环最多重试三次,三次都不中就明确提示用户手动指定。实际测试这个机制能把地图点位解析成功率从 78% 提高到 94%,剩下的 6% 再靠用户手动补。
5.3 大模型回答太发散,路线结构经常变形
有些时候模型不按预设的 JSON 结构走,直接输出一大段“贴心建议”,还要加粗加引用。这是 Prompt 约束不够强导致的。我调试后的经验是:除了在系统提示词里写“你必须输出 JSON 格式”,更需要把 JSON Schema 写在提示词里,同时给一个例子。
请严格按以下结构输出(可以没有注释,但键名必须完整): { "plan": [{ "day": 1, "spots": [{"name": "…", "arrive_time": "…", "depart_time": "…"}] }], "total_budget": 0 }模型对例子的模仿能力比对抽象描述的遵从度高得多。我试过各种表述,效果排序大概是:示例 > Schema > 自然语言指令。所以现在所有 AI 功能都标配 few-shot 示例,而且示例要精心设计成“最标准的一种情况”,不能示例本身就有歧义。
5.4 缓存策略引发的行程更新不及时
地图距离缓存 7 天过期,本来是为了省钱,结果带来了一个体验 Bug:路况变了,API 给的还是 3 天前的通勤时间。用户早上打开页面看预计 40 分钟能到,实际堵车堵了 1 小时,直接误了高铁。
我最后的方案是“分级缓存”:公共交通(地铁、公交)的时长可以缓存 7 天,因为班次表和运行时间相对稳定;驾车时长只缓存 2 小时,因为路况变化太快。打车和自驾的预算估算不确定性高,所以在界面上给驾车方案打上“实际耗时可能波动”的标注。如果你想省事,也可以缓存时间统一设短一点,代价就是 API 调用量和费用上升。这个平衡需要根据你的预算和用户群体自己拿捏。
6. 项目上线后的真实数据与迭代方向
6.1 上线初期的用户行为观察
RouteMind 上线两周后的数据里,有几个超出预期的发现。第一,用户平均每次会话产生的消息条数是 7.3 条,说明不是“问一句就走”,而是在反复调整方案;第二,最受欢迎的功能不是“智能推荐”,反而是“行程分享”——把规划好的行程生成一张带地图和时间线的长图分享给同行的人;第三,深夜时段(22 点到 1 点)的使用量占了全天近三成,旅游规划对很多人来说是睡前的“精神旅行”。
这三点在我原设计里权重都不高,但用户用脚投票了。于是我把“行程分享”提到了首屏工具栏,并且做了长图生成优化,从原来的截图拼接到用浏览器原生 API 渲染高清长图。这个改动带来约 12% 的次日留存提升,算是低成本高回报的功能迭代。
6.2 计划中的迭代方向:多人协作与多 Agent 分工
下一步开发计划有两个方向。第一个是多人协作行程编辑,几个朋友可以在同一个行程文档上同步添加地点、评论、投票,这本质上是一个轻量实时协同文档,可以用 WebSocket 加 CRDT 或者 Yjs 来实现。第二个是让不同 AI Agent 分工配合:一个 Agent 专注路线优化,一个 Agent 专注美食推荐,一个 Agent 专注天气预警,各自维护独立的上下文,再通过一个调度层合并结果。
多 Agent 协作这块,目前在 AI 原生应用圈子里讨论度很高,但在行程规划这种强约束场景里,单 Agent 容易把精力分散在“既要路线又要美食又要天气”的多目标上,效果确实不如分工。我初步设想是用户发一句需求,调度层拆成多个子任务分发给不同 Agent,等所有 Agent 返回后再合并成一份完整方案。这个方案技术上最棘手的地方在于 Agent 之间如何共享“当前路线”这个公共状态,我的思路是把公共状态抽成一份 JSON 放在内存里,各 Agent 只读写自己负责的字段,调度层做冲突仲裁。具体效果还在实验期,后面有结论了再单独写一篇。
6.3 长期思考:AI 行程规划的产品本质
做 RouteMind 这段时间,我最大的体会是:用户真正需要的不是一个“更聪明的推荐算法”,而是一个“可信赖的决策助理”。所谓可信赖,体现在几个细节上——预估时间要准,标注参考价要真,方案超预算要提前说,而不是等用户点进去才发现。这些需求没有一个是靠模型聪明就能解决的,全是工程上的数据质量和透明度问题。
所以这个项目的开发日志,与其说是记录“用 AI 做了一个网页应用”,不如说是记录“怎么把 AI 的不确定性,用工程手段装进一个需要确定性结果的场景里”。行程规划恰好是这种场景的典型代表:结果可以多样,但每个结果里的时间、距离、费用必须可预期。这个思路放在其他 AI 应用(比如健身计划、学习规划、装修预算)里同样适用。