feat: integrate NOFXi agent into dev

This commit is contained in:
lky-spec
2026-04-18 16:06:42 +08:00
parent 851f152c50
commit 5c4e7502d7
76 changed files with 17714 additions and 1082 deletions

View File

@@ -0,0 +1,613 @@
# NOFXi Agent 当前设计说明
## 目的
本文描述当前 NOFXi Agent 的实际设计,而不是早期版本的理想设计。重点回答这些问题:
- 用户消息从哪里进入
- 什么请求会进入 planner
- 当前有哪些记忆层
- planner 如何生成与执行 plan
- tool 现在是怎么设计的
- 动态快照和当前引用分别解决什么问题
- 为什么某些问题会出现“看起来有历史,但模型还是会追问”
本文对应的主要实现文件:
- `agent/agent.go`
- `agent/web.go`
- `api/agent_routes.go`
- `agent/planner_runtime.go`
- `agent/execution_state.go`
- `agent/memory.go`
- `agent/history.go`
- `agent/tools.go`
## 一句话总览
当前 Agent 的运行模型可以概括为:
1. 前端把消息发到 `/api/agent/chat/stream`
2. 后端把登录用户身份放进 context
3. Agent 除 `/clear``/status` 外,其他消息全部进入 planner
4. planner 结合多层记忆、动态快照和 tool schema 生成 plan
5. 执行 plan 中的 `tool / reason / ask_user / respond`
6. 在执行过程中持续更新执行态、短期原话、长期摘要和当前对象引用
## 请求入口
### 前端入口
前端 Agent 页面在:
- `web/src/pages/AgentChatPage.tsx`
当前聊天使用:
- `POST /api/agent/chat/stream`
请求体里会传:
- `message`
- `lang`
- `user_key`
### 后端路由入口
路由注册在:
- `api/agent_routes.go`
这里会:
1. 经过 `authMiddleware`
2. 从登录态里取出 `user_id`
3. 通过 `agent.WithStoreUserID(...)` 写入 request context
### Agent Web Handler
真正的 HTTP handler 在:
- `agent/web.go`
主要入口:
- `HandleChat(...)`
- `HandleChatStream(...)`
再往下进入:
- `HandleMessageForStoreUser(...)`
- `HandleMessageStreamForStoreUser(...)`
## 最外层分流
当前外层分流已经被收口。
`agent/agent.go` 中,除了这两个命令之外,其他输入全部交给 planner
- `/clear`
- `/status`
也就是说,现在这些都不再在外层直接处理:
- setup flow
- trade confirmation
- direct trade regex
- 自然语言配置流程
- 自然语言策略创建
这些都统一进入 planner。
这是当前设计里一个很重要的原则:
- 外层分流越少,行为边界越清晰
- 自然语言理解尽量统一交给 planner + tool
## 当前的 5 层记忆
当前不是 3 层,也不是 4 层,而是 5 层:
1. `chatHistory`
2. `TaskState`
3. `ExecutionState`
4. `CurrentReferences`
5. `Persistent Preferences`
### 1. chatHistory
定义位置:
- `agent/history.go`
作用:
- 保存最近几轮用户 / assistant 原始消息
- 给模型保留最近原话上下文
- 为后续摘要成 `TaskState` 提供原始素材
特点:
- 只保留短期原话
- 内存态
- `/clear` 时清空
适合存:
- 最近几轮对话原文
- 用户的最新措辞
- 刚刚的自然语言上下文
不适合存:
- 长期真相
- 当前外部系统状态
- 当前流程精确执行位置
### 2. TaskState
定义位置:
- `agent/memory.go`
作用:
- 保存跨轮次仍然有意义的高层摘要
- 注入 planner / reasoning / final response
持久化 key
- `agent_task_state_<userID>`
字段:
- `CurrentGoal`
- `ActiveFlow`
- `OpenLoops`
- `ImportantFacts`
- `LastDecision`
- `UpdatedAt`
适合存:
- 当前高层目标
- 跨轮次仍然成立的未闭环事项
- 关键事实
- 最近一次重要决策及其原因
不适合存:
- step 级待办
- “下一步调用哪个 tool”
- 动态余额、持仓、配置存在性
- 任何可以通过 tool 重新读取的实时状态
### 3. ExecutionState
定义位置:
- `agent/execution_state.go`
作用:
- 保存当前 plan 的执行态
- 支持 `ask_user` 之后继续执行
- 保存 plan、当前步骤、执行日志、等待状态等
持久化 key
- `agent_execution_state_<userID>`
当前关键字段:
- `SessionID`
- `Goal`
- `Status`
- `PlanID`
- `Steps`
- `CurrentStepID`
- `DynamicSnapshots`
- `ExecutionLog`
- `SummaryNotes`
- `Waiting`
- `CurrentReferences`
- `FinalAnswer`
- `LastError`
### 4. CurrentReferences
定义位置:
- `agent/execution_state.go`
作用:
- 记录当前对话里“这个 / 那个 / 刚才那个”到底指的是谁
当前支持的引用对象:
- `strategy`
- `trader`
- `model`
- `exchange`
这是为了解决一种常见问题:
- 用户明明前一轮刚说过“激进策略”
- 下一轮说“改一下这个策略”
- 如果没有结构化引用,模型虽然有聊天历史,也容易重新追问
`CurrentReferences` 不是系统状态快照,而是:
- 当前对话焦点对象
- 当前代词绑定对象
### 5. Persistent Preferences
对应工具:
- `get_preferences`
- `manage_preferences`
作用:
- 保存用户长期偏好
适合存:
- 默认中文回复
- 偏好激进风格
- 更关注 BTC / ETH
- 不喜欢高频
- 每天固定时间简报
它和 `TaskState` 的区别是:
- `TaskState` 偏向当前任务摘要
- `Persistent Preferences` 偏向长期用户画像
## DynamicSnapshots 是什么
`DynamicSnapshots` 是当前真实系统状态的快照。
它不是历史,也不是长期记忆,而是 planner 在规划前或执行中插入的“当前事实”。
当前会进入快照的典型信息包括:
- 当前模型配置列表
- 当前交易所配置列表
- 当前策略列表
- 当前 trader 列表
- 当前余额
- 当前持仓
- 最近交易历史
作用:
- 防止 planner 盲信旧结论
- 避免“之前没配置,现在其实已经配好了却还说没有”
- 避免“之前余额是 A现在拿旧 observation 继续回答”
一句话:
- `DynamicSnapshots` = 当前世界里真实有什么
## CurrentReferences 和 DynamicSnapshots 的区别
这两个容易混淆,但职责完全不同。
`DynamicSnapshots`
- 当前系统状态快照
- 是候选集合 / 当前事实
- 例如当前有两个策略:`激进``新策略`
`CurrentReferences`
- 当前对话焦点对象
- 是“这个”到底指谁
- 例如用户现在说的“这个策略”就是 `激进`
可以这样理解:
- `DynamicSnapshots` 是地图
- `CurrentReferences` 是你手指现在指着地图上的哪个点
## Planner 的输入
planner 主逻辑在:
- `agent/planner_runtime.go`
生成计划时,当前会把这些东西一起送给模型:
- 当前用户请求
- tool schema
- `Persistent Preferences`
- `TaskState`
- `ExecutionState`
- `Resume context`
- `Structured waiting state`
- `Observation context`
其中 observation context 不是旧版单数组,而是分层后的:
- `dynamic_snapshots`
- `execution_log`
- `summary_notes`
## Plan 的结构
当前 planner 只允许这 4 类 step
- `tool`
- `reason`
- `ask_user`
- `respond`
这意味着现在的 Agent 不是一个“自由发挥的回复器”,而是:
- 先规划
- 再执行步骤
- 必要时重规划
## 步骤执行流程
`executePlan(...)` 的核心逻辑是:
1. 找下一个 pending step
2. 标记 step 为 running
3. 执行对应类型
4. 写回 `ExecutionState`
5. 必要时触发 replanning
不同 step 类型行为如下:
### tool
- 调内部 tool
- 把结果写入 `ExecutionLog`
- 根据结果更新 `CurrentReferences`
- 必要时触发 replanner
### reason
- 发起一次短 reasoning 调用
- 生成一段简短中间推理
- 写入 `ExecutionLog`
### ask_user
- 进入 `waiting_user`
- 保存 `WaitingState`
- 把问题直接回给用户
### respond
- 生成最终回答
- 标记当前执行完成
## WaitingState 是什么
`WaitingState` 用来解决:
- 用户回复 `是`
- 用户回复 `继续`
- 用户回复 `那个就行`
这类短回复如果没有结构化等待状态,很容易丢上下文。
当前字段包括:
- `Question`
- `Intent`
- `PendingFields`
- `ConfirmationTarget`
- `CreatedAt`
它的作用是:
- 告诉 planner 上一轮到底在等什么
- 让这轮短回复更容易被理解成“对上一问的回答”
## CurrentReferences 如何更新
当前是双路径更新:
### 1. 用户消息命中对象名时更新
如果用户说:
- `修改激进策略`
- `停止 lky`
- `用 DeepSeek`
系统会去当前用户的策略 / trader / model / exchange 列表里尝试匹配名称或 ID。
匹配成功后,更新 `CurrentReferences`
### 2. tool 成功返回对象时更新
比如:
- `manage_strategy(create/update/activate)`
- `manage_trader(create/update)`
- `manage_model_config(update)`
- `manage_exchange_config(update)`
只要 tool 返回了具体对象,系统就会把对应 ID / name 写回当前引用。
## Tool 设计
当前 tool 是“资源型 tool”设计不是“页面动作型 tool”。
### 当前主要工具
配置资源:
- `get_exchange_configs`
- `manage_exchange_config`
- `get_model_configs`
- `manage_model_config`
策略资源:
- `get_strategies`
- `manage_strategy`
trader 资源:
- `manage_trader`
交易 / 查询资源:
- `search_stock`
- `execute_trade`
- `get_positions`
- `get_balance`
- `get_market_price`
- `get_trade_history`
### 为什么这么设计
优点:
- tool schema 稳定
- 行为边界清晰
- planner 更容易学会
- 资源增删改查统一
当前 `manage_strategy` 支持:
- `list`
- `get_default_config`
- `create`
- `update`
- `delete`
- `activate`
- `duplicate`
当前 `manage_trader` 支持:
- `list`
- `create`
- `update`
- `delete`
- `start`
- `stop`
## 为什么“创建策略”不该默认依赖交易所和模型
当前设计里,策略模板应该是独立资源:
- `strategy`
而运行态对象是:
- `trader`
更合理的边界是:
- 创建策略模板:用 `manage_strategy`
- 把策略跑起来:用 `manage_trader`
也就是说:
- 策略不默认依赖交易所和模型
- 只有当用户要求“运行 / 部署 / 创建 trader”时才需要进一步关联 exchange / model / trader
## 当前一个完整例子
用户输入:
`帮我创建一个新的激进策略模板,名字就叫激进。创建完后,再把这个策略绑定到 trader lky。`
当前大致流程:
1. 前端请求 `/api/agent/chat/stream`
2. 后端注入 `store_user_id`
3. Agent 进入 planner
4. planner 刷新动态快照:
- 当前策略
- 当前 trader
5. 生成 plan例如
- `get_strategies`
- `manage_strategy(create)`
- `manage_trader(update)`
- `respond`
6. 执行 `manage_strategy(create)` 后:
- 写入 `ExecutionLog`
- 更新 `CurrentReferences.strategy`
7. 执行 `manage_trader(update)` 时:
- 直接使用刚创建策略的 ID
8. 输出最终回复
如果此后用户继续说:
`把这个策略的 prompt 改激进一点`
系统会优先从 `CurrentReferences.strategy` 理解“这个策略”。
## 为什么看起来“有历史”,模型还是会追问
因为“有聊天历史”不等于“有结构化对象绑定”。
如果没有 `CurrentReferences`
- 模型只能依赖原话文本推断“这个策略”是谁
- 一旦中间插入多条消息,或者有多个候选策略
- 就容易重新追问
所以当前设计里,`CurrentReferences` 是补齐这一块的关键。
## 当前已知限制
### 1. 外层虽然已经大幅收口,但仍然不是纯 graph runtime
现在比之前更统一,但整体仍然是:
- Agent 主入口
- Planner
- Tool 执行
而不是完整 node-graph 引擎。
### 2. ExecutionState 仍然是按 userID 单槽位
这意味着:
- 同一用户的多个并行任务仍然可能相互影响
更彻底的方向应该是:
- 按 thread / session 多实例存储
### 3. CurrentReferences 目前还是轻量实现
当前只覆盖:
- strategy
- trader
- model
- exchange
后面如果要更强,需要考虑:
- 多候选冲突消解
- 昵称映射
- 跨更长会话的稳定实体绑定
## 当前设计的核心思想
一句话总结:
- `chatHistory` 记原话
- `Persistent Preferences` 记长期偏好
- `TaskState` 记高层摘要
- `ExecutionState` 记当前流程
- `DynamicSnapshots` 记当前事实
- `CurrentReferences` 记当前指代对象
- `planner` 决定步骤
- `tools` 执行落地动作
这就是当前 NOFXi Agent 的实际运行设计。