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/邮箱/微信) - 提供了至少一种联系方式(手机/WhatsApp/邮箱/微信)
- 对话至少进行了3轮以上(客户说了3条以上消息) - 对话至少进行了3轮以上(客户说了3条以上消息)
- 客户表现出真实采购意向而非简单咨询 - 客户表现出真实采购意向而非简单咨询
- 客户询问价格、优惠、运费或样品/交期安排,应视为意向积极信号(有真实采购兴趣)
- 客户主动索要我方联系方式(WhatsApp/邮箱/电话),视为最强意向信号之一 ### 积极信号(加分项,非必要条件)
以下信号不作为 interested 的必要条件,出现时在 reasoning 中体现并提升评估信心:
- 客户询问价格、优惠、运费或样品/交期安排(有真实采购兴趣)
- 客户主动索要我方联系方式(WhatsApp/邮箱/电话),是最强意向信号之一
### not_interested(无意向) ### 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 - 客户仅泛泛浏览("what pipes do you have"),没给规格、数量或联系方式 → pending
- 客户明确无采购计划("just looking" / 已从别家采购)→ not_interested - 客户明确无采购计划("just looking" / 已从别家采购)→ not_interested
+2 -1
View File
@@ -7,12 +7,13 @@
字段说明: 字段说明:
- 意向品类:客户想采购的管道品类(如 HDPE给水管、PVC排水管、PPR管、双壁波纹管、球墨铸铁管等) - 意向品类:客户想采购的管道品类(如 HDPE给水管、PVC排水管、PPR管、双壁波纹管、球墨铸铁管等)
- 规格要求:自由文本,保留客户原话(如 "DN110 PE100 SDR11"、"4 inch SCH40"、"PN16")——不拆分、不翻译、不过度结构化,客户怎么说就怎么记 - 规格要求:自由文本,保留客户原话(如 "DN110 PE100 SDR11"、"4 inch SCH40"、"PN16")——不拆分、不翻译、不过度结构化,客户怎么说就怎么记
- 标准要求:客户提到的执行标准(如 ISO4427、ASTM、EN12201、SCH40/SCH80 系列)——客户提到才记,没提到不要猜 - 标准要求:客户提到的执行标准(如 ISO4427、ASTM D3035、EN12201、SCH40/SCH80 系列)——编号原样保留,客户提到才记,没提到不要猜
- 采购数量:数量或工程量(如 "5000米"、"3x40HQ"、"20000条") - 采购数量:数量或工程量(如 "5000米"、"3x40HQ"、"20000条")
- 用途:管道用途(给水/燃气/排污/灌溉等) - 用途:管道用途(给水/燃气/排污/灌溉等)
- 项目类型:市政工程/农田灌溉/房建项目/经销商备货等 - 项目类型:市政工程/农田灌溉/房建项目/经销商备货等
- 目标国家:客户所在或项目所在的国家/地区 - 目标国家:客户所在或项目所在的国家/地区
- 预算提及:客户提到的预算或目标价(有才记,原样保留) - 预算提及:客户提到的预算或目标价(有才记,原样保留)
- 字段值统一用中文概括(如 Kenya→肯尼亚、irrigation→灌溉);唯一例外是规格要求——必须原样保留客户原文,绝不翻译
其中联系方式如果有的话格式为:{"手机": "xxx", "WhatsApp": "xxx", "邮箱": "xxx", "微信": "xxx"} 其中联系方式如果有的话格式为:{"手机": "xxx", "WhatsApp": "xxx", "邮箱": "xxx", "微信": "xxx"}
- 如果对话中客户更正了此前提供的联系方式(如 wrong number / 号码错了 / use this / 换成 / 又给了一个新号),联系方式必须以客户最后提供的值为准覆盖旧值 - 如果对话中客户更正了此前提供的联系方式(如 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 排除必须直接挂在 万块? 之后: 挂在末尾裸 万 备选上会被 - 公里/km 排除必须直接挂在 万块? 之后: 挂在末尾裸 万 备选上会被
万块? 的空块匹配绕过("3万公里"仍命中,实测)。 万块? 的空块匹配绕过("3万公里"仍命中,实测)。
- 预算复述(裸 budget+数字/中文数字万)、金融裸数(首付/月供)收严; - 预算复述(裸 budget+数字/中文数字万)、金融裸数(首付/月供)收严;
里程 k 形式(30k km/45k miles)除外。质保期不再豁免(政策变更 2026-09-10: k 形式的 km/miles 除外项保留(建材语境已无里程语义,兜底无害)。质保期
不再豁免(政策变更 2026-09-10:
保修期限是编造的公司政策承诺,一并墙级拦截,见 PRICE_PATTERNS 时间承诺组)。""" 保修期限是编造的公司政策承诺,一并墙级拦截,见 PRICE_PATTERNS 时间承诺组)。"""
import re import re
@@ -23,19 +24,43 @@ PRICE_PATTERNS = [
# 时间承诺: 去掉 warranty 豁免(政策变更 2026-09-10——保修期限是编造的 # 时间承诺: 去掉 warranty 豁免(政策变更 2026-09-10——保修期限是编造的
# 公司政策承诺,不再白名单) # 公司政策承诺,不再白名单)
re.compile(r"\d+\s*[- ]?(day|week|month)s?\b", re.I), 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") # 只收 数字+年 与 warranty/质保/保修 的组合(含倒装"质保3年"/"warranty of 8 years")
re.compile(r"\d+\s*years?\b[^.!?]{0,15}warranty", re.I), 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"warranty\s*(?:of|for|:)?\s*\d+\s*years?\b", re.I),
re.compile(r"\d+\s*年[^。!?]{0,8}(?:质保|保修)"), re.compile(r"\d+\s*年[^。!?]{0,8}(?:质保|保修)"),
re.compile(r"(?:质保|保修)[^。!?]{0,8}\d+\s*年"), re.compile(r"(?:质保|保修)[^。!?]{0,8}\d+\s*年"),
# 中文时限承诺: 天/周/个月(年不收——"2024年款" 是车型年款) # 中文时限承诺: 天/周/个月(年不收——裸 "2024年" 是年份/项目日期,
# 带质保/保修的年限由上方定向组收)
re.compile(r"\d+\s*(?:天|周|个月)"), re.compile(r"\d+\s*(?:天|周|个月)"),
# 裸预算复述(F3,方案B旗舰形态——冒烟 6/6 破防即此形,原模式完全漏检) # 裸预算复述(F3,方案B旗舰形态——冒烟 6/6 破防即此形,原模式完全漏检)
re.compile(r"(budget|预算)[^\d\n]{0,12}\d", re.I), re.compile(r"(budget|预算)[^\d\n]{0,12}\d", re.I),
re.compile(r"(两|三|四|五|六|七|八|九|十)\s*万(?!\s*(公里|km))"), re.compile(r"(两|三|四|五|六|七|八|九|十)\s*万(?!\s*(公里|km))"),
# 金融裸数(F4) # 金融裸数(F4)
re.compile(r"(down\s+payment|monthly\s+payment|首付|月供)[^\d\n]{0,10}\d", re.I), 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 = [ DEFLECTIONS_EN = [
@@ -51,10 +76,22 @@ DEFLECTIONS_ZH = [
# F6(D6×过滤器矛盾): 客户已留联系方式时,迂回话术不得再索要——否则与 # F6(D6×过滤器矛盾): 客户已留联系方式时,迂回话术不得再索要——否则与
# 注入的"禁止再索要联系方式"提醒直接打架 # 注入的"禁止再索要联系方式"提醒直接打架
DEFLECTIONS_EN_CONTACT = [ 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 = [ 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 = [ PIVOTS_ZH_CONTACT = [
"客户经理稍后会把准确报价发到你的 WhatsApp。", "客户经理稍后会把准确报价发到你的 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: 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: 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: else:
pool = PIVOTS_ZH if zh else PIVOTS_EN pool = {"zh": PIVOTS_ZH, "es": PIVOTS_ES, "en": PIVOTS_EN}[lang]
return pool[0] return pool[0]
def deflection_for(user_msg: str, rotate: int = 0, def deflection_for(user_msg: str, rotate: int = 0,
has_contact: bool = False) -> str: has_contact: bool = False) -> str:
zh = bool(re.search(r"[一-鿿]", user_msg or "")) lang = _lang_pool(user_msg)
if has_contact: 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: 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)] 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: def no_contact_value(sentence: str, user_history: list[dict] | None = None) -> bool:
"""零号码墙(v2): AI 输出不得含任何 ≥10 位数字串或邮箱。 """零号码墙(v2): AI 输出不得含任何 ≥10 位数字串或邮箱。
唯一豁免: 该串出现在近期客户消息中(客户自己号码的礼貌回显)。 唯一豁免: 该串出现在近期客户消息中(客户自己号码的礼貌回显)。
无值比对——年份/里程/VIN/续航天然安全(已验证)。""" 无值比对——年份/规格编号天然安全(已验证)。"""
for m in LONG_NUMBER_RE.finditer(sentence or ""): for m in LONG_NUMBER_RE.finditer(sentence or ""):
c = digits_only(m.group(0)) c = digits_only(m.group(0))
if user_history and any(c in digits_only(msg.get("content") or "") if user_history and any(c in digits_only(msg.get("content") or "")
@@ -189,7 +189,7 @@ git add -A && git commit -m "feat: 系统提示词建材化(管道外贸人设+
**Files:** **Files:**
- Modify: `app/prompts/extractor.md`、`app/prompts/evaluator.md` - Modify: `app/prompts/extractor.md`、`app/prompts/evaluator.md`
- [ ] **Step 1: 重写 extractor.md 字段定义** - [x] **Step 1: 重写 extractor.md 字段定义**
中文键画像字段改建材语义(设计文档§4.3): 中文键画像字段改建材语义(设计文档§4.3):
- `意向品类`(HDPE给水管/PVC排水管/PPR…) - `意向品类`(HDPE给水管/PVC排水管/PPR…)
@@ -197,16 +197,16 @@ git add -A && git commit -m "feat: 系统提示词建材化(管道外贸人设+
- `标准要求`:单独一列(ISO4427/ASTM/EN/SCH 系列,客户提到才记) - `标准要求`:单独一列(ISO4427/ASTM/EN/SCH 系列,客户提到才记)
- `采购数量`、`用途`(给水/燃气/排污/灌溉/…)、`项目类型`、`目标国家`、`预算提及`、`联系方式`字段原样保留参考项目机制 - `采购数量`、`用途`(给水/燃气/排污/灌溉/…)、`项目类型`、`目标国家`、`预算提及`、`联系方式`字段原样保留参考项目机制
- [ ] **Step 2: 重写 evaluator.md 评估语义** - [x] **Step 2: 重写 evaluator.md 评估语义**
三级保留;示例改管道采购:明确品类+规格+数量=interested;仅泛泛浏览=pending;明确无采购计划=not_interested。 三级保留;示例改管道采购:明确品类+规格+数量=interested;仅泛泛浏览=pending;明确无采购计划=not_interested。
- [ ] **Step 3: 新增 extractor 字段静态断言测试(spec §9.1 第4条落地;严格 TDD 可提前到 Step 1 之前先写先跑红)** - [x] **Step 3: 新增 extractor 字段静态断言测试(spec §9.1 第4条落地;严格 TDD 可提前到 Step 1 之前先写先跑红)**
`tests/test_company_facts.py` 风格,新建断言:extractor 提示词加载后包含 `tests/test_company_facts.py` 风格,新建断言:extractor 提示词加载后包含
`意向品类`、`规格要求`、`标准要求` 字段定义,且不含 `车型`/`意向品牌` 汽车字段。 `意向品类`、`规格要求`、`标准要求` 字段定义,且不含 `车型`/`意向品牌` 汽车字段。
- [ ] **Step 4: 跑全量测试并 Commit** - [x] **Step 4: 跑全量测试并 Commit**
```bash ```bash
python -m pytest -q && git add -A && git commit -m "feat: 提取/评估提示词建材化(规格自由文本+标准单列)" python -m pytest -q && git add -A && git commit -m "feat: 提取/评估提示词建材化(规格自由文本+标准单列)"
@@ -317,6 +317,15 @@ python -m pytest tests/test_boundary_filter.py -q
把上述新词表同步进 breach_rate.py 的统计模式(保持与墙一致,供上线观察期用)。 把上述新词表同步进 breach_rate.py 的统计模式(保持与墙一致,供上线观察期用)。
**Step 6b: 打包前序任务遗留的提示词微修(Task 3/4 质量审查移交,随本任务 commit):**
1. extractor.md 字段说明末尾补一行(Task 4 Important):「字段值统一用中文概括(如 Kenya→肯尼亚、irrigation→灌溉);唯一例外是规格要求——必须原样保留客户原文,绝不翻译」
2. evaluator.md:「询问价格/索要联系方式」两条移出 interested 必要条件列表,单独设「积极信号(加分项,非必要条件)」小节(Task 4 Minor 1)
3. evaluator.md interested 示例补「聊了几轮后」(Task 4 Minor 2)
4. extractor.md 标准要求示例改 "ASTM D3035" 并补「编号原样保留」(Task 4 Minor 4)
5. system.md line 181「固定英文短句」统一为「引导留资模板句」(Task 4 Minor 5)
6. system.md line 18 与语言规则段确认无残留字面张力(Task 3 复审 nano-nit 已修 line 19,复核即可)
修完跑 test_extractor_evaluator_prompts.py 相关断言(必要时同步微调断言)。
- [ ] **Step 7: 全量测试 + Commit** - [ ] **Step 7: 全量测试 + Commit**
```bash ```bash
+9 -2
View File
@@ -1,5 +1,5 @@
"""职能边界破防率观测(只读): AI 回复中出现"价格形态数字"的轮次占比。 """职能边界破防率观测(只读): AI 回复中出现"价格形态数字"的轮次占比。
方案B(输出过滤)的启动依据: >10% 即砌墙。锚定式匹配,年份/续航数字天然不命中。""" 方案B(输出过滤)的启动依据: >10% 即砌墙。锚定式匹配,年份/规格数字天然不命中。"""
import re import re
import sqlite3 import sqlite3
import sys import sys
@@ -8,10 +8,17 @@ DB = sys.argv[1] if len(sys.argv) > 1 else "./data/cc_ai_kefu.db"
PATTERNS = [ PATTERNS = [
(r"\$\s?\d", "美元符"), (r"\$\s?\d", "美元符"),
(r"[€£]\s?\d", "欧元/英镑符"),
(r"\d[\d,.]*\s*(usd|美元|万块?|块钱)", "货币单位"), (r"\d[\d,.]*\s*(usd|美元|万块?|块钱)", "货币单位"),
(r"\d[\d,.]*\s*(d[óo]lares?|euros?\b(?!\s*pallets?)|рублей|руб\.?)",
"货币单位(多语言)"),
(r"\b(?:precio|costo|custo|preço|цена|стоимость|(?:ال)?سعر|(?:ال)?ثمن)\b"
r"[^\d\n]{0,20}\d", "价格名词前置(多语言)"),
(r"(price|cost|quote)[^.\d]{0,20}(?<![a-z])(?!(?:19|20)\d\d(?!\d))\d", (r"(price|cost|quote)[^.\d]{0,20}(?<![a-z])(?!(?:19|20)\d\d(?!\d))\d",
"价格动词邻接(年份/车型编号排除)"), "价格动词邻接(年份排除)"),
(r"\d[\d,.]*\s*k\b(?!\w)", "25k形式"), (r"\d[\d,.]*\s*k\b(?!\w)", "25k形式"),
(r"\d+\s*[- ]?(day|week|month|d[ií]as?|semanas?|дн(?:ей|я)|недел[ьи]|"
r"أيام)s?\b", "交期/时间承诺"),
] ]
COMPILED = [(re.compile(p, re.I), name) for p, name in PATTERNS] COMPILED = [(re.compile(p, re.I), name) for p, name in PATTERNS]
+83
View File
@@ -223,3 +223,86 @@ def test_age_and_year_talk_still_clean():
assert not violates_boundary("does it come with warranty?") assert not violates_boundary("does it come with warranty?")
assert not violates_boundary("warranty details from our manager") assert not violates_boundary("warranty details from our manager")
assert not violates_boundary("how many years warranty?") # 无承诺数字的反问 assert not violates_boundary("how many years warranty?") # 无承诺数字的反问
# ---- Task 5: 多语言价格墙(建材市场: 欧/英/南美/俄语区/中东) ----
# GLM 多语言回复的最后防线: 宁可模式少而准,不可误杀规格数字
# (DN800 被墙替换成迂回话术对客户是灾难)
MULTILINGUAL_PRICE_HITS = [
"El precio es 500 dólares", # 西语 词前置
"Costo por tonelada 320", # 西语
"O preço é 1200 reais", # 葡语(巴西)
"Цена 500 за тонну", # 俄语
"Стоимость 800 долларов", # 俄语
"السعر 500 دولار", # 阿语
"€500 per ton", # 欧元符号
"£420 per container", # 英镑符号
"500 dólares", # 数字+货币词后缀
"Delivery in 15 días", # 西语时间承诺
"Срок 2 недели", # 俄语时间承诺
]
# 建材规格形态: 不得误杀
SPEC_NON_HITS = [
"DN800 double wall corrugated pipe",
"PN16 pressure rating",
"SDR11 and SDR17 both available",
"PE100 material",
"600mm diameter",
"SCH40 and SCH80",
"3/4 inch and 2 inch sizes",
"ISO4427 certified",
"20ft container load", "40ft HQ",
"5.8m and 11.8m lengths",
"PE80 PE100 grades",
# Task 3 质量审查发现: customs 是 custo 超集,高频外贸词,不得误杀
"customs paperwork for 20ft containers",
"HS code 3917 for customs clearance",
# euro pallet(欧标托盘)是建材物流高频词,"50 euro pallets" 非价格
"50 euro pallets loading",
]
def test_multilingual_price_hits_violate():
for s in MULTILINGUAL_PRICE_HITS:
assert violates_boundary(s), s
def test_pipe_spec_forms_still_clean():
for s in SPEC_NON_HITS:
assert not violates_boundary(s), s
def test_deflection_and_pivot_spanish_routing():
from app.services.boundary_filter import (
DEFLECTIONS_ES, DEFLECTIONS_ES_CONTACT, PIVOTS_ES, PIVOTS_ES_CONTACT,
)
assert deflection_for("¿Cuánto cuesta?") in DEFLECTIONS_ES
assert deflection_for("¿Cuánto cuesta?", has_contact=True) in DEFLECTIONS_ES_CONTACT
assert pivot_for("¿Me pasas el precio?") in PIVOTS_ES
assert pivot_for("¿Me pasas el precio?", has_contact=True) in PIVOTS_ES_CONTACT
# 既有二分支路由不回归: 汉字→ZH, 无西语特征→EN
assert deflection_for("多少钱") not in DEFLECTIONS_ES
assert deflection_for("how much?") not in DEFLECTIONS_ES
def test_spanish_pools_are_wall_clean():
# ES 替换句自身不得触发价格墙/零号码墙(防递归与自破)
from app.services.boundary_filter import (
DEFLECTIONS_ES, DEFLECTIONS_ES_CONTACT, PIVOTS_ES, PIVOTS_ES_CONTACT,
)
for t in (DEFLECTIONS_ES + DEFLECTIONS_ES_CONTACT
+ PIVOTS_ES + PIVOTS_ES_CONTACT):
assert not violates_boundary(t), t
assert not no_contact_value(t), t
def test_no_auto_wording_left_in_any_pool():
# 行业残留清零: "cars"/"什么车" 不得出现在任何话术池(含零号码墙替换句)
import app.services.boundary_filter as bf
for name in [n for n in dir(bf)
if n.startswith(("DEFLECTIONS", "PIVOTS", "NO_CONTACT"))]:
pool = getattr(bf, name)
for t in (pool if isinstance(pool, list) else [pool]):
assert "cars" not in t.lower(), f"{name}: {t}"
assert "什么车" not in t, f"{name}: {t}"
+24
View File
@@ -67,3 +67,27 @@ def test_evaluator_pipe_procurement_examples():
assert "采购需求" in EVALUATOR_PROMPT assert "采购需求" in EVALUATOR_PROMPT
assert "随便看看" in EVALUATOR_PROMPT # not_interested: 明确不买 assert "随便看看" in EVALUATOR_PROMPT # not_interested: 明确不买
assert "泛泛" in EVALUATOR_PROMPT # pending: 仅泛泛浏览 assert "泛泛" in EVALUATOR_PROMPT # pending: 仅泛泛浏览
# ---- Task 5/6b: 质量审查移交的提示词微修 ----
def test_extractor_field_values_chinese_summary():
# 字段值统一中文概括;唯一例外是规格要求原样保留
assert "中文概括" in EXTRACTOR_PROMPT
assert "绝不翻译" in EXTRACTOR_PROMPT
def test_extractor_standard_example_with_number_kept():
# 标准要求示例 ASTM D3035(具体编号)+ 编号原样保留
assert "ASTM D3035" in EXTRACTOR_PROMPT
assert "编号原样保留" in EXTRACTOR_PROMPT
def test_evaluator_positive_signals_are_bonus_not_required():
# 询价/索联系方式移出 interested 必要条件,单独"积极信号"小节
assert "积极信号(加分项,非必要条件)" in EVALUATOR_PROMPT
assert "非必要条件" in EVALUATOR_PROMPT
def test_evaluator_interested_example_mentions_rounds():
# interested 示例补"聊了几轮后"(轮次语境)
assert "聊了几轮后" in EVALUATOR_PROMPT