做知识库问答这套东西,我前后换过三套技术栈,从最早拿小参数模型硬扛,到后来上 Llama 3 重新搭一整套检索增强链路,踩的坑基本都集中在“模型之外”的地方。这次把 Llama 3 接进自有知识库的实践过程完整摊开讲一遍,重点不是复述模型结构,而是把从文档到答案这条流水线上每一段的取舍、参数和坑位说清楚。适合正在评估开源模型落地可行性的后端、算法同学,也适合手上有几百上千份内部文档、想做问答但不知道从哪下手的朋友。第一篇先解决“地基”问题:架构怎么定、机器怎么算、数据怎么洗、检索怎么搭、生成怎么约束。
1. 先把问题想清楚:为什么是“Llama 3 + 知识库”而不是“直接微调”
动手之前我最怕的一种情况是,团队里有人觉得“模型什么都知道”,于是跳过检索直接把文档塞进微调数据里训一遍,然后上线发现改一份制度文档要重训一次模型。这个路径不是不行,是维护成本高到离谱。所以在写第一行代码之前,得先把自己要解决的问题分类清楚。
1.1 三个真实场景拆解出不同需求
我接触过的知识库需求,粗分下来大概三类。第一类是稳定事实查询,比如“年假怎么算”“报销需要哪些材料”,答案长期不变,问法千奇百怪,核心诉求是准,其次才是语言流畅。第二类是跨文档归纳,比如“这三个季度的复盘里,提到过哪些共性问题”,需要模型同时看多份材料再收敛,对上下文长度和指令跟随能力要求高。第三类是带推理的操作指引,比如“这台设备报 E03,按手册下一步该做什么”,需要模型理解流程顺序,还得严格按手册走,不能自由发挥。
这三类需求里,第一类的检索命中率决定一切,第三类的提示词和拒答机制决定一切,反而“模型本身有多聪明”不是最关键的变量。我见过用 7B 级别模型把第一类做到可用的情况,也见过用很大参数模型做第三类结果天天胡说的情况。想明白这一点,后面的选型就不会走偏。这里的判断依据其实很朴素:知识库问答的错误,八成来自“没找到对的材料”,两成来自“找到材料但没说对”。先把八成解决掉。
1.2 检索增强和微调的取舍逻辑
经常被问到“能不能不搞检索,直接微调”。我一般会先反问三个问题:文档多久变一次?答案需不需要给出处?预算够不够反复训练?这三问基本能定方向。
| 维度 | 检索增强方案 | 全量微调方案 | 我的实际选择 |
|---|---|---|---|
| 知识更新 | 换文档、重建索引即可,分钟级到小时级 | 需要重训,天级起步 | 检索增强 |
| 答案可溯源 | 天然能带出原文片段和文档名 | 很难定位到具体出处 | 检索增强 |
| 表达能力(口吻、格式) | 靠提示词约束,稳定性一般 | 训练后非常稳 | 微调更强 |
| 长尾问题 | 检索不到就拒答,边界清晰 | 容易把训练集内容“背”出来乱答 | 检索增强 |
| 成本 | 一次索引 + 每次推理 | 训练 + 推理双份成本 | 检索增强为主 |
结论很清楚:知识类需求用检索增强,风格类需求才考虑微调。我现在的常规做法是检索增强打底,如果发现模型在特定格式上老是跑偏(比如输出表格字段总错位),再补一轮轻量微调去修格式,两件事不冲突。至于为什么不全量微调,还有个更实际的原因:知识库里的内容涉及业务口径,一旦被训进权重,想撤回来只能重训,而检索方案里把那份文档删掉、索引重建,五分钟就干净了。
1.3 这一篇的交付边界
得说明白这一篇写到哪里。整条链路我拆成四段:文档处理、向量化与索引、检索与重排、生成与约束。这一篇覆盖前两段加检索骨架,生成部分只讲提示词结构和拒答,评测和调优留到后面单独说。这样安排的原因是,我实测下来,前面几段做扎实,后面调提示词的收益才有意义;反过来,如果检索召回本身就是脏的,你在提示词里写八百字“不要编造”也没用,模型只能从脏材料里编。
顺带说一句模型选择。Llama 3 系列我用的是 8B 版本做主力开发,70B 版本只在效果对照时跑过。8B 的甜点在于单卡能跑、响应速度能接受、指令跟随在中文场景下经过提示词约束后基本够用。如果你手头的文档以中文长句、大量表格为主,8B 会有点吃力,这时候要么上更大参数,要么在检索环节下更多功夫把上下文喂得更干净。
2. 整体架构设计与选型考量
架构这件事,网上流传的图大多长得很漂亮,但真落地时真正影响体验的是几个很土的问题:文档从哪来、切多碎、存哪里、取几条、怎么拼。这些问题的答案合起来才叫架构。我习惯先把数据流画成一条直线,再考虑每一段要不要加旁路。
2.1 分层架构:从文档到答案的流水线
我最终定下来的是一条五段式流水线,每段都能独立替换和单独测试,这点很重要。因为如果整条链路糊在一起,出了问题你根本不知道是切分错了还是模型笨。
第一段是采集:从文档库、共享盘、导出的 PDF 里把原始文件拉过来,统一转成纯文本,同时保留来源路径、更新时间、版本号这些元数据。第二段是清洗与切分:去页眉页脚、修断行、把超长文档切成语义完整的块,每块配好元数据。第三段是向量化与索引:把每块文本编码成向量写进向量库,同时把原文和关键词索引也存一份,后面混合检索要用。第四段是检索与重排:拿用户问题先去召回一批候选,再用重排模型精排,取前几条拼成上下文。第五段是生成与后处理:按模板拼提示词,调模型,解析输出,补引用,做兜底。
这套分层的价值在于排查路径清晰。答案不对,我先看召回的前五条里有没有正确材料;有,问题在生成;没有,问题在检索;检索候选里也没有,问题在切分和索引。一次定位到具体环节,比盯着整条链路瞎猜高效得多。我建议你在工程里给每一段都留日志,尤其是召回阶段的原始分数和文档 ID,后面调参全靠它。
2.2 参数规模与量化方案的取舍
选 8B 还是 70B,不是拍脑袋。我的判断标准是三条:单次问答可接受的延迟、单机显存上限、任务对推理深度的要求。如果场景以“查条款、给原文、短回答”为主,8B 足够;如果要做多文档归纳、需要模型自己权衡冲突信息,8B 明显力不从心,70B 或者更大参数才能稳住。
量化方面我踩过一个坑:早期为了省显存直接上 4bit 量化,结果发现模型在“严格按给定材料回答”这件事上明显变差,开始自作主张补充材料里没有的内容。后来换成 8bit,拒答率和忠实度都回到可接受区间。我的经验是知识库问答场景里,量化的边际收益不如你想象的大,精度损失却会放大成忠实度问题。如果显存实在紧张,宁可缩短上下文窗口、减少并发,也别把量化压到最低档。
| 方案 | 权重占用(8B) | 权重占用(70B) | 适用判断 |
|---|---|---|---|
| FP16 | 约 16GB | 约 140GB | 效果最好,显存富裕时首选 |
| INT8 | 约 8GB | 约 70GB | 性价比区间,我目前的默认选择 |
| INT4 | 约 4.5GB | 约 40GB | 只建议在演示或极低资源场景使用 |
顺便说一句,70B 在 4bit 下的表现,我实测并不比 8B 在 INT8 下稳定多少,尤其在中文长文档上,量化带来的格式崩坏更明显。所以如果预算只够一张卡,老老实实 8B + INT8,把省下来的精力投到检索质量上,收益更大。
2.3 向量库、编码模型、推理引擎的选型对照
这三样是整条链路里最容易被“别人用什么我用什么”带偏的部分。我实际对比过一轮,结论写在下表里。
| 组件 | 候选 | 我的实际选择 | 理由 |
|---|---|---|---|
| 向量库 | FAISS / Milvus / pgvector / Qdrant | 中小规模用 pgvector,上了百万级切块换 Milvus | pgvector 省一个中间件,元数据过滤直接写 SQL,运维成本最低 |
| 编码模型 | 中文优化型向量模型、多语言向量模型 | 中文为主的库用中文优化版本,中英混排用多语言版本 | 编码模型和语料语言不匹配是召回差的头号原因 |
| 推理引擎 | 通用推理服务框架、轻量本地推理、手写推理脚本 | 服务化部署用推理服务框架,单机验证用轻量本地推理 | 前者吞吐高、支持连续批处理,后者起服务快,适合调参阶段 |
| 重排模型 | 交叉编码器类重排模型 | 单独部署一个小参数重排模型 | 召回 20 条、重排取 5 条,命中率提升明显 |
关于向量库,我想多说一句。很多人一上来就上分布式向量库,结果数据量只有几千条,白白多维护一套集群。几千到几十万条切块,pgvector 完全扛得住,而且它能和业务表做 join,权限过滤、版本过滤写起来很自然,这点在内部知识库里价值极高——你总不能让实习生问到只有管理层能看的制度文档。规模真上去了再迁移,切换成本其实不高,因为向量本身是可以重新灌的。
3. 环境准备与显存估算实操
这部分是我最想写给“卡不够”的朋友看的。很多教程默认你有八张卡,实际场景里往往就一张 24GB 的卡,还得和别的服务挤。所以算清楚显存,比抄一份配置文件重要得多。
3.1 显存占用的完整计算过程
显存主要由三块构成:模型权重、KV 缓存、运行时开销。前两块能算,第三块留余量。
以 8B 模型为例,FP16 下权重大约 16GB,这个是死数。INT8 大约 8GB,INT4 大约 4.5GB。KV 缓存的算法是:每 token 每层的缓存大小等于 2(键和值)× KV 头数 × 头维度 × 每元素字节数。这个模型是 32 层、8 个 KV 头、头维度 128,FP16 下每 token 每层是 2×8×128×2 = 4096 字节,也就是 4KB。乘上 32 层,每 token 约 128KB。如果上下文给到 8192 token,单条序列的 KV 缓存就是 8192 × 128KB ≈ 1GB。
再算并发的账。假设你要扛 8 路并发、每路 4096 token,那 KV 缓存就是 8 × 4096 × 128KB = 4GB。加上 INT8 权重 8GB,再加上推理框架的激活和碎片开销,通常要预留 20% 到 30%,实际占用大概落在 15GB 上下。这意味着一张 24GB 的卡跑 8B + INT8 + 8 路并发是舒服的,跑 16 路就紧张了,得往下压上下文长度或者减并发。
如果你要跑 70B,80 层、同样 8 个 KV 头、头维度 128,每 token 每层还是 4KB,乘 80 层就是每 token 320KB。8192 上下文单条序列约 2.6GB,四路并发就 10GB 往上,加 INT8 权重 70GB,基本就是多卡的事了。
提示:算完显存别急着按满配启动,先把并发数打到估算值的六成跑一轮,看实际显存曲线再往上加。我吃过一次亏,按理论值配了最大并发,结果长文本请求一进来就爆显存,服务直接重启。
3.2 服务拉起与关键参数说明
推理服务的启动参数里,真正影响体验的就那么几个。上下文长度要按你的实际拼装需求定,我一般给 8192,因为检索拼进去三到五条、每条五百字左右,加上系统提示词和对话历史,8192 是够的,给太大反而浪费 KV 缓存。最大生成长度我卡在 1024,知识库问答很少有超过这个长度的合理回答,超过的基本是在胡扯,截断反而帮忙兜底。
温度我设得很低,接近 0,因为要的是稳定复现而不是创意。重复惩罚轻微给一点,防止模型在长句里绕圈。这里有个细节:知识库场景不要开采样多样化,同一个问题两次回答不一样,用户会立刻不信任系统。宁可回答得死板一点,也要保证前后一致。
# 以常见推理服务框架为例的启动参数思路(具体命令按你的框架调整) --model /path/to/model \ --dtype int8 \ --max-model-len 8192 \ --max-num-seqs 8 \ --gpu-memory-utilization 0.85 \ --served-model-name kb-chatgpu-memory-utilization这个值我一般给 0.85,留一点给系统和其他进程。给到 0.95 在压力测试时容易 OOM,尤其是有其他服务共用同一张卡的情况。另外启动后别立刻接流量,先跑一轮固定问题的冒烟测试,确认模型能正常返回、不会截断在奇怪的位置。
3.3 冒烟测试与性能基线
上线前我一定会做三件事。第一件是固定问题回归,准备二十条标准问答,每题跑三次,看答案是否稳定、有没有明显跑偏。第二件是延迟基线,记录不同上下文长度下的首 token 延迟和总耗时,我的目标是首 token 两秒内、整段回答十五秒内,超过这个数用户就开始不耐烦了。第三件是并发压测,从 1 路逐步加到估算上限,观察显存和延迟曲线的拐点,那个拐点就是你的实际承载能力。
顺便记录一下我这边 8B + INT8 的实测情况:单路请求、上下文 2000 token 左右时,首 token 大概 0.6 到 0.9 秒,输出速度每秒三十多字;八路并发下首 token 延迟涨到两秒多,输出速度掉到每秒十几字。这个数据不算漂亮,但对内部工具来说够用。如果是要对外提供服务,就得考虑加卡或者上更激进的批处理配置。
4. 知识库数据治理:决定效果上限的一步
我得说句实话,这套系统最后效果好不好,七成取决于这一步。模型和检索都是工具,材料本身烂,什么工具都救不回来。我见过最典型的失败案例是,把所有 PDF 直接丢掉转成文本、按固定字符数硬切,结果切出来的块全是半句话,检索出来模型也读不懂。
4.1 清洗与切分的具体策略
清洗要解决的是几个很具体的问题。页眉页脚每页重复,会在检索时反复命中同一段无意义内容,必须按行频次统计剔掉。PDF 转文本常见的硬换行,会把一句话拆成三行,得按标点判断合并。表格转文本后列错位,这种情况我一般单独把表格抽出来,转成“字段名:值”的键值对形式再入库,比硬塞进段落里效果好得多。
切分策略上,我最后定的是按语义边界切、控制块大小在 300 到 500 字、块间重叠 80 字。三百字以下容易信息不完整,检索命中一段话说一半;五百字以上会让上下文里塞进太多无关内容,稀释了关键信息。重叠八十字的目的是防止答案刚好跨在两个块的边界上,这个参数我试过五十和一百,八十在中文语料上比较平衡。
| 文档类型 | 切分方式 | 块大小 | 重叠 | 备注 |
|---|---|---|---|---|
| 制度、规范类长文 | 按章节标题切,超长再按段落切 | 400 字 | 80 字 | 标题作为元数据带上 |
| 问答对、FAQ | 一问一答为一个块 | 原样 | 0 | 问题和答案不要拆开 |
| 操作手册 | 按步骤编号切 | 300 字 | 50 字 | 保留步骤序号,别丢 |
| 表格数据 | 转键值对或单行成块 | 一行一块 | 0 | 表头要补进每块 |
| 会议纪要 | 按议题切 | 500 字 | 100 字 | 时间、参与人写入元数据 |
这个表是我反复调过之后稳定下来的。关键在于不同文档类型不能一套规则通吃。FAQ 拆开就废了,制度文档切太碎又丢上下文。所以我的处理流程里第一步是先按文档类型打标签,再分流到不同的切分器。多写几十行代码,效果差一个档次。
4.2 元数据设计:被严重低估的部分
很多人做知识库只存文本和向量,这是个大坑。元数据至少要有这几样:来源文档名、文档版本或更新时间、章节路径、文档类型、可见范围。前三个用于展示引用和判断新旧,第四个用于分流检索策略,第五个用于权限过滤。
其中最容易被忽略的是版本。知识库里最尴尬的情况是,一份制度改了两版,旧版没删,检索把旧版召回上来,模型一本正经地按旧规回答。我在索引结构上加了一条硬规则:同一逻辑文档只保留最新有效版本,历史版本打上失效标记但不参与检索。这样既留了审计痕迹,又不会污染答案。
元数据还有一个进阶用法:章节路径带进上下文。拼接的时候不要只给正文,而是给“文档名 > 第三章 > 第三节:报销流程”这样的路径,模型对结构的理解会明显变好,回答时也更容易说清出处。这个小改动我实测对“跨章节归纳”类问题的提升很明显。
4.3 脏数据的典型形态与处理手法
我整理过一份脏数据清单,基本上每接一个新的文档源都会命中其中几项。
| 脏数据形态 | 具体表现 | 处理手法 |
|---|---|---|
| 扫描件无文字层 | 转出来是空的或者乱码 | 走 OCR,但要人工抽检,OCR 错字比没字更坑 |
| 页眉页脚污染 | 每块都带“内部资料 第 X 页” | 按行频次统计,出现率超过三成的短行直接剔除 |
| 目录页混入 | 检索命中一堆“……第 12 页” | 识别目录特征(大量省略号和页码),整页丢弃 |
| 表格列错位 | 字段和值对不上 | 单列处理,走结构化解析而不是纯文本转换 |
| 重复文档 | 同一份文件多个副本 | 用内容指纹去重,保留更新时间最新的 |
| 编码混乱 | 中文变问号或方块 | 统一转 UTF-8,转换失败的单独列出来人工看 |
注意:清洗环节千万别做“全自动静默处理”。我建议每次清洗都输出一份统计报告,包含处理前后的块数、丢弃的块数、丢弃原因分布。有一次我发现某类文档丢弃率高达四成,追下去才发现是转换器把某个字符当成了分页符,整段被切断。没有这份报告,这个 bug 会一直藏着。
5. 向量化与检索链路落地
数据弄干净之后,编码和检索这一段其实是相对机械的活儿,但参数选择上的讲究不少。这一段我踩的坑主要集中在一件事上:用错了编码模型,后面怎么调都白费。
5.1 编码模型的选择与语言匹配问题
选编码模型第一条原则是语料语言和模型训练语言要对上。我早期图省事用了一个以英文为主的通用向量模型,结果中文检索效果非常差,问“报销流程”召回的全是“流程”相关但语义无关的段落。换成中文优化过的向量模型之后,同一批问题的召回命中率肉眼可见地提上来。
第二条原则是查询和文档要用同一个模型编码。这个听起来像废话,但我真的见过有人文档用 A 模型编码、查询用 B 模型编码,然后抱怨相似度全是乱的。不同模型的向量空间不互通,混用等于随机数。
第三条是维度选择。维度高的模型表达能力强,但存储和计算成本也高。我这边用的是 1024 维,几十万块的数据量在 pgvector 上建索引,查询延迟可以接受。如果数据量上千万,就得认真考虑降维或者换更紧凑的索引类型。
还有一个细节值得说:查询侧要不要加指令前缀。有些向量模型区分“查询”和“文档”两种编码模式,查询侧需要加一段指令前缀才能发挥最佳效果。这点一定要看模型说明,加不加差得很明显。我实测过同一批问题,加了正确前缀之后召回的前三条里出现正确答案的比例提升了两成左右。
5.2 索引构建与混合检索的工程实现
纯向量检索有个天生的短板:对精确匹配不敏感。比如用户问一个具体的编号、一个专有名词,向量检索可能召回一堆语义相近但编号不对的内容。所以我在向量检索之外并行跑一路关键词检索,两路结果融合。
融合方式我用的是倒数排名融合,简单说就是不看原始分数(两路分数不可比),只看各自排名,按排名倒数加权求和。这个方法不需要调分数阈值,工程上很省心。
# 混合检索的核心逻辑示意 def hybrid_search(query, top_k=20, vec_k=20, kw_k=20, k=60): vec_hits = vector_store.search(embed(query), top_k=vec_k) kw_hits = keyword_index.search(query, top_k=kw_k) scores = {} for rank, hit in enumerate(vec_hits): scores[hit.id] = scores.get(hit.id, 0) + 1.0 / (k + rank + 1) for rank, hit in enumerate(kw_hits): scores[hit.id] = scores.get(hit.id, 0) + 1.0 / (k + rank + 1) merged = sorted(scores.items(), key=lambda x: x[1], reverse=True) return [doc_store[doc_id] for doc_id, _ in merged[:top_k]]k取 60 是个常用经验值,作用是压平排名的权重差异,避免某一路的第一名绝对压制另一路。这个参数不用太纠结,我试过 30 和 100,差异不大。真正影响效果的是两路各自召回多少条,我一般各召回 20 条,融合后取 20 条进重排。
关键词那一路的实现,中文得先分词。分词的粒度直接影响匹配效果,我建议用带自定义词典的方案,把业务专有名词加进去,否则“XX 审批单”这种词会被切碎,关键词检索就失效了。
5.3 重排与上下文拼装
召回 20 条之后不能全塞给模型,得重排。重排模型的作用是把“语义相关”和“真正能回答问题”区分开,这两者不是一回事。我用的思路是拿问题加每条候选做一次交叉编码,输出一个相关性分数,按分数取前五条。
为什么取五条?因为上下文预算是有限的。每条五百字,五条就是两千五百字,加上系统提示词和对话历史,总共三四千字,还在合理范围内。取十条的话上下文逼近上限,模型注意力被稀释,反而容易抓错重点。我做过对比,从五条加到十条,答案准确率没有提升,延迟却涨了三成。
拼装顺序也有讲究。我的做法是按重排分数从高到低排,但每条都带上来源标记和文档名,这样模型在回答时能引用。拼装模板大概是这个意思:
[资料1] 来源:《某某管理办法》第三章 正文内容…… [资料2] 来源:《某某操作手册》第 4 节 正文内容…… 请仅依据以上资料回答,资料中没有的信息直接说明无法确认。提示:拼装时务必给每条资料编号,后面做引用解析全靠这个编号。没有编号,模型说“根据资料”,你根本不知道是哪条,排查的时候只能干瞪眼。
6. 提示词结构与拒答机制
到这一步,材料和上下文都准备好了,剩下的就是让模型老老实实按材料说话。这件事比想象中难,因为模型的默认倾向是“尽量给出有帮助的回答”,哪怕材料里没有,它也会顺着常识往下编。
6.1 系统提示词的分层写法
我的系统提示词一般分四层,顺序不能乱。第一层定角色和任务边界,明确说这是内部知识库问答,只回答与知识库内容相关的问题。第二层定证据规则,强调只能依据给定资料,不得引入外部知识。第三层定输出格式,包括回答长度、是否需要分点、引用怎么标。第四层定拒答口径,明确什么情况下必须拒答、拒答时怎么措辞。
你是内部知识库问答助手,服务对象是内部员工。 【证据规则】 1. 只能使用下方【参考资料】中的内容作答。 2. 资料未覆盖的部分,明确回答“现有资料中没有找到相关内容”。 3. 不得依据常识、经验或推测补充任何信息。 4. 资料之间存在冲突时,指出冲突并优先采用标注日期较新的内容。 【输出要求】 1. 回答控制在 300 字以内,条理清楚时用分点。 2. 引用资料时在句末标注编号,如 [资料1]。 3. 不确定的地方必须显式说明不确定,不要模糊带过。 【拒答口径】 - 与知识库无关的问题:说明该问题不在知识库范围内。 - 资料不足以回答:说明缺少哪方面信息。 - 涉及权限外内容:说明无权查询。写这段东西有几个坑。第一,别写“尽量”“最好”这类软约束,模型会当成建议。要写“必须”“不得”。第二,拒答口径要具体到句式,不能只说“要拒答”,否则模型会用各种花式说法拒答,用户体验很割裂。第三,格式要求别写太复杂,超过五条模型就开始漏。
还有一个实操技巧:把参考资料放在系统提示词之后、用户问题之前,用清晰的分隔标记隔开。实测这个位置比放在用户消息内部效果更稳,模型对“这是外部给定材料”的认知更清楚。
6.2 引用回填与答案后处理
模型输出的引用编号是文本形式的,需要后端做一次回填,把编号映射回具体的文档名、章节和原文链接。这一步看起来是纯工程活,但对信任度影响极大。用户看到“根据《某某办法》第三章 [资料2]”并且能点开原文,和看到一段没有出处的回答,感受完全不同。
回填的实现要注意两点。一是编号容错,模型有时会写成“资料 2”带空格,有时写成“【资料2】”,正则要宽松一点。二是无效编号过滤,模型偶尔会引用不存在的编号,这种情况我直接把这处引用去掉,而不是整个回答作废。
后处理还包括长度截断和敏感词过滤。截断按句子边界切,别切在半句话上。至于内容层面的过滤,我的原则是宁可多拦一点,内部工具里出现不当表述的代价远高于少回答一个问题。
6.3 把拒答率当成核心指标来管
大部分团队只盯准确率,我更关注拒答率。拒答率太低说明模型在硬答,风险高;太高说明检索没做好,用户会觉得系统没用。我的经验区间是百分之十到二十之间比较健康,具体看知识库覆盖度。
降低“该答却拒答”的办法,主要是补检索而不是改提示词。我遇到过的典型情况是,问题问得很口语化,比如“出差住宿标准是多少”,但文档里写的是“差旅住宿费用限额”,向量检索没召回。这种时候补一批问法变体,或者把标题也编码进去,效果比反复调提示词好。
处理“不该答却作答”的办法,除了提示词约束,我加了一层相似度门槛:如果重排后的最高分低于某个阈值,直接返回兜底话术,不进模型。这个阈值需要按你自己的数据分布去标定,我一般是拿一批已知无法回答的问题跑一遍,看分数的分布,取一个能挡住大部分的分位点。这层门槛的价值在于,它比提示词更硬,模型没机会编。
7. 常见问题与排查实录
这一段是我最想保存下来的部分,因为下面每一条都是真实踩过的,文档里不会写。
7.1 问题速查表
| 现象 | 最可能的原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 答案明显是旧版本内容 | 旧文档没清、索引重复 | 查召回结果的文档 ID 和版本 | 同逻辑文档只留最新版,历史版不参与检索 |
| 检索结果语义相关但答不上 | 切分太碎,块内信息不完整 | 打印召回块的原文 | 加大块大小,或按章节切 |
| 专有名词搜不到 | 向量模型对该词不敏感 | 试关键词检索能否命中 | 开混合检索,补自定义词典 |
| 回答里出现材料外信息 | 提示词约束太软或量化过度 | 检查提示词措辞与量化档位 | 加硬约束,量化提到 INT8 |
| 同一问题两次答案不同 | 温度过高 | 看采样参数 | 温度降到接近 0 |
| 长文档回答被截断 | 最大生成长度不够 | 看输出是否停在半句 | 提高上限,或在提示词里限制长度 |
| 表格类问题总答错 | 表格转文本时列错位 | 看原始转换结果 | 表格单独走结构化解析 |
| 大量该答的问题被拒答 | 相似度门槛过高或召回缺失 | 看最高重排分 | 下调阈值,补问法变体 |
| 响应越来越慢 | 索引膨胀、上下文变长 | 看单次请求的上下文 token 数 | 控制召回条数,定期重建索引 |
7.2 三个印象最深的排查过程
第一个是**“答得很对但出处全错”**。当时用户反馈引用点开是另一份文档,我一开始以为是回填逻辑写错了,查了半天代码没问题。后来打印召回列表才发现,两份文档有大量重复段落,向量检索把另一份召回上来了,内容几乎一样,所以答案对、出处错。处理方式是做内容指纹去重,同时给重复段落打上“同源”标记,检索时优先返回主文档。这个 bug 特别隐蔽,因为答案是对的,只有细看引用才会发现。
第二个是**“某些问题突然全部拒答”**。排查发现是某天新导入了一批文档,其中一份超长表格被错误切分,产生了上千个几乎一样的碎块,把索引分布带偏了,导致其他问题的重排分数整体被压低,撞上了拒答阈值。这个事的教训是,导入流程必须有异常检测,比如单次导入产生的块数超过历史均值的三倍就告警,人工看一眼再放行。
第三个是**“中文标点导致的匹配失败”**。用户输入用的是全角问号,关键词检索那边分词后没处理,导致精确匹配失效。这种问题在混合检索里特别容易暴露,因为向量那一路还能扛住,关键词那一路直接归零。处理很简单,查询预处理阶段统一标点和空白字符。但如果不做,你会一直觉得“关键词检索没什么用”,其实是用错了。
7.3 几条不太好写进文档的经验
第一条,先做小样本闭环,再谈规模。我建议一开始只挑二十份最有代表性的文档,把整条链路跑通,人工看五十个问题的回答质量。这五十个问题能暴露八成问题。很多人上来就导几千份文档,跑完之后发现问题一大堆,却不知道从哪查起,因为变量太多。
第二条,把每次调参的前后对比存下来。切分大小从 300 改到 500、召回从 5 条改到 8 条,这些改动的影响要用同一批问题去测,不然你记不住哪个改动真的有用。我吃过连续改了五处、效果变差却不知道是哪处的亏,最后只能全部回滚重来。
第三条,别迷信单一指标。准确率高不代表好用,我见过准确率九成但用户抱怨不断的系统,因为剩下那一成错得离谱,而且错得毫无征兆。真实体验里,错得可预期比错得少更重要。所以我在设计上宁可让系统在边界模糊时说“不确定”,也不要它自信地给出一个错误答案。
第四条,上下文里的资料顺序会影响答案。我把最相关的一条放最前面,实测比按文档顺序排效果好,模型对靠前内容的关注度明显更高。这个细节没有理论依据可讲,纯粹是试出来的,但确实有效。
第五条,如果你的知识库里有大量流程类内容,强烈建议在元数据里加上“流程编号”和“前置步骤”这类字段,检索时把相邻步骤一起召回。因为流程类问题最大的坑是只召回中间一步,模型照着答,用户按着做,结果漏了前置条件。这个坑我在一个操作指引场景里踩过,后来加了相邻块扩展召回才解决。
最后分享一个我自己常用的判断方法:新接一个知识库,先不看模型输出,只把召回的前五条打印出来,逐条读。如果这五条里你都拼不出正确答案,那问题百分百在检索侧,先别动提示词。这个习惯帮我省了大量在提示词上瞎折腾的时间。