☰
企业知识库问答系统落地:RAG+LangChain+DeepSeek完整实践
2026/10/20 14:09:35 网站建设 项目流程

最近在帮一家制造企业搭内部知识库问答系统,技术选型最终定为 RAG,框架用 LangChain,生成模型接 DeepSeek。整套系统从需求确认到生产环境跑起来,踩了不少坑,也摸索出一套相对稳定的搭建路径。这篇文章不打算讲概念,直接把从选型、架构设计、代码实现到调优排错的全过程写出来,给正在做企业私有知识库问答、或者刚接触 RAG 的团队一个可参考的落地参考。

这套方案解决的核心问题是:企业内部大量制度文件、产品手册、技术文档散落在各个地方,员工想找一条制度规定根本不知道去哪搜,即便搜到了也是关键词匹配,经常答非所问。用 RAG 搭一个统一入口,让员工用自然语言提问,系统自己去文档库里检索相关内容再生成答案,并且能标注答案来自哪份文档,这才是企业问答该有的形态。适合有 Python 基础、想快速在企业内部落地知识库问答的开发者或技术负责人阅读。

1. 为什么企业私有知识库问答要选 RAG,而不是微调或纯搜索

1.1 三种主流方案对比,RAG 赢在可控性

动手之前,我先把能想到的方案列了一圈:传统搜索引擎、模型微调、RAG。不列不知道,一列就清楚为什么最终选了 RAG。

传统搜索在内部知识库场景下最直接的痛点是不理解语义。员工搜"离职要办哪些手续",如果文档里写的是"员工离场工作交接",关键词搜索基本召回不到,因为字面完全不重合。就算做了分词和同义词扩展,也解决不了"问法不同但语义相同"的问题。更麻烦的是,企业文档里的很多内容是表格、多级标题、交叉引用,传统搜索很难把这种结构转换成可用的答案。

模型微调是很多人第一反应想到的方案:把企业知识灌进大模型参数里。但实际一评估就发现几个硬伤。第一是更新成本,文档一变就要重新训练或增量训练,企业制度经常改,不可能每周花几万去微调。第二是可控性差,模型学进去的知识没法追溯,回答的时候到底引用了哪一条制度,说不清楚,合规审计直接卡死。第三是训练数据不好准备,企业内部文档格式混乱、质量参差不齐,要清洗到可以训练的程度,工作量比搭 RAG 大得多。

RAG 的思路完全不同,它把"知识存储"和"答案生成"拆开了。知识放在向量数据库里,更新文档时只需要增量重建索引,几秒钟的事;回答问题时,系统先从知识库里检索出最相关的几段内容,再把这几段内容作为上下文,扔给大模型去组织语言。大模型不是凭记忆回答,而是基于检索到的证据回答,相当于开卷考试。这意味着每一条回答都能溯源到原始文档,幻觉能大幅减少,这是企业场景最看重的一点。

1.2 RAG 的核心逻辑:开卷考试,先查资料再答题

RAG 对很多人来说是个黑盒,但拆开看就三层:索引、检索、生成。索引阶段把企业文档切块、向量化、存入向量数据库;检索阶段把用户问题也做向量化,在向量库里找最相似的内容块;生成阶段把检索到的内容块和用户问题一起拼进 Prompt,交给语言模型生成最终答案。

这里最核心的转化是"文本到向量"。文档切出来的每一段文字都会被一个 Embedding 模型映射成一组高维数字,语义相近的文本在这个向量空间里的距离也近。用户提问后,同样把问题转成向量,然后去向量空间里做最近邻搜索。这个过程和关键词无关,只和语义相关,所以能解决"问法不同但意思相同"的问题。

生成阶段的 Prompt 至关重要,它直接决定模型是"老老实实照着资料回答"还是"自己发挥"。我在生产环境里用的 Prompt 几乎是一句一句抠出来的,后面会详细写。你可以把它理解成给模型立规矩:你是企业知识库助手,只能依据提供的资料回答,资料里没有的就说不知道,并且要标注答案来自哪段资料。这条规矩反复强调之后,模型的自由发挥空间会被压到很小。

