feat: CC强制墙——触发轮整段缓冲+流末country code判定+模板句整段替换

场景①三轮提示词修复 0/8 后的需求方决策(增补设计 §2): GLM 对反惯性的
CC 重发追问结构性无视,升级为输出侧确定性强制。

- 触发: 本轮 completion_marks == ["_cc_asked"](裸号在场且未问过)
- 流式无法撤回 → 触发轮跳过逐句流出,整段缓冲(缓冲期间价格/零号码/
  再索要三墙照常逐句检查,优先级不变);流末统一判定
- 完整回复含 "country code"(中文轮认"国家代码",与注入的逐字追问句
  同词)→ 零干预原样流出;不含 → 整段替换为 CC 模板句正文,logger.warning
  留痕
- 模板句与 _CC_NOTE_* 同源化: _CC_ASK_* 抽出为共享正文,_CC_WALL_EN
  加 "Got it — " 衔接头(_CC_WALL_ZH 与正文同句)——注入/替换两处
  逐字锚点不漂移
- _cc_asked 计数照常流前落库,一次性消耗不受替换影响
- 测试 5 项: 替换(EN/ZH)+计数落库 / 含 country code 零干预(EN/ZH) /
  非触发轮三形态(带前缀号/已耗/双渠道齐)原样流出

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-08 13:34:34 +08:00
parent 2e08ab22b6
commit 5f124e691b
2 changed files with 176 additions and 5 deletions
+128
View File
@@ -1051,3 +1051,131 @@ def test_contact_completion_marks_filtered_from_push(async_db, monkeypatch):
profile_out = log.request_payload["profile"]
assert not [k for k in profile_out if str(k).startswith("_")] # 内部键零泄漏
asyncio.run(manager.db.close())
# ---- CC 强制墙(2026-10-08 增补设计 §2: 场景①输出侧确定性强制) ----
# 前情: 三轮提示词修复后真实 GLM 验收 0/8——GLM 对"反惯性"的 CC 重发追问
# 结构性无视。墙层兜底: 触发轮整段缓冲,流末判 "country code" 在场与否。
class _ConfirmAI(FakeAI):
"""CC 触发轮 GLM 抗命典型形态: 纯确认句,零追问(生产验收复刻)。"""
async def stream_chat(self, messages, context=""):
for token in ("Sure! ", "Got it — our manager will reach out to you "
"shortly."):
yield token
_CONFIRM_REPLY = "Sure! Got it — our manager will reach out to you shortly."
def test_cc_wall_replaces_confirmation_reply(async_db, monkeypatch):
"""触发轮 + 纯确认句 → 客户所见/落库均为 CC 模板句正文,原句零残留;
_cc_asked 计数照常落库(一次性消耗不受替换影响)。"""
from app.services.conversation import _CC_WALL_EN
(manager, conv), _ = _setup(async_db, monkeypatch, ai=_ConfirmAI(extract={}),
source=ConversationSource.standalone)
events = []
async def run():
async for e in manager.stream_process_message(
conv.session_id, "my whatsapp is 0501234567"):
events.append(e)
asyncio.run(run())
streamed = "".join(e.get("content", "") for e in events
if e["type"] == "stream_chunk")
assert streamed == _CC_WALL_EN # 客户所见 = 模板句
assert "country code" in streamed # 追问语义在场
assert "reach out to you shortly" not in streamed # 原确认句零残留
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert tail == _CC_WALL_EN # 落库与流出一致
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_cc_wall_passes_through_asking_reply(async_db, monkeypatch):
"""触发轮 + 回复已含 "country code" → GLM 自发服从,零干预逐字节原样。"""
class ObedientAI(FakeAI):
async def stream_chat(self, messages, context=""):
for token in ("Could you send the full number with the country "
"code, like +966 5x xxx xxxx? ",
"Just so our manager can reach you directly."):
yield token
reply = ("Could you send the full number with the country code, "
"like +966 5x xxx xxxx? Just so our manager can reach you "
"directly.")
(manager, conv), _ = _setup(async_db, monkeypatch, ai=ObedientAI(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 tail == reply # 原样(缓冲后整段流出,不增不减一字)
asyncio.run(manager.db.close())
def test_cc_wall_passes_through_chinese_asking_reply(async_db, monkeypatch):
"""中文触发轮 + 回复含 "国家代码" → 同样视为服从,零干预
(检测词表含中文形态——ZH 轮 GLM 逐字复述的是中文追问句)。"""
class ObedientZhAI(FakeAI):
async def stream_chat(self, messages, context=""):
for token in ("麻烦重发一下带国家代码的完整号码呗(比如 "
"+966 5x xxx xxxx)?", "客户经理直接加你。"):
yield token
reply = ("麻烦重发一下带国家代码的完整号码呗(比如 +966 5x xxx xxxx)?"
"客户经理直接加你。")
(manager, conv), _ = _setup(async_db, monkeypatch,
ai=ObedientZhAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(manager, conv.session_id, "我的WhatsApp是0501234567"))
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert tail == reply
asyncio.run(manager.db.close())
def test_cc_wall_inactive_on_non_trigger_turns(async_db, monkeypatch):
"""非触发轮(带前缀号/_cc_asked 已耗/双渠道齐)→ 确认句原样流出。"""
from app.services.conversation import _CC_WALL_EN
# (a) 带前缀号: 首轮即留 +966… → 邮箱轻补轮,非 CC 触发
(m1, c1), _ = _setup(async_db, monkeypatch, ai=_ConfirmAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(m1, c1.session_id, "my whatsapp is +966501234567"))
t1 = [m["content"] for m in asyncio.run(m1._load_history(c1.id))
if m["role"] == "assistant"][-1]
assert t1 == _CONFIRM_REPLY
asyncio.run(m1.db.close())
# (b) _cc_asked 已耗: 首轮裸号(CC 轮被替换),次轮号码仍裸 → 邮箱轻补轮
(m2, c2), _ = _setup(async_db, monkeypatch, ai=_ConfirmAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(m2, c2.session_id, "my whatsapp is 0501234567"))
asyncio.run(_send(m2, c2.session_id, "so how about that pvc pipe?"))
tails = [m["content"] for m in asyncio.run(m2._load_history(c2.id))
if m["role"] == "assistant"]
assert tails[0] == _CC_WALL_EN # 前提: 第一轮确是 CC 触发轮(被墙替换)
assert tails[1] == _CONFIRM_REPLY # 第二轮墙不再触发 → 确认句原样
asyncio.run(m2.db.close())
# (c) 双渠道齐: 号码带前缀 + 邮箱 → 无任何轻补,墙关闭
(m3, c3), _ = _setup(async_db, monkeypatch, ai=_ConfirmAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(m3, c3.session_id,
"reach me on whatsapp +966501234567 or a@b.com"))
t3 = [m["content"] for m in asyncio.run(m3._load_history(c3.id))
if m["role"] == "assistant"][-1]
assert t3 == _CONFIRM_REPLY
asyncio.run(m3.db.close())
def test_cc_wall_chinese_template(async_db, monkeypatch):
"""中文触发轮 + 英文纯确认句 → 中文模板句正文替换(语言随本轮判定)。"""
from app.services.conversation import _CC_WALL_ZH
(manager, conv), _ = _setup(async_db, monkeypatch, ai=_ConfirmAI(extract={}),
source=ConversationSource.standalone)
asyncio.run(_send(manager, conv.session_id, "我的WhatsApp是0501234567"))
msgs = asyncio.run(manager._load_history(conv.id))
tail = [m["content"] for m in msgs if m["role"] == "assistant"][-1]
assert tail == _CC_WALL_ZH
assert "国家代码" in tail
asyncio.run(manager.db.close())