ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

医疗问答RAG全流程指南:从知识库搭建到检索调优

医疗问答RAG全流程指南:从知识库搭建到检索调优 简介基于 RAG 与大模型技术的医疗问答系统源码及配套资料面向计算机相关专业正在准备毕业设计、课程设计或期末大作业的学生也适合想通过完整项目学习大模型应用开发的初学者。项目经导师指导并以99分高分通过代码完整可直接运行核心覆盖医疗知识图谱构建、命名实体识别、大模型 LoRA 微调、RAG 问答检索与 Web 交互界面等关键环节技术栈涉及 Python、LangChain、ChatGLM、Neo4j 与 PyTorch。压缩包共75个文件大小约84.65MB含10个 Python 脚本、7个可运行的 Jupyter Notebook、7个 JSON 数据文件、3个 YAML 配置、19个文本说明以及18张界面与流程截图。源码中提供了从数据预处理、实体与关系抽取、nl2cypher 查询生成、模型微调到前端登录与问答页面的完整链路Notebook 还按“解析”“微调运行”“结果分析”等阶段拆解便于边读边跑。读者可复用医疗标注数据、微调配置与推理脚本借助截图和说明快速掌握项目流程还便于替换数据或调整模型投入二次开发。目前已有178人浏览学习是一份扎实的高分毕设参考与RAG项目实战范本。1. 引子当患者问“要不要去医院”RAG 比大模型更值得托底基于 RAG 与大模型技术的医疗问答系统这两年最常被拿来当毕设题目但你如果只是照着 RAG 教程把 demo 跑通再去答辩大概率会被问倒检索不准、引用编造、答案前后矛盾——这三座大山几乎每个新手都会撞上。医疗问答和通用闲聊的本质区别在于它不能靠模型“背答案”患者问“发烧两天带黄痰要不要去医院”大模型能写出八百字居家护理建议但本地流感流行情况和急诊指征它一概不知搜索引擎又全是广告和科普混杂。RAG 框架要解决的就是让大模型“先查证、再说话”把诊疗指南和药品说明书预先切块入库问题来了先召回证据生成时只依据证据作答末尾能定位到原文。这套链路不仅适合毕设预问诊、导诊、院内知识库问答都是同一个套路。下面从选型讲到检索调优再给五个排查点和一套验收脚本。2. 为什么医疗问答绕不开 RAG架构选型与知识库形态的取舍2.1 RAG 在医疗场景的三层流水线入库、召回、生成的职责拆解一个能落地的医疗问答 RAG 系统从工程上拆成三段离线入库、在线召回、在线生成。离线入库是把诊疗指南、药品说明书、专家共识清洗后切块做向量化并写入索引在线召回是拿用户问题同时走关键词和语义检索找出最相关的若干片段在线生成则是把召回片段拼进提示词交给大模型约束输出。三段职责必须分开千万别混在一起调——我见过不少同学一上来就调提示词结果答案还是错最后发现是召回回来的证据本身就不对。医疗场景和通用问答有个决定性差异纠错成本高。电商客服说错退货政策顶多赔一张优惠券医疗问答如果说错禁忌后果是没法补救的。所以医疗 RAG 的重心不是“生成得更顺”而是“召回得更准、引用更可溯源”。这也意味着离线入库阶段的质量直接决定系统上限后面生成阶段做再多提示词工程也只是在烂地基上装修。另一个常见误区是问“要不要上重排”。如果知识库就几百条文档跑一次 top 5 检索也能应付但医疗知识库只要做到指南、药典、分诊规则三类条目就得上万粗召回噪声极大重排不是可选项而是必选项。可以把概念理解成漏斗检索负责“别漏”重排负责“别错”大模型只负责“把对的证据说成人话”。2.2 向量库与结构知识库怎么分工非结构化文本走检索症状-疾病-用药关系走图谱这是热词里问得最多的问题“RAG 知识库和结构知识库到底怎么区分、各自用在哪儿”我的经验是按知识的形态而不是按数据形式划分。诊疗指南里的“脓毒症早期识别流程”“急性胸痛危险分层标准”这类叙述性文本属于非结构化知识适合切块后进向量库做语义检索而“布洛芬→禁忌→孕妇”“胸痛→建议就诊科室→心内科”这类多对多的实体关系属于结构化知识适合放在关系表或知识图谱里供精确过滤和路径查询。有同学一上来就想上 ontology把所有疾病、症状、药品、禁忌全建成本体图谱工程量和维护成本在毕设周期内基本不可控。更务实的常见做法是“向量为主、规则表为辅”向量库管长的指南叙述分诊规则表和药品禁忌表单独建成结构化数据检索时先查规则表命中就直接给出查询不到再走向量召回。这样既保住了覆盖广度又保住了精确关系不丢。这里补一个热词里“RAG 知识库能不能存图片”的答案能但不建议直接存。医疗影像和图文报告这类内容更适合把图片的描述文字、报告结论抽出来入库图片本体挂在对象存储里回答时通过引用 ID 跳转。把图片直接向量化进库检索效果差而且 GPU 成本和存储成本都不划算。2.3 大模型底座怎么选微调不是第一选项私有化部署要算哪笔账底座模型的选择直接决定整个系统的成本结构。如果是毕设或者企业内部预研走私有化部署是主流Qwen、ChatGLM 这类开源底座7B 量化后单卡 24GB 显存就能跑推理配合 vLLM 之类的推理框架能支撑 demo 级别的并发。如果追求效果并且数据合规允许接闭源 API 省事但医疗数据出域这一条在很多场景直接把它否掉。关于微调行业里最常见的坑是顺序反了先是知识没做好就忙着微调花了一堆算力结果模型把术语说得更顺溜了但该不知道的还是不知道。微调在医疗问答里的定位应该是“锦上添花”比如让模型更习惯医学表述、稳定输出 JSON 格式而不是把知识灌进参数里。先跑通 RAG把检索命中率做上去再评估要不要做 LoRA 轻量微调。参数层面也就两笔账。一笔是 embedding 模型bge-large-zh 这一档CPU 就能跑显存占用几乎可以忽略。另一笔是底座模型7B fp16 权重约 14GB加 8K 上下文的 KV Cache单卡 24GB 够用如果非要上 70B 或 32K 以上长上下文就是多卡和推理框架的活了。很多团队在做医疗私有化部署时第一步不是买卡而是把底座从 7B 起步验证指标再决定要不要加预算——这个顺序最稳。3. 医疗知识库构建从诊疗指南到可检索向量池的完整流水线3.1 数据清洗规则去广告、去时效性内容、保留哪四类语料医疗知识库选语料优先级我一般排成四类诊疗指南与专家共识、权威药品说明书、医院公开的分诊规则与就诊流程、经过审核的科普文章。前两类是“硬知识”错了会出事故后两类是“软知识”辅助回答“什么情况该来医院”。语料来源必须保留来源标记后面引用溯源全靠它。至于论坛问答、自媒体“养生文”清洗成本再低也别要引用了就是事故。清洗阶段最常见的翻车是拿爬虫直接抓网页就切块入库结果每个 chunk 里都带“本站声明”“广告”“点击咨询”这些噪声检索时噪声文本还会被当成证据拼进答案。清洗策略一般分三步先去 HTML 标签和不可见字符再做行级去噪最后按句号切分过滤含广告词的行。import re def clean_medical_text(raw: str) - str: # 去掉 HTML 标签与空白噪声 text re.sub(r[^], , raw) text re.sub(r\s, , text) # 广告与站内噪声关键词命中则丢弃该句 noise_keywords (免责声明, 点击咨询, 本站未注明, 广告, 来源网络整理, 阅读全文, 发布时间) sentences [s for s in text.split(。) if s.strip()] kept [s for s in sentences if not any(k in s for k in noise_keywords)] return 。.join(kept)这段代码逻辑很简单但有两个参数值得说明。noise_keywords不是一次性写死的我一般从入库结果里随机抽 200 条做人工翻检把高频噪声加进这个列表迭代两轮就干净了。split(。)是按中文句号切句再过滤比直接按行过滤更稳因为很多网页的广告文本和正文会混在同一行。另外清洗完记得做一次全文去重用 MD5 或者标题相似度都行医疗文档在不同站点互相转载的情况非常严重不去重会让检索结果里出现七八条同义词片段。3.2 实体抽取与结构化用正则加词典把“药名-剂量-禁忌”拆出来医疗问答里最要命的是“药品适应症能不能用、禁忌是什么、剂量怎么给”这三类信息。它们有个共同点藏在表格里。我们把药品说明书 PDF 转成文本之后如果直接整段切块表格的行列关系基本全丢。所以入库前要把药品信息单独抽出来建成结构化记录一条记录对应一个药品字段包括通用名、商品名、适应症、用法用量、禁忌、不良反应。实体抽取不需要一上来就上大模型。先拿正则配合词典把高频字段抽出来覆盖面大概能到八成剩下两成的复杂描述再交给大模型做二次校对。这个顺序的好处是快、可控、每一类字段都能定位到是哪个正则或哪条词典规则命中出了问题好排查。下面的代码是一个最简抽取示例目标是抽药品名和剂量import re PATTERN_DOSE re.compile( r(?Pdrug[\u4e00-\u9fa5A-Za-z·]{2,20}?) r(?:片|胶囊|口服液|注射液)? r(?:|,|的)? r(?Pdose\d(?:\.\d)?\s*(?:mg|g|ml|片|粒|袋)) ) def extract_dose(line: str): match PATTERN_DOSE.search(line) if match: return {drug: match.group(drug), dose: match.group(dose), raw: line.strip()[:60]} return None这个正则有三个容易写错的地方中文药名有时候带商品名后缀所以[\u4e00-\u9fa5A-Za-z·]里加了间隔号剂量单位必须写全mg/g/ml/片/粒/袋都见过漏一个单位类型后面的抽取就会断量词前面的“每次”“每日”这类频次信息需要单独再抽一条规则不要塞进这个正则否则匹配粒度会乱。抽出来的结构化记录统一写进 JSONL一行为一条药品记录字段为generic_name、brand_names、dose_info、contra_keywords。禁忌字段我会用关键词分类而不是整句入库比如把“孕妇禁用”“哺乳期妇女慎用”“肝肾功能不全者禁用”分别打标签这样第 5 章要做的“禁忌过滤”才有的放矢。3.3 切块与向量化chunk 大小、重叠窗口、embedding 模型的医学适配切块参数是整个医疗 RAG 里最“玄学”也最影响效果的部分。我踩出来的经验是不要按固定字符数硬切要按文档结构切。先把文档拆成“章节标题 正文块”再在章节内部按段落切。比如一个指南文档里“急性胸痛的危险分层”和“急性胸痛的急诊处理”是两个章节硬按 500 字切块很容易把一个完整流程从中间截断检索时召回半截内容答案自然残缺。我一般用的配置是 chunk 长度 500 到 800 字重叠窗口 80 到 150 字。为什么重叠窗口不能省因为句子边界处经常有承上启下的信息比如“上述患者”“该药物”这类指代词如果没有重叠前半块的结尾和后半块的开头各丢一半语义。下面是一个按句子组块、带重叠的实现思路def chunk_by_sentences(text: str, chunk_size: int 600, overlap: int 100): sentences [s for s in re.split(r(?[。]), text) if s.strip()] chunks, current, current_len [], [], 0 for s in sentences: if current_len len(s) chunk_size and current: chunks.append(.join(current)) # 重叠窗口从已有句中取最后约 overlap 字长的句子保留 overlap_text for keep in reversed(current): if len(overlap_text) len(keep) overlap: break overlap_text keep overlap_text current, current_len [overlap_text], len(overlap_text) current.append(s) current_len len(s) if current: chunks.append(.join(current)) return chunks这里推荐按(?[。])这句正则做句级切分中文医疗文档的句子边界基本就是这三个标点。overlap参数的坑在于如果从尾部截overlap个字符可能从句子中间截断下一块的开头是半句话所以我在代码里做了“按完整句子回退”的处理。embedding 模型选型方面通用中文 embedding 对“心梗”和“心肌梗死”这类同义词通常能兜住但对“拜新同”和“硝苯地平控释片”这种商品名和通用名的关系往往无能为力——这不是模型问题是语料里没见过。解决方式不是换模型而是在检索阶段做别名扩展后面第 4 章会专门讲。3.4 入库与索引元数据过滤字段设计让每条证据都带来源和版本向量入库的时候新手最容易只顾着存文本和向量把元数据丢得一干二净。等调优时想按“只看药品说明书”“只查最新版指南”过滤才发现没有字段可用只能重建索引。入库阶段至少要给每个 chunk 打上这几类元数据doc_id文档唯一标识、source_type指南/说明书/分诊规则/科普、title与section章节路径、version版本日期、raw_path原文文件路径。source_type直接决定了后面能不能做“回答药品问题时只检索说明书”的硬过滤。import sqlite3 conn sqlite3.connect(medical_kb.db) conn.execute( CREATE TABLE IF NOT EXISTS chunks ( chunk_id TEXT PRIMARY KEY, doc_id TEXT NOT NULL, source_type TEXT NOT NULL, title TEXT, section TEXT, version TEXT, content TEXT NOT NULL )) conn.execute(CREATE INDEX IF NOT EXISTS idx_doc ON chunks(doc_id)) conn.execute(CREATE INDEX IF NOT EXISTS idx_version ON chunks(version)) def insert_chunk(chunk_id, doc_id, source_type, title, section, version, content): conn.execute( INSERT OR REPLACE INTO chunks VALUES (?,?,?,?,?,?,?), (chunk_id, doc_id, source_type, title, section, version, content)) conn.commit()这段是标量代码但有个隐藏设计INSERT OR REPLACE加上chunk_id主键是为了支持版本更新。同一份指南出新版时用同样的doc_id但新的chunk_id重新写入旧版本按doc_id批量删除。向量索引那边要跟着做一件事每条向量的 ID 和 SQLite 里的chunk_id一一对应删除旧版本时用doc_id查出所有chunk_id再把这些向量从向量索引里删掉。两边的一致性检查放定时任务每天跑一次否则就会出现第 5 章要讲的“答案引用了过期版本”事故。4. 检索召回与重排命中率比模型智商更值钱4.1 混合召回BM25 与向量检索为什么要双路并行而不是二选一RAG 的瓶颈从来不在生成在召回。医疗问答的查询文本有两种极端形态一种是“发烧两天咳嗽有痰怎么办”这类口语化描述措辞松散但语义明确靠向量检索效果好另一种是“拜新同能不能掰开吃”关键词非常精准任何同义改写都是画蛇添足靠 BM25 精确命中最稳。用户不会按你想的格式提问所以召回层必须双路并行BM25 管精确词向量检索管语义泛化。def hybrid_search(query, top_k20, weight_bm250.4, weight_vec0.6): bm25_hits bm25_index.search(query, top_k) vec_hits vec_index.search(query, top_k) merged {} for doc_id, score in bm25_hits: merged[doc_id] weight_bm25 * score for doc_id, score in vec_hits: merged[doc_id] merged.get(doc_id, 0.0) weight_vec * score ranked sorted(merged.items(), keylambda x: x[1], reverseTrue) return [doc_id for doc_id, _ in ranked[:top_k]]weight_bm25和weight_vec这两个参数不要指望一个固定值吃遍所有问题。我一般先用验证集做一次网格搜索BM25 权重在 0.3 到 0.5 之间、向量权重在 0.5 到 0.7 之间跑 10 组组合看证据命中率。如果知识库里药品说明书占比高BM25 权重可以调大如果用户问题偏口语化向量权重往上提。另外双路召回都要做候选上限控制我习惯每路取 top 40合并去重后交给重排不要每路只取 5 个再拼——召回阶段的主要任务是“别漏”重排阶段才负责“收紧”。4.2 查询改写患者口语问法和医学实体名之间的那座桥“胸口闷”“有点透不过气”“心口堵得慌”这三句话在语义上指向同一个医学场景但和“胸痛”这个标准实体之间的向量距离可能很远。查询改写就是在召回前先架一座桥把口语化表达映射到医学词典里的标准术语再用改写后的词去检索。这个方案比单纯依赖 embedding 更可控,因为映射规则是显式可维护的。ALIAS_MAP { 胸口闷: 胸痛, 胸闷: 胸痛, 心口堵: 胸痛, 发烧: 发热, 拉肚子: 腹泻, 喘不过气: 呼吸困难, 血压高: 高血压, 血糖高: 高血糖, } def rewrite_query(query: str): expanded [query] for phrase, std in ALIAS_MAP.items(): if phrase in query: expanded.append(std) expanded.append(query.replace(phrase, std)) return list(set(expanded))注意这里有个关键参数行为改写后的结果不是替代原始 query而是和原始 query 一起并行检索。因为“胸口闷”可能同时对应“胸痛”和“焦虑相关躯体症状”只替换成“胸痛”反而窄化了意图。三个检索词的结果做合并再统一去重进重排原始语义才不会丢。词典规模方面不用追求大我从线上日志里统计高频口语表达其实就几十条覆盖五成以上改写需求剩下靠向量检索兜底。每条别名都要标来源哪个科室或哪个 FAQ 里收集的方便后面定期补录。4.3 重排与阈值top-k 取值、相似度阈值以及“宁可少给不要错给”粗召回之后直接拼进 prompt是很多 RAG demo 效果差的元凶。双塔向量模型算出的相似度是“粗匹配”它分不清“这条证据讲的是布洛芬的适应症而用户问的是禁忌”。重排阶段用 cross-encoder 对“查询 候选 chunk”逐对打分才能真正分辨这类细粒度差异。常见做法是加载 bge-reranker-base 这一档模型它对计算资源要求不高CPU 也能跑推理。from sentence_transformers import CrossEncoder reranker CrossEncoder(BAAI/bge-reranker-base) def rerank(query, cand_docs, top_k10): pairs [(query, doc) for doc in cand_docs] scores reranker.predict(pairs) scored sorted(zip(cand_docs, scores), keylambda x: x[1], reverseTrue) return [doc for doc, _ in scored[:top_k]]重排模型的输出分数不是余弦相似度那种天生有界值所以阈值要自己在验证集上卡。我给的参考区间是重排分数阈值 0.3 上下——低于这个线的内容基本是噪声。重排后的 top-k 取 8 到 12 比较合适别贪多上下文窗口再大塞进 20 条证据只会稀释关键信息。这里还牵扯到一个检索哲学宁可少给不要错给。答案如果没有可用证据正确行为是回答“无法判断”而不是硬找一条相似内容凑数。所以第 5 章会讲到阈值卡在什么位置直接决定幻觉率的高和低。参数常见取值调整方向双路粗召回 top-k各取 30~50证据命中率偏低时上调重排后 top-k8~12上下文有限时下调重排分数阈值0.3 上下幻觉率偏高时上调BM25/向量权重0.4 / 0.6看知识库类型与问题风格embedding 相似度阈值0.45 上下只在粗召回阶段用别拿来卡最终结果4.4 上下文拼接证据怎么编号、按什么顺序喂给大模型召回做完了最后一道工序是把证据拼进提示词。这一步的细节直接决定答案“看着专不专业”。我的做法是把重排后的每条证据编号为 [1] 到 [N]原样附在提示词里并要求大模型作答时引用这些编号。这既是给模型的约束也是给系统的溯源接口——答案里能解析出引用了哪几条证据。提示词模板大致长这样先是系统指令说明“你是预问诊助手只依据以下证据回答证据没有覆盖的内容回答‘无法判断’并建议咨询医生”然后把证据块按编号列出最后是用户问题。这里有个新手经常犯的错把用户问题也混在证据块里。一旦用户问题出现在证据位置模型可能把它当成上下文的一部分回答时把问题复述一遍当答案。用户问题必须放在独立的用户消息位置不能和证据块混排。证据顺序方面同样重要的一条原则把重排分数最高的证据放最前面并告诉模型“优先参考 [1][2]”。如果用户问的是分诊建议优先引用分诊规则表如果问的是药品禁忌优先引用药品说明书。用source_type做一次软过滤比让模型自己在乱序证据里找答案稳得多。5. 医疗问答系统常见问题排查五个让 RAG 翻车的坑与后悔药5.1 检索命中了药品说明书答案却在说反话否定语义被切块拦腰截断现象用户问“孕妇能不能吃布洛芬”系统回答“可以使用”而说明书原文明确写着“孕妇禁用”。去查检索命中的 chunk发现“适应症用于缓解轻至中度疼痛”和“禁忌孕妇禁用”被切到了两个不同的 chunk而相似度最高的 chunk 恰好是适应症那一段。 原因切块时只看文本长度没有对说明书做结构识别。适应症、禁忌、用法用量这些字段在原文里是平级章节硬按长度切会把“禁忌”这个大杀器切丢。 解决结构化优先。药品说明书不从文本切块开始而是先按 3.2 的方式抽成字段级记录禁忌字段单独存。检索时如果用户 query 里出现“孕妇”“哺乳期”“儿童”等对象词先走禁忌表精确查一次命中即直接返回“禁用/慎用”结论并附带原文不再走向量召回。这块逻辑放在检索前面等于给系统加了一道“禁忌闸门”。5.2 患者说“胸口闷”搜不到“胸痛”同义词与查询改写缺失现象用户问“胸口闷是怎么回事”系统召回的证据全是关于焦虑、胃食管反流的科普对应胸痛的急诊指南一条都没被召回。 原因embedding 模型对口语表达和医学标准术语的对齐能力有限“胸口闷”和“胸痛”在语义空间的相似度并不高。这不是换个 embedding 模型能彻底解决的。 解决走 4.2 的查询改写把“胸口闷”映射到“胸痛”并且改写后的词要和原 query 并行检索。这里有一个经验参数改写召回的结果如果重排分数超过直接召回结果说明改写有效如果连续一周都低于直接召回说明这条别名映射有问题需要复查是不是过度收敛了语义。别名表要允许撤回不是加上就完事的。5.3 剂量和禁忌被切到两个 chunk窗口重叠设了等于没设现象回答“每日三次每次两片饭后服用”但漏了“餐后服用”这个关键信息因为剂量在 chunk A 末尾服用方式在 chunk B 开头。 原因重叠窗口设了但太小只有 50 字左右一个长句就冲垮了重叠区。而且表格类内容被硬切行尾的半句话根本没进入重叠区。 解决重叠窗口按 chunk 长度的五分之一起步600 字的 chunk 配 120 字重叠。同时对表格类原文做保护检测到 Markdown 表格时整表作为一个 chunk不参与长度切分。代码层面的处理是入库前先把表格行合并成一段“表头 分号分隔的行”再进入切块函数这样才不会把表格的行数据拆成碎块。5.4 医院内网部署时 embedding 模型跑不动向量化耗时暴露的选型失误现象知识库 3 万条文档离线入库时发现 bge-large-zh 在 CPU 上跑要十几个小时增量更新一次也要半小时整个迭代节奏被打乱。 原因只考虑了 GPU 显存没考虑 CPU 环境下的吞吐。医院内网常见情况是只有普通服务器没有推理卡。 解决分两层处理。入库阶段用 ONNX Runtime 优化后的 embedding 模型批量 256 条一次推吞吐能比 PyTorch 原版快三到五倍如果还慢就把模型降级到 medium 档检索质量损失可以用重排模型补回来。另外增量更新不要全量重跑只对新增和变更的 doc_id 跑一遍向量化写入时按 3.4 的INSERT OR REPLACE覆盖时间能从小时级降到分钟级。5.5 换了指南版本答案还在引用旧数据索引版本管理与一致性检查现象2024 版指南已经替换入库用户问某个治疗方案的推荐等级答案引用的还是 2019 版的旧证据编号。 原因入库时新版本用的是新的doc_id旧版本没有被标记失效检索结果里新旧版本同时出现重排模型不知道哪个更新按相似度排序时旧版本排在前面。 解决入库时version字段必须参与检索过滤同一doc_id系列只保留最新版本进候选集。然后再加一道一致性巡检脚本每天统计一次“证据引用分布”如果某一天引用的证据里出现两个相邻版本日期直接告警。参考代码就是第 3.4 节里那张表检索前先WHERE version (SELECT MAX(version) WHERE doc_id ?)这一步能在源头防住版本错乱。6. 最后的验收技巧用 100 条带证据的问答对给系统上强度跑通 demo 容易难在证明系统“真的行”。我的习惯是从第一天就建一套迷你评测集规模不用大100 条问题就够了但每条必须带标准答案和黄金证据编号。花两天人工标注这 100 条后面调参能省两周瞎猜的时间。答案的引用编号是需要约定格式的让系统在句子末尾输出[证据编号]评测脚本按正则解析引用了哪些编号。import re def evaluate(qa_pairs, answers): hit, ref_hit, halluc 0, 0, 0 total_ref, n 0, len(qa_pairs) for item, ans in zip(qa_pairs, answers): gold set(item[gold_doc_ids]) cited set(re.findall(r\[(\d)\], ans)) if gold cited: hit 1 ref_hit len(gold cited) total_ref len(cited) if not cited: halluc 1 return { 证据命中率: hit / n, 引用准确率: ref_hit / max(total_ref, 1), 无引用回答率: halluc / n, }三个指标的定位各不同证据命中率衡量检索层好不好引用准确率衡量生成层有没有乱引无引用回答率是幻觉率的反向指标。我自己的血泪经验是肉眼觉得“这答案挺顺”量化一看引用准确率只有 0.6问题全出在重排阈值太低把不相关的证据也放了进来。这类问题不量化根本发现不了。收尾再补一个上强度的小技巧把 100 条里的“无证据但强结论”类答案挑出来人工复查这类是最危险的幻觉。如果复查通过率低于 80%就回头调重排阈值和提示词里的“无法判断”约束。这套流程跑顺了你的系统才算真正从“能答”走到“敢答”。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进