医疗信息化的项目做多了,你会发现一个规律:凡是和模型工作流沾边的AI功能,最后都会被拆成一张图——谁在前、谁在后、哪一步需要人工兜底。最近我们团队在改造一套医疗辅助审核服务,技术选型恰恰落在FastAPI、LangGraph与SpringAI三个词上,跑完一轮对比和落地,踩了不少坑,也积累了一些值得说的经验。这篇文章从选型、架构到部署,完整记录这一轮实践,重点说清楚三个问题:FastAPI和SpringAI到底怎么分工、LangGraph在医疗流程里充当什么角色、以及项目上最容易翻车的几个环节怎么处理。适合正在做医疗AI应用或企业级AI服务的开发者和架构师参考。
1. 医疗场景给技术选型出的三道难题
医疗信息化系统,尤其是辅助诊断、审核、分诊这类应用,和普通互联网项目的技术约束差别很大。想直接照搬通用AI项目方案,往往在第一个月就会被现实锤打。
1.1 响应延迟:线上问诊和实时分诊不能等
普通聊天机器人对延迟的容忍度能到3到5秒,但医疗场景里,患者在线问诊、医生开单时触发审核,系统如果转圈超过1秒,使用者就会明显感到卡顿。更棘手的是,医疗AI业务流程通常是多轮判断,而不是单次大模型调用。每次判断可能是"症状描述 -> 结构化提取 -> 规则校验 -> 模型打分",链路上每一环都要耗时间。
这要求技术方案必须具备两个能力:接口层并发处理能力强,任务流程层可以异步编排。FastAPI的异步特性天然适合第一点,LangGraph则把第二点变成了显式的状态图管理,每一步都能控制超时和重试。
1.2 数据类型复杂:文本、结构化指标、影像描述并存
医疗AI系统处理的数据不是单纯的字符串。患者主诉是自由文本,检验单是结构化数值,影像报告又带着长文本描述。SpringAI的优势在于它和Java生态的深度集成,企业里现成的数据访问层、消息队列、事务管理可以直接衔接,不用绕路。FastAPI这边则更擅长快速构建轻量服务,尤其在Python生态里调用机器学习模型没有额外开销。
真正的问题不是谁强谁弱,而是谁能在你的团队和现有系统里"少折腾地"跑起来。这个判断标准,后面第六节我会详细展开。
1.3 模型工作流:医疗AI不是单次调用而是多步决策
这是我特别想强调的一点。很多人一开始把智能审核想成"给大模型发一段医嘱,返回一个判断",落地时才意识到不对。医嘱合理性审核至少需要经过药品冲突检查、过敏史对照、剂量计算、历史病历关联、医生确认等级判断多个环节。每一步可能调用不同的模型或规则引擎,而且步骤之间存在条件分支。
LangGraph这类编排框架正是为此设计的。它把流程定义成一张图,节点是处理函数,边是流转条件,任何一步失败都有明确的回退路径。相比自己维护一张状态机或一堆if-else嵌套,LangGraph的状态管理和断点续跑能力让整个流程可观测、可干预。
2. 两套技术栈的定位差异与分工逻辑
明确一点,FastAPI + LangGraph 和 SpringAI不是非此即彼的关系。在我们这个医疗项目里,它们是同时存在的,各管一段。
2.1 FastAPI与SpringAI:服务骨架的两种选择
FastAPI本质上是Web框架,负责接收HTTP请求、处理并发、返回响应,它不关心业务逻辑怎么编排。SpringAI则是构建AI应用的一组组件集合。如果只用SpringAI,你自然会被带进Spring Boot的体系里,因为它的核心价值就是和Spring生态无缝拼接。
这么说可能更直观:FastAPI像一家反应极快的接待前台,谁来了都能马上接住,但内部流程它不管。SpringAI更像一间标准化的诊室,桌椅器械都配套好了,你按流程坐进去就行,前提是诊所整体装修走的是一个体系。
2.2 LangGraph登上舞台的真正原因
我们最初用FastAPI裸写业务逻辑,审核流程里的每一步都用Python函数串起来。一开始还好,只有3个步骤。等流程扩展到7个节点、3个条件分支后,问题立刻暴露:一旦模型超时或者返回异常,根本没法准确定位是哪一步挂了,更没法在中间步骤人工介入修改结果后继续跑。
LangGraph解决的核心痛点是把"流程"本身当成数据来管理。图上每个节点可以单独设置重试策略,支持人在回路的干预点。医生觉得自动审核结果可疑时,可以暂停状态流转,修正输入后重新执行后续路径。这种可中断、可干预的特性,在医疗场景里远比"全自动一步到底"重要。
2.3 一套混合架构,各取其长
我们的落地形态长这样:SpringAI服务负责企业内部数据对接和批量审核任务调度,面向的是系统间集成和复杂事务场景。FastAPI + LangGraph服务负责实时交互场景,比如在线问诊时的即时分诊、医生开单时的实时审核提醒。数据落在同一套底层存储上,但上层的业务路径各走各的。
这套架构最大的收益是,实时路径可以独立扩容。大促活动或疫情高峰期,问诊流量暴涨时只扩FastAPI集群,而不需要把整个Spring Boot应用一起搬走。批量审核路径则保持稳定性,白天晚上跑大批离线任务,也不影响在线服务。
3. 医疗场景三个核心模块的落地实现
抽象的道理说多了容易飘,直接看三个真实落地的模块实现。这三块各代表一种典型模式:多步流程、大模型对话、异步流式交互。
3.1 智能审核模块:医嘱合理性校验的LangGraph流程设计
医嘱合理性审核是我们花时间最多的模块。流程上设计成四段:基础规则引擎、药品冲突检索、大模型语义检查、人工抽检兜底。
LangGraph里的实现思路是这样的:图的状态对象是一个自定义的OrderReviewState,携带医嘱文本、患者ID、检查结果等字段。整个过程通过不同的节点函数修改状态:
from langgraph.graph import StateGraph, END from typing import TypedDict, Optional class OrderReviewState(TypedDict): order_text: str patient_id: str rule_result: Optional[dict] conflict_result: Optional[list] model_result: Optional[dict] final_verdict: Optional[str] def check_basic_rules(state: OrderReviewState) -> dict: # 检查重复开药、超量、禁忌症等规则 return {"rule_result": {"dosage_exceed": False}} def check_drug_conflict(state: OrderReviewState) -> dict: # 查药品冲突库,返回高风险药品组合列表 return {"conflict_result": []} def model_semantic_check(state: OrderReviewState) -> dict: # 调用大模型,对非结构化描述做语义分析 return {"model_result": {"risk_level": "low"}} def human_review(state: OrderReviewState) -> dict: # 高风险病例标记,交由医生确认 return {"final_verdict": "pending_human"} builder = StateGraph(OrderReviewState) builder.add_node("basic_rules", check_basic_rules) builder.add_node("drug_conflict", check_drug_conflict) builder.add_node("model_check", model_semantic_check) builder.add_node("human_review", human_review) builder.set_entry_point("basic_rules") builder.add_edge("basic_rules", "drug_conflict") builder.add_edge("drug_conflict", "model_check") # 高风险才进入人工复核 builder.add_conditional_edges("model_check", lambda s: "human_review" if s["model_result"]["risk_level"] == "high" else END) builder.add_edge("human_review", END) graph = builder.compile()每个节点都设计成独立函数,输入输出只依赖状态对象。这样做的直接好处是测试特别好写,任何一个节点的函数都能脱离整个图单独验证。医疗场景对正确性要求高,单元测试的价值怎么强调都不过分。
3.2 病历质控模块:SpringAI系统提示词的真实配置方式
病历质控是另一个典型场景。出院小结、手术记录这类文书需要检查完整性、逻辑一致性。我们用SpringAI做这块,核心是用系统提示词约束模型按照固定规范检查文本。
网上查SpringAI提示词配置时资料很零散,这里直接给一个可以跑的配置。在application.yml里定义模型相关变量:
spring: ai: openai: base-url: http://你的模型地址:8000/v1 api-key: no-key-needed chat: options: model: qwen2.5-14b-instruct temperature: 0.1温度设置是容易被忽略的细节。病历质控是判断任务,温度越低输出越稳定。我见过不少项目把温度设成默认值,结果模型同一份病历每次判断结果都飘,这就是典型的提示词工程参数没校准。
系统提示词放在Java代码里用文本块管理:
String systemPrompt = """ 你是一名病历质控专家。请根据以下要求检查病历: 1. 检查患者基本信息是否完整。 2. 检查主诉、现病史、既往史是否逻辑一致。 3. 检查诊断与治疗措施是否对应。 4. 输出格式为JSON:{"完整性缺失项": [], "逻辑问题": [], "风险等级": "高/中/低"} 不要输出任何解释性文字,只输出JSON。 """;这里的关键是输出格式约束。病历质控结果需要被下游系统解析,如果让大模型自由发挥文本,后续处理全是坑。强制JSON输出、只输出JSON,比任何后处理都省事。
3.3 智能导诊分诊:FastAPI异步流式接口设计
导诊分诊是患者端的入口功能,患者描述症状后,系统推荐科室并给出就医建议。这类交互对响应体验要求很高,我们用了FastAPI的流式响应,让模型边生成边返回,用户不需要盯着无声的加载圈。
核心代码思路:
from fastapi import FastAPI from fastapi.responses import StreamingResponse import asyncio app = FastAPI() async def triage_stream(patient_input: str): # 第一步:先返回科室匹配结果,这部分是规则引擎算的,不依赖模型 yield "data: " + '{"triage": "消化内科"}' + "\n\n" # 第二步:再流式返回大模型生成的补充建议 async for chunk in llm_stream_generate(patient_input): yield "data: " + chunk + "\n\n" @app.post("/api/triage") async def triage(patient_input: str): return StreamingResponse( triage_stream(patient_input), media_type="text/event-stream" )前端收到第一个事件后立即渲染科室推荐,后面的建议文案后续填充。体感上几乎无延迟,这也是FastAPI相对传统同步框架的优势所在。需要注意SSE协议要求响应头正确设置text/event-stream,客户端解析也要对多事件行的协议有相应处理。
4. 医疗报告数据的高性能查询与推送通道
医疗AI跑完的审核结果、质控报告,最终要面对一个现实问题:这些数据是给人看的,而医生和患者都等不起慢查询。
4.1 为什么不能让前端轮询
早期版本我们确实做过轮询方案,前端每2秒问一次审核结果好了没。流量一上来就露馅了:查询接口被打满,数据库连接池告警,用户体验还差。后来统一改成长连接推送,服务端完成后主动推给前端,效果立竿见影。
SSE就是为这种场景设计的。相对WebSocket,它更轻量,基于普通HTTP,不需要额外维护长连接协议状态,对移动端网络穿透也友好。医疗场景里服务器主动通知的场景特别多,审核完成、超时预警,全都适合用SSE。
4.2 数据查询与模型结果回写
高并发推送之外,数据侧的压力不容小觑。审核结果要实时写入数据库,还要支持按时间、科室、审核结果类型多个维度筛选,没做好索引优化,报表页面能卡到怀疑人生。
我们采用的策略是读写分开。在线服务写入结果走业务库,分析查询走只读从库。业务库只保留近期热点数据,历史记录定期归档到分析库。这种设计不算新鲜,但在医疗AI项目里常常被忽视,因为团队精力都在模型效果上,等到数据量上来才开始补课,代价就大了。
4.3 Redis缓存热点防线
高频查询的筛选结果,比如某科室近7天审核汇总,在Redis里缓存60秒。这样小组件页面频繁刷新也不会打到数据库。
import redis.asyncio as redis r = redis.from_url("redis://localhost:6379/0") async def get_daily_summary(department: str): cache_key = f"review_summary:{department}:7d" cached = await r.get(cache_key) if cached: return cached result = await db.query_daily_summary(department) await r.set(cache_key, result, ex=60) return result注意并发穿透问题。如果缓存失效瞬间正好有大量请求过来,数据库还是会被打。简单方案是在缓存更新期间加一个进程内锁,或者用Redis分布式锁保证只有一个请求去查库回填,其他请求等待。这个细节遇到过实操问题,值得留意。
5. 选型分水岭:什么项目用FastAPI + LangGraph,什么项目用SpringAI
回到主题本身,两套技术栈到底怎么选。我给的判断框架不是比性能参数,而是看项目现状和团队情况。
5.1 技术栈继承是最现实的约束
现有系统是Java系,团队主力是Java工程师,那SpringAI是顺理成章的选择。硬要把FastAPI塞进Java为主的组织,光运维和协作成本就能拖垮项目。反过来,如果团队已经是Python系,或者核心算法模型是Python写的,那用SpringAI反而要处理Python和Java模型服务之间的调用开销。
这个结论看着像废话,但很多技术对比文章恰恰不说。真实世界里的选型,技术优劣只是其中一个变量,组织惯性往往更决定走向。
5.2 流程复杂度决定要不要上LangGraph
如果业务只有"调用一次模型,返回结果",根本不需要LangGraph,FastAPI一个异步路由就够了。只有当流程有多节点条件分支、需要人工干预的地方、状态需要持久化恢复时,LangGraph的价值才体现出来。
我们审核模块早期只有3个节点时,确实感觉引入LangGraph有点重。到后来扩展到7个节点,每多一个节点都牵一发动全身的时候,状态图管理的优势就完全展示了。这个判断时机很重要,早引入觉得过度设计,晚引入就面临重写。
5.3 一个简化的选择判断表
我整理了一张表,开发前可以和团队过一遍。
| 维度 | FastAPI + LangGraph | SpringAI |
|---|---|---|
| 团队主力语言 | Python | Java |
| 现有系统架构 | 微服务/轻量服务 | Spring Boot生态 |
| 实时交互要求 | 高,适合SSE流式 | 中,更偏企业级集成 |
| 多步流程编排 | 强,显式状态图 | 中,可借助状态机 |
| 模型生态集成 | 直接,Python库丰富 | 需适配AI组件 |
| 批量任务处理 | 一般 | 强,Spring生态成熟 |
这个表不是绝对标准,但能在需求还不清晰的早期阶段快速帮团队收敛方向。我们项目最终采用混合架构,就是因为对比了一圈后发现,单一选型都会在某些维度上妥协太多。
6. 部署与打包现场:Windows应用打包与uvicorn日志丢失的解决
选型和编码只是前半程,部署阶段才是真正考验工程能力的地方。这轮项目里踩了两个高频问题,都和FastAPI相关,网上讨论较多,我也给出实际操作方案。
6.1 PyInstaller打包FastAPI项目避坑
很多医疗信息化项目的交付环境是Windows服务器,需要在脱离Python解释器的环境里运行。PyInstaller打包FastAPI项目时会遇到几个典型问题。
第一个坑是动态导入丢模块。FastAPI项目的路由文件经常用相对导入,PyInstaller静态分析时可能漏掉部分模块,导致启动时爆ModuleNotFoundError。解决办法是在spec文件里手动添加隐藏导入。
# app.spec 片段 hiddenimports=[ 'uvicorn.logging', 'uvicorn.loops', 'uvicorn.protocols', 'uvicorn.protocols.http', 'uvicorn.protocols.websockets', 'uvicorn.lifespan', 'uvicorn.lifespan.on', ]第二个坑是静态资源路径失效。打包后exe运行时,__file__指向临时解压目录,项目里的模板、模型权重文件路径全部失效。稳妥做法是把资源文件放在exe同级的data目录,代码里用sys.executable所在目录拼接绝对路径。
import sys, os from pathlib import Path def resource_path(relative_path: str) -> Path: base_path = Path(sys.executable).parent if hasattr(sys, "executable") else Path(__file__).parent return base_path / "data" / relative_path这类问题排查起来最耗时间,因为打包环境复现困难,很多战友卡在这类问题上很久。建议首次打包前先把spec文件研究明白,别急着直接命令行出exe。
6.2 uvicorn日志丢失的根因与处理
另一个高频问题是uvicorn日志丢失。开发环境一切正常,部署后日志文件里就是没内容。根子在于uvicorn的日志配置和logging模块整合方式特殊。
初始化时用了uvicorn.run(app, log_config=None),等于完全禁用uvicorn的日志配置。这种情况下,如果想要日志输出,必须在自己的logging配置里添加对应的logger名称。
import logging LOGGING_CONFIG = { "version": 1, "disable_existing_loggers": False, "loggers": { "uvicorn": {"handlers": ["default"], "level": "INFO"}, "uvicorn.error": {"handlers": ["default"], "level": "INFO"}, "uvicorn.access": {"handlers": ["access"], "level": "INFO"}, }, }还有一类日志丢失情况和多进程模型有关。通过multiprocessing启动多个worker时,被fork出来的子进程可能继承不到日志处理器。这时候需要在子进程入口处重新配置logging。
另外提醒一句,Windows部署还有一个很隐蔽的问题:uvicorn的reload模式在Windows下对文件监听的实现并不完全可靠,生产环境务必关闭reload,否则既影响性能,又可能引发日志混乱。
6.3 服务健康检查与自动拉起
生产环境我建议加一个独立的健康检查端点,不要复用业务接口。专门的/health接口只做两件事:检查数据库连接是否正常,检查关键依赖服务是否可达。
from fastapi import FastAPI from fastapi.responses import JSONResponse app = FastAPI() @app.get("/health") async def health_check(): db_ok = await check_database_connection() model_ok = await check_model_endpoint() return JSONResponse( status_code=200 if db_ok and model_ok else 503, content={"db": db_ok, "model": model_ok} )配合监控系统,服务异常时自动拉起进程。别小看这个基础动作,医疗系统夜间模型服务假死的问题,靠这一招解决了不少麻烦。
7. 医疗AI项目中的数据安全与合规避坑建议
医疗场景里技术问题绕得过去,数据安全和合规的坑一步都不能踩。这一节不展开讲条款,只聊工程上必须落地的几条底线。
7.1 数据最小化原则,能虚的不传实的
调用大模型做审核分析时,提示词里尽量少拼个人敏感信息。能传脱敏后的标识符,就别传完整姓名、身份证号。能只传年龄段就传年龄段,不要带具体出生日期。这条原则要在代码规范层面强制,而不是靠个人自觉。我们在网关层做了统一字段脱敏,业务代码里即使不小心传了,网关也会拦截替换。
7.2 模型私有化部署,不碰外部公开服务
医疗数据出域在任何场景下都是高压线。大模型推理必须走私有化部署,本地跑或内网推理。这也是为什么前面代码示例里base-url指向的都是局域网模型服务地址。走外部API必然产生数据出域,这条线无论如何不能越。
从工程角度,私有化模型的好处不仅是合规,还带来延迟可控和稳定性。外部公共API服务受网络波动影响大,高峰期排队严重,医疗场景扛不住这种不确定性。
7.3 数据流转过程加密
模块之间的内部调用也建议走加密通道,尤其是智能审核的结果数据。我们的做法是:所有服务间调用统一走内部mTLS,消息队列启用SSL。数据库敏感字段在应用层做AES加密,而不是只依赖数据库实例权限控制。
日志系统同样要过滤,不允许将完整敏感字段写入明文日志。这一条很多团队容易忽略,排错时打印整条请求体,里面全是敏感信息。建议统一日志工具类,自动对敏感字段做掩码处理。
7.4 首个试点模块选相对简单、风险低的场景
落地节奏上,别一上来就挑战高风险的自动诊断。先从一个相对简单、边界清晰的质控模块开始,比如病历完整性检查。这类场景即使用户判断失败,造成的风险也小,但能让整个技术链路充分跑通,积累数据基础。
跑通之后再逐步扩展到药物审核、分诊建议。不仅是规避风险,也是给团队适应时间。医疗场景的特殊性决定了,稳定可靠永远比功能多远更重要。
8. 一些总结与个人体会
项目做下来最深刻的体会:FastAPI、LangGraph和SpringAI的组合不是固定答案,而是针对不同业务切片的工具解法。实时、高并发的交互层交给FastAPI;多步、可干预的审核流交给LangGraph;企业级、批量化的集成层交给SpringAI。这个分工在我们项目里经受住了流量和数据量的双重验证。
最后分享一个工程上的小技巧:医疗场景的AI服务上线初期,不要迷信全自动。在关键节点保留人工确认的入口,既能减少误判带来的风险,也为后续模型迭代积累标注数据。系统跑顺后,再逐步提高自动化比例。
这个思路放到技术实现上,就是前面讲的:在LangGraph图里为高风险分支添加人工复核节点;在SpringAI审核模块里为高危结果设置强制人工抽检开关。既稳住了业务方信心,也给算法团队提供了真实的反馈数据。医疗AI的落地,讲究的就是这种在稳妥与技术之间找平衡的功夫。