问诊状态 Schema
问诊过程中独立维护的结构化状态对象,包含主诉、症状、持续时间、诱因、危险信号、缺失字段等。比原始聊天记录更重要,是模型的工作记忆。
#type / concept
#status / evergreen
#tech / ai
[!info] related notes
- 所属 MOC: Context Engineering MOC, BodySense MOC
- 前置概念: Context Engineering
- 工作流: 咨询 Agent 工作流设计
- 安全检测: Red Flag 检测器
- 信息采集: 结构化信息采集
- 提取工具: Function Calling
问诊状态 Schema
问诊状态(ConsultationState)是问诊过程中独立维护的结构化状态对象,包含主诉、症状、持续时间、诱因、危险信号、缺失字段等。它比原始聊天记录更重要——messages 是证据,structured state 是工作记忆。
为什么需要独立状态
不要让模型每轮都从聊天记录里重新推理这些字段。原始 messages 有三个问题:
- 信息分散 — “膝盖下坠感”在第 1 轮,“一周了”在第 3 轮,“走路明显”在第 5 轮
- 格式不稳定 — 用户可能说”没有疼”也可能说”不疼”,模型需要每轮重新解析
- 无法驱动逻辑 — 代码无法从 messages 里可靠判断”是否已收集疼痛信息”
独立的 consultation_state 让:
- 模型直接读取已收集的信息,不重复推理
- 代码根据
missing_slots决定下一步动作 - UI 渲染结构化看板
- 测试可以验证状态是否正确
Schema 定义
{
"stage": "collecting_details",
"chief_complaint": "膝盖下坠感",
"body_parts": ["膝盖"],
"symptoms": ["下坠感"],
"duration": "一周",
"aggravating_factors": ["走路"],
"relieving_factors": [],
"severity": null,
"pain_level": null,
"swelling": null,
"trauma": null,
"instability": null,
"red_flags": [],
"missing_slots": ["pain_level", "swelling", "trauma", "instability"],
"last_question": "是否伴随疼痛或肿胀?",
"pending_answer_for": ["pain", "swelling"],
"confidence": {
"body_part": 0.95,
"duration": 0.80,
"pain_level": null
},
"updated_at": "2026-06-30T10:30:00Z"
}
字段说明
核心字段
| 字段 | 类型 | 说明 |
|---|---|---|
| stage | enum | 当前问诊阶段 |
| chief_complaint | string | 主诉(用户最初的问题) |
| body_parts | string[] | 涉及的身体部位 |
| symptoms | string[] | 已确认的症状列表 |
| duration | string | 持续时间 |
| severity | string | 严重程度 |
诱因和缓解因素
| 字段 | 类型 | 说明 |
|---|---|---|
| aggravating_factors | string[] | 加重因素(如”走路""久坐”) |
| relieving_factors | string[] | 缓解因素(如”休息""热敷”) |
细节字段
| 字段 | 类型 | 说明 |
|---|---|---|
| pain_level | string/null | 疼痛程度(none/mild/moderate/severe) |
| swelling | bool/null | 是否肿胀 |
| trauma | bool/null | 是否有外伤史 |
| instability | bool/null | 是否有不稳定感(打软腿、卡顿) |
流程控制字段
| 字段 | 类型 | 说明 |
|---|---|---|
| red_flags | string[] | 已触发的危险信号 |
| missing_slots | string[] | 尚未收集的关键字段 |
| last_question | string | 上一轮 AI 问了什么 |
| pending_answer_for | string[] | 用户当前回答对应的字段 |
元数据字段
| 字段 | 类型 | 说明 |
|---|---|---|
| confidence | object | 各字段的置信度(0~1) |
| updated_at | datetime | 最后更新时间 |
问诊阶段枚举
| 阶段 | 说明 | 进入条件 |
|---|---|---|
| start | 初始状态 | 新会话 |
| collect_chief_complaint | 收集主诉 | 用户描述了问题 |
| collect_details | 收集细节 | 主诉已确认 |
| red_flag_screening | 排查危险信号 | 基本信息已收集 |
| preliminary_assessment | 初步判断 | 信息足够分析 |
| rehab_recommendation | 康复建议 | 初步判断完成 |
| follow_up_plan | 观察和复查建议 | 方案已给出 |
状态更新机制
更新来源
状态由两个来源更新:
- LLM tool call:
extract_symptom_info抽取候选信息 - Deterministic merge:后端做字段合并、置信度判断、冲突处理
不要完全相信模型的 tool call 结果。 更优雅的方式是:
LLM 负责抽取候选信息
后端负责合并、校验、版本化
冲突处理
用户可能在不同轮次给出矛盾信息:
第 1 轮:"没有疼"
第 4 轮:"走久了有点疼"
状态不应该简单覆盖,而应该保留历史:
{
"pain_level": {
"current": "走久后轻微疼痛",
"source_turn_id": "turn_004",
"confidence": 0.86,
"history": [
{"value": "无明显疼痛", "turn_id": "turn_001"},
{"value": "走久后轻微疼痛", "turn_id": "turn_004"}
]
}
}
missing_slots 驱动问诊
missing_slots 是驱动问诊流程的核心字段。当它不为空时,Agent 优先追问最关键的 1~2 个问题:
def decide_next_action(state):
if state.red_flags:
return "safety_advice"
if state.missing_slots:
return "ask_followup"
if state.stage == "collecting_details":
return "preliminary_assessment"
return "general_response"
详见 咨询 Agent 工作流设计。
与 Function Calling 的关系
LLM 通过 extract_symptom_info tool call 提取信息:
SYMPTOM_TOOL = {
"name": "extract_symptom_info",
"parameters": {
"type": "object",
"properties": {
"body_part": {"type": "string"},
"symptom_type": {"type": "string"},
"duration": {"type": "string"},
"trigger": {"type": "string"},
"severity": {"type": "string"},
"pain_level": {"type": "string"},
"swelling": {"type": "boolean"},
},
"required": ["body_part"],
},
}
但提取结果只是候选信息,后端 merge 逻辑负责:
- 去重(同一字段相同值不重复记录)
- 冲突检测(新值与旧值矛盾时保留历史)
- 置信度更新
- missing_slots 重新计算
与 messages 的关系
messages = 证据(原始对话记录)
consultation_state = 工作记忆(结构化快照)
- messages 告诉你”用户说了什么”
- consultation_state 告诉你”我们已经知道了什么、还缺什么”
两者互补,不可替代。
数据库存储
建议 consultation_state 存在会话级别,每轮更新:
ALTER TABLE consultation_sessions
ADD COLUMN consultation_state JSONB DEFAULT '{}';
CREATE INDEX idx_consultation_state_stage
ON consultation_sessions ((consultation_state->>'stage'));