| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227 |
- 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)
|