本阶段继续围绕智能问诊主链路进行完善。前几次已经完成了用户模块、基础资源模块、问诊会话和医生复核的雏形,这次重点放在 GraphDx 多智能体诊断服务接入SSE 流式输出 上,让后端不仅能保存一轮问诊消息,还能把 AI 推理、候选疾病、建议检查和医生分流结果逐步返回给前端。

整体目标是:患者发送一条消息后,后端先保存消息,再调用 GraphDx 进行医学推理,然后将结构化结果写入数据库,并通过 SSE 事件推送给前端。

1. 本阶段开发目标

本次主要完成以下几部分:

  • 接入 GraphDx/ 目录中的诊断智能体。
  • 将 GraphDx 原始动作统一转换为后端可处理的 ask / test / diagnose / escalate
  • 保存每一轮 AI 推理中的观察项、候选疾病、检查建议和 trace 信息。
  • 调用改写服务,将结构化结果改写成患者侧和医生侧都能理解的中文文案。
  • 使用 SSE 返回阶段事件、候选快照、最终消息和完成状态。

问诊流式接口目前仍然是核心入口:

POST /api/v1/consult-sessions/{session_id}/messages/stream
X-Session-Token: <session_token>
Content-Type: application/json

{
  "message_text": "最近三天头晕,站起来时更明显",
  "client_request_id": "client-uuid"
}

2. GraphDx 接入方式

项目中后端业务代码主要维护在 server/ 目录下,GraphDx 作为诊断推理依赖放在独立目录中。这样做可以避免把业务接口、用户鉴权、数据库事务和多智能体内部逻辑耦合在一起。

目前接入逻辑主要放在:

server/src/services/consult_ai_service.py

整体调用流程如下:

患者输入消息

ConsultService 保存消息

ConsultAiService

加载 GraphDx Agent

GraphDx 生成下一步动作

提取 observations

提取候选疾病

提取建议检查

统一封装 ConsultAiDecision

返回给问诊服务

GraphDx 原始返回中最重要的是 action_typeaction_content。后端会先做动作归一化,只允许以下几类动作进入后续流程:

@staticmethod
def _normalize_action_type(action_type: str) -> str:
    normalized = str(action_type).strip().lower()
    if normalized in {"ask", "test", "diagnose", "escalate"}:
        return normalized
    raise ServiceError(f"unsupported GraphDx action_type: {normalized}", 503)

其中:

  • ask:信息不足,继续追问患者。
  • test:建议进入检查流程。
  • diagnose:候选诊断较明确,需要医生确认。
  • escalate:风险较高,需要转医生复核。

这样后端服务层就不需要直接理解 GraphDx 内部日志,而是只处理统一后的业务动作。

3. AI 结果结构化与中文改写

GraphDx 输出更偏结构化和推理过程,直接展示给患者并不友好。因此本阶段在 GraphDx 后面增加了一层改写服务:

GraphDx 原始推理结果 -> 后端结构化字段 -> AiRewriteService 中文改写

改写服务会固定要求返回四个字段:

{
  "assistant_text_cn": "面向患者的中文回复",
  "doctor_summary_cn": "面向医生的临床摘要",
  "patient_question_cn": "需要继续追问患者的问题",
  "risk_notice_cn": "风险提示"
}

这一层的设计重点是 只改写表达,不改变医学事实。也就是说,候选疾病、检查建议和风险判断都来自 GraphDx,改写模型只负责把内容变成更清晰的中文说明。

当前一轮 AI 结果会被拆分保存到多张表中:

GraphDx Decision

consult_observations

consult_candidate_diseases

exam_suggestions

ai_inference_traces

consult_messages

症状观察项

候选疾病 Top5

建议检查项目

推理轨迹与耗时

患者/助手展示消息

这样做的好处是前端不必只依赖一段文本。后续页面可以单独展示候选疾病快照、建议检查、分诊结果和完整时间线。

4. SSE 流式返回设计

普通 HTTP 接口只能等全部计算结束后一次性返回。但问诊场景中,AI 推理和中文改写都可能耗时,如果前端长时间没有反馈,体验会比较差。

所以这里继续使用 SSE:

Content-Type: text/event-stream

当前流式接口会先立即返回分析阶段,再持续返回心跳或最终事件。典型事件如下:

stage
message_saved
heartbeat
rewrite_completed
candidate_snapshot
department_assigned
doctor_review_required
exam_suggested
assistant_message
done

实际事件流可以理解为:

SQLite RewriteService GraphDx ConsultService FastAPI 患者端 SQLite RewriteService GraphDx ConsultService FastAPI 患者端 发送消息 stream 保存患者消息 stage: ANALYZING message_saved 调用诊断智能体 action / candidates / tests 中文改写 assistant_text_cn 等字段 finalize_stream_message 保存观察项、候选疾病、trace rewrite_completed candidate_snapshot assistant_message done

其中 heartbeat 的作用是告诉前端:后端还在处理中,连接没有断开。

5. 状态与异常处理

流式问诊容易出现两个问题:

  1. AI 调用时间较长,前端不知道是否还在运行。
  2. 后端进程中断后,会话可能一直停留在处理中。

因此本阶段对状态也做了处理。患者发送消息后,会话会进入:

AI_PROCESSING

AI 完成后,根据动作进入不同状态:

ask

test / diagnose / escalate

超时或异常

CREATED / COLLECTING_INFO

AI_PROCESSING

GraphDx 动作

COLLECTING_INFO

WAITING_DOCTOR_REVIEW

COLLECTING_INFO

同时后端维护当前活跃的 trace_id。如果查询会话时发现某个会话长期停留在 AI_PROCESSING,并且对应 trace 已经不在活跃任务中,就会释放回信息收集状态,避免页面一直卡死。

示例代码结构如下:

def recover_orphan_processing_session(self, session_id: int, active_trace_ids: set[str]) -> None:
    session = self.repo.get_session(session_id)
    if session is None or session.session_status != self.PROCESSING_STATUS:
        return
    latest = self.repo.get_latest_status_history(session_id)
    if latest is None or latest.trace_id in active_trace_ids:
        return
    # 超过处理 TTL 后释放回 COLLECTING_INFO

6. 总结

本阶段的重点是把“能调用 AI”推进到“能稳定接入问诊业务”。目前后端已经可以完成:

  1. 患者消息持久化。
  2. GraphDx 多智能体推理。
  3. 候选疾病、观察项、检查建议保存。
  4. 中文改写结果生成。
  5. SSE 流式事件推送。
  6. AI 处理中断后的状态恢复。

相比前一阶段,本次实现更接近真实使用场景。下一步主要继续完善医生端复核、检查单确认、设备时隙预约和检查结果回流,让 AI 问诊真正闭环到院内资源调度。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