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
+6 -3
View File
@@ -9,8 +9,11 @@
- 提供了至少一种联系方式(手机/WhatsApp/邮箱/微信)
- 对话至少进行了3轮以上(客户说了3条以上消息)
- 客户表现出真实采购意向而非简单咨询
- 客户询问价格、优惠、运费或样品/交期安排,应视为意向积极信号(有真实采购兴趣)
- 客户主动索要我方联系方式(WhatsApp/邮箱/电话),视为最强意向信号之一
### 积极信号(加分项,非必要条件)
以下信号不作为 interested 的必要条件,出现时在 reasoning 中体现并提升评估信心:
- 客户询问价格、优惠、运费或样品/交期安排(有真实采购兴趣)
- 客户主动索要我方联系方式(WhatsApp/邮箱/电话),是最强意向信号之一
### not_interested(无意向)
客户表现出以下特征:
@@ -26,7 +29,7 @@
- 客户还在比较或犹豫阶段
## 快速示例
- 客户说明确的品类+规格+数量(如 "HDPE PE100 DN110 SDR11, 5000 meters, water project in Kenya")并留了联系方式 → interested
- 客户说明确的品类+规格+数量(如 "HDPE PE100 DN110 SDR11, 5000 meters, water project in Kenya"),聊了几轮后并留了联系方式 → interested
- 客户仅泛泛浏览("what pipes do you have"),没给规格、数量或联系方式 → pending
- 客户明确无采购计划("just looking" / 已从别家采购)→ not_interested
+2 -1
View File
@@ -7,12 +7,13 @@
字段说明:
- 意向品类:客户想采购的管道品类(如 HDPE给水管、PVC排水管、PPR管、双壁波纹管、球墨铸铁管等)
- 规格要求:自由文本,保留客户原话(如 "DN110 PE100 SDR11"、"4 inch SCH40"、"PN16")——不拆分、不翻译、不过度结构化,客户怎么说就怎么记
- 标准要求:客户提到的执行标准(如 ISO4427、ASTM、EN12201、SCH40/SCH80 系列)——客户提到才记,没提到不要猜
- 标准要求:客户提到的执行标准(如 ISO4427、ASTM D3035、EN12201、SCH40/SCH80 系列)——编号原样保留,客户提到才记,没提到不要猜
- 采购数量:数量或工程量(如 "5000米"、"3x40HQ"、"20000条")
- 用途:管道用途(给水/燃气/排污/灌溉等)
- 项目类型:市政工程/农田灌溉/房建项目/经销商备货等
- 目标国家:客户所在或项目所在的国家/地区
- 预算提及:客户提到的预算或目标价(有才记,原样保留)
- 字段值统一用中文概括(如 Kenya→肯尼亚、irrigation→灌溉);唯一例外是规格要求——必须原样保留客户原文,绝不翻译
其中联系方式如果有的话格式为:{"手机": "xxx", "WhatsApp": "xxx", "邮箱": "xxx", "微信": "xxx"}
- 如果对话中客户更正了此前提供的联系方式(如 wrong number / 号码错了 / use this / 换成 / 又给了一个新号),联系方式必须以客户最后提供的值为准覆盖旧值
+1 -1
View File
@@ -178,7 +178,7 @@
### 系统提醒配合
- 系统会在上下文提醒里告诉你客户是否已留联系方式
- 提醒说"已提供"时:绝对禁止再要,确认收到即可;此时拒绝实质问题改用确认式:"Our manager will send the full quotation to your WhatsApp."——固定英文短句("Please leave your contact...")只在客户**未留资**时使用
- 提醒说"已提供"时:绝对禁止再要,确认收到即可;此时拒绝实质问题改用确认式:"Our manager will send the full quotation to your WhatsApp."——引导留资模板句("Please leave your contact...")只在客户**未留资**时使用
- 提醒说"疑似提供"时:礼貌请客户确认一次,不要当成没给过重新索要
### 注意
+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 "")