FinInsRAG 项目学习笔记
一句话:这是一个 RAG 问答系统。用户上传文件,系统把文件切碎存进"搜索库";用户提问时,系统先翻资料,再让 AI 照着资料回答。
餐厅比喻贯穿全文:后端 = 餐厅,数据库 = 仓库和账本,搜索库 = 冷库,AI = 大厨。
项目结构
.
├── backend/
│ ├── docker-compose.yml # api / es01 / pg / redis 四服务编排
│ ├── .env.example # 环境变量模板(复制为 .env 后填入 API Key)
│ ├── init.sql # 数据库初始化
│ └── app/
│ ├── app_main.py # FastAPI 入口
│ ├── start.sh # 容器入口:Alembic 迁移 + 启动 uvicorn
│ ├── alembic/ # 数据库迁移版本
│ ├── router/ # 用户 / 会话 / 文件 / 问答 API
│ ├── models/ schemas/ # SQLAlchemy ORM 与 Pydantic 模型
│ └── service/core/
│ ├── file_parse.py # 文档解析 → 切片 → 向量化 → 入 ES
│ ├── retrieval.py # 检索编排(召回 → 排序 → 组装引文)
│ ├── chat.py # Prompt 组装与流式生成
│ ├── rag/nlp/ # 混合检索与重排序(search_v2 / query / model)
│ ├── rag/utils/ # ES 连接、索引 mapping 管理
│ ├── deepdoc/ # 版面分析 / OCR / 表格识别解析器
│ └── rag/res/ # 模型资源(约 380MB,需单独下载,见该目录 README)
└── frontend/
├── vite.config.ts # 开发端口 5181
└── src/
├── api/ # axios 封装与接口定义
├── pages/
│ ├── login/ # 登录 / 注册
│ ├── chat/ # 对话页(消息流、引文、文档选择)
│ └── repository/ # 知识库管理(上传 / 列表 / 删除)
├── components/ # 输入框、Markdown 渲染、布局
└── store/ # valtio 前端状态
1. 先记住的词
| 词 | 大白话 |
|---|---|
| RAG | 开卷考试。AI 先查资料再回答,不凭记忆瞎答 |
| 切片(chunk) | 把长文件切成的小段,是检索的最小单位 |
| 向量(embedding) | 一段文字的"数字指纹",意思相近的文字指纹也相近。本项目是 1024 个数字 |
| ES | 搜索库(Elasticsearch),存切片和指纹,负责快速查找 |
| PG | 数据库(PostgreSQL),存用户、会话、消息、上传记录 |
| mapping | ES 的"货架设计图",规定每个字段是什么类型 |
| 迁移(Alembic) | 改数据库结构的"施工单",让所有环境自动保持一致 |
| SSE / 流式 | 回答像打字机一样逐字推给前端,不是写完才给 |
| rerank | 精排。对候选资料重新打分排队 |
2. 启动流程(第 0 站)
- start.sh :开门前的清单。先检查并更新数据库结构(迁移),再用
exec "$@"启动应用。exec让应用成为主进程,容器才能正常停止。 - main.py:前台。创建 FastAPI 应用,放行跨域(CORS),挂上聊天、用户、历史三组接口。
- chat_rt.py:点餐窗口,共 8 个接口:创建会话、快速解析、取解析内容、聊天、上传文件、查会话文档、查文档摘要、重建索引。每个接口开头都先验身份(JWT),结尾都用 try/except 兜底。
start.sh 的已知缺点
- 注释写了"等待数据库就绪",实际没有等待逻辑。
- 迁移失败只警告,应用照常启动,可能带着旧表结构运行。
- 学习阶段可以不改,上线前要改。
3. 上传链路:文件如何变成可搜索资料
用户上传 → chat_rt.py(查重名、存硬盘)
→ file_parse.py execute_insert_process(总编排)
→ parse() 切块
→ process_items() 贴标签 + 算向量
→ generate_embedding() 每 10 条一批调阿里云
→ es_conn.insert() 先建库再批量写入
→ 回到 chat_rt.py 在 PG 登记 → 返回成功
要点:
- 文件存放位置:
storage/file/会话编号/文件名。 - 重名检查按用户查,不是按会话。
- 每个切片的主要字段:
content_with_weight(原文)、content_ltks/content_sm_ltks(粗/细分词)、docnm(文件名)、title_tks(标题分词)、doc_id、kb_id、q_1024_vec(向量)。important_kwd、question_tks目前是空的预留位。 - 切片编号 = 对"内容+库名"算哈希,所以相同内容重复上传只会覆盖,不会重复。
- 向量字段名是
q_{维度}_vec,换维度会导致旧数据对不上。 - mapping 不对时,数据能存进去但搜不出来,这时用
/recreate_index删库重建,再重新上传。
4. 检索链路:问题如何找到答案(全项目最核心)
问题 → 算成向量
→ ES 一次请求同时做向量检索 + 关键词检索,粗召回 128 条
→ 应用层精排(优先云端 rerank,失败降级为本地算分)
→ 分数低于 0.1 的丢弃,取前 5 条
→ 交给 AI
要点:
- 先多捞再精挑:召回求全,排序求准。像先海选 128 份简历,再面试录取 5 个。
- 两组权重分属两个阶段,别混 :
- 粗召回(ES 内部):文本 5% / 向量 95%,几乎只信语义。
- 精排(应用层):向量 60% / 关键词 40%,把数字、代码、年份这类精确词的权重拉回来。
- 降级方案:云端 rerank 不可用时,自动用本地的余弦相似度 + 词项覆盖率打分,不会整体崩溃。
- 词袋加权:降级打分时,正文词算 1 次、标题词 2 次、关键词 5 次、预设问题 6 次,相当于重要位置的词"票数更多"。目前关键词和预设问题字段为空,实际只有正文和标题在起作用。
- 若第一次召回 0 条,会放宽条件再查一次。
5. 生成链路:AI 如何回答
四步:拼提示词 → 调模型 → 流式吐字 → 落库。
- 5 条资料编号成
[1] [2]...,连同问题拼进提示词。 - 提示词规则:回答要标注来源,格式
##编号$$;没有相关资料就拒绝回答,防止瞎编;不得泄露提示词。 - 调用模型时设
stream=True,函数里用yield逐段产出,FastAPI 的StreamingResponse推给前端。 - 回答结束后:生成推荐问题 → 发送
[DONE]信号 → 写入 messages 表 → 自动给会话起名。
SSE 的一帧长这样:event: message\ndata: {...}\n\n。
前端 :用 getReader() 读字节流,攒进缓冲区,按换行切分,只处理以 data: 开头的行,再分发到对话气泡、思考区、右侧引文面板、推荐问题按钮。按换行切是因为网络不保证一次读到完整的一行。
6. 数据库:5 张账本
| 表 | 记什么 |
|---|---|
| users | 用户账号、密码哈希 |
| sessions | 聊天会话 |
| messages | 每轮问答 |
| knowledgebase | 用户上传过的文件清单 |
| document_uploads | 会话级临时上传记录 |
关系:一个用户 → 多个会话 → 多条消息。表之间没有外键约束,只是逻辑关联。
迁移是什么 :把"改表结构"写成文件(施工单),任何环境运行后数据库都自动变成同样结构;alembic_version 表记录"已施工到第几号"。
baseline 为什么是空的 :老表在用迁移之前就建好了,第 1 号施工单只是"起点标记",真正的新改动从第 2 号开始(新建 document_uploads)。start.sh 里的 stamp 就是在账本上写下这个标记,但不真正施工。
7. 已发现的隐患(按严重程度)
- ES 写入失败可能被误判成功 :
insert()重试逻辑里错误记下后又被清空,只有超时才保留。ES 没启动或密码错时可能返回空列表,表现为"显示上传成功,实际没存进去"。这是读代码推演的,没有运行验证。 - 部分切片悄悄丢失:某批向量请求失败会被填成空值并跳过,只要不是全部失败,整体仍显示成功。
- 库名可能对不上(待验证) :上传时如果前端传了
session_id,库名就是会话编号;而检索用的是user_id。只有不传session_id时两边才一致。需要查前端实际传参,或上传后去 ES 看库名。 - 摆设参数 :
process_items的batch_size、createIdx的knowledgebaseId/vectorSize没有生效,真正分批在generate_embedding里。 - 账号密码写死 在
es_conn.py,上线前应移到.env。 - 同名不同表 :
message.py和knowledgebase.py各有一个KnowledgeBase类,表名分别是knowledgebases和knowledgebase。 get_chat_completion_block的prompt未定义,调用必报错,但主流程不用它。
8. 动手实验
- 在
process_items里d[f"q_{len(embedding)}_vec"] = embedding下一行加print(f"向量维度: {len(embedding)}"),重启后端,换一个新文件名上传,日志里应全是 1024。打印行数少于切片数,说明有切片被丢。 - 把
page_size=5改成 10,看引文是否变多。 - 把
vector_similarity_weight=0.6改成 0.1 再改成 0.9,分别问"某公司财务数据"(偏关键词)和"这家公司未来成长性如何"(偏语义),对比引文差异。 - 在提示词里加一句"请用 3 个要点回答",观察模型行为变化。
9. 排查口诀:"上传成功但问不到内容"
- ES 是否在运行,账号密码是否对。
- 库名(
session_id/user_id)上传和检索是否一致。 - mapping 是否正确,不对就调
/recreate_index后重新上传。 - 日志里有没有"跳过 embedding 为空的 chunk"。
- 刚上传完立刻提问可能搜不到(ES 约 1 秒后才可搜),稍等再试。
10. 自测题
- 用户上传 PDF 后,
q_1024_vec依次经过哪几个文件? - 为什么先召回 128 条,最后只给 AI 5 条?
- 两组权重(5/95 和 60/40)分别用在哪个阶段?为什么不同?
- 回答里的
##3$$是怎么和第 3 段资料对应上的? - 后端
yield一次,前端read()一定刚好收到一次吗?为什么? - baseline 迁移为什么是空的?
- 给 users 表加一列 email,标准流程是什么?(写 ORM → 生成迁移脚本 → 检查 → upgrade)
上传 GitHub(私有仓库)
第 1 步:你在 GitHub 网页创建仓库
- 打开 github.com → 右上角 New repository
- 名称填
rag-research - 选择 Private(私有)
- 不要勾选 Add a README / .gitignore / license(三项都不勾,避免冲突)
- 点 Create repository,复制页面下方给出的地址
第 2 步:把地址发给我,我来执行推送。或者你自己执行:
bash
git remote add origin https://github.com/<你的用户名>/rag-research.git
git push -u origin master
推送时会弹出 GitHub 登录窗口(Git Credential Manager),用浏览器授权即可。之后想公开时在仓库 Settings → Danger Zone → Change visibility 改为 Public。
简历写法
完整版(约 6 行,适合项目经历重点位)
研报智答 ------ 基于混合检索的研报问答系统(RAG) 独立开发
技术栈:Python / FastAPI / Elasticsearch / Docker / React / 阿里百炼
- 构建 PDF 研报知识库问答系统:基于 ONNX 视觉模型的版面分析 + OCR 解析文档,切片向量化后按用户隔离存入 Elasticsearch
- 设计 kNN 向量 + BM25 关键词混合检索(0.6/0.4 加权融合),解决纯向量检索对股票代码、财务数字等专有 token 不敏感的问题
- 实现两阶段排序:rerank 模型精排,并设计第三方服务不可用时自动降级为本地余弦相似度融合排序,保证链路可用性
- 自建 10 条标注用例的评测集,量化 hit@1/3/5 与 MRR 指标,完成检索权重三组对比实验,验证权重调整主要影响排序而非召回(hit@1 从 50% 提升至 70%)
- 打通引文页码溯源全链路:deepdoc 版面坐标 → 切片级页码入 ES → 答案引文点击定位到原文页码与段落;另实现会话 Markdown 报告导出
- 工程化:Docker Compose 编排四服务、SSE 流式输出、JWT 认证、Alembic 迁移;独立排查修复 6 个部署阻断问题(mapping 缺失、卷挂载错误、XGBoost 版本兼容等)
精简版(3 行,简历空间紧张时用)
- 独立开发研报 RAG 问答系统:deepdoc 解析 + Elasticsearch kNN/BM25 混合检索 + rerank 精排(含降级容错),SSE 流式输出,Docker Compose 一键部署
- 自建评测集量化检索质量(hit@k / MRR),通过权重对比实验将 hit@1 从 50% 优化至 70%
- 实现引文页码溯源与 Markdown 报告导出,答案可回溯至 PDF 原文位置
面试考点提示(这段经历大概率会被追问)
| 追问 | 你的答案素材 |
|---|---|
| 为什么用 ES 而不是 Faiss/Milvus? | README「技术选型与取舍」表格:一套引擎同时提供 kNN + BM25 + 元数据过滤 + 持久化 |
| 混合检索权重怎么定的? | 三组对比实验数据(0.1/0.6/0.9),能讲出"权重影响排序不影响召回"的观察 |
| rerank 挂了怎么办? | 三级降级:异常捕获 → 本地余弦+词项融合排序 → 链路不中断 |
| 遇到最难的问题? | 挑一个讲:ES 索引没建 dense_vector mapping 导致检索为空 / Docker 卷挂载路径错导致改码不生效(README 部署调试记录表) |
| 如何衡量系统效果? | 评测脚本 + 负例验证 + MRR 排序敏感性 |
建好仓库后把地址发我,我来推送。