-
Notifications
You must be signed in to change notification settings - Fork 163
11_Tech_Agent_Design
本页记录 WyckoffAgent 当前代码中的 Agent(智能体)工程设计,重点解释 CLI(命令行)/ TUI(终端图形界面)里的完整 Agent Runtime(智能体运行时),以及 Web(网页端)、MCP(模型上下文协议)如何复用工具层但采用不同运行时。
代码核对时间:2026-06-02,主仓库
main@c456715。本页以当前代码为准;历史 Streamlit MVP 不再属于main的 Agent 架构。
本页只描述公开系统设计和代码结构,不包含个人运行数据、账户信息或本地历史内容。
| 英文术语 | 中文释义 |
|---|---|
| Agent | 智能体:能根据目标自主选择工具、多轮执行并输出结果的程序 |
| Runtime | 运行时:负责模型调用、工具执行、循环控制、上下文压缩和追踪 |
| CLI | 命令行界面:通过终端命令使用系统 |
| TUI | 终端图形界面:在终端里显示类似 GUI 的交互界面 |
| Web | 网页端:浏览器里的在线读盘室 |
| CF Pages | Cloudflare Pages 边缘页面:承载 Web 前端和边缘函数 |
| MCP | 模型上下文协议:给 Claude Code、Cursor 等外部 Agent 暴露工具的协议 |
| Provider | 模型适配层:把不同模型供应商统一成同一套调用接口 |
| Tool Schema | 工具定义:告诉模型工具名、用途和参数格式 |
| Tool Call | 工具调用:模型决定调用某个工具并给出参数 |
| ReAct | 推理-行动循环:模型先推理,再调用工具,观察结果后继续推理 |
| Loop Guard | 循环守卫:运行时强制模型补调必须工具的保护机制 |
| Doom-loop | 死循环:模型反复用同一参数调用同一工具 |
| Scratchpad | 运行追踪文件:记录一轮对话中的模型、工具、压缩和最终结果 |
| Sub-agent | 子智能体:只负责某类任务的小 Agent |
| Context Window | 上下文窗口:模型一次请求能容纳的最大上下文长度 |
| Compaction | 上下文压缩:把旧对话压成摘要,给后续轮次腾空间 |
| Memory | 记忆:跨会话保留的稳定偏好和决策逻辑 |
| Fallback | 故障切换:主模型失败时自动换备用模型 |
| Reasoning | 推理内容:部分模型输出的内部思考过程 |
| Token | 令牌:模型上下文长度和计费的基本单位 |
| SSE | 服务端事件流:流式响应的一种传输格式 |
| Daemon Thread | 守护线程:后台线程,进程退出时自动回收 |
| System Prompt | 系统提示词:约束模型行为的顶层指令 |
| Skill | 技能模板:预设的一类用户任务提示 |
WyckoffAgent 不是单一入口,而是三条通道共享同一套金融能力:
| 通道 | 入口 | 运行时 | 对话/工具编排 | 适合场景 |
|---|---|---|---|---|
| React Web(网页端)/ CF Pages(Cloudflare Pages 边缘页面) | web/apps/web/ |
Vercel AI SDK(Vercel 的 AI 工具包)streamText(流式文本生成) |
Web 端独立工具循环,stopWhen(stepCountIs(10))(达到 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(Vercel 的 AI 工具包)工具循环,保留流式输出、工具调用、上下文本地摘要和 reasoning(推理内容)透传,但不接入 CLI 的 SQLite(本地轻量数据库)记忆、scratchpad(运行追踪文件)和 sub-agent(子智能体)。 -
MCP 不具备会话智能:
mcp_server.py只是把 Wyckoff 能力注册成 MCP tools(模型上下文协议工具),推理和多轮编排由外部客户端负责。
核心实现:cli/runtime.py::AgentRuntime。
它把一次用户请求拆成一个 provider-agnostic(不绑定具体模型供应商)的事件循环。这里保留两张图:
- 简版图:用于快速理解 Agent Runtime(智能体运行时)的主循环,适合作为本节的入口视图。
- 详版图:用于展开工程细节,说明记忆、压缩、工具审批、后台任务、大结果落盘、错误回灌和循环保护。
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 / 保存日志"]
简版图只表达一件事:Agent 不是一次模型问答,而是“模型推理 → 工具执行 → 观察结果回灌 → 再次推理”的闭环。先看这张图可以快速建立整体理解,再继续阅读后面的工程细节。
详版图沿着三条线展开:
- 模型前处理:识别必需工具、召回记忆、压缩旧工具结果、必要时压缩上下文。
- 工具执行治理:最大轮数、并发/串行分批、高风险审批、后台任务、死循环检测。
- 模型后处理:工具结果或错误回灌、大结果落盘、漏调必需工具时纠偏、最终保存日志。
flowchart TD
U["用户消息"] --> X["resolve_turn_expectation<br/>识别本轮必需工具"]
X --> M["build_memory_context<br/>召回相关记忆"]
M --> I["prepend_memory_context<br/>临时注入 relevant-memories"]
I --> S["shrink_stale_tool_results<br/>压缩旧工具结果"]
S --> C{"估算 token(令牌)是否超过<br/>压缩阈值"}
C -->|是| C1["flush_memory_before_compaction<br/>压缩前提取持久偏好"]
C1 --> C2["compact_messages<br/>生成 500 字以内摘要"]
C -->|否| L
C2 --> L["provider.chat_stream<br/>模型流式调用"]
L --> TO["_iter_with_timeout<br/>60 秒无 chunk 则超时"]
TO --> K["消费 chunk<br/>流式片段"]
K -->|text_delta| T["RuntimeEvent:text_delta<br/>TUI 流式渲染"]
K -->|thinking_delta| R["RuntimeEvent:thinking_delta<br/>记录推理内容"]
K -->|usage| U1["RuntimeEvent:usage<br/>累计 token 用量"]
K -->|tool_calls| A["追加 assistant tool message<br/>保留 tool_call_id"]
A --> MR{"超过 MAX_TOOL_ROUNDS=15"}
MR -->|是| ML["done:工具调用轮次超限"]
MR -->|否| B["partition_tool_calls<br/>按并发安全性分批"]
B --> BC{"concurrency_safe"}
BC -->|是| P1["ThreadPoolExecutor<br/>最多 5 线程并发"]
BC -->|否| P2["串行执行<br/>避免写操作互相踩踏"]
P1 --> DL
P2 --> DL
DL{"doom-loop 检测<br/>最近 6 次内重复 3 次"}
DL -->|触发| AB["追加 tool_error<br/>中止本轮工具批次"]
DL -->|未触发| AP{"requires_approval<br/>是否高风险工具"}
AP -->|是| CF["确认弹窗<br/>允许 / 总是允许 / 修改 / 拒绝"]
AP -->|否| BG
CF --> BG{"background<br/>是否后台任务"}
BG -->|是| BT["BackgroundTaskManager<br/>返回 task_id"]
BG -->|否| EX["ToolRegistry.execute<br/>执行确定性 Python 函数"]
EX --> ER{"工具是否报错"}
ER -->|是| TE["tool_error<br/>错误回灌给模型"]
ER -->|否| TR["tool_result<br/>结果回灌给模型"]
BT --> TR
TE --> FMT
TR --> FMT["format_tool_result_for_context<br/>大结果超过 50,000 字符落盘"]
FMT --> O["追加 tool observation<br/>写回 messages"]
AB --> O
O --> C
K -->|无工具调用| G{"Loop Guard<br/>必需工具是否漏调"}
G -->|漏调且重试小于 2| RT["注入 retry user message<br/>要求直接调用必需工具"]
RT --> C
G -->|漏调且重试耗尽| W["追加不可靠警告"]
W --> D
G -->|未漏调| D["done event<br/>保存 chat log / scratchpad"]
ML --> D
详版图不是要求一次性读完,而是作为工程细节索引。需要理解大结果处理时,可以看 format_tool_result_for_context 这条线;需要理解模型漏调工具时,可以看 Loop Guard 这条线;需要理解重复工具调用保护时,可以看 doom-loop 这条线。
run_stream() 只产出统一的 RuntimeEvent(运行时事件)字典。TUI(终端图形界面)、headless wrapper(无界面兼容封装)、sub-agent(子智能体)和测试都消费这些事件,而不是各自重新实现模型调用和工具循环。
两张图共同强调两个设计重点:
- 模型前后都由确定性代码兜底:进入模型前先做记忆召回、旧结果压缩、上下文压缩;模型返回后再做工具分批、审批、后台任务、大结果落盘和错误回灌。
- 结束条件不是“模型说完了”:无工具调用时还要经过 Loop Guard(循环守卫)检查;漏调必需工具会注入 retry user message(纠偏用户消息),最多 2 次,仍失败才带警告结束。
主要事件:
| 事件 | 来源 | 用途 |
|---|---|---|
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(聊天日志),结束本轮 |
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(缓存读写令牌)。
为了降低 API 的输入(Input)开销,缩短首字响应延迟(TTFT),Claude Provider (cli/providers/claude.py) 实现了高度精准的滑动 Prompt 缓存(Prompt Caching)机制。
-
静态基底缓存 (Static System Prompt Caching):系统提示词作为前置静态上下文,被包装为 Block 格式并标记
"cache_control": {"type": "ephemeral"}。由于系统提示词在整个会话中保持不变,它构成了缓存的稳定起点。 -
滑动边界推进 (Sliding Boundary Promotion):在每次向 Claude 发起 API 请求时,系统自动识别当前消息序列的最后一条消息,并在其最后一个 Content Block 上附加
"cache_control": {"type": "ephemeral"}。- 随着对话轮次的不断累加,这个缓存控制标记会随最后一轮消息向后滑动推进。
- 这种设计使 LLM 服务端能够缓存当前轮次之前 100% 的历史会话内容。后续对话发起时,只需计算并写入最新一轮增量消息的 Token,前序的几万 Token 均可通过缓存读取,最高可减免 90% 的输入 Token 计费并大幅降低时延。
在底层消息格式构建函数 _build_messages() 中,系统根据消息角色 (role) 分别实现了细粒度的缓存注入:
-
用户消息 (User):将文本内容包装为 Block 列表,在
text块上注入缓存控制标记:{ "role": "user", "content": [{"type": "text", "text": msg["content"], "cache_control": {"type": "ephemeral"}}] } -
助手消息 (Assistant):合并
content中的文本块与tool_use块,在最后一个内容 Block 上追加缓存标记:content[-1]["cache_control"] = {"type": "ephemeral"}
-
工具返回结果 (Tool):在转换后的
tool_result内容 Block 上附加缓存标记:content_block = { "type": "tool_result", "tool_use_id": msg.get("tool_call_id", ""), "content": result, } content_block["cache_control"] = {"type": "ephemeral"}
在流式响应解析 chat_stream() 中,系统通过监听 Claude SDK 的 message_start 事件,从其 usage 元数据字段中精准提取缓存相关的计数指标:
-
cache_read_input_tokens(缓存读取命中数):代表成功从服务端缓存中直接复用的 Token 数量(费率极低)。 -
cache_creation_input_tokens(缓存创建写入数):代表本轮新增写入缓存的 Token 数量(费率等同于普通输入 Token)。
这些指标被统一包装在返回的 usage 信息中并同步更新至 TUI 状态栏,为用户提供直观的成本控制和延迟优化统计反馈。
当前 CLI(命令行)主要 provider(模型适配层):
| Provider(模型适配层) | 文件 | 说明 |
|---|---|---|
| Claude | cli/providers/claude.py |
Claude tool_use / tool_result 双向转换及滑动 Prompt 缓存 |
| OpenAI-compatible(OpenAI 兼容接口) | cli/providers/openai.py |
OpenAI、DeepSeek、Qwen、Kimi 等兼容端点 |
| Gemini | cli/providers/gemini.py |
Gemini function calling 适配 |
| Fallback(故障切换) | cli/providers/fallback.py |
限流、超时、服务端错误时切换备用 provider(模型适配层) |
OpenAI-compatible provider(OpenAI 兼容模型适配层)有三类兼容处理:
- 首次调用携带
stream_options、tool_choice、frequency_penalty。 - 第三方端点不支持时逐步去掉不兼容参数。
- 对把工具调用输出成
<tool_call>...</tool_call>文本的模型做兜底解析。
流式读取由 _iter_with_timeout() 包装:生产者 daemon thread(守护线程)负责读 stream(流式响应),主线程从 queue(队列)取 chunk(流式片段);60 秒无新 chunk 就抛出超时,避免 TCP 半开连接把 TUI 卡死。
模型元数据由 cli/model_registry.py 和 cli/model_metadata.py 维护,覆盖 context window(上下文窗口)、reasoning(推理能力)和成本显示:
-
wyckoff model cost <id> --context-window N可以显式保存模型上下文窗口。 - 未显式配置时,
infer_context_window()会按模型名推断常见窗口;未知模型默认按 64K token(令牌)处理。 -
FallbackProvider(故障切换模型适配层)按默认模型和 fallback(备用模型)顺序尝试,只在限流、超时、网络错误和服务端错误等可恢复异常上切换;配置错误不会被静默吞掉。
CLI(命令行)工具系统在 cli/tools.py:
-
TOOL_SCHEMAS:给模型看的 JSON Schema(JSON 格式工具参数协议)。 -
ToolSpec:工具元数据,包括中文展示名、是否并发安全、是否需要确认、是否后台执行。 -
ToolRegistry:工具注册表,负责 provider(模型适配层)/ context(上下文)注入、确认回调、后台任务提交和实际执行。 -
ToolContext:工具上下文,跨工具共享state、provider、registry、on_progress,也是 sub-agent(子智能体)委派的桥。
CLI(命令行)当前注册 19 个工具:
| 类型 | 工具 |
|---|---|
| 金融分析 |
search_stock_by_name、analyze_stock、portfolio、get_market_overview、get_market_history、screen_stocks、generate_ai_report、generate_strategy_decision、query_history、run_backtest
|
| 数据修改 | update_portfolio |
| 后台任务 | check_background_tasks |
| Sub-agent(子智能体)委派 |
delegate_to_research、delegate_to_analysis、delegate_to_trading
|
| 本地工具 |
exec_command、read_file、write_file、web_fetch
|
调度策略:
-
concurrency_safe=True(并发安全)的工具可在同一轮连续工具调用中并行执行,最大ThreadPoolExecutor(max_workers=5)(5 线程线程池)。 - 非并发安全工具串行执行,避免写操作、后台任务或外部副作用互相踩踏。
-
requires_approval=True(需要用户审批)的工具在 TUI 中弹确认框:允许一次、总是允许、修改后执行或拒绝。 -
background=True(后台执行)且 TUI 注入了BackgroundTaskManager(后台任务管理器)时,工具立即返回task_id(任务编号),实际任务在 daemon thread(守护线程)中运行。
当前并发安全工具:
search_stock_by_name、analyze_stock、portfolio、get_market_overview、get_market_history、query_history
当前高风险确认工具:
update_portfolio、exec_command、write_file
为了防止模型越权执行高风险工具,系统设计了双重确认拦截机制:
-
静默拦截:若
requires_approval=True且未配置 confirm 回调时,ToolRegistry会直接阻断调用,告知模型必须先调用ask_user解释风险并获取同意。 -
上下文确认检索:
ToolRegistry.execute会在每轮调用前扫描messages对话历史。只有当在历史中检索到由ask_user返回的“用户同意”(如 "确认"、"继续"、"yes"、"allow")时,该高风险工具才会被放行执行。这有效防止了模型跳过询问直接操作真实仓位。
当前后台工具:
screen_stocks、generate_ai_report、generate_strategy_decision、run_backtest
上下文压缩逻辑在 cli/compaction.py,TUI(终端图形界面)和 headless runtime(无界面运行时)共用。
触发规则:
- context window(上下文窗口)优先来自用户配置的
context_window,也就是wyckoff model cost <id> --context-window N保存的值。 - 未配置时,根据模型名在
cli/model_metadata.py::infer_context_window()中做本地推断,例如 Claude 200K、Gemini 2 1M、OpenAI/GPT 常见 128K、DeepSeek 64K。 - 未知模型统一按 64K context window(上下文窗口)处理。
-
COMPACT_RESERVE_RATIO = 0.25的含义是预留 25% 上下文作为 safety reserve(安全缓冲),不是“已使用 25% 就压缩”。压缩阈值是context_window - reserve。 - 预留安全缓冲是为了给 system prompt(系统提示词)、tool schema(工具定义)、后续 tool message(工具消息)、retry message(纠偏消息)和最终输出留空间。工具型对话可能突然追加大结果,token(令牌)估算也有误差,等到窗口快满再压缩更容易导致请求失败或延迟上升。
- 最近上下文按 token budget(令牌预算)保留,默认最多保留 20K token(模型令牌),最少保留 4K token(模型令牌),并保证至少 4 条消息;这里的 4 条只是最低保底,实际会优先按最近 token(模型令牌)预算保留更多上下文。
- tail(尾部消息)边界不会从
tool消息开始;如果 tail 中有tool_call_id(工具调用编号),会向前扩展,把对应 assistant tool call(助手工具调用)一并纳入,避免孤儿工具结果。
当前阈值公式:
reserve = min(max(16_384, context_window * 25%), context_window / 2)
threshold = context_window - reserve
| 模型/来源 | Context Window(上下文窗口) | 预留缓冲 | 压缩阈值 |
|---|---|---|---|
| DeepSeek | 64K | 16.4K | 47.6K |
| GPT / OpenAI-compatible(OpenAI 兼容接口)常见模型 | 128K | 32K | 96K |
| Gemini 2 | 1M | 250K | 750K |
| Claude | 200K | 50K | 150K |
| 用户显式配置 | 以 context_window 为准 |
同一公式计算 | 同一公式计算 |
| 未知模型 | 64K | 16.4K | 47.6K |
压缩流程:
flowchart LR
A["messages(消息)超阈值"] --> B["按 token budget(令牌预算)<br/>切 head/tail(头部/尾部)"]
B --> C["压缩前 memory flush(记忆刷写)"]
C --> D["工具结果智能摘要"]
D --> E["LLM(大语言模型)生成 500 字以内摘要"]
E --> F["[对话摘要] + 接续确认 + tail(尾部消息)"]
工具结果不会粗暴截断:
-
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/,上下文只保留引用、预览和结构化索引。
当前实际阈值:
| 常量 | 当前值 | 含义 |
|---|---|---|
MAX_TOOL_RESULT_CHARS |
50,000 | 单次工具结果超过 5 万字符就落盘 |
PREVIEW_CHARS |
2,000 | 上下文里保留的大结果预览长度 |
SHRINK_THRESHOLD |
800 | 旧轮次工具结果超过 800 字符会被二次压缩 |
Web(网页端)也有上下文压缩,但它是 TypeScript(前端脚本语言)本地摘要:prepareChatMessagesForModel() 使用同样的“保留 safety reserve(安全缓冲)后再触发压缩”口径构造 [读盘室对话摘要],不调用 CLI compactor(命令行压缩器),也不写入 CLI(命令行)记忆。
记忆系统在 cli/memory.py,只服务 CLI(命令行)/ TUI(终端图形界面)。
目标是记住稳定、可复用的信息,而不是把每天行情和临时买卖事实都塞进长期记忆。
自动抽取的 L1 原子记忆只有两类:
-
[偏好]→preference:投资风格、禁忌、操作习惯。 -
[决策]→decision:非显而易见的决策逻辑和原因。
明确不抽取:
- 具体买卖了哪只股票。
- 临时调仓事实。
- 当天市场状态。
- 工具调用细节。
分层记忆:
| 层级 | 类型 | 生成方式 |
|---|---|---|
| L1 |
preference / decision
|
会话结束后从最近 40 条消息提取 |
| L2 | scenario |
L1 数量达到阈值后刷新可复用场景 |
| L3 | persona |
L1 数量达到阈值后刷新用户画像 |
写入时机:
- TUI(终端图形界面)退出或保存时异步调用
save_session_summary()。 - 上下文压缩前调用
flush_memory_before_compaction(),避免旧上下文被摘要前丢失稳定偏好。 - 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>"]
注入格式通过 prepend_memory_context() 把召回记忆包在 <relevant-memories>(相关记忆)中,再把当前用户消息包在 <current-user-message>(当前用户消息)中。这样模型可以参考长期偏好,但不会把记忆误当成当前任务进度。
每条记忆保留 source_ref=chat_log:<session_id>(来源引用)。CLI(命令行)可用 wyckoff memory trace <id> 回看来源,避免长期摘要变成不可验证黑盒。
容量控制在 integrations/local_db.py:preference 50 条、persona 5 条、scenario 20 条、session 50 条、fact 50 条、stock_opinion 30 条、decision 30 条、market_view 20 条。超出后按类型裁剪最旧记录;90 天以上的普通记忆会被清理,preference 和 persona 长期保留。
模型偶尔会只输出计划、不调用工具。cli/loop_guard.py 把部分数据型任务从 prompt(提示词)约束提升为运行时约束。
典型约束:
| 用户意图 | 必须工具 |
|---|---|
| 查看持仓 | portfolio(mode="view") |
| 持仓体检 / 诊断 | portfolio(mode="diagnose") |
| 上下文跟进“做一下体检” | 结合最近消息判断仍需 portfolio
|
如果模型漏调必需工具,runtime(运行时)会注入 retry user message(纠偏用户消息),最多重试 2 次;仍失败则把警告前置到最终回答里。
当前实际阈值:
| 常量 | 当前值 | 含义 |
|---|---|---|
MAX_TOOL_ROUNDS |
15 | 单轮用户请求最多 15 轮模型-工具往返 |
MAX_INCOMPLETE_TOOL_RETRIES |
2 | 漏调必需工具时最多纠偏 2 次 |
DOOM_LOOP_WINDOW |
6 | 死循环检测看最近 6 次工具调用 |
DOOM_LOOP_THRESHOLD |
3 | 同工具同参数出现 3 次即判定疑似死循环 |
DOOM_LOOP_EXEMPT |
check_background_tasks |
后台任务状态查询允许重复 |
Doom-loop(死循环)防护用于阻止同一工具同参数反复调用:
- 精确匹配:最近 6 次调用中同一
(tool_name, args_hash)出现 3 次。 - 模糊匹配:参数文本长度达到 50 字符后,3-gram Jaccard 相似度达到 0.8 的同工具调用出现 3 次。
-
check_background_tasks这类轮询工具豁免。
触发后 runtime(运行时)会写入工具错误 observation(观察结果),并中止本轮剩余工具调用,避免模型把外部 API(应用程序接口)或本地任务打爆。
后台任务在 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 完成回调注入通知"]
任务内部可通过 cli.progress 上报 stage(阶段)、detail(细节)、progress(进度)。TUI 顶部的 BackgroundTaskPanel(后台任务面板)展示活跃任务,用户可以继续提问。
后台任务运行完毕后,系统通过注入 [SYSTEM NOTIFICATION] 的方式唤醒并驱动 Agent Loop:
-
通知格式:任务的执行报告会被格式化为类似
[SYSTEM NOTIFICATION - NOT USER INPUT]\n<task-notification>\n...\n</task-notification>的结构注入到会话历史中。 - 规避幻觉:这种前缀和 XML 标签可明确告知模型该内容并非来自用户的即时输入,防止模型产生角色幻觉(如误以为是用户在发问),从而让模型能够自然地在对话中总结后台任务的产出并主动汇报用户。
Sub-agent(子智能体)基础设施在 cli/sub_agents.py,当前只有 CLI(命令行)/ TUI(终端图形界面)可用。
三个内置角色:
| Sub-agent(子智能体) | 职责 | 工具子集 |
|---|---|---|
research |
数据收集、全市场扫描、信号、复盘、回测 |
search_stock_by_name、analyze_stock、get_market_overview、get_market_history、query_history、screen_stocks、run_backtest、check_background_tasks
|
analysis |
个股诊断、持仓体检、AI 研报 |
analyze_stock、portfolio、get_market_overview、get_market_history、generate_ai_report
|
trading |
去留决策、攻防指令、调仓执行 |
portfolio、update_portfolio、generate_strategy_decision、analyze_stock、get_market_overview、get_market_history
|
主 Agent 通过 delegate_to_research、delegate_to_analysis、delegate_to_trading 调用子 Agent。
实现要点:
-
SubAgentToolProxy(子智能体工具代理)只暴露允许的 schemas(工具定义),并在执行时拒绝越权工具。 - 每个 sub-agent(子智能体)启动自己的
AgentRuntimemini loop(小型循环),使用独立 system prompt(系统提示词)和上下文。 - 子 Agent 使用同一个 provider(模型适配层)和 ToolRegistry(工具注册表),所以能共享登录态、数据源和确认机制。
- TUI(终端图形界面)通过
tool_context.on_progress转发子 Agent(子智能体)的text_delta、tool_start、tool_result、done事件,以灰色斜体展示执行进度。
这个设计把“主 Agent 负责路由、规划、预算和最终回答”和“子 Agent 专注单类任务”拆开,减少复杂投研任务把主会话上下文搅乱。
cli/scratchpad.py 为每个 CLI(命令行)/ TUI(终端图形界面)turn(对话轮次)写一份 JSONL trace(逐行 JSON 运行轨迹)到 ~/.wyckoff/scratchpad/。
记录内容:
| 事件 | 字段 |
|---|---|
init |
用户输入、session_id |
thinking |
模型 reasoning(推理内容) |
tool_result |
工具名、参数、结果、耗时、状态 |
compaction |
压缩前后消息数 |
final |
最终回复、token、耗时 |
error |
异常信息 |
所有明显敏感字段会脱敏:api_key、token、password、secret、authorization、cookie。
Scratchpad(运行追踪文件)独立于 SQLite chat log(本地聊天日志):即使中途崩溃、工具超时或长任务异常,也能留下足够证据复盘“模型为什么这样回答”。
Web(网页端)核心文件:web/apps/web/src/lib/chat-agent.ts。
它不是 CLI Runtime(命令行智能体运行时)的移植,而是面向 CF Pages(Cloudflare Pages 边缘页面)的独立实现:
- 使用
streamText()(流式文本生成)做多步工具调用。 -
stopWhen(stepCountIs(10))(达到 10 步就停止)限制最大工具轮数。 - 自己维护
StepInfo(步骤信息),向前端回调onStep、onTextDelta、onFinish、onError。 - 使用
prepareChatMessagesForModel()做本地摘要式上下文压缩。 - 通过
/api/llm-proxy代理模型请求,统一 base_url(模型服务地址)、安全校验和错误处理。 - 用
buildReasoningFetch()解析 SSE(服务端事件流)中的reasoning_content(推理内容),并在下一轮补回 assistant message(助手消息),兼容 DeepSeek 等模型的 thinking mode(思考模式)。
Web(网页端)工具当前包括 13 个:
search_stock、view_portfolio、market_overview、market_history、query_recommendations、query_tail_buy、plan_portfolio_update、execute_portfolio_update、analyze_stock、screen_stocks、generate_ai_report、generate_strategy_decision、intraday_analysis
与 CLI(命令行)的差异:
- Web(网页端)没有本地 shell/file/web_fetch(命令行、文件、网页抓取)工具。
- Web(网页端)没有 CLI SQLite(命令行本地轻量数据库)记忆和 scratchpad(运行追踪文件)。
- Web(网页端)调仓是
plan_portfolio_update+execute_portfolio_update两步,靠 system prompt(系统提示词)和工具分离做确认边界。 - Web(网页端)的
screen_stocks是读取最新漏斗结果,不是在用户请求时启动 CLI(命令行)后台漏斗。
MCP(模型上下文协议)入口:mcp_server.py。
MCP Server(模型上下文协议服务)使用 FastMCP("wyckoff") 注册工具,外部 Agent(智能体)通过 stdio(标准输入输出)调用。
当前 MCP(模型上下文协议)工具有三类:
| 权限层 | 工具 |
|---|---|
| Tier 1(第一权限层):本地历史 | query_history |
| Tier 2(第二权限层):行情/引擎 |
search_stock_by_name、analyze_stock、get_market_overview、screen_stocks、run_backtest、market_regime、wyckoff_diagnose、intraday_analysis、intraday_rescue_check、run_funnel_simulation
|
| Tier 3(第三权限层):用户数据/LLM(大语言模型) |
portfolio、update_portfolio、generate_ai_report、generate_strategy_decision
|
MCP(模型上下文协议)会从环境变量或本地 CLI(命令行)登录态构造 ToolContext(工具上下文):
SUPABASE_USER_IDSUPABASE_ACCESS_TOKENSUPABASE_REFRESH_TOKEN
与 CLI(命令行)/ Web(网页端)的关键区别是:MCP 自身不做 memory(记忆)、compaction(上下文压缩)、retry(纠偏重试)、sub-agent(子智能体)或后台面板。它只返回一次工具调用结果,复杂编排由 Claude Code、Cursor 等 MCP(模型上下文协议)客户端完成。
Skills(技能模板)在 cli/skills.py,本质是“预设 user message(用户消息)模板”,执行后仍走完整 CLI Agent Runtime(命令行智能体运行时)。
为了控制初始 System Prompt 的体积,避免模型因信息过载而分心,同时也为了最大化 Prompt Caching 的命中率,Wyckoff Agent 采用了**技能延迟加载(Lazy Loading)**的设计:
-
Skills 列表动态注入:在每次
AgentRuntime.run_stream启动时,系统只会把已加载的内置与用户自定义 Skills 的名称和简短描述以<system-reminder>块的形式注入到系统提示词底部。 -
execute_skill延迟展开:完整的 Skill 指令 Markdown 不会预置入 Context。当模型判定用户意图匹配某个 Skill(如/checkup)时,它会主动调用execute_skill工具。此时,系统读取 Skill 对应文件的详细 instructions,并作为tool_result结果在下一轮返回给模型。这不仅节约了海量的前期输入 Token,还极大地加快了冷启动响应速度。
内置 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(技能模板)。
当前 CLI(命令行)的模型选择由用户配置决定:默认模型负责正常对话,fallback(备用模型)只在可恢复错误时接管。
模型配置入口:
| 命令 | 作用 |
|---|---|
wyckoff model add |
交互式添加 provider(模型供应商)、API key(接口密钥)、model(模型名)和 base_url(模型服务地址) |
wyckoff model default <id> |
设置默认模型 |
wyckoff model fallback <id> |
设置 fallback(备用模型) |
wyckoff model cost <id> --input-per-1m N --output-per-1m N --context-window N |
配置成本和 context window(上下文窗口)元数据 |
wyckoff model usage --days N |
汇总最近 N 天的本地模型用量和估算成本 |
模型元数据来源:
- 用户显式配置优先,例如
context_window、输入/输出 token(令牌)单价。 - 未配置
context_window时,cli/model_metadata.py按模型名推断常见上下文窗口。 - 未知模型按 64K token(令牌)上下文窗口处理,保证压缩阈值和 UI(用户界面)展示都有保守默认值。
FallbackProvider(故障切换模型适配层)的切换边界也比较明确:
- 会切换:限流、超时、网络连接错误、服务端错误。
- 不切换:API key(接口密钥)错误、模型名错误、参数不兼容等配置问题。
- 切换后会更新当前 active provider(活跃模型适配层),runtime(运行时)读取到的
context_window也跟随当前活跃模型。
这样的设计把“模型可用性恢复”收敛在 fallback(故障切换)里;不同任务使用哪个模型由用户在默认模型和备用模型配置中显式决定。
| 原则 | 当前实现 |
|---|---|
| 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(OpenAI 兼容接口)/ Gemini / fallback(故障切换)都收敛到同一接口 |
| 失败可恢复 | 超时、fallback(故障切换)、retry(纠偏重试)、doom-loop(死循环)中止和后台任务状态共同兜底 |
- Home
- 01_Product_Overview
- 02_Finance_Wyckoff_Method
- 03_Finance_Quantitative_Metrics
- 04_Finance_Sector_Rotation_Regime
- 05_Finance_Risk_Management
- 06_Backtest_Methodology
- 07_Backtest_Simple_vs_Compound
- 08_Research_Strategy_Decay
- 09_Tech_Architecture
- 10_Tech_LLM_RAG_Integration
- 12_Tech_Actions_Operations
- 13_Tech_Python_Engineering