1. 一个AI旅游Agent到底要拆成几块先说说我为什么会去拆这个东西。去年年底接了个私活帮一个做定制游的团队搭一套AI行程助手需求听起来特别简单用户在小程序里说“我想五一去成都玩四天带老人预算五千”Agent回一份能直接下单的行程。真上手才发现这句话背后要串起来的东西远比想象中多——前端要处理多轮对话和流式渲染中间要有个能调工具的“大脑”工具层要接地图、天气、酒店、票务最后还得落到真实的支付链路上把钱收回来。这套东西现在业内有个比较统一的叫法AI旅游Agent。它跟传统的OTA搜索框最大的区别在于搜索框是“人找货”Agent是“货找人”——用户只描述意图剩下的检索、比价、组合、下单动作由模型驱动工具链自动完成。而支撑这套体验的完整技术栈我习惯把它切成四层来看前端对话层、Agent编排层、MCP工具层、支付交易层。这四层任何一层掉链子用户体验都会断在某个环节上。这篇文章我打算按这四层逐层拆重点讲清楚每一层选型的理由、踩过的坑以及那些文档里不会写的实操细节。适合正在做或准备做AI Agent类产品的同学看尤其是涉及真实交易闭环的场景——因为一旦牵扯到钱很多“demo能跑”的方案就立刻不成立了。文中涉及MCP的部分我会讲得细一些毕竟这是当前工具调用事实上的主流协议也是热词里被问得最多的。需要先说明的是下面提到的具体参数、代码和配置一部分来自我自己的项目实践一部分是基于行业常见做法做的合理推演我会尽量标注清楚哪些是实测、哪些是建议值你落地时按自己业务量级调整。2. 前端对话层流式渲染与状态管理才是真难点2.1 为什么不用现成聊天组件直接套很多人第一反应是找个开源的Chat UI组件库接个接口就完事。我一开始也这么想结果第一版上线第二天就被投诉了用户发完消息后界面卡住十几秒然后“唰”地一下蹦出一大段文字中间没有任何反馈。问题出在流式输出上——大模型生成是逐token返回的如果前端不做增量渲染用户感知到的就是“卡死”。旅游场景对这一点尤其敏感因为Agent经常要输出结构化的行程卡片、地图点位、价格表格不是纯文本。我的做法是把消息体设计成混合内容块文本块走流式追加卡片块等结构化数据完整到达后再整体渲染。这样用户先看到“正在为你规划成都行程……”的文字反馈几秒后卡片再补进来体感上快很多。具体实现上前端用SSEServer-Sent Events接收流比WebSocket更轻旅游Agent这种“一问一答”的场景没必要上双向长连接。关键代码大概长这样const es new EventSource(/api/chat/stream?session_idxxx); es.onmessage (e) { const chunk JSON.parse(e.data); if (chunk.type text) appendText(chunk.delta); if (chunk.type card) renderCard(chunk.payload); if (chunk.type done) es.close(); };注意SSE默认有重连机制如果后端没做好幂等断线重连会导致同一条消息重复渲染。我的处理是给每个chunk带一个自增seq前端丢弃seq小于已渲染最大值的包。2.2 多轮对话的状态到底存哪这是第二个坑。旅游规划天然是多轮的用户先说“去成都”再说“带老人”再补“预算五千”。这些信息要累积成一个槽位slotAgent才能生成完整行程。槽位存前端还是后端我的结论是后端为主、前端为辅。后端维护一份权威的会话状态通常落Rediskey用session_id前端只缓存最近几轮用于展示。原因很简单用户可能中途换设备、刷新页面甚至从对话切到手动编辑行程再切回来状态必须能恢复。前端存的那份只是“显示用”不能作为决策依据。槽位结构我建议设计成可扩展的JSON别用固定字段{ destination: 成都, days: 4, companions: [老人], budget: 5000, preferences: [慢节奏, 少爬山], confirmed: false }confirmed这个字段很关键——它标记用户是否已经确认行程可以进入下单环节。没有这个显式确认Agent很容易在用户还在犹豫时就触发支付流程体验非常糟糕。2.3 前端技术栈的取舍如果是小程序场景用uniapp一套代码多端跑是常见选择但要注意uniapp的SSE支持在小程序端并不完整很多情况下得退化成轮询或者用WebSocket模拟。我实测下来小程序端用WebSocket 自定义分帧协议更稳虽然代码量大一点但可控性高。如果是H5或桌面端直接上React/Vue 原生EventSource就行没必要引入electron这种重方案——除非你要做本地文件导出、离线行程管理这类桌面特性。electron技术栈的优势在于能访问本地文件系统比如把行程导出成PDF或日历文件但代价是包体积和内存占用都上去了纯在线Agent用不上。3. Agent编排层工具调用的“大脑”怎么设计3.1 单Agent还是多Agent旅游规划这个任务我试过两种架构。单Agent就是一个大模型拿着所有工具多Agent是拆成“景点规划Agent”“酒店Agent”“交通Agent”各管一摊。实测下来中小规模场景单Agent足够超过15个工具再考虑拆分。原因是工具一多模型的工具选择准确率会明显下降。我做过一个粗糙的统计工具数在10个以内时选错工具的概率大概5%到20个时升到15%以上。旅游场景的工具其实不算多——地图、天气、酒店搜索、票务、支付加起来七八个单Agent完全扛得住。硬拆多Agent反而增加了Agent之间的通信开销和状态同步复杂度。3.2 工具描述怎么写才不容易被选错这是很多人忽略的细节。工具能不能被正确调用八成取决于描述写得好不好。我见过把工具描述写成“查询酒店”四个字的模型根本不知道什么时候该用它。好的描述要包含三要素什么时候用、输入是什么、输出是什么。举个例子酒店搜索工具的描述我会这么写当用户已确定目的地和入住日期需要查找可预订酒店时调用。 输入city城市名、checkinYYYY-MM-DD、checkoutYYYY-MM-DD、 guests人数、budget_per_night每晚预算可选 输出酒店列表含名称、价格、评分、可订状态 不适用于仅询问酒店推荐但未确定日期的情况最后那句“不适用于”特别有用能挡掉一批误调用。这个技巧我是从实际badcase里总结出来的——模型经常在用户只是随口问“成都有什么好酒店”时就触发搜索加了排除条件后误触发率降了一大截。3.3 编排逻辑里的重试与降级真实环境里工具调用失败是常态地图API限流、酒店接口超时、支付网关抖动。编排层必须有一套重试 降级策略否则用户看到的就是“抱歉出错了”。我的策略分三档瞬时错误重试、持续错误降级、关键路径兜底。瞬时错误如超时自动重试2次间隔用指数退避持续错误如接口连续失败降级到备用数据源比如主酒店接口挂了就切到缓存或另一家供应商关键路径如支付失败则必须明确告知用户并保留订单草稿绝不能静默失败。实操心得重试一定要设上限我见过没设上限导致接口雪崩的案例。另外重试的请求要带幂等键否则可能重复下单——这个坑在支付环节是致命的。4. MCP工具层把外部能力标准化接进来4.1 MCP到底是什么为什么旅游Agent需要它MCP全称Model Context Protocol直译是“模型上下文协议”。你可以把它理解成AI和外部工具之间的USB接口——以前每接一个工具都要写一套适配代码现在只要工具方实现了MCP ServerAgent这边用统一的客户端就能调。旅游Agent特别需要这个因为它的工具来源太杂了地图可能是百度地图MCP天气是某个气象服务酒店是自建接口票务又是另一家。没有统一协议的话光适配层就能写死人。热词里“百度地图mcp ai”“同花顺mcp”“禅道mcp”这些本质都是各家把自己的能力包装成MCP Server供Agent调用。MCP的核心概念有三个Resources资源、Tools工具、Prompts提示模板。旅游场景里Resources可以是一份城市景点库Tools是搜索酒店、查天气这些动作Prompts则是预设的“生成行程”模板。理解这三者的分工是用好MCP的前提。4.2 一个旅游MCP Server的最小实现我拿酒店搜索举例写一个最小可用的MCP Server。用Python的话官方有SDK核心就是注册工具和处理调用from mcp.server import Server from mcp.types import Tool, TextContent app Server(hotel-mcp) app.list_tools() async def list_tools(): return [Tool( namesearch_hotel, description当用户确定目的地和日期后搜索可预订酒店, inputSchema{ type: object, properties: { city: {type: string}, checkin: {type: string}, checkout: {type: string}, guests: {type: integer} }, required: [city, checkin, checkout] } )] app.call_tool() async def call_tool(name, arguments): if name search_hotel: result await do_search(arguments) return [TextContent(typetext, textresult)]这段代码的关键在于inputSchema——它用的是标准JSON Schema模型能直接读懂参数结构。我强烈建议把required字段写清楚不然模型可能漏传参数导致调用失败。4.3 MCP接入时的几个真实坑第一个坑是工具数量爆炸。MCP的好处是接入方便坏处是你会忍不住接一堆。我一开始接了地图、天气、酒店、票务、汇率、签证、攻略……十几个Server结果模型选择困难响应也变慢。后来砍到核心五个体验反而更好。经验是只接当前任务链路必需的其余按需动态加载。第二个坑是流式输出与MCP的配合。热词里有人问“使用mcp工具流式输出内容到文件”这在实际中确实是个需求——比如Agent生成行程的同时要写一份文件。我的做法是MCP工具本身返回完整结果流式输出由编排层负责两者解耦。硬要让MCP工具自己流式返回协议支持不完善容易出问题。第三个坑是鉴权和授权。热词里“codex 接入 figma mcp 怎么授权”问的就是这个。MCP Server如果涉及用户私有数据比如用户的订单历史必须做鉴权。常见做法是OAuth或者API KeyKey要存在服务端绝不能下发到前端。我见过把Key写在前端配置里的等于把家门钥匙贴在门上。4.4 MCP与自建工具接口怎么选不是所有工具都值得包成MCP。我的判断标准是这个工具会不会被多个Agent复用。如果只是旅游Agent自己用的酒店接口直接写个函数调用更简单如果是地图、支付这种多个业务线都要用的能力包成MCP Server才划算一次开发多处复用。另外MCP目前生态还在快速演进不同实现的兼容性参差不齐。我实测下来官方SDK的稳定性最好第三方Server要挑维护活跃的用。遇到“codex无法找到mcp”这类问题九成是Server的启动配置或路径写错了先查日志再怀疑协议。5. 支付交易层钱的事一点都不能含糊5.1 旅游Agent的支付和普通电商有什么不同普通电商是“用户选好商品→下单→支付”路径是确定的。旅游Agent不一样它的订单是动态组合出来的机票酒店门票接送可能来自不同供应商价格实时变动。这就带来两个特殊问题一是价格锁定二是分布式事务。价格锁定指的是Agent给用户展示的行程总价必须在用户点击支付前保持有效。但供应商价格可能几分钟就变。我的做法是生成订单时向各供应商预占库存并锁定价格锁定有效期通常15分钟超时自动释放。用户在这15分钟内完成支付否则订单失效需要重新确认。分布式事务是更麻烦的问题。热词里“电商支付分布式事物实现方案”问的就是这个。旅游订单跨多个供应商A供应商扣款成功、B供应商失败怎么办标准答案是TCCTry-Confirm-Cancel先Try阶段预占所有资源全部成功再Confirm任何一个失败就Cancel全部。这个逻辑必须在服务端实现不能依赖前端。5.2 支付通道接入的实操细节国内旅游场景主流是微信支付和支付宝。接入本身不复杂坑主要在回调处理和对账上。回调处理的关键是幂等。支付平台可能重复回调你的订单状态机必须能识别“这笔已经处理过了”。我的做法是用支付平台的交易号做唯一索引重复回调直接返回成功但不重复发货。def handle_payment_callback(trade_no, order_id): if PaymentRecord.exists(trade_notrade_no): return success # 已处理直接返回 with transaction.atomic(): order Order.objects.select_for_update().get(idorder_id) if order.status ! pending: return success order.status paid order.save() PaymentRecord.create(trade_notrade_no, order_idorder_id) return success对账是另一个容易被忽略的环节。每天凌晨拉取支付平台的账单和本地订单逐笔核对发现差异及时处理。我见过因为没做对账一笔退款没同步导致用户投诉的案例。对账逻辑不复杂但必须做。注意热词里出现“支付通道信息错误”“支付宝支付链接提取”这类词实际工作中遇到支付报错第一步永远是查商户配置和签名九成的“通道错误”是配置问题不是通道问题。5.3 支付安全的三条红线第一条金额校验必须在服务端。前端传来的金额只能作为参考真实扣款金额以服务端订单为准。我见过前端传金额直接被信任的代码这是重大安全隐患。第二条敏感信息不落前端。商户密钥、证书这些绝不能出现在前端代码或网络请求里。所有签名操作在服务端完成。第三条支付结果以服务端查询为准。不能只依赖前端回调或用户截图必须主动向支付平台查询订单状态。用户说“我付了”不算数平台说“付了”才算数。6. 常见问题与排查速查实际做这套东西的过程中问题基本集中在几个地方。我整理成一张表方便你遇到时快速定位。现象可能原因排查方向对话卡住无响应SSE连接断开或后端未flush查Nginx缓冲配置关闭proxy_buffering工具频繁选错工具描述模糊或数量过多精简描述加“不适用”条件砍工具数MCP Server连不上启动配置或路径错误查Server日志确认端口和启动命令支付回调重复处理未做幂等用交易号做唯一索引订单价格与展示不符未锁价或锁价超时检查预占逻辑和锁定时长行程生成一半中断模型输出超长或超时分段生成设置合理max_tokens再补几个文档里不会写的避坑技巧。第一流式输出一定要设超时我遇到过模型卡住不返回也不报错的情况前端一直转圈加个30秒超时兜底。第二MCP工具调用要记日志包括入参、出参、耗时出问题时这是唯一的线索。第三支付环节的测试要用沙箱环境跑通全流程包括成功、失败、超时、重复回调四种情况别等上线才发现问题。最后分享一个我自己的体会这套技术栈里最难的不是某一层技术而是层与层之间的衔接。前端的状态和后端的槽位要对齐Agent的工具调用和MCP的返回格式要对齐订单的状态机和支付的回调要对齐。每一处衔接都是一个潜在的bug点。我的建议是先把一条最小链路跑通——用户说一句话Agent调一个工具返回一个结果走一次支付——然后再往上加功能。链路通了剩下的都是体力活。