混合检索排序实战:从稀疏到LLM重排序的ADHD症状句子检索

混合检索排序实战:从稀疏到LLM重排序的ADHD症状句子检索 从 Sparse 到 LLM RerankingADHD 症状句子检索的混合排序方案这次我们来看一个学术竞赛里的实战项目DSGT-ARC 团队在 eRisk 2026 Task 3 上提交的 ADHD 症状句子检索方案。项目标题很直白——DSGT-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 症状句子”。这里的难点有两个症状表达很口语化。用户不会写“我存在注意力维持困难”而是写“我上课总是走神作业拖到最后一刻”。这种表达与教科书定义相差很远纯关键词匹配会漏掉大量目标句。负样本干扰大。帖子里有大量无关的日常叙事——比如“今天天气不错”“我吃了午饭”——这些句子和症状句在语言上没有任何明显区别需要更深层的语义理解才能区分。DSGT-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 均可生产环境推荐 LinuxPython3.9 及以上包管理工具pip 或 conda深度学习框架PyTorch版本需对应 CUDA 版本Transformers 库用于加载句向量模型和 LLM向量索引FAISS 或 Milvus小规模直接用 NumPy 也可以GPU运行 LLM Reranking 需要 NVIDIA GPU显存建议至少 8G具体以模型为准磁盘空间取决于模型大小句向量模型通常几百 MBLLM 权重从几 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, encodingutf-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, encodingutf-8) as f: json.dump(corpus, f, ensure_asciiFalse) print(fBM25 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, encodingutf-8) as f: for line in f: corpus_texts.append(json.loads(line)[text]) # 批量生成向量batch_size 根据显存调整 embeddings encoder.encode(corpus_texts, batch_size32, show_progress_barTrue) np.save(outputs/corpus_embeddings.npy, embeddings) print(fEmbedding 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 cant 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, paddingTrue, truncationTrue, return_tensorspt, ) 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(titleADHD Symptom Retrieval API) class SearchRequest(BaseModel): query: str top_k: int 20 # 启动时加载索引生产环境建议换成独立的向量数据库 corpus_texts [] with open(data/corpus.jsonl, r, encodingutf-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, host127.0.0.1, port8000)启动命令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 cant 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 cant 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}, timeout60, ) outputs.append(resp.json()) with open(outputs/batch_results.json, w, encodingutf-8) as f: json.dump(outputs, f, ensure_asciiFalse, indent2) print(fBatch 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-gpuutilization.gpu,memory.used --formatcsv -l 1如果是 7B 参数模型FP16 推理需要大约 14G 显存量化到 INT8 可以降到 7G 左右。但这属于典型值实际占用要以你选择的模型、推理框架和 batch 大小为准。如果显存不足优先考虑量化模型或者把候选集裁到几百条以内再交给 LLM。9.4 性能优化建议预处理阶段过滤过短的句子。少于 5 个词的句子通常不具备足够上下文信息。Semantic 检索阶段先用 Sparse 结果缩小范围再算向量能大幅减少 embedding 推理数量。LLM Reranking 阶段对候选集做截断只选前 200 到 500 条防止推理耗时过长。如果想加速 Semantic 检索可以把向量索引换成 FAISSimport 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 结果确认英文按空格切分中文改用 jiebaSemantic 检索结果全部无关向量模型未正确加载打印 query 和 document 向量维度检查模型名是否拼写正确确认句子没有全部变成空串LLM Reranking 分数都一样模型输出层理解错误查看模型类型和 logits 形态换用 cross-encoder 模型或改为分类模型取概率启动 API 时端口被占用8000 端口已有服务netstat -anofindstr :8000显存不足导致推理报错模型太大或 batch 太大观察nvidia-smi显存占用量化模型、降低 batch_size、减少候选集数量批量任务中途卡住某条查询请求超时看服务端日志和超时时间对 API 加超时控制服务端增加异常捕获结果里大量无关句子召回阶段阈值设置太低检查召回数量和各阶段分数调高 LLM Reranking 的阈值或收紧 Semantic 相似度阈值中文文本无法正确分词没有安装中文分词器检查 tokenize 逻辑安装 jieba 或使用中文 tokenizer11. 最佳实践与使用建议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. 总结与下一步DSGT-ARC 这个方案最值得尝试的地方是把“稀疏检索 语义检索 LLM 重排序”三种思路组合成一个多级漏斗每一级都在控制成本并提升精度。对正在做 RAG、健康文本挖掘、文献筛选或相似句检索的读者这套架构可以直接借鉴。建议第一步先跑通 Sparse Semantic 两阶段用一个小型候选集验证数据链路是否正常确认没问题以后再加入 LLM Reranking对比三个阶段各自的准确率提升和耗时成本。最容易踩的坑有两个一个是中文或英文的分词逻辑不匹配导致召回为零另一个是 LLM 推理显存不够导致服务崩溃——这两点在测试阶段就会暴露提前准备好优化策略可以节省大量时间。后续可以扩展的方向包括把 Sparse 换成更轻量但更精准的短语匹配模型把 Semantic 召回升级为 FAISS 或 Milvus 向量数据库把 LLM Reranking 换成蒸馏后的小型 cross-encoder 以降低推理成本。如果数据规模和性能要求继续提高整条管线还可以打包成 Docker 服务和现有的分析平台对接。这套方案不限定于 ADHD 症状检索你可以把查询词、标注数据和评测标准替换成任意垂直领域的文本筛选任务。先跑通再优化这是最稳妥的落地路径。建议收藏备用后面踩坑的时候直接对照排查表。