投研智库 RAG Agent(一):项目背景、痛点与总体架构

投研智库 RAG Agent(一):项目背景、痛点与总体架构

「投研智库 RAG Agent」专栏第 1 篇。本专栏以连载形式完整记录一个企业知识库问答系统从 0 到 1 的构建过程------本篇讲清楚:为什么做、目标是什么、整体怎么设计。

专栏目录

  1. 项目背景与总体架构(本篇)
  2. 环境底座:Docker Compose 一键拉起向量库全家桶
  3. 导入链路(上):LangGraph 图设计与前置解析节点
  4. 导入链路(中):「长切短合」文档切分策略
  5. 导入链路(下):实体识别、BGE-M3 向量化与 Milvus 双集合
  6. 查询链路(上):实体确认三分支与双路召回
  7. 查询链路(下):RRF 融合、Rerank 精排与效果验证
  8. 工程化收官:FastAPI 双服务、两段式 SSE 与生产化改造清单

一、为什么要做这个系统:三个真实痛点

资管业务里,基金产品说明书、投研材料、制度合规文档分散在各处,客服和业务人员的知识获取一直有三个痛点:

  1. 实体表述不一致 :用户说的是基金简称、代码、口语别名,文档里是全称------直接检索经常"答非所问",更危险的是跨实体误答(问 A 产品答成 B 产品,在金融场景这是事故级问题);
  2. 口语化提问 vs 正式文档表达的语义鸿沟:用户问"这产品会不会亏",文档写的是"风险等级与回撤说明",普通向量检索召回不到;
  3. 裸 LLM 不可信:无溯源、会幻觉、无法拒答------企业场景需要"有依据才回答,没依据就承认不知道"。

所以这个系统的设计目标从第一天起就不是"能搜到一些 chunk",而是四个字:可信回答------不串实体、可拒答、可溯源、可观测。后面每一篇文章里的设计决策,都能回溯到这四个字。

把"可信回答"再拆细一层,它对应四条可验证的工程要求:

目标 工程落点 对应机制(后文详解)
不串实体 检索必须带实体过滤条件 导入侧实体标签 + 查询侧实体确认三分支(第 5、6 篇)
可拒答 置信度不足 / 证据不足时明确说"不知道" 0.85/0.60 双阈值 + 生成端证据约束(第 6、7 篇)
可溯源 答案能给出引用的 chunk 与原文出处 chunk 元数据(文件名/标题/part 序号)全程携带(第 4、5 篇)
可观测 每一步在干什么、卡在哪,运维能看到 节点级任务状态 + SSE 进度事件 + 中间产物落盘(第 8 篇)

二、技术选型总览

选型 一句话理由
编排 LangGraph StateGraph 固定编排图,控制流由工程代码决定,确定性场景可控优先
Web 框架 FastAPI 原生 async、Pydantic 校验、自带 Swagger,SSE 支持好
向量库 Milvus 2.5 dense + sparse 双向量字段、hybrid search、生产级
Embedding BGE-M3(本地,FlagEmbedding) 一个模型同时出稠密+稀疏向量,8192 token 长输入
精排 BGE-Reranker-Large(本地) cross-encoder 细粒度相关性
PDF 解析 MinerU 扫描版/表格 PDF 解析成 Markdown,保留标题层级
对象存储 MinIO 文档图片持久化(Milvus 内部存储也复用同一实例)
会话历史 MongoDB 7 多轮对话历史落库(任务进度走内存任务表,见第 8 篇)
生成模型 云端 LLM(OpenAI 兼容接口) 生成走 API,Embedding/Rerank 本地跑省成本

选型里有一条贯穿性原则:高频批量调用(向量化、精排)本地跑,低频高价值调用(生成、改写)走云端------导入一份手册要向量化几百个 chunk,走 API 又贵又慢。

版本约束上有两个实际教训值得记录:

  • pymilvus[model]>=2.6 与 Milvus Server 2.5.4 搭配使用,sparse 向量相关 API 在旧版本 SDK 上行为不一致,升级 SDK 前先查 release note;
  • flagembedding>=1.3.5 依赖特定版本区间的 transformers,锁定在 uv.lock 里避免隐式升级把本地推理搞挂。

三、总体架构:Import / Query 双 Agent 流水线

