内容
# 📈 金融研究代理
一款使用实时金融数据、新闻和SEC文件研究股票并回答后续问题的AI代理。
**实时演示:** [financial-research-agent.streamlit.app](https://financial-research-agent.streamlit.app) | **使用Claude Code构建**
---
## 为什么存在
我构建了这个来回答一个我找不到满意答案的问题:*LLM代理是否可以生成基于真实来源的投资摘要,以及如何验证?*
答案需要构建代理和测量层来审计它。
---
## 我测量了什么(以及我发现了什么)
### Grounding评估(LLM作为裁判)
我建立了一个评估框架,审计每个摘要中的量化主张和前瞻性声明是否与检索的源上下文相符。Sonnet裁判(温度0)将每个主张标记为`支持`、`不支持`或`推断`。
**早期结果:不支持的主张比例为49%。** 代理的近一半陈述没有得到任何检索的支持。
在迭代提示约束并强制生成基于源材料后:**不支持的主张比例为3%。
提示工程工作——而不是检索架构——实际上起到了关键作用。
### 重新排序A/B实验
我在RAG管道中添加了可选的交叉编码器重新排序,并运行了一个受控的4臂评估,涉及10个股票代码,以衡量是否改善了基础:
| 臂 | 主张 | 基础 | 未支持 | 检索延迟 |
| --- | ---: | ---: | ---: | ---: |
| 基线(前3名,无重新排序) | 66 | 92.4% | 0.0% | 4.1秒 |
| 普通前5名(无重新排序) | 74 | 86.5% | 1.4% | 4.5秒 |
| 重新排序20→3 | 84 | 78.6% | 0.0% | 20.7秒 |
| 重新排序20→5 | 69 | 85.5% | 0.0% | 20.4秒 |
**结论:** 重新排序增加了4-5倍的延迟,但没有可靠的基础收益。默认情况下,它处于关闭状态。测量框架是可交付的——它展示了该功能并不需要,而不是假设它会有帮助。
### QLoRA微调实验
小型本地模型是否可以在较低成本的情况下替代Claude Haiku进行章节生成?
我使用QLoRA在104个确定性、无Claude的训练对上微调了**Qwen2.5-1.5B-Instruct**,这些训练对由真实的SEC文件和金融数据构建。微调模型为2个摘要部分(财务健康和风险因素)提供服务;其他两个部分仍在使用Haiku,因为无法为它们构建确定性目标——这是关于数据的诚实发现,而不是试图掩盖的差距。
| | 每份成本 | 基础 |
| --- | ---: | ---: |
| 基线(所有Haiku) | $0.00538 | 88.6% |
| 混合(2个部分使用本地模型) | $0.00248 | 85.4% |
**54%成本降低,基础略有下降。** 默认情况下处于关闭状态——对于大多数用户来说,权衡不值得,但基准测试已提供给需要成本节约的用户。
我还使用手动编写的PyTorch训练循环(`fine_tune_pytorch_loop.ipynb`)重新实现了相同的微调 - 自定义`Dataset`,手动梯度累积和`optimizer.step()`,手动编写的余弦LR,没有Hugging Face `Trainer`。在相同的基准测试中,与`Trainer`在相同的数据和配置(`adamw_torch`,余弦调度,梯度累积8)下进行比较,两个损失曲线在21个优化器步骤中紧密跟踪 - 两个起点都在1.4-1.5左右,趋势一致,最终达到**0.50(原生)**和**0.35(Trainer)**。曲线反复交叉,因此最终步骤的差距处于运行噪声范围内(~7个优化器步骤/epoch,加上洗牌顺序和4位内核非确定性),而不是系统性差异 - 确认手动编写的循环在梯度累积和优化器步骤级别复制了`Trainer`的训练动态。


### 多代理协调实验
将单个代理分解为监督-协调团队是否能提高基础,或者只是增加成本?
我将摘要管道重构为监督图:**规划器**将股票代码分解为SEC检索子问题和覆盖点,**研究**代理根据现有的RAG和模型路由代码执行计划,内联**基础批评者**使用与离线评估相同的LLM作为裁判,为草稿摘要评分,而**监督器**将摘要发送回修订(限制在2次传递),直到达到基础阈值。它被标记为`MULTI_AGENT_ENABLED`(默认为关闭),原始的单代理管道保留为A/B控制。
我运行了两个路径上的标准10股票代码集,由相同的温度-0 Sonnet裁判评分,检索保持在基线(无重新排序,前3名):
| 路径 | 主张 | 未支持 | 每份$ | 每份延迟 | 修订次数/份 |
| --- | ---: | ---: | ---: | ---: | ---: |
| 单代理(控制) | 73 | 1(1.4%) | $0.0269 | 26.1秒 | 0.00 |
| 多代理 | 71 | 0(0.0%) | $0.0515 | 43.9秒 | 0.00 |
| 差异 | | -1.4点 | +92% | +68% | 无 |
两个发现,陈述明确:
1. **批评者触发了0次修订,涵盖所有10个真实草稿。** 基本管道已经将不支持的主张驱动到大约3%的整体水平,在执行摘要和展望部分的有效评估中,实际上为0,因此批评者查看了每个初稿,发现无需修复,并通过了。没有修复循环的余地。
2. **单个代理不支持的主张未复制。** 在73个主张中,恰好有一个被标记(在WMT上)。重新运行WMT生成了不同的草稿,零不支持的主张,确认孤立的标志是温度-0.2生成方差,而不是基本合成提示的系统性弱点。
协调成本约为 **+92%每份** 和 **+68%延迟** (规划器添加了一个Haiku调用,内联批评者为每个摘要添加了一个Sonnet裁判),而基础收益在此工作负载上为零。
**结论:默认关闭。** 在此工作负载中,多代理路径未显示出基础收益,因为单代理基线已经达到基础底线,因此批评者无需恢复。 这是对该语料库的陈述,而不是对多代理协调的一般性声明。 修订循环本身经过验证是有效的(注入的错误草稿被实时裁判捕获,发送回并在第二次传递时清理); 它在实践中处于休眠状态,而不是损坏。 保留该工具以重新衡量,如果更难的语料库,更薄的检索,较弱的基本模型或更长的摘要 ever 创建真正的基础余地。 如果发生这种情况,比较(基线/规划器-仅/规划器+批评者)是下一个记录步骤,以将任何增益归因于规划器或批评者。 与重新排序实验一样,可交付成果是测量结果,表明何时该功能有价值或无价值。
---
## 它做什么
**生成摘要** - 输入股票代码,应用程序生成结构化的投资摘要:
1. 获取股票数据(yfinance),新闻(NewsAPI)和SEC文件摘要(EDGAR)
2. 运行两个并发的Pinecone RAG查询,以实际文件文本为基础,生成SEC文件亮点和风险因素部分
3. 使用Claude Haiku并行生成四个中间部分
4. 从Claude Sonnet流式传输执行摘要和展望,接收预先编写的部分作为上下文
5. 在Redis(确切的键`research:{TICKER}`)和PostgreSQL中缓存已完成的摘要
**提出后续问题** - LangGraph ReAct代理根据需要选择工具(股票数据,新闻,SEC文件或RAG搜索)回答自由格式问题。
---
## 技术栈
| 层 | 技术 |
| --- | --- |
| LLM - 章节生成 | Claude Haiku 4.5(并行4个部分) |
| LLM - 综合 + ReAct代理 | Claude Sonnet 4.6 |
| 代理框架 | LangGraph - `create_react_agent`(后续)+ 监督`StateGraph`(可选的多代理摘要管道) |
| 金融数据 | yfinance |
| 新闻 | NewsAPI |
| SEC文件 | SEC EDGAR REST API |
| SEC RAG | LlamaIndex + Pinecone + HuggingFace `bge-small-en-v1.5` |
| 重新排序(可选) | 交叉编码器`BAAI/bge-reranker-base` |
| 可观察性 | LangSmith(工具调用,令牌,延迟,成本) |
| 简介缓存 | Redis(确切的键`research:{TICKER}`) |
| 持久性 | PostgreSQL通过SQLAlchemy |
| 异步任务 | Celery + Redis |
| REST API | FastAPI |
| 前端 | Streamlit |
| 云(后端) | AWS ECS Fargate,RDS PostgreSQL,Secrets Manager,ECR |
| 基础设施即代码 | Terraform |
| CI/CD | GitHub Actions → ECR → ECS(OIDC,无静态密钥) |
| 开发 | Claude Code |
---
## 架构
### 简介管道
```
"生成简介"
│
▼
Redis缓存(research:TICKER) ──命中──► 缓存简介
│ 失败
▼
获取股票数据(yfinance)
│
▼
┌──────────────────┐ ┌─────────────────┐
│ 获取公司新闻 │ │ 获取SEC文件 │ 并行
└──────────────────┘ └─────────────────┘
│ │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ Pinecone RAG(x2) │ 并发
└───────────┬───────────┘
│
┌───────────────┼───────────────────┐
▼ ▼ ▼ ▼
Haiku Haiku Haiku Haiku 4个并行调用
财务 最近 SEC 风险
健康 开发 高亮. 因素
└───────────────┴───────────┴───────┘
│
┌───────────▼───────────┐
│ Sonnet:执行摘要 │ 流式传输到浏览器
│ + 展望 │
└───────────────────────┘
```
### 后续问题
```
"询问"(自由格式问题)
│
▼
LangGraph ReAct 代理(claude-sonnet-4-6)
├─ 获取股票数据
├─ 获取公司新闻
├─ 获取SEC文件
└─ 查询SEC文件(Pinecone RAG)
│
▼
答案
```
### 多代理简介管道(可选,`MULTI_AGENT_ENABLED=true`)
单代理简介管道可以替换为监督-协调图。它默认关闭 - 单代理路径保持为生产默认和A/B控制,并生成**相同的简介模式和API响应**,因此下游没有任何变化。切换标志以比较两个路径。
```
"生成简介" (MULTI_AGENT_ENABLED=true)
│
▼
┌─────────┐ 将股票代码分解为研究计划:SEC RAG子问题和覆盖点
│ 规划器 │ 以及基础部分
└────┬────┘
▼
┌──────────┐ ◄──── 修订(批评者反馈添加到合成提示中)
│ 研究 │ 重用现有的检索 + 模型路由 + 合成代码;
└────┬─────┘ 修订传递重新合成执行摘要 + 展望
▼
┌──────────────────┐ 现有的LLM作为裁判,促进为内联节点 --
│ 基础批评者 │ 为源基础评分(一个裁判,与离线评估共享;
└────┬─────────────┘ `agent/grounding.py`)
▼
┌────────────┐ 不支持% ≤ CRITIC_MAX_UNSUPPORTED_PCT → 完成;否则发送
│ 监督者 │ 回研究,限制在MAX_REVISIONS传递
└────┬───────┘
▼
最终简介
```
- **一个裁判,两个调用者。** 内联批评者和离线基础评估都调用`agent/grounding.py:grade_brief()` - 只有一个裁判提示和评分定义,而不是两个可以漂移的副本。
- **模式安全的修订。** 修订传递重用已经有基础的中间部分,并且只通过相同的`_synthesis_prompt`重写执行摘要 + 展望,因此简介格式不会损坏。
- **有界循环。** `MAX_REVISIONS`(默认2)限制了批评者→研究重试;如果预算耗尽,监督者接受最佳努力。
- **跟踪。** 每个节点(规划器/研究/批评者/监督者)都是自己的LangSmith跨度。
---
## AWS部署
FastAPI后端被容器化并在**AWS ECS Fargate**上运行,具有真正的**RDS PostgreSQL**数据库,**AWS Secrets Manager**中的秘密,**GitHub Actions**管道在每次推送到`main`时部署。整个基础架构在**Terraform**(`infra/`)中定义。Streamlit前端保留在Streamlit Cloud;Redis/Celery保留在此环境中(缓存noop和异步端点禁用)。
```
推送到main
│
▼
GitHub Actions ──OIDC(无长期AWS密钥)──► 假设作用域IAM角色
1. pytest(CI门控)
2. docker build → 推送镜像(最新 + 提交SHA)→ Amazon ECR
3. 注册新任务定义修订 → 更新ECS服务(等待稳定)
│
▼
ECS Fargate任务 (公共子网,公共IP,安全组锁定到我的IP)
FastAPI容器(uvicorn,单个工作线程;bge-small模型内置于镜像)
│ │
▼ ▼
RDS PostgreSQL(t3.micro) Secrets Manager
research_briefs表 ANTHROPIC / NEWS / PINECONE / LANGSMITH密钥,
(私有,安全组锁定到 数据库URL,Redis URL - 由执行角色注入为任务
任务的安全组) 环境变量
```
**设计选择**
- **Terraform,从头到尾** - ECR,RDS,Secrets Manager,IAM角色,安全组,ECS集群/任务定义/服务和GitHub OIDC提供程序都位于`infra/`。本地状态;`terraform.tfvars`(带有我的IP)被gitignore。
- **无静态云凭证** - GitHub Actions通过**OIDC**进行身份验证,假设存储库范围的IAM角色仅具有将映像推送到ECR和部署服务的权限。存储库中不存储任何长期有效的内容。
- **机密从不存储在映像或git中** - 它们存储在Secrets Manager中,并在运行时通过执行角色作为环境变量注入到任务中。
- **成本意识** - RDS `t3.micro`在免费层;Fargate在**公共子网中运行,具有公共IP(无NAT网关)**以避免NAT成本;任务的安全组锁定到单个IP,因此未经身份验证的API无法访问
- **镜像** - `python:3.13-slim`,内置嵌入式模型,因此冷启动不会命中HuggingFace Hub;在CI中构建(无需本地Docker)。
### 暂停以节省成本
Fargate在任务运行时计费,因此当不演示时,我会暂停它,并在访谈时恢复:
```bash
infra/ecs-scale.sh 0 # 暂停 — 停止任务(无Fargate计算成本;RDS保留免费层)
infra/ecs-scale.sh 1 # 恢复 — 启动新任务(~1-2分钟启动)
infra/ecs-ip.sh # 打印正在运行任务的公共IP +基本URL
```
或原始一行程序:
```bash
aws ecs update-service --cluster financial-agent-cluster --service financial-agent-api \
--desired-count 1 --region us-east-1 # 0暂停
```
没有负载均衡器,因此任务在每次恢复时获得**新的公共IP**(`infra/ecs-ip.sh`获取它)。服务在Terraform中忽略`desired_count`,因此以这种方式缩放不会与`terraform apply`冲突。
参见 [`infra/README.md`](infra/README.md) 获取应用步骤。
---
## 本地运行
**1. 克隆并安装**
```bash
git clone https://github.com/schen9999/financial-agent.git
cd financial-agent
pip install -r requirements.txt
```
**2. 添加API密钥** -- 复制`.env.example`到`.env`:
| 密钥 | 获取位置 |
| --- | --- |
| `ANTHROPIC_API_KEY` | [console.anthropic.com](https://console.anthropic.com) |
| `NEWS_API_KEY` | [newsapi.org](https://newsapi.org) |
| `REDIS_URL` | [upstash.com](https://upstash.com) (免费层) |
| `DATABASE_URL` | PostgreSQL连接字符串 |
| `PINECONE_API_KEY` | [pinecone.io](https://pinecone.io) (免费层) |
**3. 运行**
```bash
streamlit run app.py
```
---
## API端点
| 方法 | 端点 | 描述 |
| --- | --- | --- |
| `GET` | `/health` | 健康检查 |
| `POST` | `/research` | 生成简介(同步) |
| `POST` | `/research/async` | 提交研究作业 |
| `GET` | `/research/status/{job_id}` | 轮询异步作业状态 |
| `POST` | `/ask` | ReAct代理答案 |
| `GET` | `/history/{ticker}` | 指定股票代码的过去简介 |
---
## MCP服务器
代理的工具也通过官方`mcp` Python SDK(FastMCP)通过**模型上下文协议**公开,因此任何MCP客户端(Claude Desktop,MCP Inspector等)都可以通过协议调用金融工具。 这是**独立且附加的**:`mcp_server.py`重用了现有的LangChain工具(没有重复的逻辑),并且不涉及FastAPI应用程序,Streamlit UI或代理的任何内容。
公开的工具:
| MCP工具 | 参数 | 返回值 |
| --- | --- | --- |
| `get_stock_data` | `ticker` | 价格,市值,市盈率,收入,利润率,公司信息 |
| `get_price_history` | `ticker` | 12个月每日收盘价 + 涨跌幅 |
| `get_company_news` | `company_name` | 5篇最新新闻文章 |
| `get_sec_filings` | `ticker` | 最新10-K / 10-Q摘要来自EDGAR |
| `query_sec_filings` | `ticker`,`question` | 索引10-K / 10-Q文本的RAG答案(Pinecone) |
**运行它**
```bash
pip install "mcp[cli]==1.28.0" # 已在requirements.txt中
mcp dev mcp_server.py # 在stdio上启动MCP检查器(最适合快速演示)
python mcp_server.py # 原始stdio传输(Claude Desktop启动)
MCP_TRANSPORT=streamable-http python mcp_server.py # 而不是stdio的HTTP传输
```
**连接Claude Desktop** -- 添加到`claude_desktop_config.json`(使用此存储库的venv Python和`mcp_server.py`的绝对路径),然后重启Claude Desktop:
```json
{
"mcpServers": {
"financial-research-agent": {
"command": "/abs/path/financial-agent/.venv/Scripts/python.exe",
"args": ["/abs/path/financial-agent/mcp_server.py"]
}
}
}
```
**注意**
- **stdout卫生(规范合规)。** 在stdio上,stdout是JSON-RPC通道,因此每个工具正文都在`redirect_stdout(sys.stderr)`下运行。传输在启动时捕获真正的stdout,因此库打印(例如RAG管道的`[rag] ...`行)转到stderr,并且永远不会破坏协议。
- **Windows事件循环修复。** 在Windows上,服务器强制使用异步IO `SelectorEventLoop`(在导入时设置,在加载任何异步/异步机制之前)。默认的`ProactorEventLoop`使本机扩展 - 特别是yfinance的`curl_cffi`/libcurl和torch/Hugging Face RAG堆栈 - 在stdio上的FastMCP工作线程中挂起。 在Windows上无影响。
- **冷启动约13秒**,主要是由导入LangChain引起的,该导入会拉入重用的工具。 重用的RAG堆栈(LlamaIndex + Pinecone +嵌入模型)是延迟加载的,因此它只在第一次调用该工具时加载,服务器启动(并且运行其他四个工具)而无需`PINECONE_API_KEY`。
- **每次调用超时。** 重用的工具没有请求超时,因此受限/缓慢的上游(yfinance,SEC,NewsAPI)将挂起服务器。 每个调用在包装器层以`MCP_TOOL_TIMEOUT`(默认30秒)为界限,如果需要更长时间,请提高它。
- 工具需要与应用其余部分相同的密钥(新闻的`NEWS_API_KEY`,RAG的`PINECONE_API_KEY`);它们从`.env`读取。
---
## 免责声明
仅供参考。不构成财务建议。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器