Agent+检验解读:怎样做成风险提示助手,而不是越界诊断
检验报告解读类 Agent 很容易从“提示异常和建议复查”滑向“判断疾病、给出治疗建议”。本文只讨论技术架构和工程流程示例,不提供诊断、治疗、分诊或用药建议;文中的阈值、风险分层、升级规则均为示例规则,真实项目必须由医疗专业人员和机构规范确认。
问题背景:Agent 为什么容易越界
在检验报告场景里,用户通常上传一组指标,希望系统解释“有没有问题”“严重不严重”“下一步怎么办”。如果直接把结构化指标丢给 LLM,模型很可能生成诊断性表达,例如“考虑某疾病”“建议使用某药物”等,这在产品边界、合规审计和用户安全上都不可接受。
工程上要解决的不是“让模型更会医学”,而是把 Agent 约束为一个风险提示助手。它只做三件事:识别报告中的异常项,基于可配置规则生成风险提示,输出复查或咨询专业人员的建议。它不做疾病判断,不给药物方案,不替代医生决策。
这类系统的关键不是单点 Prompt,而是“规则引擎 + LLM 模板 + 输出校验 + 审计日志”的闭环。
技术目标和边界定义
本文示例面向 Python、FastAPI、规则引擎和 LLM API 的后端实现。目标是构建一个可回溯、可配置、可拦截的检验报告风险提示服务。
建议先把输出边界写成机器可执行的策略,而不是只写在产品文档里:
- 允许输出:异常项名称、检测值、参考范围、风险提示、复查建议、就医咨询提醒
- 禁止输出:疾病诊断、治疗方案、药物建议、分诊结论、确定性因果判断
- 必须声明:结果仅为技术系统生成的风险提示,不能替代医疗专业判断
- 必须记录:输入指标、命中的示例规则、LLM 原始输出、最终输出、拦截原因
在真实项目中,参考范围、阈值和提示等级需要来自机构确认的规则库。代码里的规则只用于演示工程结构。
方案概览:把 Agent 拆成四层
一个相对稳妥的架构可以拆成以下流程:
字段标准化负责把“白细胞”“WBC”“白细胞计数”等映射到统一编码。规则引擎只判断“是否触发提示”,不生成诊断。LLM 只负责把规则结果改写成用户可读文本。边界校验器负责拦截越界内容,必要时退回模板输出。
这样设计的重点是:LLM 不是事实来源,也不是最终裁判。最终输出必须被规则和校验器约束。
数据结构:先把报告输入结构化
实际业务中,检验报告可能来自 OCR、PDF 解析或接口数据。本文从结构化 JSON 开始,避免把重点放到文档解析上。
一个最小输入可以这样定义:
{"report_id":"rpt_001","items":[{"code":"WBC","name":"白细胞计数","value":12.1,"unit":"10^9/L","ref_low":3.5,"ref_high":9.5},{"code":"ALT","name":"丙氨酸氨基转移酶","value":68,"unit":"U/L","ref_low":7,"ref_high":40}]}注意这里没有年龄、性别、病史等上下文,因此系统不能推导诊断。即使补充更多上下文,也应按照机构规范明确允许的输出范围。
核心实现:规则命中、LLM 改写和边界拦截
下面是一个简化版 FastAPI 服务。它演示了三件事:用示例规则生成风险提示,用 Prompt 限制 LLM 角色,再用关键词和结构校验拦截越界表达。
fromfastapiimportFastAPIfrompydanticimportBaseModelfromtypingimportList,Optionalfromdatetimeimportdatetimeimportreimportuuid app=FastAPI(title="Lab Risk Hint Agent Demo")classLabItem(BaseModel):code:strname:strvalue:floatunit:strref_low:Optional[float]=Noneref_high:Optional[float]=NoneclassLabReport(BaseModel):report_id:stritems:List[LabItem]classRuleHit(BaseModel):code:strname:strlevel:strmessage:strFORBIDDEN_PATTERNS=[r"诊断为",r"确诊",r"考虑.*病",r"建议使用.*药",r"服用",r"治疗方案",r"无需就医",r"立即手术"]defrun_rules(items:List[LabItem])->List[RuleHit]:hits=[]foriteminitems:ifitem.ref_lowisNoneoritem.ref_highisNone:continueifitem.value>item.ref_high:hits.append(RuleHit(code=item.code,name=item.name,level="attention",message=f"{item.name}高于本次报告参考上限,建议结合近期状态按机构规则复查或咨询专业人员。"))ifitem.value<item.ref_low:hits.append(RuleHit(code=item.code,name=item.name,level="attention",message=f"{item.name}低于本次报告参考下限,建议结合近期状态按机构规则复查或咨询专业人员。"))returnhitsdefbuild_prompt(report:LabReport,hits:List[RuleHit])->str:hit_text="\n".join([f"-{h.name}:{h.message}"forhinhits])returnf""" 你是检验报告风险提示助手,不是医生。 只能基于已命中的规则输出风险提示和复查建议。 禁止输出疾病诊断、治疗方案、药物建议、分诊结论。 必须包含“不能替代医疗专业判断”的说明。 报告ID:{report.report_id}规则命中:{hit_text}请用三段输出: 1. 异常项概览 2. 风险提示 3. 复查或咨询建议 """defmock_llm_call(prompt:str)->str:return("异常项概览:本次报告存在部分指标超出参考范围。\n""风险提示:白细胞计数和丙氨酸氨基转移酶需关注,建议结合近期状态观察变化。\n""复查或咨询建议:建议按机构规则复查,或咨询医疗专业人员。本结果不能替代医疗专业判断。")defvalidate_output(text:str)->tuple[bool,List[str]]:reasons=[]forpatterninFORBIDDEN_PATTERNS:ifre.search(pattern,text):reasons.append(f"命中禁止表达:{pattern}")required="不能替代医疗专业判断"ifrequirednotintext:reasons.append("缺少边界声明")returnlen(reasons)==0,reasonsdeffallback_output(hits:List[RuleHit])->str:ifnothits:return"未发现基于示例规则触发的风险提示。本结果不能替代医疗专业判断。"lines=["以下为基于示例规则生成的风险提示:"]forhinhits:lines.append(f"-{h.name}:{h.message}")lines.append("本结果不能替代医疗专业判断。")return"\n".join(lines)defwrite_audit_log(report_id:str,hits:List[RuleHit],raw:str,final:str,reasons:List[str]):audit_event={"event_id":str(uuid.uuid4()),"report_id":report_id,"time":datetime.utcnow().isoformat(),"rule_hits":[h.dict()forhinhits],"llm_raw_output":raw,"final_output":final,"blocked_reasons":reasons}print(audit_event)@app.post("/lab-risk-hint")deflab_risk_hint(report:LabReport):hits=run_rules(report.items)prompt=build_prompt(report,hits)raw_output=mock_llm_call(prompt)ok,reasons=validate_output(raw_output)final_output=raw_outputifokelsefallback_output(hits)write_audit_log(report.report_id,hits,raw_output,final_output,reasons)return{"report_id":report.report_id,"risk_level":"attention"ifhitselse"none","output":final_output,"blocked":notok,"blocked_reasons":reasons}本地运行时可以先用mock_llm_call,等流程稳定后再接入真实 LLM API。接入时建议保留原始输出和最终输出,便于排查“模型是否越界”以及“拦截器是否误伤”。
排查重点:越界内容通常从哪里漏出来
第一类问题是 Prompt 写得太宽。比如“请解读报告并给出建议”会诱导模型扩展到诊断和处置,应改成“只能改写规则命中结果”。
第二类问题是规则和文案混在一起。规则层应输出结构化结果,例如code、level、message,不要在规则里写大段医学解释,否则后续难以审计。
第三类问题是只做输入控制,不做输出控制。医疗健康场景下,输出校验是必要防线。关键词拦截不是万能方案,但可以作为第一层,再叠加分类模型、人工抽检和灰度发布。
第四类问题是没有降级策略。当 LLM 输出不合规时,系统不能直接报错给用户,也不能放行原文。更合适的做法是回退到规则模板,保证输出仍然可读且边界清晰。
审计回溯:让每一次提示都能解释
风险提示助手上线后,日志不是普通调试信息,而是问题定位的依据。建议至少记录以下字段:
- 报告 ID 和请求时间
- 标准化后的指标数据
- 命中的示例规则版本
- LLM Prompt 摘要或模板版本
- LLM 原始输出
- 最终展示输出
- 拦截原因和降级结果
规则库也需要版本号。否则同一份报告在不同时间得到不同提示时,很难判断是规则调整、模型变化还是输入解析问题。
如果涉及真实用户数据,还需要做脱敏、访问控制和留存周期管理。日志系统不应成为新的敏感数据泄露点。
扩展建议:从 Demo 到可上线服务
从工程落地看,可以按三个阶段推进。
第一阶段只做结构化输入和规则模板输出,不接 LLM。目标是把指标标准化、规则配置、审计日志跑通。
第二阶段引入 LLM 改写,但只允许它基于规则命中结果生成自然语言。此时必须上线输出校验和降级模板。
第三阶段再增加质量评估,例如抽样复核、越界率统计、规则命中分布、用户反馈闭环。评估指标不应只看“回答是否流畅”,更应关注“是否越界”“是否可回溯”“是否稳定”。
总结
检验报告 Agent 的工程重点不是让模型自由发挥,而是把它限制在风险提示助手的角色内。一个可控方案通常包括结构化输入、可配置示例规则、受控 Prompt、输出拦截、降级模板和审计日志。
本文代码只展示技术架构思路,不构成医疗建议。真实项目中的参考范围、风险等级和升级规则,需要由医疗专业人员和机构规范确认后再进入规则库。下一步可以把规则配置外置到数据库,并增加规则版本管理、自动化测试集和人工复核后台。
本文作者:超能文献团队(https://suppr.wilddata.cn/)