Files
sundynix-agentix/LOCAL_AGENT_DESIGN.md

19 KiB
Raw Permalink Blame History

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. 复用已有接口——ToolCallerToolCallingModel、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,可移植。它只吃两个注入:ToolCallingModelLLM+ 一组 tool.BaseTool(工具)。
  2. 这两样都已在干净接口后面
    • LLMo.agentPool(b).ToolCallingModel() —— 池抽象,可换成"gateway LLM 代理客户端"。
    • 工具:mcpTool.InvokableRunm.caller.CallTool(ctx, subject, ToolCall)callerToolCaller 接口subject可替换路由函数;工具还是 list_tools 动态发现,加工具零改循环。

最难的两块(LLM 访问、工具调用)已被提前解耦。 这是本方案成本可控的根本原因。


3.5 客户端可行性:Go + Wails 是最佳形态

直接结论:支持,而且 Go+Wails 比 Electron/Tauri 都强。 因为客户端与服务端 agent 是同一种语言(Go——档 B 不是"用另一门语言重写循环",而是直接共享同一份 Eino 循环代码

3.5.1 同语言 = 消掉最贵的成本

客户端技术 档 B 要付的代价
ElectronJS/TS react.NewAgent/mcpTool/ToolCaller 全用 TS 重写
TauriRust 用 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-migrationgo-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/EinoDAG 工作流编排(另一产品面,维持不变)                     │
└─────────────────────────────────────────────────────────────────────────────┘
                         ▲ LLM 调用(每轮决策)        ▲ 事件上抛(异步)
                         │                             │
┌────────────────────── 执行平面(客户端 Wails Go host)──────────────────────┐
│  本地 Agent 运行时(档 B:react 循环搬到这里)                               │
│   ├─ ToolCallingModel = gateway LLM 代理客户端(实现 Eino ChatModel 接口)   │
│   ├─ 本地工具族 local.*read/write/edit/bash/grep/globos / 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.PublishTokenNATS 出 token)、execTracerspan)、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-genprotowails3-migration 坑) 把可复用 agent 单元挪进 sundynix-shared(或新模块),desktop 与 dispatcher 共享 import 搬家(注意 go.work 依赖坑)
4 事件回传 服务端内部 NATS 客户端 → 服务端事件通道(复用 WS 或 NATS)+ 幂等落库 新增(复用现有总线)

没有一条是"架构不支持"或"推倒重来"——全是解耦、搬家、加一个代理端点。


8. 安全模型(最重要)

LLM 驱动在用户机器上跑命令 = 最高危面。默认全部收紧,宁可先严后松:

  1. 路径沙箱:用户显式选一个工作目录根;所有 local.* 只能在根下操作,越界直接拒(软链接/.. 逃逸也拦)。
  2. 命令白名单/黑名单rm -rfcurl|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+ 客户端 runner 注册 + subject 桥接 + cwd 沙箱 ToolCaller 接口 2026-07-24 落地并 live 验通(实现见 JARVIS_BRAIN_DESIGN P4gateway local_runner.go + desktop localrunner.go;沙箱逃逸单测全拦;离线优雅降级)
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 式"本地项目记忆文件,与服务端记忆如何合并?