Flask编排EasyOCR与DeepSeek:多模态中医诊疗平台实践
简介这是一套基于Flask的多模态中医诊疗平台完整源码与使用说明面向中医健康咨询、智能问答及中医药知识展示场景适合对AI应用开发、图像文字识别或中医信息化感兴趣的初中级开发者。平台集成DeepSeek AI模型与EasyOCR图像识别支持文本、图片、文档、音频等多种输入方式并内置中药大全、方剂大全、知识文章与评论互动等模块。资源共112个文件包含41个HTML页面、22个Python后端文件、大量图片素材及4个操作演示视频压缩包整体约63.83MB代码结构清晰前端页面与后端逻辑分离便于二次开发。已有150人学习下载。借助该资源可快速搭建一套可运行的智能中医诊疗应用通过源码理解流式响应、会话历史、文件处理等关键实现并参照使用说明完成本地部署与功能扩展。1. 多模态中医诊疗平台先从“可运行”而不是“完美”开始我发现很多中医问诊系统把“多模态”做成了两张表单一张填主诉一张传照片最后交给模型时只用了文本照片被丢在数据库里当附件。这其实只是“多附件”不是多模态。真正能用的平台要让 DeepSeek 在同一个上下文里同时看到你的主诉和 EasyOCR 从图像里提取出来的观察信息再给出咨询建议。我会按一个最小可复现的源码结构把 Flask 编排、EasyOCR 图像识别、DeepSeek 模型调用这三段串起来并给出每个环节的参数与避坑点。适合 Flask 开发者、多模态应用初学者以及做软件综合实践选题的同学参考。2. 用 Flask 编排 DeepSeek 和 EasyOCR先理清多模态问诊请求链2.1 为什么是 Flask面向多模态交互的轻量编排层Flask 不负责图像识别也不负责大模型推理它只做三件事接收用户请求、调度两个子服务、聚合返回。这种编排层用 Flask 正合适因为它的请求上下文和路由规则比 FastAPI 更直观对刚接触多模态应用的人来说源码可读性比极端性能重要。中医健康咨询是低频交互单机 Flask 用开发服务器跑已经能演示要上生产时再用 gunicorn 起多进程也不改业务代码。另外Flask 的before_request和after_request钩子非常适合记录耗时和异常。多模态链路的耗时主要发生在 EasyOCR 和 DeepSeek 调用上如果中间件能记录每个阶段的耗时定位问题就很快。这也是我推荐用 Flask 做这个项目的原因它不是性能最强的但它能把“请求从进来到出去”这条线画得清清楚楚。2.2 标准化四诊数据模型让文本和图像在同一个结构里对齐多模态融合的关键不是把图片 base64 直接塞给文本模型而是先通过 EasyOCR 把图像转成带置信度的文字再和用户的主诉文本做拼接。这个拼接过程需要一个统一的数据结构下面给出我常用的字段设计。字段类型说明session_idstring会话ID用于多轮问诊complaint_textstring用户输入的主诉例如“口干舌燥”ocr_textstringEasyOCR 从图片中提取的文本ocr_confidencefloatOCR 平均置信度低于阈值时提醒image_typestring图片类型舌象/面色/报告单raw_image_pathstring原图保存路径便于回放model_replytextDeepSeek 返回的咨询建议created_atdatetime记录时间为什么一定要有ocr_confidence和image_type后面 DeepSeek 提示词里会用它们来区分“舌象文字”和“报告单文字”同时当置信度低于 0.6 时会加一句“观察信息可能不清晰”。这比把所有 OCR 结果一股脑塞给模型更可靠。实际上多模态融合论文里常说的特征对齐落到工程上并不是非要向量对齐而是在请求到达 DeepSeek 之前把图像信息翻译成文本并语义对齐。OCR 结果和主诉在同一个字符串里就是一个最朴素的特征对齐方式。2.3 写一个最小 Flask 应用和请求追踪中间件# app.py import tempfile import time from flask import Flask, request, jsonify app Flask(__name__) app.config[MAX_CONTENT_LENGTH] 8 * 1024 * 1024 # 限制上传 8MB # 保存图片到临时目录避免长期占用磁盘 TEMP_DIR tempfile.mkdtemp(prefixtcm_uploads_) app.before_request def log_request(): request.start_time time.time() app.after_request def log_response(response): cost_ms (time.time() - request.start_time) * 1000 app.logger.info(%s %s - %s, %.1fms, request.method, request.path, response.status_code, cost_ms) return response app.route(/health) def health(): return jsonify({status: ok}) if __name__ __main__: app.run(host0.0.0.0, port5000, debugTrue)逻辑说明before_request记录开始时间after_request输出耗时方便后面定位是 OCR 慢还是 DeepSeek 慢。MAX_CONTENT_LENGTH防止超大图片拖垮进程超过 8MB 时 Flask 会直接返回 413不需要手动判断。TEMP_DIR用tempfile.mkdtemp创建程序退出后由系统临时目录策略清理开发期不用管。参数说明host0.0.0.0方便局域网内的手机扫码测试但调试模式下不要暴露给公网。如果5000端口被占用把port改成5001即可。2.4 依赖清单与 PyCharm 运行方式新建requirements.txt内容如下flask3.0 easyocr1.7 openai1.30 requests2.31然后在 PyCharm 的终端执行pip install -r requirements.txt运行app.py。如果只是在命令行开发用python app.py也可以。openai库并不是调用 OpenAI 专用它支持自定义base_urlDeepSeek 控制台提供的接口兼容 OpenAI 协议所以用这个库最省事。如果不想引入 openai直接用 requests 也可以第 4 章会给两种写法的取舍。注意easyocr 首次运行会下载检测和识别模型最好先在有网环境执行一次最小代码把模型缓存下来。3. EasyOCR 图像识别落地舌象和面色怎么变成文本特征3.1 EasyOCR 在中医图像处理中的边界EasyOCR 的任务是“提取图片中的文字”它本身不判断舌象颜色。那中医场景怎么用常见的做法是用户上传的是检查报告单、中药方、舌诊报告这类带文字的照片EasyOCR 把字拉出来如果要识别舌象照片本身需要先准备一个图像分类模型。但标题里只集成了 EasyOCR所以本文按“图像文字提取”来落地。换句话说EasyOCR 贡献的是“能让 DeepSeek 读到的外带信息”和用户主诉一起进入模型。如果你想扩展成真正的舌象分析可以把 EasyOCR 换成一个舌苔颜色分类模型那是另一条技术路线。但在这个平台里OCR 的角色已经足够重要图片里的“舌红少苔”如果只靠用户手打漏字率会很高有了 OCR至少能把报告单上的文字原样提取。3.2 用 EasyOCR 提取图像文字Reader 与 readtext 的最小代码import easyocr # gpu 参数服务器没有 CUDA 时务必设置为 False reader easyocr.Reader([ch_sim, en], gpuFalse, verboseFalse) def extract_ocr(image_path: str) - list: results reader.readtext(image_path, detail1, paragraphFalse) # results 的每一项是 [box, text, confidence] return [(text, float(conf)) for _, text, conf in results] if __name__ __main__: for text, conf in extract_ocr(tongue_report.jpg): print(f{conf:.3f}: {text})逻辑说明Reader初始化时指定ch_sim识别简体中文en保留英文。gpuFalse在无 CUDA 的机器上不吃显存代价是回稍微慢一点。readtext返回的三元组里box是四个顶点坐标我们暂时用不到所以用下划线忽略。参数说明detail1表示返回每个文本块的置信度paragraphFalse表示不做段落合并避免把两行不同位置的文字拼成一句话。等后面需要上下文连贯时再开paragraphTrue也不迟。3.3 把 OCR 结果拼成语义化文本并过滤低置信度直接把所有 OCR 文本丢给 DeepSeek会因为图片里的水印、日期、无效数字而干扰回答。所以要加一个过滤与拼接函数def build_ocr_context(ocr_outputs, min_conf0.6): valid_lines [] used_results [] for text, conf in ocr_outputs: # 去掉空格、纯数字和过短的文本 cleaned text.strip().replace( , ) if len(cleaned) 2: continue if conf min_conf: continue valid_lines.append(cleaned) used_results.append((text, conf)) if not valid_lines: return None, 0.0 context .join(valid_lines[:10]) # 最多取10条防止提示词过长 avg_conf sum(c for _, c in used_results) / len(used_results) return context, avg_conf逻辑说明min_conf默认 0.6如果照片是手机随手拍的可以降到 0.45但 DeepSeek 会收到更多噪声。valid_lines[:10]控制输入长度一个舌诊报告上通常不会超过 10 条关键文字超过时优先保留置信度高的。函数返回值是(context, avg_conf)前端可以用avg_conf显示“图片识别可信度”。注意这里保存的是整理后的文本原始 OCR results 也应该单独留一份日志。特别是当模型回答离谱时你要能分辨是 OCR 识别错还是模型推理错。3.4 EasyOCR 的三个必调参数和避坑记录参数推荐值说明gpuFalse开发期关闭避免显存不足detail1返回置信度供过滤paragraphFalse关闭段落合并保留原始文本块decoderbeamsearch精度高但慢可改用 greedy 提速避坑记录中文模型必须选ch_sim选ch_tra会输出繁体图片旋转超过 45 度时识别率骤降最好在预处理时用 OpenCV 把图片摆正大图直接 readtext 会很慢先用cv2.resize把最长边限制到 1280。提示如果你不是提取文字而是想识别舌苔颜色等图像特征EasyOCR 并不擅长。这时候需要换成图像分类模型或者用 EasyOCR 训练自己的模型识别特定符号但那是独立的训练流程和本文的推断链路是两件事。4. DeepSeek API 调用封装多模态提示词才是真正融合点4.1 DeepSeek 模型与 OpenAI 兼容客户端的选型DeepSeek 官方提供了兼容 OpenAI 的 HTTP 接口所以用 openai 库把base_url指向 DeepSeek 即可。这是目前最省事的方案比直接 requests 少写签名和 header。如果你在研究本地部署 DeepSeek同一套 OpenAI 客户端也可以指向本地 vLLM 或 Ollama 服务只需要改base_url和model名。关于 DeepSeek API 如何调用最常见的错误是把api_key直接写在代码里。开发期可以临时用环境变量读取生产环境一定要走密钥管理服务。这个封装要区分“平台代码”和“密钥配置”否则源码发出去key 也跟着泄漏。4.2 设计多模态提示词模板核心是把complaint_text和ocr_context组合并给出明确的角色设定。SYSTEM_PROMPT ( 你是一位中医健康咨询顾问。请根据用户的主诉和观察信息 给出体质判断、调理建议和饮食建议。观察信息来自图像文字识别 如果观察信息为空或置信度低请明确说明图片信息不足。 ) def build_prompt(complaint_text, ocr_context, image_type): if ocr_context and len(ocr_context) 0: observation f{image_type}观察信息{ocr_context} else: observation 图片信息不足 return f用户主诉{complaint_text}{observation}\n请用一段话回答。逻辑说明image_type可以是“舌象”“面色”“报告单”把它放到描述文字里让 DeepSeek 理解这条 OCR 证据的来源。对比一下如果不做这个动作直接传“患者口干舌燥OCR舌红少苔”模型可能当成一句不连贯的摘要加上来源后模型会按中医逻辑解释舌红少苔和口干的关系。这就是多模态统一处理和普通拼接的区别。4.3 用 openai 库发起请求并处理异常from openai import OpenAI client OpenAI( api_keysk-your-key, # 替换成 DeepSeek 控制台的 key base_urlhttps://api.deepseek.com, # 以实际控制台地址为准 timeout30.0, ) def chat_once(complaint_text, ocr_context, image_type, temperature0.3): prompt build_prompt(complaint_text, ocr_context, image_type) try: resp client.chat.completions.create( modeldeepseek-chat, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: prompt}, ], temperaturetemperature, max_tokens800, streamFalse, ) return resp.choices[0].message.content except Exception as e: # 网络抖动或限流时降级为给出提示而不是让整个平台崩溃 return f服务暂时不可用请稍后再试{type(e).__name__}参数说明temperature设为 0.3让咨询建议保持稳定避免同一问题两次回答差异太大。max_tokens配 800足够覆盖体质判断、调理建议和饮食建议。如果出现APITimeoutError或RateLimitError上面的兜底会返回一段提示前端不用改就能展示。调用时不要在提示词里放患者真实姓名可以在 API 之外做匿名化。4.4 把回复结构化为 JSON 并写入问诊记录中医咨询建议如果只返回一大段话前端很难做结构化展示。常见的做法是在提示词里要求模型输出 JSON然后用json.loads解析兜底。import json, re def get_structured_reply(complaint_text, ocr_context, image_type): prompt build_prompt(complaint_text, ocr_context, image_type) prompt \n请严格按下面的 JSON 格式返回{\constitution\:\体质\, \advice\:\调理建议\, \diet\:\饮食建议\} raw chat_once(complaint_text, ocr_context, image_type) try: data json.loads(raw) except json.JSONDecodeError: # 兼容模型在 JSON 前后附加了说明文字 match re.search(r\{.*\}, raw, re.S) data json.loads(match.group(0)) if match else {constitution: 未知, advice: raw, diet: } return raw, data逻辑说明先强制要求 JSON再用正则把第一个{...}抓出来这样即使模型多说了两句也能解析。不要把 JSON 解析错误直接抛出来否则用户会看到 500。最后把raw原文和data同时入库方便回溯模型原始返回。参数示例值作用modeldeepseek-chat选择对话模型temperature0.3控制回复随机性max_tokens800控制输出的最大长度streamFalse开发期关闭方便调试5. 用“问诊快照”验证多模态管道是否真的打通5.1 把原始请求与回复存成一份 JSON开发多模态平台时最大的问题不是模型答得不对而是你无法判断“答得不对”是因为 OCR 没提取到、提示词没传对还是 DeepSeek 本身跑偏。所以我习惯在每个环节都留快照把用户上传的图片 OCR 结果、提示词、DeepSeek 原始返回一起存为 JSON 文件。这样不需要前端就能验证每个环节是否真的用了多模态信息。import json, datetime def save_snapshot(session_id, complaint, ocr_text, ocr_conf, raw_reply, structured): record { session_id: session_id, complaint: complaint, ocr_context: ocr_text, ocr_confidence: round(ocr_conf, 3) if ocr_conf else None, raw_reply: raw_reply, structured: structured, created_at: datetime.datetime.now().isoformat(), } path fsnapshots/{session_id}.json with open(path, w, encodingutf-8) as f: json.dump(record, f, ensure_asciiFalse, indent2) return path保存raw_reply而不是只保存结构化字段是因为模型返回的原文里可能藏着“图片信息不足”之类的重要内容。这相当于保留源代码与原始版本避免只留下解析后的结果后面想排查都无从下手。5.2 用一条 curl 命令做全链路回归假设你已经把 OCR 和 DeepSeek 封装到/api/consult路由里验证命令是curl -X POST http://127.0.0.1:5000/api/consult \ -F phototongue_report.jpg \ -F complaint口干舌燥 \ -F session_idtest_001返回的 JSON 应该包含ocr_text、ocr_confidence、advice三个字段。如果ocr_text为空去查 EasyOCR 的模型有没有下好如果ocr_confidence很低去检查照片光照如果ocr_text正常但advice没有相关内容去检查build_prompt里的observation是否真的拼接进去了。这个技巧的价值在于它把多模态融合算法落地成一个可观察、可回放、可回归的闭环。后续再调 DeepSeek 的temperature或 EasyOCR 的min_conf只需要重放这批快照文件名对比structured字段的变化就能快速判断改动是否有效。本文还有配套的精品资源点击获取