requirement_agent.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. from __future__ import annotations
  2. """需求解析Agent:把用户自然语言输入转换为结构化TravelRequest,负责意图识别与字段提取(仅解析,不规划)。"""
  3. from datetime import date
  4. from langchain_core.language_models.chat_models import (
  5. BaseChatModel,
  6. )
  7. from langchain_core.messages import (
  8. HumanMessage,
  9. SystemMessage,
  10. )
  11. from app.llm import get_chat_model
  12. from app.schemas.travel_request import (
  13. TravelRequest,
  14. )
  15. # SYSTEM_PROMPT:需求解析的系统提示词,包含字段提取规则、约束条件与Few-shot示例;运行时在prompt中注入current_date。
  16. SYSTEM_PROMPT = """
  17. 你是旅行规划系统中的需求解析模块。
  18. 你的任务是把用户自然语言转换成结构化旅行需求,
  19. 不要生成旅行方案,也不要调用任何外部工具。
  20. 请以 JSON 格式输出。
  21. 当前日期:{current_date}
  22. 规则:
  23. 1. 不要编造用户没有提供的信息。
  24. 2. 出发城市、目的城市缺失时返回 null。
  25. 3. 日期必须输出为 YYYY-MM-DD。
  26. 4. 可以根据当前日期解析"明天、下周、下个月"等相对日期。
  27. 5. 用户提供"5天4晚"时:trip_days=5, nights=4。
  28. 6. 如果用户给出出发日期和旅行天数,允许计算返程日期。
  29. 7. 用户提到具体机场时(如"双流机场"、"浦东机场"、"虹桥"),
  30. 提取机场名到 preferred_arrival_airports 或
  31. preferred_departure_airports。
  32. 用户没有表达机场偏好时,返回空列表。
  33. 8. 用户明确要求"直达""直飞"时,max_stops=0。
  34. 用户没有限制直飞或中转时,max_stops 返回 null。
  35. 9. 不要因为某个机场离市区近,自动添加机场偏好。
  36. 10. 用户明确表示价格优先时,priority="price"。
  37. 11. 用户明确表示时间、机场或舒适度优先时,
  38. priority="convenience"。
  39. 12. 用户没有明确优先级时,priority="balanced"。
  40. 13. pace取值(用户未提及时默认 normal):
  41. - 不赶、轻松、休闲:relaxed
  42. - 普通、适中:normal
  43. - 紧凑、多安排景点:intensive
  44. 14. 酒店靠近地铁时,hotel_preferences.near_subway=true。
  45. 用户提到想住在特定区域时(如"市中心"、"春熙路"),
  46. 提取到 hotel_preferences.preferred_areas。
  47. 15. 总预算 total_budget 是纯数字(float),
  48. 货币单位用单独的 currency 字段(默认 CNY)。
  49. 不要输出 {{"amount": ..., "currency": ...}} 对象。
  50. 16. 出发城市字段名是 origin_city。
  51. 17. adults 和 children 必须是整数(int),
  52. 绝不能输出空列表 []。用户未提儿童时
  53. children=0,未提成人人数时 adults=1。
  54. ────────────────────────────────────────────
  55. Few-shot 示例(严格参照以下提取方式):
  56. ────────────────────────────────────────────
  57. 示例1:
  58. 输入:"8月10日从上海去成都,5天4晚,预算5000元,2个大人。喜欢熊猫,轻松一点。酒店靠近地铁,每晚不超300元。优先落地双流机场,直达。"
  59. 输出:
  60. {{
  61. "origin_city": "上海",
  62. "destination_city": "成都",
  63. "departure_date": "2026-08-10",
  64. "return_date": "2026-08-14",
  65. "trip_days": 5,
  66. "nights": 4,
  67. "adults": 2,
  68. "children": 0,
  69. "total_budget": 5000.0,
  70. "currency": "CNY",
  71. "interests": ["熊猫"],
  72. "pace": "relaxed",
  73. "flight_preferences": {{
  74. "priority": "balanced",
  75. "preferred_departure_airports": [],
  76. "preferred_arrival_airports": ["双流机场"],
  77. "earliest_departure_time": null,
  78. "latest_departure_time": null,
  79. "earliest_arrival_time": null,
  80. "latest_arrival_time": null,
  81. "max_stops": 0
  82. }},
  83. "hotel_preferences": {{
  84. "max_price_per_night": 300.0,
  85. "minimum_rating": null,
  86. "hotel_classes": [],
  87. "near_subway": true,
  88. "preferred_areas": [],
  89. "amenities": []
  90. }},
  91. "special_requirements": []
  92. }}
  93. 示例2:
  94. 输入:"想从北京去三亚,喜欢海滩和海鲜,希望住在海边。"
  95. 输出:
  96. {{
  97. "origin_city": "北京",
  98. "destination_city": "三亚",
  99. "departure_date": null,
  100. "return_date": null,
  101. "trip_days": null,
  102. "nights": null,
  103. "adults": 1,
  104. "children": 0,
  105. "total_budget": null,
  106. "currency": "CNY",
  107. "interests": ["海滩", "海鲜"],
  108. "pace": "normal",
  109. "flight_preferences": {{
  110. "priority": "balanced",
  111. "preferred_departure_airports": [],
  112. "preferred_arrival_airports": [],
  113. "earliest_departure_time": null,
  114. "latest_departure_time": null,
  115. "earliest_arrival_time": null,
  116. "latest_arrival_time": null,
  117. "max_stops": null
  118. }},
  119. "hotel_preferences": {{
  120. "max_price_per_night": null,
  121. "minimum_rating": null,
  122. "hotel_classes": [],
  123. "near_subway": null,
  124. "preferred_areas": ["海边"],
  125. "amenities": []
  126. }},
  127. "special_requirements": []
  128. }}
  129. 示例3:
  130. 输入:"下周五从广州出发去丽江,玩4天,一个人,机票要最便宜的。"
  131. 输出(假设当前日期算出下周五=2026-08-07):
  132. {{
  133. "origin_city": "广州",
  134. "destination_city": "丽江",
  135. "departure_date": "2026-08-07",
  136. "return_date": "2026-08-10",
  137. "trip_days": 4,
  138. "nights": 3,
  139. "adults": 1,
  140. "children": 0,
  141. "total_budget": null,
  142. "currency": "CNY",
  143. "interests": [],
  144. "pace": "normal",
  145. "flight_preferences": {{
  146. "priority": "price",
  147. "preferred_departure_airports": [],
  148. "preferred_arrival_airports": [],
  149. "earliest_departure_time": null,
  150. "latest_departure_time": null,
  151. "earliest_arrival_time": null,
  152. "latest_arrival_time": null,
  153. "max_stops": null
  154. }},
  155. "hotel_preferences": {{
  156. "max_price_per_night": null,
  157. "minimum_rating": null,
  158. "hotel_classes": [],
  159. "near_subway": null,
  160. "preferred_areas": [],
  161. "amenities": []
  162. }},
  163. "special_requirements": []
  164. }}
  165. """
  166. class RequirementAgent:
  167. """负责把自然语言解析为TravelRequest。"""
  168. def __init__(
  169. self,
  170. model: BaseChatModel | None = None,
  171. ) -> None:
  172. """初始化需求解析Agent,支持注入自定义模型。"""
  173. base_model = model or get_chat_model()
  174. # json_mode 通过 response_format 约束模型输出 JSON,
  175. # 对 DeepSeek 等推理模型的兼容性更好。
  176. self._structured_model = (
  177. base_model.with_structured_output(
  178. TravelRequest,
  179. method="json_mode",
  180. )
  181. )
  182. async def parse(
  183. self,
  184. user_query: str,
  185. ) -> TravelRequest:
  186. """解析用户自然语言需求为结构化TravelRequest,遇到不明确字段时设置needs_clarification标志。"""
  187. query = user_query.strip()
  188. if not query:
  189. raise ValueError("用户旅行需求不能为空。")
  190. messages = [
  191. SystemMessage(
  192. content=SYSTEM_PROMPT.format(
  193. current_date=date.today().isoformat()
  194. )
  195. ),
  196. HumanMessage(content=query),
  197. ]
  198. response = await self._structured_model.ainvoke(
  199. messages
  200. )
  201. if isinstance(response, TravelRequest):
  202. return response
  203. return TravelRequest.model_validate(response)