
说实话三分钟搭建这个说法确实容易让人怀疑——RAGRetrieval-Augmented Generation检索增强生成听起来像是个高大上的技术方向怎么也不像三分钟能搞定的东西。但如果你愿意把范围收窄到公司内部FAQ问答系统这样一个非常明确的场景我可以说这个时间基本能实现甚至还能留出两分钟喝口茶。我前阵子正好帮某团队搭过一套内部问答系统。需求特别朴素公司规章制度散落在十几个文档里新员工入职要到处问人行政同学被同样的问题反复轰炸。他们最初的诉求不是上一个AI系统而是能不能让一个机器人替我们回答那些带标准答案的问题。这个定位很重要——FAQ系统的核心根本不是生成而是找到RAG恰好就是干这个的。这套系统我用了大概一个晚上从零写出来但真正的核心代码跑通第一版确实只花了很短的时间。这篇文章我就把整个思路拆开讲从为什么内部FAQ选RAG最优、技术选型怎么配最省事到具体代码怎么过一遍、上线后有哪些坑最后再聊聊怎么让答案不光是生成出来而是可信地检索出来。1. 先搞清楚RAG解决什么以及FAQ场景为什么非它不可1.1 内部FAQ的几个真实痛点不是没有模型而是模型不知道做内部问答系统最直接的念头是直接扔给大模型拿公司制度去回答回答不上来就说不知道。但这里有个致命问题通用大模型在训练时根本没见过你公司的内部资料。你问它年假折算怎么算它只会告诉你劳动法通用的说法而公司内部可能有完全不一样的规定——比如入职满半年但不满一年年假按转正天数折算这类特殊逻辑。模型不知道这些就会一本正经地编。这就是经典的幻觉问题。很多人觉得幻觉是模型质量问题但在FAQ场景里幻觉的根源是知识缺失。你给模型一本内部手册它就能回答不给它只能瞎编。所以问题变成怎么把手册喂给模型同时不每次把整个手册塞进上下文——成本高、速度慢、还可能超过模型上下文限制。1.2 RAG在这里扮演的主角检索那段才是重点生成只是最后一公里RAG的英文全称是Retrieval-Augmented Generation中文叫检索增强生成。一句话解释先从一个资料库里把跟问题最相关的几个片段捞出来再把这些片段连问题一起交给大模型让模型基于这些片段作答。内部FAQ场景里检索这一步才是灵魂。因为FAQ的问题绝大多数是封闭型问题——答案就藏在文档的某个角落关键是你能不能把那个角落找出来。这跟我们平时用搜索引擎是一个逻辑你不需要搜索引擎生成答案你需要它帮你找到最相关的页面。RAG只是往前走了一小步找到页面之后再让模型把页面里的关键信息提炼成一段人话。所以我的一个观点是如果你要做内部问答系统技术重心不应该放在怎么让模型说得更漂亮上而应该放在怎么让检索结果更准上。检索准了答案基本就对了检索不准模型再厉害也只能对着不相关的资料强行发挥结果自然很烂。2. 一分钟技术选型追求最简和够用的组件搭配RAG系统通常由三部分组成嵌入模型、向量数据库、大模型。做技术选型时最容易犯的错是性能焦虑——总觉得要用最新的模型、最重的数据库才靠谱。实际跑下来内部FAQ这种量级的场景根本扛不住重型方案反而轻量方案体验极好。2.1 嵌入模型中文场景优先看小尺寸模型嵌入模型的作用是把一段文字变成一串数字向量让语义相近的文字在向量空间里靠得近。FAQ检索的准确率很大程度上取决于嵌入模型的质量。对于中文环境推荐直接用开源的中文嵌入模型BAAI/bge-small-zh或BAAI/bge-large-zh。我实测下来small版本对内部文档的检索效果已经足够好而且速度快、显存占用小。当然如果你用的是OpenAI的接口也可以选text-embedding-ada-002效果稳定但要考虑数据不外传的需求。公司内部资料往往比较敏感本地嵌入模型是更稳妥的选择。一个我很想强调的细节嵌入模型一旦确定轻易别换。因为索引库里的所有向量都是用这个模型生成的换了模型意味着所有旧向量都得重新生成、重新建库。在早期阶段这会浪费很多时间。2.2 向量数据库从小而美的Chroma开始向量数据库是存向量、做相似度检索的地方。常见的方案有Chroma、FAISS、Milvus、Elasticsearch加向量插件等。对于三分钟搭建的目标直接选Chroma别犹豫。原因很简单Chroma支持pip直接安装、无需额外起服务、数据存在本地文件夹对轻量项目来说零运维。Milvus功能强大但需要依赖Docker部署一套分布式服务这对一个内部小FAQ系统来说纯属杀鸡用牛刀。FAISS虽然没有服务依赖但对索引的管理需要手写更多代码。Chroma的另一个好处是API设计比较直观collection.add()添加文档collection.query()做检索两条命令就能跑通全链路。2.3 大模型API调用与本地部署的平衡点大模型负责最后一步——把检索到的片段转化成语义通顺的回答。这个组件在选型时有两条路第一条路是调用商用API。好处是省事、效果好、不需要GPU。缺点有两个一是内部资料会经过第三方服务器敏感数据请务必谨慎二是每次调用都有网络延迟和费用高频使用成本不低。第二条路是部署本地模型。推荐用vLLM框架本地部署蒸馏过的对话模型比如Qwen2.5-7B-Instruct这类。7B级别在当前硬件条件下用一块消费级显卡就能跑如果公司有旧工作站加一块二手卡也划算。本地部署的好处是数据完全在自己手里延迟稳定长期看成本可控。我的建议是开发阶段用API测试通过、确定要上线时再切换本地模型。这样既不影响快速迭代又能保证最终方案的私密性和稳定性。3. 三分钟主流程从文档目录到可提问的FAQ服务现在进入正题。以下是我验证过多次的一套代码流程核心依赖只有三个chromadb、langchain或直接用原生逻辑、sentence-transformers。如果你有一定基础也可以完全不用LangChain直接手写调用逻辑反而更透明。3.1 文档准备与分块切得不科学后面都白搭文档准备通常是最耗时的环节但你可以先不管格式把docx、pdf、txt统一抽取成纯文本再交给分块逻辑。分块是RAG系统里最容易被低估的步骤。块太大检索结果里掺杂大量无关内容模型容易被带偏块太小语义不完整检索时匹配不到关键信息。内部FAQ场景建议把块大小控制在200~400个字。太短的制度条款可以合并太长的章节按自然段落断句。这里要注意不能只靠固定字符数硬切那样会把一句完整的话从中间截断。推荐的做法是按段落先切如果段落过长用标点符号句号、分号作为次级分隔符再切。简单说就是能整段就整段整段太长就按语义断点切。3.2 把文档写入向量库建立索引就是这几行代码用Chroma建立索引的代码大致如下。我把它整理成了可复制可运行的最小版本from chromadb import Client from chromadb.config import Settings from sentence_transformers import SentenceTransformer # 1. 初始化嵌入模型 embedder SentenceTransformer(BAAI/bge-small-zh) # 2. 初始化Chroma客户端数据存本地 client Client(Settings(chroma_db_implduckdbparquet, persist_directory./faq_db)) collection client.get_or_create_collection( namefaq, embedding_functionNone # 这里我们在外部手动生成向量 ) # 3. 添加文档chunks是[(doc_id, text, metadata)]列表 for doc_id, content, meta in chunks: vec embedder.encode(content).tolist() collection.add( ids[doc_id], embeddings[vec], documents[content], metadatas[meta] ) print(f索引构建完成共 {collection.count()} 个片段)这段代码做了什么事说白了就是把文档切成小块每块转成一串向量然后和原文一起存进Chroma。meta可以存来源文档名、页码、章节标题等信息后面做引用溯源会非常有用——这是很多人一开始忽略的细节。3.3 查询链路检索、拼接、生成的标准姿势查询链路是整个系统的大脑。用户输入一个问题系统先检索出最相关的TopK个片段再把这些片段和问题组装成一个带上下文的提示词交给大模型生成答案。def ask(collection, question, k4): # 1. 问题向量化 q_vec embedder.encode(question).tolist() # 2. 检索TopK相关片段 results collection.query( query_embeddings[q_vec], n_resultsk, include[documents, metadatas, distances] ) # 3. 拼接上下文 context \n\n.join([ f[片段{i1}] {doc} for i, doc in enumerate(results[documents][0]) ]) # 4. 组装提示词交给LLM生成答案 prompt f请根据以下资料回答问题。如果资料中没有相关信息请直接回复资料中未找到相关说明请咨询行政部门。 资料 {context} 问题{question} 回答 return call_llm(prompt), results[metadatas][0]这段代码里有几个值得反复推敲的地方。第一TopK的选择。k值太小容易漏k值太大容易混入不相关内容。FAQ场景我先用k4起步再按效果调整。有些模糊问题可以适当升到k6但别贪多——上下文越长模型越容易迷失重点还增加计算成本。第二提示词是后续调优的主要阵地。上面这段提示词里特意加了一句兜底话资料中未找到相关说明请回复……很多人会忽略这句但这句话恰恰是抑制幻觉的第一道防线。没有它模型哪怕没资料也会硬编一段听起来合理的答案。第三调用LLM的姿势。这里我用call_llm()做了抽象。速度快、又能控制提示词的方案是让call_llm直接读取OpenAI兼容的API格式或者调用本地vLLM提供的OpenAI接口。只要把模型地址改一改就能无缝切换API和本地模型。这就是选型阶段预留的弹性。4. 上线运行后真正的工程问题权限、更新与失败兜底代码跑通只是第一步真正让内部FAQ系统能长期用的往往是代码之外的工程细节。4.1 公司内部部署的最小权限方案内部FAQ系统有个容易被忽略的敏感点它承载的是公司内部文档不能对公网完全开放。但既然说最简就不建议你上一大套身份认证系统。我的做法是先用最简单的方式把系统部署在内网通过公司内部单点登录做前端代理验证后端API不做额外鉴权。如果公司没有现成的单点登录那就先用共享密钥——在请求头里约定一个固定token前端登录态校验后后端只校验token是否匹配。为什么够用因为内网部署本身已经筛掉了外部访问token只是防止内网里的无关同事乱调API。当然后续如果要对不同部门做权限区分那就不是三分钟能解决的事了要单独规划。4.2 FAQ答案过时怎么办重建索引比精准更新更重要内部FAQ和公开问答系统最大的不同是公司制度会变。员工手册每年更新假期规定临时调整。如果你的索引库不跟着更新模型就会拿旧版本资料回答新问题这在行政场景会引发很多麻烦。最容易想到的方案是更新的时候删掉对应片段再重加。但实际操作中发现定位哪个片段属于哪次更新非常麻烦而且容易漏。更稳健的做法是更新时直接删掉来源文档相关的旧记录重新添加新版本的全部文档。如果你用的是Chroma删除可以按metadata筛选collection.delete(where{source: 员工手册_2025版.pdf})随后重新跑一遍索引脚本把新版本资料全部灌进去。这比追着增量更新要省心得多。如果文档总量不大几千个片段内全量重建的耗时也就几十秒完全接受得了。4.3 检索不到内容时的兜底回复设计这是整个工程里最影响体验的细节之一。我见过很多RAG系统检索结果不好时模型要么硬着头皮说一堆似是而非的话要么说一堆推卸责任的废话。两者都很糟糕。前者让用户误以为答案就是那样后者让用户觉得系统完全没有价值。正确的做法是三层兜底第一层检索阶段如果TopK相似度分数普遍很低——也就是问题和所有资料都不搭边——直接不调用大模型返回已配置好的兜底话术。第二层如果检索到了一些内容但分数不够高把内容拼进提示词同时明确告诉模型资料可能不相关请谨慎作答并保留那条未找到说明的指令。第三层如果大模型生成了内容但在后处理中发现没有引用任何资料片段说明模型大概率在自由发挥。这时候可以强制改成资料中未找到相关说明作为最终输出。听起来像三道保险其实每道也只是几行代码的事。但它决定了用户对这个系统的信任程度。一个经常一本正经胡说八道的问答系统比一个老实地承认不知道的系统更让人反感。5. 提升实用性的两个关键点重排与引用溯源聊到这里系统已经能回答大部分问题了。但如果你要在团队里认真推广有两件事非做不可一是让检索结果再精确一层二是让答案可回溯、可信赖。5.1 为什么常规TopK检索在FAQ场景会失效Chroma的向量检索本质是找向量空间里距离最近的几个片段。它擅长语义匹配却不擅长判断哪个片段最权威。典型的问题是当你问年假怎么算时向量库里可能有三段话都说到了年假福利体系但只有一段包含了具体的折算公式另外两段只是提及。单纯按相似度排序公式那段很可能排到第二位、第三位最后被拼进来的信息不够完整。解决办法是引入一个轻量级重排rerank环节先粗召回retrieve8~10个候选片段再用一个交叉编码器模型cross-encoder对每个候选片段和原始问题做逐字符级的语义匹配打分最后按新得分重新排序取得分最高的TopK片段送入大模型。from sentence_transformers import CrossEncoder rerank_model CrossEncoder(BAAI/bge-reranker-base) def rerank(question, candidates, top_k3): pairs [(question, doc) for doc in candidates] scores rerank_model.predict(pairs) ranked sorted(zip(candidates, scores), keylambda x: x[1], reverseTrue) return [doc for doc, score in ranked[:top_k]]这个环节对内部FAQ的改善非常明显。用不用重排答案的准确率经常就是60分和90分的区别。而且bge-reranker-base这个模型很轻量跑一个问题的重排耗时只有几十毫秒对内部系统来说完全可接受。5.2 引用溯源让答案可信看得见比说得好更重要在第二步给Chroma添加数据时我们专门在metadata里塞了source、page等字段。这些字段的用处在展示答案时一下子体现出来。理想的答案输出格式应该是这样的根据《员工手册》第五章第3节入职满半年但不满一年的员工年假按转正天数折算。 来源 - 员工手册_2025版.pdf第56页 - 请假制度说明.docx第2页不要嫌这个引用丑。在公司内部场景里引用链接意味着同事可以去原始文档里核对行政同学也不用一遍一遍复制同样的答案。一个带出处的答案信任度远远高于一段魔术般出现的完美回复。实现上也不复杂。前面代码中ask()函数返回了results[metadatas][0]。后端API把元数据和答案一起往前端传前端用一个固定模板渲染即可。如果某些答案引用了多个片段就按片段顺序把这些片段的来源全部列出来用户自己判断以哪份资料为准。6. 我踩过和使用后的几个关键体会6.1 chunk_size不是越大越好保住一个片段只讲一件事我一开始做FFAQ时犯过一个典型错误为了省事把整个章节切成一个片段。结果就是用户问转正流程需要什么材料检索回来的片段把转正流程、试用期考核标准、离职交接全部混在了一起。模型在这个混合上下文里杀红了眼挑了一段其实和问题不太契合的内容来回答看着挺像那么回事细看完全答偏了。后来我把分块策略改成一个片段就讲清楚一个事。具体做法是先按章节标题粗切再把每个章节内的段落按语义聚合聚合条件是该段落群讨论的是同一个主题。这个主题分隔可以用LLM帮你判断也可以自己人工看一遍。内部文档通常不多人工看一遍的成本其实很低而且效果极其可靠。6.2 评估问答效果的最低成本方法构建一份20~30条的标准问题清单系统上线前最怕什么就怕你以为效果不错但用户在真实环境里问的问题跟你想的完全不一样。我的做法是准备一份标准问题清单涵盖四类「能直接答上的」「需要从多个文档拼接答案的」「文档里有但表述完全不同」「文档里完全没有的」。跑一遍系统记录每一类的通过情况。特别注意第二类和第三类它们最能反映检索和重排的真实水平。这套评估不需要什么复杂的RAG评测框架Excel拉个小表把每个问题的答案手工标记通过/不通过就行。20~30个问题跑完系统的能力上限和短板基本就清楚了。之后每次调整分块参数、嵌入模型、重排模型也都跑同一套题目对比分数决断非常客观。6.3 这套架构还能怎么延伸从FAQ起步你会发现这套架构很快会从问答系统演变成内部知识助手。常见延伸方向有一是接入更多数据源。比如把公司内部Wiki、工单系统历史回复、会议纪要等全部接入。只要把文档转成文本、按同样逻辑分块剩下的链路完全复用。这块的关键在于设计好metadata里的来源体系否则最后会变成一锅数据大杂烩。二是加语义缓存。对高频重复的问题开启缓存命中相同或高度相似的问题时直接返回上一次的答案能大幅降低接口费用和延迟。实现思路是在LLM前加一层向量检索把历史问题和答案存进另一个Chroma库里。三是主动推送。既然知识库已经向量化可以做一个小工具根据同事正在编写的文档实时推荐相关制度条款。这个比较进阶但对内部协作效率的提升非常有帮助。写在最后的体会回头再看三分钟搭建内部FAQ系统这件事我越来越觉得技术上的时间成本确实不高真正消耗时间的反而是整理文档和测试迭代这两件事。文档如果不整理系统跑得再快也是垃圾进、垃圾出测试如果不做你根本不知道今天说的效果不错到底是主观感受还是客观评价。我也建议所有打算上手RAG的人不要一上来就买一堆重型组件先用最简单的方式把端到端链路跑通哪怕只是跑30条标准问题也比在纸面上研究一万字架构图有用得多。这个FAQ项目是我做过最轻量的RAG应用但也是让我对检索增强这四个字理解最深的一次实践。如果你准备在自己公司复刻这套方案建议从行政部、IT支持这类问题高度标准化、答案长期稳定的场景切入。这类场景的FAQ答案不容易过时数据量小领导能快速看到效果后面推广起来阻力也小得多。系统跑上一个月收集一批真实问答记录后再迭代知识库和提示词整个团队就会自动把问人改成问系统了。