一、架构

本周架构方向的核心工作是解决一个实际开发中频繁出现的问题:大模型的输出是概率性的文本,但前端需要的是类型严格的 JSON。 如果不在工程层面强制约束,前端代码会充斥着脆弱的 try-catch,后端也会频繁 500。

系统数据流转是一个带状态机的闭环:前端用户输入自然语言 → FastAPI 接收后挂载 Agent → Agent 完成实体提取后触发 Tool Calling 调用图谱接口 → 后端根据检索结果组装响应,按 action_type 分发 PASS / BLOCK / CLARIFY / FALLBACK 四条路径 → 前端状态机按枚举值条件渲染对应界面。

核心接口契约 /api/analyze_prescription 的响应体设计是本周最重要的输出。彻底抛弃了 {"text": "..."} 的简陋做法,要求后端必须返回结构化指令:

{
  "code": 200,
  "data": {
    "action_type": "BLOCK",
    "risk_level": "High",
    "analysis_result": {
      "extracted_drugs": ["泰诺", "布洛芬"],
      "conflict_rule": "泰诺(含对乙酰氨基酚)与布洛芬同属非甾体抗炎药,叠加使用存在极高的严重肝损伤风险。",
      "graph_source": "Rule_ID_042"
    },
    "ui_directives": {
      "alert_title": "极高危用药冲突拦截",
      "clarify_prompt": null
    }
  }
}

 二、 AI 模块

本周完成了 GraphRAG 检索模块的核心开发,并将其封装为 Tool 接入 ReAct Agent。整体流程已跑通,但过程中遇到了两个教训值得记录。

图谱数据结构:节点 + 有向边

采用 Python 字典 + JSON 模拟图结构(放弃重量级 Neo4j,控制部署成本)。节点覆盖药物、患者状态、食物三类,边带有 severity(HIGH/MEDIUM/LOW)、机制描述和处置建议。节点的 aliases 字段是关键——用户不说"ibuprofen",他们说"布洛芬"、"芬必得"、"美林",这三个说法必须映射到同一节点。

最大发现:图谱结果不能直接塞进 Prompt

检索逻辑写完后原以为直接把 JSON 结果传给 LLM 就好,测试发现不行。原始 JSON 有两个问题:"severity": "HIGH" 对 LLM 只是个字符串,不会自动理解"HIGH = 必须阻断";加上节点 ID 是英文,LLM 生成中文解释时偶尔夹杂英文节点名。

解决方案是加一个序列化层,把结构化数据翻译成带语义强化的自然语言:

冲突 1:【高危 · 必须阻断】
  类型:药物相互作用
  涉及:布洛芬 ↔ 华法林
  机制:布洛芬抑制血小板聚集,与华法林联用显著增加出血风险
  ...

决策规则:存在任意【高危·必须阻断】冲突时,最终结论必须为 blocked,
不得使用模糊或建议性措辞。

加了序列化层之后 LLM 几乎不再出现"用温和口吻描述高危冲突"的问题。

坑一:实体对齐比想象中难

用户输入"消炎药",Agent 提取出来的就是"消炎药"这个词,字典里没有,检索返回空。临时方案是在 System Prompt 里加约束:非特指名称必须标记为【无法识别-需追问】,不允许直接传入图谱。商品名到通用名的别名映射还是靠手动维护,是个无底洞,后续考虑接药品数据库 API 辅助。

坑二:患者状态实体的语义变体太多

"胃溃疡史"、"有胃病"、"胃不好"、"之前做过胃镜有溃疡"——字符串差异大到别名字典覆盖不了。解决方案是把患者状态提取完全交给 LLM,在 System Prompt 里定义固定标签集合(胃溃疡/妊娠期/肾功能不全等),LLM 做语义到标签的映射,图谱只接受标准标签。两者接口清晰分离。


三、后端与模型微调:第一条数据-模型-服务链路打通

本周完成了第二阶段最核心的目标:从 MIMIC-III 临床数据出发,到 Qwen2.5-3B LoRA 微调,到 FastAPI 接口服务,第一条完整链路正式跑通。

统一数据结构落地

