你有没有花了三个小时规划旅行,最后还是不知道去哪儿吃饭的经历?
信息分散是旅行规划最大的痛点:景点攻略在一个网站,天气预报在另一个,酒店预订又是第三个,手动整合下来,往往比旅行本身还累。
如果能有一个 AI 助手,你只需要说"我想去北京玩 3 天,喜欢历史文化,预算中等",它就能自动生成完整行程,包括景点、餐饮、酒店和预算,甚至在地图上帮你标好路线——这就是本文要介绍的智能旅行助手。
这不是 Demo,是一个从数据模型到前后端全部打通的完整项目。我们来拆解它的核心设计。
一、一个 Agent 搞不定的问题
最直觉的思路是:让一个 AI Agent 把所有事情做了——搜景点、查天气、找酒店、出行程。
但这条路很快就会遇到麻烦:
工具调用串行:每次只能调用一个工具,景点、天气、酒店三类信息要一步步来,等待时间叠加。
提示词爆炸:一个 Agent 要理解"景点偏好"、"天气格式"、"酒店筛选"、"预算估算"……提示词越写越长,LLM 越来越容易搞混各部分的格式和要求。
调试困难:出了问题不知道是哪个环节的锅,是景点搜索不准,还是天气解析出错,还是行程整合有问题?
解决方案:把任务分解,让不同的 Agent 各司其职——就像旅行社里有专门的景点顾问、酒店顾问、行程规划师,各自专注,最后汇总。
二、四个 Agent 的分工
这套系统设计了四个专门的 Agent:
前三个 Agent 负责"取数",第四个负责"用数"。PlannerAgent 不需要调用任何外部 API,它的工作就是把其他 Agent 的输出 + 用户原始需求,整合成一份完整、可用的旅行计划(JSON 格式)。
协作流程如下:
用户输入 ↓AttractionSearchAgent → 景点列表WeatherQueryAgent → 天气信息HotelAgent → 酒店列表 ↓PlannerAgent(整合所有信息) ↓完整旅行计划(JSON)每个 Agent 的提示词都很精简,只需要告诉它做什么、用什么工具、输出什么格式:
ATTRACTION_AGENT_PROMPT = """你是景点搜索专家。**工具调用格式:**`[TOOL_CALL:amap_maps_text_search:keywords=景点,city=城市名]`根据用户偏好({preferences})搜索{city}的景点。必须使用工具搜索,不要编造信息。"""而 PlannerAgent 的提示词则重点约束输出格式,确保返回的 JSON 能被系统直接解析:
PLANNER_AGENT_PROMPT = """你是行程规划专家。**输出格式(严格遵守):**{ "city": "城市名称", "days": [...], "weather_info": [...], "budget": { "total_attractions": 180, "total_hotels": 1200, "total_meals": 480, "total_transportation": 200, "total": 2060 }}**规划要求:**1. 每天安排 2-3 个景点2. 温度为纯数字(不带°C)3. 包含早中晚三餐4. 包含预算信息"""三、数据模型:先定义"语言",再写代码
多个 Agent 之间要传数据,前后端之间要传数据,如果各自用字典随便存,格式很快就乱了。
这里用 Pydantic 定义统一的数据结构。Pydantic 是 Python 的数据验证库,用类来表示结构,自动做类型检查和转换。
思路是自底向上:先定义最小单元,再逐层组合。
from pydantic import BaseModel, Field, field_validatorfrom typing import Optional, List# 最基础:地理坐标classLocation(BaseModel): longitude: float = Field(..., ge=-180, le=180) latitude: float = Field(..., ge=-90, le=90)# 景点classAttraction(BaseModel): name: str address: str location: Location visit_duration: int # 建议游览时间(分钟) ticket_price: int = 0 image_url: Optional[str] = None# 天气(带自定义解析)classWeatherInfo(BaseModel): date: str day_temp: int night_temp: int day_weather: str @field_validator('day_temp', 'night_temp', mode='before')defparse_temperature(cls, v):# 高德 API 返回 "16°C",需要转成数字if isinstance(v, str): v = v.replace('°C', '').replace('℃', '').strip()return int(v)return v# 单日行程classDayPlan(BaseModel): date: str attractions: List[Attraction] = [] hotel: Optional[Hotel] = None# 完整旅行计划classTripPlan(BaseModel): city: str start_date: str end_date: str days: List[DayPlan] = [] weather_info: List[WeatherInfo] = [] budget: Optional[Budget] = None这样做有两个直接好处:
类型错误早发现: ticket_price传了字符串?Pydantic 立刻报错,而不是等到计算预算时才炸。前后端格式一致:前端 TypeScript 的类型定义和后端 Pydantic 模型一一对应,减少对接成本。
四、MCP 工具:一次配置,16 个工具全有
三个 Agent 都需要调用高德地图 API。最省事的做法是用 MCP(Model Context Protocol)——一个连接 LLM 和外部工具的标准化协议。
项目用的是 amap-mcp-server,一个封装了高德地图全套 API 的 MCP 服务器。只需要配置一次:
from hello_agents.tools import MCPToolmcp_tool = MCPTool( name="amap_mcp", command="npx", args=["-y", "@sugarforever/amap-mcp-server"], env={"AMAP_API_KEY": "你的高德Key"}, auto_expand=True# ← 自动展开成多个工具)auto_expand=True 这个参数很关键:MCPTool 会自动查询服务器提供了哪些工具,然后为每个工具创建独立的 Tool 对象。一行配置,Agent 就拥有了高德的 16 个工具(POI 搜索、天气查询、路线规划……)。
共享实例,而不是每个 Agent 各建一个。三个 Agent 共用同一个 MCPTool,只启动一个 MCP 服务器进程,避免了资源浪费和 API 频率限制问题:
classTripPlannerAgent:def__init__(self): self.mcp_tool = MCPTool(...) # 只创建一次 self.attraction_agent = SimpleAgent(...) self.attraction_agent.add_tool(self.mcp_tool) # 共享 self.weather_agent = SimpleAgent(...) self.weather_agent.add_tool(self.mcp_tool) # 共享 self.hotel_agent = SimpleAgent(...) self.hotel_agent.add_tool(self.mcp_tool) # 共享五、把数据喂给 PlannerAgent
前三个 Agent 分别取回景点、天气、酒店数据后,需要把这些信息整合成一段提示词,交给 PlannerAgent 生成最终行程:
def_build_planner_query(self, request, attraction_resp, weather_resp, hotel_resp):returnf"""请根据以下信息生成{request.city}的{request.days}日旅行计划:**用户需求:**- 目的地: {request.city}- 日期: {request.start_date} 至 {request.end_date}- 偏好: {request.preferences}- 预算: {request.budget}**景点信息:**{attraction_resp}**天气信息:**{weather_resp}**酒店信息:**{hotel_resp}"""PlannerAgent 接收这段提示词,不调用任何工具,只做推理:把三份数据整合成一个符合 TripPlan 格式的 JSON,包含每日行程、天气安排、预算明细。
六、前端:让 AI 的输出真正"可用"
后端生成了 JSON,前端要把它变成用户真正能用的界面。
地图可视化:用高德地图 JS API,把每个景点的经纬度标注到地图上。
const initMap = async () => {const AMap = await AMapLoader.load({ key: 'your_web_key', version: '2.0' })const map = new AMap.Map('amap-container', { zoom: 12 }) tripPlan.days.forEach(day => { day.attractions.forEach((attraction, index) => {const marker = new AMap.Marker({ position: [attraction.location.longitude, attraction.location.latitude], title: attraction.name, label: { content: `${index + 1}`, direction: 'top' } }) map.add(marker) }) })}行程编辑:AI 生成的行程不一定完全合心意,用户可以进入编辑模式,删除或调换景点顺序。编辑前先深拷贝原始计划,取消时可以还原:
const toggleEditMode = () => { editMode.value = true originalPlan.value = JSON.parse(JSON.stringify(tripPlan.value)) // 深拷贝}const cancelEdit = () => { tripPlan.value = originalPlan.value // 还原 editMode.value = false}导出功能:用 html2canvas 把页面内容截成图片,再用 jsPDF 转为 PDF,方便保存和分享:
const exportAsPDF = async () => {const element = document.getElementById('trip-plan-content')const canvas = await html2canvas(element, { scale: 2, useCORS: true })const pdf = new jsPDF('p', 'mm', 'a4')const imgData = canvas.toDataURL('image/png')const imgWidth = 210const imgHeight = (canvas.height * imgWidth) / canvas.width pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight) pdf.save(`${tripPlan.city}旅行计划.pdf`)}七、几个值得记住的设计决策
回顾整个项目,有几个设计点值得单独记一下:
1. 数据模型是系统的"地基"。前后端用同一套数据结构,Pydantic 在后端做验证,TypeScript 在前端做类型检查,格式问题在开发阶段就能发现,而不是等到运行时。
2. 共享 MCP 实例。不是每个 Agent 都新建一个连接,而是共用一个 MCPTool,资源效率更高,也更容易控制 API 调用频率。
3. PlannerAgent 不调工具。整合推理和数据获取分开,PlannerAgent 只负责"想清楚怎么安排",不负责"去哪里找数据"。这让每个 Agent 的职责边界非常清晰。
4. 模拟进度条比真实进度更重要。后端处理需要 10-30 秒,前端的进度条是模拟的(每 500ms 推进一次),但它让用户知道系统在工作,不会因为"没反应"就以为卡死了。
小结
这个智能旅行助手项目展示了一个完整的多智能体应用从设计到落地的全过程:
任务分解:把复杂问题拆成 4 个专注的 Agent,而不是塞给一个 Agent 全搞定 数据建模:用 Pydantic 定义统一的数据结构,让前后端"说同一种语言" 工具集成:MCP 协议让外部 API 变成 Agent 可以自主调用的工具 体验闭环:地图可视化、行程编辑、PDF 导出,让 AI 输出真正可用
如果你正在学习 Agent 开发,这个项目是一个很好的参考:它不是玩具,是一个能跑起来、能用起来的真实应用。
夜雨聆风