简介:这是一套基于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_id | string | 会话ID,用于多轮问诊 |
| complaint_text | string | 用户输入的主诉,例如“口干舌燥” |
| ocr_text | string | EasyOCR 从图片中提取的文本 |
| ocr_confidence | float | OCR 平均置信度,低于阈值时提醒 |
| image_type | string | 图片类型:舌象/面色/报告单 |
| raw_image_path | string | 原图保存路径,便于回放 |
| model_reply | text | DeepSeek 返回的咨询建议 |
| created_at | datetime | 记录时间 |
为什么一定要有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(prefix="tcm_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(host="0.0.0.0", port=5000, debug=True)逻辑说明:before_request记录开始时间,after_request输出耗时,方便后面定位是 OCR 慢还是 DeepSeek 慢。MAX_CONTENT_LENGTH防止超大图片拖垮进程,超过 8MB 时 Flask 会直接返回 413,不需要手动判断。TEMP_DIR用tempfile.mkdtemp创建,程序退出后由系统临时目录策略清理,开发期不用管。
参数说明:host="0.0.0.0"方便局域网内的手机扫码测试,但调试模式下不要暴露给公网。如果5000端口被占用,把port改成5001即可。
2.4 依赖清单与 PyCharm 运行方式
新建requirements.txt,内容如下:
flask>=3.0 easyocr>=1.7 openai>=1.30 requests>=2.31然后在 PyCharm 的终端执行pip install -r requirements.txt,运行app.py。如果只是在命令行开发,用python app.py也可以。openai库并不是调用 OpenAI 专用,它支持自定义base_url,DeepSeek 控制台提供的接口兼容 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'], gpu=False, verbose=False) def extract_ocr(image_path: str) -> list: results = reader.readtext(image_path, detail=1, paragraph=False) # 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保留英文。gpu=False在无 CUDA 的机器上不吃显存,代价是回稍微慢一点。readtext返回的三元组里,box是四个顶点坐标,我们暂时用不到,所以用下划线忽略。
参数说明:detail=1表示返回每个文本块的置信度,paragraph=False表示不做段落合并,避免把两行不同位置的文字拼成一句话。等后面需要上下文连贯时,再开paragraph=True也不迟。
3.3 把 OCR 结果拼成语义化文本并过滤低置信度
直接把所有 OCR 文本丢给 DeepSeek,会因为图片里的水印、日期、无效数字而干扰回答。所以要加一个过滤与拼接函数:
def build_ocr_context(ocr_outputs, min_conf=0.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 的三个必调参数和避坑记录
| 参数 | 推荐值 | 说明 |
|---|---|---|
| gpu | False | 开发期关闭,避免显存不足 |
| detail | 1 | 返回置信度,供过滤 |
| paragraph | False | 关闭段落合并,保留原始文本块 |
| decoder | beamsearch | 精度高但慢,可改用 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_key="sk-your-key", # 替换成 DeepSeek 控制台的 key base_url="https://api.deepseek.com", # 以实际控制台地址为准 timeout=30.0, ) def chat_once(complaint_text, ocr_context, image_type, temperature=0.3): prompt = build_prompt(complaint_text, ocr_context, image_type) try: resp = client.chat.completions.create( model="deepseek-chat", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": prompt}, ], temperature=temperature, max_tokens=800, stream=False, ) 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同时入库,方便回溯模型原始返回。
| 参数 | 示例值 | 作用 |
|---|---|---|
| model | deepseek-chat | 选择对话模型 |
| temperature | 0.3 | 控制回复随机性 |
| max_tokens | 800 | 控制输出的最大长度 |
| stream | False | 开发期关闭,方便调试 |
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 = f"snapshots/{session_id}.json" with open(path, "w", encoding="utf-8") as f: json.dump(record, f, ensure_ascii=False, indent=2) return path保存raw_reply而不是只保存结构化字段,是因为模型返回的原文里可能藏着“图片信息不足”之类的重要内容。这相当于保留源代码与原始版本,避免只留下解析后的结果,后面想排查都无从下手。
5.2 用一条 curl 命令做全链路回归
假设你已经把 OCR 和 DeepSeek 封装到/api/consult路由里,验证命令是:
curl -X POST http://127.0.0.1:5000/api/consult \ -F "photo=@tongue_report.jpg" \ -F "complaint=口干舌燥" \ -F "session_id=test_001"返回的 JSON 应该包含ocr_text、ocr_confidence、advice三个字段。如果ocr_text为空,去查 EasyOCR 的模型有没有下好;如果ocr_confidence很低,去检查照片光照;如果ocr_text正常但advice没有相关内容,去检查build_prompt里的observation是否真的拼接进去了。
这个技巧的价值在于,它把多模态融合算法落地成一个可观察、可回放、可回归的闭环。后续再调 DeepSeek 的temperature或 EasyOCR 的min_conf,只需要重放这批快照文件名,对比structured字段的变化,就能快速判断改动是否有效。
本文还有配套的精品资源,点击获取