fix: 墙层审查修复——10位号穿透/CC轮回显closing用CC句/breach号码观测/拼接分隔

审查(With fixes)四项:

- Important-1: LONG_NUMBER_RE {9,17}→{8,17}——旧量词最小匹配 11 字符,
  纯 10 位号(沙特裸号 0501234567,CC 模板范例市场本体)穿透。实测钉死
  行为边界: 逗号不在字符类,"50,000,000" 千分位形态仍放行(与
  "45,000 km" 同机制,审查预估会命中系漏算逗号);空格分隔 ≥10 位命中;
  DN110/5000m/VIN 规格形态全放行
- Important-2: CC 触发轮回显被零号码墙拦下时(violation==no_contact),
  closing 改用 CC 模板句而非 NO_CONTACT_ALREADY——GLM 有 3/3 回显倾向,
  客户该收到追问而非"稍后联系"虚假承诺;价格墙路径不变。设计文档 §2
  补记该裁决
- Minor-4: breach_rate.py 补号码回显(≥10位,与墙同形)与邮箱回显两条
  观测模式,补上漏放观测盲区
- Minor-5: violated 收尾与已流出前缀之间补空格分隔("...xxxx? Got it —"),
  空格随 closing 一起流出,客户所见与落库一致;CC/触发轮缓冲未流出,
  emitted 恒空天然不受影响

