一个企业级RAG知识库的完整复盘:从技术选型到生产架构

3 阅读9分钟

一套企业级 RAG 知识库:从 Demo 到生产架构的完整复盘

一个 Python 全栈开发者的技术实践。从选型到上线,从踩坑到优化,全记下来了。


为什么我要自己做一个生产级 RAG 系统

大模型应用这两年越来越热。知识库、智能客服、文档问答——企业什么都想做,需求到处都是。

但聊过几个潜在合作方之后,我发现一个很尴尬的问题。对方第一个问题永远是这个:

"你有没有做过的案例?"

没有。一个能跑通的 Demo 说服不了任何人。Chroma + OpenAI API 五分钟搭起来的东西,我自己都不好意思叫作品。

所以干脆按真实企业需求的标准,从头搭一套完整的 RAG 知识库。不是为了交差,就是想看看自己能不能搞定——文档处理、检索优化、多租户、权限、性能、成本,全过一遍。


技术选型:Demo 思维和企业思维是两套东西

最常见的 Demo 方案

前端:Vue3 + Element Plus
后端:FastAPI
AI 框架:LangChain
向量数据库:Chroma
大模型:OpenAI GPT-4

能跑。但仅限于演示。企业场景一来,每个环节都出问题。

企业场景到底哪里不一样

数据安全。 很多企业文档不能出境。OpenAI 用不了,你得准备国产模型或者本地部署方案。

数据量。 Chroma 处理几千条还行。几十万份文档上去,检索延迟直接爆炸。

并发。 不只是你一个人在用。中型企业几十个人同时查,系统不能排队。

权限。 不同部门看不同文档,不同角色不同操作。Demo 里根本没有考虑。

运维。 文档要更新,模型要监控,成本要控制。不是跑起来就完事了。

最终选型

层次选型为什么
前端Vue3 + Element Plus组件丰富,企业后台风格
后端FastAPI + Celery异步处理文档解析,避免超时
AI 框架LangChain + LangGraph复杂检索流程需要状态管理
向量数据库Milvus几十万文档,Chroma 扛不住
大模型本地布署 + DeepSeek API敏感数据走本地,普通查询走云端
文档解析Unstructured + MinerUPDF 里的表格和图片不能丢

选型第一天就意识到:企业项目不是 Demo 的放大版。是另一个物种。


文档处理:比我想的难了不止十倍

踩了四个坑

坑 1:扫描件 PDF

企业文档里大量扫描件。不是文字型 PDF,是纯图片。得先 OCR。中文 OCR 对正式文档还行,但一行错一个字,检索效果就大打折扣。

解法:PaddleOCR 做中文识别,对关键字段(合同金额、日期、当事人名称)做二次校验。

坑 2:表格数据

合同里的金额表格、赔偿标准表,常规 PDF 解析器直接当乱码扔了。用户问"违约金怎么算",AI 根本找不到那条数据——数据在表格里,解析器压根没读出来。

解法:MinerU 解析 PDF,能识别表格并保留结构。表格转成 Markdown 格式存入向量库,检索时和文字同等对待。

坑 3:文档层级结构

法律文书有"条→款→项"的层级,合同有"第一章→第一节→第一条"。RecursiveCharacterTextSplitter 按字符数切,用户问"第三条第二款是什么",检索出来的片段牛头不对马嘴。

解法:自定义 Chunker,按文档层级结构切分,每个 chunk 保留父级标题路径:

class HierarchicalChunker:
    """按文档层级结构切分"""
    
    def split(self, document: str) -> List[Chunk]:
        sections = self._parse_hierarchy(document)
        chunks = []
        for section in sections:
            context_path = " > ".join(section.ancestors + [section.title])
            chunks.append(Chunk(
                content=section.content,
                metadata={"context": context_path, "level": section.level}
            ))
        return chunks

坑 4:批量处理性能

一份文档解析+Embedding 大概 15 秒。2000 份同步处理,8 个多小时。

解法:Celery 异步任务队列,10 个 Worker 并行。WebSocket 实时推送进度。2000 份文档 25 分钟搞定。


检索质量:从 67% 到 92% 的四步优化

Demo 跑出来准确率大概 67%——问 10 个问题,6-7 个能命中相关内容。离可用差得远。

第一步:混合检索(67% → 78%)

纯向量检索对精确查询很弱。"民法典第 1043 条"这种,向量相似度不如关键词匹配。

同时跑向量检索和 BM25 关键词检索,然后按权重合并结果:

from langchain.retrievers import EnsembleRetriever

vector_retriever = milvus.as_retriever(search_kwargs={"k": 20})
bm25_retriever = BM25Retriever.from_documents(docs, k=20)

hybrid_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.3, 0.7]
)

第二步:查询重写(78% → 85%)

用户输入和文档表述差太远了。"那个赔偿怎么算" vs 文档里的"劳动合同解除的经济补偿金计算标准"。

用 LLM 把用户的口语问题转成正式检索词,必要时拆成多个子查询:

rewrite_prompt = """将用户的自然语言问题改写为更适合检索的表述:
1. 使用正式术语
2. 补充隐含上下文
3. 可以拆分为多个子查询(用 | 分隔)"""

