from __future__ import annotations """行程评审Agent:从风险控制角度独立评审已规划行程,识别节奏、地理、预算等维度的问题并给出修改意见。""" import json from typing import Any 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.itinerary import ItineraryPlan from app.schemas.place import MapResearchResult from app.schemas.review import TripReviewResult from app.schemas.route import RouteEvaluationResult from app.schemas.travel_request import TravelRequest from app.schemas.validation import ( PlanValidationResult, ) # REVIEWER_PROMPT:行程评审的系统提示词,定义多维度评审标准与JSON输出结构。 REVIEWER_PROMPT = """ 你是多角色旅行规划系统中的独立Trip Reviewer。 你的职责是审查Itinerary Planner生成的旅行方案。 你不负责重新规划,也不负责查询新数据。 上游已经完成: 1. Requirement Analyst解析用户需求; 2. Resource Agent查询真实航班和酒店; 3. Destination Agent查询景点、餐厅和天气; 4. Route Evaluator评估酒店位置; 5. Itinerary Planner生成行程; 6. Plan Validator完成确定性校验。 审查原则: 一、尊重确定性校验 1. Plan Validator发现的error属于阻塞问题。 2. 不能否定或忽略Validator的确定性结果。 3. Validator只有warning时,可以根据实际影响判断 是否需要阻塞方案。 4. 不要重新计算价格、日期或ID真实性。 二、用户需求匹配 检查方案是否符合: 1. 用户兴趣; 2. 用户旅行节奏; 3. 总预算要求; 4. 酒店偏好; 5. 航班偏好; 6. 特殊要求。 三、行程体验 检查: 1. 到达日是否安排过满; 2. 返程日是否预留足够时间; 3. 每天活动数量是否与pace一致; 4. 是否留有合理休息和用餐时间; 5. 每日主题是否连贯; 6. 是否存在明显来回折返; 7. 酒店选择是否有充分理由; 8. 航班选择是否有充分理由。 四、真实性 1. 不得提出输入候选之外的新景点、餐厅、 酒店或航班。 2. 不得编造营业时间、门票、天气、距离或交通时间。 3. 地理判断只能依据输入提供的路线数据。 4. 没有景点之间路线数据时, 不得断言两个景点一定很近或很远。 五、通过标准 下列问题可标记为blocking: 1. 与用户核心需求明显冲突; 2. Validator存在error; 3. 到达日或返程日安排明显不可执行; 4. relaxed行程仍然非常紧凑; 5. 方案遗漏旅行日期或核心住宿安排; 6. 选择理由与实际候选信息明显矛盾; 7. 大量活动缺乏真实数据依据。 下列问题通常只标记warning: 1. 个别活动说明不够详细; 2. 存在更好的表达方式; 3. 预算没有覆盖餐饮或门票; 4. 天气暂时无法查询; 5. 个别时间需要用户出发前确认。 六、输出要求 1. approved=true表示不存在blocking问题。 2. approved=false时,必须给出明确、 可执行的revision_feedback。 3. revision_feedback要告诉Planner具体修改什么, 不能只写”优化行程”。 4. 不通过时最多给出6条核心修改要求。 5. 通过后可以在recommendations中给出 非阻塞建议。 七、输出格式(必须严格遵守字段名) 你必须返回一个 JSON 对象,顶层字段如下: { “approved”: true或false, “summary”: “审查总结(必填,1-3句话概括审查结论)”, “strengths”: [“方案优点”], “issues”: [ { “category”: “requirement_fit|pace|geography|flight|hotel|budget|completeness|grounding|user_experience|other”, “severity”: “blocking或warning”, “message”: “问题描述”, “day_index”: 日期序号(可选), “activity_name”: “相关活动名称(可选)”, “evidence”: “证据(可选)”, “suggestion”: “修改建议(可选)” } ], “recommendations”: [“非阻塞建议”], “revision_feedback”: [“不通过时的具体修改指令”] } 注意: - 不要在最外层包裹 {“tripReviewResult”: {...}},直接输出上述 JSON。 - 如果 Validator 有 error,必须 approved=false 并在 revision_feedback 中引用这些 error。 """ def build_reviewer_context( *, request: TravelRequest, plan: ItineraryPlan, validation: PlanValidationResult, map_result: MapResearchResult, route_result: RouteEvaluationResult, ) -> dict[str, Any]: """压缩Reviewer需要的上下文。""" selected_hotel_route = next( ( hotel for hotel in route_result.evaluated_hotels if hotel.hotel_id == plan.selected_hotel.hotel_id ), None, ) route_context: dict[str, Any] | None = None if selected_hotel_route is not None: route_context = { "hotel_id": ( selected_hotel_route.hotel_id ), "hotel_name": ( selected_hotel_route.hotel_name ), "final_score": ( selected_hotel_route.final_score ), "average_distance_meters": ( selected_hotel_route .average_distance_meters ), "average_duration_seconds": ( selected_hotel_route .average_duration_seconds ), "nearest_subway_distance_meters": ( selected_hotel_route .nearest_subway_distance_meters ), "reasons": ( selected_hotel_route.reasons ), "warnings": ( selected_hotel_route.warnings ), "route_legs": [ { "destination_name": ( leg.destination_name ), "distance_meters": ( leg.distance_meters ), "duration_seconds": ( leg.duration_seconds ), "source": leg.source, } for leg in selected_hotel_route.route_legs ], } attraction_catalog = [ { "poi_id": place.poi_id, "name": place.name, "category": place.category, "address": place.address, "location": place.location, "match_reason": place.match_reason, } for place in map_result.attractions ] restaurant_catalog = [ { "poi_id": place.poi_id, "name": place.name, "category": place.category, "address": place.address, "location": place.location, } for place in map_result.restaurants ] return { "travel_request": request.model_dump( mode="json" ), "itinerary_plan": plan.model_dump( mode="json" ), "deterministic_validation": ( validation.model_dump(mode="json") ), "selected_hotel_route_evaluation": ( route_context ), "available_attractions": ( attraction_catalog ), "available_restaurants": ( restaurant_catalog ), "weather": [ item.model_dump(mode="json") for item in map_result.weather ], "map_notes": map_result.notes, "route_warnings": route_result.warnings, } class TripReviewerAgent: """独立审查Planner生成的行程。""" def __init__( self, model: BaseChatModel | None = None, ) -> None: """初始化行程评审Agent,支持注入自定义模型。""" base_model = model or get_chat_model( timeout=180.0 ) # json_mode 对 DeepSeek 推理模型兼容性更好。 self._structured_model = ( base_model.with_structured_output( TripReviewResult, method="json_mode", ) ) async def review( self, *, request: TravelRequest, plan: ItineraryPlan, validation: PlanValidationResult, map_result: MapResearchResult, route_result: RouteEvaluationResult, ) -> dict[str, Any]: """对行程方案做独立评审,返回包含阻塞问题、警告与修改意见的结构化评审结果。""" context = build_reviewer_context( request=request, plan=plan, validation=validation, map_result=map_result, route_result=route_result, ) context_json = json.dumps( context, ensure_ascii=False, indent=2, ) user_message = f""" 请独立审查下面的旅行方案和上游数据: {context_json} 重点关注用户体验、偏好匹配、节奏、 安排合理性和确定性校验结果。 必须以 JSON 格式返回 TripReviewResult 结构化结果。 不要输出结构化结果之外的解释。 """ response = await self._structured_model.ainvoke( [ SystemMessage( content=REVIEWER_PROMPT ), HumanMessage( content=user_message ), ] ) if isinstance(response, TripReviewResult): structured = response else: if ( isinstance(response, dict) and "tripReviewResult" in response and len(response) == 1 ): response = response["tripReviewResult"] structured = TripReviewResult.model_validate( response ) return { "structured_response": structured, "messages": [], }