第一步是把上周的设计变成可执行的代码。本周完成了四个核心对象的定义:DrugItem(药物条目,含名称/剂量/给药途径/频次)、DiagnosisItem(诊断条目,含 ICD 编码和诊断名称)、PatientContext(患者完整上下文,是本阶段最核心的统一对象,把原本散落在多张表和临床文本里的信息整合到同一结构)、ExtractionOutput(结构化抽取的输出格式,与后端返回格式完全一致,降低联调成本)。

MIMIC-III 样本构造

使用了六张表:PATIENTS.csv(人口学)、ADMISSIONS.csv(住院信息)、DIAGNOSES_ICD.csv + D_ICD_DIAGNOSES.csv(诊断编码与名称)、PRESCRIPTIONS.csv(用药记录)、NOTEEVENTS.csv(临床文本,重点取 discharge summary)。

(SUBJECT_ID, HADM_ID) 为主键做住院级别聚合,从 discharge summary 中提取 Chief Complaint 和 HPI 段落,缺失字段直接记录在 missing_fields 中——这让模型能学到"信息不足时输出缺失项"的行为。最终样本转换为聊天格式 SFT 数据(system 要求严格输出 JSON / user 输入病例摘要 / assistant 输出目标 JSON)。

Qwen2.5-3B-Instruct + LoRA 原型实验

用 LoRA 对 Qwen2.5-3B 做轻量微调(显存成本低,适合 4090 单机快速实验),当前阶段不追求最终性能,只验证四件事:模型能否稳定输出合法 JSON、能否覆盖关键字段(年龄/性别/诊断/用药)、缺失字段能否被正确识别、训练后模型能否被后端直接调用。目前这四条都基本跑通。

FastAPI /api/extract 骨架完成

接口逻辑:接收病例摘要文本 → 调用微调模型 → 获取输出 → 尝试解析 JSON → 返回原始输出与解析结果。这是系统的第一个可调用服务接口,也是前后端联调的起点。

当前存在的问题: MIMIC-III 文本段落提取仍依赖规则匹配,鲁棒性有限;药物名称标准化尚未完成(后续接入 TWOSIDES 时还需归一化映射);结果表数值当前为占位值,待正式实验完成后替换。


四、前端:Axios 封装完成,联调踩了三个坑

本周完成了网络层建设——Axios 封装、拦截器设计,以及与 FastAPI 的第一次联调。表面上只是常规的网络层配置,但联调过程中出现了三个非预期问题。

Axios 封装设计

实例化配置 baseURL 读取环境变量(.env.development 中的 VITE_API_BASE_URL),配合 vite.config.js 代理避免硬编码后端地址,timeout 设为 30 秒(原因见下)。

请求拦截器负责注入 token 和开发环境日志;响应拦截器做了差异化处理——普通接口返回 data.data,但 Agent 的 /consult 接口直接返回业务数据,两者需要区分对待。

所有网络调用通过 Pinia Store 封装,组件不直接操作请求,只感知业务状态,网络层彻底隔离:

// store 中
async submitConsultation(payload) {
  const data = await consult(payload)
  this.processAgentResponse(data)
  return { success: true, status: data.status }
}
// 组件中
const result = await store.submitConsultation({ symptoms: '...' })

联调三坑

坑一:CORS 预检失败。 前端报 CORS 错误,后端说已配置。原因是 Content-Type: application/json 加上自定义 Header 会触发 OPTIONS 预检请求,而后端最初没有正确处理 OPTIONS。解决方案是在 FastAPI 里加完整的 CORSMiddleware 配置。这个坑的教训是:CORS 问题不只有一种,预检请求失败和跨域失败是两回事,要分别排查。

坑二:字段命名不一致。 后端部分字段用下划线、部分用驼峰,前端解析失败导致页面异常。解决方案是统一约定为下划线(符合 Python/FastAPI 惯例),同时在 Store 的 processAgentResponse 里加了防御性字段映射,即使后端偶尔有字段漏出也不崩溃。这个问题说明接口契约(林煒这周完成的那份文档)在开发前就应该定好字段命名规范。

坑三:请求超时。 timeout 原先设的 10 秒,Agent 推理耗时超过这个值,请求直接被中断。调整为 30 秒,并加了 loading 状态告知用户系统正在处理。

Logo

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

更多推荐