研报是投研的弹药,但弹药管理是噩梦。做几年基本面,研报文件夹里就躺着几万份 PDF,按日期堆着,想找某个公司某个时点的信息,只能靠记忆加全文搜索碰运气。直接问大模型也不行——它会编数字、混口径,给的结论还没法证伪(这个我在 InvestPilot 那篇里讲过)。
InvestRAG 是给这个问题做的回答:把散落的上万份券商研报、调研纪要,做成一个可以自然语言问答的知识库。目前入库 14122 份文档、428974 个文本块,覆盖 2025 年 8 月至今。提问时系统先从库中找出最相关的原文段落,再交给大模型基于原文作答——数字有出处,不编。
理想状态是:我问”泡泡玛特 2026 年 Q1 的海外扩张进展”,系统从研报库里找到德银、瑞银、高盛关于泡泡玛特的原文段落,带出处,组织成一份带引用的回答。
这就是 RAG(检索增强生成):先检索,再回答。理解问题和组织语言交给大模型,事实依据由原文段落兜底。
它长什么样
六层架构:
6. 提问 CLI(scripts/ask.py)/ MCP server(接入 Claude Code)
5. 生成 LLM 基于检索原文作答 + 标注 [1][2] 出处
4. 检索 向量检索 + FTS5 关键词(RRF 融合)+ reranker 精排 + 时间衰减
3. 存储 Qdrant 本地嵌入式(段落 + 向量 + 元数据)
2. 处理 语义切块(表格整体保留)+ 元数据抽取(公司/行业/券商/日期)
1. 解析 PDF → 结构化文本 + 表格(pymupdf 快路 + MinerU OCR 乱码)
技术栈:Python 3.12 + uv / bge-m3 向量化 / bge-reranker-v2-m3 精排 / Qdrant / SQLite FTS5 / qwen3.6:35b-mlx(本地 LLM)/ MinerU(OCR)。
几个我自己觉得有点意思的设计
设计 1:混合检索——向量 + 关键词 RRF 融合
单纯向量检索对投研不够:向量擅长语义相似,但精确数字/公司代码/专有名词容易漏。所以我加了 SQLite FTS5 关键词检索(trigram 中文子串匹配),和向量检索结果用 RRF(Reciprocal Rank Fusion) 融合。
这样语义相关和精确命中都照顾到了。FTS5 磁盘版还解决了原来 rank_bm25 内存版在 17 万 chunks 时 6 分钟超时的问题。
设计 2:Reranker 精排——cross-encoder 把分数拉开
RRF 融合后的分数很扁平(都在 0.03 附近,区分度低)。我加了 bge-reranker-v2-m3 cross-encoder(把问题和段落成对喂进去打分的模型),对 RRF 的 top 30 逐对重打分。精排后分数从 0.03 的扁平拉开到 0.5–0.97,排在前面的结果靠谱多了。MPS GPU,~2-3s/查询。
设计 3:时间衰减加权——投研的时效性
投研信息有强时效性,半年前的研报代表公司不同阶段。检索时我加了时间衰减加权:半衰期 180 天,半年前的文档权重折半;但设了 15% 下限,再老也保留权重,留作历史回溯。
配合 mode 参数:“now”(现状,提示时效)和 “backtest”(回溯,聚焦该时段不提示过期)。
设计 4:表格整体保留
研报 60%+ 的信息在表格里。普通解析把表格读成乱码,或者切块时把表格拦腰截断。我的做法:语义切块时表格整体作为一个 chunk,不按固定 token 硬切。解析层用 MinerU(强表格解析器)处理 OCR,pymupdf 处理正常 PDF(快路)。
设计 5:MCP server——接入 Claude Code / InvestPilot
InvestRAG 注册了一个 MCP server,提供 rag_search 工具。Claude Code(或 InvestPilot agent harness)可以调用它检索研报库。关键设计:研报原文不出本地——bge-m3 检索 + LLM 抽取摘要都在本地,只把精炼的 {summary, sources} 返回给云端 agent。配额友好,也避免金融原文过云端触发风控。
与 InvestPilot 的关系
这两个项目是互补的:
| InvestRAG | InvestPilot | |
|---|---|---|
| 定位 | 知识库 / 信息检索 | 分析框架 / 财务建模 |
| 解决 | ”研报里怎么说的" | "这只票值不值” |
| 输出 | 业务事实 + 市场共识(带出处) | 财务模型 + 蒙特卡洛 + 赔率 |
| LLM 角色 | 理解原文 + 组织答案 | 推理 + 建模 + 审计 |
InvestRAG 的 MCP 工具接入 InvestPilot 后,agent 做财务深挖时可以随时调用研报库查业务事实——一个抓 facts,一个算数字。
几个踩过的坑
① 66% 的研报是乱码。 券商研报 96% 是图片型 PDF,pymupdf 提取出来全是乱码。必须 OCR。我用了 MinerU(pipeline 后端 + 多进程),benchmark 后最快 4.3s/份(原来串行 533h → 多进程 29h,10 倍加速)。踩了 paddlepaddle 缺失导致 7 倍慢的坑。
② rank_bm25 内存爆炸。 块数到 17 万以上时,内存版 BM25 全扫一遍要 6 分钟,检索直接超时。换 SQLite FTS5(磁盘版,trigram 中文),<1s。
③ embed 是入库瓶颈。 bge-m3 长文本(500 字/chunk),MPS GPU 也要 ~10s/份。总 token 决定耗时,减 chunk 数无效。试过 CPU 多进程(12 核理论超 GPU,但启动/序列化开销不值)、ONNX CoreML(待验证)。目前接受 ~10s/份。
④ catalog 锁反复。 ProcessPoolExecutor worker 跑完 MinerU 后不退出,持 SQLite catalog 写锁 → 后续 ingest 全 database is locked。try/finally + pkill 部分修复,后台场景仍偶发。
⑤ ollama 默认 num_ctx=262K 致超时。 ollama 0.31.2 基于显存自动设 context window,openai 库默认 timeout 连不上。限制 num_ctx=8192 + timeout=120 解决。
当前状态
- 入库 14122 份 / 428974 chunks(2025-08 ~ 2026-07)
- 混合检索(向量 + FTS5 + RRF)+ reranker 精排 + 时间衰减
- 作答 qwen3.6:35b-mlx(本地,21GB MLX)
- MCP server 接入 Claude Code
- 待 OCR ~18696 份老报告 + 21k 音频纪要 ASR
结尾
InvestRAG 不替我做投资决策,它只负责把原文和出处找出来。