1.3 为什么组合偏偏是 LangChain + DeepSeek

选 LangChain 的理由其实很简单:生态成熟,组件齐全,不必从零开始写。文档加载有几十种 Loader,文本切分有各种 Splitter,向量库封装了 Chroma、FAISS、Milvus,模型接口兼容 OpenAI 格式的都能直接接。企业知识库的文档格式五花八门,光解析 PDF、Word、Markdown、Excel 就需要很多适配代码,用 LangChain 可以省掉大半工作量。

选 DeepSeek 的理由,一是中文理解能力在同类模型里属于第一梯队,二是 API 的性价比确实高,三是它的接口协议兼容 OpenAI 格式,LangChain 可以直接复用现有的 ChatOpenAI 封装,只需要改 base_url 和 model 参数。对于企业内部几十人使用的问答系统,调用成本完全可以接受。更关键的是,DeepSeek API 响应速度稳定,作为生成层不会成为体验瓶颈。

有人会问,为什么不用完全本地化的开源模型。如果企业数据不允许出内网,那确实需要本地部署生成模型,但这意味着要准备 GPU 资源、维护模型服务,成本会高一个量级。我们这次的需求是数据可以走内部知识库,但推理走公共 API 也能接受,所以选择了这个组合。架构上做了解耦,后续如果安全要求升级,只需把 DeepSeek 换成企业内网部署的模型接口,LangChain 这边的调用代码改动量很小。

2. 架构设计与关键组件选型:每一环都要知道为什么

2.1 一条完整的 RAG 链路包含哪些节点

我们最后敲定的链路不算复杂,但每个节点都反复验证过。整体分两条线:离线索引线和在线问答线。

离线索引用定时任务触发,先把企业内部的文档从各个源拉下来,源包括共享目录里的 Word、PDF、Markdown,还有内部 Wiki 导出的 HTML。文档拉到本地后,先做格式解析和清洗,去掉页眉页脚、图表水印、多余空行,然后把长文档切成固定大小的块,切块时设置重叠区域,避免语义被拦腰截断。接下来每个块交给 Embedding 模型转成向量,连同原文、文件路径、章节号、更新时间等元数据一起存入向量库。这步完成后,知识库就建立起来了。

在线问答链路接收到用户问题时,先判断是否需要权限过滤。如果系统接入了企业账号体系,检索前就要加上部门、密级这类过滤条件,确保没人能问到权限之外的内容。通过过滤后,问题文本走同一个 Embedding 模型转成向量,在向量库里做相似度检索,取回 top-k 个文档块。然后把这 k 块文本和用户问题、系统 Prompt 拼在一起,发送给 DeepSeek 模型。模型生成答案,同时把召回文档的标题和原文路径作为引用来源一并返回。最后把答案渲染到前端界面,展示给员工。

这个链路里最容易出问题的不是模型,而是前面那些看似基础的环节。文档没解析干净、切块切碎了、向量模型跟业务术语不匹配,都会导致最后答案不可用。所以我特别建议企业团队在动手写代码前,先把文档样本拿出来做一轮小规模验证,确认每一环的输出质量,再铺开全量数据。

2.2 Embedding 模型选型:中文场景不要无脑用通用模型

Embedding 模型决定了"召回"的上限,生成模型再强,召不回相关内容也是白搭。很多团队第一次做 RAG,喜欢直接用海外模型的文本向量接口,结果在中文企业文档上效果很差。原因不复杂,海外模型大部分训练语料以英文为主,对中文的理解深度、对中文专业术语的语义把握都不够。

