fix(customer-service): 自述等级的说明行不能挡住产品名
实测连问「季季盈90天起投多少」→「C3 客户能买它吗」→「那我能买它吗」,第三问转人工。 原因是上一步刚加的客户自述等级说明("您提到自己是 C3。…以您在公司留存的测评结果为准") 占了回答首行,而 _topic_of 只看首行,产品名被挤到第二行,追问于是丢掉了指代对象。 这是 _topic_of 第三次因为"只认固定格式"而咬人(前两次:FAQ 的"问"字、适当性回答没有 冒号)。改成遍历回答前四行、逐行尝试解析,并把单行解析抽成 _topic_in。 同时补上自述等级时的依据说明:客户说"我是 C3"而系统答"您没有测评结果",在他眼里是 矛盾的,必须点明判断以档案为准、不以自述为准。自述等级只用于这一句说明,**绝不**参与 裁决。拒绝后的指引也从"联系客户经理或拨打热线"改成"请先完成风险测评"。 新增三条单测:自述等级识别(含小写)、自述说明行占首行时仍能取到主语、 "为 R2"开头说明前面没有产品名时返回空。
This commit is contained in:
@@ -251,11 +251,32 @@ class CustomerServiceAgent(BaseAgent):
|
||||
return self._guide_to_human("适当性校验调用失败")
|
||||
if not isinstance(decision, dict):
|
||||
return self._guide_to_human("适当性校验返回格式异常")
|
||||
text = self._suitability_text(product, risk_level, decision)
|
||||
claimed = self._self_claimed_level(request.message)
|
||||
if claimed:
|
||||
# 客户自述等级时必须点明判断依据,否则他会觉得"我明明说了我是 C3,
|
||||
# 你却说我没有任何测评结果"——两句话在他眼里是矛盾的。
|
||||
# 依据在档案、不在自述,这既是合规要求,也得跟客户讲明白。
|
||||
text = (
|
||||
f"您提到自己是 {claimed}。适当性判断以您在公司留存的、在有效期内的"
|
||||
f"风险测评结果为准,不以本次自述为准。\n{text}"
|
||||
)
|
||||
return CoreResult(
|
||||
text=f"{self._suitability_text(product, risk_level, decision)}\n{DISCLAIMER}",
|
||||
text=f"{text}\n{DISCLAIMER}",
|
||||
intent=self._classified_intent,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _self_claimed_level(message: str) -> str:
|
||||
"""客户在问句里自述的风险等级("我是 C3""C3 客户能买吗"),没有则返回空串。
|
||||
|
||||
只用来在回答里说明判断依据,**绝不**参与裁决——客户等级只能来自档案。
|
||||
"""
|
||||
for level in range(1, 6):
|
||||
if f"C{level}" in message.upper():
|
||||
return f"C{level}"
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def _previous_topic(cls, request: AgentRequest) -> str:
|
||||
"""上一轮回答里的主语(产品名);取不到返回空串。"""
|
||||
@@ -331,7 +352,7 @@ class CustomerServiceAgent(BaseAgent):
|
||||
f"{product}为 {level_name},与您当前的风险测评结果不匹配,"
|
||||
"按照投资者适当性管理规定暂时无法购买。",
|
||||
)
|
||||
lines.append("如需了解具体原因或申请重新测评,请联系您的客户经理或拨打客服热线。")
|
||||
lines.append("请先联系您的客户经理完成风险测评,之后即可查询可购买的产品范围。")
|
||||
valid_until = str(decision.get("assessment_valid_until") or "")
|
||||
if valid_until:
|
||||
lines.append(f"风险测评有效期至 {valid_until[:10]},过期需重新测评。")
|
||||
@@ -389,21 +410,29 @@ class CustomerServiceAgent(BaseAgent):
|
||||
# 短到这种程度的问题,自己通常不构成完整意图("起投多少""风险高吗")
|
||||
_MIN_STANDALONE_CHARS = 8
|
||||
|
||||
@staticmethod
|
||||
def _topic_of(answer: str) -> str:
|
||||
"""从客服上一轮的回答里取出"这一轮在说哪个产品",取不出来就返回空串。
|
||||
@classmethod
|
||||
def _topic_of(cls, answer: str) -> str:
|
||||
"""从客服上一轮的回答里取出"这一轮在说哪个产品",取不出来返回空串。
|
||||
|
||||
回答是我们自己组装的,只有两种形状,都取自知识块的字段:
|
||||
- 行级块:`南方季季盈90天:起投金额 1万元`(首个冒号前就是产品名)
|
||||
- 整节块:`### 2.1 南方季季盈90天`(首个标题行去掉 # 号)
|
||||
遍历前几行而不是只看首行:适当性回答在客户自述等级时会先插一行
|
||||
"您提到自己是 C3。…以您在公司留存的测评结果为准",产品名被挤到第二行——
|
||||
只看首行会让紧随其后的追问丢掉指代对象(实测直接转人工)。
|
||||
"""
|
||||
for line in answer.splitlines()[:4]:
|
||||
topic = cls._topic_in(line.strip().lstrip("#").strip())
|
||||
if topic:
|
||||
return topic
|
||||
return ""
|
||||
|
||||
取不出来时宁可返回空、退化成不带主语,也不要拿兜底话术当主语:
|
||||
「抱歉,这个问题我暂时无法给出准确答复」这种句子拿去检索只会把问题带偏。
|
||||
@staticmethod
|
||||
def _topic_in(first: str) -> str:
|
||||
"""解析单行里可能的主语;解析不出返回空串。
|
||||
|
||||
回答是我们自己组装的,主语只可能出现在三种形状里:
|
||||
- 行级块:「南方季季盈90天:起投金额 1万元」——首个冒号之前;
|
||||
- 整节块:「### 2.1 南方季季盈90天」——标题行;
|
||||
- 适当性:「南方季季盈90天为 R2(中低风险),…」——"为 R"之前。
|
||||
"""
|
||||
first = next((line.strip() for line in answer.splitlines() if line.strip()), "")
|
||||
first = first.lstrip("#").strip()
|
||||
# 适当性回答的首行是「{产品名}为 R2(中低风险),与您当前的风险测评结果不匹配…」,
|
||||
# 产品名在"为 R"之前。这一条必须在冒号判断**之前**:那一行里没有冒号。
|
||||
for level in range(1, 6):
|
||||
index = first.find(f"为 R{level}")
|
||||
if index >= 0:
|
||||
@@ -411,7 +440,8 @@ class CustomerServiceAgent(BaseAgent):
|
||||
return candidate if 0 < len(candidate) <= 20 else ""
|
||||
topic = first.split(":", 1)[0].strip() if ":" in first else first
|
||||
# FAQ 型回答的首行是"问:C1 客户能买什么产品?",冒号前只有一个"问"字;
|
||||
# 拿它当主语只会把检索词污染成"问 那它风险高吗"。
|
||||
# 拿它当主语只会把检索词污染成"问 那它风险高吗"。兜底话术同理,它含",",
|
||||
# 会被下面的长度/标点校验挡掉。
|
||||
if not topic or topic in {"问", "答"}:
|
||||
return ""
|
||||
if len(topic) > 20 or "," in topic or "。" in topic:
|
||||
|
||||
Reference in New Issue
Block a user