多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于MCP协议与GPT-4o的AI旅游规划产品实战:3天上线全记录

基于MCP协议与GPT-4o的AI旅游规划产品实战:3天上线全记录 1. 为什么我盯上了MCP这个协议先说结论MCPModel Context Protocol本质上就是给大模型装了一双手。以前我们用GPT-4o写代码、查资料它只能“说”不能“做”。你想让它帮你查个航班、订个酒店、调个地图API得自己写一堆胶水代码把函数调用Function Calling的JSON Schema手搓一遍再在服务端做路由分发。MCP把这个过程标准化了——模型通过统一的协议描述自己能调用哪些工具客户端负责执行结果回传。听起来像插件系统但比插件轻比Function Calling规范。我之所以在3天内能做出一个AI旅游规划产品并上线核心原因就是MCP把“工具接入”这件事的边际成本压到了极低。以前接一个高德地图API我得写请求封装、参数校验、错误处理、结果格式化至少半天。现在只要写一个MCP Server声明工具名、参数、返回结构剩下的交给协议层。GPT-4o负责理解用户意图、编排调用顺序Next.js负责前端渲染和流式输出。整个链路清晰得不像话。这个产品解决的是什么问题简单说用户输入“我想去成都玩3天预算3000喜欢美食和人文”系统自动调用地图API查景点、调用天气API看预报、调用酒店API比价最后生成一份带时间轴、预算分配、交通建议的完整行程。不是那种模板化的“第一天宽窄巷子、第二天锦里”的垃圾攻略而是根据实时数据动态生成的。适合谁参考独立开发者、想快速验证AI Agent产品的小团队、以及所有对MCP协议好奇但还没动手的人。2. 整体架构设计与技术选型逻辑2.1 为什么是Next.js而不是纯后端框架Next.js在这个项目里承担了三个角色前端页面、API路由、流式输出通道。你可能会问为什么不用Express或者FastAPI单独做后端因为AI旅游规划产品的核心体验是“实时生成感”——用户输入需求后行程要像打字机一样逐字蹦出来而不是等30秒后一次性刷新。Next.js的App Router配合Vercel AI SDK天然支持Server-Sent EventsSSE流式传输。我在app/api/chat/route.ts里用streamText把GPT-4o的输出直接推给前端前端用useChat钩子接收整个过程不需要自己维护WebSocket连接。另一个原因是部署。Vercel对Next.js的支持是亲儿子级别的git push之后自动构建、自动分配域名、自动HTTPS。我一个人3天做完根本没时间折腾Nginx配置和SSL证书。Vercel的免费额度对于MVP阶段完全够用每天几千次请求毫无压力。2.2 MCP Server的拆分策略我一共写了三个MCP Server分别对应地图、天气、酒店三个领域。为什么不写一个大而全的Server因为MCP的设计哲学是“单一职责”。每个Server独立进程、独立端口、独立生命周期。地图Server挂了不影响天气Server调试的时候也清晰——我可以在终端里单独跑地图Server用MCP Inspector测试工具调用不用启动整个应用。具体拆分如下Server名称工具数量核心工具数据源map-server4searchPOI, getRoute, getDistance, geocode高德地图Web APIweather-server2getForecast, getCurrentWeather和风天气APIhotel-server3searchHotels, getHotelDetail, comparePrices某OTA平台开放接口每个Server用TypeScript写基于modelcontextprotocol/sdk。这个SDK提供了Server类和StdioServerTransport你只需要定义工具列表和处理函数。我实测下来一个最简单的MCP Server从零到跑通不超过80行代码。2.3 GPT-4o在链路中的角色定位GPT-4o不是用来“生成攻略文本”的它的核心任务是意图解析和工具编排。用户说“我想去成都吃火锅看熊猫”GPT-4o要拆解出目的地成都偏好美食动物然后决定先调geocode拿到成都坐标再调searchPOI搜火锅店和熊猫基地再调getForecast看那几天天气最后调searchHotels找春熙路附近的酒店。这个编排逻辑如果自己写if-else至少几百行而且脆弱得要命。交给GPT-4o之后我只需要在System Prompt里写清楚“你有这些工具可用请按需调用”剩下的它自己规划。这里有个关键细节GPT-4o的Function Calling返回的是工具调用请求不是最终答案。我的API路由需要拦截这个请求转发给对应的MCP Server执行拿到结果后再塞回对话历史让GPT-4o继续生成。这个循环可能跑2-5轮直到GPT-4o认为信息足够输出最终行程。3. MCP Server的实操实现细节3.1 从零写一个地图MCP Server先看依赖。package.json里只需要三个东西modelcontextprotocol/sdk、axios、zod。zod用来定义参数类型SDK会自动把zod schema转成JSON Schema给GPT-4o看。import { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { z } from zod; import axios from axios; const server new Server({ name: map-server, version: 1.0.0, }, { capabilities: { tools: {} } });定义工具的时候inputSchema用zod写比手搓JSON Schema舒服太多。比如searchPOIserver.tool( searchPOI, 根据关键词和城市搜索兴趣点返回名称、地址、坐标、评分, { keyword: z.string().describe(搜索关键词如火锅、熊猫基地), city: z.string().describe(城市名称如成都), limit: z.number().optional().default(5).describe(返回结果数量) }, async ({ keyword, city, limit }) { const response await axios.get(https://restapi.amap.com/v3/place/text, { params: { keywords: keyword, city: city, offset: limit, key: process.env.AMAP_KEY, extensions: all } }); const pois response.data.pois.map(p ({ name: p.name, address: p.address, location: p.location, rating: p.biz_ext?.rating || 暂无评分, type: p.type })); return { content: [{ type: text, text: JSON.stringify(pois, null, 2) }] }; } );注意return的格式必须是{ content: [{ type: text, text: ... }] }。这是MCP协议规定的返回结构GPT-4o只认这个。我一开始返回了裸对象结果模型完全无视排查了半小时才发现是格式问题。启动Server的代码就三行const transport new StdioServerTransport(); await server.connect(transport); console.error(Map MCP Server running on stdio);用console.error而不是console.log因为stdio传输模式下stdout被协议占用日志必须走stderr否则会污染通信数据。这个坑我踩过表现为Server启动后客户端收不到任何响应查了半天才发现是日志输出到了stdout。3.2 天气和酒店Server的差异化处理天气Server比地图简单因为和风天气的API返回结构很规整。但有个细节天气预报只能查未来7天如果用户规划的是10天后的行程得在工具描述里写清楚限制让GPT-4o知道“查不到就告诉用户”。我在getForecast的description里加了“仅支持未来7天超出范围返回空数组”这样模型不会硬编造数据。酒店Server最麻烦的是价格比较。不同OTA平台的接口字段名不一样有的叫price有的叫amount有的含税有的不含。我在Server内部做了归一化统一输出{ name, price, currency, breakfast, cancelPolicy }。这个归一化逻辑放在MCP Server里而不是GPT-4o的Prompt里因为模型做数值比较不可靠容易算错。让代码做确定性的事让模型做模糊判断的事这条边界要划清楚。3.3 把MCP Server接入Next.js的API路由Next.js这边我用modelcontextprotocol/sdk的Client类连接MCP Server。因为Server是stdio模式Client需要启动子进程。在Vercel的Serverless环境里子进程是可行的但冷启动会慢1-2秒。我的优化方案是在API路由的模块顶层初始化Client利用Vercel的实例复用机制避免每次请求都重启Server。import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; let mapClient: Client | null null; async function getMapClient() { if (mapClient) return mapClient; const transport new StdioClientTransport({ command: node, args: [./mcp-servers/map-server/dist/index.js] }); mapClient new Client({ name: nextjs-client, version: 1.0.0 }, { capabilities: {} }); await mapClient.connect(transport); return mapClient; }然后在streamText的tools参数里把MCP Server的工具列表动态注入。Vercel AI SDK支持tool函数我写了一个适配器把MCP的tool定义转成AI SDK的格式。这样GPT-4o看到的工具列表和MCP Server实际提供的完全一致不需要手动同步。4. 前端交互与流式输出的实现4.1 用useChat钩子实现打字机效果前端页面极其简单一个输入框、一个发送按钮、一个消息列表。核心是useChatconst { messages, input, handleInputChange, handleSubmit, isLoading } useChat({ api: /api/chat, onFinish: (message) { // 行程生成完毕可以触发保存或分享 } });useChat自动处理SSE流每收到一个token就更新messages状态React重新渲染用户就看到文字一个个蹦出来。我实测下来GPT-4o的首token延迟在800ms左右之后每秒输出30-50个token体验很流畅。但有个问题工具调用的中间状态用户看不到。GPT-4o在调searchPOI的时候前端是静默的用户可能以为卡住了。我的解决方案是在API路由里当检测到工具调用时往流里插入一个特殊标记比如[正在搜索成都的火锅店...]前端解析到这个标记就显示一个加载提示。这个标记用data类型的流事件发送不会污染最终文本。4.2 行程结果的结构化渲染GPT-4o最终输出的行程是Markdown格式但纯Markdown渲染出来很丑。我在前端做了一个简单的解析器识别## 第一天、- 09:00 宽窄巷子这种模式转成时间轴组件。每个景点卡片显示名称、地址、建议停留时间、预算。这个解析器不复杂200行以内但视觉效果提升巨大。预算汇总部分我让GPT-4o在最后输出一个JSON块前端用JSON.parse提取后渲染成饼图。这里有个技巧在System Prompt里明确要求“最后输出一个budget标签包裹的JSON”比让模型自由发挥可靠得多。模型有时候会忘记我加了few-shot示例之后准确率到95%以上。4.3 部署上线的最后一步Vercel部署没什么好说的vercel --prod一把梭。但环境变量要配好OPENAI_API_KEY、AMAP_KEY、QWEATHER_KEY、OTA_API_KEY。MCP Server的代码要编译成JS放在mcp-servers/*/dist/目录下Vercel构建时会一起打包。注意vercel.json里要配置functions的maxDuration默认10秒不够我设了60秒因为GPT-4o多轮工具调用可能跑30秒以上。域名绑定之后我第一时间用手机4G网络测了一遍确认没有跨域问题、没有SSL警告、流式输出正常。从写第一行代码到线上可访问正好72小时。5. 踩坑记录与排查技巧5.1 MCP Server无响应的三种常见原因第一种stdout被日志污染。前面提过console.log会破坏stdio通信。排查方法把Server单独跑起来在终端输入一个JSON-RPC请求看有没有正常返回。如果返回里混了日志文本就是这个问题。第二种工具参数类型不匹配。zod的z.number()如果传了字符串SDK会直接抛异常但异常信息可能被吞掉。我的做法是在每个工具处理函数里包一层try-catch把错误信息通过content返回给模型而不是让Server崩溃。这样GPT-4o能看到“参数错误limit应该是数字”它会自动修正重试。第三种Client连接超时。Vercel冷启动时子进程启动可能超过5秒。我在Client的connect方法里加了重试逻辑最多重试3次每次间隔1秒。实测下来冷启动成功率从70%提升到99%。5.2 GPT-4o不调用工具的排查思路有时候模型会“偷懒”明明有工具可用它直接编造答案。比如用户问“成都明天天气”它不调getForecast直接说“明天晴25度”。这是幻觉必须杜绝。我的解决方案是在System Prompt里加一条硬规则“任何涉及实时数据的问题必须先调用工具获取禁止凭记忆回答。如果工具返回空如实告知用户。”同时在API路由里做后置校验如果GPT-4o的输出里包含“天气”“温度”等关键词但对话历史里没有对应的工具调用记录就强制重新生成。这个校验逻辑用正则匹配简单但有效。5.3 流式输出中断的修复用户网络不稳定时SSE连接可能断开前端useChat会报错。我加了onError回调自动重试一次。如果重试还失败就把已生成的部分内容保留提示用户“网络波动点击继续”。这个体验比直接报错好很多。另一个坑是Vercel的Serverless函数有响应大小限制如果行程特别长比如7天详细规划流式输出可能被截断。我的应对策略是限制单次生成的token数在streamText里设maxTokens: 2000超出部分让用户点击“展开更多”再请求一次。这样既控制了成本又避免了截断。5.4 成本控制的实操数据GPT-4o的定价是输入$2.5/百万token输出$10/百万token。一次完整的旅游规划平均消耗输入3000 token含System Prompt和工具定义输出1500 token成本约$0.0225折合人民币一毛六。加上地图和天气API的调用费用高德免费额度每天5000次和风免费额度每天1000次MVP阶段每天100个用户总成本不到20块钱。这个账算下来独立开发者完全扛得住。6. 后续可扩展的方向这个产品目前只做了“生成行程”但MCP的潜力远不止于此。我下一步打算加两个Server一个是“翻译Server”调用DeepL API把行程里的景点介绍自动翻译成用户母语另一个是“分享Server”生成一个短链接把行程存到数据库用户可以把链接发给朋友。这两个Server加起来不超过200行代码但产品完整度会提升一个档次。另外MCP的生态正在快速膨胀。我注意到已经有社区贡献的MCP Server可以接Notion、Slack、GitHub。如果把这些串起来理论上可以做一个“AI旅行管家”行程生成后自动创建Notion页面、自动发Slack通知同行伙伴、自动在GitHub上开一个issue记录预算。这种跨工具的编排能力才是MCP真正可怕的地方。最后分享一个小技巧调试MCP Server的时候用npx modelcontextprotocol/inspector启动一个Web界面可以可视化地查看工具列表、手动调用工具、看返回结果。这个工具省了我大量时间比在终端里手搓JSON-RPC请求舒服多了。
返回列表