返回博客列表
最佳实践2026-08-0629 分钟阅读

危机资源零编造红线:当 AI 凭记忆"编"出一个不存在的热线号码

从一次真实事故看"幻觉零容忍"工程:静态核验表 + 双通道注入 + 预算闭环

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 应用的实践者 理念:"再复杂的技术,也能用代码讲清楚"

⚠️ 本文涉及心理危机话题。如果你或身边的人正处于危机中,请立即联系文末经核验资源。


📝 摘要

本文结构概览: 本文从一次真实事故切入:心理模块处于 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小时", ...}
      ]
    },
    ...
  ]
}

设计亮点

  1. hard_rules 写在数据文件里:不仅是代码注释,更是"数据契约"——任何维护这张表的人都能看到红线。
  2. verified_at 留空 = 待核验:发布前必须人工逐一拨打验证,填入日期后才算"上线"。
  3. 季度复核:复用 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)?
  • 是否考虑了境外/特殊场景(不硬塞不适用资源)?

避坑指南

  1. "功能关闭"不等于"安全信息也可以关闭":即使心理模块完全 inert,热线号码仍然必须可用。
  2. Prompt 软约束是必要但不充分的:必须配合代码级注入,确保 LLM 上下文中有正确信息。
  3. "宁可不给,不给错的":加载失败时返回空,而非 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 个关键点

  1. LLM 幻觉在危机场景 = 直接伤害:一个错号码可能关闭用户唯一的求生通道。
  2. 静态核验表是唯一数据源:crisis_resources.json 4 级分级、季度核验、hard_rules 三条红线。
  3. 双通道注入消除真空:mh_active 治理通道 + 每轮兜底通道,无论系统状态如何,LLM 始终有核验号码可用。

1 个核心公式

零编造 = 静态核验表(唯一源) + 双通道注入(无真空) + prompt红线(软约束) + 架构唯一性(硬强制)

6.2 下一步学习方向

后续主题

  • 📖 下一篇 79:《词表校准与误报治理:从实测回放看召回率与误报率的平衡术》
  • 🔜 80:《L4 监护人告警:10 条前置校验如何避免骚扰式告警》
  • 🔜 81:《伦理边界与 AI 安全等级:能力越强护栏越硬》

扩展阅读

  • 系列第 63 篇:《确定性约束引擎:从 Prompt 引导到代码级强制的范式转变》
  • 系列第 74 篇:《分层风险治理总览》

6.3 互动环节

思考题

  1. 如果 crisis_resources.json 被意外删除,系统会怎样?这是"安全失败"还是"危险失败"?
  2. 除了热线号码,还有哪些"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-96load_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.pyCOMPANION「热线只用静态资源」指令

关键方法load_crisis_hotlines / _inject_crisis_hotline_fallback 数据规模:4 级 × ~3 条 = ~12 条资源;兜底注入前 3 条,~180 字


附录 B:参考文献(APA 7)

  1. Olisaeloka, L., et al. (2026). Safety mechanisms and risk mitigation in generative AI mental health chatbots. MDPI Healthcare, 14(10), 1395.
  2. American Psychological Association. (2024). Generative AI chatbots & wellness apps health advisory.
  3. 上一篇:《WeClaw_77|RiskLevel 全序 Bug:一个缺失的 __lt__ 如何让告警链路静默失效》
  4. 下一篇:《WeClaw_79|词表校准与误报治理:从实测回放看召回率与误报率的平衡术》

🆘 危机求助资源(静态核验) 如果你或身边的人正处于心理危机中,请立即联系以下经核验资源:

  • 全国心理援助热线:988(24小时)
  • 心理援助热线:12356
  • 北京心理危机研究与干预中心:010-82951332(24小时)
  • 存在即时自伤风险:120
  • 境外用户:请拨打当地急救电话或前往就近医院急诊

版权声明:本文为 CSDN 博主「yweng18」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。

原文链接https://blog.csdn.net/yweng18