☰
AI旅游Agent全栈拆解:MCP工具编排与支付闭环实战
2026/10/7 12:19:22 网站建设 项目流程

1. 为什么我要拆这个AI旅游Agent的技术栈

去年年底接了个私活,帮一个做定制游的团队搭一套AI旅游助手。需求听起来不复杂:用户用自然语言描述想去哪、几天、预算多少、偏好什么,Agent给出完整行程,最后能直接下单支付。但真动手才发现,这东西横跨的领域比想象中多得多——前端对话交互、意图理解、工具调用协议、第三方服务编排、订单状态机、支付回调,每一层都有坑。

市面上讲AI Agent的文章大多停留在"提示词怎么写""怎么接大模型API"这个层面,很少有人把一条完整的链路从头到尾拆开讲。尤其是MCP(Model Context Protocol)出来之后,工具调用的组织方式变了,很多老架构需要重新思考。再加上支付这一环,涉及订单、回调、对账,是真正决定产品能不能跑通商业闭环的关键。

这篇东西我打算按我自己实际搭的那套架构来拆,从用户在前端敲下第一句话,到支付成功回调落库,把每一层的技术选型、核心逻辑、踩过的坑都摊开讲。适合正在做或打算做AI应用、尤其是涉及多工具编排和交易闭环的开发者看。前端部分我会讲对话流怎么设计,中间层重点讲MCP怎么组织工具,后端讲订单和支付怎么保证一致性。不堆概念,讲能直接抄的东西。

2. 整体架构设计与分层思路

2.1 四层架构的划分逻辑

我最终落地的架构分成四层,从上到下依次是对话交互层、Agent编排层、工具服务层、交易与数据层。这个划分不是拍脑袋定的,而是根据"变更频率"和"职责边界"两个维度切出来的。

对话交互层负责和用户打交道,包括消息流式渲染、多轮上下文管理、富文本卡片展示(比如行程单、价格明细)。这一层变更最频繁,产品经理今天想加个快捷回复,明天想改个气泡样式,所以必须独立,不能和业务逻辑耦合。

Agent编排层是大脑,负责理解意图、决定调用哪些工具、组织工具返回结果、生成最终回复。这一层用MCP来管理工具,是整套系统的核心。

工具服务层是手脚,每个工具封装一个具体能力:查航班、查酒店、查景点、算价格、创建订单、发起支付。这些工具可以是内部服务,也可以是第三方API的封装。

交易与数据层管钱和数据,包括订单状态机、支付网关对接、回调处理、对账。这一层对一致性要求最高,不能出错。

这么分的好处是:换个大模型,只动编排层;换个支付渠道,只动交易层;前端改版,只动交互层。各层之间通过明确定义的接口通信,互不干扰。

2.2 为什么选MCP而不是自己写Function Calling

早期我是直接用大模型原生的Function Calling来做工具调用的,工具定义写在系统提示词里,模型返回一个JSON,我解析后去调对应服务。这套东西跑通没问题,但工具一多就乱。

问题出在三个地方。第一,工具定义和业务代码耦合,加一个工具要改提示词、改解析逻辑、改路由,三处联动容易漏。第二,不同模型厂商的Function Calling格式不一样,换模型要重写一遍。第三,工具多了之后提示词爆炸,模型经常选错工具或者参数填错。

MCP解决的正是这几个问题。它把工具的定义、发现、调用标准化了。工具以Server的形式独立部署,通过标准协议暴露自己的能力(resources、tools、prompts),Agent作为Client去连接这些Server,动态发现有哪些工具可用。加工具只需要部署一个新的MCP Server,Agent端不用改代码。换模型也不影响,因为MCP是模型无关的。

注意:MCP不是银弹。它的优势在于工具数量多、需要动态发现、需要跨团队复用的场景。如果你就三五个固定工具,自己写Function Calling反而更轻。别为了用而用。

2.3 数据流全景

