企业级 RAG Agent:从零构建一个带评估闭环与权限治理的知识库问答系统

最近从头搭了一个企业级 RAG Agent 知识库问答系统,经历了从 POC 到”基本合格”的完整过程,中间还经过了三轮外部审查。这篇做个系统性的技术复盘,把架构、难点、以及最想分享的工程经验记下来。

项目是什么

一句话:把公司内部文档变成可问答的知识库,回答带引用溯源,权限按部门隔离。

解决的问题很现实——企业文档散落在 PDF / Word / Markdown 里,员工检索困难、回答没有依据、权限难隔离。这个系统把文档统一摄入建索引,员工用自然语言提问,系统从文档里检索依据并[n] 引用标注回答,回答内容严格限定在知识库范围内。

最终形态:

登录页

架构与核心链路

1
2
3
4
5
Web 管理台 (Vue3) ── SSE/REST ──► API 层 (FastAPI) ──► Agent 编排 (LangGraph)

摄入链路:上传/扫描 → 解析(OCR) → 结构分块 → 脱敏 → 向量化 → Milvus

LLM 网关 (OpenAI 兼容) · Langfuse trace · Prometheus

核心问答链路:上传/扫描文档 → 解析(Docling/PaddleOCR)→ 结构感知分块 → 向量化 → 混合检索 → Agent 编排 → 流式回答 + 引用

几个关键设计:

  • 混合检索:Milvus 稠密向量(BGE-M3)+ 内置 BM25(中文分词)+ RRF 融合,再经 bge-reranker 精排。指令权限的部门 filter 直接下推到向量库查询阶段执行,禁止”查全量再过滤”
  • Agent 编排(LangGraph):意图路由(知识问答/总结/闲聊拒答/需澄清)、CRAG 反思(检索置信度低自动改写重查)、多跳拆解、主动改写、多查询扩展、术语表变体(”发版”→”发布”)、工具调用(Agent 可执行只读 SQL 查询统计类问题)。
  • RBAC 三角色:超级管理员(全权)/ 部门管理员(负责 N 个部门,只能管理负责部门的文档、只答负责部门)/ 普通用户(仅问答)。权限统一入口,QA 检索与文档管理共用,避免权限判断散落。

三个最想分享的工程点

1. 评估驱动的开发(不是”调 API 拼 demo”)

这是整个项目最核心的方法论:先建评估,再改代码

  • 黄金集 30 条(覆盖四类意图 + 各业务域)+ 检索评估(recall@k / MRR)+ RAGAS 生成评估(faithfulness / answer_relevancy / context_precision)
  • 回归门禁:评估结果对基线,指标劣化 >5% 直接 exit 1,阻断合并
  • 反馈回流:用户在界面上点踩的回答,自动沉淀为评估用例,下次回归自动带上真实失败样本

举一个实际的优化过程:评估里 answer_relevancy 得分偏低(0.636),我诊断发现低分集中在”机制解释类”问题——回答发散,把跟问题无关的实现背景也展开了。于是给系统提示词加了一条”直接回答核心、只输出与问题直接相关的内容”的规则,全量验证后 0.636 → 0.681,context_precision 0.789 → 0.918。

这个过程还让我发现一个关键事实:RAGAS 的 judge 单次评估方差 ±0.1——同一段回答跑两次可能差 0.08~0.3。所以对比指标必须多次取均值,否则优化结论可能纯属噪声。这条经验写进了文档,避免后面的人被方差骗。

2. 安全审计不是一次性的

项目分别做了自审计两轮外部 AI 审查,一共发现并修复了 38 个问题。这里列几个印象最深的(都是真实可利用的漏洞):

  • 部门 filter 注入:权限过滤的部门名直接拼进 Milvus filter 字符串,恶意部门名 a"] or doc_id != "x" or department in ["a 可以构造永真表达式绕过部门隔离读全库。修复:部门名白名单 + 注册/上传/管理三入口校验。
  • 空部门用户全库可见:注册时部门留空 → 过滤生成空串(=不过滤)→ 普通用户能搜到所有部门数据。修复:空部门视为”零可见”,生成永假过滤表达式。
  • 只读 SQL 工具表白名单绕过:Agent 的 SQL 工具用正则检测表名,但 SELECT * FROM documents, users 这种逗号分隔能被绕过去查 users 表。修复:完整解析 FROM/JOIN 子句逐个校验。
  • worker 崩溃丢任务:摄入 worker 读到消息后、确认前崩溃,消息永久滞留 Pending List,重启也不重投。修复:启动时用 XCLAIM 认领滞留消息重新入队。

外部审查的价值在于它真的去读了代码并给出了利用路径——被审出问题不丢人,丢人的是审出了问题不改。我把审查记录(FIX_LIST、IMPROVEMENT_PLAN、复审报告)都留在仓库里,面试和别人 review 的时候直接能看完整的”发现问题 → 修复 → 验证”闭环。

3. 工程化基建(测试、迁移、CI,一样不少)

简历项目最容易被一眼看穿的地方就是只有功能没有工程。这个项目补全了:

  • 173 个测试全绿 + ruff / mypy / format 三道门禁全零错误
  • Alembic 数据库迁移(不是无脑 create_all)——空库实测 upgrade head 建出全部 7 张表
  • CI 流水线(ruff → mypy → pytest → 前端构建 + 手动触发的评估回归,因为评估依赖真实 LLM key,外部波动不阻断主干)
  • 16 份文档与代码同步(架构、需求、部署手册、运行手册、故障 Runbook、CHANGELOG、贡献指南)
  • 运维 Runbook 记录了 9 个真实踩过的坑(含症状→诊断→处置→预防)

界面与最终效果

问答工作台(流式回答 + 引用溯源)

文档管理(按角色可见)

踩过的几个值得记的坑

  1. redis-py 8.x 默认超时改成了 5 秒——把 Stream 的阻塞读(block=5s)全部打断,worker 每轮都在报错刷日志。显式设 socket_timeout=None 解决。
  2. Windows 上 uv run 和 uvicorn 会争用 websockets 的 pyd 文件锁——服务进程必须用 .venv 直接调解释器。
  3. Git Bash 传中文参数会被按 GBK 编码——curl 传中文部门名存库直接乱码,测试和脚本统一用 Python requests。
  4. RAGAS judge 方差 ±0.1——上面说过,对比必须多次取均值。
  5. 前端一个隐蔽 bugJSON.parse(localStorage.getItem('rag_user') || '{}') 在值是字符串 'null' 时返回 null,再取 .username 直接抛 TypeError,整个组件树卸载白屏——当时是截博客预览图时发现的,做了安全解析修复。

最终数据

  • 功能:摄入(8 格式 + OCR)/ 混合检索 / Agent 编排 / RBAC / 脱敏 / 审计 / 评估闭环 / Connector 自动摄入
  • 质量:173 测试全绿,ruff / mypy / format 三零,GitHub Actions CI
  • 治理:三轮外部审查 38 个问题全闭环,16 份文档同步,Alembic 迁移
  • 评估基线:recall@5=0.852 / MRR=0.796 / faithfulness=0.931 / answer_relevancy=0.681 / context_precision=0.918

仓库:https://github.com/FrostLeafKEE/rag-agent

如果你也在做 RAG 相关的项目,最想建议的一点是:先建评估,再动代码——没有度量,一切”看起来变好了”都只是幻觉。