简介:一套融合通义千问大模型与传统中医知识的智慧医药系统项目,基于SpringBoot框架构建,以人工智能充当智能医生,提供中医诊断与养生咨询。整体功能设计简洁,界面简约大气,适合编程初学者、人工智能应用开发者学习,也适用于学校项目答辩或毕业设计选题。压缩包共十七个文件,大小约九十四兆,主要包含脚本程序、模型数据、配置文件、说明文档、演示视频与音频等,目录结构围绕项目运行与展示需求组织,便于按需查阅。目前已有一百二十五人学习下载。资源附带了完整的前后端启动脚本、依赖清单、中英文说明、面部特征点模型及演示录屏,能够直观呈现调用大模型接口、处理自然语言请求、输出中医知识的过程,对理解人工智能与传统医学结合的实现路径、掌握一体化开发方式很有帮助。
1. 从问诊单到方剂:这套基于大模型的中医诊断系统到底怎么工作
一个患者说自己“怕冷、清鼻涕、嗓子疼,前两天吹了风”,这套系统给出的不是“感冒”两个字,而是“风寒束表、卫阳被遏”的证型判断,外加荆防败毒散加减的剂量建议——这就是基于AI大模型的中医诊断系统在干的事。它把传统问诊单拆成结构化字段,交给本地部署的大模型做八纲辨证,最后输出证型、治法、方剂和服药说明。适合两类人:一是想验证大模型在垂直行业能不能真正落地的AI工程师,二是想给诊所或健康管理场景搭一个辅助辨证原型的开发者。这篇文章按我实际拆这套资源的过程,把辨证链路、模型部署、前端渲染和几个高频坑逐一讲透,文末会给出一个可以拿去用的验证方法。
2. 辨证推理链路:八纲辨证提示词框架与寒热虚实JSON输出
2.1 问诊单为什么设计成JSON而不是自由对话
先解决一个方向性问题:这套资源里的问诊数据,为什么非要转成JSON结构再丢给大模型,而不是像ChatGPT那样直接闲聊?
我的判断是,中医辨证本质上是一个结构化分类任务,患者的主诉、兼症、舌象、脉象本身就是分类特征。“风寒束表”和“风热犯卫”的区分,靠的是“怕冷还是怕热”“鼻涕是清还是黄”“咽喉是痒还是痛”这些离散特征,而不是一段自由文本里的语气和修辞。把输入结构化,模型要做的事就变成“理解特征组合并映射到证型”,难度比“从一大段口语里抽取关键信息”低一个量级。
这套资源在工程上的第一个关键决策,就是把输入限制成如下结构:
{ "chief_complaint": "怕冷,清鼻涕,嗓子疼", "onset": "2天", "chills_fever": "怕冷明显,无明显发热", "sweating": "无汗", "thirst": "口不渴", "stool": "大便偏稀", "urine": "小便清长", "tongue": "舌淡苔薄白", "pulse": "脉浮紧", "constitution": "平时体质偏弱" }字段名、取值倾向、可选范围都是固定的。这样做的第二个好处是:前端表单可以直接把每个字段渲染成单选、多选或下拉框,患者不需要组织语言,模型也不需要对口语做归一化。我在实际跑这套流程时发现,凡是让患者自由输入“症状描述”的方案,后期都会被同一个症状的不同说法折磨——这个坑到第5章专门展开。
输入结构定了,输出结构也要定。辨证结果如果让模型随意发挥,前端就没法渲染,医生也没法核对。这套资源把输出也约束成了JSON,核心字段包括证型、辨证依据、治法、主方、药材剂量、加减项、注意事项和置信度。输入输出全结构化,意味着整条链路可以脱离人工介入自动运行,这是它能做成一个“系统”而不是一个“聊天机器人”的根本原因。
2.2 提示词搭建:先讲辨证思路,再输出结构化JSON
跑通这套系统的核心,其实是一段并不算长的System Prompt。我把这套资源里最有价值的提示词框架还原如下,你拿到资源后可以对照自己的模型微调:
你是一位执业中医师,擅长八纲辨证。请根据患者问诊单,完成以下任务。 第一步,先用两段话分析患者的寒热、虚实、表里、阴阳倾向,说明你判断的关键依据。 第二步,再输出一个JSON对象,字段必须严格遵循: { "pattern": "证型,例如:风寒束表", "basis": "辨证依据摘要", "therapy": "治法,例如:辛温解表、宣肺散寒", "formula": "主方名称,例如:荆防败毒散", "herbs": [{"name": "药材", "dose": "剂量", "note": "炮制或煎法说明"}], "modification": "随症加减说明", "caution": "注意事项", "confidence": 0.0 } 约束: 1. 只允许引用经方和时方,禁止自创方剂。 2. 药材剂量参考《中国药典》,毒性药材不得超出法定剂量上限。 3. 患者信息不足以判断时,confidence不得高于0.6,并在caution里注明需要追问的项目。 4. 禁止输出JSON之外的任何结构。这段提示词里有三个设计细节值得单独说。
第一,“先分析、后输出”的顺序。这是整套资源里最有效的一招。让模型先写两段推理文字再出JSON,准确率比“直接出结果”高不少。原因在于,多步推理让模型被迫把寒热、虚实、表里的判断过程走一遍,降低了它直接跳到结论的概率。模型在辨证这种需要特征组合的任务上,跳步输出往往就是幻觉的开始。
第二,置信度字段。这个设计在中医场景里非常关键。模型不是总能做出可靠判断——比如患者没填舌象和脉象,模型硬判的可靠性就很低。设置confidence字段,等于给系统留了一条“承认不确定”的出路。我在跑测试时把阈值定在0.7,低于这个值的诊断结果在前端会被标注“建议线下就诊”,而不是硬着头皮给方子。
第三,毒性药材的剂量约束。大模型对“附子”“细辛”这类药材的剂量上限认知很不稳定,自由度一高就会编出离谱数字。把“参考《中国药典》”写进提示词,可以把剂量错误率压低不少,但这还不够——第5章会讲在代码层再加一道白名单校验。
2.3 证型到方剂的映射不是让模型凭空开方
提示词约束了“禁止自创方剂”,模型仍然可能在经方集合里挑错。这个资源在辨证和开方之间加了一层规则映射,我把它理解成“半开放决策”。模型只负责判定证型,而证型到主方的对应关系,走的是内置映射表:
| 证型 | 治法 | 基础方 | 常见加减方向 |
|---|---|---|---|
| 风寒束表 | 辛温解表 | 荆防败毒散 | 头痛加川芎、白芷;咳嗽加杏仁 |
| 风热犯卫 | 辛凉解表 | 银翘散 | 咽痛加玄参、射干;口渴加芦根 |
| 脾胃虚寒 | 温中健脾 | 理中丸 | 寒重加附子(先煎);呕吐加半夏 |
| 肝气郁结 | 疏肝解郁 | 逍遥散 | 胁痛加川楝子;失眠加合欢皮 |
| 气阴两虚 | 益气养阴 | 生脉散 | 汗多加浮小麦;心悸加酸枣仁 |
代码层的判断逻辑是:如果模型输出的证型在映射表里,直接取对应主方;如果不在,就把模型输出的formula字段当作候选,进入人工复核通道。这一层规则把“模型选错方剂”的风险挡掉一部分,同时也保证输出的方剂一定来自可溯源的清单,而不是模型脑子里拼出来的组合。
这套“结构化输入+推理前置提示词+证型映射白名单”的链路,是这套资源最值得下载的部分。模型可以换,提示词可以调,但这个架构本身是通用的——你把这个架构里的中医辨证换成其他领域的故障诊断,它照样成立。
3. 模型部署与接口封装:Ollama加载量化模型与问诊调用
3.1 模型选型与量化说明
这套资源在本地跑、用开源模型,部署方案是Ollama。选它不是因为花哨,是因为一条命令就能拉起一个支持OpenAI兼容接口的推理服务,对做原型验证来说成本最低。
模型层面我建议从Qwen2.5 7B的指令微调版开始,量化级别选q4_K_M。选7B而不是14B的理由很实际:辨证任务没有复杂的数学和代码推理,7B的语义理解能力在这个场景下已经够用,对显存的要求也低得多。q4_K_M是质量和体积的平衡点,4比特量化之后模型文件不到5GB,一张6GB显存的显卡或者16GB内存的Mac都能跑;再低到q2或q3,模型输出JSON的稳定性会明显下降,经常出现字段残缺或括号不闭合的问题。
这套资源里没有写死模型,你完全可以把模型替换成其他中文指令模型。但我给一个经验值:参数量低于7B的中文模型,在“先推理后输出JSON”的双阶段任务上翻车率会显著上升,表现为输出一段说明文字后拒绝输出JSON,或者JSON里的药材列表缺失。所以建议别低于7B这个档位。
3.2 首次部署与启动校验
下载资源解压后,先确认Ollama服务可用。部署命令分两步:
# 拉取模型,q4_K_M是4比特量化版 ollama pull qwen2.5:7b-instruct-q4_K_M # 启动服务,默认监听127.0.0.1:11434 ollama serve拉取时注意观察网络状态,模型文件接近5GB,断了就重新执行pull,Ollama支持断点续传。服务起来后先用一条命令验证推理是否正常:
ollama run qwen2.5:7b-instruct-q4_K_M "用一句话回答:八纲辨证包含哪八纲?"如果模型能答出“阴阳、表里、寒热、虚实”,说明服务可用。接着把Ollama的API地址记下来,后面所有问诊调用都会走这个HTTP端点。默认是http://localhost:11434,如果你在同一台机器上跑前端和调用脚本,不需要改配置。
3.3 diagnose函数封装与输出容错
直接调Ollama的generate接口当然能跑,但一个能交付的系统,必须在调用层做三件事:参数固定、JSON解析容错、失败兜底。这套资源里给出了一段可改的Python封装,核心逻辑如下:
import json import requests OLLAMA_URL = "http://localhost:11434/api/generate" def build_prompt(patient: dict) -> str: system = """你是一位执业中医师,擅长八纲辨证。 请先分析患者寒热、虚实、表里、阴阳倾向,再输出JSON对象。""" # 把问诊单JSON作为用户消息传给模型 return f"{system}\n患者问诊单:\n{json.dumps(patient, ensure_ascii=False)}" def diagnose(patient: dict) -> dict: prompt = build_prompt(patient) payload = { "model": "qwen2.5:7b-instruct-q4_K_M", "prompt": prompt, "stream": False, "temperature": 0.2, "top_p": 0.9, "max_tokens": 1024, "repeat_penalty": 1.1, } resp = requests.post(OLLAMA_URL, json=payload, timeout=120) raw = resp.json()["response"] return parse_result(raw) def parse_result(raw: str) -> dict: # 优先提取```json代码块 if "```json" in raw: raw = raw.split("```json")[1].split("```")[0] else: # 退而求其次,截取第一个{到最后一个}之间的内容 start, end = raw.find("{"), raw.rfind("}") if start != -1 and end != -1: raw = raw[start:end+1] try: result = json.loads(raw) except json.JSONDecodeError: raise ValueError("模型输出JSON解析失败,原始输出:" + raw[:200]) return result参数说明就按我调过的来:
temperature设0.2。辨证任务不是创意写作,低温度能让模型每次输出都偏保守,减少随机发散。你如果把它调到0.7以上,同一个问诊单跑三次能出三种证型,这种系统没法交付。
top_p设0.9,配合低temperature使用。max_tokens设1024,这个长度足够输出两段推理文字加一个完整的JSON,太小会截断方剂列表。repeat_penalty设1.1,防止模型在输出药材列表时重复某味药。
parse_result这个函数是整个调用层的安全网。大模型偶尔会在JSON外裹一层解释文字,或者把JSON放在段落中间,这段代码会把杂质剥掉再解析。注意兜底逻辑的边界:如果解析失败,我选择直接抛异常而不是返回空结果——宁可让前端显示“请重新提交”,也不能给患者一张缺了剂量的半成品方剂。
这里有个判断分享给你:封装层绝不能追求“尽量解析成功”。我在调试时发现,正则强行修复残缺JSON,有时候会把“附子10g”修成“附子100g”,这种静默错误比显式失败危险得多。所以解析失败就大声报错,让调用方决定怎么处理。
4. 问诊界面与结果渲染:从表单字段到可打印的诊断卡
4.1 问诊表单字段设计
这套资源的前端界面不算复杂,但表单字段的设计是从辨证需求倒推出来的。我拆的时候数了一下,主界面包含了11个字段,每个字段都和提示词里的JSON键一一对应:
| 字段 | 控件类型 | 选项/范围 | 设计理由 |
|---|---|---|---|
| 主诉 | 文本输入 | 30字以内 | 必填,作为整体语境 |
| 发病时长 | 下拉 | <3天 / 3-7天 / >7天 | 判断表证还是里证倾向 |
| 怕冷怕热 | 单选 | 怕冷明显 / 怕热明显 / 无明显 | 寒热辨证核心特征 |
| 出汗 | 单选 | 无汗 / 有汗 / 自汗 / 盗汗 | 卫表是否开合 |
| 口渴 | 单选 | 口不渴 / 口渴喜热饮 / 口渴喜冷饮 | 判断寒热真假 |
| 大便 | 单选 | 偏干 / 偏稀 / 正常 | 里证虚实参考 |
| 小便 | 单选 | 清长 / 短赤 / 正常 | 配合口渴判断寒热 |
| 舌象 | 多选 | 舌淡 / 舌红 / 苔白 / 苔黄 / 苔腻 | 辨证重要依据 |
| 脉象 | 下拉 | 浮 / 沉 / 迟 / 数 / 弦 / 细 / 滑 | 提示词里直接引用 |
| 既往病史 | 多选 | 糖尿病 / 高血压 / 胃病史等 | 用药安全过滤 |
| 体质 | 单选 | 平和 / 气虚 / 阳虚 / 阴虚 / 痰湿 | 辅助证型判断 |
这些字段几乎全用单选和下拉收口,没有给患者留自由发挥的空间。这是刻意为之——模型处理“自定义文本”和“点选预设选项”的稳定性差距非常大,用控件约束输入,等于在源头消灭了第5章要讲的归一化问题。
其中“口渴”这个字段我单独说。它是辨别寒热真假的关键:真热假寒的患者,往往口渴喜冷饮;真寒假热的患者,可能表现为口渴但喜热饮。这两个在语义上很接近,但证型方向完全相反,所以不能只做“口渴/不渴”二选一,必须把“喜热饮”还是“喜冷饮”拆开。
4.2 诊断结果渲染:把推理链和方剂分开展示
拿到模型输出的JSON后,前端渲染的要点是“把辨证依据和方剂分开”。证型、治法、方剂是结果,但患者和医生都需要看到“为什么是这么判”。这套资源的结果页包含四块:证型标识卡、辨证依据折叠面板、方剂明细表、置信度警示条。
function renderResult(result) { // 证型标识 const pattern = document.getElementById("pattern"); pattern.textContent = result.pattern; // 辨证依据,默认展开让医生核对 const basis = document.getElementById("basis"); basis.textContent = result.basis; // 方剂表格 const tbody = document.querySelector("#herbs-table tbody"); tbody.innerHTML = ""; result.herbs.forEach((herb) => { const row = document.createElement("tr"); row.innerHTML = `<td>${herb.name}</td><td>${herb.dose}</td><td>${herb.note || ""}</td>`; tbody.appendChild(row); }); // 置信度警示:低于0.7建议线下就诊 const confidence = document.getElementById("confidence"); const progress = document.getElementById("confidence-bar"); progress.style.width = `${result.confidence * 100}%`; const warning = document.getElementById("offline-warning"); if (result.confidence < 0.7) { warning.style.display = "block"; } }这里有三个设计细节是资源里自带的,我觉得值得留用。
第一,方剂明细表里药材名、剂量、煎法说明三者分列。剂量是医疗安全信息,必须单独一列让医生一眼扫到;煎法说明(如“附子先煎”“生姜三片引”)放在note列,是给患者执行时看的。
第二,置信度用进度条加警示横幅双通道展示。进度条是视觉参考,横幅是强制提示。低于0.7时显示“建议线下就诊,本结果不可作为处方依据”,这句文案在系统里写死,不允许前端改动。理由很直接:辅助辨证系统的第一原则是不要鼓励患者自行抓药。
第三,辨证依据默认展开而不是折叠。很多AI产品把推理过程藏起来,但医疗场景相反——医生必须能快速看到模型判断的依据,否则他没法决定采信还是推翻。依据越透明,系统才越有可能被专业用户接受。
5. 避坑指南:归一化、毒性剂量与上下文漂移的四条翻车实录
5.1 “胃疼”与“胃脘痛”:症状归一化做不好,辨证就漂移
第一版系统上线测试时,我发现一个诡异现象:同一患者,第一次说“胃疼”,模型判成“肝气犯胃”;一周后复诊说“胃脘痛”,模型判成“脾胃虚寒”。患者症状没变,证型却变了。
原因出在模型对不同表述的语义理解不稳定。“胃疼”是口语,“胃脘痛”是书面语,哪怕它们指向同一个部位,模型在组合“症状+舌象+脉象”时,还是会因为措辞差异调整特征权重。这个问题的隐蔽之处在于,单次诊断看不出问题,只有同一患者复诊时才会暴露。
解决方法是加一层症状归一化映射,在构建提示词之前把口语替换成统一术语。我在代码里维护了一个同义词表,“胃疼/胃痛/胃脘痛”统一映射为“胃脘疼痛”,“拉肚子/腹泻/泄泻”统一映射为“大便稀溏”。这样模型每次看到的都是同一套术语,复诊对比才有意义。从那以后我习惯把所有与患者直接交互的文本入口都过一遍归一化,前端下拉选项本身也用统一术语做label。
5.2 模型编了一个不存在的经方,还开出附子30g
这是个让我冷汗直流的案例。我拿一个“脾肾阳虚”的测试问诊单跑模型,输出结果里出现了一首听起来很像经方的方剂,但我在方剂库里查不到;更离谱的是方中“附子”剂量写的是30g,而《中国药典》规定的附子常规剂量上限是15g,30g属于超量使用。
原因有两层。第一,模型在“禁止自创方剂”的约束下,仍然可能把两三个相似方剂的名字融合,生成一个似曾相识的伪经方;第二,模型对毒性药材的剂量上限没有可靠记忆,它在训练语料里见过附子大剂量的个例,就会把个例当作通用值。
这个坑靠提示词约束是堵不死的,必须在代码层加两道保险:第一道是药材白名单,模型输出herbs数组后,逐一校验药材名是否在预置清单里,不在的直接丢弃并告警;第二道是剂量上下限表,附子、细辛、生半夏这类毒性药材有明确的min和max值,超出范围的剂量直接拒绝整张方剂。我现在跑这套系统的默认策略是:宁可扔给人工复核,也不能让一个编造方剂通过校验。
5.3 多轮追问后证型自己跟自己打架
资源里带了一个补充问诊功能,模型在置信度不足时会反问患者。功能本身没问题,但实测中发现一个坑:患者在五轮追问后补充了“夏天怕吹空调”,模型把证型从“风寒束表”悄悄改成了“阳虚外感”,而下结论的依据里用的还是最初那版辩证逻辑。前后矛盾,患者和医生都懵。
原因在于长对话上下文膨胀,模型对早期system prompt里的约束记忆淡化了,容易被新信息带偏。解决方法是给追问加硬限制:轮数上限固定为5轮,答满5轮仍不置信就强制降级为“建议线下就诊”;每一轮都把完整的system prompt重放一遍,而不是只传对话历史;同时把上一轮已经判出的寒热、虚实倾向单独提出来,作为“既有判定”塞进下一轮的prompt里,要求模型“如无足够证据不得修改既有判定”。这样既保留了补充问诊能力,又不会让结论在对话中反复横跳。
5.4 JSON字段名飘忽不定,解析器差点被带崩
我把模型换过一个版本之后,发现parse_result偶发返回空字段。查了原始输出才发现,新模型在某些问诊单上把“pattern”写成了“zheng_type”,把“herbs”写成了“medicines”。字段名变了,解析器拿到None,前端直接渲染空白。
原因不复杂,提示词里虽然给了JSON模板,但模型在长输出时偶尔会“忘键”,用自己更熟悉的同义字段代替。解决方法是给parse_result加一个字段映射兜底:读取时同时尝试"pattern"、"zheng_type"、"zhengxing"三个键,herbs和medicines同理;同时把workload降下来,在提示词末尾加一句“必须原样返回模板中的键名,不得替换”。我再后来给输出JSON加了一道schema校验,任何未知字段触发告警并列入待审清单,防止静默吞掉结构变化。
6. 验证一个诊断靠不靠谱:置信度、追问与复现性测试
模型输出一张诊断卡,不代表它可以见患者。我在落地这套系统时最常用的一套验证方法,总共四步,每一步成本都很低,但对系统质量是刚性的。
第一步,批量回放历史问诊单。我手上攒了十几个真实标注过的病例,每次改完提示词或换模型,就把这批数据全部跑一遍,对比新旧输出的证型和方剂差异。改提示词影响到的病例数超过三分之一,说明改动方向偏了,需要回滚。
第二步,复现性测试。同一个问诊单固定跑5次,统计证型一致率。我给自己定的标准是至少4次一致才算稳定,达到这个标准才允许进测试环境。如果5次出现3种证型,基本可以断定是temperature设置有问题,我会先把temperature压到0.1再重新测。
第三步,置信度分布审计。把最近100个问诊结果的confidence画个分布,如果中位数低于0.7,说明当前提示词或者问诊表单设计有问题——大概率是字段收集的信息量不够,模型在很多病例上都在“勉强作答”。这时候该改的不是提示词,而是回到第4章问诊表单里补字段。
第四步,方剂配伍核对。把模型输出的药味清单和它声称的主方做比对,计算Jaccard相似度。相似度低于0.5的,极有可能是模型选错了主方或者自创了方剂,直接送到人工复核队列。这一步和我第5章说的药材白名单校验配套使用,一个管“有什么药”,一个管“药合理不合理”。
最后说一个我自己的习惯:从那以后,我每次改完提示词或调完模型参数,都强制把历史问诊单跑一遍对比,验证通过前绝不上线。这套系统是用来辅助判断一个人该怎么调理身体的,你要是拿一个自己都解释不了的输出点“提交”,将来出了问题连个对标依据都拿不出来——所以宁可慢一点,也别跳过这一步。希望帮到你。
本文还有配套的精品资源,点击获取