一个本地可复现的 FastAPI 服务,用于生物医学文档检索、RAG 问答、结构化临床试验字段抽取,以及多步 Agent 报告生成。
基于 FastAPI、FAISS、LangGraph、Pydantic 构建。默认配置无需任何外部 API key——内置 HashEmbedding + FakeLLM,保证 CI 和离线开发可跑;生产环境可通过环境变量切换真实嵌入模型、重排器和 LLM。
- 混合检索(Hybrid RAG) — 稠密检索(FAISS)与稀疏检索(BM25)通过 RRF(Reciprocal Rank Fusion)融合,可选挂载 BGE Reranker 做精排。检索结果返回带
citation_id、excerpt、score、metadata的结构化证据。 - 多策略查询改写 — standalone(规则,去代词)、step-back(LLM 抽象化)、HyDE(LLM 生成假设性答案文档再做稠密检索),按权重融合多查询的检索结果。
- 三层记忆 — working(会话内 FIFO 滑窗)、episodic(项目级事件,按时间衰减 + 关键词重叠召回)、semantic(结构化事实,向量召回),基于 SQLite 持久化。
- 结构化临床试验抽取 — 用 Pydantic v2 定义临床试验字段(phase、indication、endpoint、sample_size、criteria),通过「Prompt 约束 + model_validate_json 严格校验」两道防线将 LLM 输出收敛为强类型对象。
- Agent 报告工作流 — 基于 LangGraph StateGraph 的多步流水线,将 retrieve、extract、summarize、return 拆分为可观测节点(success/skipped/failed 三态),含检索质量反馈环(top score 过低时改写 query 重检索)与可回放的执行轨迹树(trace_tree)。
- 可插拔设计 — 嵌入、重排、LLM 均为 Protocol 抽象。默认零依赖(HashEmbedding + FakeLLM + NoopReranker),可切换 sentence-transformers(BGE-M3)/ OpenAI 兼容 API。
- 文档摄入 — 支持同步与异步(FastAPI BackgroundTasks)两种摄入端点,样本文档按字符窗口切分入 FAISS 索引。
- RAG 评估套件 — 独立的评估脚本(
evals/),覆盖文档级 source 命中和关键词覆盖,退出码可作为 CI 门禁。
uv sync
uv run uvicorn app.main:app --reload打开 http://localhost:8000/docs 查看 Swagger UI。
跑测试:
uv run pytest跑 RAG 评估(dense vs hybrid 对比):
uv run python evals/run_rag_eval.py # 基线评估
uv run python evals/run_hybrid_eval.py # dense vs hybrid 对比用 Docker 运行:
docker compose up --buildcurl http://localhost:8000/health{"status":"ok","service":"biomed-agent-demo"}将内置样本文档加载进内存 FAISS 索引。重复调用会重建索引而非追加重复 chunk。
curl -X POST http://localhost:8000/documents/ingest \
-H "Content-Type: application/json" \
-d '{"source":"samples"}'{"document_count":3,"chunk_count":12,"vector_store_path":".local/faiss"}通过 FastAPI BackgroundTasks 在后台执行的摄入请求。立即返回 job_id,可轮询状态。
curl -X POST http://localhost:8000/documents/ingest-jobs \
-H "Content-Type: application/json" \
-d '{"source":"samples"}'{"job_id":"ingest_a1b2c3d4e5f6","status":"pending","source":"samples"}轮询任务状态:
curl http://localhost:8000/documents/ingest-jobs/ingest_a1b2c3d4e5f6{
"job_id": "ingest_a1b2c3d4e5f6",
"status": "succeeded",
"source": "samples",
"document_count": 3,
"chunk_count": 12,
"error": null
}任务状态流转:pending → running → succeeded(或 failed)。不存在的 job_id 返回 404。
注意: 任务存储仅在内存中——重启后丢失,跨 uvicorn worker 不可见。适用于开发与单 worker 部署。
对已摄入的文档索引提问。响应包含带相似度分数和引用元数据的检索结果。
curl -X POST http://localhost:8000/query \
-H "Content-Type: application/json" \
-d '{"question":"What is the primary endpoint of the ADC trial?","top_k":3}'{
"answer": "...",
"sources": [
{
"document_id": "trial_adc_001",
"source": "trial_adc_001.md",
"chunk_index": 2,
"score": 0.1396,
"text": "...",
"citation_id": "trial_adc_001.md#2",
"excerpt": "Primary Endpoint: Objective response rate ...",
"metadata": {
"citation_id": "trial_adc_001.md#2",
"document_id": "trial_adc_001",
"source": "trial_adc_001.md",
"chunk_index": 2,
"score": 0.1396
}
}
],
"disclaimer": "This project is intended for research and development prototyping. It does not provide medical advice."
}若未摄入文档,端点返回空 sources 列表,并说明无法从可用文档中确定答案。
从样本文档或原始文本中抽取结构化的临床试验元数据。
curl -X POST http://localhost:8000/extract/trial \
-H "Content-Type: application/json" \
-d '{"document_id":"trial_adc_001"}'直接传入文本而非 document_id:
curl -X POST http://localhost:8000/extract/trial \
-H "Content-Type: application/json" \
-d '{"text":"A Phase II trial of ADC-101 in HER2-positive advanced solid tumors with primary endpoint objective response rate."}'{
"result": {
"trial_id": "trial_adc_001",
"phase": "Phase II",
"indication": "HER2-positive solid tumors",
"intervention": "ADC-101",
"primary_endpoint": "Objective response rate",
"secondary_endpoints": ["Progression-free survival", "Safety"],
"sample_size": 120,
"inclusion_criteria": ["Adult patients", "ECOG performance status 0-1"],
"exclusion_criteria": ["Uncontrolled infection"]
},
"validation_status": "valid"
}文档不存在返回 404。所有错误遵循统一的响应结构:
{
"error": "not_found",
"message": "Document not found: samples/missing_trial.md",
"request_id": null
}生成带可观测多步工作流的、有来源支撑的报告。每个步骤记录其名称、状态和摘要。
curl -X POST http://localhost:8000/agent/report \
-H "Content-Type: application/json" \
-d '{"topic":"ADC clinical trial"}'{
"report": "Summary for ADC clinical trial. ... Extracted trial design: phase Phase II; primary endpoint: Objective response rate; sample size: 120.",
"steps": [
{"name": "retrieve_documents", "status": "success", "summary": "Retrieved 3 source chunks."},
{"name": "extract_trial_fields", "status": "success", "summary": "Extracted structured trial fields from trial_adc_001: Phase II, primary endpoint Objective response rate."},
{"name": "summarize_findings", "status": "success", "summary": "Generated a source-grounded report summary."},
{"name": "return_response", "status": "success", "summary": "Returned report, steps, and source references."}
],
"sources": ["trial_adc_001.md#0", "pubmed_adc_summary.md#1"],
"trace_tree": {
"step": "retrieve_documents",
"status": "success",
"summary": "...",
"children": [{"step": "extract_trial_fields", "status": "success", "...": "..."}]
}
}若检索质量不足,retrieve 节点会触发 query 改写 + 重新检索(replan);若检索内容不含临床试验数据,extract_trial_fields 步骤标记为 skipped。
默认配置纯本地,无需 API key。所有配置项通过环境变量(或 .env 文件)注入,完整样例见 .env.example。
LLM_PROVIDER=fake
EMBEDDING_PROVIDER=hash
RAG_MODE=dense
RERANKER_PROVIDER=none
QUERY_REWRITE_ENABLED=falseLLM_PROVIDER=openai-compatible
LLM_API_KEY=your-key
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chatLLM_BASE_URL 可指向任何提供 OpenAI 兼容 /chat/completions 接口的服务。当前范围不含多模态、流式、原生 tool-calling。
RAG_MODE=hybrid # dense + BM25 用 RRF 融合
RRF_K=60 # RRF 常数
QUERY_REWRITE_ENABLED=true # 需要 LLM_PROVIDER=openai-compatibleuv sync --extra semantic # 装 sentence-transformers / torchEMBEDDING_PROVIDER=sentence # BGE-M3 等
EMBEDDING_MODEL_NAME=BAAI/bge-m3
EMBEDDING_DIMENSION=1024 # 须与模型真实输出维度对齐
RERANKER_PROVIDER=bge # BGE cross-encoder 精排
RERANKER_MODEL_NAME=BAAI/bge-reranker-baseapp/
├── api/routes/ # HTTP 端点(health / documents / query / extract / agent)
├── core/ # 配置(pydantic-settings)、依赖容器、错误处理
├── ingestion/ # 文件加载与文本切分
├── rag/ # 嵌入(可插拔)、FAISS 向量库、BM25 稀疏检索、RRF 融合、Reranker、Prompt
├── query_rewrite/ # 多策略查询改写(standalone / step-back / HyDE)
├── memory/ # 三层记忆(working / episodic / semantic)+ SQLite 持久化
├── extraction/ # Pydantic 校验 schema 与结构化字段抽取
├── llm/ # LLM 客户端 Protocol、fake 默认、OpenAI 兼容客户端
├── agent/ # LangGraph StateGraph 工作流、状态定义、工具函数、trace_tree
├── services/ # 业务编排(document / query / extraction / ingest job / agent)
└── schemas/ # 请求/响应 Pydantic 模型
service 层将 HTTP 路由与领域逻辑解耦。各模块单一职责,可独立替换——例如嵌入、重排、LLM 均为 Protocol 抽象,切换实现只需改 container.py 一处 + 环境变量,下游零改动。
| 层 | 技术 |
|---|---|
| API 框架 | FastAPI、Uvicorn |
| 校验 | Pydantic v2 |
| 向量库 | FAISS(内存,IndexFlatIP) |
| 稀疏检索 | rank-bm25(中英文混合分词) |
| 融合 | RRF(Reciprocal Rank Fusion) |
| 嵌入 | HashEmbedding(默认)/ sentence-transformers(BGE-M3,可选) |
| 重排 | BGE Reranker(可选) |
| LLM | FakeLLM(默认)/ OpenAI 兼容(DeepSeek/Qwen/Ollama/vLLM 等) |
| 记忆持久化 | SQLite(标准库 sqlite3) |
| 工作流编排 | LangGraph(StateGraph) |
| 测试 | pytest、FastAPI TestClient |
| 打包 | uv |
| 容器 | Docker、Docker Compose |
evals/ 提供两个评估脚本:
run_rag_eval.py— 基线评估,对预定义 case 检查 top source 命中与关键词覆盖。退出码 0 = 全过,可作 CI 门禁。run_hybrid_eval.py— 在同一批 case 上对比 dense 与 hybrid 两种模式的 source 命中数与 term 覆盖,判定规则:hybrid 不能在 source 命中上退步。
两个脚本都使用 FakeLLM + HashEmbedding,无外部 API key 依赖,完全可复现。
仓库内置三份合成的生物医学样本文档:
| 文件 | 类型 | 内容 |
|---|---|---|
samples/sop_cell_culture.md |
标准操作流程 | 细胞培养复苏、传代、冻存 |
samples/pubmed_adc_summary.md |
文献综述 | ADC 肿瘤学综述,含临床试验结果 |
samples/trial_adc_001.md |
临床试验摘要 | Phase II ADC-101 试验设计与终点 |
这些是虚构样例,不含真实患者数据或专有信息。
- 本地向量库 — 内存 FAISS 索引不跨重启持久化,不支持多租户或分布式查询。生产部署应换远程向量库。
- 内存任务存储 — 异步摄入任务仅在进程内存中,重启丢失、跨 worker 不可见、不支持取消或队列容量控制。
- 无鉴权 — API 无内置 auth 层。非本地环境应部署在反向代理或 VPN 之后。
- 仅研究原型 — 系统面向研发工作流原型设计,未经临床决策支持验证,不得用于医学诊断或治疗决策。
- 合成样例数据 — 所有内置文档均为虚构。任何有意义的评估应替换为真实(去标识)数据。
- 记忆模块未接入主流程 — 三层记忆模块已就绪(写入/召回接口 + 持久化),但尚未在
QueryService中消费做多轮上下文。
MIT