Skip to content

11_Tech_Agent_Design

Wyckoff edited this page Jun 1, 2026 · 48 revisions

Agent 设计专题

本页记录 WyckoffAgent 当前代码中的 Agent 工程设计,重点解释 CLI/TUI 里的完整 Agent Runtime,以及 Web、MCP 如何复用工具层但采用不同运行时。

代码核对时间:2026-06-01,主仓库 main@942a34d。本页以当前代码为准;历史 Streamlit MVP 不再属于 main 的 Agent 架构。


0. 当前实现边界

WyckoffAgent 不是单一入口,而是三条通道共享同一套金融能力:

通道 入口 运行时 对话/工具编排 适合场景
React Web / CF Pages web/apps/web/ Vercel AI SDK streamText Web 端独立工具循环,stopWhen(stepCountIs(10)) 在线读盘室、浏览器用户
CLI / TUI wyckoff cli/runtime.py::AgentRuntime 完整 ReAct loop、记忆、上下文压缩、后台任务、sub-agent 本地深度投研和工具型 Agent
MCP Server wyckoff-mcp FastMCP stdio 无内置对话 loop,只暴露工具 Claude Code / Cursor 等外部 Agent 调用

共享层是 agents/chat_tools.py 和核心分析引擎;不同入口只是在运行时、权限、展示和状态管理上分化。

关键边界:

  • CLI/TUI 是最完整的 Agent Runtime:负责 provider 调用、工具执行、并发分批、上下文压缩、Loop Guard、doom-loop 检测、scratchpad、后台任务和 sub-agent。
  • Web 不复用 AgentRuntime:它使用 web/apps/web/src/lib/chat-agent.ts 中的 Vercel AI SDK 工具循环,保留流式输出、工具调用、上下文本地摘要和 reasoning 透传,但不接入 CLI 的 SQLite 记忆、scratchpad 和 sub-agent。
  • MCP 不具备会话智能mcp_server.py 只是把 Wyckoff 能力注册成 MCP tools,推理和多轮编排由外部客户端负责。

1. CLI Agent Runtime

核心实现:cli/runtime.py::AgentRuntime

它把一次用户请求拆成一个 provider-agnostic 的事件循环:

flowchart TD
    U["用户消息"] --> M["记忆召回 / 系统提示"]
    M --> C["上下文检查与压缩"]
    C --> L["provider.chat_stream"]
    L --> K["消费 chunk"]
    K -->|text_delta| T["流式渲染"]
    K -->|thinking_delta| R["推理内容记录"]
    K -->|tool_calls| A["追加 assistant tool message"]
    A --> B["按工具并发安全性分批"]
    B --> E["执行工具"]
    E --> O["tool observation 回灌 messages"]
    O --> C
    K -->|无工具调用| G["Loop Guard 检查必需工具"]
    G -->|漏调| P["注入 retry user message"]
    P --> C
    G -->|完成| D["done event / 保存日志"]
Loading

run_stream() 只产出统一的 RuntimeEvent 字典。TUI、headless wrapper、sub-agent 和测试都消费这些事件,而不是各自重新实现模型调用和工具循环。

主要事件:

事件 来源 用途
text_delta Provider 流式文本 TUI 逐行渲染
thinking_delta / thinking reasoning chunk / round 结束 推理展示和 scratchpad 记录
tool_calls 模型请求调用工具 清除临时流式文字,进入工具执行
tool_start 工具开始 展示工具名、参数、spinner
tool_result / tool_error 工具结束 回灌 observation,更新执行摘要
model_start 多轮工具调用的新一轮模型推理 恢复“思考中”状态
compaction 上下文被压缩 提示消息条数变化
retry Loop Guard 触发 强制模型补调用必需工具
usage Provider token 统计 状态栏计费/用量
done 最终回答 保存 chat log,结束本轮

2. Provider 抽象

Provider 接口定义在 cli/providers/base.py

class LLMProvider:
    def chat(self, messages, tools, system_prompt="") -> dict: ...
    def chat_stream(self, messages, tools, system_prompt="") -> Generator[dict, None, None]: ...
    @property
    def name(self) -> str: ...

