trip_planner_agent.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718
  1. """多智能体旅行规划系统"""
  2. import json
  3. from concurrent.futures import ThreadPoolExecutor
  4. from typing import Dict, Any, List
  5. from hello_agents import SimpleAgent
  6. from .mcp_tool import MCPTool
  7. from ..services.llm_service import get_llm
  8. from ..services.amap_service import get_amap_service
  9. from ..models.schemas import TripRequest, TripPlan, DayPlan, Attraction, Meal, WeatherInfo, Location, Hotel, TransportSegment
  10. from ..config import get_settings
  11. # ============ Agent提示词 ============
  12. ATTRACTION_AGENT_PROMPT = """你是景点搜索专家。你的任务是根据城市和用户偏好搜索合适的景点。
  13. **重要提示:**
  14. 你必须使用工具来搜索景点!不要自己编造景点信息!
  15. **工具调用格式:**
  16. 使用maps_text_search工具时,必须严格按照以下格式:
  17. `[TOOL_CALL:amap_maps_text_search:keywords=景点关键词,city=城市名]`
  18. **示例:**
  19. 用户: "搜索北京的历史文化景点"
  20. 你的回复: [TOOL_CALL:amap_maps_text_search:keywords=历史文化,city=北京]
  21. 用户: "搜索上海的公园"
  22. 你的回复: [TOOL_CALL:amap_maps_text_search:keywords=公园,city=上海]
  23. **注意:**
  24. 1. 必须使用工具,不要直接回答
  25. 2. 格式必须完全正确,包括方括号和冒号
  26. 3. 参数用逗号分隔
  27. """
  28. WEATHER_AGENT_PROMPT = """你是天气查询专家。你的任务是查询指定城市的天气信息。
  29. **重要提示:**
  30. 你必须使用工具来查询天气!不要自己编造天气信息!
  31. **工具调用格式:**
  32. 使用maps_weather工具时,必须严格按照以下格式:
  33. `[TOOL_CALL:amap_maps_weather:city=城市名]`
  34. **示例:**
  35. 用户: "查询北京天气"
  36. 你的回复: [TOOL_CALL:amap_maps_weather:city=北京]
  37. 用户: "上海的天气怎么样"
  38. 你的回复: [TOOL_CALL:amap_maps_weather:city=上海]
  39. **注意:**
  40. 1. 必须使用工具,不要直接回答
  41. 2. 格式必须完全正确,包括方括号和冒号
  42. """
  43. HOTEL_AGENT_PROMPT = """你是酒店推荐专家。你的任务是根据城市和景点位置推荐合适的酒店。
  44. **重要提示:**
  45. 你必须使用工具来搜索酒店!不要自己编造酒店信息!
  46. **工具调用格式:**
  47. 使用maps_text_search工具搜索酒店时,必须严格按照以下格式:
  48. `[TOOL_CALL:amap_maps_text_search:keywords=酒店,city=城市名]`
  49. **示例:**
  50. 用户: "搜索北京的酒店"
  51. 你的回复: [TOOL_CALL:amap_maps_text_search:keywords=酒店,city=北京]
  52. **注意:**
  53. 1. 必须使用工具,不要直接回答
  54. 2. 格式必须完全正确,包括方括号和冒号
  55. 3. 关键词使用"酒店"或"宾馆"
  56. """
  57. PLANNER_AGENT_PROMPT = """你是行程规划专家。你的任务是根据景点信息、天气信息和出行人群,生成个性化的旅行计划。
  58. 请严格按照以下JSON格式返回旅行计划(**transportation_details字段由系统自动填充,你无需生成,但必须保证attractions和hotel的address字段真实准确**):
  59. ```json
  60. {
  61. "city": "城市名称",
  62. "start_date": "YYYY-MM-DD",
  63. "end_date": "YYYY-MM-DD",
  64. "days": [
  65. {
  66. "date": "YYYY-MM-DD",
  67. "day_index": 0,
  68. "description": "第1天行程概述",
  69. "transportation": "交通方式概览",
  70. "accommodation": "住宿类型",
  71. "hotel": {
  72. "name": "酒店名称",
  73. "address": "酒店地址",
  74. "location": {"longitude": 116.397128, "latitude": 39.916527},
  75. "price_range": "300-500元",
  76. "rating": "4.5",
  77. "distance": "距离景点2公里",
  78. "type": "经济型酒店",
  79. "estimated_cost": 400
  80. },
  81. "attractions": [
  82. {
  83. "name": "景点名称",
  84. "address": "详细地址",
  85. "location": {"longitude": 116.397128, "latitude": 39.916527},
  86. "visit_duration": 120,
  87. "description": "景点详细描述",
  88. "category": "景点类别",
  89. "ticket_price": 60
  90. }
  91. ],
  92. "meals": [
  93. {"type": "breakfast", "name": "早餐推荐", "description": "早餐描述", "estimated_cost": 30},
  94. {"type": "lunch", "name": "午餐推荐", "description": "午餐描述", "estimated_cost": 50},
  95. {"type": "dinner", "name": "晚餐推荐", "description": "晚餐描述", "estimated_cost": 80}
  96. ]
  97. }
  98. ],
  99. "weather_info": [
  100. {
  101. "date": "YYYY-MM-DD",
  102. "day_weather": "晴",
  103. "night_weather": "多云",
  104. "day_temp": 25,
  105. "night_temp": 15,
  106. "wind_direction": "南风",
  107. "wind_power": "1-3级"
  108. }
  109. ],
  110. "overall_suggestions": "总体建议",
  111. "budget": {
  112. "total_attractions": 180,
  113. "total_hotels": 1200,
  114. "total_meals": 480,
  115. "total_transportation": 200,
  116. "total": 2060
  117. }
  118. }
  119. ```
  120. **出行人群定制指南:**
  121. 根据不同的出行人群,调整行程安排风格:
  122. - **独自旅行**: 推荐经济型住宿(青旅/青舍),安排社交友好型活动,景点紧凑高效,推荐当地特色小吃,控制预算
  123. - **情侣夫妻**: 安排浪漫景点(日落观景台、情侣步道),推荐氛围好的餐厅,选择舒适型以上酒店,安排双人体验活动
  124. - **朋友结伴**: 安排集体互动性强的活动,推荐娱乐项目,住宿可选多人间或民宿,餐饮推荐适合聚会的场所
  125. - **家庭亲子**: 安排儿童友好的景点(科技馆、动物园、主题乐园),节奏要宽松,餐饮选择适合孩子的餐厅,住宿推荐家庭房
  126. - **公司团建**: 安排团队协作活动,推荐大型场地,兼顾会议讨论空间与休闲娱乐,住宿可选度假型酒店
  127. - **老年旅行**: 行程节奏舒缓,景点平坦少爬坡,步行距离短,推荐养生餐饮,住宿选择舒适型电梯房
  128. - **研学旅行**: 安排博物馆、科技馆、历史文化遗址等教育性景点,每个景点预留充足学习时间,可安排讲解服务
  129. **重要提示:**
  130. 1. weather_info数组必须包含每一天的天气信息
  131. 2. 温度必须是纯数字(不要带°C等单位)
  132. 3. 每天安排2-3个景点
  133. 4. 考虑景点之间的距离和游览时间
  134. 5. 每天必须包含早中晚三餐
  135. 6. 提供实用的旅行建议
  136. 7. 行程安排必须符合用户选择的"出行人群"类型
  137. 8. **必须包含预算信息**:
  138. - 景点门票价格(ticket_price)
  139. - 餐饮预估费用(estimated_cost)
  140. - 酒店预估费用(estimated_cost)
  141. - 预算汇总(budget)包含各项总费用
  142. """
  143. class MultiAgentTripPlanner:
  144. """多智能体旅行规划系统"""
  145. def __init__(self):
  146. """初始化多智能体系统"""
  147. print("🔄 开始初始化多智能体旅行规划系统...")
  148. try:
  149. settings = get_settings()
  150. self.llm = get_llm()
  151. # 创建三个独立的MCP工具实例,每个Agent独享一个
  152. # 这是并行化的前提:多个子进程同时调用高德MCP不会相互干扰
  153. print(" - 创建MCP工具实例(景点搜索)...")
  154. self.amap_tool_attraction = MCPTool(
  155. name="amap",
  156. description="高德地图服务(景点搜索)",
  157. server_command=["uvx", "amap-mcp-server"],
  158. env={"AMAP_MAPS_API_KEY": settings.amap_api_key},
  159. auto_expand=True
  160. )
  161. print(" - 创建MCP工具实例(天气查询)...")
  162. self.amap_tool_weather = MCPTool(
  163. name="amap",
  164. description="高德地图服务(天气查询)",
  165. server_command=["uvx", "amap-mcp-server"],
  166. env={"AMAP_MAPS_API_KEY": settings.amap_api_key},
  167. auto_expand=True
  168. )
  169. print(" - 创建MCP工具实例(酒店推荐)...")
  170. self.amap_tool_hotel = MCPTool(
  171. name="amap",
  172. description="高德地图服务(酒店推荐)",
  173. server_command=["uvx", "amap-mcp-server"],
  174. env={"AMAP_MAPS_API_KEY": settings.amap_api_key},
  175. auto_expand=True
  176. )
  177. # 创建景点搜索Agent
  178. print(" - 创建景点搜索Agent...")
  179. self.attraction_agent = SimpleAgent(
  180. name="景点搜索专家",
  181. llm=self.llm,
  182. system_prompt=ATTRACTION_AGENT_PROMPT
  183. )
  184. self.attraction_agent.add_tool(self.amap_tool_attraction)
  185. # 创建天气查询Agent
  186. print(" - 创建天气查询Agent...")
  187. self.weather_agent = SimpleAgent(
  188. name="天气查询专家",
  189. llm=self.llm,
  190. system_prompt=WEATHER_AGENT_PROMPT
  191. )
  192. self.weather_agent.add_tool(self.amap_tool_weather)
  193. # 创建酒店推荐Agent
  194. print(" - 创建酒店推荐Agent...")
  195. self.hotel_agent = SimpleAgent(
  196. name="酒店推荐专家",
  197. llm=self.llm,
  198. system_prompt=HOTEL_AGENT_PROMPT
  199. )
  200. self.hotel_agent.add_tool(self.amap_tool_hotel)
  201. # 创建行程规划Agent(不需要工具)
  202. print(" - 创建行程规划Agent...")
  203. self.planner_agent = SimpleAgent(
  204. name="行程规划专家",
  205. llm=self.llm,
  206. system_prompt=PLANNER_AGENT_PROMPT
  207. )
  208. print(f"✅ 多智能体系统初始化成功")
  209. print(f" 景点搜索Agent: {len(self.attraction_agent.list_tools())} 个工具(独立实例)")
  210. print(f" 天气查询Agent: {len(self.weather_agent.list_tools())} 个工具(独立实例)")
  211. print(f" 酒店推荐Agent: {len(self.hotel_agent.list_tools())} 个工具(独立实例)")
  212. except Exception as e:
  213. print(f"❌ 多智能体系统初始化失败: {str(e)}")
  214. import traceback
  215. traceback.print_exc()
  216. raise
  217. def plan_trip(self, request: TripRequest) -> TripPlan:
  218. """
  219. 使用多智能体协作生成旅行计划
  220. Args:
  221. request: 旅行请求
  222. Returns:
  223. 旅行计划
  224. """
  225. try:
  226. print(f"\n{'='*60}")
  227. print(f"🚀 开始多智能体协作规划旅行...")
  228. print(f"目的地: {request.city}")
  229. print(f"日期: {request.start_date} 至 {request.end_date}")
  230. print(f"天数: {request.travel_days}天")
  231. print(f"偏好: {', '.join(request.preferences) if request.preferences else '无'}")
  232. print(f"出行人群: {request.traveler_group if request.traveler_group else '未指定'}")
  233. print(f"{'='*60}\n")
  234. # ── 阶段一:并行执行无依赖的搜索/查询任务 ──
  235. print("🚀 阶段一:并行搜索景点、天气、酒店...")
  236. with ThreadPoolExecutor(max_workers=3) as executor:
  237. future_attractions = executor.submit(
  238. self.attraction_agent.run,
  239. self._build_attraction_query(request)
  240. )
  241. future_weather = executor.submit(
  242. self.weather_agent.run,
  243. f"请查询{request.city}的天气信息"
  244. )
  245. future_hotel = executor.submit(
  246. self.hotel_agent.run,
  247. f"请搜索{request.city}的{request.accommodation}酒店"
  248. )
  249. # 等待全部完成(屏障),按原始顺序获取结果
  250. print(" ⏳ 等待三个并行任务完成...")
  251. attraction_response = future_attractions.result()
  252. print(f"📍 景点搜索完成: {attraction_response[:200]}...\n")
  253. weather_response = future_weather.result()
  254. print(f"🌤️ 天气查询完成: {weather_response[:200]}...\n")
  255. hotel_response = future_hotel.result()
  256. print(f"🏨 酒店搜索完成: {hotel_response[:200]}...\n")
  257. # ── 阶段二:依赖阶段一的结果,顺序执行 ──
  258. print("📋 阶段二:生成行程计划...")
  259. planner_query = self._build_planner_query(request, attraction_response, weather_response, hotel_response)
  260. planner_response = self.planner_agent.run(planner_query)
  261. print(f"行程规划结果: {planner_response[:300]}...\n")
  262. # 解析最终计划
  263. trip_plan = self._parse_response(planner_response, request)
  264. # 步骤5: 调用高德地图MCP获取真实交通路线数据
  265. print("🚗 步骤5: 获取真实交通路线数据...")
  266. trip_plan = self._enrich_with_real_routes(trip_plan, request)
  267. print(f"交通路线获取完成\n")
  268. print(f"{'='*60}")
  269. print(f"✅ 旅行计划生成完成!")
  270. print(f"{'='*60}\n")
  271. return trip_plan
  272. except Exception as e:
  273. print(f"❌ 生成旅行计划失败: {str(e)}")
  274. import traceback
  275. traceback.print_exc()
  276. return self._create_fallback_plan(request)
  277. def _build_attraction_query(self, request: TripRequest) -> str:
  278. """构建景点搜索查询 - 直接包含工具调用"""
  279. keywords = []
  280. if request.preferences:
  281. # 如果用户有明确的偏好,使用偏好标签作为关键词
  282. keywords = request.preferences
  283. else:
  284. keywords = "景点"
  285. # 根据出行人群调整搜索关键词
  286. group_keywords = {
  287. "独自旅行": "景点",
  288. "情侣夫妻": "浪漫景点",
  289. "朋友结伴": "热门景点",
  290. "家庭亲子": "亲子景点",
  291. "公司团建": "景点",
  292. "老年旅行": "公园",
  293. "研学旅行": "博物馆"
  294. }
  295. if request.traveler_group and request.traveler_group in group_keywords:
  296. # 如果用户没有明确偏好,使用人群推荐的关键词
  297. if not request.preferences:
  298. keywords = group_keywords[request.traveler_group]
  299. # 直接返回工具调用格式
  300. query = f"请使用amap_maps_text_search工具搜索{request.city}与{keywords}相关的景点。\n[TOOL_CALL:amap_maps_text_search:keywords={keywords},city={request.city}]"
  301. return query
  302. def _build_planner_query(self, request: TripRequest, attractions: str, weather: str, hotels: str = "") -> str:
  303. """构建行程规划查询"""
  304. # 出行人群定制指导
  305. group_guidance = {
  306. "独自旅行": "该用户是独自旅行:\n- 推荐经济型住宿(青旅/青舍),安排社交友好型活动\n- 景点紧凑高效,推荐当地特色小吃\n- 控制预算,推荐性价比高的选择",
  307. "情侣夫妻": "该用户是情侣/夫妻出行:\n- 安排浪漫景点(日落观景台、情侣步道等)\n- 推荐氛围好的餐厅,选择舒适型以上酒店\n- 安排双人体验活动,注重私密性和舒适度",
  308. "朋友结伴": "该用户是朋友结伴出行:\n- 安排集体互动性强的活动,推荐娱乐项目\n- 住宿可选多人间或民宿\n- 餐饮推荐适合聚会的场所,推荐热闹区域",
  309. "家庭亲子": "该用户是家庭亲子出行(有儿童):\n- 安排儿童友好的景点(科技馆、动物园、主题乐园)\n- 行程节奏要宽松,避免安排过满\n- 餐饮选择适合孩子的餐厅,住宿推荐家庭房",
  310. "公司团建": "该用户是公司团建:\n- 安排团队协作活动,推荐大型场地\n- 兼顾会议讨论空间与休闲娱乐\n- 住宿可选度假型酒店,推荐集体用餐",
  311. "老年旅行": "该用户是老年旅行:\n- 行程节奏舒缓,景点平坦少爬坡\n- 步行距离短,每个景点预留充足休息时间\n- 推荐养生餐饮,住宿选择舒适型电梯房",
  312. "研学旅行": "该用户是研学旅行:\n- 安排博物馆、科技馆、历史文化遗址等教育性景点\n- 每个景点预留充足学习时间\n- 可安排讲解服务,注重知识性"
  313. }
  314. traveler_note = ""
  315. if request.traveler_group and request.traveler_group in group_guidance:
  316. traveler_note = f"\n**出行人群:** {request.traveler_group}\n{group_guidance[request.traveler_group]}\n"
  317. query = f"""请根据以下信息生成{request.city}的{request.travel_days}天旅行计划:
  318. **基本信息:**
  319. - 城市: {request.city}
  320. - 日期: {request.start_date} 至 {request.end_date}
  321. - 天数: {request.travel_days}天
  322. - 交通方式: {request.transportation}
  323. - 住宿: {request.accommodation}
  324. - 偏好: {', '.join(request.preferences) if request.preferences else '无'}
  325. {traveler_note}
  326. **景点信息:**
  327. {attractions}
  328. **天气信息:**
  329. {weather}
  330. **酒店信息:**
  331. {hotels}
  332. **要求:**
  333. 1. 每天安排2-3个景点
  334. 2. 每天必须包含早中晚三餐
  335. 3. 每天推荐一个具体的酒店(从酒店信息中选择)
  336. 3. 考虑景点之间的距离和交通方式(仅填写transportation概览字段即可)
  337. 4. 返回完整的JSON格式数据
  338. 5. 景点的经纬度坐标和地址(address)要真实准确
  339. 6. 行程安排必须充分考虑"出行人群"的特点
  340. """
  341. if request.free_text_input:
  342. query += f"\n**额外要求:** {request.free_text_input}"
  343. return query
  344. def _enrich_with_real_routes(self, plan: TripPlan, request: TripRequest) -> TripPlan:
  345. """调用高德地图MCP获取真实交通数据,填充transportation_details"""
  346. try:
  347. amap = get_amap_service()
  348. city = request.city
  349. # 用户交通方式 → MCP route_type 映射
  350. route_type_map = {
  351. "公共交通": "transit",
  352. "自驾": "driving",
  353. "步行": "walking",
  354. "混合": "transit", # 默认用公共交通
  355. }
  356. route_type = route_type_map.get(request.transportation, "transit")
  357. type_label = {
  358. "transit": "公共交通",
  359. "driving": "自驾",
  360. "walking": "步行",
  361. }
  362. for day in plan.days:
  363. details = []
  364. waypoints = [] # (name, address)
  365. # 起点: 酒店(如果有地址)
  366. if day.hotel and day.hotel.address:
  367. waypoints.append((day.hotel.name, day.hotel.address))
  368. elif day.hotel and day.hotel.location:
  369. waypoints.append((day.hotel.name, f"{city}市"))
  370. else:
  371. waypoints.append(("酒店", f"{city}市区"))
  372. # 中间点: 景点
  373. for attr in day.attractions:
  374. addr = attr.address or f"{city}市"
  375. waypoints.append((attr.name, addr))
  376. # 终点: 回酒店(如果酒店在起点后有地址)
  377. if day.hotel and day.hotel.address and len(waypoints) > 1:
  378. waypoints.append((day.hotel.name, day.hotel.address))
  379. # 逐个分段调MCP(带降级重试)
  380. total_duration = 0
  381. total_distance = 0
  382. had_fallback = False
  383. for i in range(len(waypoints) - 1):
  384. from_name, from_addr = waypoints[i]
  385. to_name, to_addr = waypoints[i + 1]
  386. # 按优先级尝试路线类型
  387. route_types_to_try = [route_type]
  388. if route_type == "transit":
  389. route_types_to_try = ["transit", "driving"]
  390. elif route_type == "driving":
  391. route_types_to_try = ["driving", "transit"]
  392. segments = []
  393. attempted_types = []
  394. success_type = None
  395. for try_route_type in route_types_to_try:
  396. attempted_types.append(try_route_type)
  397. segments = amap.get_route_via_http(
  398. origin_address=from_addr,
  399. destination_address=to_addr,
  400. origin_name=from_name,
  401. destination_name=to_name,
  402. origin_city=city,
  403. destination_city=city,
  404. route_type=try_route_type
  405. )
  406. if segments:
  407. success_type = try_route_type
  408. break
  409. is_fallback = success_type and success_type != route_type
  410. if segments:
  411. for seg in segments:
  412. seg_from = from_name if not details else seg.get("from_name", from_name)
  413. seg_to = to_name if i == len(waypoints) - 2 else seg.get("to_name", to_name)
  414. seg["from_name"] = seg_from
  415. seg["to_name"] = seg_to
  416. # 处理回退: 用户选公交但用了驾车数据
  417. if is_fallback:
  418. had_fallback = True
  419. seg["type"] = type_label.get(route_type, "公共交通")
  420. seg["route_detail"] = (seg.get("route_detail", "") + " · 驾车参考").strip(" ·")
  421. if "驾车" not in seg.get("instruction", ""):
  422. seg["instruction"] += "(驾车参考路线)"
  423. details.append(seg)
  424. total_duration += seg.get("duration", 0)
  425. total_distance += seg.get("distance", 0)
  426. else:
  427. # 所有API都失败,尝试LLM估计路线
  428. llm_segments = self._estimate_routes_with_llm(
  429. from_name=from_name,
  430. to_name=to_name,
  431. city=city,
  432. route_type_label=type_label.get(route_type, "公共交通")
  433. )
  434. if llm_segments:
  435. for seg in llm_segments:
  436. seg["from_name"] = from_name
  437. seg["to_name"] = to_name
  438. details.append(seg)
  439. total_duration += seg.get("duration", 0)
  440. total_distance += seg.get("distance", 0)
  441. else:
  442. # LLM也失败,生成占位段
  443. road_type_cn = type_label.get(route_type, "公共交通")
  444. details.append({
  445. "type": road_type_cn,
  446. "instruction": f"从{from_name}前往{to_name}",
  447. "from_name": from_name,
  448. "to_name": to_name,
  449. "departure_time": "08:00",
  450. "duration": 30,
  451. "distance": 2000,
  452. "route_detail": "路线规划暂不可用",
  453. })
  454. total_duration += 30
  455. total_distance += 2000
  456. # 回填详细交通数据到DayPlan(dict -> TransportSegment)
  457. day.transportation_details = [
  458. TransportSegment(**seg) for seg in details
  459. ]
  460. # 更新概要transportation字段
  461. if total_duration > 0:
  462. road_type_cn = type_label.get(route_type, "公共交通")
  463. dist_km = round(total_distance / 1000, 1)
  464. fallback_note = "(部分路段为驾车参考)" if had_fallback else ""
  465. day.transportation = f"{road_type_cn} · 共{dist_km}公里 · 约{total_duration}分钟{fallback_note}"
  466. print(f" ✅ 第{day.day_index + 1}天交通: {len(details)}段, {day.transportation}")
  467. except Exception as e:
  468. print(f"⚠️ 获取真实路线数据失败: {e}")
  469. return plan
  470. def _estimate_routes_with_llm(
  471. self,
  472. from_name: str,
  473. to_name: str,
  474. city: str,
  475. route_type_label: str = "公共交通"
  476. ) -> List[Dict]:
  477. """当高德API路线获取失败时,用LLM估计交通路线"""
  478. prompt = f"""请估计从"{from_name}"到"{to_name}"(位于{city})的{route_type_label}路线。
  479. 根据你对{city}的了解,生成合理的路线分段信息。只返回JSON数组,不要其他文字:
  480. [
  481. {{
  482. "type": "步行/公交/地铁",
  483. "instruction": "具体乘坐指引(如'乘坐1路公交车从火车站到市中心')",
  484. "from_name": "起点站名或地点",
  485. "to_name": "终点站名或地点",
  486. "duration": 15,
  487. "distance": 2000,
  488. "route_detail": "线路详情(如'经过5站'或'约2公里')"
  489. }}
  490. ]
  491. 要求:
  492. 1. type取值 "步行"/"公交"/"地铁",可组合多个分段
  493. 2. duration单位分钟,distance单位米,数值要合理
  494. 3. 根据{city}实际公交/地铁线路命名习惯来写
  495. 4. 仅返回JSON数组,不要markdown标记"""
  496. try:
  497. from hello_agents import SimpleAgent
  498. estimator = SimpleAgent(
  499. name="route_estimator",
  500. llm=self.llm,
  501. system_prompt="你是城市交通专家,根据起点终点和城市信息合理估计路线。只返回JSON。"
  502. )
  503. response = estimator.run(prompt)
  504. # 提取JSON
  505. json_str = response.strip()
  506. if "```json" in json_str:
  507. json_str = json_str.split("```json")[1].split("```")[0]
  508. elif "```" in json_str:
  509. json_str = json_str.split("```")[1].split("```")[0]
  510. import re
  511. match = re.search(r'\[.*?\]', json_str, re.DOTALL)
  512. if match:
  513. data = json.loads(match.group())
  514. if isinstance(data, list):
  515. print(f" ✅ LLM路线估计成功: {len(data)}段")
  516. return data
  517. except Exception as e:
  518. print(f" ⚠️ LLM路线估计失败: {e}")
  519. return []
  520. def _parse_response(self, response: str, request: TripRequest) -> TripPlan:
  521. """
  522. 解析Agent响应
  523. Args:
  524. response: Agent响应文本
  525. request: 原始请求
  526. Returns:
  527. 旅行计划
  528. """
  529. try:
  530. # 尝试从响应中提取JSON
  531. # 查找JSON代码块
  532. if "```json" in response:
  533. json_start = response.find("```json") + 7
  534. json_end = response.find("```", json_start)
  535. json_str = response[json_start:json_end].strip()
  536. elif "```" in response:
  537. json_start = response.find("```") + 3
  538. json_end = response.find("```", json_start)
  539. json_str = response[json_start:json_end].strip()
  540. elif "{" in response and "}" in response:
  541. # 直接查找JSON对象
  542. json_start = response.find("{")
  543. json_end = response.rfind("}") + 1
  544. json_str = response[json_start:json_end]
  545. else:
  546. raise ValueError("响应中未找到JSON数据")
  547. # 解析JSON
  548. data = json.loads(json_str)
  549. # 转换为TripPlan对象
  550. trip_plan = TripPlan(**data)
  551. return trip_plan
  552. except Exception as e:
  553. print(f"⚠️ 解析响应失败: {str(e)}")
  554. print(f" 将使用备用方案生成计划")
  555. return self._create_fallback_plan(request)
  556. def _create_fallback_plan(self, request: TripRequest) -> TripPlan:
  557. """创建备用计划(当Agent失败时)"""
  558. from datetime import datetime, timedelta
  559. # 解析日期
  560. start_date = datetime.strptime(request.start_date, "%Y-%m-%d")
  561. # 出行人群描述
  562. group_desc = {
  563. "独自旅行": "适合独自旅行者",
  564. "情侣夫妻": "适合情侣/夫妻浪漫之旅",
  565. "朋友结伴": "适合朋友结伴游玩",
  566. "家庭亲子": "适合家庭亲子活动",
  567. "公司团建": "适合公司团建活动",
  568. "老年旅行": "适合老年休闲之旅",
  569. "研学旅行": "适合研学教育之旅"
  570. }
  571. traveler_desc = group_desc.get(request.traveler_group, "")
  572. # 创建每日行程
  573. days = []
  574. for i in range(request.travel_days):
  575. current_date = start_date + timedelta(days=i)
  576. group_note = f"({traveler_desc}) " if traveler_desc else ""
  577. day_plan = DayPlan(
  578. date=current_date.strftime("%Y-%m-%d"),
  579. day_index=i,
  580. description=f"第{i+1}天行程{group_note}- 探索{request.city}",
  581. transportation=request.transportation,
  582. accommodation=request.accommodation,
  583. attractions=[
  584. Attraction(
  585. name=f"{request.city}景点{j+1}",
  586. address=f"{request.city}市",
  587. location=Location(longitude=116.4 + i*0.01 + j*0.005, latitude=39.9 + i*0.01 + j*0.005),
  588. visit_duration=120,
  589. description=f"这是{request.city}的著名景点",
  590. category="景点"
  591. )
  592. for j in range(2)
  593. ],
  594. meals=[
  595. Meal(type="breakfast", name=f"第{i+1}天早餐", description="当地特色早餐"),
  596. Meal(type="lunch", name=f"第{i+1}天午餐", description="午餐推荐"),
  597. Meal(type="dinner", name=f"第{i+1}天晚餐", description="晚餐推荐")
  598. ]
  599. )
  600. days.append(day_plan)
  601. return TripPlan(
  602. city=request.city,
  603. start_date=request.start_date,
  604. end_date=request.end_date,
  605. days=days,
  606. weather_info=[],
  607. overall_suggestions=f"这是为您规划的{request.city}{request.travel_days}日游行程{'(适合' + traveler_desc + ')' if traveler_desc else ''}。建议提前查看各景点的开放时间。"
  608. )
  609. # 全局多智能体系统实例
  610. _multi_agent_planner = None
  611. def get_trip_planner_agent() -> MultiAgentTripPlanner:
  612. """获取多智能体旅行规划系统实例(单例模式)"""
  613. global _multi_agent_planner
  614. if _multi_agent_planner is None:
  615. _multi_agent_planner = MultiAgentTripPlanner()
  616. return _multi_agent_planner