feat: 多语言输出墙(€/£/西葡俄阿价格词+时间承诺+ES话术池)+话术建材化+防误杀

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-29 15:42:34 +08:00
parent 8fa3d7a8d0
commit 7003d13f7a
8 changed files with 206 additions and 23 deletions
+68 -12
View File
@@ -9,7 +9,8 @@
- 公里/km 排除必须直接挂在 万块? 之后: 挂在末尾裸 万 备选上会被
万块? 的空块匹配绕过("3万公里"仍命中,实测)。
- 预算复述(裸 budget+数字/中文数字万)、金融裸数(首付/月供)收严;
里程 k 形式(30k km/45k miles)除外。质保期不再豁免(政策变更 2026-09-10:
k 形式的 km/miles 除外项保留(建材语境已无里程语义,兜底无害)。质保期
不再豁免(政策变更 2026-09-10:
保修期限是编造的公司政策承诺,一并墙级拦截,见 PRICE_PATTERNS 时间承诺组)。"""
import re
@@ -23,19 +24,43 @@ PRICE_PATTERNS = [
# 时间承诺: 去掉 warranty 豁免(政策变更 2026-09-10——保修期限是编造的
# 公司政策承诺,不再白名单)
re.compile(r"\d+\s*[- ]?(day|week|month)s?\b", re.I),
# 年限保修定向(双向): year 不能进时间模式(车龄 "under 5 years old" 会误杀),
# 年限保修定向(双向): year 不能进时间模式(使用年限 "under 5 years old" 会误杀),
# 只收 数字+年 与 warranty/质保/保修 的组合(含倒装"质保3年"/"warranty of 8 years")
re.compile(r"\d+\s*years?\b[^.!?]{0,15}warranty", re.I),
re.compile(r"warranty\s*(?:of|for|:)?\s*\d+\s*years?\b", re.I),
re.compile(r"\d+\s*年[^。!?]{0,8}(?:质保|保修)"),
re.compile(r"(?:质保|保修)[^。!?]{0,8}\d+\s*年"),
# 中文时限承诺: 天/周/个月(年不收——"2024年款" 是车型年款)
# 中文时限承诺: 天/周/个月(年不收——裸 "2024年" 是年份/项目日期,
# 带质保/保修的年限由上方定向组收)
re.compile(r"\d+\s*(?:天|周|个月)"),
# 裸预算复述(F3,方案B旗舰形态——冒烟 6/6 破防即此形,原模式完全漏检)
re.compile(r"(budget|预算)[^\d\n]{0,12}\d", re.I),
re.compile(r"(两|三|四|五|六|七|八|九|十)\s*万(?!\s*(公里|km))"),
# 金融裸数(F4)
re.compile(r"(down\s+payment|monthly\s+payment|首付|月供)[^\d\n]{0,10}\d", re.I),
# ---- 多语言墙(建材市场: 欧/英/南美/俄语区/中东) ----
# 货币符号: €/£(参考项目只有 $)
re.compile(r"[€£]\s?\d"),
# 价格名词前置(西/葡/俄/阿): 词后 0-20 非数字字符内出现数字
# ⚠️ 拉丁词必须加 \b 词边界——custo 是英文 customs 的子串,外贸高频,
# "customs paperwork for 20ft" 会被误杀(Task 3 质量审查发现)
# ⚠️ 阿语定冠词 ال 与词连写(السعر): 词首 \b 落在 ل|س 之间(两者皆 \w)
# 不成立,备选须收 (?:ال)? 形态——裸 \bسعر\b 实测漏检 "السعر 500 دولار"
re.compile(r"\b(?:precio|costo|custo|preço|цена|стоимость|"
r"(?:ال)?سعر|(?:ال)?ثمن)\b[^\d\n]{0,20}\d", re.I),
# 货币名词后置(数字+货币词): dólares/euros/рублей(西里尔 долларов
# 由上方 стоимость 前置组兜住,不单列)
# ⚠️ euros 排除 pallets 邻接——"50 euro pallets"(欧标托盘)是建材
# 物流高频词,非价格
re.compile(r"\d[\d,.]*\s*(?:d[óo]lares?|euros?\b(?!\s*pallets?)|"
r"рублей|руб\.?)", re.I),
# 多语言时间承诺(西/葡 dias|semanas,俄 дн/недел,阿 أيام;与英文
# days/weeks 组同构)
# ⚠️ 不收 horas/hours/小时——system.md 的 "reply within 12 hours"
# 依赖此豁免(响应时效承诺非交期承诺)
# ⚠️ 俄语变格只收 дн(ей|я)/недел[ьи] 主形态,其余变格(дню/неделю
# 等)不收——宁可少而准
re.compile(r"\d+\s*[- ]?(?:d[ií]as?|semanas?|дн(?:ей|я)|недел[ьи]|أيام)\b"),
]
DEFLECTIONS_EN = [
@@ -51,10 +76,22 @@ DEFLECTIONS_ZH = [
# F6(D6×过滤器矛盾): 客户已留联系方式时,迂回话术不得再索要——否则与
# 注入的"禁止再索要联系方式"提醒直接打架
DEFLECTIONS_EN_CONTACT = [
"Our manager will send the exact quote to your WhatsApp shortly. Anything else about the cars?",
"Our manager will send the exact quote to your WhatsApp shortly. Anything else about our pipes?",
]
DEFLECTIONS_ZH_CONTACT = [
"客户经理稍后会把准确报价发到你的 WhatsApp。还想了解什么车?",
"客户经理稍后会把准确报价发到你的 WhatsApp。还想了解什么管道产品?",
]
# ---- Task 5: ES 话术池(南美第一大流量) ----
# 与 EN 池语义对齐的西语替换句(注意: 是"替换句",绝不含价格数字,
# 且不得触发本墙自身——见 test_spanish_pools_are_wall_clean)
DEFLECTIONS_ES = [
"El precio exacto depende de la especificación y el flete — nuestro gerente te lo prepara. ¿Me dejas tu WhatsApp o correo?",
"Para un precio exacto, nuestro gerente necesita tu especificación y destino — déjame tu WhatsApp y lo tienes hoy.",
"Prefiero darte una cifra exacta y no un estimado — déjanos tu WhatsApp y nuestro gerente te lo envía.",
]
DEFLECTIONS_ES_CONTACT = [
"Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve. ¿Algo más sobre nuestras tuberías?",
]
@@ -101,24 +138,43 @@ PIVOTS_EN_CONTACT = [
PIVOTS_ZH_CONTACT = [
"客户经理稍后会把准确报价发到你的 WhatsApp。",
]
PIVOTS_ES = [
"Para el precio exacto — ¿cuál es tu WhatsApp? Nuestro gerente te lo envía.",
]
PIVOTS_ES_CONTACT = [
"Nuestro gerente te enviará el precio exacto a tu WhatsApp en breve.",
]
# Task 5: 语言路由三分支——汉字→ZH;西语特征(重音符/ñ/¿¡ 粗匹配,与
# 汉字检测同构)→ES;否则 EN。葡语借道 ES 池(拉美沟通惯例相近)
def _lang_pool(user_msg: str):
if re.search(r"[一-鿿]", user_msg or ""):
return "zh"
if re.search(r"[áéíóúñ¿¡]", user_msg or ""):
return "es"
return "en"
def pivot_for(user_msg: str, has_contact: bool = False) -> str:
zh = bool(re.search(r"[一-鿿]", user_msg or ""))
lang = _lang_pool(user_msg)
if has_contact:
pool = PIVOTS_ZH_CONTACT if zh else PIVOTS_EN_CONTACT
pool = {"zh": PIVOTS_ZH_CONTACT, "es": PIVOTS_ES_CONTACT,
"en": PIVOTS_EN_CONTACT}[lang]
else:
pool = PIVOTS_ZH if zh else PIVOTS_EN
pool = {"zh": PIVOTS_ZH, "es": PIVOTS_ES, "en": PIVOTS_EN}[lang]
return pool[0]
def deflection_for(user_msg: str, rotate: int = 0,
has_contact: bool = False) -> str:
zh = bool(re.search(r"[一-鿿]", user_msg or ""))
lang = _lang_pool(user_msg)
if has_contact:
pool = DEFLECTIONS_ZH_CONTACT if zh else DEFLECTIONS_EN_CONTACT
pool = {"zh": DEFLECTIONS_ZH_CONTACT, "es": DEFLECTIONS_ES_CONTACT,
"en": DEFLECTIONS_EN_CONTACT}[lang]
else:
pool = DEFLECTIONS_ZH if zh else DEFLECTIONS_EN
pool = {"zh": DEFLECTIONS_ZH, "es": DEFLECTIONS_ES,
"en": DEFLECTIONS_EN}[lang]
return pool[rotate % len(pool)]
@@ -139,7 +195,7 @@ NO_CONTACT_ALREADY = ("Got it — our manager will reach out to you at your "
def no_contact_value(sentence: str, user_history: list[dict] | None = None) -> bool:
"""零号码墙(v2): AI 输出不得含任何 ≥10 位数字串或邮箱。
唯一豁免: 该串出现在近期客户消息中(客户自己号码的礼貌回显)。
无值比对——年份/里程/VIN/续航天然安全(已验证)。"""
无值比对——年份/规格编号天然安全(已验证)。"""
for m in LONG_NUMBER_RE.finditer(sentence or ""):
c = digits_only(m.group(0))
if user_history and any(c in digits_only(msg.get("content") or "")