533 lines
15 KiB
Markdown
533 lines
15 KiB
Markdown
# A 股 Deep Research 私有投研平台
|
|
|
|
> 面向 A 股投资研究的私有化 Deep Research 平台。
|
|
>
|
|
> 自动抓取财经新闻 → 中文正文提取 → LLM 投资事件抽取 → 向量化 → Qdrant 知识库 → MCP/Agent 深度研究。
|
|
|
|
**项目定位**:A 股研究辅助与知识管理平台,**非**自动交易、**非**预测、**非**投资顾问。
|
|
|
|
---
|
|
|
|
## 技术栈
|
|
|
|
| 模块 | 选型 |
|
|
| ---- | ---- |
|
|
| 抓取 | Crawl4AI(异步 + JS 渲染) |
|
|
| 正文提取 | GNE(中文新闻正文识别) |
|
|
| LLM | DeepSeek / Qwen(百炼) |
|
|
| Embedding | BGE-M3(本地) / Qwen Embedding(远程) |
|
|
| 向量库 | Qdrant |
|
|
| 调度 | APScheduler |
|
|
| 服务化 | MCP |
|
|
| 运行时 | Python 3.11 + uv |
|
|
| 部署 | Docker Compose |
|
|
|
|
---
|
|
|
|
## 当前状态
|
|
|
|
| 已完成 | M0~M8、**M9 投研 Agent Prompt** |
|
|
| --- | --- |
|
|
| 进行中 | 等待 M9 验收 |
|
|
| 下一步 | 项目整合与优化 |
|
|
|
|
详见 `continuation.md`。开发遵循 `project_plan.md` 9 个 Milestone,分阶段交付。
|
|
|
|
---
|
|
|
|
## Qdrant 部署模式说明
|
|
|
|
本项目 Qdrant 支持两种运行模式,**默认本地文件模式(零依赖)**。
|
|
|
|
### 模式一:本地文件模式(默认,推荐)
|
|
|
|
```
|
|
类比:SQLite——无独立进程,Python 进程内嵌加载存储引擎
|
|
数据:data/qdrant_storage/ (纯文件,跟随项目目录)
|
|
进程:无守护进程
|
|
端口:不监听任何端口
|
|
并发:单进程串行读写(批处理场景足够)
|
|
```
|
|
|
|
**优点**:无需 Docker、无需手动安装、无需 root、树莓派 ARM64 兼容。
|
|
|
|
**启动方式**:代码中调用即自动加载,无需额外步骤。
|
|
|
|
```python
|
|
from vectorstore import make_qdrant_client, VectorStore
|
|
c = make_qdrant_client() # 默认 path="data/qdrant_storage"
|
|
s = VectorStore(c)
|
|
# ... 读写操作 ...
|
|
s.close()
|
|
```
|
|
|
|
### 模式二:Docker Server 模式(可选,x86 推荐)
|
|
|
|
```
|
|
类比:PostgreSQL——独立守护进程,通过网络端口访问
|
|
数据:Docker volume qdrant_storage/
|
|
进程:容器 a_share_qdrant
|
|
端口:6333 (REST) / 6334 (gRPC)
|
|
并发:多客户端网络访问
|
|
```
|
|
|
|
**适用场景**:Cherry Studio 远程连接、多机共享知识库、生产环境。
|
|
|
|
```bash
|
|
# 启动 Qdrant 服务
|
|
docker compose --profile m6 up -d
|
|
|
|
# 代码中指定 host
|
|
c = make_qdrant_client(host="192.168.1.160", port=6333)
|
|
```
|
|
|
|
**注意**:树莓派 5 ARM64 内核使用 16KB 内存页,Qdrant 官方 Docker 镜像内置的 jemalloc 仅支持 4KB 页,**在树莓派上会启动即崩溃**(`exit 134`)。树莓派请使用本地文件模式。
|
|
|
|
---
|
|
|
|
## 快速开始
|
|
|
|
### 统一 CLI
|
|
|
|
所有操作通过 `a-share` 命令完成,替代之前的 8 个脚本入口:
|
|
|
|
```bash
|
|
# 模块操作
|
|
uv run a-share crawl # M1 抓取
|
|
uv run a-share crawl --source cls # 单源
|
|
uv run a-share extract --date 20260616 # M2 提取
|
|
uv run a-share dedup --date 20260616 # M3 去重
|
|
uv run a-share events --date 20260616 # M4 LLM 抽取
|
|
uv run a-share events --provider qwen # 切换 LLM
|
|
uv run a-share embed --date 20260616 # M5 向量化
|
|
uv run a-share ingest --date 20260616 # M6 入库
|
|
|
|
# 全链路
|
|
uv run a-share pipeline --once # 立即执行全链路
|
|
|
|
# 检索(直接查 Qdrant,无需写代码或配 Cherry Studio)
|
|
uv run a-share search "宁德时代固态电池"
|
|
uv run a-share search "政策" --source cls --sentiment positive
|
|
uv run a-share search "风险" --stock 001212 --min-importance 3
|
|
|
|
# 日报(生成 HTML 报告并上传)
|
|
uv run a-share report # 生成今日日报
|
|
uv run a-share report --date 20260616 # 指定日期
|
|
uv run a-share pipeline --once --report # 全链路末尾自动生成日报
|
|
|
|
# 状态总览
|
|
uv run a-share status
|
|
```
|
|
|
|
### 环境准备
|
|
|
|
```bash
|
|
# 1. 安装 uv (macOS)
|
|
brew install uv
|
|
|
|
# 2. 创建虚拟环境并同步依赖
|
|
uv sync
|
|
|
|
# 3. 复制环境变量模板
|
|
cp .env.example .env
|
|
# 编辑 .env 填写 API Key 等敏感配置
|
|
|
|
# 4. 验证 Python 版本(应为 3.11.x)
|
|
uv run python --version
|
|
|
|
# 5. 首次安装浏览器(Crawl4AI 依赖 Playwright Chromium)
|
|
uv run python -m playwright install chromium
|
|
```
|
|
|
|
### 运行抓取(M1)
|
|
|
|
```bash
|
|
# 抓取全部启用源
|
|
uv run python -m scripts.run_crawler
|
|
|
|
# 只抓单个源
|
|
uv run python -m scripts.run_crawler --source cls
|
|
|
|
# 只试跑不写文件
|
|
uv run python -m scripts.run_crawler --no-save
|
|
|
|
# 调试日志
|
|
uv run python -m scripts.run_crawler --log-level DEBUG
|
|
```
|
|
|
|
抓取产物路径:`data/raw/{source_id}/{YYYYMMDD}/`
|
|
|
|
- `{url_hash}.html` 原始 HTML
|
|
- `{url_hash}.md` Crawl4AI 输出的 Markdown
|
|
- `index.jsonl` 元数据(每行一条 CrawlResult,不含正文大字段)
|
|
|
|
日志:`logs/crawler.log`(自动轮转 10MB,保留 5 份)
|
|
|
|
### 运行正文提取(M2)
|
|
|
|
M2 以 M1 的产物为输入,从 HTML 抽出标准化 `Article`(标题/正文/时间/作者/图片)。
|
|
|
|
```bash
|
|
# 处理今日所有源
|
|
uv run python -m scripts.run_extractor
|
|
|
|
# 指定日期
|
|
uv run python -m scripts.run_extractor --date 20260616
|
|
|
|
# 只处理单个源
|
|
uv run python -m scripts.run_extractor --source sina --date 20260616
|
|
```
|
|
|
|
提取产物路径:`data/processed/{source_id}/{YYYYMMDD}/`
|
|
|
|
- `{url_hash}.json` 完整 Article(含正文)
|
|
- `index.jsonl` 扁平元数据(每行一条,不含正文,便于检索)
|
|
|
|
日志:`logs/extractor.log`
|
|
|
|
### 运行去重(M3)
|
|
|
|
M3 在 M2 输出之上做三层判重(URL Hash → 内容 Hash → SimHash 模糊),指纹持久化到 SQLite。
|
|
|
|
```bash
|
|
# 处理今日全部源
|
|
uv run python -m scripts.run_dedup
|
|
|
|
# 指定日期
|
|
uv run python -m scripts.run_dedup --date 20260616
|
|
|
|
# 只处理单源
|
|
uv run python -m scripts.run_dedup --source sina --date 20260616
|
|
|
|
# 调阈值/窗口
|
|
uv run python -m scripts.run_dedup --simhash-threshold 3 --window-days 30
|
|
|
|
# 重建指纹库(全量重跑时使用)
|
|
uv run python -m scripts.run_dedup --reset
|
|
```
|
|
|
|
去重产物路径:
|
|
|
|
- `data/dedup/fingerprints.sqlite3` 指纹库(跨日累积)
|
|
- `data/deduped/{YYYYMMDD}/uniques/{url_hash}.json` 唯一文章(可送 M4+ 处理)
|
|
- `data/deduped/{YYYYMMDD}/duplicates.jsonl` 重复记录(含命中层 / 命中目标)
|
|
|
|
日志:`logs/dedup.log`
|
|
|
|
### 运行 LLM 事件抽取(M4)
|
|
|
|
M4 调用 DeepSeek 或 Qwen,从 M3 唯一文章中抽取结构化投资事件(stock_codes/sentiment/importance/event_type 等)。
|
|
|
|
```bash
|
|
# 默认 DeepSeek(读 .env 的 DEEPSEEK_API_KEY)
|
|
uv run python -m scripts.run_event_extraction --date 20260616
|
|
|
|
# 切换 Qwen(百炼)
|
|
uv run python -m scripts.run_event_extraction --provider qwen --date 20260616
|
|
|
|
# 指定模型
|
|
uv run python -m scripts.run_event_extraction --provider qwen --model qwen-plus
|
|
|
|
# 联调小批量
|
|
uv run python -m scripts.run_event_extraction --limit 5
|
|
|
|
# 提速(并发,默认 3)
|
|
uv run python -m scripts.run_event_extraction --concurrency 5
|
|
|
|
# 跳过 M3,直接读 M2 处理产物(单源)
|
|
uv run python -m scripts.run_event_extraction --no-deduped --source cls
|
|
```
|
|
|
|
抽取产物路径:`data/events/{YYYYMMDD}/`
|
|
|
|
- `{url_hash}.json` 完整 ExtractedEvent
|
|
- `index.jsonl` 扁平摘要(每行一条)
|
|
- `failed.jsonl` 失败列表(若有)
|
|
|
|
环境变量(`.env`):
|
|
- `LLM_PROVIDER` 默认 `deepseek`,可选 `qwen`
|
|
- `LLM_MODEL` 覆盖默认模型
|
|
- `LLM_TEMPERATURE` 默认 0.1
|
|
- `LLM_TIMEOUT_SEC` 默认 60
|
|
- `DEEPSEEK_API_KEY` / `DEEPSEEK_BASE_URL`
|
|
- `DASHSCOPE_API_KEY` / `QWEN_BASE_URL`
|
|
|
|
日志:`logs/llm.log`
|
|
|
|
### 运行 Embedding 向量化(M5)
|
|
|
|
M5 把 M4 输出的事件用 DashScope `text-embedding-v3`(默认,1024 维)或本地 BGE-M3 向量化。
|
|
|
|
```bash
|
|
# 默认 DashScope(读 .env 的 DASHSCOPE_API_KEY)
|
|
uv run python -m scripts.run_embedding --date 20260616
|
|
|
|
# 强制覆盖 provider/model(避免 .env 旧值干扰)
|
|
uv run python -m scripts.run_embedding --provider dashscope --model text-embedding-v3
|
|
|
|
# 用 M3 唯一文章为输入(跳过 M4 事件)
|
|
uv run python -m scripts.run_embedding --input deduped
|
|
|
|
# 用 M2 处理产物为输入(跳过 M3/M4)
|
|
uv run python -m scripts.run_embedding --input articles --source cls
|
|
|
|
# 启用本地 BGE-M3(需先 uv sync --extra local-embedding)
|
|
uv run python -m scripts.run_embedding --provider local-bge
|
|
```
|
|
|
|
输出路径:`data/embeddings/{YYYYMMDD}/`
|
|
|
|
- `{url_hash}.json` 完整 EmbeddingResult(含 1024 维向量)
|
|
- `index.jsonl` 扁平摘要(不含向量)
|
|
- `failed.jsonl` 失败列表
|
|
|
|
环境变量(`.env`):
|
|
- `EMBEDDING_PROVIDER` 默认 `dashscope`,可选 `local-bge`
|
|
- `DASHSCOPE_EMBEDDING_MODEL` DashScope 模型(默认 `text-embedding-v3`)
|
|
- `LOCAL_EMBEDDING_MODEL` 本地模型(默认 `BAAI/bge-m3`)
|
|
- `DASHSCOPE_API_KEY` / `QWEN_BASE_URL`(M4 已配)
|
|
|
|
日志:`logs/embedding.log`
|
|
|
|
### 运行 Qdrant 入库与检索(M6)
|
|
|
|
M6 把 M5 的嵌入向量 + M4 的事件标签写入 Qdrant 本地知识库(文件模式,无需 Docker)。
|
|
|
|
```bash
|
|
# 入库(默认本地文件模式 data/qdrant_storage/)
|
|
uv run python -m scripts.run_qdrant_ingest --date 20260616
|
|
|
|
# 重建 collection + 全量入库
|
|
uv run python -m scripts.run_qdrant_ingest --recreate
|
|
|
|
# 试跑 N 条
|
|
uv run python -m scripts.run_qdrant_ingest --limit 10
|
|
|
|
# 指定存储路径
|
|
uv run python -m scripts.run_qdrant_ingest --path /mnt/data/qdrant
|
|
|
|
# 内存模式(测试)
|
|
uv run python -m scripts.run_qdrant_ingest --memory
|
|
```
|
|
|
|
检索示例:
|
|
|
|
```python
|
|
from vectorstore import VectorStore, SearchFilter, make_qdrant_client
|
|
import json
|
|
|
|
c = make_qdrant_client()
|
|
store = VectorStore(c)
|
|
|
|
# 基础语义检索
|
|
probe = json.load(open("data/embeddings/20260616/abc.json"))
|
|
hits = store.query(query_vector=probe["vector"], top_k=10)
|
|
|
|
# 结构化过滤
|
|
hits = store.query(
|
|
query_vector=probe["vector"], top_k=10,
|
|
filter=SearchFilter(source_id="cls", importance_min=3, sentiment="positive"),
|
|
)
|
|
store.close()
|
|
```
|
|
|
|
环境变量(`.env`):
|
|
- `QDRANT_COLLECTION` Collection 名(默认 `a_share_news`)
|
|
- `QDRANT_HOST`/`QDRANT_PORT` 远程 HTTP 模式(不常用)
|
|
|
|
日志:`logs/qdrant.log`
|
|
|
|
### 运行全链路定时任务(M7)
|
|
|
|
M7 把 M1→M6 串成一个 Pipeline,支持单次执行和定时守护。
|
|
|
|
```bash
|
|
# 立即执行一次全链路
|
|
uv run python -m scripts.run_scheduler --once
|
|
|
|
# 指定日期
|
|
uv run python -m scripts.run_scheduler --once --date 20260616
|
|
|
|
# 只执行部分步骤(逗号分隔)
|
|
uv run python -m scripts.run_scheduler --once --steps crawler,extractor,llm
|
|
|
|
# 启动定时守护进程(按 .env 中 SCHEDULE_TIMES 自动触发)
|
|
uv run python -m scripts.run_scheduler
|
|
```
|
|
|
|
定时时间由 `.env` 中 `SCHEDULE_TIMES` 控制(默认 `07:00,12:00,18:00,22:00`)。
|
|
|
|
Pipeline 总耗时约 4-5 分钟(100 篇文章),其中 M1 抓取(含浏览器渲染)最耗时(~3 分钟)。
|
|
|
|
日志:`logs/scheduler.log`
|
|
|
|
### MCP 服务(M8)
|
|
|
|
M8 暴露 5 个 MCP 工具给 Cherry Studio / Claude Code,对接 Qdrant 知识库。
|
|
|
|
**5 个工具**:
|
|
|
|
| 工具 | 功能 | 过滤字段 |
|
|
| --- | --- | --- |
|
|
| `search_news` | 通用语义检索 | - |
|
|
| `search_company_news` | 按公司检索 | company_names |
|
|
| `search_industry_news` | 按行业检索 | industries |
|
|
| `search_stock_events` | 按股票代码检索 | stock_codes |
|
|
| `search_sentiment_trend` | 按情绪检索+统计 | sentiment |
|
|
|
|
**Cherry Studio 配置**(设置 → MCP 服务器 → 添加):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"a-share-research": {
|
|
"command": "uv",
|
|
"args": ["run", "python", "-m", "scripts.run_mcp_server"],
|
|
"cwd": "/home/pi/news"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Claude Code 配置**(`.mcp.json`):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"a-share-research": {
|
|
"command": "uv",
|
|
"args": ["run", "python", "-m", "scripts.run_mcp_server"],
|
|
"cwd": "/home/pi/news"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**调试**(HTTP SSE 模式,浏览器访问 `http://<host>:8765/sse`):
|
|
|
|
```bash
|
|
uv run python -m scripts.run_mcp_server --sse 8765
|
|
```
|
|
|
|
日志:`logs/mcp_server.log`
|
|
|
|
**使用示例**(在 Cherry Studio / Claude Code 中输入):
|
|
|
|
```
|
|
用 search_news 搜索"宁德时代固态电池最新进展"
|
|
|
|
用 search_stock_events 查 300750 最近的重大事件
|
|
|
|
用 search_sentiment_trend 分析"AI 算力"相关新闻的情绪变化
|
|
|
|
用 search_company_news 搜"贵州茅台"关于价格调整的新闻
|
|
|
|
用 search_industry_news 搜"半导体"行业的最新政策
|
|
```
|
|
|
|
**工作原理**:每个查询 → DashScope 嵌入 → Qdrant 语义检索(2.3ms)→ 格式化 Markdown 返回。
|
|
|
|
#### systemd 部署(推荐)
|
|
|
|
将调度器安装为系统服务,开机自启、崩溃自动重启。
|
|
|
|
```bash
|
|
# 安装
|
|
sudo cp scripts/a-share-research.service /etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable a-share-research
|
|
|
|
# 日常操作
|
|
sudo systemctl start a-share-research # 启动
|
|
sudo systemctl stop a-share-research # 停止
|
|
sudo systemctl restart a-share-research # 重启
|
|
sudo systemctl status a-share-research # 查看状态
|
|
|
|
# 查看日志
|
|
journalctl -u a-share-research -f # 实时
|
|
tail -f logs/scheduler.log # 文件
|
|
```
|
|
|
|
服务进程:`uv run python -m scripts.run_scheduler`(PID 见 `systemctl status`)
|
|
|
|
### 修改新闻源
|
|
|
|
编辑 `configs/sources.yaml`,字段说明见文件顶部注释。新增源**无需改代码**(配置驱动原则)。
|
|
|
|
### 运行测试
|
|
|
|
```bash
|
|
uv run pytest # 单元测试(默认排除集成)
|
|
uv run pytest -m integration # 真实抓取烟测(需联网+浏览器)
|
|
uv run pytest tests/test_crawler.py -v
|
|
uv run pytest tests/test_crawler.py::test_extract_article_links_basic
|
|
uv run pytest --cov=crawler # 覆盖率
|
|
```
|
|
|
|
### 代码质量
|
|
|
|
```bash
|
|
uv run ruff check . # Lint
|
|
uv run ruff format . # 格式化
|
|
uv run mypy crawler/ # 类型检查
|
|
```
|
|
|
|
### Docker(M6 起启用)
|
|
|
|
```bash
|
|
docker compose --profile m6 up -d # 启动 Qdrant
|
|
docker compose logs -f
|
|
docker compose down
|
|
```
|
|
|
|
---
|
|
|
|
## 目录结构
|
|
|
|
```
|
|
news/
|
|
├─ crawler/ # M1 新闻抓取(Crawl4AI)
|
|
├─ extractor/ # M2 中文正文提取(GNE)
|
|
├─ dedup/ # M3 三层去重
|
|
├─ llm/ # M4 投资事件抽取
|
|
├─ embedding/ # M5 向量生成
|
|
├─ vectorstore/ # M6 Qdrant 客户端封装
|
|
├─ scheduler/ # M7 定时任务
|
|
├─ mcp_server/ # M8 MCP 服务
|
|
├─ api/ # 对外接口
|
|
├─ app/ # 应用入口
|
|
├─ configs/ # 网站源 / 系统配置(YAML)
|
|
├─ prompts/ # LLM Prompt 模板
|
|
├─ tests/ # pytest 测试
|
|
├─ scripts/ # 运维脚本(含 run_crawler)
|
|
├─ docs/ # 设计文档与决策记录
|
|
├─ logs/ # 运行日志(git ignored)
|
|
├─ data/ # 抓取与缓存(git ignored)
|
|
├─ pyproject.toml
|
|
├─ docker-compose.yml
|
|
├─ .env.example
|
|
├─ CLAUDE.md # Claude Code 开发约束
|
|
├─ project_plan.md # 项目计划
|
|
├─ continuation.md # 上下文恢复
|
|
└─ README.md
|
|
```
|
|
|
|
---
|
|
|
|
## 开发约束
|
|
|
|
详见 `CLAUDE.md`。关键纪律:
|
|
|
|
- 分阶段开发,每个 Milestone 验收通过后再进入下一个;
|
|
- 配置驱动,不硬编码;
|
|
- 异常必须记录日志并显式抛出;
|
|
- 所有新增代码须含类型注解;
|
|
- Prompt 单独文件维护,不写死在代码里;
|
|
- 代码用英文命名,注释/文档用中文。
|
|
|
|
---
|
|
|
|
## 许可
|
|
|
|
私有项目,未授权禁止使用与传播。
|