基于大模型的中医诊断系统实战:四诊合参、证型推理与本地部署

发布时间:2026/10/9 7:21:34
基于大模型的中医诊断系统实战:四诊合参、证型推理与本地部署
简介这是一套面向Java Web初学者与高校课程设计者的中医智能诊断项目源码以SpringBoot搭建简约大气的百科查询式界面并接入阿里通义千问大模型充当智能医生实现中医知识咨询与辅助诊断适合项目答辩、毕业设计或AIGC入门练手。压缩包共17个文件约94.13MB包含5个Python脚本、4个文本说明、2个dat人脸关键点模型、2份md文档以及mp4、mp3、avi演示素材和index索引文件覆盖前端交互、模型调用与养生知识库等模块。目前已有125人学习下载。读者可从中获得完整可运行的项目结构、大模型API调用示例、人脸特征点检测与rPPG相关脚本以及配套演示视频与说明文档便于快速理解AIGC与传统医学结合的落地思路并在此基础上二次开发或撰写论文。1. 从一份中医诊断系统压缩包说起大模型到底能诊什么把「一个基于AI大模型的中医诊断系统.zip」解压后多数人第一反应是找模型权重第二反应是找问诊入口第三反应是发现真正难的不是模型而是怎么把「望闻问切」翻译成模型能吃的输入。中医诊断的核心链路是四诊合参望诊看舌象面色闻诊听声息气味问诊收集主诉与病史切诊取脉象。大模型能直接处理的是问诊文本能间接处理的是舌象图像和脉象时序信号而「辨证论治」这一步本质是一个带强领域约束的推理任务。这个系统要解决的不是替代中医师而是把结构化问诊、舌象特征提取、证型推理和方剂推荐串成一条可复现的工程链路。适合谁做有 Python 基础、想跑通一个垂直领域大模型应用落地的开发者以及想把中医诊断流程数字化的从业者。32G 内存能装 AI 大模型吗能但只能跑量化后的 7B 级别模型这是后面部署章节要算的账。2. 中医诊断系统的大模型选型7B、13B 还是 API 调用2.1 为什么中医辨证不适合直接套通用大模型通用大模型在中医辨证上的最大问题是「证型漂移」。你输入「舌红苔黄、脉数、口渴」它可能给你输出「阴虚火旺」也可能输出「实热证」甚至混着说。原因在于中医证型之间不是互斥分类而是有兼证、转化和主次关系。通用模型的训练语料里中医数据占比极低它对「肝郁脾虚」和「肝胃不和」的边界没有稳定认知。常见做法是用通用大模型做问诊对话和症状归一化用微调或 RAG 做证型推理两者分开。我一般会把证型推理做成「检索 约束解码」先从证型知识库召回候选证型再让模型在候选集里选而不是让它自由生成。另一个坑是术语标准化。用户说「拉肚子」系统得映射到「泄泻」用户说「睡不着」得映射到「不寐」。这一步不做后面检索和推理全是噪声。中医诊断系统里症状归一化模块的准确率直接决定最终辨证质量比换更大的模型更有效。2.2 本地部署 7B 量化模型的最小配置与命令如果走本地路线32G 内存的机器可以跑 7B 的 4-bit 量化模型。用 llama.cpp 或 Ollama 都行这里给一个 llama.cpp 的加载命令适合已经转成 GGUF 格式的模型文件。# 加载 7B 4-bit 量化模型上下文 4096CPU 推理 ./llama-cli \ -m ./models/qwen2-7b-instruct-q4_k_m.gguf \ -c 4096 \ -n 512 \ --temp 0.3 \ --top-p 0.9 \ -p 你是一名中医师请根据以下症状进行辨证舌红苔黄脉数口渴喜冷饮小便短赤。这段命令里-c 4096是上下文长度中医问诊多轮对话建议不低于 4096--temp 0.3是温度辨证任务要低温度保证稳定不要用默认的 0.8--top-p 0.9控制采样范围。-n 512是最大生成 token 数方剂推荐一般够用。如果内存吃紧把-c降到 2048但多轮问诊会丢历史。量化等级选q4_k_m是精度和内存的平衡点q3以下辨证质量下降明显血泪经验是别省这点内存。2.3 用 API 调用大模型做问诊对话的取舍如果本地机器只有 16G 内存或者不想折腾量化走 API 是更现实的选择。API 路线的优势是模型能力强、无需维护劣势是数据出域和调用成本。中医问诊涉及用户健康信息如果做的是内部工具或研究原型API 可以接受如果面向真实用户得考虑数据合规。代码上用 OpenAI 兼容接口调用的最小示例如下。from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://api.example.com/v1 # 替换为实际兼容接口地址 ) def consult(symptoms: str) - str: resp client.chat.completions.create( modelqwen2-7b-instruct, messages[ {role: system, content: 你是一名中医师请按四诊合参进行辨证输出证型、治法、代表方剂。}, {role: user, content: symptoms} ], temperature0.3, max_tokens512 ) return resp.choices[0].message.content print(consult(舌红苔黄脉数口渴喜冷饮小便短赤。))这里temperature0.3和本地部署逻辑一致辨证任务不要高温度。system提示词里明确要求输出结构否则模型会写成散文。实际项目中我会在system里加一句「如果症状不足以辨证请追问缺失信息」这样多轮问诊才能跑起来。API 和本地部署不是二选一常见做法是问诊对话走 API证型推理走本地小模型加知识库兼顾体验和成本。3. 把四诊数据喂给大模型问诊结构化与舌象特征提取3.1 问诊文本的结构化抽取与症状归一化大模型拿到一段自由文本问诊记录第一步是抽症状。比如「最近老觉得累吃饭没胃口大便不成形早上起来嘴里发苦」需要抽成{疲劳, 纳差, 便溏, 口苦}。这一步用大模型做 few-shot 抽取效果稳定关键是给足示例。下面是一个可复用的抽取函数。import json EXTRACT_PROMPT 从以下问诊文本中抽取中医症状输出 JSON 数组每个症状用标准中医术语。 示例输入最近老觉得累吃饭没胃口大便不成形 示例输出[疲劳, 纳差, 便溏] 现在输入{text} 只输出 JSON不要解释。 def extract_symptoms(text: str) - list: resp client.chat.completions.create( modelqwen2-7b-instruct, messages[{role: user, content: EXTRACT_PROMPT.format(texttext)}], temperature0.1, max_tokens256 ) raw resp.choices[0].message.content.strip() # 去掉可能的 markdown 代码块标记 raw raw.replace(json, ).replace(, ).strip() return json.loads(raw)temperature0.1是抽取任务的标准配置越低越稳定。max_tokens256对单次问诊够用。这里有个坑模型有时会在 JSON 前后加「好的以下是抽取结果」这类废话所以代码里做了replace清洗。更稳的做法是用 JSON mode 或 function calling但需要模型支持。症状归一化还需要一张同义词表比如「拉肚子→泄泻」「睡不着→不寐」这张表建议人工维护不要指望模型自己对齐。3.2 舌象图像的特征提取与量化描述舌象是望诊的核心。大模型本身不直接处理图像除非用多模态模型。常见做法是先用 CV 模型提取舌色、苔色、苔厚、裂纹等特征再把特征转成文本描述喂给大模型。舌色分类可以用一个轻量 CNN苔厚可以用分割后的面积比。下面是一个用 OpenCV 做舌体区域提取和颜色统计的示例。import cv2 import numpy as np def extract_tongue_features(img_path: str) - dict: img cv2.imread(img_path) img cv2.resize(img, (512, 512)) hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 舌体区域大致在图像中央用椭圆掩膜粗定位 mask np.zeros(img.shape[:2], dtypenp.uint8) cv2.ellipse(mask, (256, 256), (180, 220), 0, 0, 360, 255, -1) tongue cv2.bitwise_and(img, img, maskmask) # 统计舌体区域的平均色调和饱和度 h_mean hsv[:, :, 0][mask 255].mean() s_mean hsv[:, :, 1][mask 255].mean() v_mean hsv[:, :, 2][mask 255].mean() return {hue: round(h_mean, 1), saturation: round(s_mean, 1), value: round(v_mean, 1)}这段代码是粗定位实际项目里舌体分割要用 U-Net 或 SAM 微调否则背景干扰很大。hue均值可以粗略区分淡白舌、红舌、绛舌saturation辅助判断苔的厚薄。提取完特征后映射成文本「舌色偏红苔黄腻舌体胖大」。这个文本再和问诊症状拼在一起送进大模型。注意舌象特征提取的误差会直接传导到辨证结果所以舌象模块的验证要单独做不能只看端到端效果。3.3 脉象信号的时序特征与文本化脉象是最难数字化的。如果系统接的是脉象仪拿到的是压力时序信号需要提取脉率、脉律、脉位、脉势等特征。常见做法是算主波频率、波峰间隔方差、上升支斜率。下面是一个从脉象信号提取基础特征的示例。import numpy as np from scipy.signal import find_peaks def pulse_features(signal: np.ndarray, fs: int 200) - dict: # signal 是压力传感器采集的一维时序 peaks, _ find_peaks(signal, distancefs*0.3, prominence0.1) if len(peaks) 2: return {rate: 0, rhythm_cv: 0, amplitude_mean: 0} intervals np.diff(peaks) / fs # 峰间隔单位秒 rate 60.0 / intervals.mean() # 脉率次/分 rhythm_cv intervals.std() / intervals.mean() # 节律变异系数 amplitude_mean signal[peaks].mean() return { rate: round(rate, 1), rhythm_cv: round(rhythm_cv, 3), amplitude_mean: round(amplitude_mean, 3) }fs200是采样率实际按设备改。distancefs*0.3假设最小峰间隔 0.3 秒对应脉率上限 200 次/分。rhythm_cv大于 0.1 提示脉律不齐对应中医的「结代脉」。这些数值再映射成文本「脉率 78 次/分节律整齐脉势中等」。脉象模块的坑在于个体差异大同一人不同时间测都有波动所以脉象特征只能作为辅助不能作为辨证主依据。4. 证型推理与方剂推荐RAG 加约束解码的落地方式4.1 用证型知识库做检索增强的完整流程证型推理不能靠模型自由发挥得用知识库约束。流程是症状列表 → 向量检索召回候选证型 → 大模型在候选集内推理 → 输出证型和置信度。先建证型知识库每条包含证型名、主症、兼症、舌脉、治法、代表方。下面是一个用 sentence-transformers 做检索的示例。from sentence_transformers import SentenceTransformer import numpy as np model SentenceTransformer(shibing624/text2vec-base-chinese) # 证型知识库实际项目从数据库加载 syndromes [ {name: 肝郁脾虚, symptoms: 胁胀作痛情志抑郁腹胀便溏舌淡苔白脉弦细}, {name: 脾胃湿热, symptoms: 脘腹痞满口苦口黏大便不爽舌红苔黄腻脉滑数}, {name: 阴虚火旺, symptoms: 潮热盗汗口干咽燥舌红少苔脉细数}, ] corpus [s[symptoms] for s in syndromes] emb model.encode(corpus, normalize_embeddingsTrue) def retrieve(query: str, top_k: int 2) - list: q model.encode([query], normalize_embeddingsTrue) scores (emb q.T).flatten() idx np.argsort(scores)[::-1][:top_k] return [syndromes[i] for i in idx]normalize_embeddingsTrue让余弦相似度退化成点积计算更快。top_k2是召回候选数实际项目建议 3 到 5太少容易漏太多干扰推理。检索到的候选证型拼进 prompt让模型做最终判断。这里的关键是检索质量决定上限如果知识库覆盖不全模型再强也选不出正确证型。4.2 约束解码让模型只在候选证型里选约束解码是保证输出可控的核心手段。最简单的做法是在 prompt 里明确列出候选并要求只输出候选之一。更严格的做法是用 logits processor 限制 token 范围但实现复杂。对多数项目prompt 约束加后处理校验就够了。def diagnose(symptoms: list, candidates: list) - dict: cand_str \n.join([f- {c[name]}{c[symptoms]} for c in candidates]) prompt f根据以下症状从候选证型中选择最匹配的一个。 症状{.join(symptoms)} 候选证型 {cand_str} 要求只输出证型名称和一句话理由格式为「证型xxx理由xxx」。 resp client.chat.completions.create( modelqwen2-7b-instruct, messages[{role: user, content: prompt}], temperature0.1, max_tokens128 ) text resp.choices[0].message.content.strip() # 后处理校验输出必须在候选集内 for c in candidates: if c[name] in text: return {syndrome: c[name], raw: text} return {syndrome: None, raw: text} # 校验失败触发人工或重试后处理校验这步不能省。模型有时会输出候选集外的证型或者把两个证型拼在一起。校验失败时常见做法是降级到规则匹配或返回「需人工复核」。temperature0.1配合候选约束实测稳定性比自由生成高很多。4.3 方剂推荐与剂量安全边界证型确定后方剂推荐从知识库直接映射不要让模型自由生成方剂。原因很简单方剂组成和剂量有严格规范模型生成容易出「看起来合理但实际不对」的方子。做法是证型到方剂的映射表模型只负责在多个候选方剂里根据兼症做选择。FORMULA_DB { 肝郁脾虚: {formula: 逍遥散, herbs: 柴胡、当归、白芍、白术、茯苓、甘草、薄荷、生姜}, 脾胃湿热: {formula: 连朴饮, herbs: 黄连、厚朴、石菖蒲、半夏、栀子、芦根}, 阴虚火旺: {formula: 知柏地黄丸, herbs: 知母、黄柏、熟地、山茱萸、山药、茯苓、泽泻、丹皮}, } def recommend(syndrome: str) - dict: return FORMULA_DB.get(syndrome, {formula: None, herbs: None})剂量安全边界必须写进系统提示系统只做方剂推荐不给具体克数或者只给常规范围并标注「需医师确认」。这是合规底线也是技术上的后悔药——一旦模型给出超量剂量责任无法界定。实际项目中我会在输出层加一个剂量白名单校验超出范围直接拦截。5. 部署与性能32G 内存跑本地大模型的避坑记录5.1 量化等级与内存占用的实测对照32G 内存能装 AI 大模型但装什么级别、跑什么量化直接决定能不能用。下面是一组实测对照模型以 7B 为例。量化等级模型文件大小加载后内存占用辨证可用性q8_0约 7.5G约 9G最好接近原始精度q5_k_m约 5.1G约 6.5G好日常够用q4_k_m约 4.1G约 5.5G可用推荐起点q3_k_m约 3.3G约 4.5G辨证开始漂移q2_k约 2.6G约 3.8G不建议输出不稳定32G 内存跑 q4_k_m 的 7B 模型加上向量检索和 Web 服务总占用在 12G 到 15G留有余量。如果跑 13B 的 q4 模型加载后约 9G加上其他模块接近 18G也能跑但推理速度明显下降。血泪经验是内存不是唯一瓶颈CPU 推理速度才是。7B q4 在普通台式机上大概 8 到 15 token/秒多轮问诊等待感明显。如果要做实时对话得考虑 GPU 或 API。5.2 推理速度优化批处理、上下文裁剪与缓存本地推理慢优化手段有限但有效。第一控制上下文长度多轮问诊只保留最近 3 轮历史症状用摘要代替原文。第二证型检索和方剂映射不走大模型走向量检索和查表省掉大量推理。第三相同症状组合做结果缓存中医问诊里重复症状组合很常见。from functools import lru_cache lru_cache(maxsize256) def cached_diagnose(symptoms_tuple: tuple) - str: symptoms list(symptoms_tuple) candidates retrieve(.join(symptoms), top_k3) result diagnose(symptoms, candidates) return result[syndrome] or 需人工复核lru_cache用症状元组做 key注意症状顺序会影响缓存命中实际项目里可以先排序再缓存。maxsize256按内存调整。这个优化在重复问诊场景下能省掉 30% 以上的推理调用。另一个坑是模型加载后不要频繁重启每次重启加载模型要几十秒Web 服务里用全局单例持有模型实例。5.3 服务化部署的最小 FastAPI 示例把诊断链路包成 HTTP 服务方便前端调用。下面是一个最小 FastAPI 示例。from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ConsultRequest(BaseModel): text: str class ConsultResponse(BaseModel): symptoms: list syndrome: str formula: str app.post(/consult, response_modelConsultResponse) def consult(req: ConsultRequest): symptoms extract_symptoms(req.text) candidates retrieve(.join(symptoms), top_k3) result diagnose(symptoms, candidates) formula recommend(result[syndrome]) if result[syndrome] else {} return ConsultResponse( symptomssymptoms, syndromeresult[syndrome] or 需人工复核, formulaformula.get(formula, ) )response_model做输出校验避免模型返回的脏数据直接透传。实际部署时模型加载放在startup事件里不要放在请求处理函数里否则每次请求都重新加载。uvicorn启动命令加--workers 1因为模型实例不能多进程共享多 worker 会爆内存。6. 辨证结果怎么验证三个可操作的评估技巧6.1 用留出法测证型推理的命中率系统做完不能只看几个 case 觉得「挺准」得有量化评估。最直接的是留出法从证型知识库里留出 20% 的条目用它们的症状描述做测试看检索加推理能不能命中正确证型。注意测试症状要去掉证型名和方剂名否则是作弊。命中率低于 70% 时优先查知识库覆盖和症状归一化而不是换模型。def evaluate(test_cases: list) - float: hit 0 for case in test_cases: symptoms case[symptoms].split() candidates retrieve(.join(symptoms), top_k3) result diagnose(symptoms, candidates) if result[syndrome] case[name]: hit 1 return hit / len(test_cases)top_k3时如果正确证型没进候选集后面推理再强也没用。所以评估要分两段先看召回率再看推理准确率。召回率低就补知识库推理准确率低就调 prompt 或换模型。6.2 用一致性检查发现证型漂移同一个症状组合换不同问诊顺序或加无关症状看输出证型是否稳定。这是发现「玄学」问题的有效手段。做法是构造扰动集原症状、打乱顺序、加一个无关症状、去掉一个次要症状分别跑诊断统计证型一致率。一致率低于 80% 说明系统对输入扰动太敏感通常是 prompt 约束不够或温度偏高。扰动类型预期行为异常信号症状顺序打乱证型不变证型跳变说明模型依赖位置加无关症状证型不变或兼证提示主证型改变说明约束不足去次要症状证型不变证型改变说明知识库边界模糊这个检查不需要标注数据自己构造几十条就能跑性价比很高。6.3 人工复核队列与置信度阈值系统再稳也有边界得留人工复核入口。做法是给每次诊断算一个置信度检索相似度低于阈值、或后处理校验失败、或证型在候选集里得分接近都进复核队列。置信度可以用检索相似度加模型输出概率近似简单点就用检索 top1 和 top2 的分差。def confidence(scores: list) - float: # scores 是检索相似度降序列表 if len(scores) 2: return scores[0] if scores else 0.0 return scores[0] - scores[1] # 分差越大越自信分差小于 0.05 时进复核队列。这个阈值按实际数据调没有万能值。我一般会先跑一批历史数据看分差分布再定阈值。复核队列不是失败而是系统设计的一部分——中医诊断本身就需要医师确认系统定位是辅助不是替代。做这个方向两年最大的教训是别在模型选型上纠结太久证型知识库的质量和症状归一化的准确率才是决定系统能不能用的关键。我见过太多人花两周调模型结果知识库只有几十条辨证全靠模型自由发挥最后效果一塌糊涂。先把知识库建到几百条把同义词表维护好再回头调模型顺序反了就是白费功夫。希望帮到你。本文还有配套的精品资源点击获取