去年秋天我接了个内部知识库问答的活两千多份文档格式从 PDF 扫描件到 Excel 台账都有。第一版直接接了个大模型跑通链路结果第一批测试就让人头皮发麻回答读起来顺得不行条款编号、比例数字、生效日期全都齐整,拿回原文一核,三条里两条是模型自己脑补出来的。这就是典型的高幻觉——不是模型不会说话是它太会说话了。后来我把底座换到Gemini 3.8 Flash图的不是它榜单多好看而是它的响应速度和上下文容量在批量场景下撑得住剩下的问题就变成了一个纯粹的工程活怎么把它的输出从看着像拧到经得起核。前后调了两周多中间推倒重来过一次最后把文档问答的引用可追溯率拉到了九成五以上答非所问和编造条款的情况基本压住。这篇就讲这个过程包括我踩过的那些文档里根本不会写的坑。适合正在做企业知识库问答、或者已经跑通 demo 但上线前被幻觉卡住的人看纯新手也能看懂代码和参数都可以直接抄。1. 幻觉不是模型坏了是任务边界没划清很多人第一次遇到编造内容第一反应是模型不行换个更贵的。我一开始也这么想白烧了一周预算。后来把错误样本按类型归了一遍才发现八成以上的幻觉根本不是模型知识不够而是我给的指令本身就留了口子。文档问答这个任务的本质不是让模型回答问题而是让模型在给定材料里找答案并复述。这两件事在模型看来差别很大前者允许它调用参数里的记忆后者不允许。你不明确说它就默认两者可以混着用。1.1 文档问答里幻觉的五张面孔把错误样本摊开看幻觉不是一种东西而是五种处理手段完全不同。分不清楚类型你调参就是瞎调。幻觉类型典型表现根因主要对策实体编造条款编号、人名、日期凭空出现模型按格式套模板填空要求逐条引用来源片段跨文档串味把 A 制度的要求安到 B 制度上检索片段混装、无编号片段编号 强制标注来源过度概括部分情况下被说成一律指令未强调限定词提示词加保留原文限定语数值误算比例、合计、差额算错让模型做算术数值交给代码算该拒未拒材料里没有却硬答缺少拒答出口写死拒答话术第一类和第二类最要命因为它们看起来最专业。我见过一条回答把两份不同年份的制度条款拼在一起逻辑自洽、格式规范只有把两份原文并排打开才能看出问题。这种错误如果混进正式流程后果比答不出来严重得多。所以我后来的原则很简单宁可拒答不可错答而且这个原则必须写进系统提示不能只是我心里想想。1.2 Flash 档模型的取舍速度换来了什么代价选 Flash 系列是有明确理由的。我们那个场景是异步批处理加在线问答混合高峰期一天几万次调用,响应时间超过三秒用户就开始骂。旗舰模型效果好但延迟和成本摆在那儿全量走旗舰不现实。Flash 档的定位就是够快够便宜代价主要体现在三个地方这三点你得提前知道不然会怀疑人生。第一是推理链偏短。遇到需要跨三段材料拼接才能得出答案的问题Flash 容易在中间跳过一步直接给出结论。它不是在骗你是它真的没走那一步。第二是对否定式指令的敏感度弱一些。你写不要使用文档之外的信息它大概率会遵守但你写除以下三种情况外不得引用外部知识这种带例外的长否定句它漏读的概率明显上升。第三是长上下文的中段注意力衰减。塞进去两万字头尾记得清楚中间那部分容易被忽略。这三点决定了后面的调优方向指令要短、要正着说、片段要少而准。1.3 先花半天给幻觉验伤别急着改提示词这一步很多人跳过直接开始改 prompt改完发现效果时好时坏因为根本没有基线。我的做法是从真实文档里抽 20 到 30 个问题覆盖五种幻觉类型人工写出标准答案和对应的证据位置。然后跑一遍原始链路把每条输出的错误类型打上标签。半天时间你就能得到一张错误分布表。我那次的结果是这样的20 条里 9 条有实体编造4 条跨文档串味3 条过度概括2 条数值错2 条该拒未拒有重叠。这个分布直接告诉我优先级应该放在引用约束上而不是我原本以为的加强检索。如果检索本身没问题加强检索就是白费劲。先定位再动手这个顺序反过来你会浪费大量时间在错误的方向上。2. 把回答改成取证提示词结构的第一轮重写第一轮重写我推翻了原来的提示词彻底换了思路。原来的提示词是你是一个专业助手请根据以下材料回答问题这是典型的聊天式写法。改成取证式之后模型的角色定位从答题者变成书记员——它的工作不是给出漂亮答案而是先找到原文再照着原文说。这个转变听起来只是措辞差异实际效果差得很远。2.1 系统提示的三段式骨架我把系统提示切成三段每段只干一件事互不掺杂。这样模型解析起来负担小也不容易漏读。【第一段任务与边界】 你是文档核对助手。你的唯一信息来源是用户消息中标注了编号的文档片段。 禁止使用片段之外的任何知识包括常识、行业惯例和你的训练记忆。 【第二段证据规则】 1. 每个结论必须紧跟引用标记格式为 [S3]可并列多个如 [S1][S4]。 2. 片段中没有直接依据时回答文档中未提及不得推测。 3. 保留原文的限定语如原则上一般情况下经批准后不得省略。 4. 涉及数字合计或比例换算时先列出参与计算的原始数字再给出结果。 【第三段输出格式】 严格按 JSON 输出字段为 answer、evidence、confidence、missing。 evidence 为来源编号数组confidence 取 high/medium/low missing 用于说明文档中缺失的关键信息无事则填空数组。三段之间的顺序有讲究。边界必须在最前因为模型对提示词开头和结尾的关注度最高中段最容易滑过去。证据规则放中间输出格式放最后是因为格式是最好验证的——如果它连 JSON 都吐不出来说明前面两段根本没读进去。2.2 让模型先出证据再出结论这是我认为收益最大的一处改动也是最反直觉的。常规做法是先给答案再附引用我改成了强制模型先列证据、再写结论。虽然最终输出顺序还是答案在前但在提示词里我要求它内部先完成取证动作。具体做法是在 JSON 结构里把 evidence 放在 answer 后面但在提示词描述中明确写先确定 evidence再由 evidence 生成 answer两者必须一一对应。实测下来实体编造的比例明显下降。原因不难理解如果先写答案模型是在一个已经成型的句子上补引用很容易补一个看起来对的编号如果先锁定编号答案就被这些编号框住了想编也编不出来。我拿一组 30 条样本做过对比同样的模型、同样的检索结果只改这一个动作顺序编造条款编号的案例从 11 条降到 2 条。剩下那 2 条是引用了正确片段但过度解读了内容属于另一类问题后面单独处理。2.3 拒答话术必须写死否则模型会自己发明台阶这条是血泪教训。早期我只写了不知道就说不知道结果模型确实拒答了但它拒答的方式五花八门有的说根据我的理解可能……有的说该问题超出文档范围但一般来说……最离谱的一条是拒答之后又补了一段参考建议。这些补丁式的输出比直接编造更难防因为前半句是对的审核的人看到未提及就放过去了后半句的错误就漏网了。后来我把拒答路径完全写死答案字段只能是固定的一句话文档中未提及不多一个字。同时在 missing 字段里列出缺什么信息才能回答。这个字段很有用它把拒答从失败变成了有信息量的失败——业务方看到缺什么能反过来去补文档。{ answer: 文档中未提及, evidence: [], confidence: low, missing: [缺少2024年之后的修订版本, 缺少适用主体的界定] }另外加了一条兜底规则如果 answer 不是固定拒答句但 evidence 为空直接判定为异常输出走人工复核队列。这条规则拦下来的量不大但拦下来的每一条都是真问题。3. 上下文不是塞得越多越好检索片段的筛选与排版提示词改完之后幻觉降了一截但没到底。剩下的问题基本都出在上下文这一侧。这里有个很常见的误区觉得上下文窗口大就往里猛塞塞得越全越保险。实际恰恰相反塞进去的每一段无关内容都在稀释模型的注意力尤其是 Flash 档中段衰减更明显。3.1 分块策略按语义段落切别按固定字数切我最早用的是固定 512 字符切块重叠 50。这个做法简单但切出来的片段经常半句话断在中间模型读到第 3.2 条规定的比例为就没了于是自己补一个数字。这就是典型的由切块方式引入的幻觉。改成按语义切之后情况好转很多。我的做法是先按标题层级做一次结构切分条文类文档按章-条-款切说明类文档按自然段切遇到表格单独成块并保留表头。单块长度控制在 200 到 600 字之间超过 800 字的强制二次切分但切分点只选在段落边界。切块方式平均块长语义完整率检索命中率幻觉率固定 512 字符512约 62%高偏高按段落切约 400约 88%中高中按标题层级切约 350约 94%中低语义完整率是我自己定义的一个粗略指标随机抽 100 块人工判断块内是否包含完整语义单元。这个数不需要很精确横向对比够用就行。可以看到命中率确实降了一点因为块变小了召回稍微吃亏但幻觉率的下降幅度更大这个交换是划算的。3.2 片段编号与引用锚点让每一句话都能回溯编号这件事看着琐碎实际是整套方案的地基。我给每个检索片段加上[S1][S2]这样的编号并且在片段头部固定附上来源标识文件名加章节路径。编号顺序按相关度排序最相关的放最前面因为开头位置的注意力权重最高。[S1] 来源员工差旅管理办法 / 第三章 / 第12条 住宿费标准按城市分级执行一线城市每晚上限600元二线城市每晚上限450元 其他城市每晚上限350元。超标部分由个人承担。 [S2] 来源员工差旅管理办法 / 第三章 / 第13条 因公务需要延长住宿天数的须在出差申请中事先注明并经直属负责人批准 未事先注明的超出天数按普通事假处理。这个格式有两个好处。第一模型引用时有了明确的抓手它不需要描述根据那份讲差旅的文件直接标[S2]就行。第二出错时能快速定位我做过统计引用错误里超过六成是编号对不上内容一看编号就知道是检索排序错了还是模型看串了行。这比事后逐句核对原文效率高得多。3.3 噪声片段的识别与剔除检索总会带回来一些相关度不高的片段。这些片段是幻觉的温床因为模型看到有材料就会倾向于用上它。我加了两道过滤。第一道是相似度阈值低于阈值的直接丢宁可少给。这个阈值需要在自己的语料上标定我的经验是从 0.3 开始往上试直到该拒未拒的比例开始上升为止。第二道是去重同一份文档的相邻块如果内容重叠超过一半只保留信息更完整的那块。这条听起来无所谓实际影响不小——重复内容会让模型误以为某个信息被多次强调从而抬高它在答案里的权重。还有一类特殊噪声废止文档。我们的知识库里存着历史版本检索时很容易把旧版召回。解决方法是在片段头部加状态标记[S1][已废止]并在提示词里明确标记为已废止的片段不得作为答案依据仅可用于对比说明。不加这个标记模型会把旧版和新版并排引用给出一个自相矛盾的答案。4. 参数与输出结构的组合调优到这一步提示词和上下文都理顺了剩下的就是参数。参数这块我不太相信网上的通用建议因为不同任务差别太大。我把自己实测下来的一套值放出来但你要在自己的数据上再验一遍。4.1 温度、top_p 与输出长度在问答场景的取值文档问答这个任务创造性是负资产。所以我倾向把随机性压到很低。参数试探区间我最终取值说明temperature0.0 - 0.50.150 太死板遇到需要轻微改写的表述会卡壳top_p0.8 - 1.00.9与温度配合别两个同时压到极低max_output_tokens512 - 20481024留足空间给 JSON 结构和多引用停止序列视格式而定/json防止模型答完之后自说自话温度和 top_p 不要同时往死里压。我试过 temperature0 配 top_p0.7结果模型在需要把口语化问题转成书面表述时表现得很僵反复输出同一句开头。0.15 配 0.9 是我试出来比较平衡的一组输出稳定但不至于失去基本的语言灵活性。停止序列这一项容易被忽略。不加停止序列的时候模型答完 JSON 有时会再补一段希望以上信息对您有帮助之类的废话直接把结构化解析搞挂。加一个显式的结束标记能省掉大量解析异常。4.2 用结构化输出锁死字段让校验层拦住大部分问题光靠提示词要求 JSON 是不够的模型会漏字段、会加字段、会把数组写成字符串。我的做法是双保险请求侧开结构化输出响应侧再跑一遍校验。import json SCHEMA_KEYS {answer, evidence, confidence, missing} def validate(raw: str) - dict: try: obj json.loads(raw) except json.JSONDecodeError: return {ok: False, reason: not_json} if set(obj.keys()) ! SCHEMA_KEYS: return {ok: False, reason: field_mismatch} if not isinstance(obj[evidence], list): return {ok: False, reason: evidence_not_list} # 拒答一致性非拒答且无证据判为异常 if obj[answer] ! 文档中未提及 and not obj[evidence]: return {ok: False, reason: answer_without_evidence} # 引用编号必须在实际下发的片段范围内 valid_ids {fS{i} for i in range(1, len(PASSED_CHUNKS) 1)} if not set(obj[evidence]).issubset(valid_ids): return {ok: False, reason: invalid_citation} return {ok: True, data: obj}这个校验层拦下来的问题比我想象的多主要集中在前三条。有个细节值得说invalid_citation这一项早期我以为是模型编编号后来查日志发现大部分是我的片段编号没和实际下发的内容对齐——排序变了但编号没跟着变。这种属于工程 bug但表现和模型幻觉一模一样如果不做校验很容易把它算到模型头上然后去调一堆没用的参数。4.3 长文档的二次核对流程单个片段能覆盖的问题第一轮就解决了。麻烦的是需要跨多个片段综合的问题。这类问题的错误率明显更高因为模型要在几个片段之间做连接。我的做法是加一轮自检调用把第一轮的 answer 和 evidence 原样回传问它每一句结论是否都能在 evidence 对应片段中找到直接依据逐句回答是或否并把找不到依据的句子列出来。这一轮用更低的温度和更短的输出限制成本增加大约三成但跨文档串味的比例降了将近一半。注意自检这一轮不要把原始问题再传一遍。传了之后模型会重新回答一遍问题而不是做核对等于白花钱。自检发现的无依据句子我的处理不是直接删掉而是降级把这条回答的 confidence 从 high 打到 medium并在返回给业务方的接口里加上标记。因为有些句子确实是有依据的只是表述做了合并机械删除会损失信息。标记出来让人来判断比机器硬删更稳妥。5. 从能跑到稳定一套可复现的评测与回归调优最怕的是改完感觉好多了。感觉是会骗人的尤其你刚花了两个小时改提示词心理上一定倾向于觉得有进步。我前后改过七八版提示词其中至少两版是实打实的退步只是当时没测出来上线之后才发现某类问题变多了。5.1 五十条就够用的自建评测集不需要搞几百上千条五十条精心标注的样本足够支撑日常迭代。关键是分布要贴近真实。我的构成是这样的事实查找 20 条跨段综合 10 条数值计算 8 条边界与例外 7 条应当拒答 5 条。这五类基本覆盖了实际流量里的主要问题形态。每条样本标注四样东西问题、标准答案或应拒答、关键证据所在片段、以及判定要点。判定要点这栏最重要它写明这条答案里必须出现哪几个要素才算对避免评审时各人标准不一。指标定义我的目标线引用可追溯率evidence 能对应到原文的比例≥ 95%答案正确率判定要点全部命中的比例≥ 85%应拒答命中率5 条拒答题中正确拒答的比例100%过度概括率丢失限定语的回答占比≤ 5%应拒答那条我卡了 100%因为漏一条就是一个潜在的错答。实际做到 100% 花了不少功夫主要是把拒答的判定条件写得更细。5.2 线上埋点与每周抽检离线评测过了不代表线上稳。线上必须埋三类点检索阶段的召回片段数和最高相似度、生成阶段的 evidence 长度和 confidence 分布、以及校验层的失败原因分布。这三个数据放在一起看能快速分辨问题出在检索还是生成。举个例子。如果某天answer_without_evidence突然涨了同时最高相似度在下降那大概率是文档更新导致检索失效不是模型的问题。反过来如果相似度正常但invalid_citation涨了就要去看是不是排序逻辑改了导致编号错位。每周固定抽 20 条线上真实请求做人工核对不要只看指标。指标是滞后的尤其是看起来对但其实错了这类指标完全反映不出来。我抽检时最有价值的一次发现是有个业务线的文档里混进了一份格式相似的会议纪要模型把纪要里的讨论意见当成了正式规定。这种问题任何指标都测不出来。5.3 效果反复退化时的排查顺序模型表现突然变差别急着改提示词。按下面这个顺序查八成问题在前三步就能定位。文档侧有没有新文档入库、旧文档更新、格式变化比如从文本 PDF 变成扫描件。检索侧分块有没有变化、索引有没有重建、相似度分布是否异常。编号与拼装侧片段编号是否与实际下发内容一致、排序是否稳定。提示词侧有没有人改过、版本是否一致、有没有被截断。模型侧是否发生了版本更新、参数是否被改动。第 5 步放最后是因为它最少见。我遇到过的十几次退化里只有一次真是模型侧变化其余全是前四步的问题。但人的直觉总是先怀疑模型这个顺序能帮你省很多冤枉路。6. 几个文档里不会写的实操细节剩下这些点比较零碎但每一条都是我真金白银踩出来的写在这儿省得你重走一遍。6.1 表格和数字类问题单独走一条路只要问题里涉及合计、比例、差额就别让模型算。Flash 档的算术能力在简单场景够用但一旦跨行跨表出错概率陡增。而且它算错的时候输出格式依然漂亮非常具有欺骗性。我的处理是把数值计算拆出来模型只负责从片段里抽取参与计算的原始数字抽完之后交给代码算算完再把结果拼回答案。提示词里明确要求先列出参与计算的原始数字及其来源编号不要自行计算。这一条加上之后数值类错误基本清零。注意抽取数字这一步本身也会出错尤其是中文里三成半数不低于这类表述。我的做法是在提示词里附一张换算对照表把常见口语化数值表述和标准写法列出来让模型按表转换。这张表只有十几行但效果出奇地好。表格类问题还有个坑跨页表格。PDF 里一张表被切成两页提取出来是两个独立的表模型看到第二页没有表头就把列的含义搞错了。解决办法是在入库阶段做表格合并或者在片段头部补一句本表续表列含义同上。这个补丁很土但管用。6.2 中英混排与单位换算的隐形坑我们文档里大量出现英文缩写和中文混排比如按 SLA 的 P95 响应时间计算。这类表述模型一般能懂但引用时容易只引一半把关键限定丢掉。另外是单位。同一份文档里万元和元混用吨和千克混用模型在综合两段材料时经常不换算就直接相加。我在提示词里加了一条硬规则涉及单位不一致的先统一单位再计算并在答案中注明换算过程。这条规则让输出变长了一点但可核对性大幅提升。还有个细节是中英文标点。模型输出 JSON 时偶尔会用中文引号包裹字符串导致解析失败。校验层里加一条标点规范化处理把中文引号、全角逗号转成半角能省掉不少莫名其妙的解析异常。6.3 成本、延迟与效果之间的那条线最后说点现实的。整套方案跑下来单次问答的调用次数从 1 次变成了 1 到 2 次自检那轮不是每次都触发token 消耗大概涨了六成。这个成本能不能接受取决于你的场景。我的做法是分级confidence 为 high 且 evidence 数量充足的问题跳过自检只有跨段综合类、或者 evidence 数量超过 3 个的才触发第二轮。这样整体触发率控制在三成左右成本增加降到两成出头效果损失很小。延迟方面Flash 档的优势在这儿体现得很明显。加了自检之后P95 延迟还是在可接受范围内如果换成更重的模型这个方案根本跑不起来。所以选型这件事不是选最好的是选那个能让你把工程手段全用上的。调这套东西花的时间比预想的多但把幻觉从每次都要人工核压到抽检就行这个转变对整个流程的意义远大于省下的那点调用成本。如果让我重来一次我会在第一天就把那 50 条评测集建起来而不是等到改了三版提示词之后才想起来要有个基线。