| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- 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": [],
- }
|