一次完整的用户请求,数据是这样流动的:

  1. 用户在前端输入"帮我规划一个三天两晚的三亚行程,预算5000以内,带小孩"
  2. 前端把消息通过WebSocket发给Agent编排层
  3. 编排层组装上下文,调用大模型
  4. 模型判断需要调用工具,通过MCP Client向对应的MCP Server发起调用
  5. 比如调用"景点查询"Server查三亚亲子景点,调用"酒店查询"Server查符合预算的酒店
  6. 工具返回结构化数据,编排层把结果喂回模型
  7. 模型生成行程方案,流式返回给前端
  8. 用户确认后点击"立即预订",前端调后端创建订单
  9. 后端生成订单,调支付网关,返回支付参数
  10. 用户完成支付,支付网关回调后端,后端更新订单状态,通知Agent

这条链路里,第4到第6步是MCP在起作用,第8到第10步是交易层在起作用。下面逐层拆。

3. 前端对话交互层的实现细节

3.1 流式输出的处理

AI旅游Agent的对话体验,流式输出是底线。用户等三秒才看到一整段回复,和逐字蹦出来的感觉完全不一样。我用的是SSE(Server-Sent Events),比WebSocket轻,单向推送够用。

前端这边核心是处理三种事件类型:text_delta(文本增量)、tool_call(工具调用通知)、done(结束)。收到text_delta就往消息气泡里追加字符,收到tool_call就显示一个"正在查询航班..."的loading状态,让用户知道Agent在干活而不是卡住了。

const eventSource = new EventSource('/api/chat/stream?session_id=xxx'); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); switch (data.type) { case 'text_delta': appendToBubble(data.content); break; case 'tool_call': showToolStatus(data.tool_name, data.status); break; case 'done': eventSource.close(); break; } };

这里有个细节:工具调用的状态展示很重要。旅游场景下,查航班、查酒店、算价格可能要好几秒,如果不给反馈,用户会以为卡死了。我实测下来,加上"正在为你查询三亚的亲子酒店..."这种具体状态,用户等待容忍度能提升不少。

3.2 富文本卡片与结构化展示

纯文本对话在旅游场景下不够用。行程单需要表格、价格需要高亮、酒店需要图片。我的做法是让模型在回复里嵌入特定标记,前端解析后渲染成卡片。

比如模型输出这样的结构:

[行程卡片] day: 1 items: - time: "09:00" activity: "亚龙湾热带天堂森林公园" note: "适合亲子,有玻璃栈道" - time: "14:00" activity: "亚龙湾沙滩" note: "免费,带好防晒" [/行程卡片]

前端用正则匹配这个标记块,解析成JSON,渲染成时间轴卡片。这样既保留了对话的自然感,又能展示结构化信息。

实操心得:不要让模型直接输出HTML或Markdown表格,解析起来容易出错。用自定义的简单标记块,容错率高,前端也好处理。标记块的格式要在系统提示词里写死,给几个示例,模型基本不会跑偏。

3.3 多轮上下文的管理

旅游规划是个多轮过程,用户会不断调整需求。"预算再低点""换成海景房""第二天不想爬山"。上下文管理做不好,模型就会忘记前面说过的约束。

我的方案是维护一个结构化上下文对象,而不是简单地把历史消息全塞给模型。这个对象包含:用户画像(带小孩、老人、预算敏感度)、已确认的行程要素(目的地、天数、日期)、待确认的要素、历史对话摘要。

每轮对话后,用一个轻量模型(或者规则)更新这个对象。下一轮请求时,把结构化对象序列化后放进系统提示词,只带最近几轮原始对话。这样既控制了token消耗,又保证了关键约束不丢失。

context = { "destination": "三亚", "days": 3, "budget": 5000, "travelers": {"adults": 2, "children": 1}, "preferences": ["亲子", "海边"], "confirmed": ["目的地", "天数"], "pending": ["具体日期", "酒店档次"] }

这个对象在服务端维护,前端只负责展示。用户改需求时,模型输出一个更新指令,服务端更新对象,下一轮生效。

4. Agent编排层与MCP工具组织

4.1 MCP的核心概念与工作方式

MCP的本质是一个客户端-服务端协议,用来标准化大模型和外部工具之间的通信。它定义了三种能力:Resources(可读取的数据,类似文件)、Tools(可调用的函数)、Prompts(预设的提示词模板)。旅游Agent主要用Tools。

