fix: 客服置信判定改为「绝对阈值 + 相对间隙」混合判定
问题(业务方实测发现):问「我们公司叫什么名字」被引导人工,但公司名称就在知识库里
——FAQ-0001「公司全称是什么?」与 COMP-001 都稳定命中 top1。所以根因不是检索不准,
而是判定规则不完整:客服方案 §2.3 要求的是「绝对阈值 AND(相对间隙 OR 分布优势)」,
实现里只做了绝对阈值 0.60。
实测校准(qwen3.7-text-embedding-flash,COSINE):
库内问法 top1 top1-top2
我们公司叫什么名字 0.592 0.100
你们公司名称是什么 0.579 0.090
公司全称是什么 0.671 0.098
你们公司总部在哪 0.764 0.245
南方科技的全称 0.855 —
库外 / 越界 top1 top1-top2
你们公司什么时候上市 0.500 0.046
推荐明天肯定涨的基金 0.488 0.023
我要投诉 0.492 0.025
今天天气怎么样 0.416 0.035
量子计算机退相干 0.416 0.044
两条结论:一是同为正确命中,口语问法的相似度天然偏低(0.592 vs 0.855),单用绝对阈值
必然误判;二是库内命中的 top1 领先幅度(≥0.09)显著大于库外(≤0.046),间隙是有效判别信号。
新规则:≥0.75 直接答(不再要求间隙);≥0.55 且间隙 ≥0.07 则回答并附「信息可能不完整」
提示;其余一律引导客户致电人工客服。两侧余量:库内最低 0.579、库外最高 0.500。
验证:ruff 通过、mypy 107 文件无错;ask_customer_service 对「我们公司叫什么名字」
正确返回南方科技有限公司;customer_service_check 由 8 项扩为 9 项,全部通过,
其中「推荐明天肯定涨的基金 / 我要投诉 / 量子计算机退相干」三条必须引导人工的用例
未被放宽后的阈值误答。
This commit is contained in:
@@ -41,10 +41,19 @@ BUSINESS_INTENTS = (INTENT_FAQ, INTENT_PRODUCT, INTENT_POLICY)
|
||||
|
||||
TOOL_NAME = "search_knowledge"
|
||||
|
||||
# 三档置信阈值(方案 §2.3,业务方已确认先按此跑通、后续用语料校准)。
|
||||
# 分数为 COSINE 相似度,落在 [0, 1]。
|
||||
HIGH_SCORE = 0.75 # ≥ 直接答
|
||||
MID_SCORE = 0.60 # ≥ 且 < 高置信:答 + 提示可能不完整;< 该值:不硬答,引导人工
|
||||
# 三档置信阈值:方案 §2.3 要求的是「绝对阈值 AND(相对间隙 OR 分布优势)」混合判定,
|
||||
# 只做绝对阈值会把口语化问法误判成"答不了"(这是实测踩到的坑)。
|
||||
#
|
||||
# 数值按本模型(qwen3.7-text-embedding-flash)**实测校准**,不是照搬经验值:
|
||||
# · 库内问法:口语「我们公司叫什么名字」top1=0.592、标准「公司全称是什么」0.671、
|
||||
# 带品牌名「南方科技的全称」0.855 —— 同样的正确答案,口语问法相似度天然更低;
|
||||
# · 库外/越界:「你们公司什么时候上市」top1 最高 0.500、「今天天气怎么样」0.416,
|
||||
# 且这些问题的 top1 与次优间隙都在 0.046 以内,而库内命中普遍在 0.09 以上。
|
||||
# 于是:库内最低 0.579 / 库外最高 0.500,绝对分两侧都有余量;间隙 0.07 又能挡住
|
||||
# 「存在并列候选」的不确定情形,避免因为一次高相似度的巧合就硬答。
|
||||
HIGH_SCORE = 0.75 # ≥ 直接答(高置信不再要求间隙:分数已足够说明问题)
|
||||
MID_SCORE = 0.55 # 需与 MIN_GAP 同时满足才答,答时附"信息可能不完整"提示
|
||||
MIN_GAP = 0.07 # top1 领先次优的最小间隙;领先不足说明有并列候选,不硬答
|
||||
|
||||
TOP_K = 5
|
||||
MAX_ANSWER_CHARS = 1200
|
||||
@@ -128,15 +137,20 @@ class CustomerServiceAgent(BaseAgent):
|
||||
if not isinstance(best, dict):
|
||||
return self._guide_to_human("命中内容格式异常")
|
||||
score = self._score(best.get("score"))
|
||||
if score < MID_SCORE:
|
||||
return self._guide_to_human(f"置信度不足:{score:.3f}")
|
||||
gap = score - self._second_score(hits)
|
||||
# 混合判定:高置信直接答;中置信必须同时满足「分数够」与「领先次优够多」。
|
||||
# 只满足其一的(分数够但两三个候选并驾齐驱)宁可引导人工——金融场景下
|
||||
# "答不了"可接受,"答错"不可接受。
|
||||
confident = score >= HIGH_SCORE
|
||||
if not confident and not (score >= MID_SCORE and gap >= MIN_GAP):
|
||||
return self._guide_to_human(f"置信度不足:score={score:.3f} gap={gap:.3f}")
|
||||
|
||||
content = str(best.get("content") or "").strip()
|
||||
if not content:
|
||||
return self._guide_to_human("命中内容为空")
|
||||
|
||||
answer = content[:MAX_ANSWER_CHARS]
|
||||
if score < HIGH_SCORE:
|
||||
if not confident:
|
||||
answer = f"{answer}\n{INCOMPLETE_NOTICE}"
|
||||
# 知识出处写进正文,而不是塞进 source_references:
|
||||
# `governance.review_output` 只接受「本次召回的记忆」与「本次成功调用的工具」两类引用
|
||||
@@ -218,6 +232,16 @@ class CustomerServiceAgent(BaseAgent):
|
||||
return 0.0
|
||||
return min(1.0, max(0.0, number))
|
||||
|
||||
def _second_score(self, hits: list[object]) -> float:
|
||||
"""次优命中分数,用于「绝对阈值 + 相对间隙」的混合判定。
|
||||
|
||||
只有一个命中时返回 0:此时间隙最大,是否回答交由绝对阈值把关,
|
||||
而不是仅凭"没有竞争者"就认定可信。
|
||||
"""
|
||||
if len(hits) < 2 or not isinstance(hits[1], dict):
|
||||
return 0.0
|
||||
return self._score(hits[1].get("score"))
|
||||
|
||||
@staticmethod
|
||||
def _source_note(hit: dict[str, object]) -> str:
|
||||
"""把知识出处写成一行正文(文件标题 + 内部文件编号),便于客户与人工核对。"""
|
||||
|
||||
@@ -39,6 +39,9 @@ CASES: list[tuple[str, str]] = [
|
||||
("南方季季盈90天的起投金额是多少?", "product"),
|
||||
("C1 保守型客户可以买什么风险等级的产品?", "policy"),
|
||||
("你们公司的客服电话是多少?", "company"),
|
||||
# 回归用例:业务方实测发现「我们公司叫什么名字」被误判成答不了(口语问法相似度天然
|
||||
# 偏低,0.592 低于旧的绝对阈值 0.60),据此把判定改成「绝对阈值 + 相对间隙」混合判定。
|
||||
("我们公司叫什么名字", "口语问法-回归"),
|
||||
("你好呀,今天心情不错", "chitchat"),
|
||||
("帮我推荐一只明天肯定涨的基金", "越界请求"),
|
||||
("我买的基金亏了,我要投诉!", "投诉"),
|
||||
|
||||
Reference in New Issue
Block a user