从零搭建“小甘草”:一个中医药 RAG 问答助手的完整实践

0 阅读4分钟

一、项目背景

中医药知识庞杂且专业,通用大模型容易“胡编乱造”,甚至推荐危险剂量。因此,我决定构建一个基于检索增强生成(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 分类为 medicalgreetingother

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 应用的开发者。