从零搭建RAG架构的财报分析Agent,Ollama本地大模型友好,附源码
摘要:本文将分享如何使用Ollama本地大模型,配合VSCode+ClaudeCode插件,从零开始构建一个基于RAG架构的财报分析Agent。我们将直面PDF解析OOM、多线程优化等实战难题,经过与longcat-2.0-preview模型的多轮"言语拉扯"和极端测试,最终打造出能自动获取PDF年报、智能分析并生成Markdown报告的完整系统。
📌 目录
- 为什么需要财报分析Agent?
- 技术选型:为什么选择Ollama本地大模型?
- 开发环境搭建:VSCode + ClaudeCode + longcat-2.0-preview
- 项目架构设计
- 开发过程中的"血泪史"
- 最终实现:功能演示
- 项目亮点与优化
- 总结与展望
为什么需要财报分析Agent?
相信各位技术同仁都有过这种体验:
场景一:老板突然丢给你一份194页的PDF年报,“小王,帮我分析下这家公司2024年的营收增长率和净利润率,顺便对比下行业平均水平…”
场景二:你需要分析同行业5家公司的财报,每家平均200页,总共1000+页的PDF,手动提取关键指标?不存在的,这辈子都不可能手动的。
场景三:你终于写了一个Python脚本提取PDF文本,结果发现表格数据全乱了,财务报表的表格结构复杂到你怀疑人生…
于是,我决定:与其重复劳动,不如造个轮子。一个能自动下载、解析、分析财报并生成报告的Agent,它不香吗?
技术选型:为什么选择Ollama本地大模型?
为什么是Ollama?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Ollama本地模型 | ✅ 数据隐私安全 ✅ 无API调用费用 ✅ 低延迟 ✅ 可离线使用 |
❌ 需要GPU资源 ❌ 模型能力受限 |
| 云端API(GPT-4等) | ✅ 模型能力强 ✅ 无需本地资源 |
❌ 数据泄露风险 ❌ API费用高 ❌ 网络依赖 |
最终选择:Ollama + 可外接API的双模式设计
这样既能保证数据隐私(财报数据可是商业机密!),又能在本地模型能力不足时切换到云端API。一举两得!
技术栈一览
后端框架: FastAPI + Uvicorn
PDF解析: Docling 2.92.0 + PyMuPDF
向量数据库: ChromaDB (嵌入式,无服务端)
大模型: Ollama (默认) / OpenAI兼容API (DeepSeek, Moonshot等)
Embedding: Ollama Embedding (默认 nomic-embed-text)
GPU加速: CUDA via onnxruntime-gpu 1.25 (RTX 4080 SUPER 16GB)
开发环境搭建:VSCode + ClaudeCode + longcat-2.0-preview
环境准备
步骤一:安装VSCode和ClaudeCode插件
- 下载安装 VSCode
- 在插件市场搜索并安装
ClaudeCode插件 - 配置longcat-2.0-preview模型API密钥
步骤二:安装Ollama并拉取模型
如果你也是16G显存,以下两个本地可运行的模型刚刚好。
# Windows下安装Ollama
# 访问 https://ollama.com/download 下载安装
# 拉取模型和embedding
ollama pull qwen3.5:9b
ollama pull qwen3embedding:4b
步骤三:创建Python虚拟环境
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境 (Windows)
venv\Scripts\activate
# 安装依赖
pip install -r requirements.txt
使用ClaudeCode进行AI编程
ClaudeCode插件让我能够:
- 代码生成:描述需求,模型自动生成代码框架
- 代码审查:自动发现潜在bug和优化点
- 重构建议:提供代码重构方案
- 文档生成:自动生成函数和类的文档字符串
实战技巧:
- 使用
/clear命令定期清理上下文,保持对话聚焦 - 将大型任务拆解为多个小任务,逐步完成
- 遇到模型理解偏差时,提供更详细的上下文和示例
项目架构设计
整体逻辑架构图
架构图说明:
- 用户交互层:支持Web UI和API两种方式
- 业务逻辑层:提供Agent模式和确定性流水线两种分析路径
- 工具/步骤层:具体的功能实现模块
- 基础设施层:PDF解析引擎、向量数据库、大模型等
RAG架构详解
RAG核心流程:
-
索引构建阶段:
- PDF文档 → Docling解析 → 文本分块 → Ollama Embedding → ChromaDB存储
-
检索生成阶段:
- 用户提问 → 向量化 → ChromaDB语义检索 → 获取相关文档片段
- 组装Prompt(文档片段 + 联网搜索结果 + 用户问题)
- LLM生成分析报告
RAG是什么,为什么需要RAG?
- RAG检索增强生成。它是一种架构方法,解决的是大语言模型(LLM)的三大核心缺陷:
- ✅ 解决幻觉问题:基于真实文档生成,减少大模型编造内容
- ✅ 知识更新快:无需重新训练模型,只需更新向量库
- ✅ 可解释性强:能够追溯答案来源的具体文档片段
分层设计:API → Core → Infra
┌─────────────────────────────────────────────────────┐
│ API Layer │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Web UI │ │ REST API │ │
│ │ (Jinja2) │ │ (FastAPI) │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Core Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ Agent │ │ Services │ │ Domain │ │
│ │ Loop │ │ (业务服务) │ │ (领域) │ │
│ └─────────────┘ └─────────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────┐
│ Infra Layer │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌────────────┐ │
│ │ Ollama│ │Chroma│ │Docling│ │ External │ │
│ │ LLM │ │ DB │ │ Parser│ │ API/Search│ │
│ └──────┘ └──────┘ └──────┘ └────────────┘ │
└─────────────────────────────────────────────────────┘
分层设计的优势:
- API Layer:负责用户交互,处理HTTP请求,返回响应
- Core Layer:纯业务逻辑,无副作用,易于测试和复用
- Infra Layer:副作用隔离,所有外部依赖(大模型、数据库、文件系统等)的统一适配
AI开发过程
- 明确需求目标
技巧:都知道token很贵的,你对大模型说的p话越多,token消耗就越多。为了省钱又高效,咱可以跟豆包、元宝、千问众神唠嗑,让他们替你总结需求生成大模型一听就懂的“提示词”
- 给AI定规矩
上手就丢个规矩文档给他,让他先读一读,知道自己应该遵守什么。认识自己的处境(系统运行环境是windows还是liunx)。当然,文档也可以请众神协助。
- 规矩定完搭骨架
让AI确认技术架构和技术栈后,该搭建文件夹构架,该部署开发环境安装各种依赖啊,包啊的,该干嘛干嘛。
- 一切就绪写代码
准备就绪后就让AI开始按需求写代码了。聪明的AI基本可以从头到底一溜的给你写完,交付一个成品,并告知你启动方法。
- 来回拉扯修bug
这时候重头戏来了,小点的项目也许就真的好了。而稍微复杂点,大点的项目多数会有报错,或者你不满意的地方。
没事,像训斥员工一样训斥他!平时老板怎么骂你的,你怎么骂他,骂到不报错了,骂道满足自己要求了,大家happy!
放心,AI是模范员工,绝对不会记仇!
开发过程中的"血泪史"
坑一:PDF解析OOM问题
问题描述:
当我兴冲冲地扔给系统一份194页的财报PDF时,期待着它自动解析并分析,结果…
std::bad_alloc
没错,C++堆内存分配失败了!😱
原因分析:
Docling使用pypdfium2(C++库)解码PDF页面。当单次处理过多页面(50-80+)时,C++堆内存碎片化会导致std::bad_alloc,与系统可用内存无关!
即使我的机器有32GB RAM + 16GB GPU显存,依然会挂!
Docling运行分析时本身会加载2个模型分析表格和层。然后会将页面生成bmp并一次性提交内存,如果系统设置的虚拟内存不是足够大(一个pdf几十G),分分钟内存崩溃。所以首先要让windows自己管理虚拟内存,或者自己指定个极大的值(同时分析4个pdf起码设置130G),其次极致优化Docling,方向就是压缩图片,pdf分页分线程,将模型强制运行在显存中并进行CUDA加速,以下分别描述
解决方案:自适应并行分块处理
# 核心思路:将大PDF拆分为30页的小块,每个块使用独立的子进程处理
PDF (194页) → [1-30] [31-60] ... [181-194]
↓
ThreadPoolExecutor (自适应worker数)
↓
┌─────┬─────┬─────┐
│子进程│子进程│子进程│ ← 每个chunk独立子进程
│chunk│chunk│chunk│ converter构建一次,处理完退出
└─────┴─────┴─────┘
↓
按页码顺序重组markdown
自适应并行度计算:
# 每个30页面chunk约消耗44GB提交内存(commit memory)
# 系统提交内存上限 ≈ 166GB(32GB RAM + 100GB 页面文件)
# 并行度 = (安全上限 155GB - 当前基线) / 44GB
import psutil
def calculate_max_workers():
COMMIT_LIMIT = 155 # GB
CHUNK_MEMORY = 44 # GB per worker
current_commit = psutil.virtual_memory().used / (1024**3)
available = COMMIT_LIMIT - current_commit
max_workers = max(1, int(available / CHUNK_MEMORY))
return max_workers
优化效果:
| 方案 | 处理时间 | 内存占用 | 稳定性 |
|---|---|---|---|
| 原始方案(单进程) | ~300秒 | 峰值60GB+ | ❌ 经常OOM |
| 分块+子进程 | ~143秒 | 峰值44GB/worker | ✅ 稳定 |
| 自适应并行(3worker) | ~50秒 | 峰值132GB | ✅ 稳定且快速 |
坑二:多线程压缩执行时间
问题升级:
解决了OOM问题后,我发现处理4个PDF(总共597页)需要16分钟,这还只是解析阶段!加上向量化和LLM分析,用户可能要等20分钟才能看到报告…
优化策略:
- 自适应并行度:根据系统当前内存使用情况,动态调整并行worker数
- GPU加速:使用CUDA via onnxruntime-gpu进行布局/表格ONNX推理
- 参数调优:
# 性能调优参数
ENV_VARS = {
"OMP_NUM_THREADS": 20, # 匹配物理核心数
"ORT_INTER_OP_THREADS": 4, # ONNX Runtime跨操作并行
"ORT_INTRA_OP_THREADS": 8, # ONNX Runtime操作内并行
}
PDF_PIPELINE_OPTIONS = {
"layout_batch_size": 16, # 布局模型批大小
"table_batch_size": 16, # 表格模型批大小
"images_scale": 0.5, # PDF渲染缩放(~4倍解码速度提升)
"TableFormerMode": "FAST", # 快速表格检测
"do_cell_matching": False, # 跳过单元格匹配(~2x提速)
}
修改Docling的CUDA检测逻辑:
# Docling原始代码调用 torch.cuda.is_available()
# 但我安装的PyTorch是CPU-only版本 😅
# 解决方案:monkey-patch!
import docling.utils.accelerator_utils as utils
def custom_decide_device(accelerator_device, supported_devices):
"""自定义CUDA检测逻辑,改为检查ONNX Runtime的CUDAExecutionProvider"""
import onnxruntime as ort
if "cuda" in supported_devices and "CUDAExecutionProvider" in ort.get_available_providers():
return "cuda"
return "cpu"
# 运行时补丁,不修改docling库源码
utils.decide_device = custom_decide_device
最终性能:
4个PDF,597页面,143秒 → 50秒(~3x提速)
0.24秒/页(GPU加速 + 多线程并行)
坑三:与longcat模型的多轮"拉扯"
场景还原:
当你发现longcat运行着…运行着…突然开始跟你讲英文了,甚至是突然忘记了各种包已经装过,忙着开始找工具了,别犹豫他的健忘症又犯了
我(##!@#@#):回去复习下项目概况和你的任务列表...
longcat:yes ,sir
我:`不对,工具调用应该用这个格式...而且要考虑最大轮次限制...`
longcat:```python
# 修改后的代码,但又引入了新问题:
# 1. 消息历史管理混乱,导致上下文溢出
# 2. 工具结果没有正确追加到对话
我:😡 你看清楚我的需求!消息历史应该这样管理...
(虽然Claude有强大的记忆,依然要记得让AI经常保存任务进度生成TASK.MD,大模型万一重启个服务器什么的,你的session就断了)
经过N轮迭代,最终方案:
class AgentLoop:
"""Agent主循环:LLM ↔ 工具执行"""
def __init__(self, max_turns: int = 10):
self.max_turns = max_turns
self.messages = [] # 对话历史
async def run(self, user_message: str) -> str:
"""执行Agent循环"""
self.messages.append({
"role": "user",
"content": user_message
})
for turn in range(self.max_turns):
# 1. LLM推理
response = await self.llm.chat(messages=self.messages)
# 2. 检查是否有工具调用
if not response.tool_calls:
# 无工具调用,返回最终答案
return response.content
# 3. 执行工具
tool_results = []
for tool_call in response.tool_calls:
result = await self.execute_tool(tool_call)
tool_results.append(result)
# 4. 工具结果追加到对话
self.messages.append({
"role": "assistant",
"content": response.content,
"tool_calls": response.tool_calls
})
self.messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(tool_results)
})
# 达到最大轮次,强制返回
return "分析超时,请简化问题或稍后重试。"
极端测试:
为了验证Agent的稳定性,我设计了一系列极端测试:
- 循环陷阱测试:故意让工具返回诱导LLM重复调用相同工具的消息
- 超长对话测试:模拟20轮工具调用,检查上下文管理
- 工具异常测试:模拟工具执行失败,验证错误处理
- 并发请求测试:同时发起10个分析请求,检查资源竞争
测试结果:
✅ 所有测试通过!Agent能够稳定地完成复杂的多步骤任务。
最终实现:功能演示
特点一:自动获取PDF年报
用户输入:
分析以下财报 https://www.example.com/annual_report_2024.pdf
该公司2024年的营收和净利润是多少?与2023年相比增长率如何?

**关注我,私信发送提示词生成器,自动获取年报url ^ ^
系统执行流程:
- 自动下载PDF:从指定URL下载财报PDF到本地
- 智能解析:使用Docling提取文本和表格,保留文档结构
- 向量化索引:将文本内容分块,使用Ollama Embedding生成向量,存储到ChromaDB
- RAG检索:根据用户问题,语义检索相关文档片段
- 联网搜索:通过百度搜索补充实时数据(股价、行业对比等)
- LLM分析:将文档片段、搜索结果、用户问题组装成Prompt,调用大模型生成分析
- 生成报告:将分析结果保存为Markdown格式,支持SSE流式输出到前端
特点二:Agent模式自动编排

Agent模式的优势:
- ✅ 自动规划:LLM根据任务自动决定调用哪些工具、调用顺序
- ✅ 动态决策:根据工具执行结果,动态调整后续行动
- ✅ 多步骤任务:能够完成需要多个步骤的复杂任务
示例:
用户:分析特斯拉2024年Q4财报,对比比亚迪同期表现,生成竞品分析报告
Agent思考过程:
1. 需要下载特斯拉2024 Q4财报 → 调用 download_pdfs
2. 需要解析PDF → 调用 parse_pdfs
3. 需要搜索比亚迪财报 → 调用 web_search
4. 需要检索特斯拉财报关键数据 → 调用 rag_query(多次)
5. 需要检索比亚迪数据 → 调用 rag_query(多次)
6. 需要生成对比报告 → LLM生成最终报告
7. 保存报告 → 调用 save_report
特点三:双模式支持(本地+外接)

本地模式(Ollama):
# .env 配置
LLM_PROVIDER=ollama
OLLAMA_BASE_URL=http://localhost:11434
OLLAMA_MODEL=qwen3.5:9b
OLLAMA_EMBEDDING_MODEL=qwen3embedding:4b
外部API模式(如DeepSeek):
# .env 配置
LLM_PROVIDER=external
EXTERNAL_LLM_BASE_URL=https://api.deepseek.com/v1
EXTERNAL_LLM_MODEL=deepseek-chat
EXTERNAL_LLM_API_KEY=your-api-key
无缝切换:
系统通过LlmClientFactory工厂模式,根据配置自动选择对应的LLM客户端,业务代码无需修改!
class LlmClientFactory:
@staticmethod
def create(settings):
if settings.llm_provider == "ollama":
return OllamaLlmClient(settings.ollama_base_url, settings.ollama_model)
elif settings.llm_provider == "external":
return ExternalLlmClient(
settings.external_llm_base_url,
settings.external_llm_model,
settings.external_llm_api_key
)
项目亮点与优化
亮点一:Lazy-loading设计,节省启动内存
class _LazyDoclingAdapter:
"""惰性加载代理:Docling模型在首次使用时才加载"""
def __init__(self) -> None:
self._adapter: PdfParserAdapter | None = None
def parse(self, file_path: str) -> tuple[str, int]:
# Docling加载~200MB+的ML模型
# 通过惰性加载,启动时不占用内存
if self._adapter is None:
logger.info("Lazy-loading Docling parser adapter (first use)...")
self._adapter = DoclingParserAdapter()
return self._adapter.parse(file_path)
def release(self) -> None:
"""释放模型内存"""
if self._adapter is not None:
logger.info("Releasing Docling parser adapter to free memory...")
self._adapter = None
效果:
- 启动时间:从10秒降低到1秒
- 启动内存:从2GB降低到100MB
- 模型在首次解析时才加载,用完后可以手动释放
亮点二:未修改Docling源码,全运行时补丁
为什么不直接修改Docling源码?
- ✅ 方便升级:Docling版本更新时,无需手动合并修改
- ✅ 可维护性:所有定制逻辑集中在项目代码中
- ✅ 兼容性:通过monkey-patch和公开API实现,不依赖内部实现
定制点:
- CUDA检测逻辑:monkey-patch
docling.utils.accelerator_utils.decide_device - 自适应并行调度:项目自有代码
docling_adapter.py - 性能参数调优:通过Docling公开API配置
PdfPipelineOptions
亮点三:结构化日志,方便调试
# 使用JSON Formatter,日志包含时间戳、日志级别、模块名、结构化数据
{
"timestamp": "2026-05-06 20:30:15.123",
"level": "INFO",
"module": "src.core.services.parser",
"message": "PDF parsing completed",
"data": {
"file": "annual_report_2024.pdf",
"pages": 194,
"time_seconds": 50.2,
"chunks": 582
}
}
总结与展望

项目成果
✅ 功能完整:自动下载、解析、分析财报并生成报告
✅ 性能优秀:597页PDF解析仅需50秒(GPU加速+多线程)
✅ 稳定可靠:自适应并行+子进程隔离,解决OOM问题
✅ 灵活扩展:支持本地Ollama和外部API双模式
✅ 用户友好:Web UI + SSE流式输出 + Markdown报告
技术收获
- RAG架构实战:深入理解检索增强生成的原理和优化方法
- 大模型应用开发:Agent模式、工具调用、提示词工程
- 性能优化:多线程并行、GPU加速、内存管理
- 软件开发:分层架构、依赖注入、工厂模式、惰性加载
未来展望
- 🚀 支持更多文档格式:Word、Excel、PPT等
- 🚀 多模态分析:支持图表、图片的解析和分析
- 🚀 多语言支持:支持中英文财报分析
- 🚀 部署优化:Docker容器化、云原生部署
**
项目开源
仓库地址:
https://gitcode.com/ddly2000/rag-financial-agent.git
**
参考资料(让AI自己琢磨)
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、关注!你的支持是我持续创作的动力! 🙏 关注我私信索取年报分析提示词生成器
有任何问题或建议,欢迎在评论区留言讨论! 💬
作者:[谢绝采访]
发布时间:2026年5月
标签:#Ollama #RAG #财报分析 #AI编程 #VSCode #ClaudeCode
更多推荐



所有评论(0)