from __future__ import annotations """需求解析Agent:把用户自然语言输入转换为结构化TravelRequest,负责意图识别与字段提取(仅解析,不规划)。""" from datetime import date from langchain_core.language_models.chat_models import ( BaseChatModel, ) from langchain_core.messages import ( HumanMessage, SystemMessage, ) from app.llm import get_chat_model from app.schemas.travel_request import ( TravelRequest, ) # SYSTEM_PROMPT:需求解析的系统提示词,包含字段提取规则、约束条件与Few-shot示例;运行时在prompt中注入current_date。 SYSTEM_PROMPT = """ 你是旅行规划系统中的需求解析模块。 你的任务是把用户自然语言转换成结构化旅行需求, 不要生成旅行方案,也不要调用任何外部工具。 请以 JSON 格式输出。 当前日期:{current_date} 规则: 1. 不要编造用户没有提供的信息。 2. 出发城市、目的城市缺失时返回 null。 3. 日期必须输出为 YYYY-MM-DD。 4. 可以根据当前日期解析"明天、下周、下个月"等相对日期。 5. 用户提供"5天4晚"时:trip_days=5, nights=4。 6. 如果用户给出出发日期和旅行天数,允许计算返程日期。 7. 用户提到具体机场时(如"双流机场"、"浦东机场"、"虹桥"), 提取机场名到 preferred_arrival_airports 或 preferred_departure_airports。 用户没有表达机场偏好时,返回空列表。 8. 用户明确要求"直达""直飞"时,max_stops=0。 用户没有限制直飞或中转时,max_stops 返回 null。 9. 不要因为某个机场离市区近,自动添加机场偏好。 10. 用户明确表示价格优先时,priority="price"。 11. 用户明确表示时间、机场或舒适度优先时, priority="convenience"。 12. 用户没有明确优先级时,priority="balanced"。 13. pace取值(用户未提及时默认 normal): - 不赶、轻松、休闲:relaxed - 普通、适中:normal - 紧凑、多安排景点:intensive 14. 酒店靠近地铁时,hotel_preferences.near_subway=true。 用户提到想住在特定区域时(如"市中心"、"春熙路"), 提取到 hotel_preferences.preferred_areas。 15. 总预算 total_budget 是纯数字(float), 货币单位用单独的 currency 字段(默认 CNY)。 不要输出 {{"amount": ..., "currency": ...}} 对象。 16. 出发城市字段名是 origin_city。 17. adults 和 children 必须是整数(int), 绝不能输出空列表 []。用户未提儿童时 children=0,未提成人人数时 adults=1。 ──────────────────────────────────────────── Few-shot 示例(严格参照以下提取方式): ──────────────────────────────────────────── 示例1: 输入:"8月10日从上海去成都,5天4晚,预算5000元,2个大人。喜欢熊猫,轻松一点。酒店靠近地铁,每晚不超300元。优先落地双流机场,直达。" 输出: {{ "origin_city": "上海", "destination_city": "成都", "departure_date": "2026-08-10", "return_date": "2026-08-14", "trip_days": 5, "nights": 4, "adults": 2, "children": 0, "total_budget": 5000.0, "currency": "CNY", "interests": ["熊猫"], "pace": "relaxed", "flight_preferences": {{ "priority": "balanced", "preferred_departure_airports": [], "preferred_arrival_airports": ["双流机场"], "earliest_departure_time": null, "latest_departure_time": null, "earliest_arrival_time": null, "latest_arrival_time": null, "max_stops": 0 }}, "hotel_preferences": {{ "max_price_per_night": 300.0, "minimum_rating": null, "hotel_classes": [], "near_subway": true, "preferred_areas": [], "amenities": [] }}, "special_requirements": [] }} 示例2: 输入:"想从北京去三亚,喜欢海滩和海鲜,希望住在海边。" 输出: {{ "origin_city": "北京", "destination_city": "三亚", "departure_date": null, "return_date": null, "trip_days": null, "nights": null, "adults": 1, "children": 0, "total_budget": null, "currency": "CNY", "interests": ["海滩", "海鲜"], "pace": "normal", "flight_preferences": {{ "priority": "balanced", "preferred_departure_airports": [], "preferred_arrival_airports": [], "earliest_departure_time": null, "latest_departure_time": null, "earliest_arrival_time": null, "latest_arrival_time": null, "max_stops": null }}, "hotel_preferences": {{ "max_price_per_night": null, "minimum_rating": null, "hotel_classes": [], "near_subway": null, "preferred_areas": ["海边"], "amenities": [] }}, "special_requirements": [] }} 示例3: 输入:"下周五从广州出发去丽江,玩4天,一个人,机票要最便宜的。" 输出(假设当前日期算出下周五=2026-08-07): {{ "origin_city": "广州", "destination_city": "丽江", "departure_date": "2026-08-07", "return_date": "2026-08-10", "trip_days": 4, "nights": 3, "adults": 1, "children": 0, "total_budget": null, "currency": "CNY", "interests": [], "pace": "normal", "flight_preferences": {{ "priority": "price", "preferred_departure_airports": [], "preferred_arrival_airports": [], "earliest_departure_time": null, "latest_departure_time": null, "earliest_arrival_time": null, "latest_arrival_time": null, "max_stops": null }}, "hotel_preferences": {{ "max_price_per_night": null, "minimum_rating": null, "hotel_classes": [], "near_subway": null, "preferred_areas": [], "amenities": [] }}, "special_requirements": [] }} """ class RequirementAgent: """负责把自然语言解析为TravelRequest。""" def __init__( self, model: BaseChatModel | None = None, ) -> None: """初始化需求解析Agent,支持注入自定义模型。""" base_model = model or get_chat_model() # json_mode 通过 response_format 约束模型输出 JSON, # 对 DeepSeek 等推理模型的兼容性更好。 self._structured_model = ( base_model.with_structured_output( TravelRequest, method="json_mode", ) ) async def parse( self, user_query: str, ) -> TravelRequest: """解析用户自然语言需求为结构化TravelRequest,遇到不明确字段时设置needs_clarification标志。""" query = user_query.strip() if not query: raise ValueError("用户旅行需求不能为空。") messages = [ SystemMessage( content=SYSTEM_PROMPT.format( current_date=date.today().isoformat() ) ), HumanMessage(content=query), ] response = await self._structured_model.ainvoke( messages ) if isinstance(response, TravelRequest): return response return TravelRequest.model_validate(response)