上个月我干了一件以前得花两周才能搞定的事:一个人,3天,做了一个AI旅游规划产品并上线。不是那种套壳聊天机器人,是真的能根据你输入的目的地、天数、预算和偏好,帮你排出带天气、带交通、带餐厅推荐的每日行程。整个过程的核心支撑就一个东西——MCP(Model Context Protocol,模型上下文协议)。如果你正在做AI应用,或者好奇为什么2025年MCP会被叫成“AI应用的USB-C接口”,这篇文章就是我的完整实战复盘,从产品设计、技术选型,到每一个踩过的坑,都会尽量讲透。
先说结论:MCP让“AI能自己决定什么时候去拿数据”这件事变得极其简单。以前我要写一堆接口、定义一堆JSON Schema、处理多轮工具调用的状态流转;现在,我把工具封装成一个MCP Server,大模型会自动学会怎么用这批工具。下面我把3天里做的事拆开讲,希望能给同样在独立开发AI产品的人一点参考。
1. 我为什么选MCP,以及它到底解决了什么问题
1.1 没有MCP时,做AI应用累在哪
想做一个AI旅游规划产品,最朴素的思路是:前端收集用户输入,后端调用大模型API,让它生成一份行程。但这有个硬伤——大模型的训练知识有截止日期,它不知道峨眉山今天的天气、不知道这周杭州有哪些展览、更不知道从灵隐寺到宋城打车要多久。所以要给它“接上数据”,传统做法很繁琐。
我拆了下,不用MCP时需要干这些事:
- 为每个数据源写独立的HTTP调用封装,比如天气接口、POI搜索接口、路径规划接口;
- 为每个接口再写一份Function Calling的JSON Schema,告诉模型这个函数叫什么、参数是什么、返回什么;
- 在业务代码里手动管理“模型想调用哪个函数、参数是否合法、返回结果怎么塞回上下文”的全过程;
- 如果换一个模型,比如从GPT换到国产模型,函数定义语法可能还要改一遍;
- 想复用这套能力到别的场景,比如做个网页版、做个小程序版,又得把工具层再包装一次。
这些不是写不出来,而是太耗时间。一个人做产品,时间是最贵的。对3天这个目标来说,传统做法根本行不通。
1.2 MCP把“接数据”这件事标准化了
MCP是Anthropic在2024年11月底开源的一个开放协议,全称Model Context Protocol,设计目标很明确:让AI模型与外部工具、数据源之间的连接变得标准化。你要是用过USB-C口,就很好理解MCP——以前各种设备充电口五花八门,现在一个口解决所有连接问题。MCP做的事情类似:任何支持MCP的AI客户端,都能通过同一套协议去调用任何支持MCP的Server,不管底层是什么数据源、什么API。
MCP的核心概念其实没几个,第一次接触时不要被“协议”两个字吓住:
- MCP Server:提供工具、资源、提示词的一方。比如我的旅游规划Server,里面封装了天气查询、POI搜索这些能力;
- MCP Client:连接Server的一方。Claude Desktop、Codex,以及我自己写的FastAPI后端,都是Client;
- Tool(工具):最常用的一类能力,给模型“动手”的机会,比如查询天气、查询景点;
- Resource(资源):给模型“读取”的上下文,比如一份景区介绍文档,一个本地文件;
- Prompt(提示词模板):在Server端预置了可复用的提示词,调用方一键使用;
- Transport(传输方式):Server和Client之间的连接通道,本地常用stdio,远程服务用HTTP。
这套分层设计让我省了大量时间。我的Server写一次,Claude Desktop可以连,我自己写的Web应用可以连,后续如果想让Codex也具备这个能力,只要给Codex配一下Server地址就行。之前说过想试一试idea插件通义灵码接MCP,其实也是一样的思路——工具层独立出来以后,接谁都是一行配置的事。
1.3 MCP和Function Calling不是一回事
很多人会把MCP和Function Calling(函数调用)混在一起,我这里纠正一下:Function Calling是模型API提供的一种能力,让模型在生成文本时输出一个结构化的函数调用请求;MCP则是Client与工具之间的通信协议。你可以理解成Function Calling是“模型内部的一种动作表达”,MCP是“模型所在的应用与外部工具之间的标准化管道”。
也就是说,MCP并不替代Function Calling,它是在Function Calling之上的规范。模型做决策(比如“现在该查天气了”),MCP负责把决策变成实际的数据请求,再把结果送回模型的上下文。说得更直白点:没有MCP时,我需要为每个模型手写函数定义;有MCP后,这些定义只在Server里声明一次,模型通过Protocol就能理解这些工具的描述和参数结构。
这也是为什么我在第二天能腾出时间做业务逻辑和前端。底层数据接入被MCP标准化以后,我不需要反复调试“为什么模型不按我定义的参数格式传值”这种蠢问题,而可以把精力放在用户真正会用到的东西上。
2. 产品设计与技术选型:先想清楚3天能做多少事
2.1 需求边界:不做什么比做什么更重要
独立开发最忌讳一开始就铺很大。我第一天上午在纸上写了一句话需求:用户输入目的地、游玩天数、出行偏好和预算档位,系统输出一份包含天气提醒、景点安排、交通耗时和餐厅推荐的行程单。功能就做这三步:表单填写、生成中状态、展示行程。什么注册登录、支付、社区、多语言,全部砍掉,将来再补。
这是3天交付的核心经验:把产品切成一个“看起来完整、用起来顺手”的闭环,但内部只保留最必要的逻辑。对于AI产品来说,行程单页面一定把“下载/分享”做进去,因为用户最自然的动作就是把规划结果发给朋友。我用了一份简单的分享链接页面,后端存SQLite,一URL就完事。
砍掉不重要的功能,不是为了偷懒,而是为了给“生成质量”留出足够的时间。同一个请求,模型第一次可能生成四天行程,第二次可能生成三天,我怎么对齐?这些问题比注册登录难得多,必须留时间。
2.2 技术栈清单与选型理由
定方案那一刻我其实是抄自己的旧作业,但有几处是专门为MCP做的选择。整体技术栈如下:
| 层级 | 方案 | 选择理由 |
|---|---|---|
| MCP Server | Python + FastMCP(官方SDK) | 生态最成熟,FastMCP语法糖写工具效率高 |
| 业务后端 | FastAPI | 异步天然适合转发大模型请求,和MCP Client在同一进程方便调试 |
| 数据源 | 高德开放平台 Web服务API | 覆盖天气、POI、路径规划,免费配额够个人项目用 |
| 存储 | SQLite | 单文件、零运维,不需要扛高并发 |
| 前端 | Vue3 + Vite + Element Plus | 表单和卡片类UI开发最快,我熟 |
| 部署 | 一台2C4G云服务器 + Nginx + PM2 | 成本可控,生态稳,重启恢复快 |
选型逻辑很简单:官方的坑最少,免费的配额最够用,部署最省事。MCP Server本来可以用TypeScript写,但我的核心业务在Python侧,FastAPI调用工具链最自然,所以直接用Python SDK。高德开放平台的API文档虽然老,但它同时提供天气、POI搜索和路径规划,一个Key全搞定,这对个人项目很友好。
数据库方面SQLite是有点寒酸,但一个记录行程的小系统根本不需要MySQL。预算是无限膨胀的,流量是慢慢来的,先把闭环跑通比什么都强。
2.3 我定义的5个MCP工具
MCP Server不是越复杂越好,工具要“少而精”,每一个都有明确用途。我设计的第一版工具集:
| 工具名 | 输入参数 | 数据来源 | 输出内容 |
|---|---|---|---|
| get_destination_overview | 城市名 | 高德行政区划+POI聚合 | 城市介绍、最佳旅行季节、消费水平标签 |
| get_weather_forecast | 城市名、天数 | 高德天气API | 逐日天气现象、温度范围、风力、降水概率 |
| get_poi_list | 城市名、关键词、POI类型 | 高德POI搜索 | 景点/餐厅/购物点名称、评分、地址、经纬度 |
| get_transport_time | 起点经纬度、终点经纬度、出行方式 | 高德路径规划2.0 | 预计耗时、距离、方案概要 |
| build_route_suggestion | 景点列表、游玩天数 | 本地聚类算法 | 按天分割的分区建议、每日动线顺序 |
设计这些工具有几条原则:职责单一,一个工具只做一件事,别搞“超级工具”;参数少,最好不超过4个,参数越多模型越容易传错;返回精简,每次返回给模型的内容控制在2000字以内,否则会撑爆上下文;失败信息要明确,比如城市查不到就返回“未找到该城市,请检查名称”而不要抛堆栈。
有一件事必须强调:工具的description是写给模型看的,不是写给程序员看的。你得用人话写清楚“什么场景下调用它,参数怎么传”。比如get_transport_time的描述不能只写“计算交通时间”,而要写“当用户希望知道两地之间如何通行、需要估算交通耗时与距离时使用,起点终点请传经纬度”。很多AI应用翻车就翻在描述写得太抽象,模型根本不知道什么时候该用这个工具。
3. 实操记录:从第1天写代码到第3天部署上线
3.1 第1天:把MCP Server跑通,先写两个核心工具
第1天我目标很小:先把MCP Server启动起来,能把天气和POI这两个工具调通就算赢。FastMCP的代码比我想象的简洁,一个工具就是一个函数加一个装饰器:
from mcp.server.fastmcp import FastMCP mcp = FastMCP("travel-planner") @mcp.tool() def get_weather_forecast(city: str, days: int = 7) -> str: """查询指定城市未来N天的天气预报,返回JSON字符串,包含日期、天气现象、最高/最低温度、降水概率。""" import requests params = { "key": WEATHER_API_KEY, "city": city, "extensions": "all", } resp = requests.get("https://restapi.amap.com/v3/weather/weatherInfo", params=params, timeout=10) data = resp.json() # 做数据清洗和截断,再返回给模型 return json.dumps( {"city": data.get("city"), "forecast": data.get("forecasts", [])}, ensure_ascii=False, ) @mcp.tool() def get_poi_list(city: str, keyword: str, poi_type: str = "风景名胜") -> str: """搜索指定城市与关键词相关的POI信息,返回名称、评分、地址、经纬度列表。""" ...实测下来,FastMCP这个语法糖让工具开发体感接近零门槛。你只要写好函数的类型注解和docstring,组件的注册、参数的JSON Schema生成、返回序列化,SDK全帮你干了。上午写好这两个工具后,我直接用MCP Inspector做了测试——这是官方提供的一个可视化调试工具,能直接连上Server、看到有哪些工具、手动调用工具并检查返回。
下午我开始思考一个细节:模型的工具调用不会每次都按我想象的顺序来。有时候它先调POI搜索,发现需要天气来建议“明天是否适合户外”,又回头调天气。这意味着Server要支持并发调用和快速响应,每个工具都要有超时保护。我给所有外部HTTP请求设置了10秒超时,一旦高德API抽风,工具立刻返回一个友好的错误提示,而不是让模型干等。
第一天的成果是:MCP Server已经能在stdio模式下跑通,Claude Desktop配置一下mcpServers就能用,我在对话框里直接问“杭州明天适合穿什么”,它自动调用了天气工具。这个瞬间让我觉得路子对了。
3.2 第2天:解决LLM与工具的协作问题,把数据“翻译”成行程
第2天是整个过程最烧脑的一天,因为工具能用了是一回事,生成一份像样的行程是另一回事。这里有一个核心设计:底层MCP工具负责拿数据,上层业务负责编排,大模型负责“翻译”。
我用了一个三层架构:
- 第一层是MCP工具层,就是前一天写好的那些能力,纯数据获取,不做业务判断;
- 第二层是行程编排服务(FastAPI),负责接收前端参数,维护会话状态,调度MCP工具并把结果整理成结构化中间数据;
- 第三层是生成层,通过System Prompt告诉大模型“你手里有哪些数据,你要按什么格式输出行程”。
这里的关键问题是:怎么让模型别把工具返回的JSON原样贴出来,而是转化成人类读得懂的行程文案?我给System Prompt写了这样一段话:
你是一名资深旅行规划师。根据下面的实时数据(天气、POI、交通耗时)生成一份每日行程,格式为:日期、天气概述、上午/下午/晚上安排、餐厅推荐、交通提示、当日注意事项。不要直接输出JSON,所有数据须转换为自然语言和建议。若某数据缺失,请明确说明并给出替代建议。
光靠这段还不够,因为模型可能会在生成过程中“自作聪明”地编造一些不存在的景点。我为这事想了个笨办法:先让模型写行程骨架(每天去哪个区域、玩什么类型),再调用工具拿数据,最后拿着真实数据做修正与润色。这不是一次Prompt能解决的,而是分两轮。第一轮生成骨架时我还会顺便拿到POI名单,第二轮把名单交给get_poi_list去查真实评分,再用“这些是真实数据,请根据它们调整行程”的方式发回模型。
这个“数据回流”设计很重要,直接决定产品是否可信。AI旅游产品最怕的就是推荐一个已经倒闭的餐厅、说一个错误开放的景点,有了工具校验这一层,至少能把幻觉压到最低。
同时第2天下午我搭了前端。表单页用了Vue3 + Element Plus,字段只有四个:目的地、天数、偏好标签(美食/人文/自然/亲子)、预算档位。提交后进入生成中状态,后端用SSE(Server-Sent Events)把“当前在查天气”“当前在找景点”等进度推给前端,这比让用户干等30秒体验好得多。进度提示是纯前端营销式的,但确实能显著降低焦虑感。
当天成果是一个能跑通全链路的Demo:用户输入“成都、3天、美食+自然、预算中等”,差不多40秒后看到一份包含每日天气、景点动线、餐厅推荐和交通建议的行程单。第一版仍然有不少问题,比如生成结果不稳定、偶尔会漏掉晚上的安排,但链路通了,后面要修就有方向了。
3.3 第3天:部署上线,远程HTTP Transport切换与真实用户测试
第3天上午做的第一件事,是把MCP Server从stdio模式切成streamable-http模式。本地调试用stdio没有问题,就是直接由父进程拉起子进程通信,但线上部署必须让FastAPI进程通过网络协议去访问MCP Server,所以得让Server监听一个端口。
不同版本SDK的命令略有差异,我用的版本大致是:
python travel_mcp_server.py --transport streamable-http --port 9000然后在FastAPI侧,不再用本地方便的StdioServerParameters,而是通过MCP的HTTP客户端去连:
from mcp.client.streamable_http import streamablehttp_client async with streamablehttp_client("http://127.0.0.1:9000/mcp") as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() # 后续通过 session.call_tool(...) 调用具体工具这里有个坑:尽管是“远程序列化”,MCP Server和FastAPI我直接部署在同一台服务器上,走内网HTTP。千万不要把它暴露到公网,除非你做了完整的鉴权。MCP这种能调用工具的接口,一旦裸奔被人扫到,风险极大。我在Nginx层做了IP白名单,同时MCP Server启动时校验一个自定义请求头,只有业务后端知道这个头存在。
部署的整体结构长这样:Nginx监听80/443,把 /api 路径反向代理到FastAPI的8000端口,另外转发静态前端文件。FastAPI里存了一份MCP ClientSession的异步连接池,按会话维度创建,避免多个用户请求共用一个Session导致协议状态冲突。
server { listen 443 ssl; server_name yourdomain.com; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/plantrip; try_files $uri $uri/ /index.html; } }HTTPS我用certbot一次性申请,几分钟就搞定。环境变量如高德Key、模型API Key全部写进.env文件,用PM2的--env-file参数加载,绝不能写死进代码。
下午我把链接发给几个朋友试,反馈集中在两个问题:一是“生成时间还是太慢”,二是“偶尔生成的行程里会出现半天空白”。前者主要是模型返回耗时+多轮工具调用叠加导致的,我在业务层加了超时控制和缓存,同一城市同一季节的天气与POI数据缓存一小时,第二次访问明显提速;后者是因为模型两次生成的模板不稳定,我在System Prompt里给了更严格的每日模板和“若某时间段无安排也要输出建议或休息”的约束,基本解决了。
上线那一刻其实没有什么仪式感,只是把域名解析一改,Nginx reload,然后刷新页面真实验证了一遍。当我自己输入数据看到一份完整行程的时候,才确认这个3天极限目标真的完成了。
4. 常见问题排查与独立开发心得
4.1 高频问题速查表
3天里遇到各种问题,我挑几个典型的列出来,方便大家照方抓药:
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 模型拿到工具返回结果却不理解 | 工具description写得抽象,模型不知道何时调用 | 重写description,说清“何时用、参数怎么传”,附一个使用示例 |
| 工具调用超时 | 外部API响应慢 | 所有HTTP请求设10秒超时;外部返回值在进入上下文前先清洗截断 |
| 同一个请求里模型连调了7次工具 | 工具粒度太细,模型需要多次尝试才能攒够数据 | 合并同类型工具,按区域批量返回;减少模型调用次数 |
| 返回的JSON在对话里被原样输出 | 缺少“把数据转成自然语言”的约束 | 在System Prompt里强约束输出格式,并用第二轮润色修正 |
| 多用户同时使用时偶发状态串线 | 共用了一个MCP ClientSession | 按请求维度分配独立Session,连接池复用但状态隔离 |
| 模型推荐了不存在的景点 | 大模型幻觉+未用工具校正 | 先规划骨架,再查POI,再润色修正,不让模型凭空发挥 |
这里我想专门提一句Browser Use MCP和Playwright MCP的区别,因为最近问这个的人很多。Browser Use MCP的核心定位是“让AI操作浏览器去完成真实任务”,比如自动刷网页、抓取动态内容,它把浏览器变成了模型的手脚;Playwright MCP则是把Playwright测试框架的能力暴露给模型,重点在“写断言、跑用例、定位网页元素”这类质量保障场景。前者适合做数据采集和网页操作,后者适合做自动化测试。我只能说“工具选型要看你的目标到底是完成业务流程还是证明页面能用”,这两个很容易因为都能控制浏览器而被混淆,但实际上解决的问题不同。
4.2 3天赶工的几条实操心得
如果让我总结这次独立开发的非技术心得,第一条是上午写核心、下午接依赖、晚上记笔记。第一天上午我的MCP Server已经跑通,第二天上午在写最难的数据回流逻辑,第三天上午在做部署切换。每天下午都很痛苦,都在和外部接口、环境配置捉迷藏,这时候不要硬扛,出去走15分钟再回来,问题往往一下就看清了。
第二条心得是给外部API加一层缓存和降级。高德的免费配额是有每日上限的,一旦被刷爆,整个产品就废了。我在POI和天气工具外层包了一层SQLite缓存,命中率其实很高,因为旅游出行场景对同城同季节的数据重复请求非常多。另外,当某个工具连续失败两次时,业务层自动降级——不再调用该工具,而是在最终文案里生成一句“该部分数据暂不可用,建议出行前再次确认”。降级总比卡死强,这也是一个产品成熟度的体现。
第三条是日志要足够多。一个人调试AI应用最大的痛点是模型行为不可预测,我每次请求都记录了完整链路:用户参数、模型工具调用顺序、每个工具返回时间、最终输出长度。这些日志帮我定位了大部分问题。你可以把日志理解成飞机的黑匣子,没有它,你面对的就是一个说不出原因的黑盒。
4.3 后续可以往哪里扩展
这个产品我在上线后没有停,已经列了几个后续方向。首先是接更多MCP Server:酒店比价、机票查询、甚至是“小红书热门机位”这类数据,都可以封装成独立Server,插进来不用大改业务代码。其次是多Agent协作,比如规划师Agent负责行程骨架,美食Agent负责餐厅筛选,交通Agent负责通勤效率,每个Agent共享同一个MCP工具池。第三是把模型层抽成网关,这样用户可以自由切换Claude、GPT、国产大模型,而不是被锁死在单一模型上。
还有一个我很想做的方向,就是让MCP工具接入更多真实设备的实时数据。比如通过MCP读用户本地日历,结合已有的假期安排推荐目的地;或者接地图实时拥堵数据,动态调整当天的动线。这些不是空谈,现在MCP生态里这类Server已经越来越多,装配式开发会成为主流。
写在最后的实操小技巧
如果你打算自己动手做一个MCP应用,最后分享一个小技巧:给MCP工具写一个独立的mock模式。我经常要在不依赖真实外部API的情况下调试整个流程,于是给Server加了一个环境变量开关,当MOCK_MODE=1时,所有工具返回预设的示例数据。这样前端、业务流程、模型Prompt这些环节全都可以脱离网络独立测试,只有最后联调时才切回真实API。我把这个开关写进启动命令里,几乎是零成本,但让调试速度提升了一个档次。
这次项目给我的直接体感是:MCP这条路径已经把“AI应用接入外部数据”的门槛降到了个人开发者也能3天出货的程度。模型负责聪明,MCP负责连接,我负责把两者粘在一起。AI产品的爆发期往往出现在工具链成熟之后,MCP的价值正在于此。