我们这次选的是中文语料训练的 bge 系列模型,跑下来明显比通用向量接口更懂中文。它的部署也很简单,模型文件放在内网服务器上,加载到本地进程里,调用一次只消耗几百毫秒 CPU,比每次调外部向量接口更稳、更省钱,也不存在数据外传的问题。唯一需要注意的是模型的输入长度限制,一般最长支持 512 token 左右,所以文档切块要控制在模型能接受的范围内。

Embedding 模型的版本选择也有讲究,当时我们对比了 base 和 large 两个规格。large 效果确实好一点,但推理耗时和内存占用高不少。企业内部知识库的问题通常不追求极致精度,更看重批量索引速度和单机可部署性,最后选了 base 规格,配合重排模型来弥补精度差距。如果你刚开始搭,我建议直接拿 base 用起来,先把流程跑通,等发现问题再考虑升级。

2.3 向量库选型:Chroma、FAISS、Milvus 怎么决策

向量库的选择主要看数据量和并发场景。我们前期评估过三个主流方案:Chroma、FAISS、Milvus。FAISS 是向量检索库,不是完整的数据存储服务,需要自己管理持久化和并发,适合单机小规模实验;Chroma 是一个轻量级的向量数据库,支持持久化到磁盘,API 简单,几百个文档几万条向量这个量级跑起来很轻松;Milvus 是分布式向量数据库,适合千万级向量、高并发生产环境,但部署复杂度明显更高。

企业内部知识库,大部分场景不会超过几十万条向量,用户并发也就几十人,Chroma 完全够用。我们最终选了 Chroma,一个原因是可以直接复用 LangChain 的封装,另一个原因是它支持按 metadata 过滤,权限隔离实现起来成本很低。如果你的文档量一开始就很大,或者预估未来会到百万级向量,那建议一步到位上 Milvus,避免后面数据迁移。

衡量向量库还有一个容易被忽略的指标:检索质量的可观测性。Chroma 可以直接把查出来的原文档块打印出来看召回是否合理,这在调优阶段太重要了。要是封装的库太复杂,你很难判断问题是出在向量库、切分还是 Embedding,而 Chroma 的简单性让定位问题变得非常快。

2.4 DeepSeek 在链路里的角色定位与关键参数

DeepSeek 在整套系统里只干一件事:根据检索到的上下文生成最终答案。不要让它承担记忆知识库的作用,那是向量库的事。调用参数上,我们踩过很多坑,最核心的是 temperature 和 max_tokens。temperature 是生成随机性的控制参数,刚开始用了默认值,结果答案发散、用词飘,文档里明明没有的内容也会被编造。后来把 temperature 降到 0.1,回答立刻变得克制很多,基本能做到"资料里有就答,资料里没有就直说不知道"。

max_tokens 也要根据回答长度合理设置。企业制度问题的答案一般不会太长,设 512 足够,但如果是产品技术文档问答,可能要 1024。设太短会把答案截断在半句话上,设太长会增加等待时间和费用。另外,DeepSeek 的上下文窗口比早期版本大很多,但 RAG 场景下千万不要把所有检索结果都塞进去,一般取 top-k 的 4 到 6 个文档块、每块几百字,总上下文控制在模型合理范围内,既省 token,也能避免无关信息干扰模型。

还有一个容易忽略的点:模型版本会迭代。DeepSeek 官方会定期发布新版本,不同版本的指令遵循能力可能有差异。我们在代码里把 model 参数从环境变量读取,这样升级模型时不用改代码,改配置就能切。上线后要留意 API 返回的错误日志,尤其是超时和限流,需要做重试和降级策略。

3. 从零搭建:数据准备到问答全流程实操

3.1 环境准备与依赖安装

我的实操环境是 Python 3.10,操作系统用的 Linux,Windows 上也能跑,只是路径写法稍有不同。建议新建一个虚拟环境,避免依赖冲突。下面这些包是核心依赖,装完之后做基础验证。

pip install langchain langchain-openai langchain-community langchain-text-splitters pip install chromadb pip install sentence-transformers pip install pypdf

