1. 为什么我要拆这个AI旅游Agent的技术栈
去年年底接了个私活,帮一家做定制游的工作室搭一套AI旅游助手。需求听起来不复杂:用户用自然语言描述想去哪、几天、预算多少、偏好什么,Agent给出完整行程,最后能直接下单支付。但真动手才发现,这东西横跨的领域比想象中多得多——前端对话要处理流式输出和多轮上下文,中间要接地图、天气、酒店、票务这些外部能力,后端还要把行程变成可支付的订单。市面上讲AI Agent的文章大多停在"提示词怎么写"这一层,很少有人把从前端对话框一路到支付回调这条链路完整拆开讲。
这篇就干这件事。我会以一个可落地的AI旅游Agent为样本,把它拆成前端对话层、Agent编排层、MCP工具层、业务数据层、支付层五个部分,逐层讲清楚每一层在干什么、为什么这么设计、有哪些坑。核心关键词会反复出现:AI旅游Agent、前端对话、MCP、支付、技术栈。不管你是刚接触Agent开发的新手,还是已经做过几个Demo想往生产环境推的老手,应该都能从里面找到能直接抄的东西。我尽量说人话,参数和选型都给出理由,不搞那种"配置一下就好了"的糊弄式教程。
先说清楚这个Agent到底要解决什么问题。传统OTA的流程是:用户自己搜目的地、自己筛酒店、自己拼行程、自己算预算,信息过载严重。AI旅游Agent的价值在于把"搜索+筛选+组合+决策"这一串动作压缩成一次对话。用户说"下个月带爸妈去云南玩六天,不要太累,预算一万五",Agent要能理解约束(时间、人数、体力、预算),调用工具查真实数据(航班、酒店、景点开放时间),生成合理行程,并且——这是关键——把行程变成可下单、可支付的实体。没有最后这一步,它就只是个聊天玩具。
所以这个项目的技术栈天然是分层的,而且层与层之间的边界必须清晰,否则后期维护会非常痛苦。下面逐层拆。
2. 前端对话层:流式输出与多轮上下文怎么落地
2.1 为什么前端不是简单的聊天框
很多人做AI应用,前端就是一个输入框加一个消息列表,接个SSE就完事。旅游Agent不行,因为它的输出不是纯文本,而是结构化的行程卡片:第几天、住哪、玩什么、花多少钱、怎么去下一个点。如果全用Markdown渲染,用户看着累,也没法交互(比如点一下酒店卡片看详情、点一下"换一家"重新推荐)。
我的做法是前端维护两套渲染通道:文本流通道负责Agent的思考过程和解释性文字,结构化通道负责行程卡片、地图、价格明细。Agent在流式输出时,用约定的分隔标记把两类内容分开,前端解析后分别渲染。这样既保留了流式的"打字机"体验,又能渲染复杂组件。
技术选型上,如果是小程序场景,用uniapp做跨端比较省事,对话列表用scroll-view配合虚拟列表(消息多了不虚拟化会卡);如果是Web,React或Vue都行,关键是流式解析要放在Web Worker里,别阻塞主线程,否则用户滑动会掉帧。
2.2 流式输出的三种实现方式对比
| 方式 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 轮询 | 前端定时问后端"好了没" | 实现最简单 | 延迟高、请求浪费 | 早期Demo |
| SSE | 服务端单向推送 | 实现简单、兼容好 | 只能单向、连接数受限 | 纯文本对话 |
| WebSocket | 全双工 | 双向、可中断 | 实现复杂、要处理心跳 | 需要中途干预 |
旅游Agent我选的是SSE为主、WebSocket为辅的混合方案。普通对话走SSE,因为Agent输出是单向的;但当用户要"中途改需求"(比如行程生成到一半说"第三天换个景点")时,需要能打断当前生成,这时候走WebSocket发中断信号。实测下来,纯SSE方案在用户频繁改需求时体验很差,因为没法优雅中断,只能等它生成完。
注意:SSE连接在移动网络下容易被运营商掐断,一定要在前端做自动重连,并且带上last-event-id,让服务端能从断点续传,否则用户切个后台回来消息就断了。
2.3 多轮上下文的管理策略
旅游对话的上下文特别长,因为用户会不断补充约束。全量塞进prompt,token成本扛不住,而且模型容易"忘记"早期约束。我的策略是分层记忆:
- 硬约束层:目的地、日期、人数、预算,这些一旦确定就抽成结构化字段,每轮都带上,不依赖模型记忆。
- 软偏好层:口味、体力、兴趣,用摘要形式保留最近N轮的相关内容。
- 对话历史层:只保留最近若干轮原文,更早的做摘要压缩。
这样做的原因是,模型对结构化字段的遵循度远高于散落在对话里的自然语言约束。我踩过的坑是:早期把所有约束都放在对话历史里,结果用户聊到第十轮,模型把"预算一万五"记成了"两万",生成的行程直接超标。抽成字段后,每轮强制注入,再没出过这个问题。
3. Agent编排层:从意图识别到行程生成
3.1 编排框架怎么选
Agent编排这块,市面上的选择大概分三类:纯Prompt编排(靠提示词让模型自己决定调什么工具)、代码编排(用代码写死流程,模型只在节点内做决策)、混合编排。旅游Agent我强烈建议用混合编排,原因很实际:旅游业务有明确的流程(先确认需求→查资源→生成行程→确认→下单),全交给模型自由发挥,它会在"查资源"和"生成行程"之间反复横跳,浪费token还容易出错。
我的做法是把主流程用代码固定成状态机,每个状态内部再用模型做决策。比如"查资源"状态下,模型决定调哪些工具(查航班还是查酒店),但"什么时候进入生成行程状态"由代码判断(比如资源查够了、约束齐了)。这样既保留了灵活性,又保证了流程可控。
3.2 意图识别与槽位填充
用户第一句话往往是模糊的:"想去个暖和的地方玩几天"。这时候不能直接生成行程,要先做意图识别+槽位填充。意图识别判断用户是想"规划新行程"还是"修改已有行程"还是"咨询问题";槽位填充把目的地、天数、人数、预算这些必填项抽出来,缺的主动追问。
这里有个经验:追问不要一次问太多。我早期设计成一次性列出所有缺失项让用户填,结果用户直接不回了。改成一次只问一个最关键的(通常是目的地或时间),用户回答后再问下一个,完成率高很多。这跟真人导游的沟通方式是一样的,没人会一口气问你八个问题。
槽位抽取用模型做function calling比用正则靠谱得多,因为用户表达太灵活。"下个月"要转成具体日期区间,"带爸妈"要推断出大概人数和体力偏好,"不要太累"要转成每天的行程强度上限。这些规则写不完,交给模型判断更实际。
3.3 行程生成的核心逻辑
行程生成不是让模型自由发挥写一篇游记,而是在约束下做组合优化。我的做法是分两步:
第一步,模型根据约束生成一个"行程骨架":每天去哪些区域、住哪个片区、大致节奏。这一步不涉及具体酒店和航班,只定框架。
第二步,用工具查真实资源,把骨架填成实体:具体酒店、具体航班、具体景点门票。这一步是确定性的,模型只负责在候选里选,不负责编造。
为什么这么分?因为如果让模型一步到位生成带具体酒店名的行程,它大概率会编造不存在的酒店。分两步后,第一步的骨架是"软"的,允许模型发挥;第二步的填充是"硬"的,必须来自真实数据。这样既保证了行程的合理性,又保证了信息的真实性。
实操心得:行程骨架生成后,一定要做一次约束校验,用代码检查预算是否超标、每天行程时长是否超限、景点是否在营业时间。模型经常会在细节上违反约束,代码校验比指望模型自觉靠谱。
4. MCP工具层:让Agent真正能"动手"
4.1 MCP到底是什么,为什么旅游Agent需要它
MCP(Model Context Protocol)这两年被讨论得很多,但很多人还是没搞明白它解决什么问题。用一句话说:MCP是让模型调用外部工具的标准协议。在没有MCP之前,每个模型厂商有自己的function calling格式,你换个模型就要重写一遍工具定义。MCP把这层标准化了,工具提供方按MCP规范暴露能力,Agent按MCP规范调用,双方解耦。
对旅游Agent来说,MCP的价值特别大,因为它要接的外部能力太多了:地图、天气、航班、酒店、票务、汇率、签证政策。如果每个都自己写适配层,维护成本爆炸。用MCP的话,只要对应的服务提供了MCP Server,Agent这边接一次就能用。
4.2 旅游Agent需要哪些MCP工具
我把工具按用途分成四类:
| 类别 | 具体工具 | 用途 | 调用频率 |
|---|---|---|---|
| 地理类 | 地图MCP、POI搜索 | 定位、算距离、找景点 | 高 |
| 资源类 | 航班、酒店、票务 | 查真实库存和价格 | 高 |
| 环境类 | 天气、汇率、政策 | 辅助决策 | 中 |
| 交易类 | 订单、支付 | 下单和收款 | 低但关键 |
地理类里,地图MCP是核心。比如用户说"想去洱海边住",Agent要能通过地图工具找到洱海周边的住宿片区,算出各片区到主要景点的距离,才能判断哪个片区符合"不要太累"的约束。这块我实测过,纯靠模型的地理知识经常出错,必须接真实地图数据。
资源类工具是难点,因为航班酒店这些数据通常不对外开放,要么接第三方聚合API,要么自己有供应链。这块的MCP Server要处理好限流和缓存,因为Agent可能会在短时间内反复查同一个资源。
4.3 MCP工具的调用编排
工具不是越多越好,Agent面对几十个工具时会"选择困难",调用准确率反而下降。我的做法是按状态动态暴露工具:在"查资源"状态只暴露资源类工具,在"下单"状态只暴露交易类工具。这样每次模型看到的工具就几个,选择准确率高很多。
另外,工具调用要做超时和降级。地图MCP挂了,不能让整个行程生成卡死,要有兜底(比如用缓存数据或跳过该步骤)。我踩过的坑是:某个资源API响应特别慢,导致整个对话卡住,用户以为程序死了。后来给每个工具调用加了超时,超时就返回"暂时查不到,稍后重试",体验好很多。
注意:MCP工具返回的数据格式要统一。不同来源的数据字段名五花八门,Agent解析起来容易出错。我在中间加了一层数据归一化,把各来源的数据转成统一结构再喂给模型,稳定性提升明显。
5. 业务数据层:行程如何变成可支付的订单
5.1 从行程到订单的转换
行程生成完,用户满意了,下一步是把它变成订单。这一步的核心是把行程里的每一项拆成可独立下单的SKU:酒店是一晚一个SKU,机票是一程一个SKU,门票是一张一个SKU。为什么要拆这么细?因为旅游产品的退改规则是按SKU算的,酒店和机票的退改政策完全不同,混在一起没法处理。
拆成SKU后,每个SKU要带上:供应商、价格、库存、退改规则、有效期。这些信息在生成行程时就要一并查出来存好,不能等到下单时再查,否则用户确认的价格和下单时的价格可能不一致,这是旅游电商的大忌。
5.2 库存与价格的一致性处理
旅游产品的库存和价格是实时变动的,这是最头疼的地方。用户看到行程到点击支付,中间可能隔了几分钟,价格就变了。我的处理是锁价:用户确认行程时,后端对每个SKU做一次库存预占和价格锁定,锁定有效期比如15分钟。支付时校验锁是否还有效,失效就提示用户重新确认。
锁价这个机制必须有,否则会出现用户付了钱但库存没了的情况,退款纠纷很麻烦。实现上可以用Redis做分布式锁加过期时间,简单可靠。
5.3 订单状态机设计
订单状态不能随便改,要有明确的状态机:
待支付 → 已支付 → 已确认 → 已完成 ↓ ↓ 已取消 退款中 → 已退款每个状态转换都要有触发条件和校验。比如"待支付→已支付"必须由支付回调触发,不能由前端调用;"已支付→退款中"要校验退改规则是否允许。状态机用代码写死,不要交给模型判断,这是资金相关的逻辑,容不得模糊。
6. 支付层:从下单到回调的完整链路
6.1 支付方式的选择
旅游Agent的支付场景,主流是微信支付和支付宝。选哪个不是技术问题,是用户习惯问题。我的建议是两个都接,因为旅游客群年龄跨度大,年轻人用微信多,中老年用支付宝多。技术上两者都是标准的支付网关对接,差异主要在回调格式和签名算法。
支付流程本身不复杂:后端创建订单→调支付网关下单→拿到支付参数→前端唤起支付→用户支付→网关回调后端→后端更新订单状态。复杂的是回调的可靠性和幂等性。
6.2 支付回调的幂等处理
支付网关的回调可能会重复发送(网络抖动、网关重试),所以回调处理必须幂等。我的做法是:回调进来先查订单状态,如果已经是"已支付"就直接返回成功,不重复处理。同时用订单号做唯一索引,防止并发重复更新。
这里有个坑:回调处理如果耗时太长(比如要调多个下游服务),网关会认为失败并重试,导致重复回调。所以回调处理要快,只做最核心的状态更新,其他动作(发短信、通知供应商)异步处理。
实操心得:支付回调一定要做签名校验,防止伪造回调。我见过有人图省事不校验签名,结果被人伪造回调白嫖订单。校验逻辑就是按网关文档拼参数算签名,对不上就拒绝。
6.3 支付失败与退款的兜底
支付失败分几种情况:用户主动取消、余额不足、网关超时。前两种不用管,第三种要特别处理,因为可能实际扣款成功了但回调没到。我的做法是加一个主动查询机制:支付发起后,如果N分钟内没收到回调,主动去网关查订单状态,以查询结果为准。
退款同理,退款请求发出后要跟踪退款状态,不能发出去就不管了。退款失败要有告警,人工介入。旅游产品的退款金额往往不小,这块出问题就是真金白银的损失。
6.4 分布式事务的处理
下单涉及多个SKU,每个SKU可能在不同系统,这就涉及分布式事务。我的方案是最终一致性:先本地创建订单(状态待支付),然后异步去各供应商锁库存,全部锁成功才允许支付,有一个失败就回滚已锁的。用消息队列做异步补偿,保证最终一致。
不推荐用强一致的分布式事务方案(比如两阶段提交),因为旅游供应商的接口响应慢,强一致会拖垮整个下单流程。最终一致性配合对账,是更实际的选择。
7. 常见问题与排查技巧实录
7.1 对话相关的问题
问题:Agent答非所问,用户说东它答西。排查思路:先看是不是上下文太长导致模型"失忆",检查硬约束字段有没有正确注入。再看是不是工具返回的数据格式变了,导致模型解析错误。最后看提示词里有没有歧义表述。
问题:流式输出卡顿或断流。排查思路:检查SSE连接是否被中间层(如Nginx)缓冲,Nginx要关掉proxy_buffering。检查前端解析逻辑是否在主线程,卡UI。检查服务端生成速度,如果模型本身慢,前端再优化也没用。
7.2 MCP工具相关的问题
问题:工具调用失败率高。排查思路:先看是不是工具太多导致模型选错,按状态收敛工具集。再看工具描述是否清晰,描述模糊模型就乱调。最后看超时设置,超时太短会导致正常调用被误判失败。
问题:工具返回数据模型理解不了。排查思路:数据要做归一化,字段名用模型熟悉的英文。复杂嵌套结构要拍平。数值要带单位,别让模型猜。
7.3 支付相关的问题
问题:回调收不到。排查思路:检查回调地址是否公网可达,检查防火墙和白名单,检查网关后台的回调配置。用网关提供的回调测试工具先验证。
问题:重复扣款。排查思路:检查订单号是否唯一,检查回调是否幂等,检查是否有并发下单。前端要做防重复点击,后端要做订单去重。
问题:金额对不上。排查思路:检查是否有优惠券、积分抵扣没算进去,检查币种和精度(分vs元),检查是否有手续费。金额计算统一用整数分,别用浮点。
7.4 常见问题速查表
| 现象 | 可能原因 | 快速排查 |
|---|---|---|
| 对话卡住 | 工具超时/模型慢 | 看日志哪一步耗时 |
| 行程不合理 | 约束没注入/校验缺失 | 检查硬约束字段 |
| 价格变动 | 没锁价 | 加锁价机制 |
| 支付失败 | 签名错/参数错 | 对照网关文档 |
| 回调重复 | 没做幂等 | 加状态判断 |
| 库存超卖 | 没预占 | 加库存锁 |
8. 我在这个项目里踩过的几个真实坑
第一个坑是过度信任模型的地理知识。早期没接地图MCP,让模型自己算景点之间的距离,结果它把两个相距三百公里的景点排在同一天,用户一看就知道是瞎编的。接了真实地图数据后,这类问题基本消失。教训是:涉及事实性的数据,一律走工具,别让模型编。
第二个坑是支付回调没做幂等。测试环境一切正常,上线后遇到网关重试,同一个订单被处理了两次,库存扣了两次。后来加了状态判断和唯一索引才解决。这个坑很典型,测试环境很难复现,因为重试是偶发的。
第三个坑是上下文无限增长。早期没做摘要压缩,聊到二十轮后token爆了,请求直接失败。后来做了分层记忆和摘要,才稳定下来。这个坑的教训是:对话类应用一定要从一开始就设计上下文管理策略,别等出问题再补。
第四个坑是工具描述写得太随意。有个查天气的工具,描述写的是"查天气",模型经常在不需要天气的时候也调它。后来把描述改成"查询指定城市指定日期的天气预报,仅在需要判断行程是否受天气影响时调用",误调率大幅下降。工具描述就是给模型的说明书,写得越清楚,它用得越准。
这套技术栈拆下来,核心就一句话:AI旅游Agent的难点不在AI,在于把AI和真实业务系统可靠地连起来。对话、MCP、支付,每一层都有各自的坑,但真正决定成败的是层与层之间的衔接是否稳固。我个人的体会是,先把支付和订单这条"硬"链路做扎实,再往上叠AI能力,比反过来要稳得多。毕竟用户可以容忍Agent推荐得不够完美,但绝对不能容忍付了钱订单出问题。