Provider 的职责是把不同模型 SDK 的响应转换成统一 chunk:

  • text_delta:普通文本。
  • thinking_delta:DeepSeek 等模型返回的 reasoning 内容。
  • tool_calls:标准化为 {"id", "name", "args"}
  • usage:输入/输出 token,以及可用时的 cache read/write token。

当前 CLI 主要 provider:

Provider 文件 说明
Claude cli/providers/claude.py Claude tool_use / tool_result 双向转换
OpenAI-compatible cli/providers/openai.py OpenAI、DeepSeek、Qwen、Kimi 等兼容端点
Gemini cli/providers/gemini.py Gemini function calling 适配
Fallback cli/providers/fallback.py 限流、超时、服务端错误时切换备用 provider
Routing cli/model_router.py 按轮次复杂度在 heavy/light 模型间路由

OpenAI-compatible provider 有三类兼容处理:

  1. 首次调用携带 stream_optionstool_choicefrequency_penalty
  2. 第三方端点不支持时逐步去掉不兼容参数。
  3. 对把工具调用输出成 <tool_call>...</tool_call> 文本的模型做兜底解析。

流式读取由 _iter_with_timeout() 包装:生产者 daemon thread 负责读 stream,主线程从 queue 取 chunk;60 秒无新 chunk 就抛出超时,避免 TCP 半开连接把 TUI 卡死。


3. 工具注册与调度

CLI 工具系统在 cli/tools.py

  • TOOL_SCHEMAS:给模型看的 JSON Schema。
  • ToolSpec:运行时行为元数据,包括中文展示名、是否并发安全、是否需要确认、是否后台执行。
  • ToolRegistry:工具注册、provider/context 注入、确认回调、后台任务提交和实际执行。
  • ToolContext:跨工具共享 stateproviderregistryon_progress,也是 sub-agent 委派的桥。

CLI 当前注册 19 个工具:

类型 工具
金融分析 search_stock_by_nameanalyze_stockportfolioget_market_overviewget_market_historyscreen_stocksgenerate_ai_reportgenerate_strategy_decisionquery_historyrun_backtest
数据修改 update_portfolio
后台任务 check_background_tasks
Sub-agent 委派 delegate_to_researchdelegate_to_analysisdelegate_to_trading
本地工具 exec_commandread_filewrite_fileweb_fetch

调度策略:

  • concurrency_safe=True 的工具可在同一轮连续工具调用中并行执行,最大 ThreadPoolExecutor(max_workers=5)
  • 非并发安全工具串行执行,避免写操作、后台任务或外部副作用互相踩踏。
  • requires_approval=True 的工具在 TUI 中弹确认框:允许一次、总是允许、修改后执行或拒绝。
  • background=True 且 TUI 注入了 BackgroundTaskManager 时,工具立即返回 task_id,实际任务在 daemon thread 中运行。

当前并发安全工具:

search_stock_by_nameanalyze_stockportfolioget_market_overviewget_market_historyquery_history

当前高风险确认工具:

update_portfolioexec_commandwrite_file

当前后台工具:

screen_stocksgenerate_ai_reportgenerate_strategy_decisionrun_backtest


4. 上下文管理与压缩

上下文压缩逻辑在 cli/compaction.py,TUI 和 headless runtime 共用。

触发规则:

  • 根据模型名匹配 context window,如 Claude 200K、Gemini 2 1M、OpenAI/Gemini 常见 128K、DeepSeek 64K。
  • COMPACT_RATIO = 0.25,达到窗口 25% 触发压缩。
  • 最近上下文按 token budget 保留,默认最多保留 20K token,最少保留 4K token,并保证至少 4 条消息。
  • tail 边界不会从 tool 消息开始;如果 tail 中有 tool_call_id,会向前扩展,把对应 assistant tool call 一并纳入,避免孤儿工具结果。

压缩流程:

