From a7135d254980d5da8375ac08a067d86f45b8b305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=BF=E4=BA=91=E7=A7=8B=E6=9C=88?= <15273589815@163.com> Date: Thu, 10 Sep 2026 22:29:28 +0800 Subject: [PATCH] =?UTF-8?q?test(customer-service):=20=E8=A1=A5=E4=BA=A4?= =?UTF-8?q?=E6=A3=80=E7=B4=A2=E9=97=AE=E5=8F=A5=E7=9A=84=E6=96=AD=E8=A8=80?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把检索问句从"拼上一轮原话"改成"只补产品名"时同步更新了本文件,但漏了提交。 新增的断言锁住两件事:上一轮的问句原话一个字都不能进检索词(拼整句会让检索词变宽、 只能命中粗粒度的整节块),以及上一轮是兜底话术时取不出主语应退化为只查当前这一句。 --- .../test_customer_service_search_query.py | 69 +++++++++++++++---- 1 file changed, 55 insertions(+), 14 deletions(-) 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: