LangChain 构建 GPT 智能体:检索增强知识库问答实战

发布时间:2026/10/26 19:49:53
LangChain 构建 GPT 智能体:检索增强知识库问答实战 做这套东西的动机来自一件很小的事评审会上有人问某个接口字段在半年前为什么改过一次会议室里六个人翻了七分钟文档最后结论是“好像在哪份需求里提过”。这七分钟就是全部理由。用 LangChain 把自有知识库接成一个 GPT 智能体本质上就是让模型在开口之前先去你自己的资料里翻一遍拿着找到的原文回答并标出出处。它解决的问题不偏向“模型不聪明”更在于“模型不知道你们内部发生过什么”。适合三类人看手上有一堆散落文档想做成问答入口的产品或运营被派来做内部知识助手、又不想从零写检索逻辑的开发以及已经跑通 demo、但效果始终不稳定的同行。下面这套东西是我实际跑起来的版本包含架构取舍、参数怎么定、代码骨架以及上线后真正让我加班的那些坑。1. 为什么不选微调而是搭一套检索智能体1.1 三个现实约束把方案逼到了检索增强先说我最初为什么考虑过微调又为什么很快放弃。第一个约束是知识更新频率。内部文档每周都在改需求文档改字段、规范改流程、FAQ 加条目微调意味着每次改动都要重新构造数据集、重新训练、重新验证这条链路在我这里根本转不动人力成本比收益高一个数量级。检索增强的逻辑完全不同文档改了删掉旧 chunk、灌入新 chunk十分钟搞定模型本身完全不用动。第二个约束是数据量级。几百份文档、几十万字的规模放在微调场景里属于“太少”。这个量级喂进去模型记不住具体事实反而容易把原有的指令遵循能力带偏出现答什么都往那几份文档上靠的情况。检索增强不要求模型记住任何东西它只要求模型会读——而读恰恰是现在的 GPT 系列模型最擅长的事。第三个约束是可解释性。内部问答场景里用户要的往往是“这句话出自哪份文档第几节”一旦答错得能追溯到原文去判断是资料错了还是检索错了。微调后的模型给不出这种溯源。检索增强天生带引用因为答案的原料就是从某个 chunk 里取出来的把它的元数据一并带出来就行。这三条加起来方案基本没有第二种选择。1.2 LangChain 在这条链路里到底承担了什么很多人对 LangChain 有误解觉得它是“让模型变聪明”的框架。它真正解决的问题是接口统一。文档加载、文本切分、向量化、向量库读写、检索、Prompt 组装、模型调用这七八个环节每一个都有多种实现LangChain 把它们抽象成统一的类型和协议让你换向量库、换模型、换检索策略的时候只改一两行初始化而不是重写整条链路。这一点在项目初期反复试错阶段特别值钱我前后换过三种切分策略、两个向量库改动量都在几十行以内。第二个值是 LCEL 那套管道写法。用|把各个环节串成一条 Runnable流式输出、异步调用、批量处理、失败重试这些都是框架层现成的你不用自己写线程池和回调。对于一个需要边生成边往前端推字的前端体验来说这个能力省了我至少两天。但它不解决的事情同样要讲清楚文档解析的质量、切分的粒度、召回的准确率、评估集的构建——这四件事没有任何框架能替你做。我见过不少人搭完 demo 之后觉得“效果就那样”问题几乎都出在这四件事上而不是框架选错了。LangChain 是把管道铺好的水泥水管里流什么水、水干不干净还得自己管。1.3 整条链路拆开看把这套东西摊平一共九个环节。我做了一张表把我实际的选择和理由写清楚后面几章都是围绕这张表展开的。环节职责常见选项我的选择与理由加载把各种格式转成纯文本各类解析库、OCRMarkdown/文本优先PDF 用文本层扫描件单独走 OCR清洗去噪、合并断行正则规则手写规则针对自家文档格式定制切分切成可检索单元定长、按结构、语义按结构切 定长兜底500 字上下向量化文本转向量多语言、中文优化、不同维度中文优化模型1024 维存储向量 元数据嵌入式库、服务端库十万级 chunk 用嵌入式库足够检索召回候选向量、关键词、混合向量 关键词混合召回 20重排精排前 N交叉编码器加一层重排取前 5组装拼上下文和指令模板强制引用编号 拒答策略生成出答案GPT 系列模型低温度控制输出长度链路本身不复杂复杂的是每个环节的参数怎么定。接下来按顺序往下拆。2. 知识库预处理效果上限在这一步就定死了2.1 文档加载与清洗先把脏数据挡在门外我踩过最深的坑全部在解析环节跟模型一点关系都没有。第一版我图省事把 PDF 直接丢给解析器出来一堆重复的页眉页脚每页顶部都是文档标题加版本号底部是页码这些内容被切进 chunk 之后向量里混进了大量和语义无关的重复噪声检索时经常把同一份文档的十几个 chunk 一起召回因为它们的向量长得太像了。后来我加了一组清洗规则专门处理这类问题。具体规则有四条。第一去掉形如“第 X 页 / 共 Y 页”和纯数字行。第二统计全文出现频次超过页数一半的短行判定为页眉页脚整行删除。第三中文文档的软换行要合并——PDF 里一句话被排版切成两行中间那个换行不是段落分隔直接留着会让切分器误判句子边界得判断行尾是否以句号、问号、分号结尾不是就与下一行拼接。第四全角半角统一中文标点统一不然同一个词在向量空间里会被切成不同 token。import re from collections import Counter from langchain_core.documents import Document def clean_text(text: str, page_count: int 1) - str: lines text.split(\n) # 1. 去掉页码行 lines [ln for ln in lines if not re.fullmatch(r\s*第?\s*\d\s*页?\s*(/\s*共?\s*\d\s*页)?\s*, ln)] # 2. 高频短行判定为页眉页脚出现次数超过页数一半 counter Counter(ln.strip() for ln in lines if 0 len(ln.strip()) 30) noise {ln for ln, c in counter.items() if page_count 1 and c page_count * 0.5} lines [ln for ln in lines if ln.strip() not in noise] # 3. 合并中文软换行 merged, buf [], for ln in lines: buf (buf ln.strip()) if buf else ln.strip() if re.search(r[。]$, buf) or not ln.strip(): merged.append(buf) buf if buf: merged.append(buf) # 4. 全角半角与标点统一 text \n.join(merged) text text.replace(, ).replace(, ).replace(, ) return re.sub(r\n{3,}, \n\n, text)表格是另一个重灾区。PDF 里的表格解析出来往往是一列一列错位排列的语义完全丢失。我的处理方式是检测到表格区域就整块转成 Markdown 表格并且把整张表作为一个不可分割的 chunk绝不按长度切开。一张参数对照表被切成两半之后前半段有表头没数据后半段有数据没表头检索出来谁都读不懂。2.2 切分策略chunk_size 到底怎么定切分粒度是这套系统里最需要拍脑袋、也最影响效果的参数。我的判断标准只有一条一个 chunk 应该只承载一个能独立回答的问题点。如果用户问“报销标准是多少”命中的 chunk 里应该是一段完整的报销规则而不是半句话加一个无关的标题。按这个标准纯定长切分是不够的。我实际用的是“按结构优先 定长兜底”的组合。先用 Markdown 标题层级做第一轮切分把每个三级标题下的内容作为一个语义块如果这个块超过 800 字再用递归切分器按段落、句子往下拆如果拆完还有超长的比如一大段没有标点的枚举才用硬切。chunk_size我定在 500 字chunk_overlap定在 80 字。这两个数字是算出来的不是拍出来的。中文语境下500 个汉字大约对应 350 到 450 个 token落在这个区间的原因是太短200 字以下会让单个 chunk 缺少上下文检索命中了但模型读不出完整意思太长1000 字以上会让一个 chunk 里混进多个主题向量变成一个“平均语义”问什么都能沾点边但都排不到第一。80 字的 overlap 大约是 15%作用是防止一个关键句正好被切在边界上前后两半都缺主谓。from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, length_functionlen, separators[\n### , \n## , \n\n, \n, 。, , , , , ], )注意separators的顺序就是切分时的优先级从前往后尝试。把标题层级放在最前面是为了让切分尽量在章节边界断开中文句号放在换行之后、逗号之前是为了保证尽量在完整句子处断开而不是在半句话处。这个列表里千万不能漏掉中文标点只写英文的.!?会导致中文文档几乎切不动最后全部走硬切。这里补一个我后来加上的进阶技巧叫父子切分。用来检索的是小块300 字但送给模型的是它所属的大块1200 字。好处是检索精度按小块算不容易被无关内容稀释模型看到的上下文按大块算信息足够完整。LangChain 里可以用ParentDocumentRetriever直接实现也可以自己在元数据里存一个parent_id检索后回表捞父块。2.3 元数据字段设计很多人漏掉的关键一环切完块直接入库是能跑通的但跑到第二周你就会发现缺东西。元数据最起码要有这几项字段作用是否用于过滤doc_id文档唯一标识用于增量更新与删除否chunk_idchunk 唯一标识用于幂等写入否source原始文件路径或链接用于引用展示否title文档标题回答里显示“依据《XXX》”否section所属章节标题精确定位否version文档版本号否effective_date生效日期处理“最新版本”类问题是dept归属部门做权限隔离是permission密级标签检索时强制过滤是chunk_id我用doc_id 序号拼出来。这东西看着无用实际上是增量更新的关键重跑入库时用同一个chunk_id覆盖写入就不会出现同一份文档被灌进去两遍、检索时前五条全是重复内容的情况。dept和permission两个字段是权限控制的基础检索时在向量库层面做元数据过滤比检索完再在应用层筛要可靠得多——后者很容易漏掉把不该给的内容塞进上下文。3. 向量化与检索链路的落地细节3.1 Embedding 选型与批量入库的三个注意点选型上中文场景优先选中文语料占比较高的模型不要用纯英文模型硬套。我对比过同一个问题在中英混合的技术文档上中文优化模型的首条命中率明显更高。维度选 1024理由是 768 维在长文档上区分度偶有不足1536 维存储和检索开销又上来了十万级 chunk 的场景下 1024 是一个平衡点。真正重要的是建库和查询必须用同一个模型、同一个版本。我吃过一次亏中途换了 embedding 模型忘了重建索引结果查询向量和库里向量不在一个空间检索结果完全是随机的排查了两小时才反应过来。第二个注意点是批量写入的节奏。一次性把几千个 chunk 全推给 embedding 接口必然触发限流。我用的批大小是 64每批之间加一个很短的间隔并且对 429 做指数退避重试。import time from langchain_core.documents import Document def batch_embed(chunks, embeddings, batch_size64, max_retry5): vectors [] for i in range(0, len(chunks), batch_size): batch chunks[i:i batch_size] for attempt in range(max_retry): try: vectors.extend(embeddings.embed_documents([c.page_content for c in batch])) break except Exception as e: if attempt max_retry - 1: raise time.sleep(2 ** attempt) # 1s, 2s, 4s, 8s time.sleep(0.2) return vectors第三个注意点是归一化。如果向量库用余弦相似度确认一下 embed 出来的向量是否已归一化没有就手动除一下模长。有些库在没归一化时用内积近似余弦长文本的向量模长更大会天然获得更高的分数导致长 chunk 被系统性地偏爱。from langchain_community.vectorstores import Chroma vectorstore Chroma( collection_nameinternal_docs, embedding_functionembeddings, persist_directory./vector_db, collection_metadata{hnsw:space: cosine}, ) # 用稳定 id 幂等写入重跑不会产生重复 vectorstore.add_documents( documentschunks, ids[c.metadata[chunk_id] for c in chunks], )3.2 检索参数topK 不是越大越好topK这个参数我调了很久。直觉上召回越多越好实际不是。topK太大噪声 chunk 会被一起塞进上下文模型容易被无关信息带偏还会挤占 token 预算topK太小正确答案所在的 chunk 根本进不了候选后面再怎么重排也救不回来。我的做法是两段式召回阶段宽一点20 条精排阶段窄一点5 条。20 条保证召回率5 条保证送给模型的上下文干净。相似度阈值这块我建议不要设死一个固定数值。不同文档、不同问题类型的分数分布差异很大设 0.75 的结果可能是某类问题全被拒答、另一类问题全是噪声。我实际用的是相对阈值——取前 20 条的分数如果第一名分数显著高于其余比如是均值的 1.3 倍以上就只带前几条如果分数都很扁平说明检索没找到真正相关的内容触发兜底回复。MMR最大边际相关性这个选项值得开。它会在相关性和多样性之间做平衡避免召回的都是同一段话的不同切法。lambda_mult设 0.6偏相关性多一点的设置因为内部问答里答案通常就是集中的一两条不需要太强的多样性。retriever vectorstore.as_retriever( search_typemmr, search_kwargs{ k: 5, # 最终给模型的条数 fetch_k: 20, # 先召回 20 条再做 MMR lambda_mult: 0.6, filter: {dept: product, permission: {$in: [internal, public]}}, }, )3.3 混合检索与重排这两步是效果的分水岭纯向量检索有个硬伤对精确匹配不敏感。用户问“XJ-2200 的额定功率”向量检索很可能召回一堆讲功率的段落就是找不到带这个型号的那一段因为型号这串字符在语义上没什么信息量。解决办法是加一路关键词检索BM25 这类把两路结果用 RRF倒数排名融合合并。RRF 不需要归一化两路分数只看排名实现简单而且稳。def rrf_fuse(vec_ids, bm25_ids, k60): score {} for rank, doc_id in enumerate(vec_ids): score[doc_id] score.get(doc_id, 0) 1 / (k rank 1) for rank, doc_id in enumerate(bm25_ids): score[doc_id] score.get(doc_id, 0) 1 / (k rank 1) return sorted(score, keyscore.get, reverseTrue)重排是第二道分水岭。向量检索是双塔结构查询和文档分别编码再算相似度快但粗交叉编码器把查询和文档拼在一起过一遍模型精度高得多代价是慢。所以标准做法是先用向量粗排到 20 条再用交叉编码器精排取前 5。实测下来加这一层之后首条命中率有明显提升而延迟只增加了几十毫秒这个投入产出比非常高。3.4 Prompt 模板与引用溯源Prompt 这块看起来简单实际上决定了模型会不会乱编。我的模板包含四块内容角色与硬约束、资料区、问题、输出格式要求。硬约束里最重要的一句是“只能依据下面提供的资料回答资料中没有的信息直接说没有找到不要用常识补充”。这句话必须写死否则模型一旦觉得资料不完整就会自己编一段听起来很合理的答案这在内部问答里是致命的。资料区我要求每条带编号格式是[1] 《文档标题》- 章节名后面跟正文。输出格式要求模型在句末标注引用编号比如“报销额度为 5000 元 [1]”。这样前端可以把编号渲染成可点击的角标用户点一下就能跳到原文。还有一个容易被忽略的细节上下文顺序。模型对长上下文中间部分的注意力会衰减这是普遍现象。所以我会把重排后分数最高的那条放在最前面第二高的放在最后面把相对不重要的夹在中间。这个调整看起来微不足道实际对“答案就在资料里但模型没读到”这类问题的改善相当明显。from langchain_core.prompts import ChatPromptTemplate PROMPT ChatPromptTemplate.from_messages([ (system, 你是一个内部知识库问答助手。严格遵守以下规则\n 1. 只能依据【资料】中的内容回答不得使用资料之外的知识补充。\n 2. 资料中没有相关信息时直接回答“资料中未找到相关内容”不要猜测。\n 3. 每句结论后必须标注来源编号格式为 [编号]。\n 4. 回答控制在 300 字以内条理化表达不要复述资料原文。\n 5. 如果不同资料存在冲突指出冲突并分别标注来源。), (human, 【资料】\n{context}\n\n【问题】\n{question}), ]) def format_docs(docs): return \n\n.join( f[{i 1}] 《{d.metadata.get(title, 未命名)}》- {d.metadata.get(section, )}\n{d.page_content} for i, d in enumerate(docs) )4. 从检索链升级成智能体4.1 Chain 能解决八成问题剩下两成才需要 Agent一开始我就上了 Agent结果反而是退步。原因很直接Agent 每一轮都要让模型决定“下一步调什么工具”多一次模型调用就多一份延迟和成本而且模型偶尔会陷入反复调同一个工具的循环。对于“查一个事实”这种单跳问题一条固定的检索链又快又稳根本不需要 Agent 的判断能力。后来我做了个路由先用一个很轻的规则加一次分类判断把问题分成三类。第一类是事实查询直接走检索链第二类是需要多跳的比如“A 文档里的接口和 B 文档里的字段定义是否一致”这种需要分别检索两份资料再比对交给 Agent第三类是闲聊或无关问题直接兜底不浪费 token。这个改动之后平均响应时间降了将近一半成本也降下来了。Agent 真正有意思的地方在于它能自己组合工具。我给它配了三个工具知识库检索、按文档 ID 取全文、按标签列出相关文档。遇到“把最近三个月产品相关的规范文档都过一遍看看有没有互相矛盾的”这种问题它会先列文档、再逐份取全文、最后汇总这套流程用固定链写死会很难看。from langchain_core.tools import tool tool def search_knowledge_base(query: str) - str: 在内部知识库中做语义检索。适用于查询流程、规范、参数定义等事实性问题。 输入应当是一个完整的自然语言问题不要只输入关键词。 docs retriever.invoke(query) return format_docs(docs) tool def get_document(doc_id: str) - str: 按文档 ID 获取完整正文。仅在需要通读全文或对比多份文档时使用。 return load_full_text(doc_id) tool def list_documents(tag: str) - str: 按标签列出文档清单返回标题、版本和生效日期。 return query_doc_index(tag)4.2 多轮对话里的指代消解绕不开的一步单轮问答跑通之后第二个问题马上来了用户会追问。“这个流程要多久”“那它的审批人是谁”第二个问题里的“它”如果不做处理检索出去就是一句废话向量库里根本找不到对应内容。这是多轮问答最典型的失效场景。我的处理方式是在检索之前加一层查询改写把最近几轮对话和当前问题一起交给模型让它输出一个自包含的完整问题。这一步的 prompt 很短成本很低但对多轮体验的提升非常直接。REWRITE_PROMPT ChatPromptTemplate.from_messages([ (system, 把用户的最新问题改写成一个不依赖上下文、可独立检索的完整问题。\n 要求补全所有代词和省略的主语保留原问题的关键实体和限定词\n 如果原问题本身已经完整原样返回不要添加任何额外信息只输出改写结果。), (human, 对话历史\n{history}\n\n最新问题{question}), ])记忆的存储上我不建议把全部历史都塞进去。超过一定轮数之后早期的对话对当前问题的价值很低反而占 token。我的做法是保留最近 3 轮原文更早的压缩成一段摘要。摘要也会在主对话结束时异步更新不阻塞用户。4.3 成本、延迟与失败兜底算一笔账。假设知识库有 400 份文档平均每份 5000 字总计 200 万字。按 500 字切块、15% 重叠大约产生 4600 个 chunk。建库阶段一次性 embedding 这 200 万字成本在几十元这个量级跑一次十几分钟可以忽略。真正持续产生成本的是查询阶段。每次提问大致包含一次查询 embedding几十个 token、一次重排本地模型无 API 成本、一次生成上下文 5 条、约 2500 字加上回答 300 字。按中等规格模型的价格算单次问答成本在几分钱。按每天 500 次查询算一个月大概几十到一百多元。这个数字对内部工具来说完全可以接受但如果开了 Agent、一次问题触发三轮工具调用成本会翻三倍这就是我前面坚持做路由的原因。延迟方面端到端拆开看embedding 约 100 毫秒向量检索几十毫秒重排几十到一百毫秒模型生成是主要耗时首字延迟一两秒完整生成几秒。用户体感最关键的是首字时间所以生成环节一定要开流式先把“正在检索”“已找到 3 份相关资料”这类状态吐出来等待感会小很多。兜底逻辑我至少写了三层检索结果为空或最高分低于阈值时直接返回“未找到相关资料建议换个说法或联系文档负责人”不走生成模型调用超时或报错时降级成只返回检索到的原文片段让用户自己看连续失败时在前端提示稍后重试同时把这次的 query 记进日志作为后续优化切分和检索的素材。5. 上线后踩到的问题与排查手册5.1 召回类问题明明有这段就是搜不到这是最高频的一类。我整理了几种典型表现和对应处置。现象大概率原因处置动作关键词精确匹配失败型号、编号纯向量检索对字符级信息不敏感加 BM25 混合检索RRF 融合同义表述搜不到用户用词与文档用词不一致查询改写补充同义词或用多语言 embedding命中的全是同一份文档该文档 chunk 语义过于相似开 MMR或按 doc_id 去重后取表格里的数据搜不到表格被切碎或解析错位整表转 Markdown作为独立 chunk 入库旧版本内容盖过新版本元数据缺版本或未过滤加version/effective_date检索时按时间过滤长文档的后半部分永远排不上chunk 长度差异过大导致分数偏移统一 chunk 长度或改用父子切分其中第一条我想多说两句。向量检索本质上是语义相似度它对“XJ-2200”和“XJ-2100”这种只差一个数字的字符串几乎没有区分能力两个向量可能非常接近。这在内网文档里是灾难性的因为大量型号、批次号、错误码都属于这类。关键词检索只认字面恰好补上这块短板所以混合检索在技术文档场景里应该算必备不算可选。5.2 生成类问题检索对了答案还是错的检索到的资料是对的但模型回答跑偏这类问题更难查因为你要去分辨到底是上下文有问题还是 Prompt 有问题。我遇到过四种。第一种是上下文太长导致信息淹没。5 条 chunk 拼起来两三千字模型抓住了开头和结尾漏了中间。处置办法就是前面说的排序策略把最相关的放两端。第二种是资料之间互相冲突。同一个规范在新旧两版里说法不一样模型各取一半拼出一个既不符合旧版也不符合新版的答案。我的处置是在 Prompt 里明确要求“发现冲突时指出冲突并分别标注来源”同时在入库时给文档打上生效日期检索时优先返回最新版本旧版本作为补充。第三种是模型无视约束用常识补全。资料里明明没写具体数字它给你编一个看起来很像的。这种情况靠 Prompt 只能压到一定程度最有效的办法是在生成之后加一个轻量校验提取回答里引用的编号检查每个编号对应的 chunk 里是否真的包含回答中的关键数字和实体。对不上就打回重生成或者直接降级成原文片段展示。这个校验我写得比较糙但拦住问题的比例不低。第四种是拒答过于激进。资料里明明有相关内容但表述方式和问题差异较大模型保守地说“未找到”。这种多半是 Prompt 里“不要用常识补充”这条约束写得太硬。我把措辞调成了“资料中没有直接答案时可以基于资料中相关段落做有限推论但要明确标注这是推论”误拒率明显下降。5.3 工程类问题更新、并发与权限增量更新这块必须实现“先删后加”而且删除要按doc_id批量删。只加不删的结果是旧 chunk 一直躺在库里用户搜出来的是三个月前的过期内容而这份内容当前版本里已经删掉了。我现在的流程是文件修改时间变化触发同步先按 doc_id 删除该文档所有 chunk再重新切分入库最后更新索引表里的版本记录。并发方面主要问题是限流。内部工具早上九点到十点是使用高峰几十个人同时问embedding 和生成接口都会撞上速率限制。我在调用层加了信号量和队列超过并发数的请求排队等待同时对 429 做退避重试。还有一个容易忽略的点是向量库的并发读——嵌入式向量库在多进程同时写的时候容易出问题所以入库任务必须串行不能和应用服务共享同一个写入口。权限这块我的做法是在检索层做硬过滤而不是在应用层筛结果。permission字段作为元数据过滤条件传进检索器不满足条件的 chunk 在召回阶段就被排除了压根不会进入上下文。这样做的好处是不会因为代码逻辑疏漏而泄露坏处是过滤条件写错的话会全军覆没所以上线前要用不同权限的测试账号各跑一遍。还有一个隐蔽的坑文档解析失败时不要静默跳过。我第一版就是这么干的某次一批 PDF 因为编码问题全部解析成空字符串结果是索引里悄悄少了三十份文档谁都没发现直到有人问那几份文档里的内容一直答不上来。现在解析失败会写进一张失败清单表并在管理页面上标红定期清理。6. 上线前值得花时间做的评估评估这件事我一开始也觉得麻烦直到连续被两个“感觉效果变差了”的模糊反馈搞到无从下手才老老实实建了一套。做法不复杂从真实提问里攒 60 到 100 条每条标注好“应该命中哪份文档的哪一节”和“答案要点是什么”。这活儿手动做一遍大概两三个小时但它是后面所有优化的基准。有了这个集合每次改动切分参数、换 embedding 模型、调 topK跑一遍就能看到召回命中率的变化不用靠感觉。我关注三个指标文档级命中率正确答案所在文档是否出现在前 5 条里、片段级命中率答案所在的 chunk 是否被召回、答案正确率人工判断或抽样判断。前两个是检索层的第三个是生成层的分开看才能定位问题出在哪一层。我印象很深的一次是换了 embedding 模型之后答案正确率掉了但文档级命中率没变查下去发现是片段级命中率降了——新模型把大段相似内容压得更平导致真正的答案 chunk 排不到前面。如果只看最终答案这个问题根本定位不出来。参数调整上还有一个经验每次只改一个变量。同时改 chunk 大小和 topK效果变好了你也不知道是哪个起的作用下次遇到问题也没法回滚到确定的配置。最后分享一点我自己的体会。这套东西真正难的地方不在用 LangChain 把管道搭起来那部分一两天就能跑通难在文档清洗规则要一条一条加、切分粒度要一遍一遍试、评估集要一条一条标这些都是笨功夫也是效果差距的全部来源。我现在的习惯是每上线一个新领域的文档先花半小时翻二十份原始文件看看它们的格式、结构、有没有大量表格和扫描件再决定这一批走什么解析路径。这一步看着慢实际上比事后排查召回问题快得多。另外一个我很推荐的扩展方向是给智能体加上“找不到就去问人”的能力。当检索置信度低于阈值时它不硬答而是把问题整理成一条待办附上最接近的三份文档作为参考推给对应的文档负责人。这个功能上线之后知识库的更新速度明显变快了——因为大家终于知道哪些内容是真正被问过、而且答不上来的。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询