# "那个赔偿怎么算"
# → "劳动合同解除经济补偿金计算标准 | 离职赔偿金计算方法"

第三步:Reranking(85% → 90%)

混合检索召回 20 条,但 Embedding 的排序不一定准。BGE-Reranker 做 Cross-Encoder 重排序,从 20 条精选 Top 5。这一步最直接——准确率从 85% 跳到 90%。

用 BGE-Reranker 对召回结果做二次排序,只保留最相关的几条:

from FlagEmbedding import FlagReranker

reranker = FlagReranker('BAAI/bge-reranker-v2-m3', use_fp16=True)

def rerank(query, candidates, top_k=5):
    pairs = [[query, doc.page_content] for doc in candidates]
    scores = reranker.compute_score(pairs)
    ranked = sorted(zip(candidates, scores), key=lambda x: x[1], reverse=True)
    return [doc for doc, _ in ranked[:top_k]]

第四步:多轮检索(90% → 92%)

复杂问题一步检索搞不定。"解除合同后竞业限制补偿金怎么算"——需要先检索"解除合同"条款,再检索"竞业限制"条款。

LangGraph 做多步检索状态机,把"分解→检索→生成"串成一个有状态的工作流:

from langgraph.graph import StateGraph

workflow = StateGraph(AgentState)
workflow.add_node("decompose", decompose_query)
workflow.add_node("retrieve", retrieve_step)
workflow.add_node("generate", generate_answer)
workflow.add_conditional_edges("retrieve", should_continue)

四步优化总结

策略准确率提升复杂度延迟增加
混合检索+11%基本无
查询重写+7%+0.5s
Reranking+5%+1s
多轮检索+2%+2-5s

架构设计:生产级真正看的是这些

技术选型和检索优化是基本功。但一个系统算不算"生产级",看的是下面这些:

多租户

不同企业用同一套系统,数据必须隔离。

检索时带上租户过滤条件,Milvus 的标量过滤在向量检索前就完成了数据隔离:

results = milvus.search(
    expr=f'tenant_id == "{tenant_id}"',
    data=[query_embedding],
    limit=10
)

tenant_config 表里存了每个租户的独立配置:llm_api_keymodel_namedaily_token_limitembedding_modelstorage_quota。租户启动时加载自己的配置,互不干扰。每个租户可以用不同的 LLM、不同的 Embedding 模型、不同的用量上限——对系统来说,它们就是完全独立的实例,只是共享同一套基础设施。

权限控制

文档级别的访问控制。管理员看所有,普通员工只看自己部门的。

检索时根据用户角色过滤可访问的文档,权限隔离在向量检索层面就完成了:

results = milvus.search(
    expr=f'access_level in {user.roles}',
    data=[query_embedding],
    limit=10
)

成本控制

API 调用不是免费的。每个租户的每日 Token 用量、费用追踪、80% 预警、超预算自动降级——这些都不是 Demo 会考虑的,但上线以后必须做。

具体做法是:每次 LLM 调用完成后,记录 tenant_idmodel_nameprompt_tokenscompletion_tokenscosttoken_usage 表。费用按模型单价实时计算——通义千问和 GPT-4 的单价差好几倍,不能用同一个公式。

每天凌晨跑一个定时任务,汇总每个租户当天的 Token 消耗和费用。预警分两级:

  • 80% 预警:当天用量达到预算的 80%,自动发通知给租户管理员,提醒注意用量。
  • 100% 熔断:当天用量超出预算,自动切换到本地模型处理后续请求,同时发送告警。月初自动重置计数器。

核心逻辑就这几行,每天统计完用量后跑一遍:

if daily_usage > budget * 0.8:
    send_alert(tenant_id, daily_usage, budget)
if daily_usage > budget:
    switch_to_local_model()

监控

Prometheus + Grafana 监控以下核心指标,每个都设了告警阈值:

指标阈值说明
API 响应时间 P95> 3s 告警端到端响应,含检索 + LLM 生成
向量检索延迟 P99> 500ms 告警Milvus 检索耗时
LLM 调用成功率< 99% 告警连续 5 分钟低于阈值触发
Embedding 队列积压> 100 告警Celery 任务堆积
租户日用量> 预算 80% 预警成本控制联动

Sentry 收集错误日志。不是"看起来没问题",是"有数据证明没问题"。

以上这些,没有一个是 Demo 阶段会考虑的问题。但它们恰恰决定了一个系统能不能真正投入使用。


我学到的东西

  1. Demo 是一回事,跑起来是另一回事。 文档处理、权限、多租户、监控、成本控制——每个模块单独看都不难,合在一起调试的时候全是问题。我花在联调上的时间比写代码还多。但恰恰是这些"麻烦事"才值钱,因为大部分教程不写这些。

  2. 检索优化是迭代出来的,不是一步到位的。 先上混合检索,再逐步加 Reranking 和多轮检索。每步都用真实测试集验证。

  3. 国产模型 + 云端 API 的混合方案,目前最务实。 敏感数据走本地,普通查询走云端。成本和效果平衡得最好。

  4. 全栈能力 = 独立交付能力。 前端、后端、AI 引擎、部署运维一手搞定,才能把一个想法变成能跑的系统。