flowchart LR
    A["messages 超阈值"] --> B["按 token budget 切 head/tail"]
    B --> C["压缩前 memory flush"]
    C --> D["工具结果智能摘要"]
    D --> E["LLM 生成 500 字以内摘要"]
    E --> F["[对话摘要] + 接续确认 + tail"]
Loading

工具结果不会粗暴截断:

  • analyze_stock 保留 code、name、channel、phase、trigger_signals、health 等字段。
  • portfolio 保留 position_count、free_cash、positions 或 diagnostics。
  • 通用结果保留 error、message、status、code、name、result。

每轮工具执行后,shrink_stale_tool_results() 会先压缩旧轮次的大 tool result,保留最新一轮完整结果,减少多轮工具调用中的 token 污染。

超大工具结果写回模型前还会经过 cli/tool_results.py::format_tool_result_for_context();超过阈值的内容会落盘到 ~/.wyckoff/tool-results/,上下文只保留引用、预览和结构化索引。

Web 端也有上下文压缩,但它是 TypeScript 本地摘要:prepareChatMessagesForModel() 根据同样的 context-window/25% 口径构造 [读盘室对话摘要],不调用 CLI compactor,也不写入 CLI 记忆。


5. 记忆系统

记忆系统在 cli/memory.py,只服务 CLI/TUI。

目标是记住稳定、可复用的信息,而不是把每天行情和临时买卖事实都塞进长期记忆。

自动抽取的 L1 原子记忆只有两类:

  • [偏好]preference:投资风格、禁忌、操作习惯。
  • [决策]decision:非显而易见的决策逻辑和原因。

明确不抽取:

  • 具体买卖了哪只股票。
  • 临时调仓事实。
  • 当天市场状态。
  • 工具调用细节。

分层记忆:

层级 类型 生成方式
L1 preference / decision 会话结束后从最近 40 条消息提取
L2 scenario L1 数量达到阈值后刷新可复用场景
L3 persona L1 数量达到阈值后刷新用户画像

写入时机:

  1. TUI 退出或保存时异步调用 save_session_summary()
  2. 上下文压缩前调用 flush_memory_before_compaction(),避免旧上下文被摘要前丢失稳定偏好。
  3. L1 记忆达到数量条件后调用 refresh_memory_layers() 生成 L2/L3。

召回流程:

flowchart TD
    U["当前用户消息"] --> C["抽取股票代码"]
    U --> K["抽取中文关键词"]
    U --> F["FTS5 全文检索"]
    C --> S["代码精确匹配"]
    K --> L["关键词 LIKE 匹配"]
    F --> R["混合排序 + 30 天半衰期"]
    S --> R
    L --> R
    R --> P["Persona / Preference 置顶"]
    P --> I["注入 <relevant-memories>"]
Loading

注入格式通过 prepend_memory_context() 把召回记忆包在 <relevant-memories> 中,再把当前用户消息包在 <current-user-message> 中。这样模型可以参考长期偏好,但不会把记忆误当成当前任务进度。

每条记忆保留 source_ref=chat_log:<session_id>。CLI 可用 wyckoff memory trace <id> 回看来源,避免长期摘要变成不可验证黑盒。


6. Loop Guard 与死循环保护

模型偶尔会只输出计划、不调用工具。cli/loop_guard.py 把部分数据型任务从 prompt 约束提升为运行时约束。

典型约束:

用户意图 必须工具
查看持仓 portfolio(mode="view")
持仓体检 / 诊断 portfolio(mode="diagnose")
上下文跟进“做一下体检” 结合最近消息判断仍需 portfolio

如果模型漏调必需工具,runtime 会注入 retry user message,最多重试 2 次;仍失败则把警告前置到最终回答里。

Doom-loop 防护用于阻止同一工具同参数反复调用:

  • 精确匹配:最近窗口中同一 (tool_name, args_hash) 出现 3 次。
  • 模糊匹配:长参数文本做 3-gram Jaccard,相似度过高也视为重复。
  • check_background_tasks 这类轮询工具豁免。

触发后 runtime 会写入工具错误 observation,并中止本轮剩余工具调用,避免模型把外部 API 或本地任务打爆。


7. 后台任务

后台任务在 cli/background.py,由 TUI 初始化并注入 ToolRegistry

适合耗时较长但不应该阻塞对话的工具:

  • screen_stocks:全市场漏斗。
  • generate_ai_report:AI 深度研报。
  • generate_strategy_decision:攻防决策。
  • run_backtest:策略回测。

执行方式:

flowchart TD
    A["模型调用 background tool"] --> B["ToolRegistry.submit"]
    B --> C["BackgroundTaskManager 创建 task"]
    C --> D["daemon thread 执行真实函数"]
    B --> E["立即返回 task_id 给模型"]
    D --> F["progress callback 更新 TUI 面板"]
    D --> G["on_complete 注入完成通知"]
Loading

任务内部可通过 cli.progress 上报 stage、detail、progress。TUI 顶部的 BackgroundTaskPanel 展示活跃任务,用户可以继续提问;任务完成后会通过 callback 通知当前会话。


8. Sub-Agent 编排

Sub-agent 基础设施在 cli/sub_agents.py,当前只有 CLI/TUI 可用。

三个内置角色:

Sub-agent 职责 工具子集
research 数据收集、全市场扫描、信号、复盘、回测 search_stock_by_nameanalyze_stockget_market_overviewget_market_historyquery_historyscreen_stocksrun_backtestcheck_background_tasks
analysis 个股诊断、持仓体检、AI 研报 analyze_stockportfolioget_market_overviewget_market_historygenerate_ai_report
trading 去留决策、攻防指令、调仓执行 portfolioupdate_portfoliogenerate_strategy_decisionanalyze_stockget_market_overviewget_market_history

主 Agent 通过 delegate_to_researchdelegate_to_analysisdelegate_to_trading 调用子 Agent。

实现要点:

  • SubAgentToolProxy 只暴露允许的 schemas,并在执行时拒绝越权工具。
  • 每个 sub-agent 启动自己的 AgentRuntime mini loop,使用独立 system prompt 和上下文。
  • 子 Agent 使用同一个 provider 和 ToolRegistry,所以能共享登录态、数据源和确认机制。
  • TUI 通过 tool_context.on_progress 转发子 Agent 的 text_deltatool_starttool_resultdone 事件,以灰色斜体展示执行进度。

这个设计把“主 Agent 负责路由、规划、预算和最终回答”和“子 Agent 专注单类任务”拆开,减少复杂投研任务把主会话上下文搅乱。


9. Scratchpad 与可观测性

cli/scratchpad.py 为每个 CLI/TUI turn 写一份 JSONL trace 到 ~/.wyckoff/scratchpad/

记录内容:

事件 字段
init 用户输入、session_id
thinking 模型 reasoning 内容
tool_result 工具名、参数、结果、耗时、状态
compaction 压缩前后消息数
final 最终回复、token、耗时
error 异常信息

所有明显敏感字段会脱敏:api_keytokenpasswordsecretauthorizationcookie

Scratchpad 独立于 SQLite chat log:即使中途崩溃、工具超时或长任务异常,也能留下足够证据复盘“模型为什么这样回答”。


10. Web 端 Agent

Web 端核心文件:web/apps/web/src/lib/chat-agent.ts

它不是 CLI Runtime 的移植,而是面向 CF Pages 的独立实现:

  • 使用 streamText() 做多步工具调用。
  • stopWhen(stepCountIs(10)) 限制最大工具轮数。
  • 自己维护 StepInfo,向前端回调 onSteponTextDeltaonFinishonError
  • 使用 prepareChatMessagesForModel() 做本地摘要式上下文压缩。
  • 通过 /api/llm-proxy 代理模型请求,统一 base_url、安全校验和错误处理。
  • buildReasoningFetch() 解析 SSE 中的 reasoning_content,并在下一轮补回 assistant message,兼容 DeepSeek 等模型的 thinking mode。

Web 工具当前包括:

search_stockview_portfoliomarket_overviewmarket_historyquery_recommendationsquery_tail_buyplan_portfolio_updateexecute_portfolio_updateanalyze_stockscreen_stocksgenerate_ai_reportgenerate_strategy_decisionintraday_analysis

与 CLI 的差异:

  • Web 没有本地 shell/file/web_fetch 工具。
  • Web 没有 CLI SQLite 记忆和 scratchpad。
  • Web 调仓是 plan_portfolio_update + execute_portfolio_update 两步,靠 system prompt 和工具分离做确认边界。
  • Web 的 screen_stocks 是读取最新漏斗结果,不是在用户请求时启动 CLI 后台漏斗。

11. MCP Server

MCP 入口:mcp_server.py

MCP Server 使用 FastMCP("wyckoff") 注册工具,外部 Agent 通过 stdio 调用。

当前 MCP 工具有三类:

权限层 工具
Tier 1:本地历史 query_history
Tier 2:行情/引擎 search_stock_by_nameanalyze_stockget_market_overviewscreen_stocksrun_backtestmarket_regimewyckoff_diagnoseintraday_analysisintraday_rescue_checkrun_funnel_simulation
Tier 3:用户数据/LLM portfolioupdate_portfoliogenerate_ai_reportgenerate_strategy_decision

MCP 会从环境变量或本地 CLI 登录态构造 ToolContext

  • SUPABASE_USER_ID
  • SUPABASE_ACCESS_TOKEN
  • SUPABASE_REFRESH_TOKEN

与 CLI/Web 的关键区别是:MCP 自身不做 memory、compaction、retry、sub-agent 或后台面板。它只返回一次工具调用结果,复杂编排由 Claude Code、Cursor 等 MCP 客户端完成。


12. Skills 与 Prompt 模板

Skills 在 cli/skills.py,本质是“预设 user message 模板”,执行后仍走完整 CLI Agent Runtime。

内置 Skills:

命令 作用
/screen 全市场漏斗筛选
/checkup 持仓健康体检
/report AI 深度研报
/strategy 攻守决策
/backtest 策略回测

用户可以在 ~/.wyckoff/skills/*.md 中新增 skill。支持 front matter:

---
name: dcf
description: DCF 估值分析
---
请对 {user_input} 进行 DCF 估值分析。

Prompt 模板在 cli/prompt_templates.py,用于更结构化的投研任务;TUI /help 同时展示内置命令、prompt 模板和 skills。


13. LLM 智能路由

cli/model_router.py 提供 heavy/light 模型路由。

分类规则:

判为 heavy 条件
多工具结果待合成 最近消息中 tool 结果数量 >= 2
投研关键词 分析、诊断、策略、研报、决策、回测、漏斗、筛选、持仓、复盘等
长消息 最后一条用户消息超过 80 字
缺少用户消息 保守默认 heavy

其余简单确认、闲聊和短问题可走 light 模型。RoutingProvider 实现同一个 LLMProvider 接口,所以 AgentRuntime 不需要知道当前轮次到底选了哪个 provider。


设计原则总结

原则 当前实现
Runtime 统一 CLI/TUI/sub-agent/test 共享 AgentRuntime 事件循环
通道分层 Web、CLI、MCP 共享工具能力,但不强行共享同一运行时
数据先行 Loop Guard 把部分工具调用从 prompt 约束提升为状态机约束
工具可治理 schema、确认、并发安全、后台执行都在 ToolSpec / ToolRegistry 中声明
上下文可控 token budget 压缩、旧工具结果摘要、超大结果落盘
记忆克制 只沉淀稳定偏好和决策逻辑,不把临时行情写成长记忆
任务可拆 research / analysis / trading sub-agent 用工具代理隔离能力边界
运行可追溯 scratchpad + chat log + source_ref 支撑问题复盘
Provider 无关 Claude / OpenAI-compatible / Gemini / fallback / routing 都收敛到同一接口
失败可恢复 超时、fallback、retry、doom-loop 中止和后台任务状态共同兜底

Clone this wiki locally