reviewer_agent.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. from __future__ import annotations
  2. """行程评审Agent:从风险控制角度独立评审已规划行程,识别节奏、地理、预算等维度的问题并给出修改意见。"""
  3. import json
  4. from typing import Any
  5. from langchain_core.language_models.chat_models import (
  6. BaseChatModel,
  7. )
  8. from langchain_core.messages import (
  9. HumanMessage,
  10. SystemMessage,
  11. )
  12. from app.llm import get_chat_model
  13. from app.schemas.itinerary import ItineraryPlan
  14. from app.schemas.place import MapResearchResult
  15. from app.schemas.review import TripReviewResult
  16. from app.schemas.route import RouteEvaluationResult
  17. from app.schemas.travel_request import TravelRequest
  18. from app.schemas.validation import (
  19. PlanValidationResult,
  20. )
  21. # REVIEWER_PROMPT:行程评审的系统提示词,定义多维度评审标准与JSON输出结构。
  22. REVIEWER_PROMPT = """
  23. 你是多角色旅行规划系统中的独立Trip Reviewer。
  24. 你的职责是审查Itinerary Planner生成的旅行方案。
  25. 你不负责重新规划,也不负责查询新数据。
  26. 上游已经完成:
  27. 1. Requirement Analyst解析用户需求;
  28. 2. Resource Agent查询真实航班和酒店;
  29. 3. Destination Agent查询景点、餐厅和天气;
  30. 4. Route Evaluator评估酒店位置;
  31. 5. Itinerary Planner生成行程;
  32. 6. Plan Validator完成确定性校验。
  33. 审查原则:
  34. 一、尊重确定性校验
  35. 1. Plan Validator发现的error属于阻塞问题。
  36. 2. 不能否定或忽略Validator的确定性结果。
  37. 3. Validator只有warning时,可以根据实际影响判断
  38. 是否需要阻塞方案。
  39. 4. 不要重新计算价格、日期或ID真实性。
  40. 二、用户需求匹配
  41. 检查方案是否符合:
  42. 1. 用户兴趣;
  43. 2. 用户旅行节奏;
  44. 3. 总预算要求;
  45. 4. 酒店偏好;
  46. 5. 航班偏好;
  47. 6. 特殊要求。
  48. 三、行程体验
  49. 检查:
  50. 1. 到达日是否安排过满;
  51. 2. 返程日是否预留足够时间;
  52. 3. 每天活动数量是否与pace一致;
  53. 4. 是否留有合理休息和用餐时间;
  54. 5. 每日主题是否连贯;
  55. 6. 是否存在明显来回折返;
  56. 7. 酒店选择是否有充分理由;
  57. 8. 航班选择是否有充分理由。
  58. 四、真实性
  59. 1. 不得提出输入候选之外的新景点、餐厅、
  60. 酒店或航班。
  61. 2. 不得编造营业时间、门票、天气、距离或交通时间。
  62. 3. 地理判断只能依据输入提供的路线数据。
  63. 4. 没有景点之间路线数据时,
  64. 不得断言两个景点一定很近或很远。
  65. 五、通过标准
  66. 下列问题可标记为blocking:
  67. 1. 与用户核心需求明显冲突;
  68. 2. Validator存在error;
  69. 3. 到达日或返程日安排明显不可执行;
  70. 4. relaxed行程仍然非常紧凑;
  71. 5. 方案遗漏旅行日期或核心住宿安排;
  72. 6. 选择理由与实际候选信息明显矛盾;
  73. 7. 大量活动缺乏真实数据依据。
  74. 下列问题通常只标记warning:
  75. 1. 个别活动说明不够详细;
  76. 2. 存在更好的表达方式;
  77. 3. 预算没有覆盖餐饮或门票;
  78. 4. 天气暂时无法查询;
  79. 5. 个别时间需要用户出发前确认。
  80. 六、输出要求
  81. 1. approved=true表示不存在blocking问题。
  82. 2. approved=false时,必须给出明确、
  83. 可执行的revision_feedback。
  84. 3. revision_feedback要告诉Planner具体修改什么,
  85. 不能只写”优化行程”。
  86. 4. 不通过时最多给出6条核心修改要求。
  87. 5. 通过后可以在recommendations中给出
  88. 非阻塞建议。
  89. 七、输出格式(必须严格遵守字段名)
  90. 你必须返回一个 JSON 对象,顶层字段如下:
  91. {
  92. “approved”: true或false,
  93. “summary”: “审查总结(必填,1-3句话概括审查结论)”,
  94. “strengths”: [“方案优点”],
  95. “issues”: [
  96. {
  97. “category”: “requirement_fit|pace|geography|flight|hotel|budget|completeness|grounding|user_experience|other”,
  98. “severity”: “blocking或warning”,
  99. “message”: “问题描述”,
  100. “day_index”: 日期序号(可选),
  101. “activity_name”: “相关活动名称(可选)”,
  102. “evidence”: “证据(可选)”,
  103. “suggestion”: “修改建议(可选)”
  104. }
  105. ],
  106. “recommendations”: [“非阻塞建议”],
  107. “revision_feedback”: [“不通过时的具体修改指令”]
  108. }
  109. 注意:
  110. - 不要在最外层包裹 {“tripReviewResult”: {...}},直接输出上述 JSON。
  111. - 如果 Validator 有 error,必须 approved=false 并在 revision_feedback 中引用这些 error。
  112. """
  113. def build_reviewer_context(
  114. *,
  115. request: TravelRequest,
  116. plan: ItineraryPlan,
  117. validation: PlanValidationResult,
  118. map_result: MapResearchResult,
  119. route_result: RouteEvaluationResult,
  120. ) -> dict[str, Any]:
  121. """压缩Reviewer需要的上下文。"""
  122. selected_hotel_route = next(
  123. (
  124. hotel
  125. for hotel
  126. in route_result.evaluated_hotels
  127. if hotel.hotel_id
  128. == plan.selected_hotel.hotel_id
  129. ),
  130. None,
  131. )
  132. route_context: dict[str, Any] | None = None
  133. if selected_hotel_route is not None:
  134. route_context = {
  135. "hotel_id": (
  136. selected_hotel_route.hotel_id
  137. ),
  138. "hotel_name": (
  139. selected_hotel_route.hotel_name
  140. ),
  141. "final_score": (
  142. selected_hotel_route.final_score
  143. ),
  144. "average_distance_meters": (
  145. selected_hotel_route
  146. .average_distance_meters
  147. ),
  148. "average_duration_seconds": (
  149. selected_hotel_route
  150. .average_duration_seconds
  151. ),
  152. "nearest_subway_distance_meters": (
  153. selected_hotel_route
  154. .nearest_subway_distance_meters
  155. ),
  156. "reasons": (
  157. selected_hotel_route.reasons
  158. ),
  159. "warnings": (
  160. selected_hotel_route.warnings
  161. ),
  162. "route_legs": [
  163. {
  164. "destination_name": (
  165. leg.destination_name
  166. ),
  167. "distance_meters": (
  168. leg.distance_meters
  169. ),
  170. "duration_seconds": (
  171. leg.duration_seconds
  172. ),
  173. "source": leg.source,
  174. }
  175. for leg
  176. in selected_hotel_route.route_legs
  177. ],
  178. }
  179. attraction_catalog = [
  180. {
  181. "poi_id": place.poi_id,
  182. "name": place.name,
  183. "category": place.category,
  184. "address": place.address,
  185. "location": place.location,
  186. "match_reason": place.match_reason,
  187. }
  188. for place in map_result.attractions
  189. ]
  190. restaurant_catalog = [
  191. {
  192. "poi_id": place.poi_id,
  193. "name": place.name,
  194. "category": place.category,
  195. "address": place.address,
  196. "location": place.location,
  197. }
  198. for place in map_result.restaurants
  199. ]
  200. return {
  201. "travel_request": request.model_dump(
  202. mode="json"
  203. ),
  204. "itinerary_plan": plan.model_dump(
  205. mode="json"
  206. ),
  207. "deterministic_validation": (
  208. validation.model_dump(mode="json")
  209. ),
  210. "selected_hotel_route_evaluation": (
  211. route_context
  212. ),
  213. "available_attractions": (
  214. attraction_catalog
  215. ),
  216. "available_restaurants": (
  217. restaurant_catalog
  218. ),
  219. "weather": [
  220. item.model_dump(mode="json")
  221. for item in map_result.weather
  222. ],
  223. "map_notes": map_result.notes,
  224. "route_warnings": route_result.warnings,
  225. }
  226. class TripReviewerAgent:
  227. """独立审查Planner生成的行程。"""
  228. def __init__(
  229. self,
  230. model: BaseChatModel | None = None,
  231. ) -> None:
  232. """初始化行程评审Agent,支持注入自定义模型。"""
  233. base_model = model or get_chat_model(
  234. timeout=180.0
  235. )
  236. # json_mode 对 DeepSeek 推理模型兼容性更好。
  237. self._structured_model = (
  238. base_model.with_structured_output(
  239. TripReviewResult,
  240. method="json_mode",
  241. )
  242. )
  243. async def review(
  244. self,
  245. *,
  246. request: TravelRequest,
  247. plan: ItineraryPlan,
  248. validation: PlanValidationResult,
  249. map_result: MapResearchResult,
  250. route_result: RouteEvaluationResult,
  251. ) -> dict[str, Any]:
  252. """对行程方案做独立评审,返回包含阻塞问题、警告与修改意见的结构化评审结果。"""
  253. context = build_reviewer_context(
  254. request=request,
  255. plan=plan,
  256. validation=validation,
  257. map_result=map_result,
  258. route_result=route_result,
  259. )
  260. context_json = json.dumps(
  261. context,
  262. ensure_ascii=False,
  263. indent=2,
  264. )
  265. user_message = f"""
  266. 请独立审查下面的旅行方案和上游数据:
  267. {context_json}
  268. 重点关注用户体验、偏好匹配、节奏、
  269. 安排合理性和确定性校验结果。
  270. 必须以 JSON 格式返回 TripReviewResult 结构化结果。
  271. 不要输出结构化结果之外的解释。
  272. """
  273. response = await self._structured_model.ainvoke(
  274. [
  275. SystemMessage(
  276. content=REVIEWER_PROMPT
  277. ),
  278. HumanMessage(
  279. content=user_message
  280. ),
  281. ]
  282. )
  283. if isinstance(response, TripReviewResult):
  284. structured = response
  285. else:
  286. if (
  287. isinstance(response, dict)
  288. and "tripReviewResult" in response
  289. and len(response) == 1
  290. ):
  291. response = response["tripReviewResult"]
  292. structured = TripReviewResult.model_validate(
  293. response
  294. )
  295. return {
  296. "structured_response": structured,
  297. "messages": [],
  298. }