从 AI 幻觉到重试:体检报告 AI 的几个工程坑

这篇写一个我参与并维护过的体检报告 AI 流水线复盘。场景来自某年服务数十万体检场次的连锁体检机构客户,覆盖智能推项、科普、建议、矛盾识别 4 类生成任务。

我不会把它写成"一个大模型替医生写报告"的故事。真实落地里,模型生成只是中间一步,更麻烦的是:项目编码不能编,医学表述不能越界,建议风格要像机构医生写的,矛盾识别还要避开大量医学上可共存的"假冲突"。

一开始踩到的坑:模型给的答案太像答案了

智能推项最容易让人误判。用户画像、问卷、基础体检项目给进去,LLM 很容易生成一段看起来合理的推荐理由,再附几个检查项目。

问题是,客户要的不是"听起来合理"的项目名,而是本院真实存在、可挂、可计费、可展示的项目。模型一旦编出不存在的 projectIdprojectCode 或价格,这个错误就可能进入报告或前端展示。

所以链路没有让模型一步到位,而是拆成了几段:思考、抽取、检索、映射、质检。

flowchart TD A[用户画像/问卷/基础项目] --> B[LLM 生成推荐思路] B --> C[抽取候选项目名] C --> D[并发检索内部知识库/本院项目] D --> E[LLM 映射到本院项目字段] E --> F{quality_control 反查} F -->|匹配 projectId/code/name| G[保留项目字段] F -->|对不上 relation_items| H[清空编码/价格等关键字段] G --> I[报告/前端消费] H --> I

这张图的重点是最后一道 quality_control,它不是修饰性校验,而是防止幻觉透传的闸门。

为什么不直接让模型输出结构化推荐

当时也考虑过更短的链路:prompt 写清楚字段,让模型直接输出最终 JSON。这个方案开发体验更舒服,但问题集中在两个地方。

一个是长 prompt 会稀释注意力。客户业务规则、基础项目、用户画像、专项风险、输出格式都塞进去后,模型不一定能稳定照顾到每个约束。

另一个是 project 映射本质上不是"语言理解题",而是"本院库对齐题"。模型知道"低剂量胸部 CT"这个概念,不代表它知道客户库里对应哪个项目、编码是什么、价格是不是可展示。

脱敏后的调试现象大概是这样:项目名看起来对,projectId 回到 relation_items 里却查不到;有时模型还会把价格字段补得像真的一样,但这个价格并没有来自本院库。这里不能靠"看起来合理"放行,只能回查。

最终更稳的做法是:让模型负责推理和粗映射,让系统负责反查和兜底。

python 复制代码
def quality_control(model_items, relation_items):
    checked = []
    for item in model_items:
        hit = find_in_relation_items(
            relation_items,
            project_id=item.get("projectId"),
            project_code=item.get("projectCode"),
            project_name=item.get("projectName"),
        )

        if not hit:
            item["projectId"] = None
            item["projectCode"] = None
            item["projectPrice"] = None

        checked.append(item)
    return checked

这段伪代码解决的是 project 字段幻觉问题:宁可少展示价格和编码,也不要把模型编出来的字段交给下游。

专项筛查也走了类似的工程路线。系统维护二十余类专项筛查,其中 3 类因为客户业务不覆盖被注释禁用;运行时用 ThreadPoolExecutor(max_workers=20) 并发判断风险,再合并推荐项目。这个做法不优雅,规则表也要维护,但它比一个超长 prompt 更容易定位问题。

Schema 要小,边界要硬

推项侧的输出 schema 可以抽象成这样:

json 复制代码
{
  "recommendReason": "根据问卷和体检基础项目生成的脱敏推荐理由",
  "items": [
    {
      "projectName": "本院项目名称",
      "projectId": "可为空;必须能被本院库反查",
      "projectCode": "可为空;必须能被本院库反查",
      "projectPrice": "可为空;禁止模型自行编造",
      "matchedSource": "internal_relation_items"
    }
  ],
  "quality_control": {
    "unmatchedPolicy": "clear_id_code_price",
    "retry": 3
  }
}

这个 schema 示例只保留关键边界:字段可以为空,但不能假装确定;价格、编码这类业务字段必须来自本院库反查。

科普和建议侧的 schema 更关注表达约束。科普有 5 条强禁词,避免对恶性风险、寿命影响作承诺式或过度安抚式表达。建议侧则被限制在 3 类:专科就诊、进一步检查、复查;并且一条建议只能归属一个科室,排除急诊科,复查时间用阿拉伯数字。

这里我更倾向规则集,而不是 few-shot。few-shot 看起来省事,但在医疗文书里容易把示例里的疾病、部位、数值也带偏。风格问题应该拆成规则,比如"肺结节,建议 1 个月复查",而不是让模型模仿一堆医生句子。

矛盾识别:JSON 失败比你想得更烦

矛盾识别分成科间、科间性别、科内、历史 4 路,其中历史分支后来因为长上下文注意力稀释被禁用。服务层总并发是 max_workers=8,核心链路是三段式:

  1. 自然语言分析,先找可能冲突。
  2. 结构化 JSON 提取,方便系统消费。
  3. 二次审核和豁免,把假矛盾排出去。

