尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从BM25到LLM重排序:构建ADHD症状句子检索系统

从BM25到LLM重排序:构建ADHD症状句子检索系统 做心理健康领域的 NLP 系统最难的不是模型选型而是怎么定义“相关”。在 eRisk 2026 Task 3 这种任务里给定一条 ADHD 症状描述比如“难以开始一项任务”系统要从用户的社交文本中找出真正体现这个症状的句子。用户不会老老实实写“我有注意缺陷多动障碍”他们更可能写“我又拖到凌晨三点才开始写作业”“开会的时候我刷了十分钟手机”。这种表达差异决定了纯关键词匹配一定会漏纯向量检索又说不清为什么相关直接让大模型全库扫一遍又贵到不现实。从 DSGT-ARC at eRisk 2026 这个参赛系统所代表的技术路线来看最务实的做法是把稀疏检索Sparse、语义检索Semantic和 LLM 重排序LLM Reranking串成一个三级漏斗先用 BM25 这类 Sparse 方法保证基础召回和可解释性再用向量检索补充同义改写和上下文语义最后用 LLM 对候选句子做精排。这套组合不是炫技而是工程上的必然选择。这篇文章不打算复述论文而是把这条路线拆成可落地的代码流水线。读完你会理解每一级检索解决什么问题、在什么时候引入 LLM 重排序最划算、以及评估一个 ADHD 症状句子检索系统该看哪些指标。1. eRisk 2026 Task 3 在解决什么问题1.1 从 eRisk 说起eRisk 是 CLEF 实验室长期举办的共享任务全称是 Early Risk Prediction on the Internet核心目标是从互联网用户公开产生的文本中尽早识别抑郁、自伤、饮食失调、注意力缺陷多动障碍等心理风险。它和传统情感分析最大的区别在于“早期”不是等用户已经明确表达了求助信号而是在信号还比较微弱、表达还比较隐晦的时候就尝试发现风险。Task 3 聚焦 ADHD 症状句子的检索与重排序。从任务名称看系统输入是一个或多个症状描述输出是候选句子的相关度排序。为什么要做句子级而不是用户级因为风险判断需要证据。一个模型如果只说“这个用户可能有 ADHD”在真实场景里是很难被信任的如果它能指出“这句话描述的是难以维持注意力”“这句话反映了组织计划能力下降”那么后续专业人员就能根据这些证据做进一步判断。1.2 句子级检索的定义与难点如果把 Task 3 抽象成信息检索问题它就是一个 query-document 相关度排序任务query一条 ADHD 症状描述比如 difficulty concentrating、difficulty initiating tasks、often loses things。document从用户发帖中切分出来的句子。目标对所有候选句子按与 query 的相关度从高到低排序并返回 TopK。这个任务有三个难点第一表达极度口语化。ADHD 症状在真实文本里往往不是医学用语而是生活场景描述。比如“我常常同时开十几个浏览器标签页”可能是注意力分散的表现“买了三本同款笔记本然后又找不到了”可能是丢三落四的表现。关键词匹配很难覆盖这些改写。第二正样本稀缺。症状相关的句子在全部用户文本中占比极低直接训练一个二分类器很容易因为类别不平衡而失效。评测任务通常更关注排序指标比如 nDCG10、Recall100而不是简单准确率。第三隐私敏感。这里处理的是心理健康相关文本不能把用户真实 ID、社交关系链、地理位置等作为特征随意拼接更不能把原始语料直接丢给外部 API 不做脱敏。1.3 为什么需要 Reranking 流水线全库文本直接让 LLM 打分理论上最灵活但实际不可行。假设一个用户有一万条分句每条都给 LLM 生成一个分数成本和延迟都会爆炸。更合理的做法是“先粗糙召回再精细重排”。召回阶段要尽可能不遗漏相关句子排序阶段要尽可能把最相关的句子放到前面。DSGT-ARC 这个系统名里的 Sparse、Semantic、LLM Reranking本质上描述了完整的漏斗结构Sparse 和 Semantic 并行做候选生成LLM 做最终精排。这也是目前信息检索领域非常主流的生产架构。2. Sparse、Semantic、LLM 重排序能力与边界2.1 Sparse Retrieval关键词匹配依然不可替代Sparse Retrieval 的核心是词汇匹配。最经典的算法是 BM25它基于词频和逆文档频率给每个 query 词在文档中的出现计算加权分数。在 ADHD 症状句子检索里BM25 的价值在于可解释性强。模型说这句话相关至少是因为它命中了某个关键词比如 attention、focus、procrastinate。零训练成本。不需要标注数据直接对分词后的文本建索引。速度快。普通 CPU 上也能轻松处理上万条句子。适合术语和专用词。医学名词、药物名称、特定行为词往往在向量空间里反而不够突出。但 BM25 也有明显短板词汇鸿沟。query 是 difficulty organizing tasks句子是 “I always forget what I’m supposed to do next”两者没有共同关键词但语义上是相关的。只靠 BM25这种句子永远不会进候选集。2.2 Semantic Retrieval用向量补上同义改写Semantic Retrieval 通常使用双塔编码器bi-encoder把 query 和 document 分别编码成稠密向量再用余弦相似度计算相关度。常见的模型有 bge、all-MiniLM、E5 系列。它的优势是能匹配同义词和近义表达。比如 query 里的 “lose focus”能被 “my mind drifts away” 这种句子命中。对上下文有一定理解能力。可以预计算句子向量线上只需要计算 query 向量再和向量库做相似度检索。但语义检索也有代价训练数据如果不匹配领域模型容易把“相关”理解成“话题相似”。可解释性弱。模型给高分但说不出是哪个词起的作用。只做双塔编码时query 和 document 没有深度交互复杂推理能力有限。资源开销比 BM25 高需要向量索引比如 faiss、milvus。2.3 LLM Reranking把精排交给大模型Reranking 阶段的任务是对召回的 Top 候选进行更精细的打分。LLM 在这里可以做三种范式Pointwise让 LLM 对每个 query-document 对输出一个分数比如 0 到 5 分。Pairwise让 LLM 比较两个文档输出哪个更相关然后通过排序算法组合成全局排序。Listwise把整个候选列表给 LLM让它生成一个重排后的列表。LLM 重排序的最大优势是复杂语义理解。它可以识别反讽、否定、隐含表达甚至可以结合 prompt 里给出的 ADHD 症状定义来做推理。但这个优势伴随着三个问题成本高。每调用一次都是 token 消耗。延迟高。不能对全库做。输出不稳定。LLM 可能给出不一致分数需要设计 prompt 和解析逻辑。2.4 三种方法对比维度SparseBM25Semantic向量检索LLM Reranking核心原理词频与倒排索引语义向量相似度大模型上下文推理可解释性高低中计算成本低中高延迟低中高同义词处理差好很好复杂语义理解差中好是否适合全库扫描是是否典型使用位置召回召回精排从工程角度看这三种方法不是竞争关系而是互补关系。正确的姿势是让它们各司其职。3. 整体系统架构从候选生成到精排3.1 三级漏斗设计一个稳定的 ADHD 症状句子检索系统可以设计成这样一个漏斗第一级Sparse 和 Semantic 并行召回。Sparse 召回用 BM25 对全部分句打分取 Top 200。Semantic 召回用双塔模型计算句子向量与 query 向量做相似度检索取 Top 200。第二级融合与去重。用 RRFReciprocal Rank Fusion或加权线性融合合并两路结果得到 Top 100。去掉重复句子和过短句子。第三级LLM 精排。对 Top 50 或 Top 100 的 query-sentence 对用 LLM 逐条打分。按 LLM 打分重新排序取 TopK 输出。这个设计背后的原因很直接如果只用 BM25召回到不了位如果只用向量检索偶尔会漏掉有明确关键词的强相关句子如果一开始就让 LLM 介入成本不可控。三级漏斗的核心是“把预算花在最值得精排的候选点上”。3.2 数据流全景整个流程可以描述为读取原始文本。分句切分成长度合适的句子。对句子做基本预处理比如去 URL、去多余空白。建立 BM25 索引同时生成句子向量。输入 query分别得到 BM25 候选和向量候选。融合候选得到待精排列表。调用 LLM 打分。输出最终排序结果。用标注好的评测集计算 nDCG、Recall 等指标。这个数据流有一个很容易被忽略的点分句质量直接决定检索上限。如果一句话被切得太碎比如把“我经常忘记钥匙放在哪里但是我又懒得找”切成“我经常忘记钥匙放在哪里”和“但是我又懒得找”两个片段的语义都不完整。实际项目中应该优先使用基于句法或标点的分句器而不是简单按句号切。3.3 评估协议的重要性eRisk 这类任务通常看重排序质量。常用指标包括nDCGK衡量 TopK 排序质量。RecallK衡量 TopK 是否覆盖了所有相关句子。MRR衡量第一个相关结果的位置。PrecisionK衡量 TopK 中的相关比例。没有人能保证模型一次就做到完美所以评估协议必须固定下来。建议在实验开始前先确定 K 值、指标计算方式、是否对长文本做截断否则后面所有对比都是无效的。4. 环境准备与前置条件4.1 运行环境建议使用 Linux 或 macOS 环境Python 3.9 及以上版本。核心依赖如下pip install rank-bm25 sentence-transformers torch transformers如果使用 OpenAI 兼容 API 做 LLM reranking再安装 openai 包pip install openai如果文本是中文建议安装 jieba 用于分词pip install jieba如果句子数量很大做 semantic retrieval 时推荐安装 faiss-cpu 或 faiss-gpupip install faiss-cpu注意版本号请以实际安装环境为准这里只演示通用思路。不要盲目升级到最新版本尤其是 torch 和 CUDA 的组合容易出现依赖冲突。4.2 模型选择建议不同阶段的模型可以分开选择Sparse 检索直接用 BM25不需要模型。Semantic 检索可以用 all-MiniLM-L6-v2 作为起点这个模型体积小、速度快适合快速验证如果效果不够再换 bge-base-en-v1.5 或 bge-large-en-v1.5。LLM Reranking如果预算允许可以调用商业 API 的轻量级模型如果希望本地部署可以用 7B 或 8B 级别的开源模型配合 vLLM 起一个 OpenAI 兼容服务。在心理健康文本场景中需要额外注意模型的领域适配性。通用语义模型可能在“医学表达”上得分高但对“日常化表达”不敏感。可以先人工看一批检索结果判断模型是真正理解症状还是只是在做话题匹配。4.3 数据准备你需要准备两类数据一类是用户文本分句后的句子集合用于检索。另一类是评测集每条包含一个 query、一个句子以及相关度标签通常是相关或不相关。如果没有现成数据可以先用公开数据集或人工构造小规模样本验证流程。以 ADHD 症状为例可以构造这样的样本querysentencelabeldifficulty concentratingI can never finish a movie without checking my phone.1difficulty concentratingThere’s a new coffee shop near my house.0真实项目中标签最好由具备心理学背景的人员审核避免模型学习到表面语言特征。5. 完整代码实现5.1 数据准备与分句预处理我们先从原始文本构造句子集合。假设输入是一个 JSON 文件每个元素包含用户 ID 和文本内容。# 文件路径prepare_corpus.py import json import re from typing import List, Dict def split_sentences(text: str) - List[str]: # 先按常见句子结束符切分再清洗 parts re.split(r(?[.!?。])\s, text.strip()) sentences [] for part in parts: part part.replace(\n, ).strip() if len(part) 3: continue sentences.append(part) return sentences def build_corpus(raw_file: str, output_file: str) - List[Dict]: with open(raw_file, r, encodingutf-8) as f: records json.load(f) corpus [] for rec in records: user_id rec.get(user_id, ) text rec.get(text, ) for sent in split_sentences(text): corpus.append({ user_id: user_id, text: sent }) with open(output_file, w, encodingutf-8) as f: json.dump(corpus, f, ensure_asciiFalse, indent2) return corpus if __name__ __main__: # 示例用法 corpus build_corpus(raw_posts.json, corpus.json) print(fbuild {len(corpus)} sentences)这段代码有两个关键设计用正则做分句而不是简单 split能保留句子的相对完整性。过滤过短句子避免把单个单词或孤立标点送进索引。5.2 Sparse 检索BM25 候选生成下面用 rank_bm25 实现稀疏检索。英文可以直接按空格分词中文需要额外用 jieba。# 文件路径sparse_search.py import json from rank_bm25 import BM25Okapi # 如果处理中文先加载 jieba # import jieba def tokenize(text: str, language: str en) - list: if language zh: # 中文分词 return list(jieba.cut(text)) return text.lower().split() def build_bm25(corpus_file: str, language: str en): with open(corpus_file, r, encodingutf-8) as f: corpus json.load(f) tokenized_corpus [tokenize(item[text], language) for item in corpus] bm25 BM25Okapi(tokenized_corpus) return bm25, corpus def sparse_search(query: str, bm25, corpus, top_k: int 200, language: str en): tokenized_query tokenize(query, language) scores bm25.get_scores(tokenized_query) ranked_idx sorted(range(len(scores)), keylambda i: scores[i], reverseTrue)[:top_k] return [corpus[i] for i in ranked_idx], [scores[i] for i in ranked_idx] if __name__ __main__: bm25, corpus build_bm25(corpus.json) results, scores sparse_search(difficulty concentrating, bm25, corpus, top_k10) for sent, score in zip(results, scores): print(score, sent[text])这个实现非常轻量适合快速跑通流程。缺点是每次搜索都重新计算全库分数如果句子量在百万级需要把 BM25 索引固化到磁盘或者使用 Elasticsearch。eRisk 这类任务的文本量通常可控先用内存版验证完全足够。5.3 Semantic 检索向量召回接下来用 sentence-transformers 实现语义检索。这里采用先对全部句子编码再用 numpy 做余弦相似度计算的方式便于理解原理。句子量更大时建议换 faiss。# 文件路径semantic_search.py import json import numpy as np from sentence_transformers import SentenceTransformer def build_semantic_index(corpus_file: str, model_name: str all-MiniLM-L6-v2): with open(corpus_file, r, encodingutf-8) as f: corpus json.load(f) model SentenceTransformer(model_name) texts [item[text] for item in corpus] embeddings model.encode(texts, normalize_embeddingsTrue, show_progress_barTrue) return model, corpus, embeddings def semantic_search(query: str, model, embeddings, corpus, top_k: int 200): query_vec model.encode([query], normalize_embeddingsTrue)[0] scores embeddings query_vec ranked_idx np.argsort(scores)[::-1][:top_k] return [corpus[i] for i in ranked_idx], [float(scores[i]) for i in ranked_idx] if __name__ __main__: model, corpus, embeddings build_semantic_index(corpus.json) results, scores semantic_search(difficulty concentrating, model, embeddings, corpus, top_k10) for sent, score in zip(results, scores): print(round(score, 4), sent[text])这里有两个细节值得注意normalize_embeddingsTrue 确保所有向量是单位向量之后可以直接用矩阵乘法代替余弦相似度计算。语义检索输出的相似度分数可能是负值这是正常的。后续融合时不要直接拿原始分数相加最好使用排名位置信息。5.4 融合召回结果RRFSparse 和 Semantic 的分数尺度完全不同不能直接相加。这里用 RRFReciprocal Rank Fusion融合两个排序列表。RRF 的核心思想是一个文档在两个列表中排名越靠前融合分越高。# 文件路径fusion.py from typing import List, Dict def rrf_fusion(ranked_lists: List[List[Dict]], k: int 60, top_k: int 100) - List[Dict]: scores {} for ranked in ranked_lists: for rank, doc in enumerate(ranked): doc_id doc.get(id) or doc[text] if doc_id not in scores: scores[doc_id] 0.0 scores[doc_id] 0.0 scores[doc_id] 1.0 / (k rank 1) ranked_docs sorted(scores.items(), keylambda x: x[1], reverseTrue)[:top_k] return [doc for doc, _ in ranked_docs]这里为了简单直接用句子文本作为文档 ID。真实系统里建议给每个句子分配一个唯一 ID避免相同文本被误合并。融合之后就可以得到待精排的候选列表。这个列表通常控制在 100 条以内后续 LLM 精排只处理这个列表能大幅降低成本。5.5 LLM Reranking用大模型精排候选句子LLM 重排序的代码实现取决于你用的是商业 API 还是本地模型。这里以 OpenAI 兼容接口为例因为很多开源模型部署工具也支持这个协议。# 文件路径llm_rerank.py import json import os from openai import OpenAI SYSTEM_PROMPT 你是一个信息检索专家。给定一个查询和一个句子判断该句子是否与查询描述的ADHD症状相关。 请只输出一个JSON对象格式为 {score: 0.0, reason: 简短理由} 评分标准 5分句子直接描述了查询中的症状。 3分句子明显相关但表达比较间接。 1分句子主题相关但不构成症状表现。 0分句子与查询无关。 .strip() def build_user_prompt(query: str, sentence: str) - str: return f 查询{query} 句子{sentence} 请判断句子与查询的相关程度输出JSON。 .strip() def parse_llm_response(content: str): # 兼容可能带 markdown 代码块的输出 content content.strip() if content.startswith(): content content.strip() if content.startswith(json): content content[4:] return json.loads(content) def llm_rerank(query: str, candidates: list, top_k: int 10): client OpenAI( api_keyos.getenv(OPENAI_API_KEY), base_urlos.getenv(OPENAI_BASE_URL, None) ) scored [] for doc in candidates: response client.chat.completions.create( modelos.getenv(RERANK_MODEL, gpt-4o-mini), response_format{type: json_object}, messages[ {role: system, content: SYSTEM_PROMPT}, {role: user, content: build_user_prompt(query, doc[text])} ], temperature0, ) content response.choices[0].message.content try: parsed parse_llm_response(content) score float(parsed.get(score, 0.0)) reason parsed.get(reason, ) except Exception as e: print(fparse error: {e}, content{content}) score 0.0 reason parse_failed scored.append({ text: doc[text], llm_score: score, reason: reason }) scored.sort(keylambda x: x[llm_score], reverseTrue) return scored[:top_k] if __name__ __main__: candidates [ {text: I can never finish a movie without checking my phone.}, {text: There’s a new coffee shop near my house.}, {text: I lost my keys again and found them in the fridge.}, ] results llm_rerank(difficulty concentrating, candidates, top_k2) for item in results: print(item[llm_score], item[text], item[reason])需要特别提醒三点temperature 必须设置为 0否则同一个句子不同轮次可能得到不同分数。response_format 和模型相关不是所有开源模型都支持 json_object 模式。如果服务不支持可以把响应格式要求写进 prompt并做好异常解析。不要把 API Key 写在代码里用环境变量管理。5.6 串联完整流水线把上面的模块串起来就得到一个完整的检索重排序系统。# 文件路径pipeline.py import json from sparse_search import build_bm25, sparse_search from semantic_search import build_semantic_index, semantic_search from fusion import rrf_fusion from llm_rerank import llm_rerank class SymptomSentencePipeline: def __init__(self, corpus_file: str, semantic_model: str all-MiniLM-L6-v2): self.corpus_file corpus_file self.bm25, self.corpus build_bm25(corpus_file) self.sem_model, self.sem_corpus, self.embeddings build_semantic_index(corpus_file, semantic_model) def search(self, query: str, candidate_pool: int 200, fusion_topk: int 100, llm_topk: int 10): sparse_results, _ sparse_search(query, self.bm25, self.corpus, top_kcandidate_pool) semantic_results, _ semantic_search(query, self.sem_model, self.embeddings, self.corpus, top_kcandidate_pool) fused rrf_fusion([sparse_results, semantic_results], top_kfusion_topk) return llm_rerank(query, fused, top_kllm_topk) if __name__ __main__: pipe SymptomSentencePipeline(corpus.json) results pipe.search(difficulty organizing tasks) for item in results: print(item[llm_score], item[text])这个 pipeline 类可以是后续所有实验的基础。你只需要替换 corpus 文件和 query就能跑新的任务。6. 运行结果与效果验证6.1 运行命令假设你已经准备好raw_posts.json可以按顺序执行python prepare_corpus.py python pipeline.py输出应该是每个候选句子的 LLM 分数、文本和理由。6.2 预期输出示例以 query difficulty concentrating 为例可能看到这样的输出5.0 I can never finish a movie without checking my phone. 4.0 I read the same paragraph three times and still dont know what it says. 1.0 I like to watch movies on weekends. 0.0 Theres a new coffee shop near my house.这里不需要和某个具体团队的结果完全一致重点观察两点排在最前面的是不是真正描述注意力困难的句子。LLM 给出的 reason 是否合理能不能看出模型判断依据。6.3 判断成功与否的标准在本地小样本上系统跑通只是第一步。判断是否有效需要看评测指标。可以写一个简单的评估脚本计算 nDCG10。这里给出一个最小实现# 文件路径evaluate.py import numpy as np def dcg_at_k(relevance: list, k: int) - float: relevance relevance[:k] if not relevance: return 0.0 return sum(rel / np.log2(idx 2) for idx, rel in enumerate(relevance)) def ndcg_at_k(relevance: list, k: int) - float: dcg dcg_at_k(relevance, k) ideal sorted(relevance, reverseTrue) idcg dcg_at_k(ideal, k) if idcg 0: return 0.0 return dcg / idcg # 示例三位标注人员对 Top10 的相关性打分 relevance [1, 1, 0, 1, 0, 0, 1, 0, 0, 0] print(ndcg_at_k(relevance, 10))如果 nDCG10 只有 0.2 或更低通常说明 LLM 重排序没有发挥预期作用。这时优先检查候选生成阶段是否漏掉了真正相关的句子而不是急着换更大的模型。6.4 失败时先查哪里整个流水线有多个环节一个问题可能来自任何一层。建议按以下顺序排查看 BM25 结果。如果 BM25 都没召回相关句子说明 query 和句子之间没有关键词重叠需要依赖语义检索。看语义检索结果。如果向量排名靠前的句子都不相关说明模型领域适应差考虑换模型或微调。看融合结果。如果融合后相关句子的排名比单个检索还低检查 RRF 参数和候选集大小。看 LLM 输出。如果 LLM 打分和人工判断明显不一致检查 prompt 是否说清了任务以及 temperature 是否为 0。7. 常见问题与排查方法下面表格总结了最容易遇到的问题。问题现象可能原因排查方式解决方案BM25 召回为空分词粒度不合适query 关键词太抽象打印 tokenize 后的 query 和 corpus中文用 jieba英文统一小写考虑人工扩展同义词语义检索结果全是无关句子通用 embedding 模型不匹配领域随机抽样 50 条结果人工评估换用领域数据微调模型或换多语言/医学领域模型LLM 输出不是合法 JSON模型不支持 response_formatprompt 被截断打印原始 content查看完整输出增加解析回退逻辑或改用正则提取 scoreLLM 打分波动大temperature 不为 0或模型版本不稳定固定 temperature0多次运行对比使用确定性解码参数必要时设置随机种子融合后排序反而下降原始分数直接相加或候选池太小打印融合前后的相关句子排名改用 RRF增加候选池到 100 条以上评测指标很低标签定义不统一检查评测集相关性标注一致性重新定义标签标准多人标注并计算 Cohen’s Kappa这些坑在真实项目中很常见尤其是 LLM 输出解析。不要假设大模型会严格按格式输出工程上必须对解析失败做兜底。8. 最佳实践与工程建议8.1 从最简单的基线开始如果你第一次做这种系统强烈建议先只跑 BM25 基线。哪怕它效果不理想也能让你理解数据形态。然后再逐步加入 Semantic、LLM Reranking。每一步都对比指标才能知道某个模块到底带来了多少提升。如果一上来就上全套 pipeline出现问题根本定位不到哪一层。8.2 中间结果落盘Sparse 候选、Semantic 候选、RRF 融合结果、LLM 打分这四个阶段的中间结果都应该保存成 JSONL 文件。这样做的价值在于出问题时可以回看。评估时不需要重新调用 LLM节省成本。多轮实验可以横向对比。# 保存中间结果示例 import json def save_jsonl(items, path): with open(path, w, encodingutf-8) as f: for item in items: f.write(json.dumps(item, ensure_asciiFalse) \n)8.3 为 LLM Reranking 加缓存LLM API 调用既贵又慢。同一个 query 和同一个句子组合不应该重复调用。可以维护一个 query sentence 的哈希缓存import hashlib def cache_key(query: str, sentence: str) - str: key query || sentence return hashlib.md5(key.encode(utf-8)).hexdigest()在调用 API 前先查缓存命中的直接用缓存分数。这个优化能让实验成本下降一个数量级。8.4 隐私与最小化原则心理健康文本是高度敏感数据。在工程实现中要注意只使用任务必要字段不保留社交关系、设备信息、精确时间戳。对外部 API 发送文本前先做脱敏比如移除用户名、URL、电话号码。如果数据不能出域就用本地模型替代外部 API。不要为了提升效果而采集额外个人信息。8.5 输出定位为研究辅助而非医学诊断ADHD 检索系统的输出应该被定位为“研究辅助工具”不能直接作为诊断依据。建议在系统界面和 API 文档里明确声明该结果是文本分析提示需由专业人员复核。这不仅是对用户负责也是降低法律和伦理风险的必要手段。8.6 对长句做截断LLM 输入长度有限。过长的句子不仅浪费 token还可能让模型忽略关键信息。建议在进入 LLM 前把句子长度限制在合理范围内比如 200 到 300 个字符。超长句子可以按语义断点切分或者只保留核心子句。def truncate_sentence(text: str, max_len: int 300) - str: if len(text) max_len: return text return text[:max_len].rsplit( , 1)[0] ...8.7 多个模型做集成如果评测集不大可以尝试用多个语义模型生成多路候选。例如一个通用模型加一个心理学领域模型再做 RRF 融合。多路召回能显著提升 Recall100尤其适合正样本稀疏的 eRisk 任务。9. 总结与后续学习方向Sparse、Semantic、LLM Reranking 的组合解决的是心理健康文本检索中最核心的一个矛盾如何在成本可控的前提下兼顾召回率、排序质量和可解释性。BM25 保证基线向量检索补足语义空间LLM 最后处理复杂表达。这种漏斗在 eRisk 2026 Task 3 这种句子级症状检索任务里是性价比最高的工程路线。做完这套系统之后如果你还想继续深入可以从三个方向扩展第一训练自己的重排序模型。用 LLM 对训练集生成标注再用 cross-encoder 蒸馏出一个小模型这样可以降低成本并提高稳定性。第二把症状检索与用户级风险预测打通。句子级证据最终要汇总成用户级判断这里可以尝试用分数聚合、证据抽取、多任务学习等方式。第三引入可解释性输出。不只是给分数还要给出模型判定相关的原因。比如“该句出现了‘丢三落四’‘找不到钥匙’两个行为描述”这样系统才更容易被心理专业人员信任。如果你打算参加下一届 eRisk最稳妥的建议是先提交一个纯 BM25 基线确保流程和数据管道没有问题再在第二个版本加入 semantics最终版本再上 LLM reranking。这样做不会因为实验节奏太快而错过真正的问题也能让每次迭代的效果可量化。
返回列表