一、项目背景
中医药知识庞杂且专业,通用大模型容易“胡编乱造”,甚至推荐危险剂量。因此,我决定构建一个基于检索增强生成(RAG)的中医药问答系统,命名为“小甘草”——温暖、专业、安全。
核心需求:
- 严格依据中医药知识库回答,不编造内容。
- 不推荐具体药物剂量,必须提醒就医。
- 风格亲切、口语化,使用 emoji 并包含安全提示。
二、技术选型
| 环节 | 技术 |
|---|---|
| 嵌入模型(检索) | BAAI/bge-small-zh-v1.5 → 微调版 |
| 对话模型(生成) | Qwen2.5-0.5B-Instruct + LoRA 微调 |
| 向量数据库 | ChromaDB(本地) |
| 意图识别 | Ollama + qwen2:1.5b(本地 prompt) |
| Agent 服务 | FastAPI + Uvicorn |
| 后端集成 | Node.js + Express + axios |
| 前端 | Vue 3(已有项目) |
三、关键实现步骤
3.1 数据清洗与专业样本构建
原始数据包含约 11 万条问答对,其中混杂了一些通用建议(如“多喝水、休息”)。我编写脚本清洗出 9.6 万条专业中医问答(包含辨证、治法、方药等关键词),保存为 shennong_pro.json(JSONL 格式)。
python
keywords = ["中医", "中药", "方剂", "辨证", "证候", "治法", ...]
if any(k in response for k in keywords):
keep
3.2 微调嵌入模型(bge-small-zh)
使用 MultipleNegativesRankingLoss 进行对比学习,让模型学会将 query 与相关 response 的向量拉近。
python
from sentence_transformers import SentenceTransformer, losses, InputExample
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
train_examples = [InputExample(texts=[q, a]) for q, a in qa_pairs]
train_loss = losses.MultipleNegativesRankingLoss(model)
model.fit(train_objectives=[(train_dataloader, train_loss)], epochs=1)
训练后输出维度 384,检索专业文档的排序质量明显提升。
3.3 微调对话模型(Qwen2.5-0.5B + LoRA)
硬件限制(RTX 4050 6GB),无法全量微调 1.5B 模型,故改用 0.5B 模型 + LoRA + 4-bit 量化(后因 bitsandbytes 在 Windows 安装失败,改为 float16 + gradient checkpointing)。
训练数据中显式加入“小甘草”系统提示词,让模型学会风格和安全规则。
python
lora_config = LoraConfig(r=64, lora_alpha=16, target_modules=["q_proj","v_proj"], ...)
model = get_peft_model(model, lora_config)
训练 3 个 epoch(约 1 万条数据)后,模型能生成符合要求的回答,且不输出剂量。
3.4 意图识别(本地 Ollama)
为避免用户闲聊或问候时也强行检索知识库,增加了本地意图识别模块。使用 Ollama 调用 qwen2:1.5b,通过 prompt 分类为 medical、greeting、other。
python
def classify_intent(question):
prompt = f"将问题分类为 medical/greeting/other:{question}"
response = requests.post("http://localhost:11434/api/generate", json={"model": "qwen2:1.5b", "prompt": prompt})
return response.json()["response"].strip().lower()
3.5 封装 FastAPI 服务
将 rag_query.py 中的核心函数 query_tcm 封装为 /chat 接口。
python
@app.post("/chat")
async def chat(req: ChatRequest):
reply = query_tcm(req.message)
return {"reply": reply}
添加 CORS 中间件、健康检查 /health、异常捕获和日志。
3.6 Node.js 后端集成
原有后端使用 Dify,现改为调用本地 Agent 服务。
javascript
router.post('/message', async (req, res) => {
const { userId, message } = req.body;
const agentRes = await axios.post('http://localhost:8000/chat', { user_id: userId, message });
res.json({ reply: agentRes.data.reply });
});
处理超时和错误降级,保证 Agent 不可用时返回友好提示。
四、踩坑与解决
| 问题 | 解决方案 |
|---|---|
bitsandbytes 无法安装 | 改用 float16 + gradient checkpointing |
| 训练速度极慢(batch_size=1) | 增大 batch_size 到 4,启用梯度累积,降低 max_length |
| 微调后对话模型仍输出剂量 | 在训练数据中强化安全约束,并加入系统提示词 |
| Hugging Face Space 存储超 1GB | 只上传 LoRA 适配器(几 MB),基础模型从 Hub 下载 |
FastAPI 中 query_tcm 找不到 embed_model | 在 rag_query.py 顶部全局加载模型和向量库 |
| PowerShell 返回中文乱码 | 执行 [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 |
五、最终效果
用户问:“我最近拉肚子,天气炎热”,小甘草回答:
😊 别担心,我们一起来看看~根据您的情况,这很可能是湿热泄泻,中医认为暑湿伤及脾胃。
- 食疗方:可以喝点绿豆粥,清热利湿。
- 穴位指导:按揉足三里穴(膝盖外侧凹陷下四指),每次3分钟。
- 生活小贴士:避免贪凉吃冷饮,多喝温水。
✨ 以上建议仅供参考,身体不适请及时咨询专业医师。
六、后续优化方向
- 引入重排序(Reranker)提升检索质量。
- 将向量库迁移到 Supabase 云端,便于多端共享。
- 使用更小的意图分类模型(如 BERT)提高速度。
- 部署到 Hugging Face Spaces 获得永久公网链接。
七、项目代码与资源
- 微调后的嵌入模型:
./models/tcm_embedding_pro - 微调后的对话模型适配器:
./models/tcm_dialogue - 清洗后的数据集:
data/shennong_pro.json - FastAPI 服务:
api.py - Node 集成:
routes/chat.js。
总结:通过本实践,我完成了从数据清洗、模型微调、意图识别到服务封装的完整 RAG 系统搭建。整个过程充满挑战,但也让我对检索增强生成、模型部署和前后端集成有了更深的理解。希望这篇记录能帮助到同样在探索垂直领域 RAG 应用的开发者。