JSON 阶段做了最多 5 次重试。每次失败时,不是简单重问,而是把上一轮模型输出作为 assistant 消息放回上下文,让模型基于自己的错误继续修。

python 复制代码
def extract_json_with_retry(messages, max_retry=5):
    history = list(messages)
    for attempt in range(max_retry):
        output = call_llm(history, temperature=0, seed=42)
        parsed = try_parse_required_json(output)
        if parsed.ok:
            return parsed.value

        history.append({"role": "assistant", "content": output})
        history.append({
            "role": "user",
            "content": "上一次输出不是可解析 JSON。请保留医学判断,只修正 JSON 字段和格式。"
        })

    return {"items": [], "error": "json_parse_failed"}

这段伪代码解决的是结构化输出不稳定:代价是最坏情况下多次调用,收益是在当时模型和 schema 工具条件有限时,让链路先可用。

我不觉得这是一种漂亮方案。它更像工程现场里的止血贴:知道它重,但也知道没有它时,下游拿不到稳定结构。

失败样例不是噪音,是规则来源

矛盾识别里最有价值的不是 prompt 写得多长,而是把真实误报沉淀成豁免规则。系统里保留了 13 个具名假矛盾豁免案例。下面是脱敏简化后的模式级示例:

失败类型 脱敏简化样例 为什么错 处理策略
复测规则误判 原始血压异常,某次复测正常,小结写血压正常 体检业务里只要一次复测正常就可按正常处理,不能只盯原始值 在分析 prompt、二次审核和错误示例里写入血压复测规则
医学可共存差异 肛周皮赘样肿物,小结写外痔 两种描述可能指向同一类体检发现,不应按文本不一致判冲突 加入假矛盾豁免案例,由二次审核识别
附带发现误判 头颅 CT 未见明显异常,同时出现筛窦附带发现 "未见明显异常"可能针对主检查范围,不代表所有附带描述都冲突 将附带发现类情况放进豁免清单
文书风格不专业 输出"是肺结节,于一个月复查" 医学含义接近,但不像机构报告建议 用规则限制表达:阿拉伯数字、短句、复查区间取最短

这张表不是为了展示"模型错得离谱",而是提醒自己:医疗场景的错误经常不是常识题,而是业务规则题。

Coze 退到检索类子任务

项目早期试过用 Coze 承接更完整的工作流编排,后来退回到"主推理 vLLM + Coze 检索类子工作流"的组合。当时主推理模型是 GPT-OSS 120B,vLLM 里 alias 为 gemma

退回来的原因不是 Coze 不能用,而是复杂度位置不对。这个业务复杂度主要在推荐贴合度、医学误报、输出边界和客户规则上。把这些都塞进工作流平台,调试边界会变得更厚。后续客户内网部署时,Coze 代码节点沙盒冷启动约 3 秒/次,也说明这类节点不适合放在高频主链路上。

我的判断是:检索类、低频、边界清晰的子任务可以交给工作流;强规则、高频、要反查和重试的主链路,还是放在服务代码里更容易兜底。

几个留下来的判断

体检报告 AI 不应该追求"一次生成完美答案"。更现实的做法是把模型放在它擅长的位置:理解、改写、候选生成、粗判断;把确定性边界交给工程:反查、清空、禁词、schema、重试、豁免。

以参与和维护者的视角看这类系统时,我最大的感受是:别急着把 prompt 写长。先问清楚哪些字段绝对不能错,哪些输出宁可为空,哪些规则必须后置校验。只要这些边界没立住,模型越会写,风险越像真的。

如果这篇复盘对你有帮助,欢迎点赞收藏。

相关推荐
阳明山水1 小时前
自下而上 vs 自上而下 vs 最优组合预测策略解析
大数据·人工智能·深度学习·算法·机器学习
FPC_小西1 小时前
LDO 低压差线性稳压器 拆解电源稳压核心原理
人工智能·单片机·嵌入式硬件·集成学习·pcb工艺·hdi高密度互联
长空任鸟飞_阿康2 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python
硅谷秋水2 小时前
ProDrive:基于自身-环境协同演化的自动驾驶主动规划
人工智能·深度学习·机器学习·计算机视觉·自动驾驶
2601_959982212 小时前
信息科技正在重塑企业竞争力 AI时代的软件开发与数字化转型
人工智能·科技
lauo2 小时前
当手机开始“编程”:荣耀Robot Phone的影像革命与ibbot青春版的AI“挖矿”之道
大数据·人工智能·chatgpt·智能手机·ai-native
Coffeeee2 小时前
不能用公司的打包机,AI帮我实现了一套比打包机更好用的Android包构建/分发流程
android·人工智能·ai编程
江畔柳前堤2 小时前
github实战指南00-命令在哪里执行?
人工智能·线性代数·oracle·数据挖掘·github·word
不爱土豆唯爱马铃薯2 小时前
MC-032 | Git机器人monkeycode-ai自动Review和实现需求
人工智能