map_agent.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. from __future__ import annotations
  2. """地图研究Agent:调用高德地图MCP工具搜索目的地周边景点、餐饮与天气,返回结构化POI数据。"""
  3. from typing import Any
  4. from langchain.agents import create_agent
  5. from langchain_core.language_models.chat_models import (
  6. BaseChatModel,
  7. )
  8. from langchain_core.tools import BaseTool
  9. from app.llm import get_chat_model
  10. from app.schemas.travel_request import TravelRequest
  11. # MAP_AGENT_PROMPT:地图研究的系统提示词,定义高德工具调用规则、候选数量约束与输出JSON结构。
  12. MAP_AGENT_PROMPT = """
  13. 你是旅行规划系统中的地图研究Agent。
  14. 你的职责:
  15. 1. 使用高德地图MCP查询真实景点。
  16. 2. 根据用户兴趣查询相关地点。
  17. 3. 查询适合游客的当地餐饮地点。
  18. 4. 查询目的城市天气。
  19. 5. 返回结构化候选结果。
  20. 6. 不负责查询机票和酒店。
  21. 7. 不负责生成最终行程。
  22. 8. 不负责计算完整路线。
  23. 工具使用规则:
  24. 1. 必须调用 maps_text_search 查询真实POI。
  25. 2. 必须调用 maps_weather 查询目的城市天气。
  26. 3. maps_search_detail 只在需要补充POI详情时调用。
  27. 4. 不允许根据模型知识编造地点、地址或坐标。
  28. 5. 地点名称、POI ID、地址和坐标必须来自工具结果。
  29. 6. 查询景点时,需要覆盖:
  30. - 目的城市热门景点;
  31. - 用户明确表达的兴趣。
  32. 7. 如果用户喜欢美食,需要额外查询当地特色餐饮。
  33. 8. 最多执行5次关键词搜索,避免无意义重复调用。
  34. 候选数量与多样性:
  35. - 景点保留6至10个;
  36. - 餐饮地点保留4至6个;
  37. - 同一地点不能重复。
  38. - 景点必须覆盖不同类型,同类景点最多选1个
  39. (例如:动物园/熊猫类只选1个、寺庙/道观类只选1个、
  40. 古街/古镇类只选1个、自然山水类分散到不同山脉);
  41. - 餐饮必须优先选择不同品牌/餐厅,
  42. 同一连锁品牌只选1家分店,
  43. 避免同一品牌出现两次(如饕林餐厅各分店只选1家);
  44. - 优先覆盖目的城市不同行政区,
  45. 避免所有候选集中在同一区域。
  46. 天气规则:
  47. 1. 如果旅行日期超过天气工具可查询范围,
  48. 不得编造未来天气。
  49. 2. 此时weather返回空列表,并在notes说明
  50. "旅行日期超过当前天气预报范围"。
  51. 3. 天气数据必须来自maps_weather。
  52. match_reason只能描述该地点与用户兴趣的匹配关系,
  53. 不能声称"距离酒店近"或"交通最方便",
  54. 因为路线尚未计算。
  55. 完成工具查询后,请将所有结果整理为以下JSON格式,
  56. 放在最终消息的最末尾,用```json代码块包裹:
  57. ```json
  58. {
  59. "city": "城市名称",
  60. "attractions": [
  61. {
  62. "poi_id": "高德POI ID或null",
  63. "name": "景点名称",
  64. "category": "类别",
  65. "address": "地址",
  66. "district": "区",
  67. "location": "经度,纬度",
  68. "telephone": "电话或null",
  69. "source_query": "使用的搜索关键词",
  70. "match_reason": "匹配用户兴趣的原因"
  71. }
  72. ],
  73. "restaurants": [
  74. {
  75. "poi_id": "高德POI ID或null",
  76. "name": "餐厅名称",
  77. "category": "类别",
  78. "address": "地址",
  79. "district": "区",
  80. "location": "经度,纬度",
  81. "telephone": "电话或null",
  82. "source_query": "使用的搜索关键词",
  83. "match_reason": "匹配用户兴趣的原因"
  84. }
  85. ],
  86. "weather": [
  87. {
  88. "date": "日期 YYYY-MM-DD",
  89. "week": "星期几",
  90. "day_weather": "白天天气",
  91. "night_weather": "夜间天气",
  92. "day_temperature": "白天温度",
  93. "night_temperature": "夜间温度",
  94. "day_wind_direction": "白天风向",
  95. "night_wind_direction": "夜间风向"
  96. }
  97. ],
  98. "notes": ["备注1", "备注2"]
  99. }
  100. ```
  101. """
  102. class MapResearchAgent:
  103. """查询景点、餐饮和天气的地图Agent。"""
  104. def __init__(
  105. self,
  106. tools: list[BaseTool],
  107. model: BaseChatModel | None = None,
  108. ) -> None:
  109. """初始化地图研究Agent,校验必需的高德工具是否已加载。"""
  110. allowed_names = {
  111. "maps_text_search",
  112. "maps_weather",
  113. "maps_search_detail",
  114. }
  115. selected_tools = [
  116. tool
  117. for tool in tools
  118. if tool.name in allowed_names
  119. ]
  120. loaded_names = {
  121. tool.name
  122. for tool in selected_tools
  123. }
  124. required_names = {
  125. "maps_text_search",
  126. "maps_weather",
  127. }
  128. missing_names = required_names - loaded_names
  129. if missing_names:
  130. raise RuntimeError(
  131. "地图Agent缺少高德工具:"
  132. + "、".join(sorted(missing_names))
  133. )
  134. self._agent = create_agent(
  135. model=model or get_chat_model(timeout=180.0),
  136. tools=selected_tools,
  137. system_prompt=MAP_AGENT_PROMPT,
  138. )
  139. async def research(
  140. self,
  141. request: TravelRequest,
  142. ) -> dict[str, Any]:
  143. """执行目的地地图研究。"""
  144. request_json = request.model_dump_json(
  145. indent=2
  146. )
  147. user_message = f"""
  148. 请根据下面的结构化旅行需求,
  149. 查询真实景点、餐饮地点和天气:
  150. {request_json}
  151. 必须实际调用高德地图工具。
  152. 查询结束后,输出 JSON 结构化结果。
  153. """
  154. return await self._agent.ainvoke(
  155. {
  156. "messages": [
  157. {
  158. "role": "user",
  159. "content": user_message,
  160. }
  161. ]
  162. }
  163. )