diff --git a/tests/unit/service/test_customer_service_search_query.py b/tests/unit/service/test_customer_service_search_query.py index aca9035..cf63daf 100644 --- a/tests/unit/service/test_customer_service_search_query.py +++ b/tests/unit/service/test_customer_service_search_query.py @@ -1,14 +1,22 @@ -"""客服检索问句构造的单元测试:什么时候该带上文,什么时候绝不能带。 +"""客服检索问句构造的单元测试:什么时候带上文、带什么、绝不带什么。 -背景是实测出来的:**无条件**拼接上文会让客户换话题时答非所问——同一会话先问 -「季季盈90天的起投金额是多少」,再问「基金赎回几天到账」,第二问的检索词变成两句话的 -混合,结果命中季季盈的产品块、答出产品介绍,而客户问的是赎回到账时间。 +背景全部来自实测,不是设想: + +1. **不能无条件带上文**。同一会话先问「季季盈90天的起投金额是多少」、再问 + 「基金赎回几天到账」,无条件拼接会让第二问命中季季盈的产品块、答出产品介绍。 +2. **带上文时不能带上一轮的原话**。把整句拼进来会让检索词语义变"宽",反而只命中 + 粗粒度的整节块:实测「季季盈90天起投多少 那它风险高吗」拿到的是整个产品小节, + 客户问的"风险"完全没被聚焦;换成「南方季季盈90天 那它风险高吗」才命中"风险等级"那一行。 + 上文的作用是**补主语**,不是**补内容**。 """ from app.core.contracts import AgentRequest, ConversationTurn from app.service.agent.implementations.customer_service import CustomerServiceAgent PREVIOUS = "季季盈90天的起投金额是多少" +ANSWER = "南方季季盈90天:起投金额 1万元" +TOPIC = "南方季季盈90天" +FALLBACK = "抱歉,这个问题我暂时无法给出准确答复。建议您拨打客服热线 400-XXX-XXXX 转人工客服咨询。" def _request(message: str, *history: str) -> AgentRequest: @@ -25,26 +33,59 @@ def _request(message: str, *history: str) -> AgentRequest: ) +def test_topic_extracted_from_row_block() -> None: + """行级块的回答:首个冒号前就是产品名。""" + assert CustomerServiceAgent._topic_of(ANSWER) == TOPIC + + +def test_topic_extracted_from_section_block() -> None: + """整节块的回答:首个标题行去掉 # 号。""" + assert CustomerServiceAgent._topic_of( + "### 2.1 南方季季盈90天\n\n| 项目 | 详情 |\n|------|------|" + ) == "2.1 南方季季盈90天" + + +def test_fallback_answer_yields_no_topic() -> None: + """兜底话术不是主题:拿它当检索主语只会把问题带偏,宁可退化。""" + assert CustomerServiceAgent._topic_of(FALLBACK) == "" + assert CustomerServiceAgent._topic_of("") == "" + + def test_topic_change_does_not_drag_previous_question_in() -> None: - """客户换话题时,检索词里不能出现上一轮的问题(实测的答非所问场景)。""" - query = CustomerServiceAgent._search_query(_request("基金赎回几天到账", PREVIOUS)) + """客户换话题时,检索词里不能出现上一轮的痕迹(实测的答非所问场景)。""" + query = CustomerServiceAgent._search_query(_request("基金赎回几天到账", PREVIOUS, ANSWER)) assert query == "基金赎回几天到账" assert "季季盈" not in query -def test_referring_phrase_brings_context_in() -> None: - """带明确指代词的问句要靠上文补全主语。""" - query = CustomerServiceAgent._search_query(_request("这个产品的费率是多少", PREVIOUS)) +def test_referring_phrase_brings_topic_in() -> None: + """带明确指代词的问句要靠上文补主语。""" + query = CustomerServiceAgent._search_query(_request("这个产品的费率是多少", PREVIOUS, ANSWER)) - assert PREVIOUS in query + assert query == f"{TOPIC} 这个产品的费率是多少" -def test_very_short_question_brings_context_in() -> None: - """短到不构成完整意图的问句同样需要上文("那它风险高吗"只有 6 个字)。""" - query = CustomerServiceAgent._search_query(_request("那它风险高吗", PREVIOUS)) +def test_very_short_question_brings_topic_in() -> None: + """短到不构成完整意图的问句同样需要主语("那它风险高吗"只有 6 个字)。""" + query = CustomerServiceAgent._search_query(_request("那它风险高吗", PREVIOUS, ANSWER)) - assert PREVIOUS in query + assert query == f"{TOPIC} 那它风险高吗" + + +def test_previous_question_text_is_not_carried_in() -> None: + """只补主语、不补内容:上一轮的问句原话一个字都不能进检索词。""" + query = CustomerServiceAgent._search_query(_request("那它风险高吗", PREVIOUS, ANSWER)) + + assert PREVIOUS not in query + assert "起投金额" not in query + + +def test_fallback_history_degrades_to_current_message_only() -> None: + """上一轮是兜底话术时取不出主语,退化为只查当前这一句,而不是瞎猜。""" + query = CustomerServiceAgent._search_query(_request("那它风险高吗", PREVIOUS, FALLBACK)) + + assert query == "那它风险高吗" def test_first_turn_has_no_history_to_add() -> None: