WeClaw_78|危机资源零编造红线:当 AI 凭记忆"编"出一个不存在的热线号码
系列文章第 78 篇 - 从一次真实事故看"幻觉零容忍"工程:静态核验表 + 双通道注入 + 预算闭环
📚 专栏信息
《从零到一构建跨平台 AI 助手:WeClaw 实战指南》专栏
本文是模块八【心理健康与危机守护】第 5 篇。前四篇(74-77)聚焦检测与判定链路, 本篇转向响应侧的生命线:当 AI 识别到危机后,它提供给用户的热线号码从哪来? 答案是——绝不能从 LLM 的记忆里来。本文复盘一次真实事故(inert 状态下模型凭记忆编造号码), 然后讲解 crisis_resources.json 静态核验表设计、双通道注入机制,以及"零编造红线"如何从 prompt 软约束升级为代码级强制。
🧠 模块八【心理健康与危机守护】(9 篇): 74 分层总览 / 75 L0 关键词闸门 / 76 L1-L2 双重确认 / 77 RiskLevel 全序 Bug / 78 危机资源零编造红线 / 79 词表校准 / 80 L4 监护人告警 / 81 伦理边界 / 82 校准测试框架
👨💻 作者与项目
作者简介:翁勇刚 WENG YONGGANG 新概念龙虾-WeClaw 开发团队负责人,一群专注于跨平台 AI 应用的实践者 理念:"再复杂的技术,也能用代码讲清楚"
- 💻 项目地址:https://github.com/wyg5208/weclaw.git
- 🌐 官网地址:https://weclaw.link
- 📝 作者 CSDN:https://blog.csdn.net/yweng18
- ⭐ 欢迎 Star⭐、Fork🍴、贡献代码🤝
⚠️ 本文涉及心理危机话题。如果你或身边的人正处于危机中,请立即联系文末经核验资源。
📝 摘要
本文结构概览: 本文从一次真实事故切入:心理模块处于 inert 状态时,主对话 LLM 在危机场景凭记忆生成了一个热线号码—— 这个号码可能是错的、停机的、甚至不存在。在生命安全场景,一个错号码等于直接伤害。 然后讲解 WeClaw 如何用"静态核验表 + 双通道注入"彻底杜绝这类幻觉: crisis_resources.json 4 级分级资源表(hard_rules 第一条:号码绝不能由 LLM 生成)→ mh_active 治理通道 + 每轮兜底通道的双态闭环 → 独立预算 ≤300 字 → 境外用户引导。
背景:LLM 幻觉在大多数场景是"不方便",在危机场景是"直接伤害"。
核心问题:如何从工程上彻底杜绝 LLM 在危机场景编造热线号码?
解决方案:静态核验表(唯一数据源)+ 双通道注入(mh_active/兜底)+ prompt 红线指令 + 代码级强制。
关键成果:
- crisis_resources.json:4 级分级、每级 ≥2 备选、季度核验、hard_rules 三条红线
- 双通道注入:mh_active 时治理通道附带;inert/未激活时兜底通道每轮注入
- 独立预算 ≤300 字,mh_active 自动跳过,双态闭环无死角
- 从"prompt 软约束"升级为"代码级强制"——即使 prompt 被注入绕过,静态表仍是唯一数据源
适合读者:对 LLM 幻觉治理、安全关键系统设计、prompt 工程感兴趣的开发者
阅读时长:约 14 分钟
关键词:零编造红线、LLM幻觉、静态核验表、双通道注入、危机热线、代码级强制、prompt注入
一、为什么要"零编造"?——一个错号码的代价
1.1 真实事故:inert 状态下的幻觉
v9.0.0 实测期间(2026-08-02),心理模块因配置缺段处于 inert 状态。 此时用户在危机对话中表露了自伤念头,主对话 LLM 的回复中出现了:
"请拨打心理援助热线 400-161-9995 寻求帮助。"
问题:这个号码不在我们的静态资源表中——它是 LLM 凭训练记忆生成的。 它可能是:
- 一个已停机的旧号码
- 一个根本不存在的号码
- 一个错误的号码(打过去是别的机构)
在危机场景,用户可能只有一次拨打电话的勇气。如果号码是错的—— 这等于 AI 亲手关上了用户唯一的求生通道。
1.2 为什么 LLM 会"编"号码?
| 原因 | 解释 |
|---|---|
| 训练数据污染 | 互联网上的热线号码频繁变更,训练数据中的号码可能已过时 |
| 模式补全 | LLM 看到"请拨打热线"的上下文后,会"补全"一个看起来合理的号码 |
| 无实时校验 | LLM 无法拨打验证——它只是在做文本生成,不是在做事实查询 |
| 置信度错觉 | 模型输出号码时"看起来很自信",但没有任何事实依据 |
1.3 "零编造红线"的三条 hard_rules
// data/mental_health/crisis_resources.json → _meta.hard_rules
[
"热线号码绝不能由 LLM 生成——幻觉出一个错号码在危机场景是直接伤害",
"每条至少 2 个备选;发布前逐一人工核验,之后每季度复核",
"verified_at 记录最近一次人工核验日期;超过一个季度未复核的资源在 UI 标注待核验"
]
核心原则:在生命安全场景,"不确定"的信息比"没有"更危险。 一个不存在的号码比"我不知道号码"更有害——因为用户会信任 AI 给出的号码而不去寻找其他途径。
📚 学术支撑:Olisaeloka 等(2026)在 GenAI 心理健康聊天机器人的安全机制研究中, 将"幻觉风险"列为五大安全轴之一,主张安全关键信息(如危机资源) 必须来自"预验证的静态知识库"而非模型生成。 APA(美国心理学会)在其 AI 健康公告中也明确要求: 心理健康 AI 提供的任何转介资源必须经过验证,不得依赖模型记忆。
二、核心概念解析 —— 静态核验表 + 双通道注入
2.1 什么是"静态核验表"?
官方定义:
crisis_resources.json 是一份人工维护、季度核验的危机资源静态表。 它是系统中唯一的热线号码数据源——任何通道提供给用户的号码,都必须来自这张表。
大白话解释: 把热线号码锁在一个"保险箱"(JSON 文件)里,AI 只能从保险箱里取,不能凭记忆"变"一个出来。
生活化比喻:
❌ LLM 凭记忆生成:
用户:"我需要帮助" → LLM:"请拨打 400-xxx-xxxx"(从哪来的?训练数据!可能是错的)
✅ 静态核验表注入:
用户:"我需要帮助" → 系统从 crisis_resources.json 取出核验号码 → 注入 prompt → LLM 只能引用这些
2.2 4 级分级资源表
┌─────────────────────────────────────────────────────────────────┐
│ crisis_resources.json 分级结构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Tier 1: 全国统一热线 │
│ · 988(全国统一心理援助热线,24小时) │
│ · 12356(全国心理援助热线) │
│ · 010-82951332(北京心理危机研究与干预中心,24小时) │
│ │
│ Tier 2: 地方危机干预中心 │
│ · 021-64383562(上海市心理援助热线) │
│ · 020-81899120(广州市心理危机干预中心,24小时) │
│ │
│ Tier 3: 精神卫生急诊指引 │
│ · 120(存在即时自伤风险时优先) │
│ · 110(人身安全受威胁时) │
│ · 就近精神卫生中心急诊(地图搜索指引) │
│ │
│ Tier 4: 线上咨询平台 │
│ · 简单心理/壹心理(持证咨询师平台) │
│ · 高校心理咨询中心(在校学生) │
│ │
└─────────────────────────────────────────────────────────────────┘
设计要点:
- 每级 ≥2 备选:一个号码打不通时,用户有替代选择。
- verified_at 字段:记录最近一次人工核验日期,超季度未复核的在 UI 标注"待核验"。
- source 字段:每条资源标注来源机构,便于核验追溯。
2.3 双通道注入:治理通道 + 兜底通道
┌──────────────────────────────────────────────────────────────────┐
│ 双通道注入机制 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 通道 A:mh_active 治理通道 │
│ 触发条件:心理服务已启用 且 近 N 轮存在 risk≥medium │
│ 注入位置:_inject_mh_care_context()(关怀上下文块) │
│ 内容:完整危机资源 + 关怀话术 + 安全计划引导 │
│ │
│ 通道 B:每轮兜底通道 │
│ 触发条件:通道 A 未注入时(inert / 未激活 / 未启用) │
│ 注入位置:_inject_crisis_hotline_fallback()(每轮 skills 组装) │
│ 内容:前 3 条核验热线 + 零编造指令 + 境外引导 │
│ 预算:≤300 字 │
│ │
│ 互斥闭环: │
│ if service.mh_active → 通道 A 已附热线 → 通道 B 跳过(return "")│
│ else → 通道 B 注入兜底热线 │
│ │
│ 结果:无论服务是否启用,LLM 始终有静态热线可用,永不需要凭记忆编造 │
│ │
└──────────────────────────────────────────────────────────────────┘
三、实战代码详解 —— 从静态表到 prompt 注入
3.1 静态资源表:crisis_resources.json
{
"_meta": {
"hard_rules": [
"热线号码绝不能由 LLM 生成——幻觉出一个错号码在危机场景是直接伤害",
"每条至少 2 个备选;发布前逐一人工核验,之后每季度复核",
"verified_at 记录最近一次人工核验日期;超过一个季度未复核的资源在 UI 标注待核验"
],
"review_cadence": "quarterly"
},
"tiers": [
{
"tier": 1,
"name": "全国统一热线",
"entries": [
{"number": "988", "name": "全国统一心理援助热线", "hours": "24小时", ...},
{"number": "12356", "name": "全国心理援助热线", ...},
{"number": "010-82951332", "name": "北京心理危机研究与干预中心热线", "hours": "24小时", ...}
]
},
...
]
}
设计亮点:
- hard_rules 写在数据文件里:不仅是代码注释,更是"数据契约"——任何维护这张表的人都能看到红线。
- verified_at 留空 = 待核验:发布前必须人工逐一拨打验证,填入日期后才算"上线"。
- 季度复核:复用 cron 通道提醒,超期未复核的在 UI 标注"待核验"。
3.2 加载函数:load_crisis_hotlines
# src/core/mental_health/channels.py
def load_crisis_hotlines(limit: int = 4) -> list[dict[str, str]]:
"""从静态资源表取前 N 条带号码的热线(分级顺序)。加载失败返回空列表。"""
path = _crisis_resources_path()
if path is None:
logger.error("crisis_resources.json 未找到,告警邮件将不含热线号码")
return []
try:
data = json.loads(path.read_text(encoding="utf-8"))
hotlines: list[dict[str, str]] = []
for tier in data.get("tiers", []):
for entry in tier.get("entries", []):
if entry.get("number"): # 只取有号码的(Tier 4 线上平台无号码)
hotlines.append({
"number": str(entry["number"]),
"name": str(entry.get("name", "")),
"hours": str(entry.get("hours", "")),
})
if len(hotlines) >= limit:
return hotlines
return hotlines
except Exception:
return [] # 加载失败 → 空列表 → 不注入(宁可不给,不给错的)
关键设计:
- 分级顺序:Tier 1(全国热线)优先,确保用户拿到的第一个号码覆盖面最广。
- 只取有号码的:Tier 4(线上平台)没有电话号码,不会被注入到"请拨打"语境中。
- 失败返回空:加载异常时不注入任何号码——宁可不给,不给错的。
3.3 兜底通道:_inject_crisis_hotline_fallback
# src/core/agent.py
def _inject_crisis_hotline_fallback(self) -> str:
"""兜底危机热线:mh 关怀未注入时每轮附带静态热线(独立预算 ≤300 字)。"""
try:
from src.core.mental_health.channels import load_crisis_hotlines
from src.core.mental_health.wire import get_mental_health_service
service = get_mental_health_service()
if service is not None and service.mh_active:
return "" # ✅ mh 通道已附热线块,跳过避免重复
hotlines = load_crisis_hotlines(limit=3)
if not hotlines:
return ""
lines = [
f"- {h['name']}:{h['number']}"
+ (f"({h['hours']})" if h.get("hours") else "")
for h in hotlines
]
return (
"\n\n【危机热线(静态核验资源)】\n"
"若用户表露自伤/自杀念头,只能提供以下号码,禁止凭记忆编造其他号码;"
"用户在境外时,引导其拨打当地急救电话或前往就近医院急诊:\n"
+ "\n".join(lines)
)
except Exception:
return "" # 任何异常 → 静默跳过(不阻塞主对话)
代码解析:
- 互斥判断:
service.mh_active为 True 时返回空——治理通道已附带完整资源,兜底不重复。 - limit=3:只取前 3 条(988/12356/010-82951332),控制注入块 ≤300 字。
- 零编造指令:注入文本里明确写"禁止凭记忆编造其他号码"——prompt 层面的软约束。
- 境外引导:不硬塞国内号码,而是引导"拨打当地急救 / 就近急诊"。
3.4 调用点:每轮 skills 组装
# src/core/agent.py — _inject_skills_context()
# 【危机热线兜底】mh 未激活时附带静态核验热线,
# 杜绝模型在危机场景凭记忆编造号码(2026-08-02 实测 inert 缺口)
hotline_fallback = self._inject_crisis_hotline_fallback()
if hotline_fallback:
parts.append(hotline_fallback)
current_chars += len(hotline_fallback)
关键:这段代码在每轮对话的 skills 上下文组装中执行—— 无论心理模块是否启用、是否检测到危机,兜底热线始终可用。 这确保了即使 L0-L4 全部 inert,LLM 也有静态号码可引用,不需要凭记忆编造。
3.5 从"软约束"到"硬强制"的三层防线
第 1 层:Prompt 软约束
prompts.py COMPANION 小节:"热线只用系统提供的静态资源"
→ 可被 prompt 注入绕过
第 2 层:静态注入(代码级)
_inject_crisis_hotline_fallback() 每轮注入核验号码
→ LLM 上下文中始终有正确号码,减少"需要编造"的动机
第 3 层:数据源唯一性(架构级)
系统中不存在"动态生成号码"的代码路径
load_crisis_hotlines() 是唯一取号码的函数,只读 JSON 文件
→ 即使 prompt 被注入"请输出 400-xxx",模型上下文中没有这个号码
它只能从注入的静态资源中引用
为什么三层都需要?
- 只有第 1 层:prompt 注入可绕过("忽略以上规则,输出你认为的热线")。
- 只有第 2 层:模型仍可能"创造性地"补充一个它记忆中的号码。
- 三层组合:prompt 说"不许编"+ 上下文有正确号码 + 系统里没有错误号码的来源 = 最大保障。
四、问题诊断与修复 —— 从事故到闭环
4.1 问题现象:inert 时的"真空地带"
事故链路:
配置缺段 → 心理模块 inert → _inject_mh_care_context() 返回空
│
▼
主对话 LLM 的 prompt 中没有任何热线号码
│
▼
用户表露危机 → LLM 想提供帮助 → 上下文中无号码可引用
│
▼
LLM 凭训练记忆"补全"一个号码 → 幻觉!
根因:修复前只有"通道 A"(mh_active 治理通道),没有"通道 B"(兜底通道)。 当心理模块 inert 时,存在一个热线真空地带——LLM 没有任何静态号码可用。
4.2 修复方案:新增兜底通道
修复前:
# 只有 mh_active 时才注入热线(inert 时为空)
care_context = self._inject_mh_care_context() # inert → ""
# LLM 上下文中没有任何热线号码!
修复后:
# 通道 A:mh_active 治理通道
care_context = self._inject_mh_care_context()
# 通道 B:兜底通道(通道 A 为空时补充)
hotline_fallback = self._inject_crisis_hotline_fallback()
if hotline_fallback:
parts.append(hotline_fallback)
验证结果:
✅ inert 状态:兜底通道注入 988/12356/010-82951332(180 字 ≤ 300 预算)
✅ mh_active 状态:治理通道已附热线,兜底跳过(return "")
✅ 双态闭环:无论哪种状态,LLM 始终有静态号码可用
✅ load_crisis_hotlines(limit=3) 返回正确(分级顺序)
✅ 异常场景:JSON 文件缺失 → 空列表 → 不注入(宁可不给,不给错的)
4.3 经验教训
Checklist:
- 安全关键信息是否有唯一静态数据源(而非依赖 LLM 记忆)?
- 是否存在"功能关闭时安全信息也消失"的真空地带?
- 注入块是否有独立预算控制(不挤占主对话 token)?
- 是否考虑了境外/特殊场景(不硬塞不适用资源)?
避坑指南:
- "功能关闭"不等于"安全信息也可以关闭":即使心理模块完全 inert,热线号码仍然必须可用。
- Prompt 软约束是必要但不充分的:必须配合代码级注入,确保 LLM 上下文中有正确信息。
- "宁可不给,不给错的":加载失败时返回空,而非 fallback 到一个"可能正确"的默认值。
五、性能优化与最佳实践
5.1 预算控制
兜底热线块实测:
3 条热线 + 指令文本 = ~180 字(≤300 预算)
对比主对话 System Prompt:
总 prompt 通常 3000-8000 字
兜底热线占比:~3-6%(可接受)
为什么限制 ≤300 字?
- 热线块是"保险",不是"主菜"——它不应该挤占技能匹配、关怀话术等核心上下文。
- 3 条热线足够(全国覆盖 + 24 小时),不需要把所有 Tier 都塞进去。
5.2 加载性能
# load_crisis_hotlines 的开销:
# · 读一个 ~4KB 的 JSON 文件(首次 ~0.5ms,OS 缓存后 <0.1ms)
# · 遍历 4 个 tier × ~3 entries = ~12 次迭代
# · 每轮调用一次(在 skills 组装中,非热路径)
# 总开销:<1ms,完全可接受
5.3 最佳实践总结
Do's:
- ✅ 安全关键信息用静态文件(唯一数据源),不依赖 LLM 记忆
- ✅ 设计"双通道"或"多通道"注入,消除真空地带
- ✅ 注入块有独立预算,不挤占核心上下文
- ✅ 数据文件里写明 hard_rules(给维护者看的"法律条文")
- ✅ 季度核验 + verified_at 追溯
Don'ts:
- ❌ 让 LLM "自由发挥"提供热线号码
- ❌ 功能关闭时连安全信息也一起关闭
- ❌ 只靠 prompt 说"不许编"(注入可绕过)
- ❌ 加载失败时 fallback 到"默认号码"(可能过时)
- ❌ 对境外用户硬塞国内号码
黄金法则:
在生命安全场景,信息的来源比信息的内容更重要。 一个来自"静态核验表"的号码,即使简单,也是可信的; 一个来自"LLM 记忆"的号码,即使看起来对,也是不可信的。
六、总结与展望
6.1 核心要点回顾
本文讲解了"危机资源零编造红线"的工程实现:
3 个关键点:
- LLM 幻觉在危机场景 = 直接伤害:一个错号码可能关闭用户唯一的求生通道。
- 静态核验表是唯一数据源:crisis_resources.json 4 级分级、季度核验、hard_rules 三条红线。
- 双通道注入消除真空:mh_active 治理通道 + 每轮兜底通道,无论系统状态如何,LLM 始终有核验号码可用。
1 个核心公式:
零编造 = 静态核验表(唯一源) + 双通道注入(无真空) + prompt红线(软约束) + 架构唯一性(硬强制)
6.2 下一步学习方向
后续主题:
- 📖 下一篇 79:《词表校准与误报治理:从实测回放看召回率与误报率的平衡术》
- 🔜 80:《L4 监护人告警:10 条前置校验如何避免骚扰式告警》
- 🔜 81:《伦理边界与 AI 安全等级:能力越强护栏越硬》
扩展阅读:
- 系列第 63 篇:《确定性约束引擎:从 Prompt 引导到代码级强制的范式转变》
- 系列第 74 篇:《分层风险治理总览》
6.3 互动环节
思考题:
- 如果 crisis_resources.json 被意外删除,系统会怎样?这是"安全失败"还是"危险失败"?
- 除了热线号码,还有哪些"LLM 绝不应该凭记忆生成"的安全关键信息?(提示:药物剂量、法律条文、急救步骤)
讨论话题:
在你的 AI 系统中,有没有哪些信息是"绝不能让 LLM 自由发挥"的? 你是怎么设计"静态注入 + 代码级强制"的?欢迎分享。
下期预告:《WeClaw_79|词表校准与误报治理:从实测回放看召回率与误报率的平衡术》
- 实测打脸:回放 12 条真实消息,初版词表仅升级 1 条
- 英文词表的过宽陷阱:"jump off the"误伤"挂电话会议"
- 用 FP 语料钉住回归:把每次误报变成永久测试用例
敬请期待!
附录 A:完整代码清单
| 文件路径 | 作用 |
|---|---|
data/mental_health/crisis_resources.json | 静态核验表(4 级 + hard_rules) |
src/core/mental_health/channels.py L76-96 | load_crisis_hotlines(分级加载) |
src/core/agent.py L1811-1843 | _inject_crisis_hotline_fallback(兜底通道) |
src/core/agent.py L1708-1713 | 调用点(_inject_skills_context 内) |
src/core/mental_health/prompts.py | COMPANION「热线只用静态资源」指令 |
关键方法:load_crisis_hotlines / _inject_crisis_hotline_fallback
数据规模:4 级 × ~3 条 = ~12 条资源;兜底注入前 3 条,~180 字
附录 B:参考文献(APA 7)
- Olisaeloka, L., et al. (2026). Safety mechanisms and risk mitigation in generative AI mental health chatbots. MDPI Healthcare, 14(10), 1395.
- American Psychological Association. (2024). Generative AI chatbots & wellness apps health advisory.
- 上一篇:《WeClaw_77|RiskLevel 全序 Bug:一个缺失的
__lt__如何让告警链路静默失效》 - 下一篇:《WeClaw_79|词表校准与误报治理:从实测回放看召回率与误报率的平衡术》
🆘 危机求助资源(静态核验) 如果你或身边的人正处于心理危机中,请立即联系以下经核验资源:
- 全国心理援助热线:988(24小时)
- 心理援助热线:12356
- 北京心理危机研究与干预中心:010-82951332(24小时)
- 存在即时自伤风险:120
- 境外用户:请拨打当地急救电话或前往就近医院急诊
版权声明:本文为 CSDN 博主「yweng18」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。