AI旅游Agent技术栈拆解:前端对话、MCP工具与支付交易全链路
1. 一个AI旅游Agent到底要拆成几块
先说说我为什么会去拆这个东西。去年年底接了个私活,帮一个做定制游的团队搭一套AI行程助手,需求听起来特别简单:用户在小程序里说“我想五一去成都玩四天,带老人,预算五千”,Agent回一份能直接下单的行程。真上手才发现,这句话背后要串起来的东西远比想象中多——前端要处理多轮对话和流式渲染,中间要有个能调工具的“大脑”,工具层要接地图、天气、酒店、票务,最后还得落到真实的支付链路上把钱收回来。
这套东西现在业内有个比较统一的叫法:AI旅游Agent。它跟传统的OTA搜索框最大的区别在于,搜索框是“人找货”,Agent是“货找人”——用户只描述意图,剩下的检索、比价、组合、下单动作由模型驱动工具链自动完成。而支撑这套体验的完整技术栈,我习惯把它切成四层来看:前端对话层、Agent编排层、MCP工具层、支付交易层。这四层任何一层掉链子,用户体验都会断在某个环节上。
这篇文章我打算按这四层逐层拆,重点讲清楚每一层选型的理由、踩过的坑,以及那些文档里不会写的实操细节。适合正在做或准备做AI Agent类产品的同学看,尤其是涉及真实交易闭环的场景——因为一旦牵扯到钱,很多“demo能跑”的方案就立刻不成立了。文中涉及MCP的部分我会讲得细一些,毕竟这是当前工具调用事实上的主流协议,也是热词里被问得最多的。
需要先说明的是,下面提到的具体参数、代码和配置,一部分来自我自己的项目实践,一部分是基于行业常见做法做的合理推演,我会尽量标注清楚哪些是实测、哪些是建议值,你落地时按自己业务量级调整。
2. 前端对话层:流式渲染与状态管理才是真难点
2.1 为什么不用现成聊天组件直接套
很多人第一反应是找个开源的Chat UI组件库,接个接口就完事。我一开始也这么想,结果第一版上线第二天就被投诉了:用户发完消息后界面卡住十几秒,然后“唰”地一下蹦出一大段文字,中间没有任何反馈。问题出在流式输出上——大模型生成是逐token返回的,如果前端不做增量渲染,用户感知到的就是“卡死”。
旅游场景对这一点尤其敏感,因为Agent经常要输出结构化的行程卡片、地图点位、价格表格,不是纯文本。我的做法是把消息体设计成混合内容块:文本块走流式追加,卡片块等结构化数据完整到达后再整体渲染。这样用户先看到“正在为你规划成都行程……”的文字反馈,几秒后卡片再补进来,体感上快很多。
具体实现上,前端用SSE(Server-Sent Events)接收流,比WebSocket更轻,旅游Agent这种“一问一答”的场景没必要上双向长连接。关键代码大概长这样:
const es = new EventSource('/api/chat/stream?session_id=xxx'); 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 多轮对话的状态到底存哪
这是第二个坑。旅游规划天然是多轮的:用户先说“去成都”,再说“带老人”,再补“预算五千”。这些信息要累积成一个槽位(slot),Agent才能生成完整行程。槽位存前端还是后端?
我的结论是后端为主、前端为辅。后端维护一份权威的会话状态(通常落Redis,key用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(城市名)、checkin(YYYY-MM-DD)、checkout(YYYY-MM-DD)、 guests(人数)、budget_per_night(每晚预算,可选) 输出:酒店列表,含名称、价格、评分、可订状态 不适用于:仅询问酒店推荐但未确定日期的情况最后那句“不适用于”特别有用,能挡掉一批误调用。这个技巧我是从实际badcase里总结出来的——模型经常在用户只是随口问“成都有什么好酒店”时就触发搜索,加了排除条件后误触发率降了一大截。
3.3 编排逻辑里的重试与降级
真实环境里工具调用失败是常态:地图API限流、酒店接口超时、支付网关抖动。编排层必须有一套重试 + 降级策略,否则用户看到的就是“抱歉,出错了”。
我的策略分三档:瞬时错误重试、持续错误降级、关键路径兜底。瞬时错误(如超时)自动重试2次,间隔用指数退避;持续错误(如接口连续失败)降级到备用数据源,比如主酒店接口挂了就切到缓存或另一家供应商;关键路径(如支付)失败则必须明确告知用户并保留订单草稿,绝不能静默失败。
实操心得:重试一定要设上限,我见过没设上限导致接口雪崩的案例。另外重试的请求要带幂等键,否则可能重复下单——这个坑在支付环节是致命的。
4. MCP工具层:把外部能力标准化接进来
4.1 MCP到底是什么,为什么旅游Agent需要它
MCP全称Model Context Protocol,直译是“模型上下文协议”。你可以把它理解成AI和外部工具之间的USB接口——以前每接一个工具都要写一套适配代码,现在只要工具方实现了MCP Server,Agent这边用统一的客户端就能调。
旅游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( name="search_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(type="text", text=result)]这段代码的关键在于inputSchema——它用的是标准JSON Schema,模型能直接读懂参数结构。我强烈建议把required字段写清楚,不然模型可能漏传参数导致调用失败。
4.3 MCP接入时的几个真实坑
第一个坑是工具数量爆炸。MCP的好处是接入方便,坏处是你会忍不住接一堆。我一开始接了地图、天气、酒店、票务、汇率、签证、攻略……十几个Server,结果模型选择困难,响应也变慢。后来砍到核心五个,体验反而更好。经验是:只接当前任务链路必需的,其余按需动态加载。
第二个坑是流式输出与MCP的配合。热词里有人问“使用mcp工具流式输出内容到文件”,这在实际中确实是个需求——比如Agent生成行程的同时要写一份文件。我的做法是MCP工具本身返回完整结果,流式输出由编排层负责,两者解耦。硬要让MCP工具自己流式返回,协议支持不完善,容易出问题。
第三个坑是鉴权和授权。热词里“codex 接入 figma mcp 怎么授权”问的就是这个。MCP Server如果涉及用户私有数据(比如用户的订单历史),必须做鉴权。常见做法是OAuth或者API Key,Key要存在服务端,绝不能下发到前端。我见过把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供应商失败怎么办?标准答案是TCC(Try-Confirm-Cancel):先Try阶段预占所有资源,全部成功再Confirm,任何一个失败就Cancel全部。这个逻辑必须在服务端实现,不能依赖前端。
5.2 支付通道接入的实操细节
国内旅游场景主流是微信支付和支付宝。接入本身不复杂,坑主要在回调处理和对账上。
回调处理的关键是幂等。支付平台可能重复回调,你的订单状态机必须能识别“这笔已经处理过了”。我的做法是用支付平台的交易号做唯一索引,重复回调直接返回成功但不重复发货。
def handle_payment_callback(trade_no, order_id): if PaymentRecord.exists(trade_no=trade_no): return "success" # 已处理,直接返回 with transaction.atomic(): order = Order.objects.select_for_update().get(id=order_id) if order.status != "pending": return "success" order.status = "paid" order.save() PaymentRecord.create(trade_no=trade_no, order_id=order_id) return "success"对账是另一个容易被忽略的环节。每天凌晨拉取支付平台的账单,和本地订单逐笔核对,发现差异及时处理。我见过因为没做对账,一笔退款没同步导致用户投诉的案例。对账逻辑不复杂,但必须做。
注意:热词里出现“支付通道信息错误”“支付宝支付链接提取”这类词,实际工作中遇到支付报错,第一步永远是查商户配置和签名,九成的“通道错误”是配置问题不是通道问题。
5.3 支付安全的三条红线
第一条,金额校验必须在服务端。前端传来的金额只能作为参考,真实扣款金额以服务端订单为准。我见过前端传金额直接被信任的代码,这是重大安全隐患。
第二条,敏感信息不落前端。商户密钥、证书这些绝不能出现在前端代码或网络请求里。所有签名操作在服务端完成。
第三条,支付结果以服务端查询为准。不能只依赖前端回调或用户截图,必须主动向支付平台查询订单状态。用户说“我付了”不算数,平台说“付了”才算数。
6. 常见问题与排查速查
实际做这套东西的过程中,问题基本集中在几个地方。我整理成一张表,方便你遇到时快速定位。
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 对话卡住无响应 | SSE连接断开或后端未flush | 查Nginx缓冲配置,关闭proxy_buffering |
| 工具频繁选错 | 工具描述模糊或数量过多 | 精简描述,加“不适用”条件,砍工具数 |
| MCP Server连不上 | 启动配置或路径错误 | 查Server日志,确认端口和启动命令 |
| 支付回调重复处理 | 未做幂等 | 用交易号做唯一索引 |
| 订单价格与展示不符 | 未锁价或锁价超时 | 检查预占逻辑和锁定时长 |
| 行程生成一半中断 | 模型输出超长或超时 | 分段生成,设置合理max_tokens |
再补几个文档里不会写的避坑技巧。第一,流式输出一定要设超时,我遇到过模型卡住不返回也不报错的情况,前端一直转圈,加个30秒超时兜底。第二,MCP工具调用要记日志,包括入参、出参、耗时,出问题时这是唯一的线索。第三,支付环节的测试要用沙箱环境跑通全流程,包括成功、失败、超时、重复回调四种情况,别等上线才发现问题。
最后分享一个我自己的体会:这套技术栈里,最难的不是某一层技术,而是层与层之间的衔接。前端的状态和后端的槽位要对齐,Agent的工具调用和MCP的返回格式要对齐,订单的状态机和支付的回调要对齐。每一处衔接都是一个潜在的bug点。我的建议是先把一条最小链路跑通——用户说一句话,Agent调一个工具,返回一个结果,走一次支付——然后再往上加功能。链路通了,剩下的都是体力活。