
基于LangGraph构建临床问诊助手实践
一、背景:医疗场景下的“患者问诊录入”痛点
在诊所或医院的日常运营中,“患者信息录入”(Patient Intake)是必不可少的环节,但现有方案始终存在局限:
- 在线表单僵化:固定字段无法像人类沟通那样灵活调整,例如患者提到“胸口闷”时,表单无法主动追问“闷痛持续多久”或“是否伴随出汗”;
- 电话问诊低效:依赖医护人员人工沟通,而医疗资源本就紧张,这种方式不仅占用大量时间,还难以规模化覆盖更多患者。
尽管缺乏深厚的临床专业知识和医院运营经验,通过调研发现,当前大语言模型(LLM)若经合理编排,完全有能力解决这些痛点。其核心在于让助手能够有逻辑地引导对话,并准确提取临床信息以避免幻觉。基于此洞察,ClinicAssist系统利用LangGraph进行构建,旨在打造一款高效的临床问诊助手。
二、ClinicAssist的技术架构:三层协同设计
整个系统分为三大核心组件,各层职责明确且相互衔接:
| 组件 | 技术栈 | 核心作用 |
|---|---|---|
| LLM工作流 | LangGraph | 主导对话逻辑与流程编排,控制“什么时候问什么问题”“是否需要追问”“何时进入下一阶段” |
| 后端 | FastAPI | 作为前后端桥梁,提供RESTful API(会话管理、消息处理、结构化响应生成),保障数据完整性 |
| 前端 | Next.js | 极简用户界面,负责展示对话流程、信息收集进度,以及已提取的临床结构化数据 |
其中,LangGraph驱动的LLM工作流是系统“大脑”——整个问诊流程被拆分为4个递进阶段,每个阶段的逻辑可复用,只需微调细节即可适配不同信息收集目标。
三、核心:LLM工作流的分阶段实现
四个阶段遵循统一逻辑:提问→等待患者输入→提取结构化信息→判断是否足够→循环/进入下一阶段。下面以最关键、最复杂的“症状收集阶段”(Phase 2)为例,拆解具体实现。
1. 先定目标:用Schema明确“要提取什么信息”
在设计任何提示或流程前,首先要明确“最终需要什么结构化数据”——这是避免LLM输出混乱的核心。基于临床文档规范,本文利用Pydantic定义了“症状信息Schema”:
from pydantic import BaseModel, Field
from typing import Optional, List
class SymptomsPartial(BaseModel):
main_symptoms: Optional[List[str]] = Field(description="患者主要症状,如‘头痛’‘咳嗽’")
symptom_onset: Optional[str] = Field(description="症状发作时间,如‘3天前’‘今天早上’")
associated_symptoms: Optional[List[str]] = Field(description="伴随症状,如‘发烧’‘乏力’")
additional_symptom_info: Optional[List[str]] = Field(description="其他关键细节:严重程度、诱发因素、缓解方式等")
这个Schema的价值在于:让LLM的输出被“约束”在固定格式中,后续可直接用于医生参考或系统存储,无需额外处理非结构化文本。
2. 症状收集阶段的流程设计:3个节点+1个决策
LangGraph以“图”的方式组织流程,每个“节点”对应一个动作,“边”对应节点间的流转关系:
- ask_symptoms节点:调用LLM生成问题。通过系统提示将“已收集的信息”“对话历史”传给LLM,确保问题有针对性(比如已知道“头痛”,就追问“头痛是刺痛还是胀痛”);
- human_symptoms_node节点:中断节点。暂停LLM流程,等待患者输入回答;
- extract_symptoms节点:提取结构化信息。通过LLM的“结构化输出”功能,强制让模型按照
SymptomsPartial格式返回结果:# 让LLM仅输出符合SymptomsPartial格式的内容symptoms_llm = llm.with_structured_output(SymptomsPartial)
3. 关键难题:如何判断“信息是否足够”?
这是整个阶段最需要思考的部分——既要避免信息缺失影响后续诊疗,又要防止过度追问打扰患者。最终采用“半结构化判断”方案:
- 硬条件检查:先验证核心字段是否存在(主要症状+发作时间),这两个是医生初步评估的基础,缺失则直接循环追问;
- LLM辅助判断:若核心字段齐全,再让LLM作为“判断者”,评估是否需要补充细节(如“是否需要知道头痛的严重程度”)。定义判断Schema:
class SymptomSufficiencyCheck(BaseModel): is_sufficient: bool = Field(description="信息是否足够医生初步评估") reason: Optional[str] = Field(description="需补充信息的原因,足够则为None") - 路由函数实现:根据判断结果决定流转方向:
def route_after_symptoms(state: AgentState) -> str: # 1. 硬条件检查:核心字段是否存在 has_main = state.get("main_symptoms") and len(state["main_symptoms"]) > 0 has_onset = state.get("symptom_onset") is not None if not (has_main and has_onset): return "ask_symptoms" # 核心字段缺失,循环追问 # 2. LLM辅助判断 check = check_symptom_sufficiency(state) return "ask_medhist" if check.is_sufficient else "ask_symptoms"
需要注意:这种“LLM作为判断者”的方案虽灵活,但存在“非确定性”(不同次调用可能出不同结果)。需通过以下方式优化:
- 早期加入评估:准备标注好“是否需继续追问”的测试用例,量化LLM判断的准确率;
- 迭代提示词:根据测试结果调整“判断提示”,比如明确“只有当细节影响初步诊断时才需要追问”。
四、整体流程整合:用LangGraph串联四个阶段
四个阶段(患者基本信息→症状→病史→分诊总结)的逻辑相似,只需复用“提问→提取→判断”框架,再通过LangGraph的StateGraph串联:
1. 核心步骤:添加节点与定义流转
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.memory import InMemorySaver
def build_clinical_assistant_graph():
# 1. 初始化状态图(AgentState为自定义状态类,存储对话历史和提取的信息)
graph_builder = StateGraph(AgentState)
# 2. 添加各阶段节点(仅展示关键节点)
# 阶段1:患者基本信息
graph_builder.add_node("ask_patient_info", ask_patient_info) # 提问
graph_builder.add_node("extract_patient_info", extract_patient_info) # 提取
graph_builder.add_node("human_patient_node", human_patient_node) # 等待输入
# 阶段2-4:症状、病史、分诊总结(节点定义类似,略)
# 3. 定义流转关系(边)
# 阶段1流转:提问→等输入→提取→判断是否进入阶段2
graph_builder.add_edge(START, "ask_patient_info")
graph_builder.add_edge("ask_patient_info", "human_patient_node")
graph_builder.add_edge("human_patient_node", "extract_patient_info")
# 条件边:提取后判断“循环”还是“进入阶段2”
graph_builder.add_conditional_edges(
"extract_patient_info",
route_after_patient_info, # 路由函数
{"ask_patient_info": "ask_patient_info", "ask_symptoms": "ask_symptoms"}
)
# 阶段2-4流转(逻辑类似,略)
# 最终:分诊总结→告知患者→结束
graph_builder.add_edge("triage_summary", "acknowledgement")
graph_builder.add_edge("acknowledgement", END)
# 4. 启用检查点(保存会话状态,支持中断后恢复)
checkpointer = InMemorySaver()
return graph_builder.compile(checkpointer=checkpointer)
2. LangGraph的核心优势
LangGraph借鉴了NetworkX的图论思想,让流程编排更直观:
- 节点(Node):对应“动作”(LLM生成问题、提取信息、等待人类输入);
- 边(Edge):对应“流转规则”(无条件流转、按判断结果条件流转);
- 检查点(Checkpoint):保存会话状态,避免患者中途退出后需重新开始。
五、总结与展望
ClinicAssist目前仍处于早期阶段,但其核心价值在于:用LangGraph解决了“LLM对话流程可控性”问题——通过分阶段、结构化目标设计,让LLM既能灵活引导对话,又能稳定输出可用的临床信息。
未来优化方向可包括:
- 引入临床专家参与:优化Schema设计和信息充分性判断逻辑,确保符合医疗规范;
- 增强评估体系:利用DeepEval等工具构建更全面的测试集(如“幻觉检测”“信息完整性评分”);
- 扩展场景适配:支持不同科室(如儿科、内科)的个性化问诊流程。