一个MCP Server启动后,会通过标准接口暴露自己有哪些工具、每个工具需要什么参数、返回什么结构。Agent作为Client连接上去,先调list_tools拿到工具清单,再根据用户意图决定调哪个。

传输方式上,本地工具用stdio(标准输入输出),远程工具用HTTP+SSE。我这边大部分工具是内部服务,走HTTP。每个MCP Server独立部署,可以单独扩缩容。

4.2 旅游场景的工具划分

我把工具按业务域拆成几个MCP Server:

Server名称负责的工具数据来源
目的地Server景点查询、天气查询、当地攻略内部景点库+第三方天气API
交通Server航班查询、火车查询、接送机第三方票务API
住宿Server酒店查询、房型查询、库存校验酒店供应商API
计价Server行程报价、优惠计算、税费计算内部计价引擎
订单Server创建订单、查询订单、取消订单内部订单服务
支付Server发起支付、查询支付状态支付网关

这么拆的好处是,每个Server的职责单一,团队可以并行开发。比如住宿Server由对接酒店供应商的同事维护,订单Server由后端同事维护,互不阻塞。

4.3 工具描述怎么写才不让模型选错

MCP工具能不能被正确调用,关键在工具描述。我踩过的坑是:描述写得太简略,模型分不清"查酒店"和"查房型"的区别,经常调错。

好的工具描述要包含四要素:做什么、什么时候用、参数含义、返回什么。举个例子:

