大模型驱动的旅行攻略智能助手:架构设计与工程实践
这篇文案写的是一个已经落地到第5个迭代版本、并且面向团队和用户做过完整效果演示的智能助手项目。如果你也正在做类似的大模型应用产品或者准备给自己的项目做一次像样的效果演示这篇应该能提供不少参考。1. 项目定位与整体设计思路1.1 为什么做旅行攻略智能助手而不是继续做攻略网站我这边连续做了四个版本的内容聚合类工具说实话越做越觉得传统攻略产品的体验是断的。用户要查景点、排路线、比交通、算预算往往要在五六个App和几十篇笔记之间来回横跳最后还要自己手工在表格里拼一个行程。这个项目就是冲着这个痛点去的用大语言模型把查资料、做决策、排行程这三件事合并成一次对话完成。项目名字里带着智能助手而不是攻略生成器是有意的。生成器更像一次性输出用户拿到的是一份静态文档助手强调的可交互、可调整、可持续对话。第5版的重点除了把生成质量做稳还在交互上下了功夫用户说一句预算不够系统要能实时压缩行程开支而不是重新生成一份。目标用户也做了收敛核心是两类人一类是没时间做详细攻略但愿意为体验付费的上班族第二类是第一次去某个城市、对当地完全没有概念的自由行新手。这两类人对效率和安全感的需求是一致的项目所有功能设计都围绕这两点展开。演示时我用了三个场景来证明这个价值一句话生成完整行程、实时信息变化后的动态调整、带老人小孩这种特殊需求的约束处理。三个场景分别对应效率、可靠性和适应性这也是整个项目最核心的三个能力。1.2 系统整体架构与模块拆解这个项目的架构分四层每层边界划得比较清晰。最上层是用户交互层负责对话输入、行程展示、地图可视化和结果反馈下面是服务层承担对话管理、行程生成、偏好分析这些业务逻辑再往下是大模型接入层包含Prompt管理、输出校验、模型路由最底层是数据层主要放POI知识库、用户历史会话、天气交通等实时数据缓存。我当时画架构图的时候特意在输出校验这个模块上多花了一倍时间标注因为实践证明这里才是最容易翻车的地方。如果你看过这个项目的演示视频会发现从点击生成到看到行程中间大概有3到5秒的等待实际上这段时间里数据已经跑了一个完整的生成-校验-修复循环。各层职责大致如下表层级核心组件主要职责交互层Streamlit Leaflet地图对话窗口、行程卡片、地图轨迹、拖拽调整服务层FastAPI Redis会话管理、行程调度、预算计算、缓存模型层LLM调用 Prompt模板 格式校验器生成结构化工单、修复JSON解析异常数据层SQLite 向量库 第三方APIPOI存储、语义检索、天气/交通实时数据这样分层的最大好处是每一层都能独立测试。演示前我清了SQLite缓存、单独压测了模型接口、只开着前端页面模拟交互任何一个环节出了问题都能很快定位不用整个项目一起排查。1.3 技术选型背后的取舍大模型选型上第5版我最终采用了一个主模型一个轻量备用模型的组合策略。主模型负责复杂行程生成和对话理解要求推理能力强备用模型只在主模型超时或返回明显错误时才介入负责做一些简单的格式规整和错误修复。演示时真遇到过主模型接口超时备用模型兜底让整场演示没有中断这个设计被好几个观众单独问过。框架层面没有用LangChain而是自己写了一套编排逻辑。LangChain确实封装得很快但遇到需要精细控制Prompt版本、需要做复杂状态管理的时候自研方案更容易调优。我自己用LangChain做过前两版最大的问题是当流程节点多了以后隐式依赖太多日志不好跟。第5版改成FastAPI 自研的Workflow模块后每个节点都能单独打点出问题直接看链路追踪就能定位。数据存储用SQLite而不是MySQL原因很简单本地演示环境要轻量。整个项目的POI数据量在几万条这个级别SQLite完全能扛住而且备份迁移只要拷一个文件。向量检索用的是轻量级的Chroma没有上Milvus这种重型组件因为现阶段对实时性和并发的要求还没有高到那个程度。前端选择Streamlit是权衡之后的结果。项目核心价值在后端逻辑前端只要能快速响应、展示清晰就行Streamlit配合自定义组件可以在一周内搞定交互原型而且地图组件的扩展性足够满足演示需求。2. 核心功能与效果演示拆解2.1 演示场景一一句话生成三天两夜行程这是全场的开场功能也是最直观的功能。演示输入是6月中旬去成都3天2夜偏好美食和人文预算2000到2500不想太赶。系统返回的行程卡片分了四块每日行程时间轴、必去景点和备选清单、美食推荐按早中晚餐分别给、预算明细表。每块都是可展开的观众能看到详细说明。时间轴做得比较精细每个景点都标注了建议游玩时长和交通时间比如武侯祠建议2小时 锦里建议1.5小时步行8分钟。演示时会特意放大这个细节让观众意识到这不是简单把景点塞进三天里而是真的考虑了地理和节奏。预算明细更直观它会列出交通、门票、餐饮、住宿的单独估算并给出总额与用户预算的对比。演示中预算总额显示2380恰好落在用户给出的2000到2500区间这个细节很能打动用户也是后面讲约束算法时的实证素材。2.2 演示场景二天气变化触发行程动态调整这个场景是在正式演示前临时加进去的结果成了现场反馈最热烈的环节。模拟的是用户在第一天的行程里加入了某户外景点但系统检测到当天下午有雷阵雨自动提示该景点为露天区域建议调整到第二天上午。底层实现并不复杂行程生成后系统会把每个带户外属性的POI拉出来和未来三天的逐小时天气预报做匹配只要存在降水概率超过60%的时段重叠就触发调整建议。这个规则是我在实际使用中发现用户最在意的痛点所以特意做了规则判断。演示时观众能看到弹窗里的三个选项自动调整、查看替代方案、忽略提醒。选自动调整后系统会把户外景点和次日某个室内景点交换并同步更新交通信息和用餐安排。整个调整过程跑完在大约2秒内比全量重新生成快得多因为它是基于已生成结构做局部修改而非重新调一次大模型。这个环节给观众的感知是系统懂天气会替用户考虑比单纯展示生成速度更有记忆点。2.3 演示场景三带老人小孩的约束式规划这个场景专门用来展示系统的约束处理能力。输入条件变为带两位65岁老人和一个5岁小孩每天行程不要超过3个景点中午最好能回酒店午休。系统输出的行程明显慢节奏景点之间的交通时间控制在20分钟以内并且在地图上自动避开了大量爬坡路段。这个规划结果是纯LLM规则协同完成的规则层负责过滤掉不适合老人小孩的POI类型LLM负责在剩余POI中做语义层面的组合排序。这个场景演示的效果很有说服力因为观众能直接看到同一个目的地、同样的天数在不同约束下会生成风格完全不同的行程。这也从侧面说明系统不是简单的模板填充而是真的理解约束条件在改变生成过程。演示时的互动亮点是现场有观众问如果老人腿脚不便怎么办我当场在对话里追加一句需要全程轮椅友好系统重新生成了行程且将所有步行距离超过800米的路线都换成了可推轮椅的平坦路线。这个即时反馈让现场气氛很活跃。2.4 效果演示的录制与呈现方式演示视频采用本地录屏加后期配音的方式制作。录屏工具选的OBS原因是对Streamlit这种Web应用截图帧率稳定且能同时录制系统音频和麦克风方便后期做解说。录制前我做了几件准备固定了浏览器窗口大小防止布局跳动关闭了所有无关通知在本地hosts里做了API域名映射避免演示中途出现网络波动。还额外准备了一套离线模拟数据万一外部API全部不可用系统会自动降级到预置数据保证演示流程不受影响。视频的节奏控制在1分40秒左右前40秒讲痛点引入中段55秒展示三个核心场景最后15秒做功能矩阵展示。剪辑时保留了原始的生成等待时长没有加速因为真实的加载过程反而能证明系统在做计算如果全部秒回观众反而会怀疑是预设好的假数据。3. 核心实现与技术细节3.1 Prompt设计与结构化输出约束项目对输出格式要求非常严格行程数据必须是一个符合预定义Schema的JSON否则下游的地图渲染和预算计算无法工作。为此设计了三层保障第一层在Prompt里用Few-shot示例给出标准输出格式第二层要求模型按JSON Schema输出并附带说明第三层在代码里做格式校验解析失败就自动走修复流程。Prompt模板经历了16个版本的迭代核心结构如下SYSTEM_PROMPT 你是一位专业的旅行规划师请根据用户需求生成一份可执行的行程计划。 要求 1. 行程必须包含每日的具体时间段、地点、交通方式和备注 2. 景点选择需匹配用户偏好考虑地理位置合理性和开放时间 3. 预算估算需细分为交通、门票、餐饮、住宿四类 4. 输出必须为合法JSON且符合以下Schema { trip_id: string, days: [ { day: 1, date: YYYY-MM-DD, spots: [ { time_start: HH:MM, time_end: HH:MM, poi_name: string, poi_type: scenic|food|hotel|transport, duration_minutes: 120, transit_mode: walk|metro|taxi|bus, transit_minutes: 15, estimated_cost: 0, note: string } ] } ], total_budget_estimate: { transport: 0, ticket: 0, food: 0, hotel: 0 } } ,JSON输出是LLM应用里最痛苦的环节之一尤其当返回内容超过3000个token时模型偶尔会在JSON末尾截断。第三层校验器不能只做json.loads还要能自动补全不完整的JSON。我实现的修复逻辑是先尝试直接解析失败则用备用模型对截断片段做补全再不行就退回规则解析用正则提取关键字段组装成最小结构。这个三层修复机制让格式解析成功率从最初的72%提升到了98.6%是整个项目稳定性提升最大的一次改动。3.2 POI知识库与RAG检索增强最开始的两个版本直接让大模型凭训练知识生成景点信息效果不稳定会输出已经关闭的店铺、错误的门票价格甚至把不在同一个城市的景点硬塞进同一条路线。后来决定构建自己的POI知识库用RAG替代模型记忆。知识库的数据来源主要是公开的旅游数据、景区官网信息和用户授权后的真实反馈。数据清洗环节花了不少时间因为不同来源的坐标格式不统一有的用GCJ-02火星坐标有的用WGS-84标准坐标混用会导致地图定位偏差几百米。最终统一转换成GCJ-02并做了去重校验。POI入库时每条记录除了基础信息外还挂了标签体系比如亲子友好适合老人雨天备选网红打卡等这些标签是后续约束筛选的关键。向量化采用中文专门优化的嵌入模型切分策略是每个POI单独一个向量单元而不是做文档切分保证检索粒度精确到地点级别。检索时采用混合检索关键词用BM25通过SQLite FTS5实现语义用向量相似度两者结果做合并重排。实测下来混合检索比纯向量检索在POI命中准确率上高出约11个百分点因为很多景点名称本身就是专有名词关键词匹配依然非常关键。3.3 行程规划的约束处理行程规划是项目里唯一同时依赖大模型和传统算法的模块。大模型负责理解用户偏好并选择合理的POI组合传统算法负责校验时间和地理上的可行性。约束类型大概分四类时间窗约束景区开放时间、餐食时间、地理约束两地点间交通时长不能超过阈值、体力约束每天步行总时长、景点数量上限、预算约束每日花费累加不能超限。实现上先用大模型生成一个初步行程草稿然后丢进一个约束求解器里校验。求解器不满足某条约束时会给出具体的冲突原因比如武侯祠到都江堰交通时间2.5小时超出阈值再把这些信息反馈给大模型做二次修订。这种生成-校验-反馈-再生成的循环通常只需要两轮即可收敛。核心的可行性校验代码逻辑如下def validate_route(route, constraints): errors [] for day in route[days]: # 体力约束步行总时长不能超过限制 walk_total sum(s[transit_minutes] for s in day[spots] if s[transit_mode] walk) if walk_total constraints[max_walk_minutes]: errors.append(fDay{day[day]} 步行总时长{walk_total}分钟 f超出限制{constraints[max_walk_minutes]}分钟) # 时间窗约束相邻景点间隔 for i in range(len(day[spots]) - 1): gap day[spots][i 1][time_start] - day[spots][i][time_end] if gap constraints[min_transfer_minutes]: errors.append(f景点{day[spots][i][poi_name]}到 f{day[spots][i1][poi_name]}时间衔接不足) # 预算约束 day_cost sum(s[estimated_cost] for s in day[spots]) if day_cost day[budget_limit]: errors.append(fDay{day[day]} 预算超出{day_cost - day[budget_limit]}元) return errors这套方案比直接让模型端到端规划更稳妥因为模型对时间的感知能力很弱经常产生逻辑上完全站不住脚的路线算法校验刚好能弥补这个短板。3.4 前端可视化与交互体验前端看起来简洁背后做了不少工作。地图部分用Streamlit集成了Leaflet路径绘制用的是Polyline每个POI点都绑定了一个包含详细信息的弹出窗口展示内容包括门票价格、开放时间、评分以及一段简短推荐理由。交互层面做了两个关键功能一个是行程卡片的拖拽调整用户可以直接拖动某个景点到另一个时间段系统会联动计算交通时长和预算变化并提示是否符合约束。第二个是换一个按钮对单个景点不满意可以单独替换不需要重新生成整个行程这种细粒度调整大大提升了用户掌控感。前端性能也做了优化地图瓦片采用本地缓存每次生成的路线使用唯一ID做增量渲染切换行程时只更新变化的部分减少了页面重绘开销。演示机器配置一般但全程操作没有明显卡顿正是因为做了这些优化。另外一个容易被忽略的细节是信息密度控制。第一版页面把所有信息都铺开显示看起来很乱后面改成默认展示核心行程、详细信息折叠的模式观众能一眼抓住重点需要细节时再展开。演示效果好很多这个模式也是用户反馈中最受好评的设计之一。4. 实操过程与踩坑记录4.1 正式演示前的准备清单演示翻车九成原因是准备不充分所以我列了一套标准流程每次演示前走一遍。第一项是网络环境检查确认大模型API和天气API都是可用状态并记录当前响应延迟作为基线。第二项是准备离线兜底数据把所有演示场景的返回结果预先缓存一份网络异常时自动切换。第三项是数据版本对齐确认知识库里的POI信息是最新的尤其是门票价格和开放时间这类时效性强的字段。我踩过一次坑演示时推荐的某景区门票价格和官网差了20元被现场观众当场指出来之后每次演示前必检这一项。第四项是演示脚本彩排全程走一遍至少需要30分钟。彩排时会刻意制造两个异常场景测试系统崩溃后的恢复能力以及我本人的临场应变话术。4.2 踩坑一大模型输出JSON被截断导致演示卡死这个问题是在内部测试时发现的。当时输入了一个超复杂的需求成都重庆西安三城9日游预算8000带父母和两个孩子需要每天留出半天亲子时间。生成的JSON长度接近4000个token模型在输出到第3520个token时直接断掉了解析器抛出一个未捕获的异常整个服务端500报错。刚开始只修了异常捕获但演示时如果出现白屏还是很难看。后来加了三层修复机制和失败重试同时在前端增加了行程生成中的骨架屏状态就算解析失败用户看到的也是正在重新规划而不是空白页。这次踩坑给了一个启示不能把大模型当成稳定组件去依赖所有薄弱的边界都要做好熔断和降级设计。演示时我用同样的超长输入来做测试虽然耗时超过30秒但系统给出了合理的拆分建议把三城行程拆成三个独立子行程分别生成。4.3 踩坑二POI坐标偏移导致地图定位偏差项目早期地图展示有个诡异问题某些POI点在地图上显示在河里或路中央偏差大的有400多米。排查之后发现是坐标系的锅。数据源A用的GPS坐标数据源B用的国内通用加密坐标两套坐标混着存地图上直接就漂了。统一坐标系是第一项修复后面又额外做了一步数据校正用坐标逆地址解析服务对每个POI做反向验证如果解析出来的街道地址和POI名称差距过大就标记为可疑数据进入人工复核队列。演示时为了保险我把坐标异常的POI做了可视化标记用不同颜色的点来区分已验证和待复核数据。虽然没有在正式演示中展示这个界面但彩排时通过对异常点的直接观察快速发现了几个坐标错误并及时修正。4.4 踩坑三API限流导致演示现场生成缓慢一次演示前测试时发现连续生成三个行程后第四个请求的响应时间从3秒暴涨到45秒。查了日志发现是第三方大模型API的限流机制生效了QPS超过阈值后触发排队。这个问题在单次演示场景里影响不大但如果在会场网络环境差或者有其他程序占用了出口带宽的情况下就很容易触发。我快速做了三件事第一是增加本地区缓存对重复请求直接返回缓存结果第二是支持多Key自动轮询不同的API订阅交替使用第三是增加请求排队和进度提示用户会看到一个排在第几位的进度条感知上的等待时间大大缩短。这个优化也体现在演示视频里观众看到进度条时反而觉得系统很真实相信它是在进行实时计算而非调取预设结果。4.5 演示后的效果评估与迭代方向演示结束后我做了详细的反馈收集。团队内部评估从三个维度打分生成质量是否满足用户需求、交互体验调整是否顺畅、稳定性异常处理是否自然。用户侧反馈收集主要通过问卷和现场提问整理。反馈最高的功能是天气联动调整和就地替换景点大家觉得这两个功能最直观地体现了智能。反馈最集中的改进点是希望增加多语言支持以及把行程导出为标准化文档方便分享给同行的人。基于这些反馈第6版的计划已经明确增加语音交互入口支持OTA平台比价接入以及把行程生成从单次对话升级为持续可修改的会话模式。演示的价值不只是当下的一次展示更重要的是收集到真实使用反馈反向指导产品迭代。5. 常见问题与排查技巧5.1 LLM输出格式不稳定这大概是所有大模型应用里遇到频率最高的坑表现是同样的Prompt十次请求里偶尔会有一两次不按Schema输出。排查思路从三方面入手第一步看是否是Prompt描述不够明确尽量用Few-shot示例而不是抽象描述第二步看是否是模型版本问题有些模型的小版本更新会改变输出偏好第三步看是否和输入长度相关超长输入更容易出现截断。我自己还加了一道保险就是对模型输出做一层合规化的JSON清洗把多余的Markdown代码块标记、中英文引号之类的符号统一转换。如果修复后依然不稳定建议在架构上就默认输出一定会出问题把所有下游依赖都建立在解析成功和解析失败两条分支上。解析失败不是边缘情况而是必然会发生的情况。5.2 行程可行性差有时候模型生成的行程看起来合理但真按着走会发现时间根本来不及。这不是错觉而是模型对实际距离和时间消耗的感知偏差很大。比如它可能会安排上午在市区逛完博物馆下午去距离市区80公里的景区而且还标注交通40分钟这明显不符合实际。解决这类问题靠两层约束一是知识库里给每个POI增加所在区域和到市中心的大致距离二是校验算法里把所有交通时间都乘上一个疲劳系数避免理论耗时与实际耗时完全画等号。实测中给步行速度打了85折、给骑行速度打了9折后生成的行程在实际走起来时宽松很多。这是一个很微妙的设计决策在行程太赶和行程太松之间需要一个平衡点。第5版的表现是大部分测试用户反馈刚好可以接受有一点点紧凑但不会累基本达到了设计预期。5.3 实时数据新鲜度难以保障POI和天气交通数据理论上要尽量新但每次演示都完全依赖实时API风险太高。我的做法是设计了一套分级数据策略核心POI信息采用每日定时更新加人工抽检天气交通数据采用实时拉取加缓存降级方案。缓存降级的触发条件是API请求失败或响应时间超过2秒。降级后系统会给用户展示一个该信息更新于30分钟前的提示代替直接报错。用户对这个提示的接受度很高反而会觉得产品设计很贴心。演示时如果遇到天气API缓慢我会提前准备好这句话说给观众听我们正在进行实时数据对接目前的缓存是5分钟前更新的不影响本次演示结果。5.4 演示应急处理方案演示现场出问题不可怕可怕的是没有应急方案。我整理了一套快速处理思路分享出来供参考故障场景应急动作话术模板模型接口超时切换备用模型或本地降级版本系统检测到网络波动已自动切换到本地缓存模式数据加载失败加载预置缓存数据为了演示顺畅我们使用预置的实时数据快照前端页面崩溃刷新页面并重放会话接下来我重新演示一遍大家可以关注核心逻辑演示环境断电手机热点云端备用环境我们切换到云端演示环境顺便展示远程部署能力这套方案不是让演示者去掩饰问题而是保证演示状态不断档注意力始终在产品能力上而不是环境问题上。真实演示中我使用过两次反馈都不错观众觉得团队反应很快。5.5 成本控制与性能优化每次演示调用大模型接口都会产生费用如果调用的模型较大、生成长行程一次演示可能要消耗几十万个token。我在第5版加了一个演示模式开关开启后会强制使用缓存和降级模型整体成本能降到正常模式的30%。性能优化上主要抓两个瓶颈一个是模型推理时延采用流式输出后首字延迟从1200ms降到了300ms左右用户等待感知明显改善。第二个是地图渲染性能通过按视野范围动态加载POI点的方式让前端在数据量翻倍的情况下依然保持60帧的流畅度。如果你是个人开发者做同类项目建议不要一上来就堆资源务实一点先用缓存和降级策略扛住演示场景把核心体验打磨好等真正用户量上来了再考虑扩充推理资源和引入更重型的基础设施。我在这个项目上最大的感受是大模型应用落地的瓶颈往往不在模型能力本身而在工程化的细节上。Prompt工程、数据质量、异常处理、性能优化每一项都要踏踏实实磨。第6版的方向清晰了把这次演示反馈最集中的多语言支持和行程导出做深做透让这个助手真正成为用户出行前打开的第一个工具。