测试 256 passed(253+3)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-08 13:59:41 +08:00
parent 41b2ce081f
commit 99f51e7703
6 changed files with 103 additions and 4 deletions
+5 -1
View File
@@ -199,7 +199,11 @@ def deflection_for(user_msg: str, rotate: int = 0,
# ---- 15.3: 零号码墙(doctrine v2) ----
# AI 绝不输出任何号码/邮箱: 与价格墙并列的第二道确定性红线。
LONG_NUMBER_RE = re.compile(r"\+?\d[\d\s\-().]{9,17}\d") # ≥10位连续数字(允许分隔形态)
# Important-1(审查修复): 量词 {8,17} 最小匹配 10 字符——纯 10 位号
# (沙特裸号 0501234567,CC 模板范例市场本体)必须命中。',' 不在字符类:
# "50,000,000" 千分位形态放行(与 "45,000 km" 放行同机制),空格/横线
# 分隔的 ≥10 位数字串命中;规格形态(DN110/5000m)经实测仍全放行。
LONG_NUMBER_RE = re.compile(r"\+?\d[\d\s\-().]{8,17}\d") # ≥10位连续数字(允许分隔形态)
ANY_EMAIL_RE = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
# 12 小时口径与 system.md 对齐(Task 3 质量审查 Minor 4: 原句 "replies fast"
+17 -3
View File
@@ -368,9 +368,17 @@ class ConversationManager:
logger.warning(
"No-contact wall replaced sentence (conv=%s): %r",
conv.id, dirty[:60])
closing = (NO_CONTACT_ALREADY if has_contact
else (NO_CONTACT_APP if conv.source.value == "app"
else NO_CONTACT_WEB))
if cc_wall:
# Important-2(审查修复): CC 触发轮回显被拦 → closing 改用
# CC 模板句而非 NO_CONTACT_ALREADY——GLM 有 3/3 回显倾向,
# 该路径概率高,客户该收到追问而非"稍后联系"虚假承诺;
# 价格墙路径(violation=="boundary")不变
closing = (_CC_WALL_ZH if _CJK_RE.search(content or "")
else _CC_WALL_EN)
else:
closing = (NO_CONTACT_ALREADY if has_contact
else (NO_CONTACT_APP if conv.source.value == "app"
else NO_CONTACT_WEB))
else:
logger.warning(
"Boundary filter intercepted sentence (conv=%s): %r | tail=%r",
@@ -378,6 +386,12 @@ class ConversationManager:
closing = (pivot_for(content, has_contact) if emitted
else deflection_for(content, _boundary_rotate(),
has_contact=has_contact))
# Minor-5(审查修复): 已流出前缀与替换句之间补一个空格分隔
# ("...xxxx? Got it —"——英文衔接无空格会粘连);CC/触发轮缓冲
# 未流出,emitted 恒空,天然不受影响。空格随 closing 一起流出,
# 客户所见与落库保持一致
if emitted and not emitted.endswith((" ", "\n")):
closing = " " + closing
for i in range(0, len(closing), 40):
yield {"type": "stream_chunk", "content": closing[i:i + 40]}
emitted += closing
@@ -24,6 +24,10 @@ GLM 对**顺对话惯性**的指令全部服从、对**反惯性**的 CC 重发
full number with the country code, like +966 5x xxx xxxx? Just so our manager
can reach you directly.",中文同 `_CC_NOTE_ZH` 正文),走既有 violated 替换
机制(logger.warning 留痕)
- **回显冲突裁决(2026-10-08 审查修复补记)**:触发轮若零号码墙先拦下回显句
(violation=="no_contact"),替换句改用 CC 模板句而非 NO_CONTACT_ALREADY——
GLM 有 3/3 回显倾向,该路径概率高,客户该收到追问而非"稍后联系"虚假承诺;
价格墙路径(violation=="boundary")维持原替换话术不变
- **计数**:`_cc_asked` 照常写回(一次性语义不变——无论 GLM 是否服从,本轮消耗)
- 客户配合重发完整号后:下轮号码带前缀 → CC 墙/note 均不再触发,确认句自然回归
- 客户不理睬:下轮 `_cc_asked` 已消耗 → 墙不再触发(一次性)
+4
View File
@@ -20,6 +20,10 @@ PATTERNS = [
(r"\d[\d,.]*\s*k\b(?!\w)", "25k形式"),
(r"\d+\s*[- ]?(day|week|month|d[ií]as?|semanas?|дн(?:ей|я)|недел[ьи]|"
r"أيام)s?\b", "交期/时间承诺"),
# Minor-4(审查修复): 号码回显漏放观测——与零号码墙 LONG_NUMBER_RE 同形
# ({8,17} 修复后口径),AI 回复中 ≥10 位数字串(含分隔/阿语数字形态)
(r"\+?\d[\d\s\-().]{8,17}\d", "号码回显(≥10位)"),
(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}", "邮箱回显"),
]
COMPILED = [(re.compile(p, re.I), name) for p, name in PATTERNS]
+16
View File
@@ -407,3 +407,19 @@ def test_no_contact_wall_spec_forms_still_pass():
assert not no_contact_value("DN110 PE100 pipes, 5000m total, 20ft container")
assert not no_contact_value("PN16 SDR11 to ISO4427, batches of 5000")
assert not no_contact_value("DN800双壁波纹管,5000米一批")
def test_no_contact_wall_ten_digit_number_blocked():
# Important-1(审查修复): 旧 {9,17} 最小匹配 11 字符,纯 10 位号穿透——
# 沙特裸号 0501234567 恰是 CC 模板范例市场本体。收窄 {8,17} 后命中。
assert no_contact_value("Got it — 0501234567!")
assert no_contact_value("my whatsapp is 0501234567")
# 千分位行为钉死(审查预估与实际正则的差异): ',' 不在 LONG_NUMBER_RE
# 字符类 [\d\s\-().] 内,"50,000,000" 逗号形态天然放行——与 "45,000 km"
# 放行同机制(既有误杀防线,不动);空格/横线分隔的 ≥10 位数字串命中。
# 若需求方要拦千分位大数量(MOQ 复述),显式把 ',' 加进 class 再扩测试。
assert not no_contact_value("MOQ is 50,000,000 for this grade")
assert no_contact_value("MOQ is 50 000 000 for this grade")
# 规格形态复测(修复后回归锚): 逗号千分位+短数字段仍全放行
assert not no_contact_value("DN110 PE100, 5000m per batch, 20ft container")
assert not no_contact_value("45,000 km, VIN LFV3A23C8K3123456")
+57
View File
@@ -1223,3 +1223,60 @@ def test_no_contact_wall_blocks_customer_email_echo(async_db, monkeypatch):
assert "a@b.com" not in tail
assert tail == NO_CONTACT_ALREADY
asyncio.run(manager.db.close())
# ---- 墙层审查修复(2026-10-08): CC轮回显closing/10位穿透/拼接分隔 ----
def test_cc_wall_no_contact_violation_uses_cc_template(async_db, monkeypatch):
"""Important-2: CC 触发轮回显被零号码墙拦下时,closing 改用 CC 模板句
而非 NO_CONTACT_ALREADY——GLM 有 3/3 回显倾向,该路径概率高,客户该
收到追问而非"稍后联系"虚假承诺。同时钉死 Important-1: 10 位裸号
回显(旧 {9,17} 穿透形态)确实被拦。"""
from app.services.conversation import _CC_WALL_EN
class EchoConfirmAI(FakeAI):
async def stream_chat(self, messages, context=""):
for token in ("Got it — 0501234567! ",
"Manager will contact you soon."):
yield token
(manager, conv), _ = _setup(async_db, monkeypatch,
ai=EchoConfirmAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(manager, conv.session_id, "my whatsapp is 0501234567"))
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert "0501234567" not in tail # 回显零残留(10 位穿透已修)
assert tail == _CC_WALL_EN # closing=CC 追问句,非"稍后联系"
profile = asyncio.run(manager._get_profile(conv.id))
assert (profile.raw_info or {}).get("_cc_asked") is True # 计数不受影响
asyncio.run(manager.db.close())
def test_violated_closing_separated_by_space(async_db, monkeypatch):
"""Minor-5: 中途句违规掐断时,已流出前缀与替换句之间以空格分隔
("...xxxx? Got it —"),客户所见与落库一致。"""
from app.services.boundary_filter import NO_CONTACT_WEB
class MidNumberAI(FakeAI):
async def stream_chat(self, messages, context=""):
for token in ("Great question! ", "Call us at +86 138 0013 8000 ",
"anytime."):
yield token
(manager, conv), _ = _setup(async_db, monkeypatch,
ai=MidNumberAI(extract={}),
source=ConversationSource.standalone)
events = []
async def run():
async for e in manager.stream_process_message(
conv.session_id, "how can I contact you?"):
events.append(e)
asyncio.run(run())
streamed = "".join(e.get("content", "") for e in events
if e["type"] == "stream_chunk")
assert streamed == "Great question! " + NO_CONTACT_WEB # 空格分隔钉死
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert tail == streamed # 落库=流出
asyncio.run(manager.db.close())