ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从对话到支付:AI旅游Agent四层架构与MCP工具链实战解析

从对话到支付:AI旅游Agent四层架构与MCP工具链实战解析 设想一个场景用户在一个小程序里输入帮我订下周三从上海去杭州的高铁票再订一间西湖附近的酒店预算500以内然后系统自动完成查票、比价、推荐、生成订单、支付闭环。这背后不是简单的关键词匹配而是由前端对话层、Agent编排层、MCP工具层、支付业务层四层技术栈协同完成的。这篇文章我以自己的实际项目为主线把从对话界面到支付成功的完整链路拆开讲一遍适合正在规划AI Agent产品或想了解落地细节的开发者。1. 整体架构四层各司其职为什么这么拆1.1 四层架构与各自职责先给一个整体俯视图。我做的这个AI旅游Agent对外形态是一个小程序用户在里面用自然语言对话系统负责理解、规划、执行和支付。整个技术栈拆成四层层级核心职责关键技术点前端对话层承载对话交互、卡片展示、支付跳转小程序/H5、WebSocket、SSE、JSON Schema消息协议Agent编排层意图识别、参数抽取、任务规划、状态管理LLMFunction Calling、会话管理、状态机MCP工具层统一接入票务、酒店、天气、地图等外部能力MCP Server、tools/list、tools/call支付与订单层订单生成、支付网关、供应链确认订单状态机、支付回调幂等、库存预占很多人一上来就想着用一个大模型搞定所有事这在Demo里可以生产环境根本走不通。真正的Agent产品LLM只是大脑的推理模块它需要手脚——也就是工具需要记忆——也就是会话状态还需要一套外部业务系统来兜住订单、支付、库存这些不能靠模型胡猜的核心资产。1.2 一次完整请求的数据流我用一次真实对话来串一遍用户在小程序输入帮我订下周三上海到杭州的高铁票。前端通过WebSocket把消息推给Agent编排服务带上会话ID。Agent编排层调用LLM让模型输出结构化结果意图是book_train参数是出发地上海、目的地杭州、日期2025-11-19下周三。Agent发现参数不完整——没说明出发时间于是生成一句澄清问题您大概想几点出发。前端把这句话以气泡形式展示。用户回复上午10点左右。Agent补全参数进入任务规划阶段通过MCP工具层调用票务查询Server拉取符合条件的车次列表。系统把车次列表渲染成卡片用户点选一个系统生成订单草稿。用户确认支付跳转微信支付收银台。支付成功回调到达订单服务状态置为已支付同时异步通知票务供应商出票。Agent给用户推送一条出票成功订单号XXXX的卡片消息。这一条链路里每层做每层的事任何一层出问题都能独立降级。比如MCP工具层挂了Agent可以直接告诉用户暂时查不了票而不是整体崩溃。1.3 为什么要坚持这个分层我见过不少团队把工具调用直接写死在Agent代码里结果每接一个新渠道就要改一遍编排逻辑非常痛苦。分层最核心的价值是三件事换模型不影响业务今天用GPT-4o明天想切到国产模型只要模型支持Function Calling编排层的代码不用大改。工具扩展不动核心新增一个查免税店优惠的能力只需要在MCP Server里注册一个新工具前端不用发版Agent不用重写。支付等敏感能力可以独立控制权限支付工具不在Agent的默认工具列表里只有订单状态走到待支付时才会通过代码显式拉起避免模型在对话中擅自扣款。这个分层的代价是链路变长但换来的是每个环节都能单独测试、单独部署、单独降级对于涉及真金白银的旅游场景来说这笔投入是值得的。2. 前端对话层不止是聊天框而是会话卡片操作的混合UI2.1 混合消息协议文本、卡片与可交互组件的统一旅游Agent的前端如果只是个聊天窗口用户体验会很差——因为查出来的车次、酒店、景点这些信息用纯文本列出来又长又难读用户还得自己复制去别处比对。我在项目里做的是混合消息协议一条消息可以是文本气泡也可以是一个结构化卡片甚至可以是一个带按钮的交互组件。具体做法是定义一套基于JSON Schema的消息格式{ type: card, card_type: train_list, payload: { items: [ { train_no: G7351, depart_time: 10:12, arrive_time: 11:27, duration: 1h15m, price: 73.0, seat_left: 12 } ] }, actions: [ { label: 选择这趟车, action: select_train, params: {...} } ] }前端拿到这条消息后根据card_type找到对应的渲染器把数据渲染成卡片按钮绑定select_train动作。这样做的好处是前端和Agent之间只传数据不传UI指令。Agent永远不需要关心按钮长什么样它只需要输出这里有5趟车可选剩下的事情交给前端。2.2 流式输出与弱网兜底LLM的响应是逐字生成的用户等3-5秒只看到正在输入是极其糟糕的体验。这里我用了双通道策略文本消息走SSE流式模型每生成一个token就推给前端前端实现打字机效果。结构化消息走全量推送比如车次列表、订单确认这类数据必须一次性给全不能用流式。曾经试过把JSON也流式推前端再本地拼结果弱网环境下频繁拼接出错后来果断改为流式文本 全量卡片的组合。还有一个容易踩的坑WebSocket断线重连。用户在电梯里、地铁里连接很容易断开。我们在前端做了一整套重连机制断线后自动重试带上最后的会话ID和未确认消息的序号重新建立连接后Agent会重发用户最后一次未收到确认的消息。这能避免一个很尴尬的场景——用户以为没发送成功又发了一遍结果Agent看到两个相同请求。2.3 前端技术选型跨端方案怎么定这个项目前端选了基于Vue的跨端方案打包成小程序H5。选型时的核心考量是复用度小程序的语法和H5不完全一样但通过跨端框架可以做到一套代码编译到两端对旅游这种以微信生态为主的场景很实用。支付能力微信小程序内可以直接拉起微信支付H5需要走公众号支付或跳转App。因此前端需要封装一个统一的requestPayment方法内部判断当前环境走不同的支付通道。渲染密度旅游卡片信息量大需要复杂的列表、日历选择器、地图POI展示。纯WebView方案在低端安卓机上体验不稳定所以保留了一部分原生小程序组件。前端还有一块重要工作是状态管理。一个长对话可能持续半小时几十条消息如果全部塞给LLM作为上下文Token费用和延迟都会爆炸。我的做法是前端只保留最近20条完整消息用于展示更早的历史在Agent侧压缩成摘要后继续参与推理前端不感知这件事——用户看到的依然是完整聊天记录。3. Agent编排层LLM不是万能核心在意图解析与状态流转3.1 从自由对话到结构化参数让模型输出JSONAgent编排层是整个系统最像大脑的部分但这里有个很重要的认知别让LLM自己决定下一步干什么而是让LLM输出一个结构化的行动计划由代码来执行这个计划。我们的做法是给模型一个明确的Prompt Function Calling定义要求它在理解用户意思后输出JSON包含意图、参数、缺失参数列表。举个例子{ intent: book_train, params: { from_city: 上海, to_city: 杭州, date: 2025-11-19, time_pref: 上午 }, missing_params: [train_no], followup_question: 为您找到以下上午出发的高铁请选择一趟... }关键设计是missing_params字段。模型在第一次对话后如果发现信息不足不会强行编造一个结果而是明确指出缺什么并生成追问话术。这一步能显著减少模型幻觉——比如用户没说日期模型自己编了个明天这种错误一旦进入支付环节就是事故。3.2 多轮上下文用户改口、反问与消歧多轮对话最考验Agent的地方是上下文修正。用户可能先问帮我看看后天去深圳的机票看到结果后又改口算了还是大后天吧。这种情况下Agent不能把每一次用户输入都当成独立的请求而是要联合历史一起理解。我们采用了滑动窗口 关键槽位缓存的双层方案滑动窗口最近4轮对话的原始内容直接传给LLM保证模型能理解当前语境。关键槽位缓存在Redis里维护一个session_context包含当前已经确认的参数日期、城市、人数、预算。用户每确认一个值代码就会更新槽位而不是完全依赖LLM的记忆。比如用户说还是大后天吧窗口里的历史让模型知道这是在改日期而槽位缓存让代码知道是哪个日期被替换。两者配合才能做到改得准、记得住。还有一个低成本的消歧技巧当模型拿不准用户指的杭州东站还是杭州南站时我安排Agent主动询问而不是猜。旅游场景里猜错的代价比多问一句高得多。因为一旦订错站点用户可能赶不上车这个责任是产品承担不起的。3.3 任务编排状态机确认、执行、失败恢复Agent执行任务不能像聊天一样随意尤其是涉及下单、支付这类操作时必须有一个正式的状态机PENDING → CONFIRMING → EXECUTING → DONE ↘ FAILED → RETRY/ABORT举一个订火车票的例子用户选好车次后系统进入CONFIRMING状态把订单草稿车次、时间、价格、座位发给用户请用户确认。用户点确认并支付状态进入EXECUTING。此时代码调用MCP工具去锁座下单。如果MCP工具返回成功状态置为DONE同时生成支付链接。如果MCP工具返回库存不足状态置为FAILEDAgent返回该车次余票不足为您推荐相邻车次。这个状态机解决了两个核心问题一是防止重复下单同一会话重复点确认不会产生两笔订单二是明确失败边界Agent不需要去猜自己该不该重试状态机决定。这里我强烈建议敏感操作下单、支付、取消的确认必须是显式用户操作不能用我猜用户默认同意来设计。哪怕模型置信度99.9%也要让用户亲自点一下确认按钮。这个原则能帮你挡掉99%的投诉和纠纷。4. MCP工具层标准化接入带来的三个实际收益4.1 MCP到底解决了什么问题MCPModel Context Protocol是一个让LLM应用通过统一协议访问外部工具和数据源的开放标准。在旅游Agent里它解决的问题非常具体你不可能把票务系统的API、酒店的库存接口、天气服务的推送、地图POI查询全部写死在Agent代码里。没有MCP的时候每接一个数据源你都要在Agent代码里加一段客户端调用、出错处理、数据格式转换。十几个工具接完Agent代码变成一团乱麻。有了MCP工具提供方只需要实现一个标准ServerAgent侧通过统一协议调用工具的新增、下线、变更都不会污染Agent核心逻辑。一个直观类比MCP就像USB接口。没有USB之前鼠标、键盘、打印机各有各的接口都要专门适配。有了USB外设只需要遵守同一个协议插上就能用操作系统不需要知道具体设备内部怎么工作。4.2 MCP Server怎么组装工具注册与调用流程MCP协议里有三个核心方法tools/list列出所有可用工具、tools/call调用指定工具、resources/list暴露资源数据。在实际项目中我维护了这样一个MCP Server集群票务MCP Server接高铁/机票查询和出票。酒店MCP Server接酒店库存查询、房型比价。天气预报MCP Server查目的地未来几天天气用于行程建议。POI地图MCP Server查景点、餐厅、商圈的位置和评分。Agent编排层通过tools/call调用时请求格式是这样的{ tool: train_query, arguments: { from: 上海, to: 杭州, date: 2025-11-19, time_pref: morning } }MCP Server收到后内部翻译成上游票务系统的实际API调用把返回结果标准化后回传给Agent。Agent不会看到上游API的千奇百怪的错误码只看到一个统一的success/data/error结构。4.3 权限分级与超时控制的实战细节MCP层最容易出问题的不是能不能调通而是边界和安全。我在这上面吃过亏分享几个关键经验敏感工具不进默认列表支付、下单、改签这类高权限工具不能出现在tools/list的默认返回里。只有Agent代码在特定状态比如用户已显式确认下才通过代码内部注入的方式临时启用。这样能防止模型在对话中为了帮助用户而擅自调用支付类工具。超时设置必须比上游API更长MCP Server调用上游票务系统需要1-2秒那么给MCP Server设的超时必须是3秒以上。否则Agent端先报错上游还在慢慢处理最终两边状态不一致。错误信息要翻译上游返回500 Internal Server Error不要直接把这个错误丢给LLM去解释。MCP Server要做一层错误映射把常见问题转成业务语义比如余票不足该车次已停运价格已变动。这样Agent才知道该怎么回复用户。我在开发时还用了一个技巧给MCP Server加一个debug模式通过MCP客户端工具不少IDE都支持直接连MCP调试手动调用每一个工具验证入参和出参然后再接入Agent。这样能把工具本身的问题和Agent编排的问题隔离开排查效率高很多。5. 支付闭环Agent永不直接扣款这套状态机如何兜住风险5.1 核心原则Agent只碰支付意图不碰支付动作到了大家最关心的支付环节。先说一个我在项目里踩过最深的坑最开始我天真地以为Agent既然能调用MCP工具下单那也能直接调支付接口。结果测试阶段就出了个严重问题——模型在一个多轮对话的角落因为上下文残留把一笔待支付的订单理解成了用户已确认差点直接拉起支付。从那时起我定下一条铁律写进了系统设计文档第一条Agent在任何情况下都不得发起扣款动作Agent的职责止步于生成支付意图。具体流程是这样Agent通过MCP工具完成下单生成订单草稿状态是UNPAID。Agent向前端推一条支付卡片包含订单号、金额、商品描述和一个去支付按钮。用户点击按钮前端直接调用订单服务的createPayment接口而不是经过Agent。订单服务校验订单归属、金额、状态生成预支付单拉起微信/支付宝收银台。支付完成后由支付网关回调订单服务状态推进通知供应链出票。这里的关键在于支付动作永远发生在用户和支付网关之间Agent只是中间那个递卡片的角色。LLM可以参与推荐哪趟车、哪个酒店但永远不能代替用户完成付款。5.2 订单状态机与幂等回调支付环节最怕的是回调乱序和重复通知。微信和支付宝的回调都有可能延迟、重试而且顺序不一定。我们设计的订单状态机如下CREATED → UNPAID → PAID → CONFIRMED → COMPLETED ↘ ↘→ FAILED → REFUND ↘→ CANCELLED超时未支付在这个状态机里每一条边都被代码显式约束。比如UNPAID收到支付成功回调可以转为PAID。PAID再次收到同样的成功回调直接返回已处理不再重复触发通知。UNPAID如果用户取消转为CANCELLED如果之后又收到了支付成功回调极少见但确实发生过系统会用状态机拒绝这次状态流转并进入人工核查队列。幂等的核心是订单号事件ID。回调请求里带上支付平台的交易号我们在Redis里以pay:notify:{order_id}为key做去重同一条交易号只处理一次。这样哪怕支付平台因为网络问题连发三次回调也不会重复出票。还有一个很重要的安全细节支付金额必须完全来自服务端订单绝不能信任前端传参。用户在前端点支付时前端只传order_id金额由订单服务从数据库里取。这样即使有人恶意篡改小程序请求也没法把一笔73元的订单改成7.3元。5.3 供应链异步确认与最终一致旅游Agent和买普通商品不一样支付成功并不代表商品立即到账。高铁票需要去12306出票酒店需要和PMS系统确认有房。这些供应链动作都是异步的而且可能失败。我们的方案是支付成功后订单状态进入PAID然后通过消息队列异步调供应链出票接口。出票成功状态推进到CONFIRMED给用户推送电子票卡片。出票失败比如供应商那边没库存了状态进入FAILED同时触发自动退款流程并给用户推送一张替代方案卡片。这套设计本质上是在做最终一致——支付和出票不在同一个事务里靠状态机和消息队列最终拉齐。我在做架构时也被问过为什么不直接同步调用保证强一致答案很简单供应链系统根本不在你手里12306就是慢就是可能失败你只能接受这个现实把失败处理做好。关于退款还有一个经验谈退款接口一定要做成可重试且幂等的。同一个退款请求可能因为网络超时被重发如果每次都真退款用户会收到双倍钱。我们的做法是退款单也带唯一ID退款网关按单号去重。6. 并发与稳定性LLM慢调用的异步化改造与限流降级策略6.1 LLM慢调用的异步化思路很多开发者在做AI Agent时遇到的最大性能问题是LLM部分太慢了。一个普通的对话请求模型思考加生成要2-5秒如果遇上用户同时发来几十个请求直接同步处理会让后端服务线程池瞬间被打满。AI Agent怎么扛并发这个问题我在项目上线前就遇到过。解决思路不是扛而是绕——把同步等待变成异步任务。架构改造后的流程前端把用户消息发到网关网关立即返回一个message_id对话先进入处理中状态。Agent编排层接收到消息后把任务丢进消息队列由worker异步处理。worker从队列取任务调LLM、调MCP、更新状态机。处理完成后通过WebSocket把结果推给前端。这样做的效果是用户侧感受不到性能瓶颈服务端也不会因为慢调用而阻塞。我们压测时同步方案下40个并发请求就能把服务卡死改成异步后单机轻松扛住几百个并发主要瓶颈变成了LLM服务商的速率限制。6.2 限流、降级与热数据缓存LLM调用是有成本的也有配额限制所以必须做三层控制硬限流单用户每秒最多N次LLM请求超出直接返回请稍后再试。热点缓存高频问题比如杭州有哪些必去景点的回复做缓存命中缓存就不需要再调LLM。这个策略直接省掉了大约30%的LLM成本。降级通道LLM服务超时或报错时系统自动切换到一个基于规则和检索的兜底方案——比如直接用关键词查询数据库里的常见问答保证用户至少能得到一个有依据的回复而不是一堆错误提示。旅行社上线第一个月就碰到过LLM服务商大规模故障我们当时立即切到降级通道虽然对话体验降了一级但核心的查票、订房功能还能用没有引起大规模客诉。6.3 库存预占与超时释放和一般聊天工具不同旅游Agent背后牵扯稀缺资源。用户咨询的那趟车可能只剩3张票如果Agent让他慢慢思考票可能就没了。但反过来也不能因为用户问了一句就在票务系统锁一张票——那会造成大量孤儿订单。我们的策略是短时预占 长时确认用户在车次卡片上选中一趟车时系统调用供应链接口预占座位预占有效时间5分钟。用户在这5分钟内完成支付确认预占转为正式出票。5分钟未支付系统自动释放座位订单状态变成CANCELLED。释放后有延迟所以我们做了定时任务每分钟扫描一次超时的预占记录确保释放不会迟到。这个机制在实际运行中很有效既能保证用户在下单时有票又不至于让系统替用户承担囤票的成本。当然如果所有车次都瞬间售罄预占依然会失败那时候Agent会主动推荐相邻时间段的班次尽量留住用户。最后分享一个我个人的体会做AI旅游Agent技术难度最大不是接入大模型而是把对话体验和交易严肃性之间的缝隙填平。用户会觉得他在跟一个很聪明的助手聊天但背后每一笔订单都要经得起对账、退款、售后的检验。这套从前端对话到MCP到支付的技术栈本质上是让LLM这块很聪明的引擎装进一辆规矩行驶的车——车架、刹车、安全带一样都不能少。如果你也在做类似的Agent产品建议先从支付链路的状态机和库存预占入手这两块稳住了其他的都好说。
返回列表