
做知识库问答这类项目最容易被低估的不是模型而是“检索”和“切块”这两件看起来最土的活。我前后折腾过几套基于 LangChain 的自有知识库 GPT 智能体从最开始把一堆 PDF 直接丢进去问出满屏胡话到后来能把答案连出处一起甩出来中间踩的坑基本都集中在文档处理链路上。这篇就把整套东西拆开讲它本质上是一个“检索增强生成”系统用 LangChain 把文档加载、切分、向量化、检索、提示词拼装、工具调用、会话记忆这几段串成一条流水线让大模型在回答时只看你给它的资料而不是靠训练时的记忆瞎编。适合两类人看一类是手上有内部文档、产品手册、历史工单想让它们“能对话”的工程同学另一类是想搞清楚 RAG 到底怎么落地、不想只停留在 demo 层面的技术负责人。下面的参数、代码、排查表都是我实际跑过的配置可以直接抄也可以按你的数据规模改。1. 整体方案设计与技术选型思路1.1 为什么先做检索增强而不是一上来就微调很多人第一反应是“我把文档喂给模型训练一下不就行了”。这个思路在小数据量、格式统一、问题类型固定的场景下确实可行但放到自有知识库上问题会立刻暴露文档每周都在更新微调一次的成本和周期根本跟不上模型被“灌”进去的知识没法溯源答错了你连是哪份文档害的都查不到更要命的是微调容易让模型对原本擅长的通用能力产生退化出现“答什么都像在念手册”的情况。检索增强生成RAG解决的是另一类问题把“记住知识”和“组织语言”这两件事拆开。知识放在外部向量库里随时可增删改模型每次只负责读你递过去的那几段材料然后组织成人话。这么做的好处是知识更新只涉及重新嵌入不用碰模型权重每段回答都能挂上来源文档用户点开就能核对出问题时排查路径非常清晰——要么是没检索到要么是检索到了但提示词没用好两者互不干扰。我一般的判断标准是这样知识更新频率高于每月一次、需要给出处、文档总量在几百到几十万段之间优先 RAG如果是固定领域的语言风格迁移比如让模型学会某种话术那才考虑微调。两者也不冲突实际项目里常见的是 RAG 打底、微调调风格。1.2 系统分层与每层的职责边界整套东西我习惯拆成四层每层出问题时的表现完全不同拆清楚了好定位。层级核心职责典型组件出问题时的表现数据层文档解析、清洗、切块、元数据标注加载器、文本分割器答案残缺、跨段断裂、表格错乱索引层向量化、存储、相似度检索嵌入模型、向量库、检索器答非所问、召回为空、重复内容编排层提示词拼装、工具调用、多轮记忆链、智能体执行器、记忆组件上下文超限、忘记前文、乱调工具应用层接口、鉴权、日志、评测服务框架、监控延迟高、成本失控、无法复盘分层最大的价值在于“可替换”。嵌入模型换了、向量库换了只要接口契约不变上层代码一行不用动。LangChain 在这件事上的作用其实就是提供了一套统一的抽象不管底层是哪种向量库、哪个模型供应商上层都是同一套调用方式。这一点在早期选型阶段特别省事你可以先用本地轻量方案跑通链路再换成生产级组件。1.3 组件选型的几个关键取舍选型这块我踩过最贵的坑是“一步到位”。第一个项目直接上了分布式向量库加集群部署结果数据量只有两万段运维成本比开发成本还高。后来我的原则变成按数据量分档先跑通再扩容。一万段以内本地文件型向量库完全够用持久化到磁盘服务重启直接加载零运维。十万到百万段考虑单机部署的向量数据库支持增量写入和元数据过滤。百万段以上再考虑分布式方案这时候才需要关心分片和副本。嵌入模型的选择更影响最终效果。我的经验是中文场景优先选在中文语料上训练过的嵌入模型别迷信参数量。有些模型维度很高但中文语义对齐一般检索出来的东西看着相似其实不相关。维度也不是越高越好768 维和 1536 维在多数业务场景下差距远小于切块策略带来的差距。前面那句话很重要值得单独说一遍切块策略对召回质量的影响通常大于换一个更强的嵌入模型。2. 知识库构建从原始文档到可检索向量2.1 文档清洗脏数据是万恶之源我见过太多项目在这一步偷懒直接把 PDF 丢给加载器然后抱怨模型胡说。PDF 解析出来经常带着页眉页脚、页码、水印、断行、表格错位这些噪声会被原样切进块里既占 token 又干扰语义。一份 30 页的说明书光页眉页脚就能贡献上千个无用字符。实操上我会做这几件事解析后先做一遍正则清洗去掉重复出现的页码模式、公司落款、页眉把连续换行压成单换行把被 PDF 硬断开的句子接回去判断依据是行尾没有标点且下一行首字母非大写或非数字编号表格单独抽出来转成 Markdown 表格或“字段值”的键值对文本不要让它以空格对齐的方式混在正文里。注意清洗规则一定要写成可配置的、按文档类型分组的规则集不要写成一把梭的全局正则。产品手册和会议纪要的噪声模式完全不同混在一起改一处崩一片。清洗完还要做一轮“有效性过滤”纯目录页、纯版权页、字符数低于阈值的块直接丢弃。这一步能砍掉 5% 到 15% 的无效内容对召回准确率的提升比调参明显。2.2 切块策略与参数计算过程切块是整套系统里最需要手工调的地方。切太大一个块里混了三四个主题检索命中后模型抓不住重点切太小一句话被腰斩语义不完整检索出来也答不上来。我的默认起点是块大小 500 个中文字符重叠 50 个字符。为什么是这两个数算一下就很清楚。假设一份文档正文 10 万字按步长 450500 减 50滑动块数约为 (100000 - 500) / 450 1 ≈ 222 块。检索时取 top 4注入上下文约 2000 字折算成 token 大概 1400 到 1600中文按 1 字 ≈ 0.7 到 0.8 token 估加上系统提示和用户问题整轮请求控制在 2500 token 以内对绝大多数模型都很宽裕。重叠 50 的意义在于防止关键句正好被切在边界上。我做过对比重叠 0 时涉及“条件 结论”跨句表述的问题召回失败率明显偏高重叠调到 80 到 100 后继续下降但块数增加带来的存储和检索开销也开始显现。50 是个性价比不错的平衡点。不同文档类型要区别对待制度、条款类文档按标题层级切先按“章—条”分段再在段内按长度兜底切分保证每条完整。FAQ、工单类一条一问一答天然一块千万别再切。长篇小说式的叙述文档用固定长度滑动切但要在块头补一句所属章节标题作为上下文。LangChain 里的递归字符分割器支持按分隔符优先级递归下切默认顺序是段落、换行、句号、逗号。中文场景我会把分隔符列表改成[\n\n, \n, 。, , , , , ]让它在句号处优先断开避免把一句话从中间劈开。2.3 元数据设计被严重低估的一环绝大多数教程只讲怎么切块不讲元数据。但在真实项目里元数据决定了你能不能做过滤、能不能给出处、能不能做权限隔离。我至少会给每个块打这几类标签来源文件名、章节路径、块序号、更新时间、密级或可见范围、文档类型。前四个用于溯源和展示第五个用于权限过滤第六个用于检索时按类型加权比如问操作步骤时优先召回手册类而非会议纪要。权限过滤这件事必须提前设计。等到上线后才发现某份文档不该被某个角色看到再回头改架构就难受了。做法很简单检索时把用户可见范围作为过滤条件传给向量库只在该子集内做相似度计算而不是先检索再过滤——后者会让 top_k 变小、召回率骤降。2.4 增量更新与重复内容治理知识库不是一次性工程文档会增删改。我的做法是给每份文档算一个内容指纹对清洗后的正文做哈希存进一张映射表。更新流程是新文档进来先算指纹指纹没变就跳过变了就按来源文件名删掉旧的所有块再插入新块。提示删除一定要按“来源 版本”精确删不要按内容相似度删。按相似度删会误伤那些表述接近但实际不同的条款。重复内容也是常见问题。同一份制度在三个目录下各放一份检索时 top 4 里三块内容一样白白浪费上下文预算。我在入库阶段做了近似去重对块向量做两两相似度比较超过阈值 0.97 的只保留一份并在元数据里记下多个来源路径。这样既省空间展示时还能告诉用户“该内容同时出现在 A、B、C 三处”。3. 智能体核心链路实现3.1 检索器配置别只用最朴素的相似度最朴素的相似度检索有个毛病它倾向于召回“和问题长得像”的段落而不是“能回答问题”的段落。问题里出现“怎么申请”召回的全是含“申请”二字但讲的是别的流程的段落。我一般叠加两个手段。第一个是MMR最大边际相关性它在相关性和多样性之间做权衡能把内容重复的块压下去让 top 4 覆盖更多角度。参数上把多样性系数设在 0.5 左右太高会牺牲相关性太低等于没开。第二个是多查询改写。让模型先把用户问题改写成 2 到 3 个不同表述的检索式比如把口语化问题改写成书面术语、补充同义词分别检索后取并集再去重。实测在术语和口语差异大的场景下召回率提升很明显代价是多一两次模型调用和少量延迟。还有一个容易被忽略的点混合检索。纯向量检索对精确匹配不敏感用户问某个编号、某个型号向量检索可能召回一堆语义相近但编号不同的段落。加上关键词检索BM25 之类做融合用倒数排名融合的方式合并两路结果对包含专有名词、编号、代码的查询提升很大。3.2 提示词模板把“不知道”这个出口留出来提示词这块我见过最常见的错误是把模板写得太软“请根据以下资料回答问题”。模型看到这句资料里没有答案时它就会开始自己编。我的模板结构是这样的from langchain_core.prompts import ChatPromptTemplate SYSTEM_TEMPLATE 你是一个严谨的知识库助手。请严格依据下面提供的资料回答问题。 规则 1. 只使用资料中的信息作答不得引入资料之外的知识。 2. 如果资料不足以回答直接回复根据现有资料无法确定并说明缺少什么信息。 3. 回答末尾用 [来源: 文件名#块序号] 标注依据。 4. 涉及步骤时按顺序列出涉及数字时原样引用不要换算。 资料 {context} prompt ChatPromptTemplate.from_messages([ (system, SYSTEM_TEMPLATE), (placeholder, {chat_history}), (human, {input}), ])几个细节值得说。规则 2 是关键它给了模型一个体面的退路不给退路它就一定编。规则 3 让溯源变成回答的一部分用户信任度完全不一样。规则 4 防止模型“贴心地”把“三个工作日”改成“三天”这种改写在对内问答里会引发真实争议。资料拼接时我还会做两件事给每块加上序号和来源标题方便模型引用按相关性从高到低排列因为部分模型对上下文中间位置的信息利用率偏低把最相关的放最前面更保险。3.3 链式编排与工具调用基础问答用链就够了需要“先查再算”“先查再查”的场景才上智能体。区分标准很朴素如果一个问题的答案需要多次检索或调用外部接口才能凑齐就用智能体否则用固定链稳定性和延迟都好得多。固定链的写法现在推荐用表达式语言把检索、拼装、生成串起来from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser def format_docs(docs): return \n\n.join( f[{i1}] 来源: {d.metadata.get(source)}#{d.metadata.get(chunk_id)}\n{d.page_content} for i, d in enumerate(docs) ) rag_chain ( {context: retriever | format_docs, input: RunnablePassthrough()} | prompt | llm | StrOutputParser() )需要工具的场合比如“查一下这个型号的参数再和上一代做个对比”我会给智能体挂两个工具知识库检索工具和计算工具。检索工具的描述一定要写清楚“什么时候该用”模型是靠描述来决定调不调的。描述里写“查询产品参数、政策条款、操作流程时使用”比写“搜索知识库”有效得多。注意智能体的工具数量不要贪多超过 5 个之后选择准确率会明显下降。能合并的工具就合并能固化成链的就别做成工具。3.4 会话记忆窗口不是越大越好多轮对话必须带记忆但记忆不是把历史全塞进去。我的配置是滑动窗口保留最近 6 轮同时对更早的历史做摘要压缩。6 轮这个数不是拍的是算出来的一轮问答平均 150 字6 轮约 1800 字加上检索上下文 2000 字和提示词总 token 控制在 4000 出头延迟和成本都可控。还有一个细节用户追问“那第二种情况呢”这种指代性问题直接拿原句去检索是检不到的。做法是在检索前先用模型把问题改写成独立完整的句子这叫问题压缩或指代消解再拿改写后的句子去检索。这一步不加多轮体验会断崖式下跌。4. 效果调优与常见问题排查4.1 答非所问的四类根因与定位方法线上收到的“答得不对”九成能归到四类定位方法也不一样。第一类是根本没召回。表现是回答里出现“根据现有资料无法确定”但你明知道库里有。排查顺序先看检索日志里的相似度分数如果 top 1 分数低于 0.5不同模型尺度不同看相对值基本就是召回失败再检查是不是切块把关键段切碎了最后看是不是权限过滤把文档挡掉了。第二类是召回了但排得太后。top 10 里有正确段落但 top 4 里没有。这时候调 top_k 治标加混合检索或加多查询改写治本。第三类是召回了但提示词没用上。现象是资料里明明写着一模型答了二。多半是上下文太长、关键信息被淹没或者模板里没强调“只用资料”。第四类是资料本身有矛盾。库里同时存在新旧两版制度检索各召回一半模型只好和稀泥。这种只能靠版本管理和元数据里的生效时间来解决别指望调提示词。4.2 幻觉抑制的三个实操手段幻觉不是模型坏是它被放在了必须回答的位置上。除了前面说的“留退路”我还会做两件事。一是答案后校验。拿到回答后再用一次轻量调用判断“这段话是否被给定资料支持”不支持就打回或降级成“未找到确切依据”。这一步会增加成本所以我只对高风险场景对外客服、合规问答开启内部查询场景用抽样校验就够了。二是相似度阈值兜底。检索结果最高分低于阈值时直接不进模型返回“未检索到相关内容建议换个问法或联系文档负责人”。这比让模型硬答体面得多也省了一次调用。4.3 性能与成本的实际账单延迟主要由三块构成检索、模型生成、网络。检索在十万级向量库上通常是几十毫秒量级占比很小大头是模型生成尤其是输出长回答时。我的优化顺序是先做流式输出改善体感再做检索结果缓存相同问题命中缓存直接返回最后才考虑换更小的模型。成本上算一笔账假设日均 2000 次问答每次输入 2500 token、输出 400 token模型单价按每百万输入 token 若干元计一天的成本是可以精确算出来的。真正容易失控的是两处一是智能体陷入工具调用循环二是上下文没做截断某次检索返回了超大块直接把输入撑到几万 token。前者要设最大迭代次数后者要在拼装上下文时做硬截断。4.4 常见问题速查表现象最可能的原因优先动作回答里出现资料中没有的内容提示词没约束 / 没留退路加“无法确定”规则加相似度阈值明明有答案却说不知道切块过碎 / 召回未命中检查 top 分数调大块或加重叠回答只覆盖问题一半top_k 太小 / 上下文被截断提高 top_k检查截断逻辑多轮追问答非所问未做问题改写加指代消解后再检索相同问题两次答案不同温度参数过高问答场景把温度降到 0 到 0.2表格类问题答错表格解析失败表格单独转结构化文本入库延迟突然变高检索返回超大块 / 工具循环限制单块长度设最大迭代次数新旧制度混答未做版本隔离元数据加生效时间检索时过滤4.5 评测没有评测集的调优都是玄学调参最怕“感觉好像好了一点”。我每个项目都会在开工第二周就搭一个最小评测集从真实提问里抽 50 到 100 条人工标注每条的标准答案出处哪份文档哪一段。跑评测时只看两个指标——召回命中率正确段落是否出现在 top_k 里和答案正确率回答是否与标准答案一致。这两个指标要分开看。召回命中率低说明问题在检索层调切块和检索策略召回命中率高但答案正确率低说明问题在提示词或模型跟检索没关系。分开看能省掉大量无效调参。评测集要随业务演进持续补充。每次线上出现答错的 case就把那条加进去跑一遍确认修复且没有引入回归。这套机制建立起来之后迭代速度会快一个量级。5. 上线前的工程化收尾5.1 服务化与可观测性跑通 demo 和上线之间隔着一段距离这段距离主要花在三件事上接口、日志、限流。接口层把链包成一个服务端点入参是问题和会话标识出参是答案加引用列表。这里要注意超时设置生成环节经常是长尾的把超时设成 60 秒、配合流式返回比设成 10 秒然后大量失败体验好得多。日志我至少记这几项原始问题、改写后的问题、召回的块 id 和分数、最终提示词的 token 数、模型输出、总耗时、用户反馈。有了这些线上任何一个“答得不对”的反馈都能在五分钟内定位到是哪一层出的问题。没有这些日志排查全靠猜。限流和配额放在网关层按用户维度做。知识库问答的成本曲线很陡一个用户写个脚本批量刷一天就能把月度预算烧掉。5.2 我在实际项目里的几点体会第一个体会是先做窄再做宽。第一个版本不要覆盖全公司所有文档挑一个知识密度高、问题相对集中的领域比如某一类产品的售后问答跑通全链路把评测集建起来再往外扩。上来就全量入库问题会多到你不知道从哪下手。第二个体会是用户的提问方式永远超出你的想象。测试阶段我们预设的都是完整句式上线后大量是“那个退货的”“上次说的那个流程”这种碎片表达。所以问题改写那一步不是锦上添花是必需品。第三个体会是把“不知道”当成一个正常功能来设计。早期我总想让系统尽可能多回答结果错答率居高不下用户反而不信任。后来把阈值调紧允许它说“查不到”同时给一个“反馈这个问题”的入口用户满意度反而上升了——因为错的少了而且反馈的问题第二天就进了知识库。最后分享一个排查小技巧当你怀疑是切块问题时把那个块的原文和检索分数打印出来和用户问题并排放一起看。十次里有八次你一眼就能看出这块为什么不该被召回或者为什么该被召回却没召回。这比看任何指标都快。