中医药专家问答系统: LangChain +Qwen+fastapi
中医药文化源远流长,典籍浩如烟海。对于普通用户而言,想要在《黄帝内经》《伤寒论》等经典中找到对症的方剂和用药建议,往往需要深厚的专业知识。随着大语言模型技术的发展,我们能否打造一个智能问答系统,让用户以自然语言描述症状,系统便能依据中医典籍给出有理有据的建议?带着这样的想法,我开发了「中医药专家问答系统」。本文将详细记录开发过程中的技术选型、架构设计、核心代码实现、遇到的挑战以及后续优化方向,希望能为同样对 AI + 中医药感兴趣的朋友提供参考。
二、技术选型
-
后端框架:FastAPI(异步高性能,天然支持 SSE 流式响应)
-
大语言模型:阿里通义千问
qwen-max(通过 LangChain 集成) -
LangChain 组件:
-
ChatPromptTemplate构建提示模板 -
MessagesPlaceholder管理对话历史 -
StreamingResponse实现流式输出
-
-
前端:原生 HTML/CSS/JS(简洁易部署,支持 Server-Sent Events)
-
部署:Uvicorn
三、系统架构
整体采用 B/S 架构,前端负责展示对话界面和用户输入,后端提供两个核心 API:
-
POST /api/chat:处理用户消息,支持流式返回模型思考过程及最终答案。 -
POST /api/reset:重置对话历史,开始新会话。
后端通过 LangChain 调用通义千问模型,并动态维护对话历史,使模型能够记住上下文,提供连贯的咨询体验。
text
用户 → 前端页面 → FastAPI 后端 → 通义千问 API → 流式返回 → 前端实时渲染
四、后端核心实现
4.1 环境与模型初始化
首先设置环境变量 DASHSCOPE_API_KEY,然后创建 ChatTongyi 实例,启用流式模式(streaming=True 可支持逐字输出,但本次使用 stream 方法实现更细粒度的控制)。
import os
from langchain_community.chat_models.tongyi import ChatTongyi
model = ChatTongyi(model="qwen-max")
4.2 构建提示模板与对话历史
为了增强回答的专业性和依据性,我们在 system 消息中要求模型引用中医典籍原文,并采用列表形式使内容清晰易读。同时利用 MessagesPlaceholder 插入历史对话记录。
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
chat_prompt_template = ChatPromptTemplate.from_messages([
("system", "你是一名资深的中医药专家,精通《黄帝内经》《伤寒论》《本草纲目》等典籍。请结合中医典籍的记载,为患者提供准确的用药建议和注意事项。"
"回答时应引用具体典籍原文或出处,增强依据性。如果可能,请使用分点或列表形式组织内容,使回答清晰易读。"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}"),
])
# 初始化示例对话,让模型理解上下文格式
history_data = [
HumanMessage(content="我最近感冒了,怕冷、流清鼻涕,有什么中药推荐?"),
AIMessage(content="根据《伤寒论》中'太阳病,头痛发热,身疼腰痛,骨节疼痛,恶风,无汗而喘者,麻黄汤主之'的记载,您的情况属于风寒表证,"
"可考虑使用麻黄汤加减。但麻黄汤发汗力强,体虚者需慎用,建议在医师指导下服用。"),
HumanMessage(content="有没有中成药可以替代?"),
AIMessage(
content="根据《太平惠民和剂局方》记载,感冒清热颗粒适用于风寒感冒,具有疏风散寒、解表清热的功效。其组方中荆芥穗、防风等成分可对症。"
"不过若伴有咽喉肿痛,可能需要搭配其他药物。"),
]
4.3 流式聊天接口
核心在于利用 chain.stream() 方法逐块获取模型输出,并通过 Server-Sent Events(SSE)格式实时推送到前端。此外,每次回复完成后,需要将用户输入和模型回复追加到历史列表中,维持多轮对话的上下文。
from fastapi import FastAPI, HTTPException
from starlette.responses import StreamingResponse
import json
chain = chat_prompt_template | model
@app.post("/api/chat")
async def chat(request_data: dict):
user_input = request_data.get('input', '')
if not user_input:
raise HTTPException(status_code=400, detail="请输入内容")
async def generate_response():
full_response = ""
try:
# 流式获取模型输出
for chunk in chain.stream({"history": history_data, "input": user_input}):
content = chunk.content
full_response += content
yield f"data: {json.dumps({'type': 'chunk', 'content': content})}\n\n"
# 对话结束后更新历史
history_data.append(HumanMessage(content=user_input))
history_data.append(AIMessage(content=full_response))
yield f"data: {json.dumps({'type': 'complete', 'response': full_response})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"
return StreamingResponse(
generate_response(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # 禁用 Nginx 缓冲,保证实时性
}
)
4.4 重置对话历史
用户可能需要开始新的咨询,因此提供重置接口清空历史,恢复初始示例对话。
@app.post("/api/reset")
async def reset():
global history_data
history_data = [
HumanMessage(content="我最近感冒了,怕冷、流清鼻涕,有什么中药推荐?"),
AIMessage(content="根据《伤寒论》中'太阳病,头痛发热,身疼腰痛,骨节疼痛,恶风,无汗而喘者,麻黄汤主之'的记载,您的情况属于风寒表证,"
"可考虑使用麻黄汤加减。但麻黄汤发汗力强,体虚者需慎用,建议在医师指导下服用。"),
HumanMessage(content="有没有中成药可以替代?"),
AIMessage(
content="根据《太平惠民和剂局方》记载,感冒清热颗粒适用于风寒感冒,具有疏风散寒、解表清热的功效。其组方中荆芥穗、防风等成分可对症。"
"不过若伴有咽喉肿痛,可能需要搭配其他药物。"),
]
return {'success': True}
4.5 提供前端页面
通过根路由返回 HTML 文件,省去单独部署前端的麻烦。
from fastapi.responses import HTMLResponse
@app.get("/")
async def get_frontend():
with open("./templates/index.html", "r", encoding="utf-8") as f:
return HTMLResponse(content=f.read(), media_type="text/html")
4.6 CORS 配置
为了让前端能够跨域请求 API,添加 CORSMiddleware。
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
五、前端实现
前端是一个简单的聊天界面,支持:
-
输入框发送消息
-
实时显示模型回复的流式输出(打字机效果)
-
重置会话按钮
-
自动滚动到底部
主要 JavaScript 逻辑使用 Fetch API 读取 SSE 流,逐块渲染 Markdown(简单支持加粗、列表等),提升阅读体验。关键代码如下:
async function sendMessage() {
const input = document.getElementById('userInput').value;
if (!input) return;
addMessage(input, 'user');
document.getElementById('userInput').value = '';
const responseArea = document.getElementById('responseArea');
responseArea.innerHTML = '<div class="loading">正在思考...</div>';
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: input })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullAnswer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6));
if (data.type === 'chunk') {
fullAnswer += data.content;
responseArea.innerHTML = formatMarkdown(fullAnswer);
responseArea.scrollTop = responseArea.scrollHeight;
} else if (data.type === 'complete') {
responseArea.innerHTML = formatMarkdown(data.response);
addMessage(data.response, 'ai');
} else if (data.type === 'error') {
responseArea.innerHTML = `<div class="error">错误:${data.error}</div>`;
}
}
}
}
} catch (err) {
responseArea.innerHTML = `<div class="error">网络错误:${err.message}</div>`;
}
}
六、运行与测试
6.1 环境准备
-
Python 3.10+
-
安装依赖:
pip install fastapi uvicorn langchain langchain-community langchain-core starlette -
设置环境变量
DASHSCOPE_API_KEY(通义千问的 API Key)
6.2 启动服务
将后端代码保存为 main.py,前端 HTML 放入 templates/index.html 目录,执行:
bash
uvicorn main:app --reload --host 0.0.0.0 --port 8000
访问 http://localhost:8000 即可使用。
6.3 交互测试
-
输入“我最近胃胀、食欲不振,有什么中药调理?”模型会根据《脾胃论》等典籍推荐理气和胃的方剂,并给出饮食建议。
-
输入“我嗓子疼,感觉有痰咳不出”模型会区分风热或风寒,引用《温病条辨》等典籍分析。
-
点击重置按钮,清空当前对话,回到初始示例。
七、遇到的问题与解决
-
对话历史管理
初期尝试将历史作为全局变量存储在内存中,但在并发请求下可能出现数据错乱。由于本项目仅为个人使用或小规模演示,暂未引入数据库,后续可使用 Redis 或数据库存储每个会话的历史。 -
流式输出时的 SSE 格式解析
前端需要正确处理可能被分割的data:行,通过 buffer 暂存未处理完的数据,保证 JSON 解析不报错。 -
模型回答的格式
模型有时会输出非 Markdown 的文本,前端需简单处理加粗、列表等,提高可读性。也可要求模型按固定格式输出,如“药方:...”,方便前端解析。 -
典籍引用的准确性
虽然 prompt 要求引用原文,但模型可能会生成“编造”的典籍出处。未来可结合 RAG(检索增强生成)从真实数据库中检索相关内容,增强可信度。
八、效果展示
系统运行界面如下(示例截图)

用户发送问题后,模型会逐字输出回答,并在回答中标注典籍出处
九、下一步计划
-
引入知识库 RAG
将《伤寒论》《金匮要略》等典籍分块嵌入向量数据库,检索相关段落后提供给模型,提高回答的准确性和可信度。 -
支持多会话
为不同用户或不同会话分配独立 ID,存储历史,方便用户切换话题。 -
前端优化
增加 Markdown 渲染(支持代码块、表格),并实现“复制回答”按钮。 -
部署上线
将服务部署到云服务器,配置 HTTPS,方便公网访问。 -
用户反馈机制
允许用户对回答进行评价,收集数据进一步优化 prompt。
十、总结
通过本项目,我成功实践了 LangChain 结合通义千问构建中医药智能问答系统的完整流程,并掌握了 FastAPI 流式响应的开发技巧。虽然当前版本还比较简单,但已具备实用的咨询能力,且扩展性强。
源码仓库:[]
更多推荐



所有评论(0)