{ "name": "search_hotels", "description": "根据城市、入住日期、预算范围搜索可预订的酒店列表。当用户需要找住宿但还没确定具体酒店时使用。返回酒店名称、星级、价格区间、位置。注意:这个工具只返回列表,不返回具体房型,确定酒店后需要用get_room_types查房型。", "inputSchema": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,如'三亚'"}, "check_in": {"type": "string", "description": "入住日期,格式YYYY-MM-DD"}, "check_out": {"type": "string", "description": "离店日期,格式YYYY-MM-DD"}, "budget_max": {"type": "number", "description": "每晚最高预算,单位元"} }, "required": ["city", "check_in", "check_out"] } }

注意描述里那句"这个工具只返回列表,不返回具体房型",就是防止模型以为一次调用能拿到所有信息。这种边界说明能大幅降低调用错误率。

踩坑记录:一开始我没写"什么时候用",结果模型在用户只是随口问"三亚有什么好玩的"时也去调酒店查询。加上使用场景说明后,误调用明显减少。

4.4 多工具编排与结果聚合

用户一句"帮我规划三亚三天行程",Agent可能要调五六个工具:查景点、查天气、查酒店、查航班、算价格。这些调用有依赖关系——得先确定景点和酒店,才能算价格。

我的处理方式是让模型自己决定调用顺序,但通过提示词引导它分阶段进行。第一阶段收集信息(景点、天气、酒店、航班),第二阶段聚合算价,第三阶段生成方案。每个阶段的工具返回后,把结果结构化存到上下文里,供下一阶段使用。

这里有个性能优化点:能并行的工具调用要并行。查景点、查天气、查航班之间没有依赖,可以同时发起。我在编排层做了个简单的依赖分析,把无依赖的工具调用打包并行执行,整体响应时间从8秒降到了3秒左右。

# 并行调用无依赖工具 import asyncio async def gather_travel_info(city, dates): tasks = [ call_mcp_tool("destination_server", "search_attractions", {"city": city}), call_mcp_tool("destination_server", "get_weather", {"city": city, "dates": dates}), call_mcp_tool("transport_server", "search_flights", {"to": city, "dates": dates}), ] results = await asyncio.gather(*tasks) return results

4.5 工具调用失败的处理

第三方API不稳定是常态。航班查询超时、酒店库存接口报错,这些都会发生。如果工具调用失败就直接告诉用户"查询失败",体验很差。

我的策略是降级+重试+兜底。降级是指某个工具失败时,用缓存数据或近似数据顶上。比如酒店查询失败,就用上次查询的缓存结果,标注"价格可能有变动"。重试是指对超时类错误自动重试一次。兜底是指所有工具都失败时,Agent要能生成一个不依赖实时数据的通用建议,而不是直接报错。

在MCP层面,工具返回的错误信息要结构化,包含错误类型(超时、无数据、参数错误),这样编排层才能针对性处理。

5. 交易与支付层的核心实现

5.1 订单状态机的设计

旅游订单的状态比普通电商复杂,因为涉及"待确认""已确认""待支付""已支付""已出行""已完成""已取消"等多个状态,还有超时未支付自动取消的逻辑。

我用状态机来管理,每个状态定义允许的迁移动作。核心状态和迁移如下:

当前状态允许动作目标状态触发条件
待确认确认待支付库存校验通过
待确认取消已取消用户取消或库存不足
待支付支付已支付支付回调成功
待支付超时已取消超过15分钟未支付
已支付出行已出行到达出行日期
已支付退款已取消用户申请退款

状态迁移必须走统一入口,不能各处直接改数据库。我封装了一个transition(order_id, action)方法,内部校验当前状态是否允许该动作,允许才更新,同时记录状态变更日志。这样出了问题能追溯。

注意:状态机一定要加乐观锁。并发场景下,用户点支付的同时超时任务也在跑,不加锁会出现状态覆盖。我用的是版本号机制,更新时校验版本号,不匹配就重试。

5.2 支付网关对接的关键点

支付这块,我用的是主流的第三方支付网关。对接的核心是三个接口:统一下单、支付回调、订单查询。

统一下单时,后端生成商户订单号(要保证全局唯一,我用的是"业务前缀+时间戳+随机数"),把订单金额、商品描述、回调地址传给网关,网关返回支付参数(比如二维码链接或支付表单)。前端拿到参数后唤起支付。

支付回调是重点。网关支付成功后会异步通知后端,后端要做的处理是:验签、校验金额、更新订单状态、返回成功应答。这里有几个坑:

第一,回调可能重复。网关没收到成功应答会重发,所以回调处理必须幂等。我的做法是用商户订单号做唯一索引,处理前先查订单状态,已经是"已支付"就直接返回成功,不重复处理。

第二,回调可能乱序。支付成功回调和退款回调可能先后到达,要按业务逻辑判断。我的做法是回调里带上网关的流水号和时间戳,按时间戳判断新旧。

第三,金额必须校验。回调里的金额要和订单金额比对,不一致要告警并拒绝。这是防篡改的关键。

def handle_payment_callback(data): # 1. 验签 if not verify_signature(data): return {"code": "FAIL", "msg": "签名错误"} # 2. 查订单 order = get_order_by_no(data["out_trade_no"]) if not order: return {"code": "FAIL", "msg": "订单不存在"} # 3. 幂等校验 if order.status == "已支付": return {"code": "SUCCESS", "msg": "已处理"} # 4. 金额校验 if order.amount != data["total_amount"]: alert("金额不一致", order, data) return {"code": "FAIL", "msg": "金额错误"} # 5. 更新状态 transition(order.id, "支付", extra={"gateway_trade_no": data["transaction_id"]}) return {"code": "SUCCESS", "msg": "OK"}

5.3 支付与订单的一致性保障

支付和订单是两个系统,怎么保证一致性?我用的是本地消息表+定时补偿的方案。

流程是这样的:创建订单时,在本地事务里同时写订单记录和一条"待发起支付"的消息记录。然后异步去调支付网关。如果调用成功,更新消息状态为"已发起"。如果失败,定时任务扫描"待发起"的消息重试。

支付回调更新订单状态时,同样在本地事务里写订单状态和一条"待通知Agent"的消息。Agent侧消费这条消息,更新对话上下文,告诉用户"支付成功,行程已确认"。

这套方案的好处是不依赖分布式事务框架,用本地事务+消息表+定时补偿就能保证最终一致。对于旅游这种对实时性要求不是极高的场景,完全够用。

实操心得:定时补偿的扫描频率要控制好。太频繁浪费资源,太慢影响体验。我用的是30秒扫一次,只扫超过1分钟还没处理的消息。另外补偿要有次数上限,超过5次还没成功就告警人工介入,避免死循环。

5.4 支付安全与防重放

支付接口是攻击重灾区,几个基本防护必须做:

  • 签名验证:所有回调必须验签,签名算法按网关文档来,密钥不能硬编码在代码里,用配置中心或环境变量。
  • 时间戳校验:回调带的时间戳和当前时间差超过5分钟就拒绝,防重放。
  • IP白名单:只接受网关的IP段回调,其他来源直接拒绝。
  • 金额二次校验:前面说过了,必须做。
  • 日志脱敏:支付相关日志不能记录完整卡号、密钥,要脱敏。

这些不是可选项,是底线。我见过因为没验签导致被伪造回调刷单的案例,损失不小。

6. 常见问题与排查技巧实录

6.1 MCP工具调用类问题

问题一:模型不调用工具,直接编造答案。

这是最常见的。用户问"三亚有哪些亲子酒店",模型不调工具,直接凭训练数据编了几个酒店名。原因通常是工具描述不够明确,或者系统提示词没强调"必须基于工具返回的数据回答"。

解决办法:在系统提示词里明确写"涉及实时信息(价格、库存、天气、航班)必须调用工具,不得凭记忆回答"。同时在工具描述里强调数据的实时性。我实测下来,加上这两条后,编造情况基本消失。

问题二:工具参数填错。

比如日期格式填成"2024年1月1日"而不是"2024-01-01",或者城市名填成"三亚市"而不是"三亚"。解决办法是在参数描述里给明确的格式示例,并且在工具服务端做参数归一化处理,兼容常见变体。

问题三:MCP Server连接失败。

排查顺序:先看Server进程是否存活,再看网络是否通,再看协议版本是否匹配。MCP协议还在演进,Client和Server的版本不匹配会连不上。我建议锁定版本,升级时一起升。

6.2 支付类问题速查

现象可能原因排查方向
支付回调没收到回调地址不可达、防火墙拦截检查公网可达性、查看网关回调日志
回调验签失败密钥不匹配、参数被篡改核对密钥、打印原始参数比对
订单状态没更新回调处理异常、事务回滚查应用日志、查数据库事务日志
重复扣款幂等没做好、用户重复点击检查幂等逻辑、前端按钮防重复
金额不一致优惠计算时序问题、并发修改核对下单时金额和回调金额的计算链路

6.3 几个我踩过的坑

坑一:流式输出和工具调用冲突。早期我在流式输出文本的同时发起工具调用,结果前端收到半句话就卡住了。后来改成工具调用期间先输出一个"正在查询"的状态事件,等工具返回后再继续输出文本,体验顺畅多了。

坑二:上下文对象更新丢失。多轮对话时,如果两轮请求几乎同时到达,上下文对象可能被覆盖。解决办法是给上下文加版本号,更新时校验版本,冲突就重试。

坑三:支付超时时间设太短。一开始设了5分钟,结果用户还在填信息订单就被取消了。改成15分钟,并且在前端显示倒计时,用户有紧迫感,转化率反而提升了。

坑四:MCP工具返回数据太大。酒店查询返回了200条结果,全塞给模型导致token爆炸。解决办法是在工具服务端做分页和字段裁剪,只返回模型需要的关键字段,默认返回前10条。

7. 一些关于扩展性的思考

这套架构跑通之后,我做了几次扩展,验证了分层设计的价值。

加新目的地,只需要在目的地Server里加数据源,Agent层不用动。加新的支付方式,只需要在支付Server里加一个适配器,订单层不用动。换大模型,只需要改编排层的模型调用,其他层不受影响。

如果要做多语言,前端加一层i18n,Agent层在系统提示词里加语言指令,工具层返回的数据保持结构化,展示层再翻译。这样改动可控。

如果要做B端,给旅行社用,可以在Agent层加一个"人工接管"的开关,复杂订单转人工,Agent只做辅助。这个在架构上就是编排层多一个路由分支。

我个人在实际操作中的体会是,AI应用的技术栈拆解,难点从来不在单点技术,而在层与层之间的边界怎么划、数据怎么流、异常怎么兜。把这三件事想清楚,剩下的就是填代码。MCP是个好东西,但它解决的是工具组织的问题,不是所有问题。支付这块,老老实实做幂等、做对账、做补偿,别想着走捷径。旅游这个场景,用户对价格的敏感度很高,计价逻辑一定要透明可追溯,每一分钱怎么算出来的都要能解释清楚,否则客诉会很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询