ARTICLE DETAIL

资讯详情

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

AI旅游Agent四层架构:从MCP工具到支付链路的工程实践

AI旅游Agent四层架构:从MCP工具到支付链路的工程实践 做AI旅游Agent这一年我最大的感受是真正的难点从来不在“能聊天”而在“能办事”。用户跟Agent说“帮我订一张下周三从上海到成都的机票顺便定一个离太古里近的酒店”能不能把这句话变成一次真实的航班查询、一个可支付的订单、一笔能对上的账才是决定产品能不能落地的分水岭。这篇文章我把这套东西拆开聊透一个AI旅游Agent从用户提问到最终完成支付中间到底经过哪些层、每层怎么选型、MCP在其中扮演什么角色、支付链路怎么设计才能不翻车。我会按我实际开发时的思路来写不会讲空理论涉及的代码和流程都是可直接参考的做法。适合正在做AI Agent产品或想把大模型接入真实业务交易的同学阅读前端、后端、Agent架构相关岗位都可以从中拿到对应的细节。1. 先看整体为什么一个AI旅游Agent要拆成四层1.1 一个Agent的本质LLM只做决策不直接碰真实世界理解Agent架构的关键是分清“模型”和“系统”的边界。大模型再聪明它也只是在生成文字它不知道真实的航班余票、酒店房价、用户钱包余额。而旅游业务又恰恰是一个强依赖实时数据和资金操作的场景一条错误信息就可能让用户误机、付错钱。所以整个系统的第一原则是LLM负责“说什么”系统负责“做什么大模型是决策者它把用户的自然语言意图翻译成结构化指令然后由MCP工具去执行这些指令。我把这个逻辑类比成“大脑和手”的关系LLM是大脑MCP是手支付系统是最后一道门。没有手的大脑只能空谈没有门的手则会乱花钱。这个分层好处很直接每一层可以独立演进。前端想换交互框架不动后面MCP想加新工具不影响对话协议支付想换通道Agent层甚至无感知。模块间通过清晰的数据协议连接出问题时也能快速定位到具体某一层。1.2 分层设计的核心动因为什么不要把支付直接塞给LLM在规划技术栈时最需要考虑清楚的一点是哪些能力让模型直接调用哪些能力必须包一层壳。一开始有同事问能不能直接把支付SDK的方法暴露给大模型让它自己决定什么时候扣款我的答案是坚决不行这个念头本身就很危险。大模型是概率生成器它可能记住一句话的语义但记不住签名算法、商户号、私钥和回调验签规则更谈不上对资金安全负责。支付能力如果裸露在模型面前等于把财务室钥匙交给一个非常聪明但随时可能幻觉的员工你永远不知道它哪次会把钱付错。所以支付不能作为原生Function给模型必须封装成MCP后面的“受控工具”并且要加一道人工确认机制Agent只负责填好订单草稿真正扣款必须由用户在支付收银台完成。这就是我在后面章节反复强调的原则按四层架构来组织项目后每个风险的归属都变得很清晰前端对话层负责体验与意图表达Agent编排层负责意图理解和决策MCP工具层负责执行动作和读数据支付服务层负责资金、订单和账务安全。每一层有明确的职责边界跨层调用必须通过协议和数据对象完成不允许随意穿透。2. 前端对话层让用户感觉在“聊天”而不是在用表单2.1 前端技术选型与组件布局旅游Agent的前端比普通Chatbot复杂得多。用户输入几句话之后页面不但要展示文字回复还要呈现出航班列表、酒店卡片、行程时间线、支付确认框等结构化内容。如果前端只做一个文本框加上下滚动列表产品根本没法用。我建议按“消息流卡片”的方式设计界面。每个消息可以有不同的type普通文本走Markdown渲染结构化数据走卡片组件。前端组件可以划分为消息列表区、输入区、工具状态区三块。工具状态区很重要用户在Agent思考或查询时能看到“正在查询航班”“正在锁定酒店库存”等过程提示而不是面对一个静默等待的界面这对旅游场景尤其关键因为查询超时是常有的事。前端框架层面React和Vue都可以选哪个取决于团队熟悉度我这边用的是ReactVite。真正决定体验的是消息流协议的设计和状态管理方式这块我在下一节展开。组件库方面直接选成熟方案可以用Antd或自定义轻量组件不建议花时间从零造轮子。旅游场景还有一个特色需求是地图展示和日期选择如果集成地图SDK建议在卡片组件里异步加载不要阻塞主消息流。2.2 消息协议设计不止有人话还有工具状态前端和Agent服务端之间传输的消息不能只包含“人话”还要能表达工具调用状态、结构化卡片和交互指令。我在实际项目中设计了一套协议消息用type字段区分不同角色user_text用户输入的文本或语音转写结果agent_textAgent生成的文字回复支持流式增量agent_statusAgent内部状态比如正在调用哪个工具、经历了哪几步product_card查询结果的结构化数据例如航班或酒店列表order_card待确认的订单信息包含金额、商品明细、支付按钮。每条消息再带requestId、sessionId和timestamp用于前端渲染校验和问题排查。这套协议看起来简单但解决了两个实际问题第一Agent输出对话文本和工具结果解耦前端可以分别渲染第二前端可以基于order_card触发支付流程而不是靠正则匹配文本里的“链接”非常可靠。2.3 流式输出与弱网处理Agent的文字回复如果等全部生成完再展示用户很容易觉得卡顿所以必须做流式输出。后端用SSE或WebSocket推送增量内容前端用ReadableStream读取后逐段append到消息里。我在这个环节踩过的坑是Markdown的格式在流式中容易被切碎比如代码块、加粗符号还没闭合就被渲染页面会闪一下乱码。我的解法是前端不边收边渲染Markdown而是先累积原始内容用节流策略约200毫秒做一次整体重渲染既保证流畅又避免半截语法闪烁。弱网处理是旅游场景躲不开的问题用户在高铁上、景区里信号经常不稳定。前端必须有断线重连机制加上消息序号补偿。我采用的方式是WebSocket连接断开后自动重试重连后把本地最新消息序号上报给服务端服务端从断点补发遗漏内容避免用户对话突然中断且不知道发生了什么。至于SSE还是WebSocket我的建议是如果服务端还需要给前端推送“支付结果”“订单状态变化”这类事件直接上WebSocket双向通信省去很多绕路工夫。3. MCP层Agent的“手”是怎么长出来的3.1 MCP到底解决什么问题MCP全称Model Context Protocol很多人把它当成某种神秘框架其实它的本质很简单定义了一套标准协议让大模型能够通过统一的方式发现和调用外部工具。没有MCP之前每个Agent项目都要自己写Function Calling的逻辑工具定义散落在各处换一个模型就要重写一遍适配代码。有了MCP工具能力可以独立部署成一个ServerAgent只需要通过MCP客户端连接它就能获得工具列表、调用工具、拿到结果。在旅游Agent场景里MCP的价值特别明显。因为旅游业务涉及的工具数量多、变化频繁机票查询、酒店搜索、景点门票、签证信息、优惠券核销每一项都是独立的系统和数据源。把它们全部塞进Agent主程序的函数列表会让主程序越来越臃肿每次改一个供应商对接都要发布整个服务。而拆成MCP Server后每个数据源可以独立开发、独立部署、独立更新。我举一个实际的类比没有MCP时Agent像是请了一个全科医生把所有科室的知识都背下来有了MCPAgent是导诊台分诊到对应科室由专科医生来做检查和治疗。前者的知识会过时后者的能力可持续按需接入。3.2 一个MCP Server的搭建流程目前MCP的实现主要有Python和TypeScript两种路径。Python体系里可以用FastMCP框架快速开发TypeScript官方SDK则适合嵌入Node服务。我这里以Python的FastMCP为例展示一个查询机票工具的搭建骨架from fastmcp import FastMCP mcp FastMCP(travel-agent-tools) mcp.tool() def search_flights( departure_city: str, arrival_city: str, date: str ) - dict: 查询两个城市之间的航班返回航班列表。日期格式为YYYY-MM-DD。 data real_ota_api.query(departure_city, arrival_city, date) return {flight_list: data}搭建一个MCP Server的完整步骤我整理为四步每一步都有明确的意图第一步定义工具集把Agent需要的能力列出来每个工具只做一件事工具描述里写清触发场景和参数含义第二步接数据源工具内部连接真实的OTA、酒店或票务API返回结构尽量扁平化第三步注册到Agent在Agent侧配置MCP客户端地址然后通过代码把Server列表加载进来第四步做鉴权与隔离查询类工具允许Agent自由调用涉及下单或支付的工具则必须带用户确认标记并做最小权限控制。我的项目里MCP Server和Agent主服务是分开部署的Agent主服务通过HTTPSSE的方式连接MCP Server。Web场景里采用HTTP传输比stdio更合适因为Agent服务不一定是本地进程跨机器的stdio通信反而会增加部署麻烦。3.3 工具Schema设计给LLM的“说明书”要专业MCP工具调用的成功率很大程度上不取决于模型而取决于工具的Schema写得好不好。大模型是靠描述和参数名来理解工具的很多初学Agent开发的同学把工具描述写得很随意结果模型频繁调用错误的工具或传错参数。我总结了几个必须遵守的原则工具描述要写明“什么时候用”和“什么时候不要用”。比如search_flights的描述明确写“当用户询问航班时调用不要用于查询酒店和火车票”这能显著降低幻觉调用参数个数控制在5个以内参数类型用简单json类型。复杂嵌套对象对模型的生成压力很大也容易出错返回结构要稳定且扁平不要返回多层嵌套的大JSON最好是“列表关键字段”的形式减少模型截断和混淆。下面是我项目中一个酒店搜索工具的Schema示例{ name: search_hotels, description: 根据城市和入住日期搜索酒店列表。不要用于查询民宿或公寓。, parameters: { type: object, properties: { city: {type: string, description: 城市名如杭州}, check_in_date: {type: string, description: 入住日期YYYY-MM-DD}, check_out_date: {type: string, description: 离店日期YYYY-MM-DD}, star_rating: {type: integer, minimum: 1, maximum: 5, description: 星级筛选可选} }, required: [city, check_in_date, check_out_date] } }这种清晰的结构模型解析几乎零歧义。实际测试中工具Schema规范化之后调用成功率从大约70%提升到了95%以上这个数据在团队内部非常有说服力。3.4 MCP的安全边界与权限控制接入了MCP之后Agent能调用的能力变多了安全问题随之而来。MCP工具会暴露给模型但并不是所有工具都适合模型在任何场景下直接调用。我的经验是按危险级别把工具分成三个等级第一级是只读工具比如航班查询、酒店搜索、天气查询Agent可以自由调用不需要额外授权。第二级是写操作但可逆或低风险比如创建订单草稿、锁定库存、加入收藏这类操作需要带上用户会话和操作确认。第三级是资金操作包括发起支付、退款、改签这一类绝不能由Agent单方面触发必须经由用户明确的主动操作通常由前端拉起支付收银台由用户完成支付。实现上MCP Server内部在工具函数入口处增加一个上下文标记如果标记不是“user_confirmed”直接返回“用户未确认”的错误。这个设计早期我没当回事后来在一次测试里模型自作主张调用了锁房接口虽然没有产生实际扣款但暴露了权限漏洞从那以后“用户确认标记”就成了强制项。4. 支付层让Agent真正“收钱”的临门一脚4.1 支付链路的总原则LLM永不碰密钥聊到支付层我必须先定一个硬规矩任何支付相关的密钥、商户号、签名逻辑都不能出现在Agent的代码路径里更不能作为MCP工具暴露给模型。Agent与支付系统的交互只能通过与后端业务服务之间的受控接口完成。这个原则在旅游Agent里尤其重要因为一次完整的交易会经历多个环节意图查询、价格确认、下单锁库存、支付、出票、退款任何一个环节出错都要由明确的业务逻辑兜底。模型的作用在“下单确认前”就截止了之后是幂等的业务服务来完成状态机流转。我把支付抽象成独立的后端模块与Agent编排层通过“订单服务接口”交互。Heuristic原则是Agent只能跟订单服务说话订单服务再跟支付网关说话模型既看不到支付参数也看不到回调内容。这样即使模型发生幻觉、Prompt注入或者被恶意用户乱引导它能造成的最大破坏也就是“多创建几个未支付的订单草稿”而不是直接刷走额度。4.2 从下单到回调的完整流程实际流程比想象的长我用一个具体例子来说明。用户跟Agent说“帮我订这个酒店三晚”Agent完成搜索、选出酒店后真正的支付链路是这样流转的Agent调用MCP的create_order工具把商品信息和用户ID传给订单服务订单服务生成一个预订单状态为PENDING返回订单号前端收到order_card消息展示待确认金额和商品明细用户点击“确认支付”按钮前端把确认动作传回订单服务订单服务调用支付网关的统一下单接口拿到支付参数并签名后返回给前端前端用SDK调起支付收银台用户完成支付支付网关异步回调订单服务的回调地址订单服务验签、核对金额、更新订单状态订单状态变化再通过WebSocket推送给前端前端更新order_card为支付成功同时Agent也能订阅到支付成功事件用文字回应用户“房已订好”。注意里面有两个关键点一个是订单服务必须维护预订单和支付单两个概念预订单不等于支付单支付单才是真正和第三方网关对应的记录。第二个是前端调起支付前器具先向后端拿一次最新确认价防止Agent对话过程和实际支付之间出现价格波动这种保护在对价格敏感的旅游行业特别有用。4.3 掉单、幂等与对账支付的钱坑怎么填支付环节我最常被问到的问题就是“回调没来怎么办”。分布式环境下支付网关的回调通知可能延迟、重复甚至丢失掉单是常态而不是异常。我的处理方案有三层兜底。第一层前端收到支付成功返回值后主动调一次订单服务刷新状态不等回调第二层订单服务提供主动查单接口前端在收银台关闭后调起轮询每5秒查一次订单状态最多查两分钟第三层后端有一个定时任务每隔10分钟扫一遍所有状态为“已支付但未确认”的订单主动向支付网关发起查单请求。幂等处理是另一个送分题但很多人会做错。支付回调可能因为网络抖动被发送多次订单服务必须保证多次回调只产生一次状态变更。我的做法是每个支付单维护一个callback_tx_id字段处理回调前先按这个字段去重如果已经处理过就直接返回成功响应。同时在数据库层面给支付单号加唯一索引双保险。对账是我坚持必须做的事情。每天凌晨从支付网关下载前一天的账单与本地订单表做金额和状态比对。这个动作看起来老土但它能在用户投诉之前发现“代理商漏结算”“回调丢了但钱扣了”这类问题。旅游行业如果涉及多个供应商分账还可能用到支付网关的分账能力把房费、佣金、税费按规则自动拆分这块在初期可以不用做但架构上要留出扩展位。5. 实操过程从前端到支付的一整串代码与联调顺序5.1 最小可运行版本要写哪些文件如果你要从零搭建一个AI旅游Agent的最小可运行版本我会建议只做五类文件不要贪多前端目录页面入口、消息列表组件、卡片组件、SSE或WebSocket客户端Agent服务对话处理入口、会话管理、MCP客户端配置MCP Server工具注册文件、数据查询逻辑订单服务订单创建接口、支付下单接口、回调处理接口、查单接口基础设施数据库表结构、缓存配置、支付网关沙箱配置。先把这个最小闭环跑通再把RAG知识库、多Agent协作、复杂权限这些逐层加进去。很多团队一上来就规划十几个微服务和复杂的LangGraph流程结果两周连一个能支付的demo都跑不出来这是我最想提醒的。5.2 前端核心代码简化示例前端的关键在于“消息流渲染”和“支付卡片触发”。下面是一个高度简化的React示例核心是消息分类型处理不同type走不同渲染组件function ChatMessage({ msg }) { if (msg.type agent_text) { return MarkdownContent text{msg.text} /; } if (msg.type product_card) { return HotelFlightCards data{msg.data} /; } if (msg.type order_card) { return OrderConfirmCard order{msg.data} onConfirm{() handlePayment(msg.data.orderId)} /; } if (msg.type agent_status) { return ToolStatus text{msg.data.currentStep} /; } return null; } async function handlePayment(orderId) { const res await fetch(/api/pay, { method: POST, headers: {Content-Type: application/json}, body: JSON.stringify({ orderId }) }); const payParams await res.json(); // 调用前端支付SDK拉起收银台具体SDK按所接入的支付平台提供 window.SomePaySDK.invoke(payParams); }前端不要自己拼支付参数所有支付参数都由后端下单接口返回前端只是接力把参数传给SDK。这个话题还要补充一点如果想在微信内置浏览器里做H5支付会受到平台限制必须走对应的JSAPI或小程序支付普通链接方案在微信里会被拦截。产品在设计流程时就要考虑用户在什么环境下完成支付不能默认“只要能弹扫码框就行”。为了支持流式展示核心片段如下const ws new WebSocket(/ws); ws.onmessage (event) { const data JSON.parse(event.data); if (data.type delta) { setText(prev prev data.text); } };5.3 MCP Server核心代码示例FastMCP里注册一个带“用户确认”的锁库存工具大概长这样from fastmcp import FastMCP mcp FastMCP(travel-tools) mcp.tool() def lock_hotel_room(hotel_id: str, room_type: str, date: str, user_confirmed: bool False) - dict: 锁定酒店库存。必须在用户明确确认预订意向之后才能调用。user_confirmed必须为True。 if not user_confirmed: return {ok: False, reason: USER_NOT_CONFIRMED} # 实际调用酒店库存服务锁定房间 result hotel_service.lock_room(hotel_id, room_type, date) return {ok: result.success, lock_id: result.lock_id}这个示例的核心不是业务代码而是user_confirmed这个强制参数。让模型在工具参数层面主动表达“用户是否已确认”比依靠外部状态更直接可以在模型层就拦截大多数误操作。5.4 后端下单与回调验签示例后端支付模块用最简单的伪代码表示# 创建支付单返回前端SDK需要的参数 app.post(/api/pay) def create_payment(order_id: str, user_id: str): order order_service.get(order_id) if order.user_id ! user_id: raise PermissionError payment payment_service.create( order_idorder.id, amountorder.amount, subjectorder.product_name, ) # 返回给前端的是支付参数不是密钥 return { pay_params: payment.sign_params, payment_id: payment.id } # 回调处理验签 幂等 状态流转 app.post(/api/payment/callback) def payment_callback(): raw request.get_data() ok pay_gateway.verify_sign(raw, callback_tx_id) if not ok: return invalid signature, 403 if payment.is_processed(callback_tx_id): return already processed, 200 payment.mark_paid(callback_tx_id) order_service.mark_paid(payment.order_id) return success, 200回调验签我多说一句验签是防止伪造回调的关键必须用支付平台提供的签名算法严格验证请求头和报文不要图省事只校验来源IP。回调处理完要返回明确的业务态让网关知道“我收到了”避免它反复重试。5.5 联调顺序与自测清单联调顺序建议从下往上先打通最底层再回头调体验。我列一下我的实测顺序阶段一本地调试MCP Server用MCP官方调试工具或简单测试脚本直接调用工具验证参数解析、返回结果和异常分支阶段二接入Agent框架让Agent在对话中正确调用MCP工具反复测试不同问法的触发准确率阶段三打通前端流式验证WebSocket推送和卡片组件渲染重点检查断线重连和消息补偿阶段四支付沙箱用沙箱环境跑完“确认订单-调起收银台-支付成功-回调-订单更新”全链路阶段五安全与压力做Prompt注入测试、并发下单测试、回调重复提交测试。自测清单我通常会写成一个表格验证项对应预期结果逐项打勾。这个习惯让团队在联调时少走很多弯路。6. 常见问题与排查技巧实录6.1 高频故障速查表我整理了过去半年在AI旅游Agent开发中被问得最多的问题做成一个速查表问题现象根因排查思路与解决建议MCP工具明明存在但Agent不调用工具描述不清晰模型不知道该何时用检查描述是否写明触发条件必要时在对话开头给模型说明“你可以使用哪些工具”工具调用报JSON参数错误Schema有必填参数未填或模型生成了多级嵌套对象简化参数结构必填项最小化不要用复杂嵌套数组传数据前端流式输出中断WebSocket连接断开或服务端流式生成异常看服务端日志里是否有截断加断线重连补偿消息序号用户支付成功但订单仍显示未支付回调延迟或前端没有触发主动查单前端收银台关闭后主动查单后端跑定时对账任务支付回调重复上报导致订单重复发货缺少幂等处理根据回调流水号去重数据库加唯一索引Agent在对话中虚构价格模型没有真实比价数据或工具返回不完整对话中限制模型只能引用工具返回的数值不要让其凭记忆估算这个表是我很多次踩坑之后沉淀出来的给它单独写一个使用说明排障不要从表层现象猜先从“这一层的数据从哪里来”入手。前端问题看消息流Agent问题看工具调用记录支付问题看订单状态和回调日志。6.2 几个值得单独说明的坑第一个坑是MCP工具返回的数据太复杂。我早期把一个供应商几十个字段的原始报文直接返回给模型结果模型开始“自由发挥”把折扣价当成原价告诉用户。后来我把字段精简为“航班号、时间、舱位、含税价、余票”五个核心字段模型表现立刻稳定了。工具返回的数据要提前清洗好不要在模型侧做复杂计算。第二个坑是支付沙箱和真实环境的差异。有些支付平台的沙箱接口没有真实扣款校验甚至回调速度比生产快很多倍导致你在沙箱里测出“秒到账”上生产后却因为回调延迟被用户投诉。我们后来专门在沙箱里模拟回调延迟30秒的情况前端主动查单和后端定时任务的逻辑就是在那个阶段补出来的。第三个坑是会话时间跨度过长。用户分几天陆续咨询Agent在会话恢复时可能丢失前文的支付上下文。我要求所有订单卡片在恢复会话时重新拉取订单状态并明确提示用户“当前订单等待支付”或“已过期”避免用户对着一个旧卡片反复点支付。6.3 日志与可观测性设计分布式链路里排查问题最耗时间的往往是“不知道哪一层丢了消息”。我建议每个关键节点都打印同一串traceId从对话请求开始到MCP工具调用、下单、支付回调全部带上这个ID。我在实际项目里用的方案是前端生成requestId后端在中间件里透传下去MCP工具执行、订单创建、回调处理都在日志里带上它。排查一次用户投诉时用traceId串联所有日志定位到问题只需要几分钟这在单体时代不可想象。除了日志我还给每个MCP工具的调用次数、成功率和平均耗时做了监控面板。如果某天search_flights的成功率突然下降大概率是供应商接口那边出了问题而不是Agent代码的锅。这种“工具即接口”的监控思路也与按四层架构组织系统一脉相承。做AI旅游Agent的过程中我最大的体会是这个系统的技术栈并不神秘前端就是消息流管理Agent层就是意图加工具调用MCP层把工具标准化支付层守住资金安全的底线。难点在于把每一层的协议设计清楚把关键流程的兜底做好。很多时候真正拉开差距的不是模型选得多新、框架用得多前沿而是你在回调幂等、工具参数校验、会话恢复这些“脏活”上有没有做到位。最后分享一个实用心得把上面的最小链路实现一遍后先不急着加复杂功能。拿一批真实用户的对话记录跑一遍——相比设计时预想的场景用户问法永远比文档里写的更多变。在这个过程中你会发现MCP工具的描述要怎么调整、支付卡片的确认逻辑哪里需要强化、前端状态提示在哪里不够明确。把这些边角修完整套Agent技术栈才算真正长在自己手里。
返回列表