From 7003d13f7a3806ed9cd209bce4773cc2004e2889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 29 Sep 2026 15:42:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E8=AF=AD=E8=A8=80=E8=BE=93?= =?UTF-8?q?=E5=87=BA=E5=A2=99(=E2=82=AC/=C2=A3/=E8=A5=BF=E8=91=A1=E4=BF=84?= =?UTF-8?q?=E9=98=BF=E4=BB=B7=E6=A0=BC=E8=AF=8D+=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E6=89=BF=E8=AF=BA+ES=E8=AF=9D=E6=9C=AF=E6=B1=A0)+=E8=AF=9D?= =?UTF-8?q?=E6=9C=AF=E5=BB=BA=E6=9D=90=E5=8C=96+=E9=98=B2=E8=AF=AF?= =?UTF-8?q?=E6=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- app/prompts/evaluator.md | 9 +- app/prompts/extractor.md | 3 +- app/prompts/system.md | 2 +- app/services/boundary_filter.py | 80 +++++++++++++++--- .../2026-09-29-building-materials-ai-kefu.md | 17 +++- scripts/breach_rate.py | 11 ++- tests/test_boundary_filter.py | 83 +++++++++++++++++++ tests/test_extractor_evaluator_prompts.py | 24 ++++++ 8 files changed, 206 insertions(+), 23 deletions(-) diff --git a/app/prompts/evaluator.md b/app/prompts/evaluator.md index 1757fa3..f09f883 100644 --- a/app/prompts/evaluator.md +++ b/app/prompts/evaluator.md @@ -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 diff --git a/app/prompts/extractor.md b/app/prompts/extractor.md index 0a05e54..69b5e00 100644 --- a/app/prompts/extractor.md +++ b/app/prompts/extractor.md @@ -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 / 换成 / 又给了一个新号),联系方式必须以客户最后提供的值为准覆盖旧值 diff --git a/app/prompts/system.md b/app/prompts/system.md index c41a10a..ebfe486 100644 --- a/app/prompts/system.md +++ b/app/prompts/system.md @@ -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...")只在客户**未留资**时使用 - 提醒说"疑似提供"时:礼貌请客户确认一次,不要当成没给过重新索要 ### 注意 diff --git a/app/services/boundary_filter.py b/app/services/boundary_filter.py index 254d662..788e444 100644 --- a/app/services/boundary_filter.py +++ b/app/services/boundary_filter.py @@ -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 "") diff --git a/docs/superpowers/plans/2026-09-29-building-materials-ai-kefu.md b/docs/superpowers/plans/2026-09-29-building-materials-ai-kefu.md index 048d512..5f71fa4 100644 --- a/docs/superpowers/plans/2026-09-29-building-materials-ai-kefu.md +++ b/docs/superpowers/plans/2026-09-29-building-materials-ai-kefu.md @@ -189,7 +189,7 @@ git add -A && git commit -m "feat: 系统提示词建材化(管道外贸人设+ **Files:** - Modify: `app/prompts/extractor.md`、`app/prompts/evaluator.md` -- [ ] **Step 1: 重写 extractor.md 字段定义** +- [x] **Step 1: 重写 extractor.md 字段定义** 中文键画像字段改建材语义(设计文档§4.3): - `意向品类`(HDPE给水管/PVC排水管/PPR…) @@ -197,16 +197,16 @@ git add -A && git commit -m "feat: 系统提示词建材化(管道外贸人设+ - `标准要求`:单独一列(ISO4427/ASTM/EN/SCH 系列,客户提到才记) - `采购数量`、`用途`(给水/燃气/排污/灌溉/…)、`项目类型`、`目标国家`、`预算提及`、`联系方式`字段原样保留参考项目机制 -- [ ] **Step 2: 重写 evaluator.md 评估语义** +- [x] **Step 2: 重写 evaluator.md 评估语义** 三级保留;示例改管道采购:明确品类+规格+数量=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 提示词加载后包含 `意向品类`、`规格要求`、`标准要求` 字段定义,且不含 `车型`/`意向品牌` 汽车字段。 -- [ ] **Step 4: 跑全量测试并 Commit** +- [x] **Step 4: 跑全量测试并 Commit** ```bash 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 的统计模式(保持与墙一致,供上线观察期用)。 +**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** ```bash diff --git a/scripts/breach_rate.py b/scripts/breach_rate.py index dcba105..7aee847 100644 --- a/scripts/breach_rate.py +++ b/scripts/breach_rate.py @@ -1,5 +1,5 @@ """职能边界破防率观测(只读): AI 回复中出现"价格形态数字"的轮次占比。 -方案B(输出过滤)的启动依据: >10% 即砌墙。锚定式匹配,年份/续航数字天然不命中。""" +方案B(输出过滤)的启动依据: >10% 即砌墙。锚定式匹配,年份/规格数字天然不命中。""" import re import sqlite3 import sys @@ -8,10 +8,17 @@ DB = sys.argv[1] if len(sys.argv) > 1 else "./data/cc_ai_kefu.db" PATTERNS = [ (r"\$\s?\d", "美元符"), + (r"[€£]\s?\d", "欧元/英镑符"), (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}(?