医学文档是医疗保健的重要组成部分,但分析和验证这些文档可能耗时且复杂。让我们来看看我们的创新解决方案!🚀
🎯 挑战来了医疗工作者在处理医疗文件时会遇到不少挑战,例如:
- ⏰ 繁琐的人工审核过程耗时严重
可能存在在文档分析时的人为错误
对诊断和治疗一致性的持续验证需求
难以快速提取关键信息
- 准确概括的需求
我们开发了一个现代的网络应用,采用了先进的AI技术,用于分析、总结和验证医学文件。这个应用的重点在于这些功能。
- 友好的用户界面
- ⚡ 闪电般的处理速度
-
🎯 精准分析
- ✨ 神级验证
- 📊 清晰呈现结果
🏗️ 技术架构图该应用程序是用现代技术栈开发的。
💻 前端开发- 🚀 框架:FastAPI 后端
- 🎨 UI: Tailwind CSS 和 DaisyUI 用于样式
- ⚡ 互动性 用 Alpine.js 构建响应式组件
Markdown 渲染:使用 Marked.js 实现格式化输出
⚙️ 后端 AI 模型:- 🤖 DeepSeek-R1-Distill-Llama-70B 供分析之用
- 🔄 Mixtral-8x7b 用于摘要吧
- 👁️ Mistral OCR - OCR 功能可用于处理 PDF 文档
- 📊 LangGraph (注:特定术语或专有名词)用于工作流管理
🔄 分析流程应用程序通过四个重要步骤来处理文件。
1. 📄 文档提取任务
眼部OCR识别PDF
- ✨ 文本提取与清理
- 📋 让格式统一起来
2. 医疗检查
- 📅 识别关键日期
- 🏥 提取设施信息
- 👨⚕️ 医生信息
-
👤 病人信息分析
- 💊 用药记录
3. 摘要生成
- 🎯 识别关键发现
⁻ 🏷️ 诊断集
- 💉 治疗计划的小结!
以下是关键观察,突出强调的重点。
4. ✅ 诊断确认
- 🔄 症状和诊断一致性检查
- 📊 治疗方案评估
-
💊 药物复查
- ⚠️ 风险警示
-💡 试试这些替代治疗方法
1. 🔍 智能文档管理
(注:🔍 表示搜索或调查)
- 📄 支持 PDF 文件
- ️👁️ 强大的OCR功能
- 📋 结构化信息抽取
2. 📊 详细分析
-
🔍 仔细查看医疗信息抽取
-
📝 整理发现的结构化格式
- 🎯 清晰展示关键细节
3. 智能总结
- 💡 比如说,简洁而全面的要点
- 信息层级,组织
- 重点关注最关键的部分
4. ✅ 验证系统
- 🔄 治疗与诊断的一致性核对
-
💊 药物适宜性评估
-
💡 另外的治疗建议
- ⚠️ 注意:识别风险因素
该应用程序把安全放在首位:
- 🔐 保护安全文件管理
- 🧹 临时文件打扫
-
🚫 不永久保存敏感数据。
- 💻 本地处理功能(更快,更私密)
文件夹结构
安装所需的依赖
langgraph, langchain, langchain-ollama, langchain-groq, fastapi, python-multipart, uvicorn, jinja2, graphviz
在 .env 文件中设置 API 密钥,以完成配置
GROQ_API_KEY='您的 API 关键字'
MISTRAL_API_KEY='您的 API 关键字'
Main.py
说明:Main.py
是程序中的主文件。
from typing import Dict, Any
from langchain_core.messages import HumanMessage, SystemMessage
from langgraph.graph import StateGraph,START,END
from langchain_ollama import ChatOllama
from langchain_groq import ChatGroq
from pathlib import Path
import base64
from io import BytesIO
from mistralai import Mistral
from typing_extensions import TypedDict
import os
from dotenv import load_dotenv
load_dotenv()
client = Mistral(api_key=os.getenv("MISTRAL_API_KEY"))
# 初始化分析器和摘要生成器
summary_llm = ChatGroq(
model_name="mixtral-8x7b-32768",
temperature=0
)
analyzer_llm = ChatGroq(
model_name="DeepSeek-R1-Distill-Llama-70B",
temperature=0.6
)
def extracttpdf(pdf_name):
uploaded_pdf = client.files.upload(
file={
"file_name": pdf_name, # 正确的变量名称是 pdf_name
"content": open(pdf_name, "rb"),
},
purpose="ocr"
)
#
signed_url = client.files.get_signed_url(file_id=uploaded_pdf.id)
#
ocr_response = client.ocr.process(
model="mistral-ocr-latest",
document={
"type": "document_url",
"document_url": signed_url.url,
}
)
#
text = "\n\n".join([page.markdown for page in ocr_response.pages])
return text
#
class MedicalAnalysisState(TypedDict):
file_name : str
context:str
analysis_result: str
summary: str
validation_result: str
def create_medical_analysis_chain():
# 正在从PDF中提取上下文
def extract_context(state:MedicalAnalysisState):
print("----------------------------------------------------")
print("正在从PDF中提取上下文")
print("----------------------------------------------------")
pdf_name = state['file_name']
text = extracttpdf(pdf_name)
state["context"] = text
return state
def analyze_document(state:MedicalAnalysisState):
print("----------------------------------------------------")
print("正在分析PDF中的上下文")
print("----------------------------------------------------")
messages = state["context"]
document_content = messages
# 使用Langchain groq进行医学分析
messages = [
SystemMessage(content="""你是一名医学文档分析员。提取关键信息,并使用以下部分的markdown格式整理:
### 事件日期
- 指定医疗事件发生的具体日期
### 医疗机构
- 医院或医疗中心的名称
- 位置信息
### 医疗人员
- 主治医生
- 其他参与医务人员
### 患者信息
- 主要主诉
- 重要体征
- 相关病史
### 药物信息
- 当前用药情况
- 新开处方
- 用药剂量
请确保输出格式为markdown,包含适当的标题和项目符号。"""),
HumanMessage(content=document_content)
]
response = analyzer_llm.invoke(messages)
state["analysis_result"] = response.content.split("</think>")[-1]
return state
def generate_summary(state:MedicalAnalysisState):
print("----------------------------------------------------")
print("正在从PDF中生成总结")
print("----------------------------------------------------")
analysis_result = state["analysis_result"]
messages = [
SystemMessage(content="""你是一名医学报告总结员。用markdown格式创建详细总结,包括以下部分:
### 关键发现
- 主要识别的医疗问题
- 重要观察
### 诊断
- 主要诊断
- 次要诊断(如有)
### 治疗计划
- 推荐的程序
- 开具的药物
- 随访建议
### 其他注意事项
- 重要考虑事项
- 特别指示
请确保适当使用markdown格式,包括标题、项目符号和适当的重点强调。"""),
HumanMessage(content=f"根据以下分析生成详细的医学总结报告:{analysis_result}")
]
response = summary_llm.invoke(messages)
state["summary"] = response.content
return state
def validate_diagnosis(state:MedicalAnalysisState):
print("----------------------------------------------------")
print("正在从PDF中验证诊断")
print("----------------------------------------------------")
analysis_result = state["analysis_result"]
summary = state["summary"]
messages = [
SystemMessage(content="""你是一名医学诊断验证员。以markdown格式提供你的评估,在以下部分中:
### 一致性分析
- 评估诊断是否与症状一致
- 评估治疗的适当性
- 审查药物选择
### 建议
- 可考虑的其他治疗方法
- 建议的药物调整
- 需要的额外检测
### 风险评估
- 潜在的并发症
- 药物相互作用关注
- 随访建议
请以清晰的markdown格式呈现你的答案,包含适当的标题和项目符号。"""),
HumanMessage(content=f"""分析: {analysis_result}\n总结: {summary}
根据提供的分析和总结,请提供诊断、治疗和药物是否与医疗投诉一致的评估。如果不一致,请指定应提供何种最佳治疗和药物。""")
]
response = analyzer_llm.invoke(messages)
state["validation_result"] = response.content.split("</think>")[-1]
return state
# 创建图
workflow = StateGraph(MedicalAnalysisState)
# 添加节点
workflow.add_node("extractor", extract_context)
workflow.add_node("analyzer", analyze_document)
workflow.add_node("summarizer", generate_summary)
workflow.add_node("validator", validate_diagnosis)
# 定义边
workflow.add_edge(START, "extractor")
workflow.add_edge("extractor", "analyzer")
workflow.add_edge("analyzer", "summarizer")
workflow.add_edge("summarizer", "validator")
workflow.add_edge("validator", END)
# 编译图
chain = workflow.compile()
# 生成图可视化
graph_png = chain.get_graph().draw_mermaid_png()
graph_base64 = base64.b64encode(graph_png).decode('utf-8')
return chain, graph_base64
def process_medical_document(document_path: str) -> Dict[str, Any]:
# 读取文档并处理不同编码的异常情况
# try:
# # 首先尝试 UTF-8 编码
# content = Path(document_path).read_text(encoding='utf-8')
# except UnicodeDecodeError:
# try:
# # 尝试 cp1252 编码
# content = Path(document_path).read_text(encoding='cp1252')
# except UnicodeDecodeError:
# try:
# # 尝试 latin-1 编码作为备用方案
# content = Path(document_path).read_text(encoding='latin-1')
# except UnicodeDecodeError:
# raise ValueError("无法读取文档 - 不支持的字符编码")
# 创建链并获取图可视化
chain, graph_viz = create_medical_analysis_chain()
print(f"文档路径: {document_path}")
# 处理文档
result = chain.invoke({"file_name": document_path})
return {
"analysis": result["analysis_result"],
"summary": result["summary"],
"validation": result["validation_result"],
"graph": graph_viz
}
FastAPI应用(程序api.py)
from fastapi import FastAPI, UploadFile, File, Request
from fastapi.responses import JSONResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import shutil
from pathlib import Path
from tempfile import NamedTemporaryFile
from .main import process_medical_document, create_medical_analysis_chain
import os
from datetime import datetime
app = FastAPI()
# 如果不存在,则创建所需的目录
static_dir = Path("medical_analyzer/static")
data_dir = Path("medical_analyzer/data")
for directory in [static_dir, data_dir]:
directory.mkdir(parents=True, exist_ok=True)
# 挂载静态文件
app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
# 设置模板
templates = Jinja2Templates(directory="medical_analyzer/templates")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
# 页面加载时生成图表
_, graph_base64 = create_medical_analysis_chain()
return templates.TemplateResponse(
"index.html",
{
"request": request,
"graph_base64": graph_base64
}
)
@app.post("/analyze-medical-document")
async def analyze_document(file: UploadFile = File(...)):
try:
# 验证文件扩展名
if not file.filename.lower().endswith('.pdf'):
return JSONResponse(
status_code=400,
content={"status": "error", "message": "仅支持PDF格式的文件"}
)
# 创建唯一的文件名,以避免文件覆盖
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = f"{timestamp}_{file.filename}"
file_path = data_dir / safe_filename
# 将文件保存到指定路径
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# 处理文档
result = process_medical_document(str(file_path))
return JSONResponse(content={
"status": "success",
"analysis": result["analysis"],
"summary": result["summary"],
"validation": result["validation"],
"graph": result["graph"]
})
except ValueError as e:
return JSONResponse(
status_code=400,
content={"status": "error", "message": str(e)}
)
except Exception as e:
print(f"在处理文档时出错: {str(e)}") # 调试用
return JSONResponse(
status_code=500,
content={"status": "error", "message": "在处理文档时发生错误"}
)
# 可选的维护清理端点
@app.delete("/cleanup")
async def cleanup_old_files():
"""清理超过24小时的文件"""
try:
current_time = datetime.now()
for file_path in data_dir.glob("*.pdf"):
file_age = current_time - datetime.fromtimestamp(file_path.stat().st_mtime)
if file_age.days >= 1: # 超过24小时的文件
file_path.unlink()
return JSONResponse(content={"status": "success", "message": "清理完成"})
except Exception as e:
return JSONResponse(
status_code=500,
content={"status": "error", "message": f"清理失败:{str(e)}"}
)
首页文件
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Medical Document Analyzer</title>
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.7.2/dist/full.min.css" rel="stylesheet" type="text/css"/>
<script src="https://cdn.tailwindcss.com"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: {
sans: ['Poppins', 'sans-serif'],
},
},
},
daisyui: {
themes: ["light", "dark", "cupcake", "corporate"],
},
}
</script>
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- 添加Marked.js进行Markdown渲染 -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- 添加GitHub Markdown CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/github-markdown-css/5.2.0/github-markdown.min.css">
<style>
/* 自定义Markdown内容样式 */
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 100%;
padding: 1rem;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3 {
border-bottom: 1px solid var(--border-color);
padding-bottom: 0.3em;
}
.theme-dark .markdown-body {
color: #c9d1d9;
background-color: transparent;
}
.theme-dark .markdown-body h1,
.theme-dark .markdown-body h2,
.theme-dark .markdown-body h3 {
border-bottom-color: #30363d;
}
</style>
</head>
<body x-data="{
isUploading: false,
result: null,
errorMessage: null,
isDragging: false,
theme: 'light',
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', this.theme);
},
async handleSubmit(event) {
this.isUploading = true;
this.result = null;
this.errorMessage = null;
const formData = new FormData(event.target);
try {
const response = await fetch('/analyze-medical-document', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.status === 'success') {
this.result = data;
document.getElementById('results').scrollIntoView({ behavior: 'smooth' });
} else {
this.errorMessage = data.message;
}
} catch (error) {
this.errorMessage = '处理文档时出错';
}
this.isUploading = false;
},
// 添加Markdown渲染函数
renderMarkdown(text) {
if (!text) return '';
return marked.parse(text);
}
}">
<!-- 导航栏 -->
<div class="navbar bg-base-100 shadow-lg">
<div class="navbar-start">
<a class="btn btn-ghost text-xl">MedDoc 分析器</a>
</div>
<div class="navbar-end">
<label class="swap swap-rotate btn btn-ghost btn-circle" @click="toggleTheme">
<input type="checkbox"/>
<svg class="swap-on fill-current w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z"/></svg>
<svg class="swap-off fill-current w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z"/></svg>
</label>
</div>
</div>
<!-- 英雄部分 -->
<div class="hero min-h-[40vh] bg-base-200">
<div class="hero-content text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">医疗文档分析器</h1>
<p class="py-6">上传您的医疗文档,进行即时人工智能分析、总结和验证。</p>
<button class="btn btn-primary" onclick="document.getElementById('upload-section').scrollIntoView({behavior: 'smooth'})">开始上传</button>
</div>
</div>
</div>
<!-- 主内容 -->
<div class="container mx-auto px-4 py-8">
<!-- 上载部分 -->
<div id="upload-section" class="max-w-xl mx-auto">
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title">上传文档</h2>
<form @submit.prevent="handleSubmit">
<div class="form-control w-full"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
@drop.prevent="isDragging = false">
<label class="label">
<span class="label-text">选择一个文件或将其拖到此处</span>
</label>
<div class="border-2 border-dashed rounded-lg p-8 text-center transition-all duration-200"
:class="{'border-primary bg-primary/5': isDragging}">
<input type="file" name="file" class="file-input file-input-bordered w-full max-w-xs" required/>
<p class="mt-2 text-sm text-base-content/70">支持格式:PDF、DOC、TXT(最大10MB)</p>
</div>
</div>
<button type="submit" class="btn btn-primary w-full mt-4" :disabled="isUploading">
<span x-show="!isUploading">分析文档</span>
<span x-show="isUploading" class="loading loading-spinner"></span>
<span x-show="isUploading">处理中...</span>
</button>
</form>
</div>
</div>
</div>
<!-- 错误提示 -->
<div x-show="errorMessage"
x-transition
class="alert alert-error max-w-xl mx-auto mt-8">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span x-text="errorMessage"></span>
</div>
<!-- 结果部分 -->
<div id="results" x-show="result"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 transform -translate-y-4"
x-transition:enter-end="opacity-100 transform translate-y-0"
class="mt-8 space-y-8 max-w-4xl mx-auto">
<!-- 分析结果 -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-primary">文档分析结果</h2>
<div class="divider"></div>
<div class="markdown-body" x-html="renderMarkdown(`## 分析结果\n\n${result?.analysis}`)"></div>
</div>
</div>
<!-- 概要 -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-secondary">医疗概要</h2>
<div class="divider"></div>
<div class="markdown-body" x-html="renderMarkdown(`## 医疗概要\n\n${result?.summary}`)"></div>
</div>
</div>
<!-- 验证结果 -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-accent">诊断验证结果</h2>
<div class="divider"></div>
<div class="markdown-body" x-html="renderMarkdown(`## 验证结果\n\n${result?.validation}`)"></div>
</div>
</div>
</div>
</div>
<!-- Footer -->
<footer class="footer footer-center p-10 bg-base-300 text-base-content">
<aside>
<p>版权 © 2024 - 所有权利保留</p>
</aside>
</footer>
</body>
</html>
用户界面
我们打算做几项改进:
1. 📁 支持更多类型的文档
2. 🔄 增强的验证算法
3. 与医疗数据库的整合。
4. ⚙️ 自定义分析设置
- 📤 报表导出功能:
🎉 结束了!
医学文档分析器工具在医学文档自动处理方面迈出了一大步。通过结合现代互联网技术和先进的AI模型,我们开发出了一款能够显著提升医学文档分析效率的工具,同时确保准确性和可靠性。
这个应用展示了如何在医疗环境中实际操作人工智能以减轻人工负担并提高文书处理的准确度。随着我们不断开发和完善系统,我们预计它将变得越来越有用的工具,对于医疗专业人士和管理者来说。
参考 OCR 和文档理解 | Mistral AI 文档 OCR FastAPI 框架,高性能,易学,编码快,适合生产fastapi.tiangolo.com 介绍 | 🦜️🔗 LangChain 是一个用于开发由大型语言模型(LLM)驱动的应用程序的框架。python.langchain.com 组件 - Tailwind CSS 组件(版本 5 更新来袭)Tailwind CSS 组件实例 [在项目中引入 Alpine.js有两种方法可以将 Alpine.js 引入到你的项目中:
这两种方法都有效,这完全取决于项目的具体需求。
alpinejs.dev](https://alpinejs.dev/docs?source=post_page-----4e22bbd91b4f---------------------------------------)
点击这里 跟我联系