继 Web Coding Agent 后,我做了一个本地优先的桌面 AI Agent

项目地址github.com/lhz960904/a...,求 star ⭐

两个月前,我开源过一个 Web 端的 AI Coding Agent------code-artisan。它能在浏览器里用自然语言写代码、跑代码,但形态上绕不开云端沙箱和后端,并且与本地办公资料很难结合起来。所以我又重新设计 Atrium : 一个本地优先的桌面 AI Agent 工作台。自带密钥接入任意模型,一整套 agentic 能力(工具、MCP、记忆、技能、子 Agent、任务计划)全部跑在本地机器上,对话、凭证、文件不经过任何中间服务器。

演示

📽️ 完整演示视频见 README

功能特性

  • 多供应商支持:Anthropic、Google Gemini、任意 OpenAI 兼容端点、通过 Ollama 运行的本地模型,以及外部 CLI agent(Claude Code、Codex、Gemini CLI)- 全部使用你自己的密钥,并在本地加密。

  • MCP:连接 Model Context Protocol 服务器(stdio / HTTP / SSE),在对话里直接使用第三方服务提供的工具。支持从主流工具直接导入,支持第三方服务 Oauth 授权。

  • Skills:把可复用的流程打包成技能包(SKILL.md 等)渐进式披露给 Agent。支持从 Claude Code、Codex、.Agents 多数据源读取已有技能,自动统计使用频率过滤无用 Skill 描述。

  • Subagents: 将大任务拆成子任务委派给专注的 Agent,在隔离的上下文中执行子任务并汇报结果,不污染主Agent 的上下文。支持创建和删除子Agent。

  • 跨会话记忆:包含用户身份的录入(get-acquainted 技能),会话过程中自动写入记忆,可区分全局记忆、项目域记忆 持久。后台自动总结保证记忆的长期质量。

技术架构全景

技术栈采用 Bun + Electron 39 + electron-vite + React 19 + TypeScript + Tailwind v4 ,主进程承载 Agent 循环、存储与模型解析,渲染进程只管 UI。底层 Agent 循环这次直接托给了 Vercel AI SDKstreamText + 多步工具调用),把精力省下来放在产品形态和能力完整度上。

先看一张分层总览------从最上层的 UI 到最底层的模型后端,每一层各自的职责:


如果换个角度,按数据流向看,整条依赖链路是这样的:

