ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于RAG与大模型的医疗问答系统:从知识库构建到检索生成实战

基于RAG与大模型的医疗问答系统:从知识库构建到检索生成实战 简介基于RAG与大模型技术的医疗问答系统是一套完整可运行的Python毕设项目面向计算机相关专业毕业生与需要项目实战的AI学习者评审得分98分源码均经本地编译与严格调试配套文档说明齐全适合作为毕业设计参考或RAG应用实战练习。压缩包共75个文件约84.66MB涵盖Python源码、Jupyter Notebook调试记录、JSON/YAML配置文件、文本数据与界面截图等其中py脚本覆盖WebUI、NER命名实体识别、图谱构建与推理等模块ipynb则展示了微调与解析过程便于按模块理解系统架构。已有137人学习下载。整套资源从实体识别、知识图谱构建到基于ChatGLM大模型的问答生成均有完整实现可帮助学习者快速跑通医疗问答全流程掌握RAG落地方法。1. 医疗问答系统为什么必须走 RAG一个反直觉的小场景你拿一段病历摘要问大模型“这个用药方案有没有风险”它答得条理清楚结果把“高血压”听成“低血压”把“每日一次”扩写成“每日三次”——这正是 RAG 检索增强要治的病医疗问答系统最怕的不是答不上来而是像模像样地答错了你一时还看不出错在哪。把领域知识库先检索出来再让大模型只照着这段证据作答答案错了也能顺着引用片段往回查。这套基于 RAG 与大模型的医疗问答系统用 Python 源码把知识库构建、向量检索、生成输出整条链路跑通适合做高分毕设也适合想快速搭医疗问答原型的工程师。你缺的不是一个更大的基座模型而是“给模型喂什么”的权利。2. 医疗问答 RAG 的系统架构与选型先想清楚三个问题再写代码2.1 为什么医疗场景选 RAG 而不是大模型微调成本与可信度的账本很多人在毕设开题时执着于“把大模型微调一下”觉得不微调就不算深入。但对医疗问答这种垂直场景我的建议是先做 RAG微调只作为后续可选优化。这不只是省算力的问题而是两条路的路径依赖完全不同。微调的本质是“把知识塞进参数里”。参数里的知识一旦出错你没地方改只能重新整理数据、重新训练。医疗知识恰恰是最不能容忍“不可修正错误”的领域药品说明书隔几个月就修订一次诊疗指南以年为单位更新院内用药规则更是随时变。用微调跟这种更新速度较劲等于把维护成本全部压在训练流程上。RAG 的知识在库里不在模型里更新知识时只需要把新的文档切分、嵌入、替换索引里对应的条目其他环节一概不动。可信度是第二个账本。RAG 每次回答都能连带给出“我依据的是哪几段资料”用户可以顺着引用去复核原文档。这在医疗场景里几乎是刚需——医生或患者看到一个答案第一反应是“你凭什么这么说”。而微调模型只会给你一个自信满满的结论没有任何可追溯的原文档。做毕设答辩时评委问你“这个答案的依据是什么”RAG 方案直接展示检索命中的原文片段比解释训练数据分布有说服力得多。第三个问题是成本。微调一个 7B 模型哪怕是 LoRA也要准备几千条高质量“问题-答案对”并做数据清洗还得有一块能跑得动训练的显卡。RAG 的启动成本要低一个量级CPU 就能完成文档嵌入和检索索引构建生成阶段用本地 7B 模型或兼容 OpenAI 接口的模型 API 都能跑。数据量上一套药品说明书加几份诊疗指南就能做出一个看不出破绽的 demo这对毕设节奏非常友好。所以我的结论是RAG 与微调不是互斥关系而是分工关系。RAG 负责“知识获取与可追溯”微调负责“输出风格与指令遵循”。如果你后续确实要微调也应该先有一套 RAG 管线把基线跑出来再考虑对生成模型做指令微调而不是一开始就跳进训练数据的坑。常见做法是先 RAG 后微调前者买知识后者买习惯。2.2 RAG 实战链路的三段式入库、召回、生成把 RAG 拆开看就是一条三段式管线。我一般给刚接触的人打这个比方医疗知识库就是一个巨型书柜RAG 不是把整本书抱给大模型看——上下文窗口装不下装了也会让模型看花了眼——而是先查目录把最相关的几页抽出来再让模型读这几页回答你。三段分别是入库、召回、生成每一段都有独立的坑。入库阶段把 PDF、Word、网页上的医疗文档加载进来做清洗然后按语义边界切分成片段再调用嵌入模型把每个片段转成向量最后写入向量数据库。这一段看起来身份最低实际上是整个系统准确率的天花板。库里的片段如果本身就切得支离破碎、嵌入质量差后面检索器再聪明也白搭。很多文献说 RAG 的瓶颈在检索但我在实际项目里看到的第一个翻车点往往是入库。召回阶段把用户问题也转成向量在库里找最相近的 Top-K 个片段。单用向量检索会有两个典型毛病一是医学专业术语召回不稳比如“呋塞米”这种药名语义向量容易把它跟利尿药的概念混在一起反而丢了精确的字符串匹配二是用户口语和文档书面语之间存在表达鸿沟比如用户说“血压高了”文档里写的是“血压升高”。所以实践中我很少只用单一召回词法检索和语义检索混合是标配做法。生成阶段把召回片段按编号拼进提示词交给大模型让它“只依据资料作答”。这里的关键不是模型能力而是提示词的控制力。如果不明确告诉模型“资料库没提到的内容不许说”它头顶的常识就会抢戏——模型在预训练阶段见过太多医疗文本它会觉得自己知道答案然后开始编。一份严格的系统提示词加上低温参数能把这种“上下文之外的脑补”压下去。这套架构就是标题里“RAG 与大模型”的全部关系大模型负责语言组织RAG 负责让语言组织不跑偏。后面两章我会按我的实际调试顺序把入库和召回这两段最容易踩坑的部分摊开讲。3. 把医疗知识库变成可检索的向量库预处理、切分与嵌入选型3.1 医疗文本的清洗与切分为什么按 512 字符硬切会翻车医疗文档的来源五花八门。我手里的典型素材是药品说明书 PDF、诊疗指南的 Word 版、医学教材的章节扫描件、脱敏后的院内问答记录。这些文档进系统之前必须做清洗。页眉页脚要删参考文献列表要删OCR 产生的乱码要清除PDF 里常见的“适应症/禁忌症/不良反应”多级列表要保留结构。另外要注意剂量单位的多样性μg、ug、mcg 指的是同一个东西但切分和检索时它们是完全不同的 token。我一般会提前做一份单位换算表在清洗阶段把 μg 统一成 mcg把 0.5g 和 500mg 这种等价表达尽量标注成同一种写法。清洗之后最坑的是切分策略。很多人直接拿通用的字符窗口切分器按 512 或 1024 字符硬切结果医疗文档里有大量成对出现的实体被拦腰截断。“阿莫西林胶囊”可能被切成“阿莫西林胶”和“囊”跨过切分边界“成人一次 0.5g”的剂量数字正好落在上一段末尾下一段开头是“每 6 小时一次”检索时模型看到的剂量信息是残缺的。这就是为什么我强烈不建议对医疗文本用固定字符窗口做第一版切分——先按标题、再按段落、最后按句子让语义边界决定切分位置而不是让字符数决定。下面这段代码是我在项目里常用的切分方式基于 LangChain 的递归字符切分器但参数是我反复调过的from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( separators[\n\n, \n, 。, , , ], chunk_size400, # 医疗片段我一般压在 400 字左右太多噪音会稀释注意力 chunk_overlap80, # 重叠 80 字保住跨边界的剂量和实体描述 length_functionlen, ) chunks splitter.split_text(cleaned_doc) print(f切分后块数{len(chunks)}) print(chunks[0][:80])这段代码的逻辑是优先在段落边界\n\n切段落太长再退到句号、分号实在没有标点才按空格甚至字符兜底。chunk_size 设成 400 而不是 512是因为医疗问答通常是单实体提问比如“这个药孕妇能吃吗”模型需要的是“药品名妊娠期用药”这一小段上下文塞进一千字的其他内容反而干扰判断。chunk_overlap 设成 80 是让“上一段末尾的剂量信息”能在下一段的开头再次出现这样检索命中任一片段都能看到完整数字。需要注意这个切分器不知道“标题”的存在如果文档是带章节结构的指南我会换更合适的做法from langchain_text_splitters import MarkdownHeaderTextSplitter markdown_doc convert_docx_to_markdown(raw_text) # 先把 Word 转成 Markdown splitter MarkdownHeaderTextSplitter( headers_to_split_on[(#, 章), (##, 节), (###, 小节)] ) sections splitter.split_text(markdown_doc) # 再对每个小节做一次细切分避免某节内容过长 final_chunks [] for section in sections: final_chunks.extend( RecursiveCharacterTextSplitter( separators[\n, 。, ], chunk_size400, chunk_overlap80 ).split_text(section.page_content) )标题感知切分的价值在于药品说明书里“适应症”和“不良反应”是两个独立章节把它们切成独立的 chunk检索“适应症”时就不会把“不良反应”里的出血风险也带进来。这也是后面问答串味的源头之一。如果知识库里还有影像报告截图这类图片资料先 OCR 成文本再走管线——RAG 知识库不是不能存图片而是通用向量模型直接编码图片的检索效果不如 OCR 转文字稳定别在初期给自己加复杂度。3.2 嵌入模型与向量库选型本地与 API 的取舍嵌入模型选什么直接决定召回质量的上限。医疗中文文本有自己的口味药品通用名、化学式、剂量表达、症状描述。我试过的几个常见选择可以参考这个表格嵌入模型向量维度特点适合场景BAAI/bge-large-zh-v1.51024中文语义理解强对长句和段落稳定医疗指南、药品说明书m3e-base768体积小、速度快通用中文效果好轻量原型、CPU 环境text2vec-large-chinese1024中文语料训练充分短文本检索好问题-答案对类短文本医疗问题里有一个常见考验证用户说“血压高了”文档里写的是“血压升高”理想嵌入应该把这两个表达拉近用户说“eGFR 下降”文档里写的是“肾小球滤过率降低”理想嵌入要把专业缩写和全称拉近。如果换了几种嵌入模型测下来同义表达召回始终差优先怀疑的应该是切分把关键上下文弄丢了而不是模型不行。选好模型后下一步是把切分好的片段批量转成向量并写入索引。FAISS 是单机和毕设场景最省事的选型不需要部署服务端。Chroma 适合要持久化和带元数据过滤的项目Milvus 要等文档量上到百万级再考虑。这里我给出一个完整的建库流程from sentence_transformers import SentenceTransformer import faiss import numpy as np # 本地加载中文嵌入模型首次运行会下载权重之后走缓存 model SentenceTransformer(BAAI/bge-large-zh-v1.5) embeddings model.encode(final_chunks, normalize_embeddingsTrue) vectors np.asarray(embeddings, dtypefloat32) # 用 IDMap 给每条向量标上 chunk_id方便后续回溯到文档和章节 index faiss.IndexIDMap2(faiss.IndexFlatIP(vectors.shape[1])) index.add_with_ids(vectors, np.arange(len(final_chunks))) # 保存到本地文件下次启动直接加载不用重复编码 faiss.write_index(index, medical_kb.faiss) print(f向量库构建完成共 {index.ntotal} 条片段)IDMap2 在这里有两个作用。一是做知识更新时时可以按 ID 删除旧向量、添加新向量而不是整个索引重建——这对医疗文档的定期修订很关键。二是排错时能快速从检索返回的 ID 定位到原始文档路径和章节我看到一条检索结果能立刻知道它出自哪份说明书里的哪一段而不是给一个看不懂的数字索引。normalize_embeddingsTrue 是让向量归一化这样 FAISS 的 Inner Product 距离就等价于余弦相似度排序语义更直白。向量维度是 1024如果是低配的 CPU 机器可以把嵌入模型换成 m3e-base 的 768 维检索速度会快不少代价是长文本语义略弱。这一段做下来你的库里就有了几千条带语义向量的医疗片段。注意到现在还没有一行“大模型”代码——这是医疗 RAG 系统里最反直觉的地方最影响效果的部分恰恰是最不 AI 的部分。库建得越规整后面检索器跑起来越省心。我见过太多人一上来就写大模型调用代码把库随便切了切就扔进向量数据库最后答案离谱还以为是模型不行。4. 检索与生成打通实现问答循环的 Python 代码骨架4.1 召回阶段混合检索的组合写法单靠向量检索医疗问答的召回会漏掉一类很重要的命中精确术语匹配。向量检索靠语义相似度但“呋塞米”这个药名在向量空间里可能被理解成“利尿剂”这个大概念返回一堆关于利尿剂的泛泛片段反而把原文里精确写明适应症的片段排到后面。词法检索BM25对这种术语非常敏感字符串长得像就得分高。所以我一般在召回层做混合让两类检索互相补位。具体到实现BM25 部分在 Python 里可以用 rank_bm25 库完成配合 jieba 对中文分词。医疗文档里很多术语是长复合词比如“急性肾损伤”分词器可能切成“急性/肾/损伤”BM25 的命中会受一点影响。我的做法是维护一份领域词表在分词之前先做一次最长匹配把医学术语合成一个 token。这一步不复杂但能把“阿司匹林不良反应”这种关键词的召回准确率拉起来不少。from rank_bm25 import BM25Okapi import jieba # 加载已有切分好的 chunk并对每个 chunk 做分词 tokenized_corpus [list(jieba.cut(chunk)) for chunk in final_chunks] bm25 BM25Okapi(tokenized_corpus) def keyword_search(query: str, top_k: int 10): tokens list(jieba.cut(query)) scores bm25.get_scores(tokens) return scores.argsort()[::-1][:top_k] # 返回 Top-K 的下标混合检索的融合策略很关键。常见做法是先分别取向量召回和 BM25 召回的得分列表各自归一化到 0-1 区间然后加权相加。我给一个实战中的默认值语义权重 0.6词法权重 0.4。这个比例不是算出来的是用 50 道题手测出来的——术语提问多的场景词法权重提到 0.5语义泛化提问多的场景比如“哪些药不能一起用”语义权重提到 0.7。起手不用调太细先把 0.6/0.4 跑一遍看结果分布再决定方向。召回之后还有一个容易被跳过的重要环节重排序。向量和 BM25 融合出来的 Top-K 是“粗排”质量参差不齐。真正决定生成质量的是“精排”——把 Top-K 里最可能回答问题的片段放在最前面。我一般用 cross-encoder 做这一步把问题和候选片段拼在一起打分比双塔式的向量相似度精细得多。不必每次都跑重排序但如果检索结果里混入“适应症”和“不良反应”两类片段重排序能把更相关的那类提到第一位。def hybrid_search(query: str, top_k: int 8, alpha: float 0.6): q_vec model.encode([query], normalize_embeddingsTrue)[0] vec_scores, vec_ids index.search(q_vec.reshape(1, -1), top_k * 2) bm25_ids keyword_search(query, top_ktop_k * 2) # 简单融合向量和词法分别归一化按权重相加 fusion {} for i, sid in enumerate(vec_ids[0]): fusion[sid] fusion.get(sid, 0) alpha * (1 - i / (top_k * 2)) for i, sid in enumerate(bm25_ids): fusion[sid] fusion.get(sid, 0) (1 - alpha) * (1 - i / (top_k * 2)) sorted_ids sorted(fusion, keyfusion.get, reverseTrue)[:top_k] return [final_chunks[sid] for sid in sorted_ids]这里有个细节向量召回里的 idx 是 FAISS 的 IDBM25 召回里的下标是 final_chunks 的列表下标两者如果不一致就会错位。上面代码默认 final_chunks 和 FAISS 建库时的 add_with_ids 顺序一致实际项目里我建议在入库时把 chunk_id 写进元数据而不是依赖隐式顺序这是后面排查错位最省事的方法。top_k 设置成 8 而不是 5是给生成阶段多一点上下文兜底但不要超过 12——上下文里塞的信息太多模型反而不知道听谁的。4.2 生成阶段让大模型照着证据回答的提示词结构召回做对了生成就省心一大半。但这里有一个新的风险模型看到参考资料里没有答案时它的预训练常识会主动跳出来补救。医疗场景下这种补救就是灾难——你问“这个药孕妇能吃吗”库里只有儿童用药章节模型可能凭记忆给你编一个“不建议孕妇使用”而这个结论没有依据。所以提示词必须结构性压制这种“脑补”。我的提示词分两层。系统提示词里明确限定角色和边界用户提示词里把证据放前面、问题放后面并要求引用片段编号。下面是一个可以直接跑的问答循环模型部分兼容 OpenAI 接口的本地服务from openai import OpenAI # 本地模型服务地址按你部署的方式填 base_url 和 api_key client OpenAI( base_urlhttp://localhost:11434/v1, api_keyollama, # 本地服务一般不强校验 key但接口格式要保持 ) def answer(question: str) - str: ctx \n\n.join(f[{i}] {chunk} for i, chunk in enumerate(hybrid_search(question))) messages [ {role: system, content: ( 你是医疗问答助手。只依据用户提供的参考资料作答 不要使用参考资料之外的知识。如果参考资料中找不到答案 直接回复资料库中未找到相关内容。 回答末尾必须列出你依据的片段编号。 )}, {role: user, content: ( f参考资料\n{ctx}\n\n f问题{question}\n\n 请给出回答并在末尾列出依据编号。 )}, ] resp client.chat.completions.create( modelqwen2.5:7b-instruct, temperature0.1, # 低温减少自由发挥 top_p0.3, # 采样集中到概率最高的 token max_tokens600, ) return resp.choices[0].message.content三个生成侧参数值得说明。temperature0.1 是让模型趋于保守输出更贴近参考资料原文的措辞医疗问答不需要“创造性”哪怕回答显得机械也没关系。top_p0.3 进一步收窄采样范围和 temperature 配合把随机性压到最低。max_tokens600 对医疗问答足够——回答过长反而容易在结尾引入没有根据的补充说明。模型选型上本地用 Ollama 跑 7B 级别的中文指令模型就能出效果3B 以下的模型在长文本组织上容易丢信息表达能力偏弱。如果机器跑不动 7B优先做的不是换小模型而是把检索的 top_k 从 8 降到 5并用重排序把最相关的片段放到最前面减小模型需要阅读的上下文量。这段代码里我最看重的是“回答末尾必须列出依据编号”这一行。它不只是为了让答案好看而是给整个系统留了一个“后悔药”机制当模型答错时你拿着它列出的编号回头看召回的是哪几段资料就能判断错在场是错误的片段还是模型加入了自己的知识。诊断系统问题这一步的信息量远大于单纯换模型、调温度。第 4 章写到这里你已经有一条可用的问答链路了。但一个能跑的系统和一个能交付的系统之间还隔着一批非常具体的坑。下面把这些踩坑记录摊开每一条都是我在调试中反复碰到的。5. 医疗问答系统避坑清单召回空洞、上下文串味与回答编造5.1 检索召回为空模型用常识硬答现象用户问“孕妇能服用布洛芬吗”知识库里明明有“妊娠期用药”相关章节但检索返回的 Top-K 里没有一条真正命中模型无奈之下转头用自己预训练阶段见过的常识回答给出的结论可能对但依据不在你的库里完全不可控。原因有两个。一是切分把“布洛芬”和“妊娠期用药”这两个主题拆到了不同的 chunk 里而向量检索按片段的整体语义匹配“布洛芬”被匹配到了药品简介块“妊娠期”被匹配到了妇女用药通则块两块都没法单独回答问题。二是用户口语句式“孕妇”和文档书面语“妊娠期”存在表达鸿沟向量相似度不够高被别的片段挤出了 Top-K。解决第一在清洗阶段建立领域同义词表把“孕妇/妊娠期/怀孕妇女”标成同一组检索时先做查询改写把用户说法映射到文档说法。第二调整切分策略对章节结构清晰的文档不要让段落独立成块而是把章节标题一起拼进 chunk 内容里比如“妊娠期用药布洛芬属于 FDA 妊娠分级 B 级……”。第三把 top_k 从 5 提到 8给召回多一点容错空间。排查这一步时我会先把 hybrid_search 的返回片段全部打印出来人工检查前三条是不是跟问题完全无关再决定改哪一层。打印不花钱瞎猜才费时间。5.2 上下文中同时出现适应症和不良反应模型答串了现象问“阿司匹林的适应症有哪些”回答里却混进了“出血风险增加”。模型没有答非所问但把“适应症”这个问题的答案和“不良反应”章节的信息揉在了一起。室内格局上检索返回的两类片段都是阿司匹林相关文本模型不知道它们来自不同的章节于是混合回答。原因Top-K 检索按相似度取回了同一药品不同章节的片段提示词里没有告诉模型“这些片段来自独立章节可能回答的不是同一个问题”。模型只能全部参考结果就是串味。解决入库时把 chunk 所在的一级章节名写进元数据并在提示词里用章节前缀标识每个片段比如“[不良反应] 阿司匹林……”。同时给生成阶段加一条指令“如果多个片段包含相互矛盾的信息请以与问题最相关的片段为准并说明忽略其他片段的原因。”这个指令能显著降低模型把适应证和禁忌常混在一起的频率。另外重排序在这里也能帮忙——用 cross-encoder 对 query 和每个片段单独打分把“适应症”问题对应到“适应症”章节的片段提到前面模型优先读到它。5.3 剂量、单位被切分拆散回答出现“0.5g”和“500mg”矛盾现象入库文档里写的是“每天 0.5g 至 1.0g”切分后有的 chunk 只保留了“每天 0.5g”把“至 1.0g”切到了下一块回答时模型根据残缺的信息说“每日剂量不超过 0.5g”和原文剂量上限不符。原因固定窗口切分把数字和单位拆散或者 chunk_overlap 太小没把跨边界的数字连续性保留下来。另一个常见原因是清洗阶段把“0.51.0g”里的波浪线和“”当成噪声符号删掉了数字变成了单个“0.5”后续上下文里没有“1.0g”就丢失了剂量区间信息。解决清洗阶段不要把波浪线、斜杠全部删光剂量区间和药品比例表达里的特殊符号要保留。切分阶段把 chunk_overlap 从 80 加大到 160对数字密集的片段尤其管用。再进一步入库前可以用正则把剂量片段单独提取出来做一份“剂量速查表”索引检索时如果问题里带数字优先查这个索引再结合语义检索。这条表是后悔药级别的补救手段——即使主检索切坏了剂量查询还能命中正确答案。5.4 知识库更新后旧答案还在索引的版本陷阱现象把一份旧版药品说明书从库里删掉重新跑了建库脚本但检索“昂丹司琼用法用量”时旧版说明书的剂量信息仍然出现在答案的引用片段里。用户看到的还是旧数据。原因FAISS 的 write_index 会覆盖磁盘文件但在内存里运行时旧向量的 ID 还占着索引位置。如果你只是重建了包含新文档的 chunks 文件、而没有按 ID 清理旧向量检索时旧文档依然会被匹配到。加上 IndexFlatIP 本身不支持按条件删除这个问题很容易在“快速跑一下”时被忽略。解决入库时一定要用 IndexIDMap2 的 ID 管理并维护一份文档清单每个文档对应一个版本号。重建时先按最新清单把所有待删除 ID 从索引里移除再添加新文档的向量。推荐一个更省事的方案不要原地更新 FAISS 索引每次知识库变更后重建一个新索引文件文件名带上版本号服务启动时加载指定版本。这样即使线上出了错按版本号回退也很容易。血的教训我有一版系统因为没做版本管理线上问答一直回复旧版剂量排查了三天才发现是索引覆盖的顺序问题。5.5 评测只看“答没答对”不看证据链虚高分骗了自己现象用自动化评测脚本跑了一百道题准确率显示 92%人工复核一批发现其中不少回答结论正确但理由里的引用片段和问题不匹配属于“撞对了答案”。用这种评测结果写进报告答辩时一问就露馅。原因评测 prompt 只让另一个大模型判断“最终答案是否正确”没有校验“答案依据的证据是否被召回”。RAG 系统的核心指标是召回的准确性而不是生成结果的表面正确性。生成模型很善于把不相关的资料组织得像模像样这时候你分不清系统是“真会”还是“撞蒙”。解决评测集每一题都标注“正确证据对应的片段 ID 或章节名”。跑批时检查正确引用是否出现在返回的 Top-K 里召回命中率以及模型回答末尾列出的依据编号是否包含正确引用证据运用率。这两个指标比“答案准确率”更能反映 RAG 的真实水平。不要用“大模型评分代替一切”——LLM-as-a-judge 可以做粗筛但医疗场景必须保留人工复核环节至少核对“答案是否超出引用范围”这一项。我在项目里的做法是评测得分为 80 分以上再人工抽 20 条细看重点看引用编号和答案结论是否自洽。6. 验证与迭代技巧先看召回再谈生成用 50 道题找回调参方向验证系统不是写个“跑通了”就结束而是要用一小组评测题确认每一层都在干活。我的做法是建一个 50 题的 mini 评测集不追求全追求覆盖三种典型问题一是带具体术语的比如“呋塞米的适应症”二是口语化表达需要语义理解的比如“血压高了吃什么药好”三是跨章节综合问题比如“高血压患者同时有糖尿病用药要注意什么”。跑通流程就够发现六成以上的问题。迭代顺序是固定的先修召回再修生成不要倒着来。如果 50 题里有 10 题的正确证据没出现在返回 Top-K 里那生成侧改提示词毫无意义——模型根本没看到该看的资料你让它再怎么“只依据资料回答”也没用。所以每跑完一批先统计“Top-5 命中正确证据”的比例低于 80% 就去查切分是否切断实体、嵌入模型是否匹配领域、查询改写是否把口语映射成了术语。这一步做到 90% 以上再开始折腾提示词和温度参数。生成侧的迭代用“幻觉率”这个指标卡模型回答里有多少内容超出了引用片段的范围。把回答和其引用的片段一起丢给评测脚本逐句判断“这句话能不能在上述片段里找到对应陈述”。超出的比例高于 10%优先检查提示词是不是没有强调边界其次才是调温度。永远不要靠“开一个新模型”来掩盖召回问题那相当于换了个嗓门更大的播音员来读同一份错误稿件。一个具体技巧是在做评测集时把每一题的“正确证据片段 ID”做成 JSON随系统一起保存。以后每次修改切分参数、嵌入模型、检索权重都跑一遍这 50 题对比“召回命中率”和“幻觉率”两个数的变化。这样你能明确知道每一次调整到底是变好了还是变坏了而不是靠感觉说“好像比以前聪明了”。这个 mini 回归集还会在写答辩文档时成为最有力的数据支撑。我第一次做这套系统时犯过一个特别典型的错误花了两周调提示词、换模型答案还是时好时坏。后来把所有检索结果打印出来看了才发现库里有一半的片段是乱码和重复段落根本跟问题无关。从那时起我养成了一个习惯任何一次“模型答得怪”第一件事永远是打印它引用的原始片段而不是打开模型配置面板。这个习惯替我挡掉过无数个本末倒置的调试夜。希望帮到你。本文还有配套的精品资源点击获取
返回列表