企业知识库是 RAG 落得最广、也翻车最多的场景。演示里上传几个 PDF 问一句效果惊艳,接入真实文档后就开始一本正经地编。问题几乎不在模型,而在检索和数据处理的「最后一公里」。这篇按从接入到评估的顺序,把决定成败的环节走一遍。
文档清洗
知识库的质量上限,在数据进库之前就定了。真实企业文档普遍脏:扫描件没 OCR、表格被拆散、新旧版本混在一起、PDF 夹着页眉页脚和广告。最值得投入、也最枯燥的一步就是清洗。
先把所有格式统一转成 Markdown 或纯文本,转换时校验表格和标题层级有没有丢。扫描件先跑 OCR 并人工抽查,OCR 错一个字,检索可能就串一行。重复和过期版本要清掉,建立版本管理,否则模型会检索到旧制度、给出过时答案。这一步不做干净,后面每个环节都是白费。
清洗还要顺手把元数据带上——来源文档、章节、日期、作者、文档类型。这些信息不是给模型看的,是给检索用的:有了元数据,就能在检索前先按时间范围或文档类型过滤,比指望向量空间自己把一切分开要快、也更准。
分块策略
最普遍的错误是按固定字数机械切块,把一句话、一个表格、一个步骤从中间斩断。检索命中的块语义不完整,喂给模型自然答非所问。正确的切法有三条:按语义结构切,以标题、段落、列表项为边界;每个块带上所属章节标题做上下文头,避免「孤立片段」;块大小控制在 200 到 500 字左右,太大噪声多,太小信息不全。
表格要单独处理——结构化信息改写成语义完整的自然语言描述,否则向量化后几乎失效。记住一句话:切块是为了检索命中,不是为了存储整齐。
向量化与混合检索
只靠向量相似度,在精确术语、编号、人名上会漏;只靠关键词,又抓不住换一种说法的同义问法。企业文档恰恰两类需求都有。所以正解是混合检索:向量检索管语义相近,BM25 关键词检索管精确匹配,两边各取一部分再融合排序。下面是一个自写的最小混合检索骨架:
def hybrid_search(query, vec_index, bm25_index, top_k=10):
vec_hits = vec_index.search(query, k=top_k) # 语义相近
kw_hits = bm25_index.search(query, k=top_k) # 精确词匹配
fused = {}
for hit in vec_hits + kw_hits:
doc_id, score = hit.doc_id, hit.score
# 归一化后按权重合并,简单起见这里直接累加
fused[doc_id] = fused.get(doc_id, 0.0) + score
ranked = sorted(fused.items(), key=lambda kv: kv[1], reverse=True)
return [doc_id for doc_id, _ in ranked[:top_k]]
召回阶段怕漏,所以先扩大;后面用重排来收紧。
查询改写
用户问「去年的报销政策」,文档里写的却是「差旅费用管理办法」。直接把原句拿去检索,很容易漏。查询改写是在检索前把问句改写成更适合检索的形式——补全指代、拆解复合问题、把口语换成语义相近的书面词。这一层往往被忽略,但对多轮对话里的追问尤其重要,否则第二轮「那明年呢」这种问题根本无从检索。
重排
检索回来的 top-20 里,真正相关的可能只有三条。重排模型用更精细的相关性打分重新排序,把最相关的顶到前面。这一步通常只增加百毫秒延迟,却能明显减少「答非所问」。经验是:召回阶段宁可多召回,重排阶段才讲精准。另外一个便宜的护栏是相似度阈值——检索分数低于阈值时,宁可让模型回「未找到相关信息」,也不要硬凑一个答案。
评估集
没有评估,一切优化都是盲人摸象。落地要做的第一件事不是调参,而是收集 50 到 100 个真实用户问题,人工标注正确答案和应召回的片段。之后每次改动——换切块、换嵌入模型、加重排——都自动跑一遍,看召回率、准确率、忠实度有没有下降。没有这套闭环,改来改去你根本不知道是变好了还是变坏了。
评估还要分三层看,否则出了错也不知道定位在哪一层:检索层看召回率和精确率,生成层看回答的准确性和有据性,业务层看用户满意度和任务完成率。三层指标摆在一起,才能判断问题是出在检索没召回,还是生成在编造。
权限控制
多数原型跳过、生产最头疼的一环。知识库不是人人都能看全部,权限要从源系统继承:一份文档若在受限目录里,它切出来的块也带同样的权限标记,检索时先按权限过滤,而不是生成完再拦。否则一个问答系统就成了内部信息泄露的窗口。
值不值得做
企业知识库真正的门槛不是模型也不是向量库,而是那套没人爱做的脏活:清洗、分块、评估、权限。把时间花在「换更大的模型」上,不如先回答一个问题——检索失败的到底是哪类 query、为什么。把这个问题定位清楚了,知识库才算开始靠谱。