flowchart TB subgraph Renderer[&#34;渲染进程 · React 19&#34;] UI[&#34;对话 · 设置 · 侧边栏&#34;] end subgraph Main[&#34;主进程 · Node&#34;] TRPC[&#34;tRPC 路由(CRUD / 配置)&#34;] HTTP[&#34;Hono 服务 · 127.0.0.1(对话流)&#34;] Agent[&#34;Agent 循环 · streamText<br/>工具 · MCP · 技能<br/>记忆 · 沙箱 · 权限 · 等&#34;] Providers[&#34;Provider 解析&#34;] DB[(&#34;SQLite<br/>better-sqlite3 + Drizzle + FTS5&#34;)] end Cloud[&#34;模型后端<br/>Anthropic · Gemini · OpenAI 兼容<br/>Ollama · CLI Agent(ACP)&#34;] UI -- &#34;tRPC over IPC&#34; --> TRPC UI -- &#34;HTTP / SSE 流&#34; --> HTTP HTTP --> Agent Agent --> Providers Providers -- &#34;你的加密密钥&#34; --> Cloud TRPC --> DB Agent --> DB

下面把目前涉及到的核心设计逐个展开说一说

核心模块设计

一、双通道:一个 Electron 应用,为什么要起一个 localhost server?

Atrium 的渲染进程和主进程之间有两条通道,分工很明确:

通道 走什么 干什么
tRPC over Electron IPC 同步请求-响应 线程列表、设置、Provider 配置、全文搜索等所有 CRUD
Hono HTTP 服务(127.0.0.1) SSE 流式 只负责一件事:对话

为什么 CRUD 都走 IPC 了,对话还要单独起一个本地 HTTP server?因为流式这件事 IPC 天生不擅长,而 HTTP 顺手解决了三个问题:

  1. 流式传输:Electron IPC 本质是请求-响应,多 chunk 的流式输出走 HTTP/SSE 才自然;
  2. 直接复用 AI SDK 的 useChat :Vercel 的 DefaultChatTransport 默认就是 POST 到一个 HTTP endpoint,前端几乎零改造;
  3. 可恢复的流(resumable stream) :服务端把进行中的流缓冲在内存里。对话生成到一半你刷新页面、切走再回来,生成不会断 ------客户端用 GET /api/chat/{threadId}/stream 重新接上,从断点继续往下吐 token。这套状态 IPC 维护不了。

二、ACP:集成把外部 CLI Agent 变成「模型」:

因为大家可能会订阅其他 AI Agent 产品, 所以能复用自己已有的额度是必要的。

它走的是 ACP(Agent Client Protocol) :一个为「Agent 之间通信」设计的双向 JSON-RPC 协议。Atrium 扮演 ACP 客户端 ,用 spawn 拉起外部 CLI(claude-agent-acp / codex-acp / gemini --acp),通过 stdio 上的 NDJSON(每行一个 JSON 消息)和它通信:

flowchart LR User[用户在选择器选 Claude Code] --> Route{Provider 类型?} Route -- cloud-api / ollama --> Native[&#34;Atrium 自己的<br/>streamText 循环&#34;] Route -- local-cli --> ACP[&#34;spawn 外部 CLI<br/>ACP / NDJSON over stdio&#34;] ACP --> Perm[权限请求回传 UI] ACP --> Stream[输出转成同一套 UIMessage 流] Native --> Stream

几个值得说的细节:

  • 路由分流/api/chat 入口判断 Provider 的 kind------cloud-api 和 Ollama 走 Atrium 自己的 Agent 循环,local-cli 则把整个回合交给外部 Agent 自己跑(它有自己的工具和循环,Atrium 只做中转)。
  • 权限照样收口:外部 Agent 在执行中通过 ACP 发来权限请求,Atrium 用一个 broker 把它「挂起」,弹窗交给你决定「允许一次 / 总是允许 / 拒绝」,再把结果回传,外部 Agent 才继续------你不会失去对它的控制。

三、本地全文检索:SQLite FTS5 + 结巴分词 + BM25

既然是本地优先,搜索历史对话就不能依赖云端。Atrium 直接用 SQLite 的 FTS5 建了一张虚拟表,配合触发器:消息一旦增删改,自动把文本切词后同步进全文索引,按 BM25 排序,标题命中再加权重提前。

默认分词器对中文基本无效。所以用 better-sqlite3 注册一个 jieba_cut() 自定义函数(底层是 jieba-wasm 结巴分词),让 FTS 的触发器在写索引时调它分词。

四、Skills:不造规范,直接复用已有的技能生态

Skill 就是一个带 SKILL.md 的目录------frontmatter 写 name / description / allowed-tools,正文是给 Agent 的操作指引。这套格式是 Anthropic 发明的,其他产品也默认这种格式。所以我干脆没有从零造自己的目录规范 ,而是直接去抓大家已经在用的几个数据源:内置(builtin)、共享的 ~/.agents/skillsClaude Code 生态Codex 生态。你在别的工具用过的技能,Atrium 直接就能用,不用再为它重写一遍。

几个实现细节:同名技能按来源优先级解析(builtin < codex < claude < agents,所以你放在 ~/.agents 的版本能覆盖生态里的同名拷贝),用 realpath 去重避免 symlink 重复计数。加载是渐进式披露 :默认只把每个技能的 name + description 注入上下文(连文件路径都不给),Agent 判断命中了再用 skill 工具按名拉取完整正文;激活之后,这一回合的可用工具会被收窄到该技能声明的 allowed-tools

五、子 Agent:保证主 Agent 上线文干净

主线程的上下文很宝贵,不该被一个「读 20 个文件做调研」的子任务塞满。所以 Agent 可以通过 task 工具把活儿委派给子 Agent :它在一个全新、隔离的上下文 里跑(只有一条任务 prompt),复用父级的模型、沙箱、数据库和工作区,自己跑一遍完整的 ReAct 循环------但只把最终结论回传给父对话,中间几十轮工具调用一律不进主线程。隔离本身就是目的:一大片工作在这里坍缩成一条干净的结果。

内置两个:general-purpose(继承父级全部工具的多步执行者)和 deep-research(收窄到 web_search / web_fetch / read_file / todo_write 的调研专家)。子 Agent 里禁用 task / skill / ask_clarification------不允许无限嵌套,技能在无头场景也不工作。

六、任务工具:让 Agent 不「写完就忘」

长任务最怕 Agent 走着走着开始跑偏。todo_write 工具让它把计划显式写出来([ ] 待办 / [>] 进行中 / [x] 完成),工具的返回值就是计划本身的回显(Plan updated · 3/5 done),前端同步渲染成一份看得见的任务清单。

关键不在「能写」,而在「不会被弄丢」:长对话触发上下文压缩时,旧消息会被折叠成摘要,计划很可能就糊没了。所以任务模块注册了一个 todoPreserver,在折叠发生时把当前计划原样搬进摘要,并附一句「这是当前计划,继续用 todo_write 更新它」。

七、权限系统:本地优先的两道关卡

本地 Agent 直接在你机器上读写文件、跑命令,安全模型必须交代清楚。Atrium 分两层:

  • 密钥怎么存 :用 Electron 的 safeStorage(绑定 macOS 钥匙串 / Windows DPAPI / Linux Secret Service)。系统加密不可用时直接拒绝以明文存储 ,而不是降级。明文密钥只在主进程内存里短暂出现,渲染进程永远拿不到解密后的 key
  • 工具怎么管 :调用分 full-access / default / auto-review 三档。「是否越界」的判定很具体------写到工作区外、危险或联网的 shell 命令、MCP 工具调用都算越界,会弹窗让你确认;auto-review 模式则交给一个 AI 审查员判断(它只能放行,永远不会扩大权限)。

底层执行落在一个 LocalSandbox 上。它不是安全牢笼 (命令本就以你的权限运行),价值在工程性:相对路径收敛到工作区根目录、命令超时 120s、中断时按进程树整组 kill(而不是只杀外层 shell,避免 dev server 之类留下孤儿进程)。真正的边界由上面的权限层把守。

八、上下文压缩:压缩可以,但别把「正在做的事」压没了

对话越滚越长,迟早撑爆上下文窗口。Atrium 参考其他 AI Agent 在回合内做压缩:把较早的消息用一个廉价模型折叠成摘要,只保留最近的窗口。

但压缩有个隐患------如果把 Agent「正在执行的东西」也一并摘要糊掉,它就会产生幻觉 :忘了计划到哪一步、忘了自己正在按哪个 skill 的 SOP 干活。所以压缩不是无脑摘要,而是带一组 preserver :折叠时挨个问每个 preserver「有没有必须带走的东西」,把它们的原文逐字 拼到摘要后面。目前两个关键 preserver------todoPreserver(当前 todo 计划)和 skill preserver(当前激活的 skill 正文)------保证无论压缩多少轮,Agent 手里始终攥着「我在做什么、做到哪了」的原始事实。(至于工具定义,它每一步都会重新下发,本就不在被压缩的历史里,不用担心丢。)

九、跨会话记忆:一个工具,加一套后台

记忆分两部分。

一是给 Agent 的通用记忆工具。 它是文件式的,分两个作用域------全局(memory/global)和项目域(memory/projects/<工作区>)。会话过程中 Agent 自主用 memory 工具读写,每条记忆是一个带 frontmatter(name / description / type)的 .md。注入上下文的只是一份 MEMORY.md 索引(按作用域裁剪到约 25KB,且锚在第一条消息进缓存前缀),Agent 想看某条详情再用工具点开------又是上面那套「渐进式披露 + 省缓存」的思路。

二是一套后台整理系统。 只让 Agent 往里写,时间一长记忆库会越堆越乱、重复、过期。所以后台有一个定期运行的整理调度(这块的思路参考了 Claude Code 和 Codex 的记忆后台逻辑 ):周期性地把零散、陈旧的条目重新总结、合并,同时保持 name / description / type 的结构不变。它的职责只有一个------让整座记忆库长期维持高质量、可维护,而不是随用随脏

试试看

完全开源,MIT 协议,本地跑三步:

bash 复制代码
git clone https://github.com/lhz960904/atrium.git
cd atrium
bun install   # postinstall 会为 Electron 重编原生模块
bun run dev

首次启动打开 设置 → 提供商 ,启用一个 Provider、粘贴你自己的 API key(会用系统钥匙串加密存在本地),就能开聊。也可以直接去 Releases 下打包好的安装包。

🚨 目前该应用还没有完全在 Windows 上测试过,所以不保证 Windows 用户可以正常使用,如果有小伙伴 Windows 有问题也可以把问题发在评论区,我有空去修复。

最后

代码在 GitHub:github.com/lhz960904/a...

这是一个人业余时间做的项目,本地优先意味着所有东西都在你自己手里,同时也意味着很多边界 case 还没打磨完。欢迎提 Issue 和 PR,如果觉得有意思,点个 star ⭐ 是对我最大的鼓励。

相关推荐
齐翊1 小时前
分享一个在 Claude Code 里 [同时] 用多个 ApiKey 的方法
程序员·github·agent
用户298698530141 小时前
在 React 中使用 JavaScript 将 Excel 转换为 SVG
前端·javascript·react.js
CodingSpace2 小时前
ESLint
前端
老梁agent2 小时前
工业 Agent 的边缘部署:Ollama + LangChain4j 本地推理方案
物联网·边缘计算·agent
Csvn2 小时前
异步错误捕获的六大陷阱:await 裹着 try-catch 就一定稳了吗?
前端
用户059540174462 小时前
向量库静默丢数据踩坑实录:Playwright 端到端测试让我排查了72小时
前端·css
星栈2 小时前
SPA 写累了?试试 LiveView:服务端管状态,前端不写 JS
前端·前端框架·elixir
武子康2 小时前
调查研究-206 DeepSeek DSpark 深度解析:大模型推理加速,正在从“模型能力”转向“系统工程”
人工智能·agent·deepseek
labixiong2 小时前
手写Promise--微任务、静态方法、async/await 全搞懂(三)
前端·javascript