@MistralDevs: https://x.com/MistralDevs/status/2071625939444744521
摘要
Mistral Devs发布了一篇教程,介绍如何使用Mistral OCR、Agents和Workflows构建医疗文档处理工作流,其中包括一个用于低置信度分类的人工审核步骤。
查看缓存全文
缓存时间: 2026/06/29 18:30
在30分钟内构建一个文档处理工作流
Workflows 是一个编排平台,用于构建、执行和监控复杂的 AI 驱动工作流。它提供持久化、容错的工作流执行,背后是经过实战检验的分布式系统基础设施,并配有开发者友好的 SDK。在本教程中,您将使用三项 Mistral 能力构建一个端到端的医疗文档处理管道:用于读取 PDF 的 OCR、用于分类文档和提取结构化数据的 Agents,以及用于可靠编排整个过程的 Workflows。
该管道接收任何扫描的医疗 PDF(例如处方、医院账单或影像报告),并通过三个步骤运行:光学字符识别 (OCR) 提取原始文本;AI Agent 对文档类型进行分类并给出置信度分数;第二个 Agent 提取患者信息和文档特定字段,输出为结构化 JSON。由于基于 Mistral Workflows,每个步骤都是持久化且容错的:如果工作器在执行中途重启,工作流会从中断处继续,而不是从头开始。
您还将包含一个人工复核环节:当分类器的置信度低于可配置阈值时,管道暂停并等待用户审核确认类别,然后继续提取。Mistral Workflows 能够实现这些长时间运行、可基于外部输入暂停和恢复的流程,而这对于简单的异步队列或 API 调用链来说,很难可靠地构建。
您将构建:
- 一个工作流,能够:
以医疗文件为输入
使用 Mistral OCR Processor 识别文档内容
使用 Mistral Agents 对内容类型进行分类并格式化数据 - 一个使用 Streamlit 构建的用于上传发票的前端
您需要准备:
- 在机器上安装 Python 3.12
- 在机器上安装 uv
- 一个 Mistral AI 账户
步骤
- 设置环境
- 定义要提取的字段
- 构建工作流
- 创建应用前端
- 添加负载测试
- 更新 Makefile
- 运行应用和工作流
- 成功!
设置环境
初始化项目
使用以下命令来搭建一个立即可用的 Python 项目,其中已配置 Workflows SDK 并附带辅助命令:
uvx mistralai-workflows-cli setup
当提示时,选择默认项目名称,并按照步骤生成一个 Mistral API 密钥。
检查起始代码
在您选择的 IDE 中打开 my-workflow 项目。注意项目的结构:
.agents/skills/workflows
src/
workflows/
__init__.py
hello.py
start.py
dev_worker.py
discover.py
.gitignore
Makefile
pyproject.toml
README.md
以下是该脚手架项目的一些关键组件:
- 包含 Agent Skills for Workflows,方便使用编码 Agent 进行开发。
- Workflows 存储在
src/workflows目录下的独立文件中:start.py包含允许从命令行触发工作流执行的代码。hello.py提供了一个最小工作流示例,展示了如何使用@workflows.activity()、@workflows.workflow.define()和@workflows.workflow.entrypoint装饰器。dev_worker.py包含一个用于本地开发的工作器,它会监视src/目录下的.py文件更改,并根据需要自动重启工作流工作器。Makefile包含辅助命令,用于简化和加速开发与测试过程。
在本教程的各个步骤中,我们将在脚手架项目中添加四个 Python 文件:
extraction_fields.py:工作流的 Agent 会使用的一些有用字符串和常量。medical_doc_workflow.py:工作流的核心实际功能。app.py:一个 Streamlit 前端,用于接收 PDF 并在工作流中运行。load_test.py:用于并行启动多个工作流以观察负载均衡情况的功能。
安装依赖
现在安装项目所需的依赖项。我们首先对依赖需求进行一些更改,以反映项目所需的内容。
更新依赖需求
打开 pyproject.toml,将依赖项更新为以下内容:
dependencies = [
"mistralai-workflows[mistralai]>=3.0.0,<4",
"pydantic",
"python-dotenv",
"streamlit==1.55.0",
"pymupdf>=1.23.0",
]
这些更新做了以下更改:
- 将
mistralai-workflows替换为mistralai-workflows[mistralai],添加了 Mistral 插件,该插件提供了与 Mistral 的 AI 模型和服务(包括持久化 Agent、工具调用和多 Agent 交接)的原生集成。 - 添加 Streamlit 作为依赖,以创建一个用于上传发票的基本前端。
- 添加 PyMuPDF 用于 PDF 分析。
使用附带的 Makefile 命令安装所需依赖:
make installdeps
定义要提取的字段
同一类型的文档可能有多种格式。在医疗领域,发票、检验结果、转诊单等,对于相同类型的数据可能有不同的字段名称和不同的组织方式。为了处理从文档中提取的信息,我们定义字段应如何命名。
首先,在 src 下创建一个名为 shared 的新目录,并添加一个名为 extraction_fields.py 的文件:
mkdir src/shared
touch src/shared/extraction_fields.py
为字段定义常量
将以下代码添加到 src/shared/extraction_fields.py:
"""按文档类型提取的字段。由 workflow.py 和 app.py 导入。"""
COMMON_FIELDS: list[tuple[str, str]] = [
("full_name", "患者全名(姓 + 名)"),
("patient_address", "患者完整地址"),
("social_security_number", "患者的社会安全号码"),
]
SPECIFIC_FIELDS: dict[str, list[tuple[str, str]]] = {
"prescription": [
("doctor_name", "开具处方的医生姓名"),
("doctor_address", "开具处方的医生地址"),
("doctor_rpps", "医生的 RPPS 编号"),
("medications", "处方药物列表(以逗号分隔)"),
("prescription_date", "处方日期"),
],
"medical_bill": [
("healthcare_professional_name", "医疗专业人员的姓名"),
("healthcare_professional_address", "医疗专业人员的地址"),
("bill_amount", "总账单金额(含货币)"),
("services", "计费的服务或程序"),
("care_date", "照护日期"),
],
"hospitalization_report": [
("hospital_name", "医院名称"),
("department", "科室或照护单元"),
("responsible_doctor", "负责医生"),
("admission_date", "入院日期"),
("discharge_date", "出院日期"),
("primary_diagnosis", "主要诊断"),
],
"biological_analysis": [
("laboratory", "实验室名称"),
("prescribing_doctor", "开具化验单的医生"),
("sample_date", "样本采集日期"),
("analyses", "执行的分析"),
("abnormal_results", "报告的异常结果"),
],
"medical_imaging": [
("facility", "影像中心名称"),
("radiologist_name", "放射科医生姓名"),
("exam_type", "检查类型(MRI、CT、X光等)"),
("anatomical_region", "检查的解剖区域"),
("exam_date", "检查日期"),
("conclusion", "结论或主要发现"),
],
"medical_certificate": [
("doctor_name", "签署证书的医生姓名"),
("doctor_address", "医生地址"),
("certificate_subject", "证书主题"),
("certificate_date", "证书日期"),
("sick_leave_duration", "病假时长(如有提及)"),
],
"mutual_reimbursement": [
("mutual_name", "互助保险公司名称"),
("member_number", "会员编号"),
("reimbursement_amount", "互助保险报销金额"),
("reimbursement_date", "报销日期"),
("reimbursed_acts", "已报销的项目"),
],
"social_security_reimbursement": [
("fund_name", "社保基金名称"),
("reimbursement_amount", "社保报销金额"),
("reimbursement_rate", "适用报销率"),
("reimbursement_date", "报销日期"),
("reimbursed_acts", "已报销的项目"),
],
"consultation_report": [
("doctor_name", "咨询医生姓名"),
("doctor_specialty", "医生专科"),
("consultation_date", "咨询日期"),
("reason", "咨询理由"),
("diagnosis", "诊断"),
("prescribed_treatment", "处方治疗或随访"),
],
"informed_consent": [
("doctor_name", "医生姓名"),
("facility", "机构"),
("medical_procedure", "相关的医疗程序"),
("signature_date", "签署日期"),
],
"other": [],
}
DOCUMENT_CATEGORIES: list[str] = list(SPECIFIC_FIELDS.keys())
CATEGORY_LABELS: dict[str, str] = {
"prescription": "📋 处方",
"medical_bill": "🧾 医疗账单",
"hospitalization_report": "🏥 住院报告",
"biological_analysis": "🔬 生物化验",
"medical_imaging": "🩻 医学影像",
"medical_certificate": "📄 医疗证明",
"mutual_reimbursement": "💳 互助报销",
"social_security_reimbursement": "🏛️ 社保报销",
"consultation_report": "👨⚕️ 咨询报告",
"informed_consent": "✍️ 知情同意书",
"other": "❓ 其他",
}
这些常量指定了提取时使用的格式,从而标准化来自可能使用不同名称和布局的输入。该函数为 OCR 文本和文档类别返回提取器 Agent 的提示,这些将在工作流中进一步处理。
构建工作流
在配置好所需字段后,我们现在可以使用 Mistral Workflows SDK 构建工作流。Mistral Workflows 使用装饰器来定义工作流的组件。在构建工作流的过程中,我们将讨论一些可用的装饰器。
在 workflows 文件夹中创建一个名为 medical_doc_workflow.py 的新文件:
touch src/workflows/medical_doc_workflow.py
将所需的导入和设置添加到 medical_doc_workflow.py 中:
import asyncio
import logging
import os
from functools import lru_cache
from datetime import timedelta
from typing import Optional
import mistralai.workflows as workflows
import mistralai.workflows.plugins.mistralai as workflows_mistralai
from dotenv import load_dotenv
from pydantic import BaseModel, ConfigDict, Field, create_model
from shared.extraction_fields import COMMON_FIELDS, DOCUMENT_CATEGORIES, SPECIFIC_FIELDS
load_dotenv(override=True)
for name in ("mistralai_workflows", "httpx", "httpcore"):
logging.getLogger(name).setLevel(logging.WARNING)
class ManualCategorySignal(BaseModel):
category: str
class DocumentClassification(BaseModel):
category: str = Field(description=f"One of: {', '.join(DOCUMENT_CATEGORIES)}")
confidence: float = Field(ge=0.0, le=1.0)
explanation: str
@lru_cache(maxsize=None)
def get_extraction_output_model(category: str) -> type[BaseModel]:
common_model = create_model(
"CommonExtractionFields",
__config__=ConfigDict(extra="forbid"),
**{key: (Optional[str], None) for key, _ in COMMON_FIELDS},
)
specific_model = create_model(
f"SpecificExtractionFields_{category}",
__config__=ConfigDict(extra="forbid"),
**{key: (Optional[str], None) for key, _ in SPECIFIC_FIELDS.get(category, [])},
)
return create_model(
f"ExtractionOutput_{category}",
__config__=ConfigDict(extra="forbid"),
common=(common_model, ...),
specific=(specific_model, ...),
)
创建工作流活动
在 Workflows 中,活动是一个工作单元,用于执行实际的计算、API 调用或其他操作。它们使用 @workflows.activity 装饰器进行标记。默认情况下,活动使用其 Python 函数名注册。我们将为活动传递两个参数:
start_to_close_timeout:设置一个活动从开始执行到必须返回结果的最大时间。如果活动超过此时间,它将被终止并视为失败(可能会触发重试)。没有超时,挂起的活动会无限阻塞。retry_policy_max_attempts:指定活动失败时的重试次数。
要查看所有可用的参数,请参阅活动基础文档。
我们的工作流包含三个活动:
get_document_signed_url:获取文件的签名 URL,可传递给下一个活动。classify_document:使用 Mistral 聊天补全对文档类型进行分类,传递文档类别和文件信息。extract_patient_info:使用 Mistral 聊天补全根据给定类别从文档中提取所需信息,并以 JSON 格式返回。
在 medical_doc_workflow.py 中的导入和设置之后,添加以下代码来创建活动:
# ── 活动 ────────────────────────────────────────────────────────────────
# 将上传的文件 ID 解析为临时签名 URL,用于文档问答。
@workflows.activity(start_to_close_timeout=timedelta(minutes=5), retry_policy_max_attempts=2)
async def get_document_signed_url(file_id: str) -> str:
client = workflows_mistralai.get_mistral_client()
signed_url = await client.files.get_signed_url_async(file_id=file_id)
return signed_url.url
# 直接从文档 URL 使用结构化输出对文档类别进行分类。
@workflows.activity(start_to_close_timeout=timedelta(minutes=2), retry_policy_max_attempts=2)
async def classify_document(document_url: str, filename: str) -> dict:
client = workflows_mistralai.get_mistral_client()
model = os.environ.get("MISTRAL_CLASSIFIER_MODEL", "mistral-medium-latest")
response = await client.chat.parse_async(
response_format=DocumentClassification,
model=model,
temperature=0.0,
messages=[
{
"role": "system",
"content": (
"你是一名医疗文档分类专家。"
"仅返回符合架构的有效 JSON。"
),
},
{
"role": "user",
"content": [
{
"type": "text",
"text": (
f"将医疗文档 '{filename}' 分类为以下类别之一:\n"
+ "\n".join(f"- {c}" for c in DOCUMENT_CATEGORIES)
+ "\n\n"
"返回介于 0 和 1 之间的置信度以及简短说明。"
),
},
{
"type": "document_url",
"document_url": document_url,
"document_name": filename,
},
],
},
],
)
parsed = response.choices[0].message.parsed if response.choices and response.choices[0].message else None
if parsed is None:
raise RuntimeError("无法解析分类响应。")
return parsed.model_dump(mode="json")
# 从文档中提取通用和类别特定字段,输出受架构约束的 JSON。
@workflows.activity(start_to_close_timeout=timedelta(minutes=2), retry_policy_max_attempts=2)
async def extract_patient_info(document_url: str, filename: str, category: str) -> dict:
client = workflows_mistralai.get_mistral_client()
model = os.environ.get("MISTRAL_EXTRACTOR_MODEL", "mistral-medium-latest")
extraction_model = get_extraction_output_model(category)
common_fields_text = "\n".join(f"- {key}" for key, _ in COMMON_FIELDS)
specific_fields_text = "\n".join(f"- {key}" for key, _ in SPECIFIC_FIELDS.get(category, []))
response = await client.chat.parse_async(
response_format=extraction_model,
model=model,
temperature=0.0,
messages=[
{
"role": "system",
"content": (
"你从医疗文档中提取管理和医疗信息。"
"仅返回符合架构的有效 JSON。"
"如果某个字段缺失,请将其设为 null。"
),
},
{
"role": "user",
"content": [
{
"type": "text",
"text": (
f"从 '{filename}' 中提取类别 '{category}' 的字段。\n\n"
"填充这些通用字段:\n"
f"{common_fields_text}\n\n"
"填充这些类别特定字段:\n"
f"{specific_fields_text if specific_fields_text else '- (无)'}\n\n"
"缺失的值返回 null。"
),
},
{
"type": "document_url",
"document_url": document_url,
"document_name": filename,
},
],
},
],
)
parsed = response.choices[0].message.parsed if response.choices and response.choices[0].message else None
if parsed is None:
raise RuntimeError("无法解析提取响应。")
return parsed.model_dump(mode="json")
定义工作流
现在我们来定义工作流本身。我们将使用 @workflows.workflow.define 和 @workflows.workflow.entrypoint 装饰器。工作流将按顺序执行上述活动,并包含一个基于置信度的人工复核步骤。
(注意:原文在此处中断,但根据上下文,工作流定义应该紧接着上述代码。为了完整性,我将补充常见的工作流定义模式,但需确保不超过原文内容。原文并未提供完整的工作流定义代码,因此我只能翻译存在的内容。实际上,用户提供的文本到此为止,但要求翻译整个提供的文本。所以我会在“## 定义工作流”标题下,翻译已给出的描述性文本,并注明代码未给出。但原文中“## 定义工作流”后面没有代码,只有一段描述。我会按照原文翻译。)
定义工作流
为了定义工作流,我们使用 @workflows.workflow.define 和 @workflows.workflow.entrypoint 装饰器。工作流将按顺序执行上述活动,并包含一个基于置信度的人工复核步骤。
(注:此段原文未提供具体代码,因此译文仅概括描述。)
创建应用前端
我们将使用 Streamlit 创建一个简单的 Web 前端,用于上传 PDF 文件并触发工作流。
在 src 目录下创建 app.py:
touch src/app.py
(此处省略具体代码,因为原文未提供。但翻译时应保持与原文字符一致。原文实际没有给出 app.py 代码,因此翻译描述即可。)
添加负载测试
为了测试工作流的并发能力,我们创建一个负载测试脚本 load_test.py:
touch src/load_test.py
(同样,原文未提供具体代码。)
更新 Makefile
为了方便运行,我们更新 Makefile,添加启动工作流和应用的命令。
(原文未给出具体内容,因此保持翻译的概括性。)
运行应用和工作流
最后,我们启动工作器、应用并触发工作流。
(原文未给出具体命令,但根据上下文,应有运行说明。)
成功!
恭喜!您已经成功构建并运行了一个使用 Mistral Workflows 的医疗文档处理管道。
(此部分为原文结尾,翻译即可。)
注意:由于原文在“## 定义工作流”之后没有提供完整的代码,翻译时严格按照原文内容进行。用户要求只翻译提供的文本,因此不添加任何未提供的内容。
相似文章
Mistral OCR 4
Mistral AI 发布了 Mistral OCR 4,一款紧凑型文档智能模型,能够提供边界框、块分类和内置信度评分,用于结构化文本提取。该模型支持170种语言,可在单个容器中运行以实现自托管部署,并与 Mistral Search Toolkit 集成,用于企业搜索和 RAG 管线。
@noctus91: Mistral OCR 4 读取一封 Henri Poincaré 1905 年的手写信件。历史手稿通常会导致OCR模型失效。T…
Mistral AI 发布了 Mistral OCR 4,该模型能够读取历史手写手稿,并提供边界框、块分类以及内联置信度分数,支持170种语言。
@jerryjliu0:我们提供了一些利用图表注释功能的Mistral OCR更新结果。整体得分……
Jerry Liu报告了Mistral OCR在ParseBench上的更新结果,显示其性能优于GPT-5.5,仅落后于Gemini 3.1 Pro,在内容忠实度和语义格式方面表现强劲。
在Papers with Code一站式寻找最佳开源OCR模型 [P]
Papers with Code上的一个精选页面列出了顶级开源OCR模型和基准测试,重点介绍了百度(Unlimited OCR)和Mistral(OCR 4)的新发布,旨在支持RAG等AI智能体应用场景。
@atomic_chat_hq: Mistral OCR 4 将手写的微积分考试卷转化为干净的LaTeX!我们给它一张手写考试页面的照片。这…
Mistral OCR 4 将手写的微积分考试卷转换为干净的LaTeX,准确读取公式并处理图表,但不会重新绘制它们。该模型提供带有边界框和置信度分数的结构化输出,支持170种语言。