版本问题很容易让人崩溃。当前 LangChain 的 API 更新很快,很多网上教程用的还是老式链式写法,复制下来直接报错。我的建议是安装完成后,在 Python 命令行里打印一下langchain.__version__,记住版本,再配合对应版本的文档写代码。不要盲目追最新版,稳定能跑比什么都强。如果遇到ModuleNotFoundError,多半是替代了旧包,把langchain_community单独装一下就能解决。

3.2 文档加载与清洗

企业内部文档格式基本是"群魔乱舞",有 PDF、Word、Markdown、Excel,甚至还有扫描件。先看一个最基础的加载方式,通过DirectoryLoader指定目录和扩展名,把整个目录下的文档一次性读进来。

from langchain_community.document_loaders import DirectoryLoader, TextLoader, PyPDFLoader loader = DirectoryLoader( "./docs", glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={"encoding": "utf-8"}, ) docs = loader.load() pdf_loader = DirectoryLoader( "./docs", glob="**/*.pdf", loader_cls=PyPDFLoader, ) pdf_docs = pdf_loader.load()

加载完成后千万别直接切分,先做清洗。我见过很多文档里全是页眉页脚、每行末尾的软回车、表格转出来的大量空白字符,这些噪音会让向量被严重带偏。清洗函数可以这样写。

import re def clean_text(text: str) -> str: # 去除页眉页脚常见内容,保留正文 text = re.sub(r"第\s*\d+\s*页", "", text) text = re.sub(r"[\u3000\s]+", " ", text) # 把连续三个以上的换行压缩成两个 text = re.sub(r"\n{3,}", "\n\n", text) return text.strip() for doc in docs: doc.page_content = clean_text(doc.page_content) docs = [doc for doc in docs if len(doc.page_content) > 50]

清洗过程中有一条红线要注意:不要过度清洗。有些文档里的换行是有语义的,比如表格的每个单元格换行、列表的条目换行,全压成空格反而丢失结构。更稳妥的做法是先把 PDF 转成 Markdown 再清洗,保留标题层级和列表结构。我们中途换过一次解析工具,最终阶段把所有文档统一转成 Markdown,效果提升非常明显。

3.3 文档切分:chunk_size 和 chunk_overlap 到底怎么调

切分是整个 RAG 流程里最能决定命运算的一步。切太大,检索出来的信息太杂,模型抓不到重点;切太小,一段完整的意思被劈成两半,回答起来缺胳膊少腿。我常用的基线配置是chunk_size = 500,chunk_overlap = 100,分隔符按中文语言习惯排列。

from langchain_text_splitters import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""], ) chunks = text_splitter.split_documents(docs)

为什么分隔符要这么排?RecursiveCharacterTextSplitter会从左往右依次尝试分隔符,优先用双换行保持段落完整,如果切出来的块还超过chunk_size,就往下用单换行,再不行就用句号、感叹号、问号这类句子边界。这样切出来的块尽量在语义完整的地方断开。如果把分隔符顺序写反,系统会退化成纯按字符硬切,中文句子被从中间砍断的情况非常严重。

chunk_size和chunk_overlap没有绝对最优,取决于文档类型。制度条文多的用 400 到 500 比较合适;技术手册这种上下文依赖强的建议放大到 800 到 1000,overlap 150。调参方法很朴素:拿 10 个典型问题,切完索引后逐个查召回结果,打印出每个 chunk 的文本,肉眼判断内容是否完整。观察几轮之后,你会对自己文档的特性有直觉,再调整参数就快了。

还有一类文档必须特殊处理:表格。普通文本切分会把表格切开,导致语义完全丢失。我们的做法是用MarkdownHeaderTextSplitter按标题层级切分,并把表格整体保留为一个独立 chunk,后面会根据情况把表格单独放到一个命名空间里索引。如果你处理的文档里表格很多,建议单独建一条处理管线,不要和普通正文混在一起。

3.4 向量化与入库:把文本变成可检索的数量