系统拆成两条独立的 LangGraph 流水线,各自是一个 StateGraph 固定编排图:

scss 复制代码
┌─────────────────────── Import 导入链路 ───────────────────────┐
│ node_entry → node_pdf_to_md → node_md_img → node_document_split │
│   (入口校验)   (MinerU解析)   (图片摘要+MinIO)   (长切短合)        │
│        → node_item_name_recognition → node_bge_embedding        │
│              (实体识别+实体库)          (BGE-M3 双向量)           │
│                     → node_import_milvus                         │
│                        (双集合幂等入库)                           │
└──────────────────────────────────────────────────────────────┘

┌─────────────────────── Query 查询链路 ────────────────────────┐
│ node_item_name_confirm ──┬─ 高置信(≥0.85) → 继续检索             │
│  (历史读取+改写+实体对齐)  ├─ 中置信(0.60~0.85) → 返回候选反问     │
│                          └─ 低置信(<0.60) → 拒答                 │
│        ↓                                                        │
│ node_search_embedding ──┐                                       │
│ node_search_embedding_hyde ─┤→ node_rrf → node_rerank → answer   │
│  (双路并行召回)              (RRF融合)  (BGE-Reranker) (SSE流式)  │
└──────────────────────────────────────────────────────────────┘

代码组织:目录即架构

bash 复制代码
app/
├── import_process/          # 导入链路(独立服务,端口 8000)
│   ├── agent/
│   │   ├── state.py         # ImportGraphState 状态契约
│   │   ├── main_graph.py    # 图组装:节点注册、条件边、编译
│   │   └── nodes/           # 7 个节点,一节点一文件
│   ├── api/                 # FastAPI 服务与上传接口
│   └── page/                # import.html(FastAPI 托管)
├── query_process/           # 查询链路(独立服务,端口 8001)
│   ├── agent/               # state.py + main_graph.py + 8 个节点
│   ├── api/                 # 查询与 SSE 流式接口
│   └── page/                # chat.html
├── clients/                 # Milvus / MinIO / Mongo / Neo4j 客户端封装
├── lm/                      # 本地模型:embedding_utils / reranker_utils / lm_utils
├── conf/                    # 各组件配置(从 .env 读取)
├── core/                    # logger、prompt 加载器
└── utils/                   # sse_utils、task_utils、稀疏向量归一化等
prompts/                     # 所有 Prompt 模板独立成 .prompt 文件

