先别急着嘲笑“三分钟”这个说法——我一开始也觉得是标题党,直到上周帮几位同事搭完一套公司内部 FAQ 问答系统,从写代码到跑通 demo,掐表算下来确实不到四分钟。很多团队一听到 RAG,第一反应就是“又要上向量数据库、又要调模型、又要搞 GPU”,其实在你只是想解决“内部问答”这一件事的时候,整个链路可以简化到令人发指的程度。
这篇就把这套最小可用的 RAG 实践拆开讲清楚。它会解决什么问题呢:员工不用再去翻聊天记录、猜 wiki 路径,直接问一句话,系统会从你准备的内部文档里召回相关资料,再用大模型组织成一段准确、可溯源的回答。比较适合运维、IT 支持、HR、行政这类“每天都在回答重复问题”的团队,也适合研发同学想快速给团队做个知识库问答入口的场景。
1. 先聊聊痛点:为什么公司内部总需要一套 FAQ 系统
很多团队的技术负责人没有意识到,公司内部的重复提问本质上是一个效率黑洞。新同事入职三个月还在到处问“会议室怎么预约”“报销单要贴哪些票”“账号权限找谁开”;老员工被问烦了,只能甩一个文档链接过去,而文档里的信息可能还是半年前写的。
1.1 这不是技术问题,是效率问题
一个 FAQ 系统解决的不是“智能”问题,而是“重复劳动”问题。你别小看这一件事——组织里的隐性知识往往散落在群聊记录、邮件往来、共享文档和个人笔记里。每个答案都有,但没有人知道它在哪里。问一次人,意味着提问者被打断一次,回答者也被打断一次。十个高频问题重复问一百次,就浪费了上千次的注意力和时间。
这个场景有几个非常适合 RAG 落地的特征:**
- 问题数量有限,而且高度重复,比如“怎么申请年假”在一年里会被问几百次。
- 答案基本稳定,规则类知识很少频繁变动,值得被固化下来。
- 答案必须是可查证的,报销流程、权限申请这类问题不能靠模型“自由发挥”。
- 内容量级不大,几十到几千条说明文档就够,根本不需要上重型解决方案。
所以我给它的定位不是“炫技项目”,而是“用最小成本把重复回答自动化”。
1.2 需求拆解:FAQ 系统的三大核心能力
不管什么形态的 FAQ 系统,本质上都要完成三件事:听懂问题、找到资料、给出答案。听起来普通,每一件都藏着细节。
第一是问题理解能力。员工不会按照文档里的原话去提问。“如何修改密码”和“密码老是验证失败怎么办”是同一个需求,但在传统的关键词匹配里完全是两回事。语义层面能够对齐,这是 RAG 比普通全文检索强的原因。
第二是答案检索能力。给用户一个搜索框,让他自己在几十个搜索结果里翻找,其实只是把“打扰人”换成了“自己受累”。真正的系统应该在资料库里找到最相关的三五条候选片段,而不是把整篇 wiki 倒给用户。
第三是答案组织与可信输出能力。检索到片段之后,还需要把碎片拼成一段通顺、完整的话。同时,答案必须给出依据,否则员工不敢照着操作,最后还是回来找你确认。
这里直接列一张需求对比表,看传统方案和 RAG 方案的差异:**
| 能力 | 关键词搜索 | 微调模型 | RAG 最小方案 |
|---|---|---|---|
| 同义问法命中 | 弱 | 较强 | 较强 |
| 答案可溯源 | 弱(列表无解释) | 弱 | 强(可附来源) |
| 更新成本 | 低 | 高(需重新训练) | 低(替换文档重建索引) |
| 实现成本 | 低 | 很高 | 低 |
| 对算力要求 | 无 | 高 | 很低 |
结论很明确:内部 FAQ 这个场景,RAG 是成本适中、效果上限很高的选择。
1.3 为什么是 RAG,不是别的方案
你可能要问:直接做个搜索框不就行了?还省得接大模型形成幻觉。问题在于,公司里的人提问时,通常并不知道文档里的准确用词。比如系统里叫“权限申请”,用户可能说“我要开个账号”。没有语义检索,这条就命不中。
那微调一个模型行不行?先不说数据量只有几百条,单说效果:微调需要构造大量问答对,训练完还不能保证知识同步更新。今天改了一个报销规则,微调模型还要重新训一遍,而 RAG 只需要把这个规则写进文档,重新建立索引即可。
用一句通俗的话来理解 RAG:就像给原本只顾聊天的员工配了一本随时可查的说明书。回答任何问题之前,他先翻说明书,找到相关段落,再照着说一遍。这样既不会瞎编,又不需要把全公司规则全部背下来。
2. 懒人方案选型:三分钟承诺是怎么实现的
很多人觉得“三分钟搭系统”是夸张,其实关键在于范围控制。三分钟搭建的,是整个链路的最小闭环,不是生产级系统。如果你愿意在千人规模的公司里直接上线,后面当然还要补权限、反馈、审计这些模块,但“能用的 Demo”确实花不了几分钟。
2.1 把 RAG 拆成三个环节:入库、召回、生成
RAG 的本质是一条流水线,分段理解就很清晰:
- 入库:把公司内部的 FAQ、制度文档、操作指南,切成小片段,做向量化,存进向量数据库。
- 召回:用户提问后,系统对问题做向量化,在库里搜索最相似的前 N 个片段。
- 生成:把检索到的片段作为“参考资料”塞进提示词,让大模型基于资料生成最终答复。
最小方案的关键,是让每一环都简单到不可再拆。比如向量库,就用本地文件形式,不需要启动一台数据库服务;嵌入模型,用轻量级中文模型,几百 MB 以内;大模型则用现成的 API,只需要写一次调用封装。
2.2 技术栈的最小可用组合
我实际使用的是一套特别“寒酸”的组合:**
- Python 3.9 以上,任何一台电脑都能满足。
- 文档格式全部用 Markdown 或 TXT,避免解析 PDF、Word 的额外工作。
- 切块直接用文本切分器,按段落切,不搞递归切分、不搞语义切块。
- 嵌入模型选轻量中文向量模型,大约百 MB 级,CPU 也能跑。
- 向量存储用一个嵌入式向量库,直接落盘到一个目录即可。
- 生成部分,调用大模型接口,或者公司自己部署的模型服务,封装成一个
chat()函数就够了。
这套选型不是拍脑袋。我试过更重的方案,比如先部署专业向量数据库、再加粗排模型,对于一千条以内的内部文档,完全感受不到收益,反而多了一堆要维护的东西。真正让检索效果差一个量级的,不是数据库有多强,而是你的资料整理得干不干净。
2.3 资料准备:问答对优先,长文档兜底
做 FAQ 系统,第一个要先想清楚的问题:你手上到底有什么样的资料?我一般会分两种情况处理。
第一种,已经有整理好的问答对。比如 HR 手里通常有“常见问题清单”,以“问题+答案”的形式存在。这是最理想的情况。入库时,只需要把问题和答案绑定保存,向量化时用“问题”作为检索索引,答案作为候选内容。
第二种,只有长篇手册或 wiki。例如产品说明、制度文件,里面没有现成的一问一答。这时候就需要切块,把一个长章节切成若干个 200 字左右的片段,每个片段都可以作为答案来源。检索时,系统返回最相关的片段,再由大模型把片段组织成回答。
如果你两种资料都有,直接统一处理:把问答对当作一个片段,把长文档切出来的片段当作另一个片段,全部丢进同一个向量库。检索时不分来源,只要能命中就行。这样实现最简单,效果也不差。
3. 实操实现:最小化 RAG 系统的完整代码
下面这部分是完整可复现的流程。我在一台普通办公电脑上验证过,不需要 GPU,也不需要额外启动数据库服务。你把代码复制过去,改成自己的文档路径,大概率十分钟内跑通。
3.1 先把资料整成标准片段
首先建一个目录,把所有 FAQ 文档放进去。我自己习惯用这种结构:**
faq_docs/ 01-人事制度.md 02-IT支持.md 03-行政指南.md每篇文章内部,用两个换行符号把不同主题分隔开,标题用 Markdown 的井号,正文直接写清楚步骤。这一步看着简单,其实决定了后面检索质量的一半。
然后写脚本把文档读进来并切块。切块时我用的策略是:按空行切,每块约 200 字,块之间保留 20 字重叠。用 Python 示例:**
from pathlib import Path from langchain_community.document_loaders import DirectoryLoader, TextLoader from langchain.text_splitter import CharacterTextSplitter loader = DirectoryLoader("./faq_docs/", glob="*.md", loader_cls=TextLoader) docs = loader.load() splitter = CharacterTextSplitter( separator="\n\n", chunk_size=200, chunk_overlap=20, ) chunks = splitter.split_documents(docs) print(f"切分后共 {len(chunks)} 个片段")这里解释一下为什么这么切:内部 FAQ 的一条回答通常就是三五句话,200 字以内可以覆盖;如果切得过大,检索回来的片段里会混入无关信息,模型容易引用错。重叠 20 字是为了防止关键句刚好被切断在边界上。你也可以根据实际文档长度调整,但一般不建议把单块拉到 500 字以上。
3.2 一次性写入向量库
切块完成后,下一步就是做向量化并写入本地向量库。这里我用轻量中文向量模型,它比较适合中文场景,对语义相似度的支持也不错,CPU 上也能直接跑。
from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5") vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./faq_db", ) print("向量库已写入 ./faq_db")第一次运行会下载模型权重,之后都会走缓存。向量库就是一个本地目录,后续不需要再维护任何服务进程。这里有个容易被忽略的点:嵌入模型的选择对中文检索效果影响巨大,我这里选的是轻量中文模型路线。如果你手头有更适合自己领域语料的模型,可以直接替换,代码无需改动。
向量化完成之后,用一句查询来测试召回结果:**
question = "员工忘记密码怎么重置?" results = vectorstore.similarity_search_with_score(question, k=3) for doc, score in results: print(score, doc.page_content[:100])score 越小通常代表距离越近。这个测试步骤特别重要,别急着接大模型,先确认检索这步靠谱。如果返回的前三名跟问题明显不搭,后面生成得再好也白搭。
3.3 用大模型生成最终答案
检索没问题后,接入大模型。提示词是整个环节里最不能省心的地方。我的做法是,把检索到的片段按顺序编号,塞给模型,明确要求“只能依据资料回答,资料没有的要说明不知道”。
def build_prompt(question, docs): context = "\n\n".join( f"【资料{i + 1}】{doc.page_content}" for i, doc in enumerate(docs) ) prompt = ( "你是一名公司内部知识助手。请根据下面的资料回答用户问题。\n" "要求:1. 只能使用资料中的信息,不得编造;" "2. 若资料中找不到答案,直接回答'在现有资料中未找到相关信息';" "3. 在回答末尾标注使用了哪些资料编号。\n\n" f"{context}\n\n用户问题:{question}" ) return prompt # 你只需要把 chat() 换成你实际使用的大模型 SDK answer = chat(build_prompt(question, results)) print(answer)为什么 prompt 里要写“不得编造”?因为大模型本质是语言模型,它没有“查询数据库”的能力,不约束就会一本正经地胡说。内部员工不会质疑看到的答,但这恰恰是最危险的。所以我会让模型把引用的资料编号标出来,方便员工回到原始文档核对。
到这一步,一个能跑通的最小闭环已经完成。整个流程就是“读取文档 → 切块 → 写向量库 → 检索 → 拼 prompt → 生成回答”。你可以再包一个命令行入口,方便日常使用:**
python qa_demo.py "怎么预约会议室"3.4 从“能跑”到“能用”:增量更新与轻量界面
最小闭环跑通之后,你很快就会遇到另一个需求:FAQ 是活的,规则会改。新政策发下来之后,向量库怎么更新?
最简单粗暴的办法是重新执行一次完整入库脚本,对几百条文档来说不过几秒钟,毫无压力。如果你想优雅一点,可以单独追加新内容:**
new_chunks = splitter.split_documents(new_docs) vectorstore.add_documents(new_chunks) vectorstore.persist()如果某条旧信息已经过期,需要删除,MVP 阶段我建议直接重建整个库,不要纠结单条删除。原因很简单:文档量不大,重建成本低,重建还能顺便清理掉历史脏数据。
至于界面,我最推荐的是先不做界面,直接用命令行。等你确认效果稳定了,再套一个极简的 HTTP 服务或者用开源的应用框架写一个内部页面,半天就能搞定。过早把精力花在界面上,很容易陷入“看起来很美、实质不准”的尴尬。
3.5 上线前的三分钟自测清单
我每一次搭建完这套系统,都会拿一组种子问题做快速验收。这里直接把自测清单分享给你:**
- 检索测试:随意说一个常见问题,看返回前三名是否包含正确答案。
- 变体测试:用同义改写的方式问同一个问题,看是否还能命中。
- 幻觉测试:问一个明显不在资料范围内的问题,看模型是否懂得说“不知道”。
- 溯源测试:确认回答后标注了来源编号,别人能点回去核对。
如果你做的系统在这些测试里表现都正常,就可以放心交给第一批同事使用了。
4. 常见问题与排障实录
这部分是我在实际搭建和帮别人排障过程中,遇到最多的几个问题。每一个都对应一个真实的坑,按顺序排查能省很多时间。
4.1 为什么检索出来的内容“文不对题”
最常见的情况是:问“年报什么时候提交”,检索出来的却是“年假申请流程”。表面上都是“年”,语义完全不是一回事。遇到这种情况,我建议你先单独跑一下检索脚本,把返回的 top3 片段打印出来,判断到底是“检索错了”还是“生成错了”。这一步很重要,避免你盲目调 prompt 而忽略真正的问题。
如果检索本身就错了,优先检查三件事:第一,切块粒度是不是太大?500 字的块里可能只有一句话跟问题相关,向量表示被无关内容稀释了。第二,嵌入模型是否适合你的语料?通用模型对专业术语理解有限,建议换成更大或领域更对口的中文模型再试。第三,问题本身的措辞与资料差距过大?比如你文档里写的是“权限开通”,用户问的是“我要能登录后台”,这种语义跨度对轻量模型来说确实有难度。
我自己常用的补救办法是:在 FAQ 里给每条常见问题增加三到五个同义关键词标签,入库时把这些关键词拼进问题里一起向量化。比如“权限开通”可以增加标签“登录”“账号”“后台”“访问不了”。这样就能把语义距离拉近很多。
4.2 回答为什么总是“一本正经地胡说八道”
大模型幻觉的来源,是它本质上在做“最顺的文字接龙”,而不是“查表”。如果你 prompt 里没有强约束,它会把检索到的片段当作灵感,然后展开想象,补出文档里根本没有的细节。
我的处理手段有三层。第一层是提示词强约束,明确告诉它“资料中没有就回答不知道”;第二层是要求输出来源编号,方便人工核对;第三层是把生成参数里的温度调低,让模型不要发挥。温度设置为 0.1 左右,基本不会影响回答质量。
如果你发现模型还是会把资料理解错,比如检索到的是打卡规则,它却回答成了加班费,那问题就出在资料本身的组织方式上。尽量保证每一条 FAQ 只有一个主题,别把多件事写进同一个小片段。
4.3 环境依赖与模型下载的坑
首次运行向量模型需要下载权重文件,这一步往往比写代码还耗时。即使总量不大,如果团队的下载通道较慢,也会等很久。解决办法是先在一台机器上下载好,把缓存目录打包发给其他部署环境;或者让构建脚本从内部共享目录读取权重文件,避免每台机器都重复下载。
还有两个细节:第一,向量库的持久化目录不要随意删除,删除后需要重新执行入库脚本重建,虽然不复杂,但没必要。第二,如果有两个进程同时打开同一个 Chroma 目录,可能会出现锁冲突导致写入失败,部署时不要同时启动多个 demo 进程。
4.4 从 Demo 到内部服务,还差哪些事
如果你真的想把这套系统放上线给全公司用,我建议你先补齐四件事:权限隔离、数据合规、内容运营、效果评估。
第一是权限隔离。有些制度只对特定人群可见,MVP 阶段可以给每个片段打上部门标签,检索时按用户所属部门过滤。
第二是数据合规。公司内部资料往往包含员工姓名、薪资、流程细节等敏感信息。如果生成环节调用的是外部大模型服务,务必确认数据传输链路是否合规,在无法确认的情况下,优先选择本地部署模型或者公司认可的私有化方案。
第三是内容运营。FAQ 内容是会过期的。没有专人维护的 RAG 系统,三个月后就会变成新的“过时文档”。至少要在界面上显示每条回答对应的来源文档更新时间,让使用者心里有数。
第四是效果评估。维护一组 10 到 20 条的种子问题,每次改完文档后跑一遍,对比检索命中率是否下降。这比任何 fancy 的监控面板都实用。
我个人在实际操作中的体会是,搭这套系统时永远先把“检索验证”放在“调 prompt”前面。绝大多数效果不佳的问题,根因都在召回环节,而不是生成环节。先用种子问题跑一遍检索列表,确认前三名是准的,再去调整回答风格和界面体验,整个开发周期会顺畅得多。最后再分享一个小技巧:不要一开始就追求百分之百准确,先让同事用起来,把他们的真实问题记录下来,再把这些问法补成 FAQ 关键词或新增条目。系统的效果是迭代出来的,不是一次写代码写出来的。