这一步把切好的块转成向量并写入向量库。我们用HuggingFaceEmbeddings加载本地中文模型,这里直接指定模型路径,方便内网部署。Chroma 持久化到本地目录,下次启动不用重新索引。

from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embedding_model = HuggingFaceEmbeddings( model_name="./models/bge-base-zh-v1.5", model_kwargs={"device": "cpu"}, encode_kwargs={"normalize_embeddings": True}, ) vectorstore = Chroma.from_documents( chunks, embedding_model, persist_directory="./chroma_db", )

这里有一个非常重要的细节:normalize_embeddings要设为 True。它会将向量归一化到单位长度,这样用余弦相似度计算时数值更稳定,对检索质量有正向帮助。代价是额外一点点计算量,但完全值得。

另外,入库时一定要把元数据写全。LangChain 的 Document 对象有metadata字段,我们至少会放三个键:source记录文件名,page记录页号,title记录标题或章节。生产环境还要加上department用于权限过滤、updated_at用于增量更新。后面做引用溯源、权限控制、增量重建都依赖这些元数据,一开始设计好,省得后面返工。

写入 Chroma 之后,可以用一条测试向量验证一下。把"离职流程"这类问题转成向量,用similarity_search去库里找,看召回的前三名是不是真的提到了离职相关内容。如果召回内容完全不搭,不要怀疑向量库,优先怀疑切分和 Embedding 模型是否匹配,这个排查逻辑很重要。

3.5 组装检索问答链路:LangChain 把它们串起来

核心链路代码大概几十行,关键是 Prompt 设计。先定义 DeepSeek 模型,再挂载检索器,最后用create_retrieval_chain把"检索+生成"组装起来。

