Polish project documentation and runtime config

This commit is contained in:
2026-06-09 10:22:59 +08:00
parent 5f061295d8
commit 0594fc9f8c
43 changed files with 1001 additions and 97 deletions

View File

@@ -616,6 +616,7 @@ async def _llm_intent(question: str, fallback: dict[str, Any], enabled: bool) ->
TASK_LABELS = {
"route_price": "线路报价",
"route_catalog": "线路清单",
"route_match": "线路匹配",
"nearby_resource": "景区附近酒店/餐饮",
@@ -637,6 +638,19 @@ def _route_task_needed(question: str, intent: dict[str, Any]) -> bool:
return bool(intent.get("duration_days") or intent.get("destinations")) and _has_any(question, route_terms)
def _route_price_question(question: str) -> bool:
price_terms = ("多少钱", "价格", "报价", "费用", "线路多少钱", "路线多少钱", "产品多少钱", "怎么收费")
route_terms = (
"路线", "线路", "产品", "行程", "旅游", "", "几日游", "一日游", "二日游", "三日游", "四日游",
"五日游", "六日游", "黄小西", "小西", "镇梵",
)
hotel_terms = ("酒店", "住宿", "房型", "房价", "房费", "间夜")
vehicle_terms = ("车辆", "车型", "用车", "车费", "车价")
if _has_any(question, hotel_terms) or _has_any(question, vehicle_terms):
return False
return _has_any(question, price_terms) and _has_any(question, route_terms)
def _resource_task_needed(question: str) -> bool:
resource_terms = ("酒店", "住宿", "住哪", "客栈", "民宿", "餐饮", "餐厅", "吃饭", "饭店", "美食")
near_or_choice_terms = ("附近", "周边", "可选", "选择", "推荐", "哪些", "那些", "有哪些", "有什么", "", "")
@@ -668,7 +682,9 @@ def _agent_task_plan(question: str, intent: dict[str, Any]) -> list[dict[str, An
"reason": reason,
})
if _route_catalog_question(question):
if _route_price_question(question):
add("route_price", 0.95, "命中线路产品报价问题")
elif _route_catalog_question(question):
add("route_catalog", 0.96, "命中线路清单问法")
elif _route_task_needed(question, intent):
add("route_match", 0.92, "命中出行天数/景区/推荐行程约束")
@@ -730,6 +746,8 @@ def _rule_fast_intent_method(question: str, intent: dict[str, Any]) -> str:
tasks = _agent_task_plan(question, intent)
if len(tasks) > 1:
return "rule_multi_task_fast_path"
if _route_price_question(question):
return "rule_route_price_fast_path"
if _vehicle_only_question(question, intent):
return "rule_vehicle_fast_path"
if _route_catalog_question(question):
@@ -1777,6 +1795,7 @@ def _infer_response_mode(response: dict[str, Any]) -> str:
method = _value(trace.get("method"), 120)
for token, inferred in (
("multi_task", "multi_task_agent"),
("route_price", "route_price"),
("route_catalog", "route_catalog"),
("nearby_resource", "nearby_resource"),
("hotel_resource", "hotel_resource"),
@@ -1793,6 +1812,7 @@ def _infer_response_mode(response: dict[str, Any]) -> str:
def _response_mode_label(mode: str) -> str:
labels = {
"multi_task_agent": "多任务客服 Agent",
"route_price": "线路报价",
"route_catalog": "线路清单",
"route_match_fast": "固定线路快速匹配",
"fixed_route_item": "固定线路深度匹配",
@@ -1813,6 +1833,7 @@ def _agent_capabilities(response: dict[str, Any]) -> list[str]:
capabilities: list[str] = []
mode_caps = {
"multi_task_agent": ["多意图拆解", "KG 模板编排", "结构化聚合输出"],
"route_price": ["线路报价", "团期价格", "核价边界"],
"route_catalog": ["线路清单", "同线路版本合并", "后续追问入口"],
"route_match_fast": ["固定线路召回", "景点覆盖评分", "报价规则提示"],
"fixed_route_item": ["固定线路召回", "每日行程", "费用槽位", "资源槽位"],
@@ -2408,7 +2429,9 @@ def _attach_hotel_rate_summaries(graph_data: dict[str, Any], rate_index: dict[st
def _hotel_resource_question(question: str) -> bool:
return any(token in question for token in ("房型", "淡季", "旺季", "挂牌价", "", "", "酒店价格", "住宿价格", "多少钱"))
hotel_terms = ("酒店", "住宿", "客栈", "民宿", "", "", "房费", "间夜")
price_terms = ("淡季", "旺季", "挂牌价", "房价", "房费", "酒店价格", "住宿价格", "多少钱")
return any(token in question for token in price_terms) and any(token in question for token in hotel_terms)
def _match_hotel_rate_entries(question: str, rate_index: dict[str, dict[str, Any]]) -> list[dict[str, Any]]:
@@ -3213,7 +3236,16 @@ def _score_fixed_product(intent: dict[str, Any], entry: dict[str, Any]) -> tuple
text = _fixed_product_text(entry)
route_text = _fixed_route_core_text(entry)
score = 0
score_cap = 100
reasons: list[str] = []
raw_question = _value(intent.get("raw_text"), 500)
raw_norm = _norm_text(raw_question)
product_name = _value(product.get("name"), 160)
product_name_norm = _norm_text(product_name)
exact_product_name = bool(product_name_norm and len(product_name_norm) >= 8 and product_name_norm in raw_norm)
if exact_product_name:
score += 42
reasons.append("命中指定线路名称")
desired_days = _as_optional_int(intent.get("duration_days"))
desired_nights = _as_optional_int(intent.get("duration_nights"))
product_days, product_nights = _entry_duration_values(entry)
@@ -3231,15 +3263,20 @@ def _score_fixed_product(intent: dict[str, Any], entry: dict[str, Any]) -> tuple
else:
reasons.append(f"{desired_days}天固定路线匹配")
else:
score_cap = min(score_cap, 72)
reasons.append(f"时长不匹配:需求{_duration_label(desired_days, desired_nights)},产品{_duration_label(product_days, product_nights)}")
missing_required = False
for dest in intent.get("destinations") or []:
aliases = ATTRACTION_ALIASES.get(dest, [dest])
if _contains_any(route_text, aliases):
score += 35
score += 24
reasons.append(f"覆盖{dest}")
else:
score -= 28
missing_required = True
score -= 40
reasons.append(f"未覆盖{dest}")
if missing_required:
score_cap = min(score_cap, 45)
requested_destinations = set(intent.get("destinations") or [])
if requested_destinations and not intent.get("inferred_destinations"):
extra_destinations = [
@@ -3248,11 +3285,25 @@ def _score_fixed_product(intent: dict[str, Any], entry: dict[str, Any]) -> tuple
if canonical not in requested_destinations and _contains_any(route_text, aliases)
]
if extra_destinations:
score -= min(24, 6 * len(extra_destinations))
if not exact_product_name:
score_cap = min(score_cap, max(68, 90 - 8 * len(extra_destinations)))
score -= min(36, 12 * len(extra_destinations))
reasons.append(f"含额外景点{''.join(extra_destinations[:3])},需确认是否接受")
if product.get("base_price_status") == "ready_for_reference_quote":
has_price_reference = bool(
product.get("base_price_status") == "ready_for_reference_quote"
or _value(product.get("base_price_text"), 80)
or _value(product.get("adult_settlement_text"), 80)
or _value(product.get("child_settlement_text"), 80)
or _value(product.get("free_ticket_settlement_text"), 80)
or _value(product.get("single_room_diff_text"), 80)
or _value(product.get("quote_formula"), 80)
)
if has_price_reference:
score += 8
reasons.append("已有报价表依据")
elif intent.get("price_query"):
score_cap = min(score_cap, 70)
reasons.append("线路报价数据待补")
if _is_low_budget(intent):
if any(term in text for term in ("经济", "性价比", "普通", "四钻", "4钻")):
score += 5
@@ -3272,7 +3323,7 @@ def _score_fixed_product(intent: dict[str, Any], entry: dict[str, Any]) -> tuple
score += 2
if not reasons:
reasons.append("按固定路线、景点覆盖和资源槽位综合匹配")
return score, reasons[:7]
return min(score, score_cap), reasons[:7]
def _format_price_reference(product: dict[str, Any], intent: dict[str, Any]) -> str:
@@ -4210,6 +4261,7 @@ def _fixed_route_match_fast_response(
intent_method: str,
graph_data: dict[str, Any],
) -> dict[str, Any]:
price_query = bool(intent.get("price_query") or _route_price_question(question))
entries = list(graph_data.get("products", {}).values())
strict_matches = _strict_fixed_route_matches(intent, entries)
strict_note = _strict_route_gap_note(intent, entries)
@@ -4236,11 +4288,22 @@ def _fixed_route_match_fast_response(
)
ranked = _dedupe_route_entries(ranked)
plans = [_build_fixed_route_plan(entry, idx + 1, intent) for idx, entry in enumerate(ranked[:8])]
followups = [
"要不要继续查某个景区附近可选酒店/餐饮?",
"要不要继续查这条线路的可选车辆?",
"要不要继续查某个景区的门票/观光车/保险等费用?",
]
if price_query:
for plan in plans:
plan["price_query"] = True
if plan.get("rank") == 1:
plan["label"] = "线路报价"
followups = [
"请确认出行日期属于哪个价格区间。",
"请确认成人/儿童人数、酒店档位和是否有单房差。",
"请确认是否接受线路中额外包含的景点,或需要继续找更贴合的线路。",
]
else:
followups = [
"要不要继续查某个景区附近可选酒店/餐饮?",
"要不要继续查这条线路的可选车辆?",
"要不要继续查某个景区的门票/观光车/保险等费用?",
]
evidence: list[dict[str, Any]] = []
for plan in plans:
evidence.append({"type": "固定线路产品", "name": plan["plan_name"], "summary": plan["route_summary"], "source": plan["source"]})
@@ -4258,15 +4321,20 @@ def _fixed_route_match_fast_response(
"strict_match_count": len(strict_matches),
"strict_match_note": strict_note,
"resource_counts": {"TourProduct": len(entries)},
"method": "fixed_route_item_route_match_fast_v1",
"method": "fixed_route_item_route_price_lookup_v1" if price_query else "fixed_route_item_route_match_fast_v1",
"intent_method": intent_method,
"response_mode": "route_match_fast",
"response_mode": "route_price" if price_query else "route_match_fast",
"price_query": price_query,
},
}
def _copy_fixed_text(plans: list[dict[str, Any]], followups: list[str], strict_note: str = "") -> str:
lines = ["您好,按您当前需求,先从已有固定线路产品里匹配如下:"]
is_price_query = any(plan.get("price_query") for plan in plans)
if is_price_query:
lines = ["您好,按您当前需求,先从已有固定线路产品里匹配并核对报价如下:"]
else:
lines = ["您好,按您当前需求,先从已有固定线路产品里匹配如下:"]
if strict_note:
lines.append(f"注意:{strict_note} 以下为相近替代方案,不要直接承诺完全满足客户天数/景点。")
for plan in plans[:8]:
@@ -4276,7 +4344,8 @@ def _copy_fixed_text(plans: list[dict[str, Any]], followups: list[str], strict_n
lines.append(f"{plan['rank']}. {line_prefix}{plan['plan_name']}{_duration_label(plan.get('duration_days'), plan.get('duration_nights'))}")
lines.append(f"匹配点:{''.join(plan['match_reasons'][:4])}")
lines.append(f"路线:{plan['route_summary']}")
lines.append(f"报价依据:{plan['quote_summary']}")
quote_label = "线路报价" if is_price_query else "报价依据"
lines.append(f"{quote_label}{plan['quote_summary']}")
vehicle = next((item["detail"] for item in plan.get("cost_breakdown", []) if item["category"] == "小包团用车"), "")
if vehicle:
lines.append(f"用车建议:{vehicle}")
@@ -4325,7 +4394,7 @@ def _fixed_route_item_task_response(
if kind == "route_catalog":
catalog_data = shared.setdefault("catalog_data", _cached_fixed_route_catalog_graph(graph_name))
return _fixed_route_catalog_response(question, graph_name, intent, intent_method, catalog_data, limit=5)
if kind == "route_match":
if kind in {"route_match", "route_price"}:
catalog_data = shared.setdefault("catalog_data", _cached_fixed_route_catalog_graph(graph_name))
return _fixed_route_match_fast_response(question, graph_name, intent, intent_method, catalog_data)
if kind == "nearby_resource":
@@ -4419,6 +4488,9 @@ def _fixed_route_item_response(question: str, graph_name: str, intent: dict[str,
if responses:
return _merge_fixed_task_responses(question, graph_name, intent, intent_method, executed_tasks, responses)
if _route_price_question(question):
catalog_data = _cached_fixed_route_catalog_graph(graph_name)
return _fixed_route_match_fast_response(question, graph_name, intent, intent_method, catalog_data)
if _route_catalog_question(question):
catalog_data = _cached_fixed_route_catalog_graph(graph_name)
return _fixed_route_catalog_response(question, graph_name, intent, intent_method, catalog_data)
@@ -4869,6 +4941,7 @@ async def travel_assistant_query(
if intent_method == "llm_intent_parser":
intent = _guard_llm_intent(question, rule_intent, intent)
intent = _complete_intent_defaults(question, intent)
intent["price_query"] = _route_price_question(question)
planned_tasks = _agent_task_plan(question, intent)
intent_confidence = _rule_agent_confidence(question, intent, planned_tasks)
intent["planned_tasks"] = planned_tasks