从 Sparse 到 LLM Reranking:ADHD 症状句子检索的混合排序方案
这次我们来看一个学术竞赛里的实战项目:DS@GT-ARC 团队在 eRisk 2026 Task 3 上提交的 ADHD 症状句子检索方案。项目标题很直白——DS@GT-ARC at eRisk 2026 Task 3: Sparse, Semantic, and LLM Reranking for ADHD Symptom Sentences,核心思路就是把稀疏检索、语义检索和 LLM 重排序三条路线串成一个完整的搜索排序管线。
先说重点:这个方案不是让你去训练一个新模型,而是解决一个更常见的问题——怎么从大量文本里准确捞出“和 ADHD 症状相关的句子”。它用到的方法(BM25、密集向量、Reranker)都是信息检索领域已经成熟的技术,组合在一起以后,既能保证召回率,又能把排序精度拉上去。如果你在做 RAG、文献筛选、医疗文本挖掘、或者任何一种“先召回再精排”的检索任务,这条技术路线很值得抄作业。
文章会按下面几个部分展开:任务背景和评测指标、整体技术架构、环境准备、部署启动、功能测试、API 与批量任务、资源占用、问题排查、最佳实践。代码示例全部走通用模板,需要替换的参数我会逐个标注清楚。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 项目类型 | 学术竞赛检索方案 / 混合检索排序 Pipeline |
| 任务来源 | eRisk 2026 Task 3(早期风险预测与筛查方向) |
| 研究对象 | ADHD(注意力缺陷多动障碍)症状相关句子 |
| 核心技术 | Sparse 检索(BM25 类)、Semantic 检索(Dense Embedding)、LLM Reranking |
| 主要功能 | 从候选文本中召回与疾病症状相关的句子,并对召回结果做二次排序 |
| 是否支持 CPU | 支持,Sparse 和传统 Semantic 检索可以在 CPU 上运行,LLM Reranking 建议 GPU |
| 显存需求 | 不确定,取决于所选 Backbone 模型和推理精度,需按实际环境测试 |
| 是否支持 50 系显卡 | 取决于 PyTorch / CUDA 版本,新卡需要对应驱动和算子支持 |
| 是否支持 API | 可以封装为本地 HTTP 接口,文章会给出通用示例 |
| 是否支持批量任务 | 支持,候选文本可以按目录或数据库分批处理 |
| 启动方式 | Python 脚本 / FastAPI 服务 / 命令行批处理 |
| 适合场景 | RAG 检索排序、医疗文本挖掘、舆情症状筛查、学术文献筛选 |
| 使用边界 | 不支持独立诊断,检索结果只能作为辅助研判,医疗场景须人工复核 |
2. 适用场景与使用边界
先说清楚这个方案适合谁。它本质上是一套“检索 + 重排”的文本处理流程,目标是解决一个非常具体的任务:给定一批社交媒体帖子、病历文本或问卷回答,把其中涉及 ADHD 症状表达的句子挑出来,并按相关性排序。
适合的使用场景包括:
- 科研数据筛选:从大量英文社交媒体文本中筛出与 ADHD 相关的叙述,作为数据集过滤环节。
- 临床试验候选筛选:在受试者自述文本中快速定位症状描述,辅助医生或研究人员做初步分层。
- RAG 检索链路优化:把这套“稀疏召回 + 语义召回 + LLM 重排”的管线迁移到通用检索增强生成系统里。
- 舆情与健康监测:在公开文本中统计某类健康话题的出现频率和趋势。
同时要明确使用边界:
- 这是文本检索工具,不是诊断系统。ADHD 的确诊需要专业医生结合临床量表和行为观察,任何自动检索结果都不能作为医学诊断依据。
- 涉及真实患者数据、医疗记录或社交媒体个人信息时,必须做匿名化处理,并遵守数据保护法规。公开数据集也要确认授权协议。
- LLM Reranking 的提示词和输出结果会受模型偏见影响,不适合直接用于司法、保险、招聘等高敏感决策。
- 项目属于学术竞赛方案,代码若涉及第三方模型权重,商用前需要核对模型许可证。
3. 任务背景与评测指标
eRisk 是 CLEF 组织下的一个长期赛道,全称是 Early Risk Prediction on the Internet,重点研究如何通过用户在社交媒体上的公开表达,提前识别抑郁、饮食失调、自杀倾向等心理健康风险信号。ADHD 方向的 Task 3 一般会提供一个带标注的数据集,要求参赛团队从文本序列中检索出与症状相关的证据句子。
Task 3 的典型操作方式是:给定一组文本(比如用户的 Reddit 帖子),系统需要判断哪些句子是“ADHD 症状句子”。这里的难点有两个:
- 症状表达很口语化。用户不会写“我存在注意力维持困难”,而是写“我上课总是走神,作业拖到最后一刻”。这种表达与教科书定义相差很远,纯关键词匹配会漏掉大量目标句。
- 负样本干扰大。帖子里有大量无关的日常叙事——比如“今天天气不错”“我吃了午饭”——这些句子和症状句在语言上没有任何明显区别,需要更深层的语义理解才能区分。
DS@GT-ARC 的方案用一个三段式流程来应对:
- Sparse 检索:先做零成本初筛,把候选集从十几万句缩小到几千句。
- Semantic 检索:再用句向量做基于语义的召回,找出与症状描述“意思相近但用词不同”的句子。
- LLM Reranking:最后用大模型对候选句子逐一打分、排序,输出最终结果。
这种设计的好处是每一级都在缩小数据规模,成本最高的 LLM 推理只作用于最后的小候选集,整体开销可控。
4. 整体技术流程设计
这个方案不是单一模型,而是一个多级漏斗。这个思路和工业界的搜索排序系统非常像,所以即使你不参加 eRisk,这套设计也能直接复用在文档检索、RAG 召回等任务上。
4.1 阶段一:Sparse 检索
Sparse 检索的核心是“词面匹配”。BM25 是这类方法的典型代表,它不关心语义,只统计查询词和文档词的重合情况,并引入 IDF(逆文档频率)来降低常见词的权重。在这个任务里,查询词是“ADHD 症状列表”,候选文档是社交媒体句子,BM25 会先筛选出包含注意力缺陷、冲动、多动等高频关键词的句子。
Sparse 检索的优点是速度快、解释性强,没有模型加载成本,CPU 就能跑完。缺点是召回不够,用户表达和查询词不完全重合时就直接漏掉了。
4.2 阶段二:Semantic 检索
第二阶段用句向量模型把句子映射到稠密向量空间,再通过余弦相似度计算语义相关性。这样即使句子里面没有“ADHD”这个字眼,只要意思和“经常无法集中注意力”接近,也能被召回到。
Semantic 检索需要对全量句子做 embedding 离线索引,运行时只做向量相似度计算,速度也可接受。这个阶段的召回结果会和 Sparse 结果做合并、去重,形成候选集。
4.3 阶段三:LLM Reranking
最后,把候选集交给大语言模型重新打分。Reranker 不是生成式问答,而是让模型直接输出相关性分数或返回排序结果。LLM 的优势是能综合上下文、语气、常识来判断一个句子是否真的在描述 ADHD 症状,这对口语化的社交媒体文本特别有效。
成本上,LLM Reranking 只处理前两阶段筛出来的几百到几千条候选,而不是全量文本,所以在计算开销上是可以承受的。
5. 环境准备与前置条件
这个项目对硬件的要求主要取决于第三阶段选用什么样的 Reranker 模型。如果只是跑通 Sparse + Semantic,普通 CPU 完全没有问题;如果要上 7B 或更大参数的 LLM,就需要一块足够显存的 GPU。
通用环境清单如下:
| 环境项 | 要求建议 |
|---|---|
| 操作系统 | Linux / Windows / macOS 均可,生产环境推荐 Linux |
| Python | 3.9 及以上 |
| 包管理工具 | pip 或 conda |
| 深度学习框架 | PyTorch(版本需对应 CUDA 版本) |
| Transformers 库 | 用于加载句向量模型和 LLM |
| 向量索引 | FAISS 或 Milvus,小规模直接用 NumPy 也可以 |
| GPU | 运行 LLM Reranking 需要 NVIDIA GPU,显存建议至少 8G,具体以模型为准 |
| 磁盘空间 | 取决于模型大小,句向量模型通常几百 MB,LLM 权重从几 GB 到几十 GB |
安装基础依赖时可以用下面这组命令,实际版本号需要根据你的环境调整:
# 创建虚拟环境,避免污染全局 Python python -m venv venv_adhd source venv_adhd/bin/activate # Windows 下执行 venv_adhd\Scripts\activate # 安装核心依赖 pip install torch transformers sentence-transformers pip install rank_bm25 scikit-learn numpy pandas pip install fastapi uvicorn requests如果使用 GPU 推理,需要先确认显卡驱动和 CUDA 版本匹配。建议用 PyTorch 官方命令安装对应 CUDA 版本的预编译包,例如:
# CUDA 12.1 示例,实际版本以本机为准 pip install torch --index-url https://download.pytorch.org/whl/cu121启动前还要检查端口占用情况。API 服务默认可以使用 8000 端口,如果本机有别的服务占用,换一个端口即可。模型文件建议统一放在./models目录下面,输入数据放在./data,输出结果放在./outputs,方便后续批量任务管理和日志回溯。
6. 安装部署与启动方式
这个项目的部署不像一键包那样双击就能跑,需要按脚本顺序执行。下面给出一套可用的通用流程,涉及路径、模型名、端口的地方都需要按实际环境调整。
6.1 目录结构规划
adhd_retrieval/ ├── data/ │ ├── queries.txt # 查询条件,每行一个 │ ├── corpus.jsonl # 候选句子集合 │ └── labels.txt # 可选,评测标签 ├── models/ │ └── sentence_encoder/ # 句向量模型 ├── outputs/ │ └── results/ # 检索排序结果 ├── scripts/ │ ├── 01_sparse_index.py │ ├── 02_semantic_search.py │ ├── 03_llm_rerank.py │ └── server.py # FastAPI 接口服务 └── requirements.txt6.2 Sparse 索引脚本示例
# scripts/01_sparse_index.py import json from rank_bm25 import BM25Okapi # 读取候选句子 with open("data/corpus.jsonl", "r", encoding="utf-8") as f: corpus = [json.loads(line)["text"] for line in f] # 分词函数,英文按空格切分即可;中文需要改用 jieba 或词表 def tokenize(text: str): return text.lower().split() tokenized_corpus = [tokenize(doc) for doc in corpus] bm25 = BM25Okapi(tokenized_corpus) # 保存原始文档,待后续使用 with open("outputs/corpus_tokens.json", "w", encoding="utf-8") as f: json.dump(corpus, f, ensure_ascii=False) print(f"BM25 index built, total docs: {len(corpus)}")6.3 Semantic 向量索引脚本示例
# scripts/02_semantic_search.py import numpy as np from sentence_transformers import SentenceTransformer # 指定本地模型路径,首次没有时也可以填 Hugging Face 模型名自动下载 model_name = "BAAI/bge-small-en-v1.5" encoder = SentenceTransformer(model_name) corpus_texts = [] with open("data/corpus.jsonl", "r", encoding="utf-8") as f: for line in f: corpus_texts.append(json.loads(line)["text"]) # 批量生成向量,batch_size 根据显存调整 embeddings = encoder.encode(corpus_texts, batch_size=32, show_progress_bar=True) np.save("outputs/corpus_embeddings.npy", embeddings) print(f"Embedding shape: {embeddings.shape}")6.4 LLM Reranking 脚本示例
# scripts/03_llm_rerank.py from transformers import AutoModelForSequenceClassification, AutoTokenizer import torch # 以 cross-encoder reranker 为例,模型名称按实际选择替换 model_name = "cross-encoder/ms-marco-MiniLM-L-6-v2" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForSequenceClassification.from_pretrained(model_name) model.eval() # 候选句子和查询条件 query = "difficulty concentrating, impulsivity, restlessness" candidates = [ "I can't focus on my homework at all.", "I bought a new phone yesterday.", "I always lose my keys and feel restless in class.", ] # 构造 (query, candidate) 输入对 inputs = tokenizer( [query] * len(candidates), candidates, padding=True, truncation=True, return_tensors="pt", ) with torch.no_grad(): scores = model(**inputs).logits.squeeze(-1) for candidate, score in zip(candidates, scores.tolist()): print(f"{score:.4f}\t{candidate}")6.5 启动 API 服务
API 服务用 FastAPI 包装,便于后续接入自己的工具链或做批量任务:
# scripts/server.py import json import numpy as np from fastapi import FastAPI from pydantic import BaseModel from rank_bm25 import BM25Okapi from sentence_transformers import SentenceTransformer app = FastAPI(title="ADHD Symptom Retrieval API") class SearchRequest(BaseModel): query: str top_k: int = 20 # 启动时加载索引,生产环境建议换成独立的向量数据库 corpus_texts = [] with open("data/corpus.jsonl", "r", encoding="utf-8") as f: for line in f: corpus_texts.append(json.loads(line)["text"]) tokenized_corpus = [doc.lower().split() for doc in corpus_texts] bm25 = BM25Okapi(tokenized_corpus) embeddings = np.load("outputs/corpus_embeddings.npy") encoder = SentenceTransformer("BAAI/bge-small-en-v1.5") @app.get("/health") def health_check(): return {"status": "ok"} @app.post("/search") def search(req: SearchRequest): # 1. Sparse 召回 bm25_scores = bm25.get_scores(req.query.lower().split()) sparse_idx = np.argsort(bm25_scores)[-req.top_k:][::-1] # 2. Semantic 召回 query_vec = encoder.encode([req.query]) dot = np.matmul(embeddings, query_vec.T).squeeze(-1) dense_idx = np.argsort(dot)[-req.top_k:][::-1] # 3. 合并去重 merged_indices = list(dict.fromkeys( [int(i) for i in sparse_idx] + [int(i) for i in dense_idx] )) results = [] for i in merged_indices: results.append({ "index": int(i), "text": corpus_texts[i], "bm25_score": float(bm25_scores[i]), "semantic_score": float(dot[i]), }) return {"query": req.query, "results": results} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="127.0.0.1", port=8000)启动命令:
python scripts/server.py浏览器访问http://127.0.0.1:8000/health,看到{"status": "ok"}就说明服务跑起来了。
7. 功能测试与效果验证
部署完成后,建议按下面的测试顺序逐项验证,不要直接跳到完整数据流程。
7.1 测试一:Sparse 检索是否正常
准备一个简单的查询,例如"difficulty concentrating",手动检查 BM25 返回结果里是否包含“注意力”“无法专注”等表层关键词的句子。
判断标准:返回结果里出现关键词匹配的句子,排序靠前的句子包含更多查询词。如果结果为空,检查corpus.jsonl路径、分词逻辑和中文/英文切分方式。
7.2 测试二:Semantic 检索是否正常
用同义词改写查询,比如把"difficulty concentrating"改成"I can't stay focused",看语义检索能否返回和 ADHD 注意力症状相关的句子。
如果语义检索返回的全是无关句子,大概率是向量模型没有正确加载,或者 embedding 归一化缺失。可以打印一下query_vec和embeddings的 shape,确认没有维度不匹配。
7.3 测试三:LLM Reranking 是否正常
用 5 到 10 条混合候选句子(包含症状句和无关句)跑一次 Reranking,观察模型打分情况。
一个可用的判断经验是:真正描述 ADHD 症状的句子分数应高于普通日常叙事句子。如果所有分数都一样,检查模型是回归模型还是分类模型,确认输出层是不是被当成了 logits 而不是概率。
7.4 测试四:API 接口是否正常
curl -X POST http://127.0.0.1:8000/search \ -H "Content-Type: application/json" \ -d '{"query": "restlessness and impulsivity", "top_k": 10}'预期返回结构:
{ "query": "restlessness and impulsivity", "results": [ { "index": 12, "text": "I always feel restless in class and can't sit still.", "bm25_score": 3.75, "semantic_score": 0.82 } ] }如果 curl 请求卡住,优先检查服务端日志、模型加载进度和网络端口。
8. 接口 API 与批量任务
单条查询接口只能验证功能,真正落地时要考虑批量请求。
8.1 批量查询设计
批量任务通常有两种模式:
- 离线批处理:读取
queries.txt,逐条调用检索管线,结果写入 CSV 或 JSONL。 - 在线服务:通过 API 提交多条查询,服务端异步处理,返回任务状态。
离线批处理更简单、更适合做评测。下面这个脚本演示了怎么把多个查询依次跑完并保存结果:
# scripts/batch_search.py import json import requests queries = [ "difficulty concentrating", "impulsive behavior", "restlessness", ] outputs = [] for q in queries: resp = requests.post( "http://127.0.0.1:8000/search", json={"query": q, "top_k": 10}, timeout=60, ) outputs.append(resp.json()) with open("outputs/batch_results.json", "w", encoding="utf-8") as f: json.dump(outputs, f, ensure_ascii=False, indent=2) print(f"Batch done, total queries: {len(outputs)}")8.2 批量任务注意事项
- 给每个查询加上唯一 ID,方便后续回溯。
- 服务端要做超时控制和失败重试。LLM Reranking 单条推理如果超过 30 秒,说明候选数量或模型规模需要调整。
- 批量任务日志至少要有开始时间、完成时间、成功状态、失败原因四类信息。
- 输出文件建议按日期命名,例如
results_20260201.jsonl,避免覆盖历史结果。
9. 资源占用与性能观察
这一章节是最多人在本地部署时忽略的部分。三个阶段的资源消耗差异很大,要分开观察。
9.1 Sparse 检索资源占用
BM25 只做词频统计,内存占用取决于候选句子数量。10 万条英文句子大约占用几百 MB 内存,CPU 查询一条在毫秒级别。这个阶段不需要 GPU。
9.2 Semantic 检索资源占用
句向量模型推一次 embedding 会占用一些 GPU 或 CPU 资源。如果是bge-small这类小模型,CPU 也能跑,速度大约每秒几十到几百条;如果用 GPU,显存占用通常在 1G 到 2G 左右,具体要看模型尺寸和 batch_size。
观察方式:在推理时打开任务管理器或nvidia-smi,看进程占用的显存曲线。如果显存不够,降低batch_size,或者换成更小的模型。
9.3 LLM Reranking 资源占用
这是整个管线里最重的一环。直接用nvidia-smi观察:
nvidia-smi --query-gpu=utilization.gpu,memory.used --format=csv -l 1如果是 7B 参数模型,FP16 推理需要大约 14G 显存,量化到 INT8 可以降到 7G 左右。但这属于典型值,实际占用要以你选择的模型、推理框架和 batch 大小为准。如果显存不足,优先考虑量化模型,或者把候选集裁到几百条以内再交给 LLM。
9.4 性能优化建议
- 预处理阶段过滤过短的句子。少于 5 个词的句子通常不具备足够上下文信息。
- Semantic 检索阶段先用 Sparse 结果缩小范围,再算向量,能大幅减少 embedding 推理数量。
- LLM Reranking 阶段对候选集做截断,只选前 200 到 500 条,防止推理耗时过长。
- 如果想加速 Semantic 检索,可以把向量索引换成 FAISS:
import faiss index = faiss.IndexFlatIP(embeddings.shape[1]) index.add(embeddings.astype("float32"))
9.5 端口冲突和进程残留
如果服务启动时报address already in use,先查找并关闭占用进程:
# Linux/macOS lsof -i :8000 # Windows netstat -ano | findstr :8000也可以直接换端口启动:
python scripts/server.py --port 800110. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
corpus.jsonl读取失败 | 路径错误或文件编码不对 | 检查文件是否存在,打印前 3 行 | 确认文件编码为 UTF-8,路径使用绝对路径 |
| BM25 返回结果为空 | 分词后的词表为空 | 打印 tokenize 结果 | 确认英文按空格切分,中文改用 jieba |
| Semantic 检索结果全部无关 | 向量模型未正确加载 | 打印 query 和 document 向量维度 | 检查模型名是否拼写正确,确认句子没有全部变成空串 |
| LLM Reranking 分数都一样 | 模型输出层理解错误 | 查看模型类型和 logits 形态 | 换用 cross-encoder 模型,或改为分类模型取概率 |
| 启动 API 时端口被占用 | 8000 端口已有服务 | `netstat -ano | findstr :8000` |
| 显存不足导致推理报错 | 模型太大或 batch 太大 | 观察nvidia-smi显存占用 | 量化模型、降低 batch_size、减少候选集数量 |
| 批量任务中途卡住 | 某条查询请求超时 | 看服务端日志和超时时间 | 对 API 加超时控制,服务端增加异常捕获 |
| 结果里大量无关句子 | 召回阶段阈值设置太低 | 检查召回数量和各阶段分数 | 调高 LLM Reranking 的阈值,或收紧 Semantic 相似度阈值 |
| 中文文本无法正确分词 | 没有安装中文分词器 | 检查 tokenize 逻辑 | 安装 jieba 或使用中文 tokenizer |
11. 最佳实践与使用建议
11.1 数据管理
把输入数据、模型权重、输出结果分成三个独立的目录。不要在代码里写死绝对路径,建议使用配置项或环境变量。模型文件较大,不要塞进 Git 仓库,单独用模型管理工具或直接放在固定目录。
11.2 评测优先
这个任务是有标准评测指标的。在改任何参数之前,先把基线结果跑出来并保存,比如只用 BM25 的精确率、召回率,再用 Semantic 增强,最后加入 LLM Reranking。每改一步跑一次评测,这样才能知道是哪个阶段带来了收益,而不是凭感觉调参。
11.3 日志与可回溯性
批量任务一定要加日志。每条查询记录输入、输出、耗时、各阶段分数。医疗健康类数据事关重大,无法追溯的自动化流程不适合上线。
11.4 权限与安全
API 服务不要直接监听0.0.0.0暴露到公网。本地测试时用127.0.0.1,如果需要内网访问,建议加一层鉴权。涉及患者数据、未成年人数据时,必须完成脱敏处理并符合当地数据保护法规。ADHD 症状数据属于敏感健康信息,处理和使用需要特别谨慎。
11.5 合规提醒
无论是从社交媒体采集文本还是使用第三方模型权重,都要确认来源合法、授权清晰。如果你的目标是发表论文或商用,务必查看模型许可证。Reranker 模型通常有自己的使用条款,不能想当然地认为开源模型可以随意商用。
12. 总结与下一步
DS@GT-ARC 这个方案最值得尝试的地方,是把“稀疏检索 + 语义检索 + LLM 重排序”三种思路组合成一个多级漏斗,每一级都在控制成本并提升精度。对正在做 RAG、健康文本挖掘、文献筛选或相似句检索的读者,这套架构可以直接借鉴。
建议第一步先跑通 Sparse + Semantic 两阶段,用一个小型候选集验证数据链路是否正常;确认没问题以后,再加入 LLM Reranking,对比三个阶段各自的准确率提升和耗时成本。最容易踩的坑有两个:一个是中文或英文的分词逻辑不匹配导致召回为零,另一个是 LLM 推理显存不够导致服务崩溃——这两点在测试阶段就会暴露,提前准备好优化策略可以节省大量时间。
后续可以扩展的方向包括:把 Sparse 换成更轻量但更精准的短语匹配模型,把 Semantic 召回升级为 FAISS 或 Milvus 向量数据库,把 LLM Reranking 换成蒸馏后的小型 cross-encoder 以降低推理成本。如果数据规模和性能要求继续提高,整条管线还可以打包成 Docker 服务,和现有的分析平台对接。
这套方案不限定于 ADHD 症状检索,你可以把查询词、标注数据和评测标准替换成任意垂直领域的文本筛选任务。先跑通,再优化,这是最稳妥的落地路径。建议收藏备用,后面踩坑的时候直接对照排查表。