import os from langchain_openai import ChatOpenAI from langchain.prompts import ChatPromptTemplate from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain llm = ChatOpenAI( model="deepseek-chat", # 也可以从环境变量读取 api_key=os.getenv("DEEPSEEK_API_KEY"), base_url="https://api.deepseek.com", temperature=0.1, max_tokens=512, ) retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) prompt = ChatPromptTemplate.from_messages([ ("system", """你是企业知识库助手。请严格基于以下资料回答问题。 资料中没有的信息,请明确回答“资料未提及”,不要自行编造。 回答时在句末标注来源,格式为[1][2]。 资料: {context}"""), ("human", "问题:{input}"), ]) combine_docs_chain = create_stuff_documents_chain(llm, prompt) rag_chain = create_retrieval_chain(retriever, combine_docs_chain) result = rag_chain.invoke({"input": "新员工入职流程是什么"}) print(result["answer"]) for doc in result.get("context", []): print(doc.metadata["source"], doc.page_content[:100])

k=5是召回条数,取多少直接影响效果。取 3 时答案可能信息不足;取 10 时无关信息多,模型容易被干扰,token 消耗也翻倍。我们最终停在 5,配合重排模型后是效果与成本的最佳平衡。这个值没有标准答案,你可以用相同的问题集,分别设置 3、5、8 做 AB 测试,看答案准确率和完整度。

调用时result["answer"]是生成的答案,result["context"]是实际召回到的文档块。这两个输出在调试阶段一定要打出来看,能直观看到"模型到底基于什么内容回答了"。前端展示时,把metadata["source"]拼成引用链接,员工点击就能看到原文出处,这个功能对企业问答极其重要。

3.6 让问答更实用:历史对话与上下文记忆

原始的单轮问答在真实场景下不够用,员工经常会追问"那需要什么材料""这个流程要几天"这类依赖上文的问题。LangChain 提供了消息历史机制,让模型能结合之前几轮对话理解当前问题。

from langchain_core.chat_history import BaseChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.messages import HumanMessage, AIMessage store = {} def get_session_history(session_id: str) -> BaseChatMessageHistory: if session_id not in store: store[session_id] = [] return store[session_id] # 简化版:用 RunnableWithMessageHistory 包一层 rag_with_history = RunnableWithMessageHistory( rag_chain, get_session_history, input_messages_key="input", history_messages_key="chat_history", ) result = rag_with_history.invoke( {"input": "需要准备什么材料?"}, config={"configurable": {"session_id": "user_001"}}, )

加上历史之后,要注意一个问题:检索只针对当前问题,不针对历史消息中的隐含意图。如果用户问"那社保要交多久",这个问题本身缺少主语,向量检索会召回不到。简单的方案是先用一个轻量模型做历史对话改写,把当前问题补全成独立问题再检索。这一步能显著提升追问的召回率,但同时也多一次模型调用,需要评估延迟。若并发不高,建议加上。

4. 上线后踩过的坑与调优实录

4.1 召回质量差:先别急着换模型,检查三个地方

第一次联调时,我问"公司年假怎么算",答案牛头不对马嘴。第一反应是 DeepSeek 模型不够聪明,后来打印context一看,召回的内容压根没提年假,全是团建活动描述。问题出在召回环节,跟生成模型没有关系。排查思路分三步:先看切分是否把相关段落切碎了,再看 Embedding 模型是否理解这个业务领域的说法,最后看相似度分数是不是普遍偏低。

切碎问题可以通过调整chunk_size解决。Embedding 理解问题比较棘手,企业文档里有很多内部简称,比如"离场"在文档里完全没出现过"离职",但员工提问会写"离职"。这个对普通中文向量模型来说很难直接匹配。我们做了两层兜底:第一层是维护一份"同义术语表",在提问预处理阶段做同义词扩展,把"离职"替换成"离场、离职、离开公司"再检索;第二层是引入重排模型,把 top-20 的候选结果重新打分,把真正相关的结果排到前面。这两步加进去之后,召回质量肉眼可见提升了一个档次。

4.2 回答幻觉:Prompt 和温度要一起治

就算召回对了,模型仍然可能编造。比如文档里只写了"试用期六个月",模型回答时擅自加了一句"试用期工资打八折"。这个问题光调温度治标不治本,必须让 Prompt 把行为边界说死。我们的最新版 Prompt 明确写着:"你只能使用资料中的事实。禁止使用常识进行补充。如果资料不足以回答问题,请直接回复‘资料库中未找到相关内容,建议咨询人力资源部’。"这句话效果很明显,编造率从接近三成降到个位数。

还要注意,DeepSeek 有很强的推断能力,你让它"根据资料回答",它会自动补全很多看似合理的细节。所以不要给模型发挥的空间。temperature=0.1是经验值,如果你发现偶发编造,可以继续降到 0;同时把max_tokens控制在合理范围,避免模型为了凑字数而展开。另外,回答末尾强制输出引用来源,让模型每次回答都要对着引用检查一遍,这个"自我约束"机制多次验证有效。

4.3 文档切碎导致答案缺胳膊少腿:用结构化切分补救

有一类问题特别暴露切分缺陷:问的是"试用期请假规定",文档里试用期和请假在两个不同章节,切分后各自成为独立 chunk,单个 chunk 都没有完整答案。表面看是召回不够,本质是切分粒度与知识粒度不匹配。

解决办法是把知识粒度"上移"。对于规章制度类文档,不要机械按 500 字切,而是先用MarkdownHeaderTextSplitter按二级标题切,这样整个"请假制度"章节会作为一个大 chunk 保留。如果章节仍然很长,再在这个章节内部按句子边界切。这样切出来的 chunk 虽然大小不均匀,但每个 chunk 都是一个完整的知识单元。后来我们甚至把"概况"和"流程"做了关联 metadata,检索到子章节时同时携带父章节标题,上下文信息量大幅增加。

4.4 企业落地:权限隔离、审计与成本控制

企业内部知识库绕不开权限。最粗糙的做法是所有人都能检索所有文档,这在制度问答里风险很大。我们用 Chroma 的 metadata 过滤实现部门级隔离:文档入库时标记department字段,检索时根据当前用户的部门编码生成过滤条件。

retriever = vectorstore.as_retriever( search_kwargs={ "k": 5, "filter": {"department": current_user_department} } )

这个做法的前提是登录系统已经打通了账号体系并拿到了用户部门信息。如果文档密级更高,可以在检索前加一层显式校验,拒绝无权限用户的请求。审计方面,我们记录了每一次问答的输入、输出、召回内容、来源文档和时间戳,写入独立日志表。员工可以放心使用,管理员可以追踪任何一条回答的依据。

成本控制上,最大的开销是 API token 消耗。RAG 模式下,每次问答需要把 top-k 的文档块都传给模型,token 消耗比纯对话大不少。优化手段有两个:一是提高检索精度,确保传进去的内容都是有用的;二是设置缓存,相同问题在限定时间内直接返回历史答案,命中率提高后排除了很大一部分重复咨询。我们上线一个月后,缓存命中率约四成,费用下降了三分之一左右。

4.5 常见问题速查表

症状可能原因解决方向
问题有答案但检索不到切块过碎、Embedding 语义弱、术语不一致调整 chunk 参数,换中文 Embedding,加同义词扩展
召回了相关内容但答案乱编Prompt 约束不足、温度过高强化 Prompt 边界,temperature 降到 0.1 以下
回答总是"资料未提及"chunk 太小丢上下文、k 值太小增大 chunk_size/overlap,提高 k 值,引入重排
相同问题每次回答不一样temperature 过高temperature 固定为 0,开启缓存
并发一高就超时报错模型 API 限流、向量检索过慢加本地缓存,请求重试,必要时升级模型服务并发

5. 上线前必做的几件事和个人心得体会

5.1 上线前必过的测试清单

这套系统交付前,我有一套固定的测试清单。第一项是准备 30 个真实员工问题,这些问题必须来自实际业务,不能自己拍脑袋造。把这些问题跑一遍,逐个看检索召回的context是否准确,再看答案是否完整、是否用了正确依据。第二项是专门准备几个"知识盲区"问题,比如问一个文档里完全没有的话题,看系统会不会硬答。如果它开始编,就说明 Prompt 约束还不到位。第三项是权限测试,用两个不同部门的测试账号问同样的问题,确认结果隔离正常。第四项是并发压测,20 个用户同时提问,看平均延迟和失败率。

前两项直接决定问答质量,第三项决定能不能过安全评审,第四项决定用户体感。每一项都不能省,尤其是在多个部门同时使用的场景下,忽略了任何一项都可能上线后翻车。

5.2 我再啰嗦几个容易忽视的经验

第一,文档版本管理一定要做。我们有一段时间发现答案总是引用旧制度,排查了半天才发现,文档更新后没重新跑索引,向量库里还是旧内容。后来把文档的updated_at字段纳入索引,定时任务检查到文件变化就只重建那个文件的向量,问题才彻底解决。第二,不要把"知识库"等同于"一堆 PDF 丢进去"。文档的初始质量决定最终上限,源文档本身混乱,RAG 只能做到"在混乱中找相对准确"。建议让文档所有者先按模板整理一遍,效果立竿见影,比调多少参数都管用。

第三,迭代时不要只改代码不看效果。每一轮修改后,都跑一遍那 30 个基础问题,对比修改前后的答案变化。我见过太多人调了chunk_size后觉得自己优化了,结果召回率反而下降。保留一份基线测试集,才能让优化有方向。

第四,团队里一定要有一个人能看懂 LangChain 的报错日志。生产环境出问题时,最常见的情况是 API 超时、依赖版本不匹配、向量库文件损坏。掌握这些问题的排查手法比写新功能更紧急。我们后来专门写了一个健康检查脚本,定时检查向量库能否正常加载、API 是否可用、索引是否是最新版本,有问题就告警到工作群。这样一套保障做完,系统才敢说真正达到了可用的状态。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询