三条组织原则:

  1. 一节点一文件 :每个节点文件自带 if __name__ == '__main__' 单测入口,可以脱离图独立跑(第 3 篇细讲);
  2. 客户端统一封装在 clients/ :节点不直接 import pymilvus/pymongo,连接管理、重试、转义统一收口;
  3. Prompt 不写在代码里 :全部放 prompts/*.prompt 文件,由 core/load_prompt.py 加载------改 Prompt 不动代码、diff 清晰可审。

为什么拆成两条链路?

因为两侧的优化目标完全不同:导入侧关心知识质量 (解析、分块、元数据、向量质量),查询侧关心回答可信度(理解、召回、排序、生成约束)。拆开之后问题定位也清晰------命中率差,到底是分块切碎了、向量选错了、还是 Query 改写偏了,可以分层归因。

两条链路还各自包成独立 FastAPI 服务(导入 8000 / 查询 8001):导入是低频重任务(解析+向量化,CPU 密集),查询是高频轻任务(毫秒级接口+流式推送),资源画像完全不同,拆开互不拖累。

为什么用固定编排图而不是自由 ReAct?

企业知识库问答是确定性强、风险高的场景,控制流应该由工程代码和条件边决定,而不是让模型每步自由发挥。准确说这是"预编排 Agentic Workflow ",不是开放 Agent------这个定位是有意的架构取舍,不是能力不足。(什么场景才值得让模型自主规划?姊妹专栏《研发效能 Multi-Agent 系统》讲的就是另一条路线。)

具体到这个系统,LLM 只出现在四个受控位置,每处都有明确的输入输出约束和失败兜底:

LLM 调用点 职责 输出约束 失败兜底
图片摘要(导入) 图片 → 描述文本 纯文本 跳过该图,保留原图链接
实体识别(导入) chunk → 归属实体 JSON 重试 + 空实体标记
改写与实体抽取(查询) 口语问题 → 自包含问题 + 候选实体 JSON 用原始问题降级检索
答案生成(查询) 证据 chunk → 回答 证据约束 Prompt 证据不足明确拒答

控制流永远在图手里,LLM 只做"文本进、文本出"的受限变换------这就是"可控优先"的具体含义。

四、一次典型请求的完整数据流

用一个真实问题把两条链路串起来。前提:已导入《掌柜 ZR1200W 企业级无线路由器》产品手册。

导入时发生了什么(一次性成本):

bash 复制代码
上传 PDF → MinerU 解析成带标题层级的 Markdown(约 3 万字)
        → 图片上传 MinIO + VL 模型生成摘要回填(十几张图)
        → 长切短合切分(→ 约 80 个 chunk,携带标题/part 元数据)
        → LLM 识别实体"掌柜ZR1200W" → 写入每个 chunk 元数据 + 实体库
        → BGE-M3 生成 dense(1024维) + sparse 双向量
        → Milvus 双集合幂等入库(CHUNKS + ITEM_NAME)

查询"ZR1200W 这个路由器最多带多少台设备?"时发生了什么(每次请求):

sql 复制代码
① 读 MongoDB 最近历史 → LLM 改写 + 抽实体"ZR1200W"
② 实体向量化 → ITEM_NAME 集合 hybrid search → 得分 0.93 ≥ 0.85 → 自动确认
③ fan-out 并行:改写 query 向量检索 ∥ HyDE 假设文档检索(均带实体过滤)
④ RRF 按排名融合去重 → 截断 Top10
⑤ BGE-Reranker 精排 → 精选 Top5 进 Prompt
⑥ 证据约束生成,答案带出处,SSE 逐 token 推给前端

每一步的中间结果都可检查:改写后的 query、确认的实体与得分、各路召回列表、RRF/Rerank 排序,全部落日志------排障时可以精确回答"错在哪一环"

五、构建路线:为什么按这个顺序连载

本专栏的文章顺序就是项目真实的构建顺序,背后是一条数据依赖链

复制代码
环境底座          ← 没有 Milvus/MinIO/Mongo,后面全免谈
  ↓
导入链路(7节点)  ← 库里没数据,查询无从谈起 → 数据先行
  ↓
查询链路(8节点)  ← 有数据了,才能搭检索和生成
  ↓
工程化 + 调优     ← 端到端通了,才有资格谈流式体验和效果优化

两条原则贯穿始终:

  1. 数据先行:先把"写入"做扎实,再做"读取"。导入链路是查询链路的地基;
  2. 每步可验收:每篇文章结尾都有一个"里程碑动作"证明这一步通了------不攒大招、不到最后才联调。

六、本篇小结

  • 目标:可信回答(不串实体、可拒答、可溯源、可观测),四个词各自落到具体机制上,而不是口号;
  • 架构:Import / Query 双 StateGraph 流水线 + 双 FastAPI 服务,固定编排、可控优先,LLM 只在四个受控位置出现;
  • 代码组织:一节点一文件、客户端统一收口、Prompt 独立成文件;
  • 路线:数据先行、每步可验收。

下一篇我们从最底层开始:用 Docker Compose 一键拉起 Milvus / MinIO / MongoDB / Attu,把地基打好。

下一篇:《环境底座:Docker Compose 一键拉起向量库全家桶》

相关推荐
神经蛙199615 小时前
🌍 别再硬编码中文了!Python Web 项目国际化(i18n)完全指南
后端·python
二月龙15 小时前
Spring 事务失效的 8 种场景,很多老手依然频繁踩雷
后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
长大198815 小时前
MyBatis 常见性能陷阱:N+1 查询、一级缓存踩坑解决方案
后端
用户18615580086016 小时前
MinIO Java 对接试用:从连接、上传到下载的完整示例
后端
爱勇宝16 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
极客悟道16 小时前
SDKMAN vs jEnv vs JetTUI,JDK 版本管理到底选哪个
后端
长大198816 小时前
Java8 新特性到底要不要吃透?工作中高频使用的 5 个功能总结
后端
二月龙16 小时前
Spring Bean 生命周期 & 循环依赖:90% 开发者只知结论不懂原理
后端