资讯详情 AI旅游Agent技术拆解:从对话到支付的全链路架构
📅 2026/10/31 18:57:12
1. AI旅游Agent的整体架构与设计思路1.1 为什么选择Agent形态做旅游产品先说背景。我最近在做一个面向自由行用户的AI旅游Agent产品形态很简单用户在一个聊天窗口里输入“帮我规划一个杭州三天两晚的行程”Agent自动完成路线规划、酒店比价、门票预订甚至支付下单。整个项目走通之后我发现它和传统的“AI客服”“聊天机器人”完全不同它背后是一个从对话到交易的全链路系统。为什么非要做成Agent形态而不是做一个App核心原因在于旅游决策的复杂性。传统OTA在线旅行社的交互模型是“搜索-筛选-下单”用户需要自己在一堆筛选条件里折腾比如“价格从低到高”“评分4.5以上”“距离西湖2公里以内”。但真实用户的表达方式是模糊的“预算别太高”“住的地方离地铁近一点”“第一天不要太赶”。这些约束条件天然适合用自然语言表达而Agent的作用就是把模糊的语言转成结构化的搜索条件再根据用户的实时反馈逐步收敛。另外一个原因是旅游订单的“长链路”特征。一次完整的旅行服务往往包含多个环节查航班、订酒店、规划路线、买门票、约车。用户如果在传统产品里操作需要在多个页面、多个App之间来回切换体验断链感很重。Agent可以把这些环节串起来以“会话”为单位管理整个旅程。所以这个项目的技术栈设计围绕三个核心目标展开让对话层足够轻、让工具调用足够标准化、让交易闭环足够可靠。1.2 整体技术栈与分层设计我把系统拆成了五层从用户那一端开始第一层是前端对话层负责聊天UI、流式渲染、卡片组件、会话快照。第二层是对话编排层负责意图识别、槽位填充、多轮状态管理。第三层是Agent核心层负责决策规划、工具选择、上下文管理。第四层是MCP接入层负责统一封装外部能力比如酒店搜索、机票查询、订单创建。第五层是业务系统层包含用户系统、订单中心、支付网关、对账服务。这套分层的核心理念是“对话与业务解耦”。对话层只关心用户说了什么不关心背后调用哪家酒店的接口MCP层只关心怎么把外部能力包装成标准工具不关心对话流程怎么编排。技术选型上前端用的是React TypeScript对话编排用的是LangGraph再加一层自定义的状态机Agent核心基于LLM做ReAct模式的循环推理MCP层用TypeScript写了两套Server一套查酒店一套查票务支付走的是微信支付Native 云支付网关的桥接。从结果看这个架构最大的收益是“可替换性”。比如最初接的是某家聚合机票API后面换成另一家直连渠道只需要重写对应的MCP Server上层代码完全不用动。这也是我建议任何做AI应用的同学从一开始就做好分层的根本原因——AI项目迭代太快今天选的供应商明天可能就没了或者价格模型变了如果你把所有逻辑都揉在一条链路上替换成本会搞得你怀疑人生。1.3 几个容易被忽略的架构决策有一个决策特别想说会话对象的设计。旅游Agent里一次“对话”不是简单的一问一答而是一次完整的、可能跨好几天的服务过程。用户今天说“帮我查下周去青岛的高铁”明天回来说“还是改成坐飞机吧”后天又追加一句“顺便看看青岛站附近的酒店”。这意味着系统里必须有一个持久化的会话对象它不只是一个消息列表还包含当前行程的完整状态、已确认的订单、待用户确认的选项、收集到一半的约束条件。我把这个对象设计成一颗“行程树”树上有行程节点、住宿节点、交通节点、票务节点。每个节点有自己的生命周期草稿、待确认、已确认、已取消。对话层的每一次新消息都相当于在这棵树上做一次增量修改。有人说这太复杂了还不如让LLM每次重新推理一遍。实测下来短期可以但一旦涉及支付和订单状态必须靠可靠的数据结构兜底不然LLM的幻觉会让你付出真金白银的代价。2. 前端对话层的实现要点2.1 流式对话与打字机效果前端聊天界面是整个用户体验的第一接触点它面临的第一个技术挑战就是“流式输出”。大模型返回内容不是一次性到位的而是一个token一个token地吐出来。如果等全部生成完毕再渲染用户会感觉系统“卡死了”但如果直接把每次增量直接怼到DOM里浏览器在长文本场景下会越来越卡。我采用的方案是增量文本 消息边界稳定。具体来说前端通过WebSocket接收服务端推送的增量事件每收到一个事件就往当前消息的文本缓冲区里追加内容。渲染层不做整条消息的重新渲染只对新增的文本片段做一次性插入。在React里我用了useMemo加上一个MessageText子组件避免父组件因为输入框state变化导致整个消息列表重渲。还有一个细节WebSocket断线重连后的消息拼接。用户聊到一半网络断了重新连上之后前端要把“本地已渲染的内容”和“服务端重放的完整消息”做合并。我的做法是给每条前端渲染的消息加一个clientMsgId重连后服务端回放时带上相同ID前端发现ID已存在就直接丢弃回放内容避免重复渲染。// 核心增量处理逻辑简化 const handleStreamChunk (chunk: StreamChunk) { const msgId chunk.messageId; setMessages(prev { const idx prev.findIndex(m m.id msgId); if (idx -1) return prev; const updated [...prev]; const oldText updated[idx].text; updated[idx] { ...updated[idx], text: oldText chunk.delta }; return updated; }); };这段代码看起来简单但它背后的关键决定是“不要整条替换消息文本只做追加”。如果你直接setMessages([...prev, newText])每次增量都会触发一次完整的diff和重渲染消息一长页面就开始飘。用规定了key的列表项来隔离更新范围性能会稳定很多。2.2 卡片消息与富交互旅游Agent不可能只用纯文本。查出来的酒店、航班、门票都要以卡片形式展示用户才能在卡片里直接操作比如选酒店、比价、确认预订。这要求聊天消息体支持结构化内容。我实现的方案是“消息体JSON化”每条消息有一个type字段取值是text、card、carousel、form、order。文本消息就是纯文本渲染卡片消息则是一个包含标题、图片、价格、操作按钮的结构化对象。前端根据类型分发到不同的渲染器。{ type: hotel_card, payload: { hotelId: HZ-00321, name: 西湖边上的某某民宿, price: 428, currency: CNY, rating: 4.7, location: { lat: 30.245, lng: 120.154 }, distanceToWestLake: 850, actions: [ { label: 查看详情, handler: hotel.detail }, { label: 选择此酒店, handler: hotel.select } ] } }这里有个踩坑经验卡片消息的按钮事件不要直接绑定具体的业务动作而是绑定一个抽象的handler名称。比如上面handler: hotel.select前端收到点击后把它作为一条用户消息发回给Agent带上卡片上下文Agent再做后续决策。为什么这么做第一如果卡片按钮是直接调API的那Agent就失去了一次“确认”的机会第二用户点按钮时可能已经改主意了但卡片上展示的还是旧信息如果直接调API可能会造成和真实意图的偏差。正确的路径是点击按钮 - 生成一条用户消息 - Agent重新理解 - 执行新动作。这个设计在支付场景下尤其重要。支付按钮点击之后绝不能是简单地“唤起支付”而是要先生成一个支付确认请求把订单信息重新校对一遍再发起支付。2.3 会话状态管理与会话快照旅游场景天然跨时长用户可能在通勤路上查酒店、在午休时对比价格、在晚上才下单。前端必须支持“会话快照”能力用户离开页面再回来聊天的上下文不能丢。我的做法是把消息列表和行程状态序列化到localStorage同时在服务端做一份镜像。前端每次收到新的消息增量就把整个会话快照写入localStorage写入频率做了节流增量消息即时写行程状态每5秒写一次。恢复会话时前端从localStorage拉快照按clientMsgId去重后渲染到页面上。同时调一个POST /session/restore接口把会话ID传给服务端服务端返回最新的行程状态前端和快照做合并。不一致时以服务端为准因为服务端的行程状态才涉及资金。localStorage有5MB上限会话消息长了会爆。我的策略是只保留最近的200条消息明文更早的消息用一条折叠记录替代如“前面还有15条消息点此展开”展开时再从前端缓存服务拉取。这样既保证恢复速度快又不影响体验。2.4 断线重连与心跳机制WebSocket在移动端极其不稳定尤其是用户在地铁里。必须设计重连机制和心跳保活。我用了标准的指数退避策略断线后第1秒重试、第2秒、第4秒……最多间隔30秒。每次重连成功之后发一个session.sync请求获取消息游标服务端回放游标之后的所有增量。心跳层面前端每15秒发一个ping包收到pong说明链路正常连续3次pong超时主动断开连接触发重连逻辑。一个容易被忽略的地方WebSocket的URL必须带sessionId和userId否则重连后服务端不知道你是谁。但鉴权信息不要拼在URL query里容易被日志打出来。我用的方案是鉴权放进首次连接的消息体中服务端校验通过后签发一个短期token后续重连都带这个短期token。3. MCP层Agent与外部能力的连接器3.1 MCP协议到底解决了什么问题MCPModel Context Protocol模型上下文协议对我而言最大的价值不是“标准化”本身而是把“让AI使用工具”这件事从工程拼装变成了组件化开发。在没有MCP之前Agent要调用外部API典型做法是给LLM写一堆函数定义Function Calling然后在代码里手写if (functionName searchHotel)这样的一堆分支。每接一个新API就要改Agent核心逻辑加分支、加参数映射、加返回值处理。时间一长Agent核心代码里全是业务逻辑改一处就牵一发动全身。MCP的模型是Agent通过标准协议去发现和调用“工具”。每个工具是一个MCP Server对外暴露一组定义清晰的操作。比如我的酒店MCP Server暴露了这些工具search_hotels查酒店、get_hotel_detail查详情、book_hotel预订、cancel_booking取消。Agent端做的事情变得很单纯从MCP Server拉取工具列表根据用户请求决定调用哪个工具传入参数拿到结果再组织语言回复。新增能力只需要新写一个MCP Server或者往现有Server里加工具Agent端零改动。“写代码的人不需要关心模型API”“模型不需要关心业务系统”这两个关注点被MCP清晰地分离了。3.2 旅游场景下MCP Server的搭建我写了两个MCP Server一个是酒店Server一个是票务Server。每个Server都是一个独立进程通过stdio和Agent进程通信也可以用SSE模式但我本地开发用stdio生产环境用SSE。先看一个工具的完整定义以“查酒店”为例{ name: search_hotels, description: 根据城市、入住日期、离店日期、人数等条件搜索符合要求的酒店列表, inputSchema: { type: object, properties: { city: { type: string, description: 城市名如杭州 }, checkIn: { type: string, format: date }, checkOut: { type: string, format: date }, guests: { type: integer, minimum: 1, maximum: 6 }, priceRange: { type: object, properties: { min: { type: number }, max: { type: number } } } }, required: [city, checkIn, checkOut, guests] } }这套定义看起来简单但里面的不少坑值得一提。第一个坑是日期格式一定要规范。LLM填充参数时经常会输出“下周三”“周末”“明天”这类相对日期。我在MCP Server入口加了解释器把这类相对日期标准化成YYYY-MM-DD格式。这个过程可以由LLM自己完成但为了可靠性和快速调试我选择了在Server端用moment.js做规则解析LLM只负责输出语义化字段如relativeDate: nextThursdayServer再转成具体日期。第二个坑是坐标搜索和关键词搜索的混合。用户说“西湖附近”这不是一个标准的地名。我的做法是先把这类地标名称翻译成经纬度半径再丢给酒店搜索API。这个翻译动作本身也可用LLM完成但我更倾向在MCP Server里接一个地理编码服务因为准确率和响应速度都更稳定。最终我的Hotel Server内部逻辑是接收Agent传来的城市和地标关键词 - 调地理编码服务拿到坐标 - 把坐标和酒店查询参数一起传给上游API - 对上游结果做个过滤和排序比如按评分、距离、价格综合排序- 返回结构化列表。3.3 工具调用链路的错误处理MCP Server不是万能的它后面接的是第三方API第三方API的返回经常不按文档来。我在Server层做了一层“结果规整器”把上游的返回统一转成Agent可以理解的格式同时做好三种错误状态的处理无结果返回{ status: empty, message: 该条件下没有符合条件的酒店建议放宽价格范围或日期限制 }部分失败比如查询酒店成功但查交通失败返回{ status: partial, data: { ... }, error: { component: transport, message: ... } }完全失败上游API超时返回{ status: failed, error: ..., retryable: true }第三类错误很重要retryable标记告诉Agent核心“这个工具这次没成功但你可以稍后重试”。Agent拿到这个标记后可以在对话里继续追问用户“交通查询暂时不可用要不先看酒店”有人说“这不就是把异常处理吗”对但它不是普通的异常处理因为错误信息是给LLM看的不是给程序员看的。LLM需要从错误信息里推断出如何继续对话所以错误描述必须用自然语言写清楚不能直接抛一个500 Internal Server Error让LLM一脸懵。4. 业务编排从对话到下单4.1 基于状态机的会话编排对话编排层是整个系统里最复杂的一块。因为旅游不是一次性的工具调用而是多轮对话 跨多天的过程性任务。我用了LangGraph做基础编排再加一层自定义状态机管理行程生命周期。LangGraph负责“当前这轮对话该如何推进”自定义状态机负责“整个行程处于什么阶段”。行程状态机的定义IDLE - COLLECTING - PLANNING - PROPOSAL - CONFIRMING - BOOKED - PAYING - PAID - COMPLETED PLANNING - AMENDING用户修改计划 CONFIRMING - IDLE用户取消/放弃 PAYING - PAY_FAILED支付失败 CONFIRMING - BOOKED - PAYING确认订单并进入支付每个状态之间定义了合法迁移条件。比如从CONFIRMING到BOOKED必须要求用户明确表达“确认预订”并且系统侧成功创建了订单从PAID到COMPLETED必须要求支付回调已到达并且订单状态已落库。为什么需要状态机而不是完全依赖LLM因为LLM天然有幻觉倾向它可能自作主张地认为“订单已经支付了”。状态机像一个护栏把LLM的输出限制在合法动作之内。LLM的输出先经过一次“意图解析”解析结果是一个结构化动作如amend_schedule、confirm_booking、select_hotel状态机校验这个动作在当前状态下是否合法合法才执行。4.2 多轮对话的信息收集与槽位填充旅游Agent经常会遇到信息不全的情况。用户说“帮我订下个月的酒店”但没说城市、没说日期、没说人数这时候Agent必须追问。我维护了一个“约束槽位表”每个槽位代表一个完成预订所必需的参数。槽位有两类必需槽位和可选槽位。必需槽位包括目的地、入住日期、离店日期、入住人数。可选槽位包括价格范围、偏好区域、酒店类型星级/民宿、特殊需求亲子、宠物、无烟。每次用户消息进来经过LLM的槽位提取把能填的槽位值提取出来更新状态。如果所有必需槽位都齐了生成方案如果缺就生成追问问题。一个细节追问问题时不要一次把所有缺的都问出来。用户说“帮我去杭州玩三天”你反问“请问您哪天入住、哪天离店、几位入住”用户会觉得你在背问卷。更好的方式是基于常识先给一个默认方案再让用户修正。比如Agent先回复“杭州国庆期间的酒店需要提前订我建议您10月1日入住3号离店2个人价格区间我先按500-800元每晚来查如果不对您随时纠正我。”这个“先给默认值再修正”的策略把用户的认知负担降到了最低。实测下来用户的完成率比“填空式对话”高很多。4.3 从方案到订单的转换当用户确认一个酒店方案后系统需要从“对话世界”跳转到“业务世界”。这一步的可靠性至关重要。我设计了“双阶段确认”机制。第一阶段用户点“选这个酒店”Agent生成一个待确认订单草稿展示在卡片上包含酒店信息、价格、日期、取消政策。第二阶段用户点“确认预订”系统调用MCP Server的book_hotel工具真正创建订单。这个双阶段的中间状态很重要因为用户在“选”和“确认”之间可能改变想法也可能因为看到了取消政策而犹豫。我把这两个动作分开避免用户误操作造成真实订单。创建订单成功后系统生成一个订单对象。订单对象包含订单号、酒店ID、日期范围、价格、状态、关联的会话ID。会话ID是连接对话和业务的关键字段。5. 支付流程的技术实现5.1 支付渠道选择与统一网关支付是整个链路中技术含量最高、容错要求最严格的一环。我做了一个统一支付网关对上层屏蔽具体支付渠道的区别。支付网关的核心是一个接口POST /payment/create Body: { orderId, amount, currency, channel, notifyUrl, meta } Response: { paymentId, status, payParams }channel支持微信支付NativePC扫码、小程序支付、余额支付等。payParams根据渠道不同返回不同的参数微信Native返回一个code_url二维码链接小程序返回wx.requestPayment需要的参数包。为什么做统一网关因为我不确定哪个渠道最终能跑通。先接微信后面可能接支付宝、云闪付。网关化之后业务层只需要关心“发起支付”和“接收回调”两个动作不关心具体渠道细节。5.2 支付回调与签名验证支付回调是系统可靠性最容易出问题的地方。我总结的核心原则是回调消息不是业务消息支付结果以回调为主但前端轮询转账轮询服务端主动查单为辅。流程是这样的用户在聊天卡片里点“确认支付”前端调用POST /payment/create拿到渠道的支付参数。前端唤起支付微信扫码或者小程序支付。支付完成后微信服务器往配置的notifyUrl发一个异步回调通知。网关收到回调后先验证签名再用outTradeNo商户订单号查一次微信支付接口确认订单状态双确认一致后才更新状态。订单状态更新后通过WebSocket给前端推一条order.paid消息聊天界面出现支付成功的确认卡片。签名验证是安全底线。微信支付的回调签名用的密钥要放在服务端绝对不能出现在前端任何代码或配置里。我见过有人图方便把商户API密钥直接写在前端配置文件里这是灾难性的错误。任何有密钥的文件一旦被用户下载到浏览器就等于把整个商户号的资金安全交出去了。5.3 AI场景下的支付确认交互AI场景下支付有一个特殊问题用户可能在对话里直接说“帮我付了”。这句话让LLM产生调用支付工具的意向。但是支付涉及真实资金绝不能因为一句话就直接扣款。我在支付动作前面加了一道“人工确认屏障”。LLM调用支付工具的请求会被拦截在网关层网关返回一个REQUIRE_USER_CONFIRMATION的状态同时生成一个确认卡片推给用户。卡片上明确展示订单金额、商品信息、支付方式用户必须手动点击“确认支付”按钮后端才会真正发起支付请求。这道屏障的实现原理是支付工具在MCP Server层不暴露confirm_payment只暴露initiate_payment。initiate_payment的返回值永远不直接是支付链接而是一个待确认状态的ID。前端拿到这个ID后展示确认卡片。用户点确认后前端发送一个带用户签名比如二次输入密码或者人脸验证的确认请求才会真正唤起支付。这个设计让我免去了一次险些因为LLM幻觉造成的资金风险。当时用户在对话里说“你帮我随便选个便宜点的酒店直接付了就行”LLM真的触发了initiate_payment幸好有这个屏障拦截否则那笔订单就真的扣款了。5.4 超时取消与退款对账支付还有一个避免不了的工程问题用户发起支付但没按时完成。微信支付的支付链接默认有效期是2小时Native支付可以设置。超时后订单如果还挂着既占用了库存也给用户带来困惑。我做了两步处理。第一定时任务每30秒扫描一次“待支付”状态的订单如果超过有效期通常是支付链接创建后2小时就触发“超时取消”订单置为取消状态同时释放库存。第二如果用户支付了但从未确认系统要能在回调到达时判断订单状态若订单已取消但回调显示支付成功进入“退款待处理”队列自动发起原路退款。对账我采用的是每日一次的对账单比对。每日凌晨从支付渠道拉取前一天的交易流水与本地订单表的支付记录做匹配。对不上的条目进入人工处理队列。这个环节在项目早期容易被忽略等订单量上来了才发现那时候补对账体系会非常痛苦。6. 实战中的问题排查与避坑记录6.1 前端流式渲染的性能坑我一开始用了一个比较粗暴的做法每次收到增量消息用setMessages更新整个消息数组。当会话拉到300条以上时页面明显变卡打字机效果变成了“幻灯片播放”。排查之后定位到两个问题一是消息列表没有做列表项级别的隔离任何一条消息更新都触发所有列表项重新渲染二是文本内容更新时整个消息组件的DOM被替换了导致滚动位置丢失、输入框焦点丢失。修复方案是列表项外面套一个React.memo每个消息组件的key用消息ID加版本号增量更新时只对目标消息的文本节点做插入操作用requestAnimationFrame做节流不更新整个消息对象。6.2 MCP Server的资源管理与超时MCP Server作为子进程运行时资源管理是个隐性问题。最开始我用启动进程的方式管理Server发现长时间运行后内存持续增长最终导致Agent响应变慢。后来排查发现是MCP Server里的连接池泄漏。查了一下上游API的SDK它在每次请求后没有正确释放HTTP连接。解决方法是在MCP Server里加了一层带超时控制的HTTP客户端设置最大空闲连接数和每个请求的超时时间酒店查询超时8秒创建订单超时15秒同时对每个工具调用做了Promise.race兜底。有一个教训不要把上游API的错误状态直接透传给LLM。有一次上游返回了“429 Too Many Requests”LLM居然在回复里说“系统繁忙请稍后再试”虽然这没毛病但它没有理解“这其实是供应方接口限流可以换一个供应商查询”。后来我在MCP Server错误处理里把429翻译成了“本渠道暂无库存可选择其他方式或稍后再试”LLM才知道有“切换供应商”这个选项。6.3 支付回调与前端状态的一致性最让人头疼的bug是支付成功回调已经到了服务端订单状态也更新为“已支付”但前端聊天界面还停在“等待支付”状态。用户以为没支付成功又发起了一次新的支付。根因是WebSocket推送的时序问题。支付回调线程把状态推给了session连接但此时前端的WebSocket可能正处于临时断线状态消息丢了。单纯等前端重连后再请求“消息列表”也解决不了因为订单状态更新事件不是“消息”它没有记录在会话消息表里。修复方案把“订单状态变更”也作为消息存进会话消息表。支付成功后系统写入一条类型为order_event的消息包含订单最新状态、金额、支付方式、时间戳走正常的消息推送通道。这样即使WebSocket断线前端重连后拉取最新消息列表时也能拉到这条order_event消息状态自然就同步了。这也呼应了第三层设计里“业务事件必须进会话”的原则。6.4 会话恢复时行程状态漂移另一个高频问题用户关闭浏览器后重新打开页面前端从localStorage恢复了快照但服务端的行程状态可能已经变化了比如用户在其他设备上完成了支付。我处理的方式是恢复快照时不是取本地为准而是先向服务端请求一次session.getLatest把服务端返回的行程状态和本地快照做diff任何差异以服务端为准。这个看似简单的问题背后暴露了一个架构缺陷本地快照在离线状态下可能被修改但服务端无法感知。所以我在服务端对会话状态维护了一个基于修订号的并发控制机制。每次更新状态时检查修订号是否匹配不匹配说明有其他写操作发生直接拒绝当前更新并要求前端刷新。6.5 多Agent并发下的状态竞争用户可能会在同一个会话里开多个对话分支比如一边在查酒店另一边在问“杭州有什么好吃的”。这两个意图都会走到Agent核心但它们操作的状态可能重叠如果并发执行状态变更很容易出现覆盖。我的方案是对话编排层对“写操作”做了串行化同一个会话ID下同一时间只允许一个写任务在执行。读操作可以并行写操作排队。这个串行化是通过一个会话级的事务锁实现的基于Redis的SETNX命令拿到锁的任务执行完状态更新后释放锁。锁的超时设为10秒超时自动释放避免死锁。7. 工具选型与后续扩展方向7.1 前端框架与组件库的选择前端我选了React TypeScript组件库用Ant Design聊天界面是自定义组件。选React是因为生态成熟聊天类交互需要大量的状态管理Hooks模型非常顺手。TypeScript自然是标配聊天消息体有复杂的类型结构文本、卡片、订单、表单等等类型系统可以大幅度减少低级错误。如果你从零开始做考虑到给用户的“项目复用参考”我会推荐优先考虑Next.js服务端渲染因为旅游类Agent页面——落地页、产品介绍页、登录页——这些边缘页面用服务端渲染对SEO更友好。核心聊天体验部分可以做成独立的React应用甚至可以单独部署。7.2 Agent框架选型对比Agent核心层现在有很多选择我实际比较过LangGraph、AutoGen、CrewAI、自研框架。简单说说我的选型结论LangGraph适合需要精细控制流程的项目。它的图结构非常适合旅游场景的“有限状态 多分支决策”但学习曲线陡。AutoGen适合多Agent对话协作但重对话模式在“用户-Agent”这种简单二元交互里反而显得多余。CrewAI擅长多角色分工但抽象层级太高调试复杂问题时很难定位到具体环节。自研框架灵活度最高但开发和维护成本也最高。除非你是团队里有扎实的LLM工程背景否则不建议一步到位自研。我最终采用的是“LangGraph编排 自研状态机 MCP工具层”的组合。LangGraph负责“这一轮该怎么走”状态机负责“整个流程当前在哪”MCP负责“能执行什么动作”。各司其职边界清晰。7.3 从单点功能到多城市多场景的扩展目前这个Agent已经能处理“查酒店”“规划行程”“预订门票”三类核心场景运行稳定。接下来有一批值得探索的扩展方向第一个方向是多供应方聚合比价。目前酒店数据来自一个聚合渠道价格和库存并不是全量最优。接入更多MCP Server之后以统一的search_hotels接口对齐各家供应商然后做比价排序这能让用户体验提升到一个新量级。第二个方向是主动感知型Agent。比如用户发出了机票订单Agent可以主动监测航班延误信息在延误发生时提醒并给出改签建议。这要求MCP Server增加“订阅类”能力比如航班状态推送Agent层也要增加常驻任务机制。第三个方向是支付渠道的完善。目前只接了一个支付渠道后续要把支付宝、云闪付以及跨境支付如海外卡消费都接入统一网关。网关已经预留了渠道适配器接口理论上每个新渠道只要实现一套createPayment/queryPayment/handleNotify接口即可。第四个方向是记忆能力的深化。用户第二次来咨询“成都”时系统能不能记得“这位用户上次去杭州住的是高星级酒店、预算偏高端”这需要维护一个“用户旅行偏好画像”在对话编排层的槽位提取阶段就把长期的隐式偏好更新进来。这个方法如果能沉淀下来产品的壁垒就会从“工具调用能力”跃升到“个性化服务能力”。8. 最后的几句实在话做这个AI旅游Agent项目我最深的体会是AI应用的技术难点从来不在“AI”本身而在AI之外的那一层工程。纯对话、纯生成、纯工具调用这些有模型和框架都能搞定花不了太多时间。真正花掉我大部分精力的是那些平时不起眼的地方流式绘制怎么做到不卡顿、WebSocket断了怎么拼消息、状态机怎么防止LLM乱来、支付回调怎么和前端状态保证一致、订单超时怎么处理才能不留坑。如果你也在做类似的Agent项目我的建议很实在先把“业务闭环”定义清楚再谈技术栈。一个旅游Agent如果只做到“能聊天、能推荐”那确实不需要这么复杂但一旦涉及到真实订单和真实资金设计边界就非常重要了。你在状态机上多花的时间在支付屏障上多花的时间在数据一致性上多花的时间都是在给用户的钱包和你的信誉上保险。最后分享一个小技巧给所有外部API调用加上一个“可回放”的日志系统。每次MCP Server调用上游API都记录下完整的请求参数和响应原文。调试LLM幻觉问题、排查支付对账差异、复现用户当时看到的结果时这套日志的价值远超预期。我踩过几次因为没有日志而在周末晚上对着生产环境干瞪眼的坑之后就再也不敢省这块功夫了。做AI Agent项目永远要在“快速迭代”和“可靠兜底”之间找平衡。希望这篇拆解能让你少走一些我走过的弯路。