1. 这次到底在解决什么问题先聊聊我为什么要折腾这么一套东西。前阵子公司内部群里又有人问“报销流程怎么走”我翻了半天企业内部网盘找了一份落灰的PDF打开一看还是上个季度的旧版本。这事儿不算大但隔三差五就来一次行政同事每天光回复“你去看文档”就耗掉半个钟头。后来我琢磨与其继续打口水仗不如搞一个能自动回答这些重复问题的玩意儿。看了下市面上的方案大模型API已经挺便宜了但直接让模型回答企业内部问题它满嘴跑火车——因为模型根本没见过你公司的报销制度、请假规则、设备申领流程。这时候就得靠RAG也就是检索增强生成。RAG这个技术名字听着唬人说白了就是先让系统在内部资料里“查资料”再把查到的内容连同问题一起丢给大模型让它照着资料作答。整个过程管这个叫“三分钟搭建”其实是指代码层面不复杂——如果你只是想在局域网里跑通一个Demo级的FAQ问答一个Python文件、几十行代码真的够了。但“能跑”和“能用”之间隔着一堆细节这篇文章就是把两边都讲透。适合谁来参考两类人。一类是公司IT或运维想用最低成本给内部做个知识库问答工具另一类是刚开始接触RAG的开发者想理解检索、向量、嵌入这些概念到底是怎么串起来的。不涉及复杂的分布式系统也不搞微调一切从简但该讲清楚的原理我会尽量讲清楚。2. 选型四样东西定生死2.1 为什么不用微调非要用RAG如果问大模型“我们公司年假制度是什么”它大概率会给你编一个常见的劳动法条款但未必是你们公司内部的规定。要让模型学会特定领域知识有两条路微调和RAG。微调相当于给模型“补课”但它学的是通用规律你不一定能精确控制它记住某条具体制度。就算你喂了十万个问答对它也可能把去年和今年的报销额度混在一起。而且微调成本高每次制度变更都要重来一轮。RAG的思路完全不同它不试图让模型记住任何东西只在每一次提问的时候临时去文档库里找出最相关的几段内容塞进提示词里。你可以把模型想象成一个特别听话但没记忆力的实习生你给他一份资料他只按资料回答资料之外的事绝不瞎掺和。制度改了把新文档传上去就行模型不需要重新训练。这就是RAG在企业FAQ场景里最大的优势——知识更新快、幻觉率低、成本可控。内部文档量级在几百篇以内时一台普通电脑就能扛住建索引和检索的负载。2.2 核心组件的选择与权衡RAG实战需要四样核心组件文档加载器、文本分割器、向量库、大模型API。每一样都有多种选择但这次做的是“最简单版本”我直接给出最优路径。文档加载器处理PDF、Word、Markdown这类格式。如果公司资料主要是Word和PDF建议统一转成纯文本再处理省得加载器各自为政。我用的是Python的pypdf库遇到扫描版PDF就先用OCR但这一步不是必须的——大部分内部制度文档都是电子版。文本分割器决定你检索的“颗粒度”。按固定字数切比如每500字一段是最简单的但容易把一句话从中间砍断。经验做法是按章节或段落切再结合一个最大长度限制。向量库的选择决定了检索速度和后续维护成本。本地开发用FAISS最省事儿它没有服务端就是一个本地索引文件如果想多台机器访问或在运维上更规范用Chroma或pgvector也行。这篇文章先按FAISS讲因为对新手最友好。大模型API这块国内基本就是几家主流的通用大模型选一个便宜的对话模型即可。关键是调用方式得兼容OpenAI接口风格后面写代码会省很多事。提示嵌入模型和对话模型不是一回事。嵌入模型把文本变成一串数字向量对话模型负责生成回答。不要用对话模型去做文本转向量的工作也不能指望嵌入模型给你生成回复。3. 数据准备FAQ系统的地基3.1 原始文档的收集与清洗做FAQ系统第一步不是写代码而是把散落在各处的资料收集起来。行政的制度文件、IT的操作指南、HR的流程说明先全部拉到同一个文件夹然后按模块分分类。这时候最容易踩的坑是把几千页全量文档一股脑塞进去。其实企业内部FAQ的答案往往集中在几十份核心文档里比如员工手册、报销指引、IT帮助文档。资料越聚焦检索准确率越高。我之前试过把公司三年的公告全塞进去结果问“打印机怎么连”时检索出来的却是三年前的搬迁通知因为那段文本里也出现了“打印机”三个字。文档清洗的步骤很简单过滤掉图片、页码、页眉页脚去掉表格里那些变成乱码的符号如果文档里有“待更新”“以最新为准”这类废弃提示直接删掉相关段。剩下的纯文本内容统一保存为Markdown或TXT每份文件一个独立文件文件名起得规范一点比如“报销流程-2024版.md”。实操中我建议把资料按三种用途分开第一类是问答对已有的现成FAQ格式为“问xxx 答xxx”这类最珍贵直接用于构建问答库第二类是制度原文和操作手册需要后续切分第三类是补充参考比如常见问题汇总表。三类资料准备好之后后面所有处理都是流水线作业。3.2 问答库的构建策略数据准备有个核心原则文档切分方式要跟你的问答方式对齐。如果你打算让员工用自然语言提问“出差报销需要什么”那检索单元就不能是一整个Word文档而应该是一段一段的语义块。我采用两层策略。第一层是按章节切分把Markdown里的每个二级标题作为一段这一段内部如果超过1000字再按句子边界二次切分。第二层是给每个切分出来的块自动生成一个“摘要标题”这个摘要其实就是从段首提取关键词方便有时候直接展示给前端。切分时有个经验参数块长度在300到800字之间比较合适。太短语义不完整太长检索时命中一堆无关的上下文。固定字数切分时重叠部分建议设在50到100字避免一句话被杠在两段之间。下面是我实际使用的切分逻辑def split_text(text, chunk_size500, overlap80): if len(text) chunk_size: return [text] chunks [] start 0 while start len(text): end start chunk_size # 找最近的换行符作为切点点 if end len(text): newline_pos text.rfind(\n, start, end) if newline_pos ! -1 and newline_pos - start chunk_size * 0.5: end newline_pos chunks.append(text[start:end]) start end - overlap if end len(text) else len(text) return chunks这段代码的关键不是算法本身而是注释里那一行“找最近换行符”——按语义边界切分比硬切效果好得多这是我在多次检索不准之后才悟出来的。4. 核心实现从索引到问答的完整链路4.1 环境准备与依赖安装开始写代码之前先把Python环境准备好。我用的是Python 3.10虚拟环境是必须的别直接往系统环境里装依赖避免跟其他项目打架。依赖安装就一句话pip install pypdf langchain langchain-community faiss-cpu openai tiktoken flask简单解释一下每个包的作用。pypdf用来解析PDFlangchain是一个把所有AI组件串起来的框架faiss-cpu是向量检索库CPU版对个人电脑完全够用openai是调用大模型API的SDKtiktoken负责统计token数量flask用来搭一个最轻量的HTTP接口。这些装完总共不到三百兆三分钟部署是现实的。如果公司网络环境没法直接访问外部API后面可以换成本地部署的模型但思路完全一样。为了演示方便我默认还是走云端大模型API。注意调用前在环境变量里配好密钥可别把密钥硬编码进代码里。4.2 文本嵌入与向量化存储文本嵌入是整个系统的核心它的作用就是把一段自然语言变成一串浮点数数组。两个句子如果语义相近算出来的两个向量在高维空间里挨得也近。“报销单怎么填”和“报销单填写步骤”这两句话字面不同但向量距离会很近这正是RAG能理解问题的关键。嵌入我用的模型是text-embedding-3-small它把文本编码成约1500维的向量在效果与性能之间比较平衡。调用方式如下from openai import OpenAI client OpenAI() resp client.embeddings.create( modeltext-embedding-3-small, input报销需要什么材料 ) vector resp.data[0].embedding有了向量接下来要把所有文档块的向量都算出来然后存进FAISS索引。这一步相当于给全部资料做了一张语义地图。FAISS索引创建核心代码如下from langchain.embeddings import OpenAIEmbeddings from langchain.vectorstores import FAISS from langchain.schema import Document docs [Document(page_contentchunk, metadata{source: file_name}) for chunk in all_chunks] embeddings OpenAIEmbeddings() vectorstore FAISS.from_documents(docs, embeddings) vectorstore.save_local(faiss_index)这里创建的vectorstore对象不仅存了向量还保存了原始文本和来源信息。后续问答时需要把原始文本取出来塞给大模型当参考资料所以这一步不能省。保存成本地索引文件之后下次启动系统就不用重新计算向量直接加载即可。4.3 检索与生成的完整整合FAISS检索的核心是余弦相似度查询语句先转成向量然后在向量空间里找最相近的若干条。k值我通常设为5同时要求返回的每条结果至少要超过一个相似度阈值比如0.7否则说明文档里根本没有相关内容就不要硬凑答案。接下来是整合成一个问答函数。这个函数做了三件事接收用户问题、把问题向量化并检索相关文档块、拼好提示词后调用大模型生成回答。具体代码如下def ask(question): # 1. 检索 retriever vectorstore.as_retriever(search_kwargs{k: 5}) hits retriever.get_relevant_documents(question) # 2. 拼装上下文 context \n\n.join([ f[来源文件: {h.metadata[source]}]\n{h.page_content} for h in hits ]) # 3. 调用大模型 prompt f你是一名公司内部助手请严格按照提供的资料回答用户问题。 资料如下 {context} 用户问题{question} 要求 - 只能使用资料中的信息不要自行发挥 - 如果资料中没有答案请明确回复未找到相关信息 - 回答尽量简洁清晰用口语化的中文 resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], temperature0.2 ) return resp.choices[0].message.content注意提示词里那句“只能使用资料中的信息”不是废话它是压制幻觉的关键。内部问答场景宁可回答“未找到”也不能让它瞎编一个报销流程骗同事。temperature设成0.2也是经验值太低没灵活性太高容易跑偏。最后套一层Flask就变成一个内部可用的HTTP服务了from flask import Flask, request, jsonify app Flask(__name__) app.route(/qa, methods[POST]) def qa_endpoint(): data request.get_json() question data.get(question, ) if not question: return jsonify({error: missing question}) answer ask(question) return jsonify({answer: answer}) if __name__ __main__: app.run(host0.0.0.0, port8000)跑起来之后公司内部的同事用浏览器打开一个小工具页面输入问题点击提问就能拿到答案。整个链路至此闭环。5. 实操验证与效果评估5.1 用一个真实案例走通全流程代码写完不代表能用我拿公司真实的制度文件做了三轮验证。第一轮挑的是高频问题“年假休假需要提前几天申请”结果回答是“根据员工手册年假需提前三天在OA系统中申请并等待直属领导审批”。这个结果看起来没问题但我去查了原始文档发现里面其实同时提到了“三天”和“五个工作日”两种说法上下文不同导致模型选错了。问题出在切分后的文本块丢失了语境检索到的段落正好是某个例外条款的说明。于是我调整了切分规则把“适用范围”“有效期”“例外条款”这类元信息统一附加到每个块的前缀再重新建索引后回答就准确了。第二轮测试我故意问了一些文档里没有的问题比如“公司会不会给员工发元宵节礼物”。模型按照提示词的要求回复了“未找到相关信息”而不是顺着问题编一个节日福利方案符合预期。第三轮测的是边界情况同义词替换。同事问“出差打车发票怎么报”文档里写的是“交通票据报销流程”。系统依然能检索到正确段落因为嵌入模型处理了语义相似度这就是向量检索比关键词搜索强的地方。5.2 几个指标帮你判断系统好不好用很多团队搭完RAG就结束根本不确定效果好不好。我建议至少要盯两个数检索命中率和答案准确率。检索命中率指的是系统检索出来的前5个文档块里有没有包含真正能回答问题的内容。计算方法很简单挑出50个真实的员工问题逐个把正确答案所在的文档块抽出来然后看系统每次检索时它是不是出现在结果里。我第一版跑下来命中率只有六成这就是为什么有些问题回答得很准、有些完全跑偏的原因。答案准确率更主观一些需要人工打分。让同事帮忙随机抽20个问题对着检索出的文档看回答对不对。一般做到“90%的问题能答对”就算及格毕竟是给内部员工用的他们不太在意措辞优美与否更在乎信息是否准确。根据我的经验如果命中率低于七成优先优化文档清洗和切分方式而不是盲目换大模型。大部分人以为RAG效果差是模型不行其实八成问题出在检索阶段。6. 使用中的三块硬骨头6.1 检索不准问题出在“索引”而不是“模型”我调试RAG系统最常听到的抱怨是“这个系统很蠢问什么都答不上”。但打开日志一看发现真正的问题是检索回来的文本片段驴唇不对马嘴。搜索引擎靠关键词匹配RAG靠语义匹配但语义匹配的前提是你要检索的文本本身是完整且有语义的。如果原始文档里全是大量只有表格和编号的条目切出来的块读起来像天书向量化后的结果也好不到哪里去。这类情况我一般手动加一道工序给每个文本块的前面加一段概括说明相当于给文档做摘要再向量化。另外不要迷信大模型的中间结果。有些系统先让大模型把问题改写一遍再检索叫“查询改写”。如果原问题本身已经直白清楚了改写反而引入噪音。我中途试过这个方法准确率不升反降后来干脆关掉只做原始问题检索。6.2 回答错误如何用“证据链”定位问题同事反馈“回答似乎不对”时我最忌讳直接去调提示词那样无异于隔空猜谜。正确做法是让系统在提供答案的同时把检索到的原始文档段落一并输出出来这就是证据链。实现也很简单在第4.3节那个ask函数里返回值加上hits里的来源文件名和原文即可。前端可以做成“展开查看引用来源”同事看到原文后如果觉得答案不对一眼就能判断是检索错了还是模型理解错了。这个功能刚开始觉得没必要后来发现它减少了大量沟通成本。顶端有一点容易被忽略大模型会把上下文中不同来源的信息糅合在一起造成张冠李戴。例如两个文档块都提到“报销”但一个是差旅费标准一个是办公用品采购标准模型就可能把两者混着说。我的解决办法是在拼接上下文时为每个块加上明确的前缀标签比如“【文件一差旅费标准】”让模型能区分信息来源。6.3 依赖与部署别在环境上翻车开发环境跑得飞起部署到公司服务器就歇菜这种事太常见了。RAG系统依赖Python包版本langchain这个库迭代极快几个月不更新可能一堆API就变了。我的建议是锁死依赖版本最好用requirements.txt固定所有版本号。另外faiss-cpu在Windows和Linux上安装都有小坑Windows记得装非官方预编译包Linux别用pip install faiss直接装要装faiss-cpu。公司内网不能访问外网API的场景就要考虑换成私有化部署的模型。方案是把对话模型切到本地开源模型比如通过Ollama跑一个量化版的大模型。检索部分本身不依赖外网只有嵌入与对话两处调API其中任意一个换成本地服务整个系统就能完全内网化。实操心得我最后是把系统封装成一个Docker镜像挂载在内部服务器上数据卷存索引文件和文档目录服务重启自动加载索引。整个过程大概一百行配置比裸机部署省心得多。7. 进阶方向从Demo到正经产品三分钟版本搭好后接下来怎么走下去是很多人关心的问题。我这里不准备谈太宏大的规划就说说我们是怎么从Demo走向实际可用的。第一件事是权限控制。公司内部文档有的只能HR看有的IT团队才能看如果一刀切全量进向量库那完蛋了。我见过直接把制度文档做成公开问答系统的案例结果被不该看的人看到尴尬到脚趾抓地。所以如果资料有敏感分级必须先做权限过滤在检索阶段就按用户身份过滤文档块。第二件事是反馈闭环。每次问答应该让用户可以点“这个回答有帮助吗”把负反馈样本存下来定期分析。如果发现有大量负反馈集中在同一个问题说明对应文档可能过期了或切分有问题这时候重点维护就好不需要动不动重建整个系统。第三件事是定期刷新。企业内部制度每年甚至每个季度都会改版FAISS索引不会自动感知文档变化。我的做法是写一个定时任务每天扫描文档目录的修改时间变了就自动重建相关文件的索引全量重建也可以反正是小数据量几秒钟的事。第四件事是知识库的持续积累。光有制度文档不够员工问出来的真实问题才是黄金知识。把从未命中或答案质量低的问题整理成新的“问-答”对确认后补进知识库这个系统就会越用越聪明。这也是RAG类系统最独特的优势——知识可以持续积累模型能力固定但知识库是活的。我自己做完这套系统之后最深的感受是技术本身不是什么大工程难的是把散乱的内部知识梳理成可以被检索和生成的结构化数据。这个过程中你需要跟行政、HR、IT反复确认文档的准确性和时效性技术交流会变成沟通成本。但也正因如此这个东西才真正有业务价值——它逼着公司把内部知识管理做了一次系统性的梳理。如果看完这篇文章你想动手做一个我建议循序渐进。第一天就跑通最小Demo用三五份文档、几十行代码感受一下向量检索是怎么回事接下来一周完善数据清洗和切分一个月后再考虑权限和部署。别一上来就追求完美先让它跑起来再让它变聪明这是任何知识库系统都要走的必经之路。