feat: 多语言输出墙(€/£/西葡俄阿价格词+时间承诺+ES话术池)+话术建材化+防误杀
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 / 换成 / 又给了一个新号),联系方式必须以客户最后提供的值为准覆盖旧值
|
||||||
|
|||||||
@@ -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...")只在客户**未留资**时使用
|
||||||
- 提醒说"疑似提供"时:礼貌请客户确认一次,不要当成没给过重新索要
|
- 提醒说"疑似提供"时:礼貌请客户确认一次,不要当成没给过重新索要
|
||||||
|
|
||||||
### 注意
|
### 注意
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user