稀疏检索+语义检索+LLM重排序:ADHD症状检测的多路召回实践

稀疏检索+语义检索+LLM重排序:ADHD症状检测的多路召回实践 在文本分类、信息检索与医疗健康文本挖掘交叉的场景里“稀疏检索 语义检索 LLM 重排序”这套组合已经被越来越多的竞赛方案和生产项目采用。本文以 DSGT-ARC 在 eRisk 2026 Task 3 上的方案为例拆解 ADHD 症状句子检测中的稀疏信号、语义信号和 LLM 重排序信号并给出可落地的代码思路、评估方式和工程排错建议。无论你是在参加类似的 NLP 评测任务还是想在自己的文本检索系统里引入多路召回和重排序这篇文章都值得收藏。1. 背景与核心概念1.1 什么是 eRisk Task 3eRisk 是信息检索领域里关注“心理健康风险”预测的相关任务系列主要利用用户在社交平台上的历史发帖文本去预测抑郁症、焦虑、饮食障碍、自杀风险、ADHD 等心理健康问题。Task 3 是其中关于 ADHD 症状句子检测的任务。DSGT-ARC 团队在 eRisk 2026 Task 3 中做的事情可以概括为第一阶段从候选用户中检测哪些用户可能患有 ADHD并给出按照患病概率排序的排名列表。第二阶段针对第一阶段识别出的 ADHD 用户进一步判断他们发布的句子中是否包含注意力不集中、多动、冲动、组织问题、挫折耐受性差这五类典型 ADHD 症状。这个任务不同于普通的情感分类或主题分类它是一种“风险信号发现 细粒度证据定位”的组合任务。第一阶段解决“谁有风险”第二阶段解决“风险体现在哪里”。1.2 稀疏、语义与 LLM 重排序在文本检索领域稀疏检索、语义检索和 LLM 重排序是三个层次分明又互补的技术方向。稀疏检索Sparse Retrieval核心是基于词汇重叠。常见方法有 TF-IDF、BM25、BoWSBag of Words with Stems词干词袋等。优点是计算快、可解释性强缺点是遇到同义改写、语义泛化时容易漏召回。语义检索Semantic Retrieval核心是基于向量表示。常见做法是用 Sentence-BERT、E5、BGE 等模型把查询和文档映射到高维向量空间再用余弦相似度计算相关性。优点是能处理同义表达和语义近似缺点是训练数据和模型领域适配度会直接影响效果。LLM 重排序LLM Reranking核心是利用大语言模型的指令理解能力和推理能力把第一阶段的候选文档重新排序。LLM 可以结合任务定义、约束条件、分类体系等细粒度信息给出比传统打分函数更灵活的判断。在 DSGT-ARC 的方案中这三条路线并不是彼此孤立的而是形成了一条“稀疏召回 → 语义召回 → LLM 重排序”的流水线。这也是许多 RAG 应用、企业级搜索系统、知识库问答系统的通用架构。1.3 为什么需要理解这个方案ADHD 症状句子的检测难点在于症状表达高度口语化用户在社交平台上的发帖通常没有规范医疗术语。不同用户表达同一种症状的表达方式差异很大。正常用户也可能偶尔讨论“注意力差”“没耐心”需要区分偶发表达和持续性症状。正负样本极不均衡。因此单靠传统词频方法或者单靠一个向量模型很难同时兼顾召回率和排序质量。理解 DSGT-ARC 的多信号融合方案能够帮助你在面对类似文本挖掘任务时快速制定出可行路线。2. 数据集与任务设定说明2.1 数据来源与分布根据论文描述任务的数据来自 eRisk 2023 的数据集合。训练集包含 141 名 ADHD 用户和 843 名对照用户正负样本比例大约是 1:6。这种不平衡比例在医疗文本挖掘任务中非常常见。用户输入的特征是他们在社交平台发帖的文本序列。对于第一阶段一条样本就是一个用户的全部帖子文本。对于第二阶段一条样本就是一段带有候选标签的句子或片段。在实际竞赛和项目中你还需要对数据做以下检查用户 ID 是否唯一。帖子时间戳是否完整。文本是否包含大量 URL、表情符号、删除标记。是否存在同一用户的帖子被重复收集。2.2 五类 ADHD 症状体系在第二阶段系统需要根据预定义的症状体系对句子进行排序。这里的体系选自 PCL-5 相关症状框架并结合 ADHD 特征做了细化。核心症状包括以下五类症状类别示例表达注意力不集中/注意力问题“我开会的时候总是走神完全不知道刚才讲了什么。”多动“我感觉自己一刻也坐不住总想走来走去。”冲动“我买东西从来不考虑后果看到就下单。”组织问题“我的桌面永远乱成一团计划总是半途而废。”挫折耐受性差“一遇到排队或者等待我就特别容易发火。”这种细粒度分类意味着模型不能只判断“是否和 ADHD 相关”还必须判定“具体属于哪一类症状”否则第二阶段的信息增益会非常有限。2.3 评价指标eRisk 任务中最常见的评价指标包括P10排名列表前 10 个结果中相关结果占比。NDCG10排名列表前 10 个结果的归一化折损累计增益。R10排名列表前 10 个结果中相关结果占全部相关结果的比例。这三个指标各有侧重点P10 关心“推荐结果准不准”。NDCG10 关心“相关结果是不是排在前面”。R10 关心“有没有漏掉太多正确结果”。在实际评估时你需要同时关注 Precision 和 Recall不要只看单一指标。尤其是医疗场景漏检的代价往往比误报更大。3. 算法方案拆解3.1 稀疏信号BoWS 逻辑回归在第一阶段DSGT-ARC 使用了基于 BoWS 特征的方法。BoWS 是 Bag of Words with Stemming 的缩写意思是先对文本做分词、去停用词、词干还原再构建词频向量。为什么选择 BoWS 而不是直接使用原始 BoW词干还原能够把“focus”“focusing”“focused”合并成同一个词干减少特征稀疏。去停用词可以过滤掉“the”“and”“of”这类无信息量词。词频向量配合逻辑回归可以得到每个词对 ADHD 判断的权重贡献可解释性较强。逻辑回归模型的输出可以作为用户的 ADHD 风险分数。将所有用户按分数降序排列就得到了第一阶段需要的排序列表。from sklearn.feature_extraction.text import TfidfVectorizer from sklearn.linear_model import LogisticRegression from sklearn.pipeline import Pipeline docs [ I keep losing my focus and cannot finish anything, I feel restless and need to move all the time, I am really mad at waiting in line, ] labels [1, 1, 0] vectorizer TfidfVectorizer( ngram_range(1, 2), stop_wordsenglish, sublinear_tfTrue, ) model Pipeline([ (tfidf, vectorizer), (lr, LogisticRegression(max_iter1000)), ]) model.fit(docs, labels)这段代码只用于演示思路。在实际任务中你需要先阅读 eRisk 官方数据格式把每个用户的文本拼接成用户级文档再构建训练集和验证集。3.2 语义信号Sentence-BERT 与密集检索稀疏信号虽然稳定但遇到“没话找话”“换一种说法”这类语义改写时容易失效。比如用户说“我的脑子像一团浆糊没办法持续思考”这个词和“注意力不集中”没有太多词面重叠但语义上高度相关。这时可以用语义编码模型把用户文本和症状定义分别编码成向量再计算相似度。from sentence_transformers import SentenceTransformer, util model SentenceTransformer(all-MiniLM-L6-v2) symptom_defs { inattention: difficulty sustaining attention, careless mistakes, losing things, hyperactivity: restlessness, inability to stay still, excessive movement, impulsivity: acting without thinking, interrupting others, risky decisions, } user_text I keep losing my focus and cannot finish anything user_vec model.encode(user_text, convert_to_tensorTrue) for symptom, definition in symptom_defs.items(): symptom_vec model.encode(definition, convert_to_tensorTrue) score util.cos_sim(user_vec, symptom_vec).item() print(f{symptom}: {score:.4f})在 eRisk 这类文本中直接用通用 Sentence-BERT 模型做编码往往不够理想。更推荐的做法是使用领域预训练模型或者继续在社交平台健康文本上做领域自适应。把症状定义写成多条候选证据用多向量平均或最大池化来增强稳定性。对用户文本进行窗口切分而不是只对整段文本编码一次避免长文本语义被稀释。3.3 LLM 重排序从生成到排序DSGT-ARC 方案中使用到的 LLM 模型包括 GPT-3.5-turbo、Gemini 1.5 Flash、Llama 3.1 8B、Llama 3.3 70B、Mistral 7B、Mixtral 8x7B 等。这些模型在任务中并不是用来做简单的“是或否”分类而是承担重排序职责。重排序的基本思路如下第一阶段先用稀疏或语义方法召回一批候选句子。对每个候选句子构建一个包含任务说明、症状定义、用户句子的 Prompt。让 LLM 输出一个相关性评分或者输出一个包含评分理由的 JSON。根据评分对候选句子重新排序。使用 LLM 做重排序的一个关键优势是你可以把“用户讨论 ADHD 症状时常见的表达模式”“这五种症状的边界”“评分标准”等背景知识直接写进 Prompt。3.4 多路信号融合为什么不能只依赖一路信号稀疏信号召回稳定但容易漏掉语义改写。语义信号能覆盖同义表达但训练数据偏差可能造成误判。LLM 重排序灵活但成本高、速度慢不适合对全量候选做排序。因此更合理的流程是全部候选句子 ↓ 稀疏检索BM25 / BoWS—— 快速过滤保留 Top 200 ↓ 语义检索Sentence-BERT—— 跨语言/语义扩展保留 Top 50 ↓ LLM 重排序 —— 精细打分保留 Top 10这个流水线既控制了计算成本又保证了最终排序质量。4. 核心代码实现下面给出一个贴近 DSGT-ARC 思路的完整示例工程。代码以 Python 为主使用了常见的检索和机器学习库。4.1 项目结构adhd_rerank_demo/ ├── data/ │ ├── users.txt │ └── symptoms.json ├── src/ │ ├── sparse.py │ ├── semantic.py │ ├── llm_rerank.py │ └── evaluate.py ├── config.yaml └── main.py4.2 数据预处理假设用户数据是一个 JSON 文件每条记录包含用户 ID 和帖子列表。import json import re def load_users(path: str) - list[dict]: with open(path, r, encodingutf-8) as f: raw json.load(f) return raw def clean_text(text: str) - str: text re.sub(rhttp\S, , text) text re.sub(r\w, , text) text re.sub(r\s, , text) return text.strip() def build_user_doc(user: dict) - dict: content .join(clean_text(p) for p in user[posts]) return {user_id: user[user_id], text: content}4.3 稀疏检索模块这里以 BM25 为例它比 TF-IDF 更适合处理长文本检索。from rank_bm25 import BM25Okapi import jieba # 如果是中文数据需要分词 def tokenize(text: str, language: str en) - list[str]: if language zh: return list(jieba.cut(text)) return text.lower().split() docs [ I keep losing my focus and cannot finish anything, I feel restless and need to move all the time, I am mad at waiting in line, ] tokenized_docs [tokenize(doc) for doc in docs] bm25 BM25Okapi(tokenized_docs) query unable to concentrate scores bm25.get_scores(tokenize(query))在 eRisk 场景中你需要把“五类症状定义”作为查询把用户帖子句子作为候选文档集合。BM25 只负责召回候选不负责最终判断。4.4 语义检索模块语义检索使用的模型需要根据数据语言选择。英文数据可以用all-MiniLM-L6-v2或BAAI/bge-base-en-v1.5中文数据可以用BAAI/bge-base-zh-v1.5。from sentence_transformers import SentenceTransformer, util encoder SentenceTransformer(BAAI/bge-base-zh-v1.5) query_vec encoder.encode(注意力不集中, convert_to_tensorTrue) doc_vecs encoder.encode(sentences, convert_to_tensorTrue) scores util.cos_sim(query_vec, doc_vecs)[0].tolist()注意BAAI/bge-*系列模型在计算相似度时通常会给查询添加提示词具体格式要参考模型卡说明。这里给出的是核心思路实际项目请结合模型卡提示做调整。4.5 LLM 重排序模块使用 OpenAI 风格的 API 接口时核心是设计好 Prompt 和输出格式。import json from openai import OpenAI client OpenAI( api_keyyour-api-key, base_urlhttps://your-endpoint.example.com/v1, ) SYMPTOM_DESC { inattention: 注意力不集中难以持续注意容易走神经常丢三落四。, hyperactivity: 多动坐不住总是需要动来动去。, impulsivity: 冲动不加思考就行动容易打断别人。, organization: 组织问题计划混乱桌面杂乱难以整理任务。, } def llm_rerank(query: str, candidates: list[str], symptom: str) - list[dict]: results [] for idx, cand in enumerate(candidates): prompt f 你是一个心理健康文本标注助手。 目标症状{SYMPTOM_DESC[symptom]} 用户句子{cand} 请判断这句话是否与目标症状相关 只输出 JSON 格式{{relevant: 0 或 1, score: 0.0 到 1.0, reason: 简短理由}} response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个严谨的文本标注器。}, {role: user, content: prompt}, ], temperature0, response_format{type: json_object}, ) content json.loads(response.choices[0].message.content) results.append({ index: idx, candidate: cand, **content, }) results.sort(keylambda x: x[score], reverseTrue) return results在生产环境中强烈建议把温度设置为 0避免评分抖动。同时给每次调用增加超时限制和重试机制。4.6 指标计算实现 NDCG10、P10 和 R10 并不复杂但要注意边界情况。def dcg_at_k(relevances: list[int], k: int) - float: rel relevances[:k] if not rel: return 0.0 return sum(r / float(rel_idx 1) for rel_idx, r in enumerate(rel)) def ndcg_at_k(relevances: list[int], k: int) - float: dcg dcg_at_k(relevances, k) ideal sorted(relevances, reverseTrue) idcg dcg_at_k(ideal, k) if idcg 0: return 0.0 return dcg / idcg def precision_at_k(relevances: list[int], k: int) - float: return sum(relevances[:k]) / k if k 0 else 0.0 def recall_at_k(relevances: list[int], k: int, total_relevant: int) - float: if total_relevant 0: return 0.0 return sum(relevances[:k]) / total_relevant注意这里relevances是 0/1 列表表示排序后前 N 个结果的真实相关性。4.7 整体流程编排这里用一个简单的 Python 示例串联整套流程。注意为了让读者容易理解这里做了简化实际项目需要加入异常处理、缓存和日志。def process_user(user_doc: dict, symptom_defs: dict): text user_doc[text] # 1. 稀疏召回 sparse_candidates sparse_recall(text, symptom_defs, top_k200) # 2. 语义召回 semantic_candidates semantic_recall(text, symptom_defs, top_k50) # 3. 合并候选集 merged merge_candidates(sparse_candidates, semantic_candidates) # 4. LLM 重排序 reranked llm_rerank(text, merged, symptominattention) return reranked[:10]实际工程中建议把每一步拆成独立函数不要让process_user过长。5. 常见问题与排查思路5.1 正样本量太少模型过拟合现象训练集上表现很好测试集上指标大幅下降。原因ADHD 用户只有一百多名模型很容易记住训练集用户特有的表达方式。排查思路检查训练集和测试集的用户 ID 是否重叠。使用用户级别的交叉验证而不是句子级别的随机划分。引入对照组数据做负样本采样保持比例稳定。解决方案使用 BoWS 逻辑回归作为强基线不要一开始就用复杂度高的深度模型。在语义模型上使用领域自适应继续预训练。降低模型容量增加正则化。5.2 LLM 重排序评分不稳定现象同一句话多次调用 LLM 得到的分数差异很大。原因温度参数过高或者 Prompt 表述模糊。排查步骤将temperature设置为 0。固定输出格式强制输出 JSON。增加评分锚点例如给出“完全相关”“部分相关”“不相关”三个示例。对一个候选句子采样多次取平均分。5.3 稀疏检索漏召回现象BM25 召回的候选中没有包含正确答案。原因查询词和文档间缺乏词面重合而 BM25 本质上是词面匹配。解决方案扩大召回集合从 Top 50 扩大到 Top 200。加入同义词扩展例如把“attention”扩展到“focus”“concentration”。使用伪相关反馈从第一轮检索结果中提取高频词再查一次。5.4 候选句子过长导致语义向量失真现象一句话超过 512 个 token编码后语义信息被稀释。解决方案对长文本做滑窗切分。同时保留段落级和句子级向量最后取归一化分数融合。5.5 API 调用成本过高现象LLM 重排序需要对大量候选句子做推理费用和时间消耗很大。解决方案先通过稀疏和语义召回把候选压缩到 10 到 20 条。设置 stage 阈值例如语义相似度小于 0.4 的句子不进入 LLM 重排序。使用批量推理接口而不是逐条调用。常见开源模型的本地部署可以进一步降低成本。6. 工程最佳实践与扩展思路6.1 Prompt 设计要兼顾“任务描述”和“边界定义”医疗场景下的 LLM 重排序Prompt 不能只写“这句话是否相关”。还需要描述 ADHD 症状名词的准确含义。强调“用户表达自己的经历”比“引用网上别人的经历”更有价值。提示模型区分“偶尔懒散”和“长期功能受损”。一个简单的做法是把症状描述拆成正例、负例、边界例。6.2 使用缓存机制避免重复推理对于相同的用户文本和候选句子LLM 的调用结果是可缓存的。建议用哈希值做缓存键把 LLM 评分结果存储到本地数据库或 Redis 中。6.3 引入 Agent 与 MCP 思路在更复杂的应用中可以把上述流程封装成一个检索 AgentAgent 接收用户输入决定先做稀疏检索还是语义检索。当结果不足时自动调用 Web Search 或数据库查询。当症状类别不明确时Agent 调用 LLM 进行意图澄清。MCPModel Context Protocol的作用是统一这个 Agent 与外部工具之间的通信协议。它适合把“症状查询工具”“病历数据库”“时间线分析工具”注册成标准的 MCP 工具让大模型能够按需调用。6.4 日志和可追溯性在医疗文本场景模型输出的可解释性和可追溯性很重要。每次重排序结果都要保存 Prompt 版本、模型版本、温度参数。对每个候选结果保存“为什么排序高/低”的理由摘要。用户级结果需要做二次人工抽检。6.5 处理跨时间漂移eRisk 数据来自用户的长期发帖训练集和测试集可能是不同时间段的数据。社交平台上的流行表达会随时间变化所以建议对帖子时间做切分避免训练集和验证集来自同一时间段。在测试前做“时间敏感特征检查”例如发现测试数据中出现了大量训练时不存在的新词。7. 总结与后续学习方向本文以 DSGT-ARC 在 eRisk 2026 Task 3 上的方案为背景梳理了 ADHD 症状句子检测任务中的稀疏检索、语义检索和 LLM 重排序三种信号。核心要点可以总结为以下几点不要把希望完全寄托在某一种检索模型上多路召回 重排序是更稳定实用的框架。稀疏检索提供稳定基线语义检索提升语义覆盖能力LLM 重排序解决细颗粒度判断问题。评估指标不能只看单一指标P10、NDCG10、R10 需要配合使用。医疗场景中结果的可解释性和可追溯性比模型精度更重要。接下来你可以继续学习的内容包括基于 RAG 的医疗问答系统搭建、LLM 输出结构化解析、跨语言语义检索、以及如何用 MCP 把检索服务接入 Agent 工作流。如果本文对你有帮助可以收藏备用后续搭建检索系统时对照着做多路召回和重排序会少踩很多坑。