{ "artifact": "t2l-h01-clarify-exit", "generated_at": "2026-09-19T01:58:51.965029+00:00", "date_local": "2026-09-19", "purpose": "执行批次 H 的 H-01「澄清出口 E1」(批次 H 的第一步;用户诉求:让客服「会问」而不是「一问就让转人工」)", "scope": [ "app/service/agent/implementations/customer_service.py(业务层:E5a 判定)", "tests/unit/service/test_customer_service_agent.py(新增 12 条单测)" ], "pre_existing_state": { "already_present": "E5a 的出口本体(`_exit_clarify` / `CLARIFY_TEMPLATE` / `MAX_CLARIFY_ROUNDS=2`)与 E5b/E5c 分级此前已在位", "real_gaps_found": [ "`needs_clarification` 全仓无消费方(分类器算出来、契约里有字段、没人读)", "没有任何「族」判定 ⇒ 无法区分「同族并列」与「跨族并列」", "缺主语(指代/过短且上文接不上)没有任何触发路径", "澄清触发只看分数带 ⇒ 同族并列也会被问「你要哪个」(伪问题)" ] }, "implementation": { "added_helpers": { "_intent_needs_clarification()": "消费意图分类的 `needs_clarification`(DoD ①)", "_family_of(hit)": "取 `family_id`(`D2.4` v1.4「同族 = 同一个父块」);取不到返回空串", "_missing_subject(request)": "判据与 `_search_query()` 同源(过短 or `_REFERRING_WORDS`),结论相反:上文接不上才为真", "_clarify_reason(request, hits, *, score, gap)": "返回四类理由码之一或 None(决策单点)" }, "four_triggers": { "cross_family_tie": "候选跨族且 top1 与次优咬得紧(gap < MIN_GAP)", "weak_cross_family": "分数不足(< MID_SCORE)且跨族、gap 够大", "missing_subject": "过短/指代词,且上文取不出主语", "low_intent_confidence": "意图分类 `needs_clarification=True`" }, "same_family_guard": "`family_id` 唯一且非空 + gap < MIN_GAP ⇒ 返回 None(DoD ⑥:同族并列该合并作答,不该问「你要哪个」)", "family_label_missing": "族标缺失**不当作同族**(按跨族处理)——宁可多问一句,也不把两个族的答案混着答", "wiring": "`_answer_from_knowledge` 的「不确定带」改为先问 `_clarify_reason`;理由码入 logger", "entry_guard": "澄清仅在**检索本身也不确定**时可达(分数够高或领先够多时直接作答)——不让「会问」把答得出的问题变成问一句" }, "dod_mapping": { "① needs_clarification 被消费": "`_intent_needs_clarification()`;单测 `test_classifier_needs_clarification_is_consumed`", "② 四类触发条件": "`_clarify_reason` 四个理由码;四条单测各覆盖一个", "③ 一次只问一个问题、2—3 候选": "`CLARIFY_TEMPLATE` 单问句 + `CLARIFY_LIMIT=3`(口径此前已在位,本轮回归覆盖)", "④ 候选只来自当前可见档位": "候选即工具返回的 hits,已按 `visibility` 档位裁剪;澄清模板注释写明", "⑤ 同话题上限 2 轮,超限降 E5b": "`MAX_CLARIFY_ROUNDS=2` + 轮次取自 `request.metadata.clarification_round`;单测 `test_clarification_round_cap_falls_back_to_partial_not_transfer` 验证**不建单**", "⑥ 同族并列不澄清": "`known_single_family and gap < MIN_GAP ⇒ None`;单测 `test_same_family_tie_does_not_clarify` + 出口级 `test_knowledge_exit_does_not_clarify_on_same_family_tie`" }, "tests_added": { "file": "tests/unit/service/test_customer_service_agent.py", "count": 12, "names": [ "test_cross_family_tie_triggers_clarification", "test_same_family_tie_does_not_clarify", "test_weak_score_across_families_triggers_clarification", "test_missing_subject_triggers_clarification_when_history_cannot_resolve_it", "test_missing_subject_is_not_clarified_when_history_resolves_it", "test_classifier_needs_clarification_is_consumed", "test_no_candidate_above_the_clarify_floor_means_no_clarification", "test_missing_family_label_is_treated_as_cross_family", "test_knowledge_exit_clarifies_on_cross_family_tie", "test_knowledge_exit_does_not_clarify_on_same_family_tie", "test_knowledge_exit_clarifies_when_subject_is_missing", "test_clarification_round_cap_falls_back_to_partial_not_transfer" ] }, "gates": { "targeted_pytest": { "file": "tests/unit/service/test_customer_service_agent.py", "passed": 32, "failed": 0 }, "full_pytest": { "result": "2 failed / 1591 passed / 2 skipped", "note": "2 failed = T0 基线同两项;passed 1579 → 1591(+12 = 本轮新单测)" }, "ruff": { "scope": "app tests tools", "errors": 22, "note": "= 基线" }, "mypy": { "scope": "app", "errors": 3, "note": "= 基线;中途曾因局部变量 `reason` 与「检索降级」分支同名报 str|None 赋值错误,已改名 `clarify_reason` 修掉" } }, "honest_notes": [ "同族并列**本期走 E5b**(部分答/引导),不是合并作答——合并作答要等 H-03(E4 证据约束生成),DoD ⑥ 只要求「不澄清」", "跨族 + 缺主语会先命中 `cross_family_tie` / `weak_cross_family` 理由码(行为同样是澄清),理由码不同但**结果一致**", "`needs_clarification` 只在检索也不确定时生效;检索有把握时仍然直接作答——「会问」不得以牺牲「答得准」为代价" ], "open_follow_ups": [ "H-03(E4):同族并列的**合并作答**,把 E5b 替换成证据约束生成", "H-04(E5 + 白名单收口):转人工 4 类白名单与 E5c 上下文摘要", "H-06:46 条金标评测(`D3.7`)" ] }