一、技术栈
整个项目是典型的「前后端分离 + AI 中台」结构。
后端(Python 3.10+)
| 技术 | 作用 |
|---|---|
| FastAPI | 异步 Web 框架,对外提供 REST API |
| ChromaDB | 轻量级向量数据库,做语义检索 |
| Sentence-Transformers | 本地文本嵌入(BGE 中文模型)+ Cross-Encoder 重排序 |
| rank-bm25 / 自研 BM25 | 关键词检索,与向量检索互补 |
| APScheduler | 定时任务,自动同步 PubMed 文献 |
| Qwen(阿里云 DashScope) | 主对话大模型,生成最终答案 |
| DeepSeek | 备选大模型,主模型失败/限流时自动接管 |
| Pydantic / pydantic-settings | 配置管理与请求/响应数据校验 |
前端(Node.js 18+)
| 技术 | 作用 |
|---|---|
| Nuxt.js 3 + Vue 3 + TypeScript | SSR 框架与类型安全 |
| Element Plus | 企业级 UI 组件库 |
| Tailwind CSS | 原子化样式 |
| markdown-it | 把大模型返回的 Markdown 渲染成 HTML |
一张图看懂架构
浏览器(3000) → Nuxt 前端 → FastAPI 后端(8000)
│
┌────────────┬───────┴────────┬──────────────┐
▼ ▼ ▼ ▼
ChromaDB 本地 BGE Qwen / DeepSeek PubMed API
(向量库) (嵌入/重排) (答案生成) (文献来源)
二、它是做什么的
PharmaLitQA 是一个面向生物医药 / 生信领域的文献智能问答系统。
研究人员面对的痛点是:PubMed 上每天新增海量文献,靠关键词检索既费时又容易漏。直接问通用大模型,又会遇到"幻觉"——它可能编造一个看起来很专业、实则不存在的结论。
这个系统的解法是 RAG(检索增强生成):先从一个可信的文献知识库里检索出真正相关的片段,再让大模型基于这些片段作答,并附上文献来源(PMID + PubMed 链接)。这样答案既专业,又可溯源、可核查,大幅降低幻觉。
一句话:"带着文献依据回答生物医药问题的 AI 助手"。
三、有哪些功能
围绕"问答"和"知识库管理"两条主线:
问答侧
- 🔍 智能问答:输入中文/英文问题,返回带文献引用的专业答案
- 🌐 跨语言检索:英文问题自动翻译为中文领域术语再检索
- 📎 答案溯源:每条答案附带来源文献片段、PMID 与 PubMed 原文链接
知识库侧
- 📚 知识库浏览:查看库中全部文献
- ➕ 手动录入文献:按 PMID + 标题 + 摘要添加
- 🗑️ 删除/清空:按 PMID 删除单篇或一键清空
- 🔄 PubMed 自动同步:定时(默认每天凌晨 2 点)拉取最新文献入库
- ⏰ 同步调度管理:查看同步状态、手动触发、修改定时规则
- 🔎 PubMed 条件检索:按关键词 + 日期范围在线检索 PubMed
- 💡 MeSH 术语联想:输入时联想标准医学主题词,提升检索准确度
模型可用性
- ⚡ 双模型容灾:Qwen 主、DeepSeek 备,自动故障切换
- 💰 本地嵌入:嵌入模型跑在本机,永久免费、离线可用、不耗 API 额度
四、业务流程
4.1 问答主流程(核心)
用户从提问到拿到答案,后端经历 6 步:
用户问题
│
├─① 语言判断与翻译 ──→ 非中文则译为中文领域术语
│
├─② 多路召回 ──→ 向量检索(语义) + BM25(关键词),合并去重,召回 ~20 篇
│
├─③ 重排序 ──→ Cross-Encoder 对候选深度打分,精排出 Top-5
│
├─④ 构造上下文 ──→ 把 Top-5 片段拼成带 PMID 的提示词
│
├─⑤ 大模型生成 ──→ Qwen 作答(失败自动切 DeepSeek)
│
└─⑥ 组装结果 ──→ 答案 + 来源列表(PMID/片段/相关分) 返回前端
为什么要"召回 20 → 精排 5"两段式?因为向量检索快但粗(适合从全库快速捞候选),Cross-Encoder 准但慢(适合在小候选集上精挑)。两者结合兼顾速度与精度。
4.2 文献入库流程
文献(标题+摘要)
│
├─ 分块:短摘要整段成块;长摘要按句子边界滑窗切分(带重叠)
├─ 嵌入:本地 BGE 把每块转成 1024 维向量
└─ 入库:向量 + 文本 + PMID 元数据 写入 ChromaDB;重置 BM25 索引
4.3 PubMed 自动同步流程
APScheduler 定时触发(每天 2:00)
│
├─ ESearch:按生物医药关键词 + 时间窗检索,拿到 PMID 列表
├─ EFetch :批量拉取文献标题、摘要、作者、期刊等详情
├─ 去重 :跳过库中已存在的 PMID
└─ 入库 :走 4.2 的嵌入入库流程
五、每个功能的技术实现
下面逐个拆解关键功能背后的代码实现。
5.1 智能问答(RAG 编排)
核心在 rag/rag_service.py 的 query(),它是上面 4.1 流程的代码化身:
def query(self, question: str) -> Dict[str, Any]:
# ① 翻译(仅非中文)
translated = get_translator().translate_if_needed(question)
# ② 多路召回 ~20 篇
candidates = self._multi_retrieve(translated, top_k=20)
# ③ Cross-Encoder 精排 Top-5
reranked = self._rerank(translated, candidates, top_k=5)
# ④ 拼上下文(带 PMID)
context = "\n\n".join(f"片段{i}(PMID: {r['pmid']}):{r['text']}"
for i, r in enumerate(reranked, 1))
# ⑤ 大模型生成
answer = self.qwen_client.ask_with_context(question, context, pmid=...)
# ⑥ 组装来源
return {"answer": answer, "source": [...]}
设计要点:检索用的是翻译后的文本(提升召回),但喂给大模型作答时用用户原始问题(保证回答语言/语气贴合用户)。
5.2 多路召回(Vector + BM25)
_multi_retrieve() 同时跑两路检索再用 PMID 做并集合并:
- 向量检索:本地 BGE 把 query 编码成向量,在 ChromaDB 里按 L2 距离找最近邻。擅长语义相似("OS"≈"总生存期")。
- BM25 检索:经典关键词算法,擅长精确匹配(基因名、缩写如 "KRAS G12C")。实现见
bm25_retriever.py,核心打分公式:
score += idf * (tf * (k1 + 1)) / (tf + k1 * (1 - b + b * doc_len / avgdl))
两路互补:向量召回怕生僻专有名词,BM25 召回怕同义改写,合在一起召回率更高。BM25 索引在每次查询时按知识库最新状态惰性重建,保证增删文献后立即生效。
5.3 重排序(Cross-Encoder 精排)
多路召回的 20 篇质量参差,用 cross_encoder_reranker.py 的 Cross-Encoder(ms-marco-MiniLM-L-6-v2)做精排:
pairs = [(query, doc["text"]) for doc in documents]
scores = self.model.predict(pairs) # query-doc 联合编码打分
docs.sort(key=lambda d: d["rerank_score"], reverse=True)
return docs[:top_k]
与"双塔"向量检索不同,Cross-Encoder 把 query 和 doc 拼在一起送进 Transformer,做 token 级交互注意力,相关性判断更准。代价是慢,所以只在 20 篇小候选集上用。
优雅降级:模型加载失败时自动回退到 SimpleReranker(向量距离 + 关键词匹配的加权打分),保证服务不中断。
5.4 本地嵌入(BGE)
rag/embeddings.py 用本地 BAAI/bge-large-zh-v1.5(1024 维)。两个工程细节:
# 1) BGE 查询侧需加检索指令,文档侧不加
BGE_QUERY_INSTRUCTION = "为这个句子生成表示以用于检索相关文章:"
def embed_query(self, query):
text = self.instruction + query if self.use_query_instruction else query
return self.model.encode(text, normalize_embeddings=True).tolist()
# 2) 向量归一化,让 L2 距离与余弦相似度等价排序
选本地模型而非云端 Embedding API 的原因:免费、离线、不受 API 额度限制——这对一个要持续给全库文献做嵌入的系统很关键。模型缓存在 ~/.cache/huggingface/,全局共享、只下一次。
5.5 跨语言翻译(三层混合策略)
rag/translator.py 的 HybridTranslator 不是简单调翻译 API,而是三层瀑布:
- 静态词典(
O(1),0 网络开销):内置数百条高频生物医学术语(癌种、靶点、基因),命中直接返回。 - LLM 兜底(~200ms):词典覆盖不到的,调大模型翻译。
- MeSH 查询扩展:调 NCBI MeSH 接口把术语规范化、扩展同义词,提升 PubMed 检索召回。
def _llm_fallback(self, text):
if not self.enable_llm or not self._llm_api_key:
return text # 没配 LLM 也能退回词典结果
return self.llm_translator.translate(text)
带持久化缓存(translator_cache.json),同样的词不重复翻译。即使 LLM 那层失败,也会优雅退回词典 + MeSH 结果,不阻断主流程。
5.6 双模型容灾(Qwen 主 + DeepSeek 备)
rag/qwen_client.py 把"主备切换"封装在一个 call() 里,对上层完全透明:
def call(self, prompt, ...):
try:
return self._chat(self.client, self.model, ...) # ① 先试 Qwen
except Exception as e:
logger.warning(f"Qwen 调用失败:{e}")
if self.fallback_client: # ② 自动切 DeepSeek
return self._chat(self.fallback_client, self.fallback_model, ...)
raise
只要 Qwen 报任何错(免费额度耗尽 403、限流、网络抖动、模型名错误),立刻用 DeepSeek 重试,前端无感知。两个模型都走 OpenAI 兼容协议,所以同一套 OpenAI SDK 即可调用,只是 base_url 和 model 不同。
5.7 文献分块入库
rag_service.add_literature() + _chunk_abstract()。早期版本按句号切片,导致同一句里的关键数据(如"OS 37.5 个月")和上下文被切散,检索命中的片段缺数据,模型只能答"无相关信息"。改进后的策略:
def _chunk_abstract(abstract, max_chars=500, overlap=100):
if len(abstract) <= max_chars:
return [abstract] # 短摘要整段,保留完整上下文
# 长摘要:按句子边界聚合成块,块间保留 overlap 重叠
...
短摘要不切,长摘要按句子边界做带重叠的滑窗——既不超模型长度上限,又不丢上下文。配合 BGE 嵌入,问答准确度明显提升。
5.8 PubMed 同步与调度
pubmed_sync.py:封装 NCBI E-utilities。ESearch拿 PMID 列表,EFetch批量取详情,内置生物医药关键词集做领域过滤。sync_scheduler.py:用 APScheduler 注册 cron 任务(默认0 2 * * *)。同步时先get_all_documents()取已有 PMID 做去重,只对新文献嵌入入库,避免重复与浪费。- 对外暴露
POST /api/sync/now(手动触发)、GET /api/sync/status(查状态)、POST /api/sync/schedule(改 cron)三个接口,调度策略可运行时调整。
5.9 MeSH 术语联想
GET /api/mesh/suggest(main.py):用户在前端输入时,实时调 NCBI 的 esearch + esummary 接口,把输入词映射到标准 MeSH 主题词返回下拉建议;无输入时给一批热门术语兜底,网络失败时返回空列表而非报错——不让外部依赖影响前端体验。
5.10 前端问答页
frontend/pages/index.vue:Element Plus 输入框 + 提交按钮,调 POST /api/ask。拿到结果后:
- 用 markdown-it 把答案 Markdown 渲染成 HTML(大模型常用 Markdown 排版);
- 来源用可折叠面板(
el-collapse)展示,每条带 PMID 和跳转 PubMed 的"查看原文"链接; - 支持
Ctrl/Cmd + Enter快捷提交,loading 态防重复提交。
六、小结
PharmaLitQA 的价值不在某一个"炫技点",而在于把一条完整、可靠、可溯源的 RAG 链路工程化落地:
- 检索质量:向量 + BM25 多路召回,再加 Cross-Encoder 精排,召回与精度兼顾;
- 可用性:本地嵌入永久免费、Qwen/DeepSeek 双模型容灾、翻译三层降级——每个外部依赖都有兜底;
- 数据新鲜度:APScheduler 定时同步 PubMed,知识库持续更新;
- 可信度:答案永远附带文献来源,把"AI 幻觉"约束在"有据可查"的范围内。
这套架构稍作替换(换领域语料、换嵌入/对话模型),就能迁移到法律、金融、政务等任何需要"专业 + 可溯源"问答的垂直场景。
项目地址
github:https://github.com/oinsist/PharmaLitQA
gitee:https://gitee.com/o_insist/PharmaLitQA