docs: 本地 Agent 执行设计——双平面(执行下沉/控制中心) + 与 Claude Code 对比
在保留中心化治理前提下让 Agent 操作本机文件/执行命令的设计。 含:双平面架构、与 Claude Code 逐项对比、现有架构支持度分析、 Go+Wails 客户端可行性、档 A/档 B 演进、安全门禁、P0-P4 落地阶段。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# sundynix-agentix · 本地 Agent 执行设计文档
|
||||
|
||||
> 版本:2026-07-23
|
||||
> 定位:在**保留中心化治理**的前提下,让 Agent 能像 Claude Code 那样**操作用户本机的文件、执行命令**——把"大脑/治理"留服务端,把"手/执行"下放到客户端。
|
||||
> 配套文档:`ARCHITECTURE_DESIGN.md`(架构总览)、`EINO_ADOPTION.md`(编排引擎)、`VOICE_DESIGN.md`(语音)、`SAAS_DESIGN.md`(多租户治理)、`DEPTH_ROADMAP.md`(路线图)
|
||||
|
||||
---
|
||||
|
||||
## 0. 一句话结论
|
||||
|
||||
> **中心化 ≠ 所有东西都在服务端执行。中心化 = 单一的控制/治理/观测平面。**
|
||||
>
|
||||
> 正确形态是**双平面**:**执行平面**下沉到客户端(拿 Claude Code 的手感),**控制平面**留服务端(守住计费/观测/记忆/策略)。这不是把已否决的"编排引擎下沉客户端"翻案——那否的是 DAG 工作流引擎;这里新增的是**另一个产品面**:一个 Claude-Code 式的本地操作 Agent。两者并存。
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
### 1.1 要做什么
|
||||
|
||||
让用户在桌面端说/输入「把这个目录里的 CSV 都转成 JSON」「读一下这个日志找出报错」「跑一下测试」,Agent 就能**在用户自己的机器上**读写文件、执行命令,边干边把过程流式展示,且全程仍受服务端的预算/审批/审计管控。
|
||||
|
||||
```
|
||||
用户意图 → Agent 决策(LLM) → 本地工具执行(读/写/命令) → 观察 → 再决策 → … → 结果
|
||||
↑ 经服务端 LLM 代理(记忆/计费/护栏) ↑ 在本机瞬时执行(无额外过网)
|
||||
```
|
||||
|
||||
### 1.2 核心原则
|
||||
|
||||
1. **执行在本地,控制在中心**——工具在本机跑(手感),LLM 访问/记忆/计费/观测/策略全经服务端咽喉。
|
||||
2. **复用已有接口**——`ToolCaller`、`ToolCallingModel`、NATS req/reply、HITL 审批、prompt 注册表、预算护栏、secrets 托管,尽量不重造。
|
||||
3. **安全是成败手,不是可选项**——LLM 驱动在用户机器上跑命令是最危险的面,默认收紧:路径沙箱 + 命令白名单 + HITL 审批 + 仅桌面端可用。
|
||||
4. **可降级**——本地 runner 不在线时,明确报"本地执行器不可用",而非卡死;只读能力尽量保留。
|
||||
|
||||
### 1.3 不做什么(本期)
|
||||
|
||||
- ❌ 客户端本地跑大模型(推理一律经服务端 LLM 代理出去,这是中心化的支点)。
|
||||
- ❌ 让 Web 端 / 共享工作区操作"别人的机器"(`local.*` 工具仅对有在线本地 runner 的桌面会话开放)。
|
||||
- ❌ 客户端离线自治(无网即无推理,换取治理不失控)。
|
||||
- ❌ 把 DAG 工作流引擎搬到客户端(那是另一条产品线,维持服务端)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 与 Claude Code 的对比
|
||||
|
||||
### 2.1 先破除一个误解
|
||||
|
||||
Claude Code 的"手感"**不来自本地跑大模型**——它的 LLM 也在远端(Anthropic API),每一轮决策同样要过网。手感来自三点:
|
||||
|
||||
1. **紧凑的本地工具循环**:读→改→跑→看输出→再改,一秒好几轮,**内环不过网**。
|
||||
2. **全程流式**:思考、工具输出实时可见。
|
||||
3. **工作目录内的自主感**:锁定一个 cwd,在里面自由操作。
|
||||
|
||||
**所以"本地 vs 中心化"是假二选一。** 真正的问题是两个平面各放哪。而 Claude Code 自己就是混合的:**执行本地 + LLM 远端**。
|
||||
|
||||
### 2.2 逐项对比
|
||||
|
||||
| 维度 | Claude Code | sundynix-agentix(现状) | 本设计(双平面) |
|
||||
|---|---|---|---|
|
||||
| Agent 循环位置 | 本地 CLI 进程 | 服务端 dispatcher/Eino | **档 B:客户端 Go host** / 档 A:仍服务端 |
|
||||
| LLM 访问 | 直连 Anthropic(用户持 key) | dispatcher 直连 LLM 池 | **经 gateway LLM 代理**(key 服务端托管) |
|
||||
| 工具执行 | 本地、瞬时 | NATS→mcp-go(服务端) | **本地 runner,瞬时** |
|
||||
| 每工具是否过网 | 否(内环本地) | 是(server↔mcp) | **档 B 否 / 档 A 一跳(server↔client)** |
|
||||
| 多租户/隔离 | ❌ 无 | ✅ 已建 | ✅ 保留 |
|
||||
| 计费/计量 | ❌ 无 | ✅ 积分/成本/rollup | ✅ 经 LLM 代理 + 事件上抛 |
|
||||
| 集中可观测 | ❌ 本地日志 | ✅ OTel 全链路 | ✅ 客户端事件上抛服务端 |
|
||||
| Prompt/模型治理 | 本地配置 | ✅ 注册表 + 热切换 | ✅ 保留 |
|
||||
| 权限/审批 | 本地 permission 模式 | ✅ HITL 审批节点 | ✅ 策略下推 + 本地快审批 |
|
||||
| 工作目录沙箱 | ✅ cwd | 无(服务端无本地盘概念) | ✅ 新增本地 cwd 沙箱 |
|
||||
| 团队/共享知识 | ❌ | ✅ Space/KB 共享 | ✅ 保留 |
|
||||
| 离线可用 | 部分(无 LLM 则不可) | ❌ | ❌(刻意,换治理) |
|
||||
|
||||
### 2.3 结论:我们要成为的东西
|
||||
|
||||
> **"Claude Code + 治理层"**——把 Claude Code 的本地执行手感,装进一个多租户、可计费、可观测、可审批的受管平台里。这正是 Claude Code **刻意不做**的部分,也是我们已投入建设的护城河所在。
|
||||
|
||||
---
|
||||
|
||||
## 3. 现有架构支持度(为什么不用推倒重来)
|
||||
|
||||
读 `sundynix-dispatcher/internal/eino/react_agent.go` 可知,agent 循环有两个关键事实,决定了本方案**是"换实现 + 加代理",不是"重写"**:
|
||||
|
||||
1. **循环本体是 Eino 标准件 `react.NewAgent`**,可移植。它只吃两个注入:`ToolCallingModel`(LLM)+ 一组 `tool.BaseTool`(工具)。
|
||||
2. **这两样都已在干净接口后面**:
|
||||
- LLM:`o.agentPool(b).ToolCallingModel()` —— 池抽象,可换成"gateway LLM 代理客户端"。
|
||||
- 工具:`mcpTool.InvokableRun` → `m.caller.CallTool(ctx, subject, ToolCall)`,`caller` 是 **`ToolCaller` 接口**,`subject` 是**可替换路由函数**;工具还是 `list_tools` **动态发现**,加工具零改循环。
|
||||
|
||||
**最难的两块(LLM 访问、工具调用)已被提前解耦。** 这是本方案成本可控的根本原因。
|
||||
|
||||
---
|
||||
|
||||
## 3.5 客户端可行性:Go + Wails 是最佳形态
|
||||
|
||||
> 直接结论:**支持,而且 Go+Wails 比 Electron/Tauri 都强。** 因为客户端与服务端 agent 是**同一种语言(Go)**——档 B 不是"用另一门语言重写循环",而是**直接共享同一份 Eino 循环代码**。
|
||||
|
||||
### 3.5.1 同语言 = 消掉最贵的成本
|
||||
|
||||
| 客户端技术 | 档 B 要付的代价 |
|
||||
|---|---|
|
||||
| Electron(JS/TS) | 把 `react.NewAgent`/`mcpTool`/`ToolCaller` 全用 TS 重写 |
|
||||
| Tauri(Rust) | 用 Rust 重写 |
|
||||
| **Go + Wails(我们)** | **`import` 即用**:dispatcher 与 desktop 共用同一个 `cloudwego/eino`、同一套 `contract` 类型、同一份循环逻辑 |
|
||||
|
||||
这把 §7 四个解耦点里最贵的"重写"直接消掉,只剩"搬家"。
|
||||
|
||||
### 3.5.2 界线:agent 跑在 Go host,不在 WebView
|
||||
|
||||
**Wails app = Go host(原生进程,无沙箱)+ WKWebView(只渲染 UI,经绑定调 Go)。** agent 循环跑在 Go host——所以 **WebView 的沙箱限制与执行完全无关**。逐块看,每一块都在 Go host 里正常跑:
|
||||
|
||||
| 部件 | Wails Go host | 说明 |
|
||||
|---|---|---|
|
||||
| Eino `react.NewAgent` 循环 | ✅ | 纯 Go 库;host 是正常 Go 进程,`goroutine`/`context`/`StreamReader` 全正常 |
|
||||
| 本地工具 read/write/bash/grep | ✅ | `os.ReadFile`/`os.WriteFile`/`exec.CommandContext` 以用户 OS 权限跑——正是 Wails 甩纯 Web 应用之处 |
|
||||
| LLM 代理客户端 | ✅ | `net/http` 打 gateway;实现成 Eino `ToolCallingChatModel` 接口,对循环透明 |
|
||||
| 流式输出到 UI | ✅ | Go host 用 Wails 运行时事件(`EventsEmit`)推前端渲染;同时自持连接上抛服务端 |
|
||||
| 长连接/后台 goroutine | ✅ | host 生命周期独立于 webview,启动时起 goroutine 持 gateway 连接、注册 runner |
|
||||
|
||||
### 3.5.3 唯一真实的坑:模块依赖接线(非能力问题)
|
||||
|
||||
1. **desktop 是独立 module、不在 go.work 里**(见 `wails3-migration`、`go-workspace-genproto` 坑)。
|
||||
2. **Go `internal/` 包不能跨模块 import** → dispatcher 里那份循环**必须挪到非 internal 的共享位置**(`sundynix-shared/agent` 或新模块)才能被 desktop 引。即 §7 解耦点 #3。
|
||||
3. `sundynix-desktop/go.mod` 要 require `sundynix-shared`,构建走 `GOWORK=off`,靠 **go.mod 依赖(replace 指向本地 / Gitea module proxy)**解析,不能靠 go.work。**盯紧已踩过的 genproto/indirect-require 版本冲突。**
|
||||
4. 次要:Eino 传递依赖不少,加进 desktop 会让**二进制变大、构建变慢**——可接受,但心里有数。
|
||||
|
||||
> 一句话:**能力上零障碍,Go+Wails 天然支持;成本上因同语言,档 B 从"重写"降级成"把共享单元搬进 sundynix-shared + 接好 go.mod 依赖"。** 你趟过的 go.work/genproto 坑,恰好就是这次搬家要绕的雷。
|
||||
|
||||
---
|
||||
|
||||
## 4. 双平面架构
|
||||
|
||||
```
|
||||
┌────────────────────── 控制平面(服务端,中心化治理)──────────────────────┐
|
||||
│ gateway │
|
||||
│ ├─ LLM 代理端点 ← 咽喉:注入记忆/prompt、记用量/计费、护栏、OTel、key托管 │
|
||||
│ ├─ 策略下推 → ToolPolicy / 白名单 / 预算令牌桶(复用 config 下推范式)│
|
||||
│ ├─ HITL 策略定义 → 哪些操作必须审批(人工/组织级流程仍服务端 pause) │
|
||||
│ └─ 事件汇聚 ← 客户端上抛的 token/trace/usage,进 NATS/JetStream │
|
||||
│ dispatcher/Eino:DAG 工作流编排(另一产品面,维持不变) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
▲ LLM 调用(每轮决策) ▲ 事件上抛(异步)
|
||||
│ │
|
||||
┌────────────────────── 执行平面(客户端 Wails Go host)──────────────────────┐
|
||||
│ 本地 Agent 运行时(档 B:react 循环搬到这里) │
|
||||
│ ├─ ToolCallingModel = gateway LLM 代理客户端(实现 Eino ChatModel 接口) │
|
||||
│ ├─ 本地工具族 local.*:read/write/edit/bash/grep/glob(os / os/exec) │
|
||||
│ ├─ cwd 沙箱:所有操作锁在用户选的工作目录根下 │
|
||||
│ ├─ 本地 HITL:破坏性操作快速弹窗审批(策略由服务端下推) │
|
||||
│ └─ 策略缓存 + 预算令牌桶:内环本地判定,不阻塞网络 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**关键点:只有"LLM 调用"离开本机(Claude Code 也一样),工具执行全在本地。** 手感回来,治理不漏。
|
||||
|
||||
---
|
||||
|
||||
## 5. 两档演进路线
|
||||
|
||||
> 建议**先做档 A**,用真实用例量延迟;**真的卡手了再上档 B**。你的场景(助手操作几个文件/跑个命令)工具调用频率低,档 A 那点往返大概率无感;Claude-Code 内环之痛只在"读几十文件、反复跑测试"才暴露。
|
||||
|
||||
### 档 A:只下放工具执行,循环留服务端 —— **成本低(几天级)**
|
||||
|
||||
因为 `ToolCaller` 已是接口 + 可替换 subject,只需:
|
||||
|
||||
1. 新增 `local.*` 工具族,`CallTool` 的 subject 路由到 **NATS→gateway→客户端 runner**(而非 →mcp-go)。
|
||||
2. 客户端 Go host 跑一个"迷你 mcp"执行器,登录后注册到现有连接,收调用就 `os.ReadFile`/`exec.CommandContext`,结果原路回。
|
||||
3. gateway 做 subject 桥接:把发给某用户 runner 的 NATS 请求,转到该 runner 的连接,等 reply。
|
||||
|
||||
**循环、图、LLM、记忆、计费、追踪一行不动。** 就是给工具层多插一个执行目标。
|
||||
- ✅ 立刻拿到"操作本地"能力,治理全在。
|
||||
- ❌ 每工具过一次网(server↔client),无 Claude Code 紧凑手感。
|
||||
|
||||
### 档 B:把 agent 循环搬到客户端 —— **成本中等(解耦+搬家,非重构)**
|
||||
|
||||
拦路的不是架构,是循环今天**和服务端世界缠在一起**。需拆的**四个耦合点**(见 §7)。搬完后:
|
||||
|
||||
- ✅ 内环本地、瞬时,真正的 Claude Code 手感。
|
||||
- ✅ 控制平面(LLM 代理 + 策略下推 + 事件上抛)守住中心化。
|
||||
|
||||
---
|
||||
|
||||
## 6. 控制平面:三个支点
|
||||
|
||||
### 6.1 LLM 代理(咽喉,档 B 的地基)
|
||||
|
||||
- gateway 出一个 chat/completions 风格端点;客户端每次推理经它出去。
|
||||
- 挂载:记忆注入(复用现有 recall)、prompt 版本化(注册表)、用量计量(计费/积分)、护栏/忠实度、OTel span、**provider key 永不下发客户端**。
|
||||
- 客户端把该端点实现成一个 Eino `ToolCallingChatModel`,喂给 `react.NewAgent`——对循环透明。
|
||||
- 净新增,但可复用 `sundynix-dispatcher/internal/llm` 的 client 逻辑。
|
||||
|
||||
### 6.2 策略下推(治理集中,但别让内环等网络)
|
||||
|
||||
复用现有 `PublishConfigUpdated` / `RequestConfig` 下推范式:
|
||||
|
||||
- **ToolPolicy / 白名单**:会话开始下推客户端,**内环本地判定**,异步回报;服务端可 push 撤销/调整。
|
||||
- **预算令牌桶**:服务端发额度给客户端,本地花、异步续、花光**本地硬停**,不必每调用回问。
|
||||
- **HITL**:常见破坏性操作走**本地快速审批弹窗**(不阻塞、不回服务端 pause);"哪些必须审批"由服务端策略下推,每个决策**记回服务端审计**;只有"组织级人工批"流程才走现有服务端 pause→waiting。
|
||||
|
||||
### 6.3 事件上抛(观测/计费不断)
|
||||
|
||||
- 客户端把 trace/token/usage 事件经 NATS(或现有 WS)上抛,进 JetStream 持久(计费类一律持久 + 幂等,见 `nats-durability` 约定)。
|
||||
- 服务端观测面板照常看到"agent 读了哪个文件、跑了什么命令、花了多少 token"。
|
||||
|
||||
---
|
||||
|
||||
## 7. 档 B 的四个解耦点(真实工作量所在)
|
||||
|
||||
| # | 耦合点 | 现状 | 要做的解耦 | 性质 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 循环黏服务端专属件 | `o.sink.PublishToken`(NATS 出 token)、`execTracer`(span)、`harness.BudgetFrom`(预算)、`board`/`recordAgentOutput`(喂图下游) | 把循环抽成**吃接口的独立单元**:token sink / tracer / budget 改注入,客户端本地实现 + 事件上抛。redactor 已是纯函数可直接搬 | 机械解耦 |
|
||||
| 2 | LLM 代理净新增 | 仅 dispatcher 直连 LLM 池、持 key | gateway 出 chat 端点,客户端实现成 Eino ChatModel | 新增(可复用 llm client) |
|
||||
| 3 | 打包/模块边界 | 桌面端是**独立 module、不在 go.work**(见 `go-workspace-genproto`、`wails3-migration` 坑) | 把可复用 agent 单元挪进 `sundynix-shared`(或新模块),desktop 与 dispatcher 共享 import | 搬家(注意 go.work 依赖坑) |
|
||||
| 4 | 事件回传 | 服务端内部 NATS | 客户端 → 服务端事件通道(复用 WS 或 NATS)+ 幂等落库 | 新增(复用现有总线) |
|
||||
|
||||
**没有一条是"架构不支持"或"推倒重来"——全是解耦、搬家、加一个代理端点。**
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全模型(最重要)
|
||||
|
||||
LLM 驱动在用户机器上跑命令 = 最高危面。默认全部收紧,宁可先严后松:
|
||||
|
||||
1. **路径沙箱**:用户显式选一个工作目录根;所有 `local.*` 只能在根下操作,越界直接拒(软链接/`..` 逃逸也拦)。
|
||||
2. **命令白名单/黑名单**:`rm -rf`、`curl|sh`、写系统路径、`sudo` 等默认拦;exec 默认需审批。
|
||||
3. **HITL 审批**:写文件/删除/执行命令默认走审批;可配"这次允许 / 本会话允许 / 加白名单"。
|
||||
4. **仅桌面端可用**:`local.*` 必须 gate 在"有在线本地 runner 的桌面会话";Web/共享工作区不给(否则语义即"让 A 操作 B 的机器",灾难)。写进 ToolPolicy。
|
||||
5. **归属校验**:本地 runner 只服务自己登录的用户,注册带 `user_id` + 设备指纹,服务端严格校验目标连接归属,绝不串。
|
||||
6. **审计**:每次本地文件写/命令执行,无论是否需审批,都落审计事件回服务端。
|
||||
7. **超时 + 资源限制**:命令有超时、输出大小上限,防失控进程/刷屏。
|
||||
|
||||
---
|
||||
|
||||
## 9. 可靠性 / 寻址
|
||||
|
||||
- **连接注册表**:服务端维护 `user_id/device → runner 连接`;任务提交时绑定"发起它的那台 runner"。
|
||||
- **多设备歧义**:一个用户开多台桌面端 → 任务必须绑定发起端,不能随便选。
|
||||
- **断线兜底**:客户端中途断线,工具调用超时 + 明确报"本地执行器不在线",任务优雅失败而非卡死。
|
||||
- **降级**:runner 不在线时,纯服务端能力(问答/检索/报告)不受影响。
|
||||
|
||||
---
|
||||
|
||||
## 10. 落地阶段(从现有往上加)
|
||||
|
||||
| 阶段 | 内容 | 依赖 | 产出 |
|
||||
|---|---|---|---|
|
||||
| **P0** | gateway **LLM 代理端点**:客户端推理经它,挂计量/追踪/记忆注入/护栏 | 复用 `internal/llm` | 中心化咽喉立住 |
|
||||
| **P1(档 A)** | `local.*` **只读**工具(list/read/grep)+ 客户端 runner 注册 + subject 桥接 + cwd 沙箱 | `ToolCaller` 接口 | "agent 读了我的文件"闭环 + 延迟实测 |
|
||||
| **P2(档 A)** | write/edit/bash + **本地 HITL** + ToolPolicy 下推 + 审计 | HITL 节点、config 下推 | 可操作本地,治理收紧 |
|
||||
| **P3(档 B)** | 按 §7 抽循环 → 搬 `sundynix-shared` → 客户端跑 react 循环 + 预算令牌桶 + 事件上抛 | 四个解耦点 | Claude Code 手感 |
|
||||
| **P4** | 桥接服务端图编排(Task 式子 agent)、会话 resume、CLAUDE.md 式本地项目记忆 | P3 | 两产品面打通 |
|
||||
|
||||
**决策门**:P1 跑通后,用真实用例量 server↔client 单次往返延迟。若无感 → 停在档 A 收益已足;若明显卡手(高频内环)→ 才付 P3 的钱。
|
||||
|
||||
---
|
||||
|
||||
## 11. 风险与取舍
|
||||
|
||||
- **产品定位决定 ROI**:档 B 只有当产品是"完整编码/操作 agent 在我仓库里连续折腾"才值;若是"助手偶尔干几件本地事",档 A 足矣。**先确认定位再决定要不要 P3。**
|
||||
- **模块边界坑**:desktop 不在 go.work,搬共享 agent 单元时小心 `genproto`/indirect require 冲突(见 `go-workspace-genproto`)。
|
||||
- **安全面扩大**:一旦能跑命令,任何提示注入/越权都可能落到用户机器。§8 的门禁是硬要求,不是可选。
|
||||
- **老引擎兼容**:客户端 Go 运行时无浏览器引擎版本问题;但若本地 agent 的 UI 复用现有前端,注意 WKWebView 版本坑(另见前端白屏排查)。
|
||||
|
||||
---
|
||||
|
||||
## 12. 待定问题(需产品确认)
|
||||
|
||||
1. 产品定位:受管的**团队/企业操作平台** 还是 **更强的个人本地助手**?(决定要不要档 B)
|
||||
2. 本地 runner 的连接:直连 NATS 还是经 gateway WS?(出网策略 + 部署拓扑约束)
|
||||
3. exec 的默认收紧程度:默认全审批,还是白名单内免审批?
|
||||
4. 是否需要"CLAUDE.md 式"本地项目记忆文件,与服务端记忆如何合并?
|
||||
Reference in New Issue
Block a user