1. 为什么我要拆这个 AI 旅游 Agent 的技术栈
去年下半年开始,身边做旅游、做本地生活、做 SaaS 的朋友几乎都在问同一件事:能不能做一个 AI 旅游 Agent,用户说一句"帮我安排五一去成都三天,预算三千,带老人",它就能把行程、酒店、门票、支付全流程跑通。听起来很美好,但我实际接触下来,绝大多数团队卡在同一个地方——前端对话做得挺漂亮,一到"下单支付"就断链了。
这个断链不是技术能力问题,而是架构认知问题。很多人把 AI 旅游 Agent 当成"聊天机器人 + 几个 API",结果做到一半发现:对话状态怎么持久化?工具调用怎么标准化?支付这种强一致场景怎么和 LLM 的不确定性共存?这些问题不解决,Demo 永远只是 Demo。
我这次拆解的这套技术栈,核心链路是前端对话层 → Agent 编排层 → MCP 工具层 → 业务服务层 → 支付网关层。选它作为拆解对象,是因为它把当前 AI Agent 领域最热的几个概念——MCP、工具调用、流式输出、支付回调——串成了一条真实可跑的链路,而不是停留在概念科普。
这篇文章适合三类人看:一是正在做 AI Agent 产品、卡在工程化落地的开发者;二是想理解 MCP 到底解决什么问题、值不值得投入的技术负责人;三是对"AI 怎么真的把钱收上来"这件事好奇的产品和运营。我会把每一层的选型理由、关键参数、踩坑点都摊开讲,代码和配置能给的我尽量给,让你看完能直接对着搭。
先说结论:AI 旅游 Agent 的难点从来不在"AI",而在"Agent 之外的那套工程体系"。下面我按层拆。
2. 整体架构设计与分层思路
2.1 五层架构的划分逻辑
我最终确定的架构是五层,从上到下依次是:
| 层级 | 职责 | 关键技术 | 典型耗时 |
|---|---|---|---|
| 前端对话层 | 用户输入、流式渲染、状态展示 | SSE / WebSocket、流式 Markdown | 首字 < 500ms |
| Agent 编排层 | 意图理解、任务规划、工具调度 | LLM + Function Calling / MCP Client | 1-3s |
| MCP 工具层 | 标准化工具暴露与调用 | MCP Server(stdio / HTTP) | 100-800ms |
| 业务服务层 | 行程、库存、订单、用户 | 常规后端服务 | 50-300ms |
| 支付网关层 | 下单、支付、回调、对账 | 支付 SDK + 异步回调 | 依赖第三方 |
这个划分不是拍脑袋来的。我试过把 MCP 工具层和业务服务层合并,结果发现工具定义和业务逻辑耦合在一起,改一个工具要动业务代码,测试也没法独立跑。分开之后,MCP Server 只负责"把业务能力翻译成大模型能理解的工具描述",业务服务层保持纯粹,两边可以并行开发和独立部署。
2.2 为什么选 MCP 而不是自己写 Function Calling
这是被问得最多的问题。我的答案很直接:如果你只有一个 Agent、只对接三五个工具,自己写 Function Calling 完全够用;但旅游场景的工具数量是爆炸性的。
算一笔账:一个完整的旅游 Agent 至少需要这些工具——查航班、查高铁、查酒店、查民宿、查景点门票、查天气、查当地美食、算路线、比价、下单、支付、退改、查订单。每个大类下面还有细分,光"查酒店"就可能要对接携程、飞猪、美团、Booking 等不同源。工具数量轻松破 50。
自己写 Function Calling 的问题在于:每个工具的 schema 要手写、要维护、要跟着业务变;不同 LLM 对 schema 的兼容性还不一样;工具多了之后 prompt 里塞不下,得做检索。MCP 的价值就在于它把"工具的定义、发现、调用"标准化了——工具是独立进程,通过标准协议暴露,Agent 只认协议不认实现。换 LLM、加工具、改工具,Agent 侧几乎不用动。
提示:MCP 不是银弹。它的进程间通信有开销,工具调用延迟比进程内函数调用高一个量级。对延迟极度敏感的场景(比如实时语音对话),要评估是否值得。
2.3 支付层为什么必须独立成层
旅游 Agent 和普通聊天 Agent 最大的区别是:它要动钱。动钱意味着三件事——幂等、对账、可追溯。
LLM 的输出是不确定的,同一个请求可能重试、可能被用户打断重来、可能因为超时重发。如果支付逻辑和 Agent 编排混在一起,很容易出现"用户点了一次,扣了两次钱"的事故。所以我把支付单独抽成一层,Agent 只负责"发起支付意图",真正的下单、签名、回调、对账全部由支付层用确定性代码处理。
这一层的核心原则是:LLM 永远不直接碰支付凭证和金额计算。Agent 传给支付层的只有"订单 ID + 用户 ID + 支付方式",金额从业务服务层查,签名在支付层做,回调在支付层验签。这样即使 Agent 抽风,也不会造成资金损失。
3. 前端对话层的实现细节
3.1 流式输出为什么必须做
旅游 Agent 的响应往往很长——一个三天行程可能几百字,还带表格。如果等 LLM 全部生成完再返回,用户要盯着 loading 转 5 到 10 秒,体验直接崩。流式输出把首字延迟压到 500ms 以内,用户看到字一个个蹦出来,感知上快很多。
技术选型上,我用的是SSE(Server-Sent Events)而不是 WebSocket。原因很简单:对话场景是单向的(服务端推、客户端收),SSE 基于 HTTP,天然支持断线重连、天然穿透大部分代理、实现成本低。WebSocket 适合双向实时场景,用在纯对话上是杀鸡用牛刀。
前端渲染流式 Markdown 有个坑:LLM 输出的 Markdown 是不完整的,比如表格刚输出一半、代码块没闭合。如果直接丢给 Markdown 渲染器,会渲染出错乱。我的做法是维护一个"未闭合标记栈",遇到 ``` 或 | 表格开头时先缓存,等闭合了再渲染。这个逻辑不复杂,但能省掉大量用户投诉。
3.2 对话状态怎么管
旅游 Agent 的对话不是一问一答,而是多轮填槽。用户说"去成都",Agent 要追问"几天""几个人""预算多少""什么时候出发"。这些槽位状态必须持久化,否则用户刷新页面就全丢了。
我的方案是:前端只存会话 ID,所有状态在后端。后端用一张conversation_state表,字段包括会话 ID、当前意图、已填槽位(JSON)、待填槽位、更新时间。每次用户发消息,Agent 先读状态、更新状态、再决定下一步。这样多端同步、断线恢复都没问题。
注意:槽位状态不要塞进 LLM 的 context 里靠模型自己记。模型记性不可靠,尤其是对话超过 20 轮之后。状态必须显式存储、显式注入。
3.3 前端技术栈的选择
如果是小程序场景,用 uniapp 一套代码多端跑,SSE 在小程序里要用wx.request的enableChunked模式,注意分块数据的拼接。如果是 Web 场景,直接用EventSource或者 fetch 的 ReadableStream。Electron 桌面端的话,主进程转发 SSE 到渲染进程,避免跨域问题。
这里有个细节:流式渲染要做节流。LLM 吐字速度可能很快,如果每个 token 都触发一次 React 重渲染,页面会卡。我的做法是攒 50ms 或攒够 10 个字符再渲染一次,肉眼几乎看不出延迟,但 CPU 占用降一大截。
4. Agent 编排层与 MCP 工具层
4.1 Agent 编排的核心循环
Agent 编排层干的事,本质是一个循环:理解意图 → 规划步骤 → 调用工具 → 观察结果 → 决定下一步 → 直到任务完成或需要用户输入。
旅游场景的规划比通用 Agent 复杂,因为步骤之间有依赖。比如"订酒店"依赖"确定行程日期","算总价"依赖"酒店 + 门票 + 交通都定了"。我的做法是用一个轻量的 DAG(有向无环图)来描述任务依赖,Agent 每轮只处理"当前可执行"的节点,执行完更新图状态。
为什么不直接用 ReAct 那种"想到哪做到哪"的模式?因为旅游订单是有副作用的——下单了就不能随便重来。DAG 让整个流程可预测、可回滚、可断点续跑,这对涉及支付的场景是刚需。
4.2 MCP 工具怎么定义
MCP 工具的定义分三部分:名称、描述、参数 schema。描述写得好不好,直接决定 LLM 会不会正确调用。我踩过的坑是:描述写得太技术化,LLM 理解不了;写得太模糊,LLM 乱调。
举个例子,查酒店工具的描述,我一开始写的是"查询酒店信息",结果 LLM 经常在用户还没说日期的时候就调用。后来改成"根据城市、入住日期、退房日期、人数查询可用酒店,缺少任一必填参数时不要调用",调用准确率明显提升。
参数 schema 用 JSON Schema 描述,注意几点:必填参数标required;枚举值用enum限定;日期格式统一用YYYY-MM-DD并在描述里写明。这些细节看着琐碎,但能大幅降低 LLM 传错参的概率。
4.3 MCP Server 的部署形态
MCP Server 有两种主流形态:stdio(本地进程)和 HTTP(远程服务)。
stdio 适合本地工具,比如读写本地文件、调用本地软件。它的优点是简单、无网络开销,缺点是只能本机用、没法多用户共享。
HTTP 适合远程工具,比如查酒店、下单支付。旅游 Agent 的工具绝大多数是远程的,所以我的 MCP Server 主要用 HTTP 形态部署,每个业务域一个 Server(酒店 Server、交通 Server、支付 Server),独立部署、独立扩缩容。
提示:MCP Server 要做鉴权和限流。工具是暴露给 Agent 的,Agent 背后是用户,如果不做用户级鉴权,A 用户可能通过 Agent 查到 B 用户的订单。这个坑我见过真实案例。
4.4 工具调用的错误处理
工具调用失败是常态——第三方接口超时、库存不足、参数非法。Agent 必须能区分"可重试错误"和"不可重试错误"。
我的分类是:网络超时、限流属于可重试,Agent 可以换个参数或稍后重试;参数非法、库存不足、余额不足属于不可重试,Agent 要把错误翻译成人话告诉用户,并给出替代方案。比如"这家酒店满房了,要不要看看同价位的另一家"。
这里的关键是:错误信息要结构化返回,包含错误码、错误类型、是否可重试、建议动作。Agent 拿到结构化错误才能做正确决策,拿到一句"调用失败"只能干瞪眼。
5. 支付层的工程化实现
5.1 支付流程的完整链路
旅游 Agent 的支付链路是这样的:
- 用户在对话里确认下单,Agent 调用"创建订单"工具
- 业务服务层生成订单,状态为"待支付",返回订单 ID 和金额
- Agent 调用"发起支付"工具,传入订单 ID 和支付方式
- 支付层向支付网关请求预支付,拿到支付参数(如二维码链接、JSAPI 参数)
- 前端展示支付界面,用户完成支付
- 支付网关异步回调支付层,支付层验签、更新订单状态
- 支付层通知业务服务层,业务服务层通知 Agent,Agent 告诉用户"支付成功"
这条链路里,第 6 步的异步回调是核心。很多新手只做了第 5 步的同步返回,以为用户付完就完事了,结果回调没处理,订单状态永远停在"待支付"。
5.2 幂等设计:防止重复扣款
支付层最重要的一件事是幂等。用户可能重复点击、网络可能重发、回调可能重复推送。我的做法是:
- 订单号唯一:每个订单生成时分配全局唯一订单号,重复创建返回同一订单
- 支付请求幂等:同一订单的支付请求,用订单号做幂等键,重复请求返回同一预支付结果
- 回调幂等:回调处理前先查订单状态,已支付则直接返回成功,不重复处理
这三层幂等缺一不可。我见过只做回调幂等、没做支付请求幂等的系统,用户连点两次支付按钮,生成了两笔预支付,虽然最终只扣一次钱,但账对不上,财务要疯。
5.3 支付回调的验签与对账
回调验签是安全底线。支付网关的回调带着签名,支付层必须用密钥验签,验签失败直接丢弃。这一步不能省,否则有人伪造回调就能白嫖订单。
验签通过后,还要做金额校验——回调里的金额必须和订单金额一致。我见过攻击方式是篡改回调金额,如果系统不校验,就会用 1 分钱买走几千块的旅游套餐。
对账是兜底。每天定时拉取支付网关的账单,和本地订单逐笔比对,找出"本地已支付但网关无记录""网关已支付但本地未更新"的差异单,人工介入处理。对账脚本不复杂,但能救命。
5.4 支付方式的选择
旅游场景的支付方式要覆盖:微信支付、支付宝、银行卡。小程序里微信支付是标配,H5 里支付宝更常见,App 里两者都要。
技术实现上,微信支付用 JSAPI(小程序/公众号)或 Native(扫码),支付宝用手机网站支付或 APP 支付。注意不同支付方式的回调地址、签名算法、参数格式都不一样,支付层要做适配层,对上暴露统一接口,对下适配各家 SDK。
注意:支付相关的密钥、证书绝对不能硬编码在代码里,要用配置中心或密钥管理服务。我见过密钥写死在代码里、代码传到公开仓库的事故,损失惨重。
6. 常见问题与排查技巧实录
6.1 工具调用相关
问题:LLM 不调用工具,直接编答案。
排查:先看工具描述是否清晰,再看 system prompt 是否强调"必须调用工具获取真实数据"。旅游场景绝对不能靠 LLM 编酒店价格,必须强制走工具。
问题:LLM 调用工具时参数传错。
排查:检查 JSON Schema 是否严格,必填项是否标注,枚举值是否完整。可以在 prompt 里给一两个调用示例,few-shot 能显著提升准确率。
问题:MCP Server 连不上。
排查:stdio 模式检查进程是否启动、路径是否正确;HTTP 模式检查端口、防火墙、鉴权 token。日志要打全,MCP 的握手失败往往没有明确报错。
6.2 支付相关
问题:回调收不到。
排查:回调地址必须是公网可访问的,本地开发用内网穿透工具。检查支付网关后台配置的回调地址是否正确,检查服务器防火墙是否放行。
问题:订单状态不一致。
排查:先查回调日志,看回调是否到达、验签是否通过、处理是否成功。再看是否有并发问题,同一订单的回调可能并发到达,要用数据库行锁或分布式锁串行处理。
问题:用户支付成功但 Agent 没反应。
排查:这是回调到 Agent 的通知链路断了。检查支付层到业务服务层的通知、业务服务层到 Agent 的推送是否正常。建议加一个"订单状态轮询"兜底,前端每隔几秒查一次订单状态。
6.3 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 首字延迟高 | LLM 响应慢 / 未流式 | 检查是否开启 stream |
| 工具不调用 | 描述不清 / prompt 缺失 | 优化工具描述,加 few-shot |
| 重复扣款 | 幂等缺失 | 检查订单号、支付请求、回调三层幂等 |
| 回调丢失 | 地址不可达 / 验签失败 | 检查公网地址、密钥配置 |
| 状态不一致 | 并发 / 通知断链 | 加锁、加轮询兜底 |
| 金额错误 | 未校验回调金额 | 强制校验回调金额与订单一致 |
6.4 几个独家避坑技巧
第一,Agent 的每一步都要打日志,包括意图、槽位、工具调用、返回结果。出问题时能完整回放整个决策链,比猜快一百倍。
第二,支付相关的操作全部走异步,不要让 Agent 同步等支付结果。用户支付可能要几十秒,Agent 同步等会超时。正确做法是 Agent 发起支付后立即返回"等待支付",支付成功后通过推送通知 Agent。
第三,给 Agent 设一个"最大工具调用轮数",比如 10 轮。防止 Agent 陷入死循环,一直调工具停不下来,烧 token 还烧钱。
第四,测试环境要用沙箱支付,微信、支付宝都提供沙箱环境。千万别在生产环境测支付,一不小心就是真金白银。
7. 我对这套技术栈的几点个人体会
搭完这套东西,我最大的感受是:AI Agent 的工程难度,80% 在 AI 之外。LLM 本身的能力已经足够强,真正难的是怎么让它的不确定性,和支付、订单这种强一致场景和平共处。MCP 解决的是工具标准化问题,但幂等、对账、状态机这些传统后端的老问题,一个都跑不掉。
如果让我给正在做类似项目的朋友一句建议,那就是:先把支付链路用确定性代码跑通,再往上叠 Agent。很多人反过来做,先做炫酷的对话,最后发现支付接不上,推倒重来。顺序对了,事半功倍。
后续这套架构还能往几个方向扩:一是加多 Agent 协作,比如行程规划 Agent、比价 Agent、客服 Agent 各司其职;二是加记忆层,把用户的历史偏好存下来,下次直接推荐;三是加评估体系,用真实订单数据反推 Agent 的决策质量。这些我还在摸索,有进展再分享。