我做了个跑在 Electron main 进程里的 AI Agent 引擎:4 个架构决策和 3 个坑
0. 背景:为什么要自己写一个
先说结论:我不是为了造轮子而造轮子。
起因很朴素——我想要一个能替我处理"跨仓库杂活"的 AI,但试过的方案都卡在同一条:我的代码不能上传到别人的服务器。云 Agent 的第一步是"把你的仓库传上来",到这里就结束了。
所以只剩一条路:引擎跑在我自己的机器上。
这个决定一旦定下来,后面所有架构选择基本就被锁死了。下面是我做过的几个主要决策,以及踩过的坑。
1. 决策一:把整个引擎塞进 Electron 的 main 进程
这是最反直觉的一个决定。
通常 Electron 应用的架构是:main 进程只管窗口和系统集成,业务逻辑放独立的后端服务(Node / Go / Python 皆可),两边走 HTTP 或 IPC。
我一开始也是这么做的——有独立的 worker-ant-server(REST,:9411)和 worker-ant-web(Next.js,:9410)。后来我把它们整体归档了,把引擎全部搬进 Electron main 进程。
理由是:我要解决的问题是"在本机做真事",而"本机"这件事 Electron 本来就有一等公民身份。
- 读写文件、执行命令、访问系统托盘、调系统通知——main 进程天然就在这个位置上;
- 如果再加一层后端服务,就等于平白多了一个跨进程边界:每一次工具调用、每一次事件落库、每一次产物写盘,都要序列化一次;
- 更要命的是启动复杂度:用户要跑起来,得先把后端服务起起来,再起桌面端。多一个进程就多一个失败点。
搬完之后,剩下的只有一个常驻 HTTP 服务:ANS(AI 资源中心,:27183),负责模型匹配与网关。桌面端作为客户端,用 HMAC 出站签名调它。
架构大概是这样:
┌──────────────────────────────────────────────┐
│ 桌面端 (Electron) │
│ ┌────────────┐ IPC ┌──────────────────┐ │
│ │ renderer │◀────────▶│ main 主进程 │ │
│ │ (React) │ │ · 任务引擎/调度 │ │
│ └────────────┘ │ · Agent 执行循环 │ │
│ │ · 记忆/知识图谱 │ │
│ │ · IM 适配器 │ │
│ │ · 成果交付 │ │
│ └────────┬─────────┘ │
└───────────────────────────────────┼───────────┘
│ 出站签名(HMAC)
┌─────────▼─────────┐
│ ANS 资源中心 :27183│
└─────────┬─────────┘
│
┌─────────▼─────────┐
│ 你的模型端点 │
│ (OpenAI 兼容) │
└───────────────────┘
代价是什么? 是 GUI 卡顿风险。所有耗时操作都在 main 进程里,一旦有同步阻塞,界面就假死。我在这上面花了不少功夫(后面第 4 节讲)。
2. 决策二:用事件溯源,而不是一张状态表
任务是"可暂停 / 可恢复 / 可重放 / 可审计"的。这四个词决定了数据层不能用普通的"状态表 + UPDATE"。
如果 wa_tasks 表只有一行 status 字段,那么:
- 可重放做不到——状态被覆盖了,历史没了;
- 可审计做不到——你只能看到"现在是什么",看不到"为什么变成这样";
- 可恢复很脆——进程崩在中间,你只知道状态是 running,不知道它走到哪一步。
所以我用了事件溯源:所有变更先落成一条事件(event-store),再由投影器(event-projectors)折算出当前状态视图。
好处是每一步动作都留痕,任务可以按事件流重放;坏处是读模型和写模型要分开维护,投影器的正确性成了新的责任面(每加一个事件类型,就要检查一遍所有读它的地方)。
这条后来还救过我:"任务是不是真的完成了"这个判断,本质上就是一个投影问题——如果完成状态由多个出口各自写入,迟早会出现"三本账对不上"。我被迫把这些出口收敛成同一把尺(见下节)。
3. 决策三:任务自动拆解 + 子代理
一个真实任务很少是原子的。"把所有仓库的 TODO 汇总成一份 issue 列表"至少要经过:找仓库 → 各自扫描 → 聚合 → 生成草稿。
所以引擎会把任务自动拆解成子任务,并且可以派给不同类型的子代理:
- 部署 / 探索 / 构建 / 验证 四类专职子代理
- 以及 辩论、蜂群 两种协作模式
拆解带来一个很容易被忽略的问题:子任务"做什么"和"为什么有它"是两件事。
我一开始只让模型产出子任务的描述(做什么),结果出现了一批"看起来合理、但和总目标没关系"的子任务——它只是在做事情,不是在做事。后来我给拆解加了目的因:每个子任务必须携带"我为什么存在",缺这一项的条目在解析口就被拒掉,其余照建。
这一条的经验是:约束要下在解析口,不要下在事后校验。事后校验意味着坏数据已经进库了,你还得再去清。
4. 决策四:「完成」必须有人签字
这是我吃过最大的亏。
早期版本的判定很简单:Agent 说自己做完了,就算做完了。结果就是——它经常在没做完的时候说做完了。
后来我把它改成三段:
- 产出校验:任务声明的产物是不是真的在盘上(文件存在、非空、格式对);
- 验收评分:由引擎对产物打分,而不是由执行的模型自称;
- 质量分入闸:分数回流到调度策略——低分完成不会被认为是"连续成功"。
第 3 条特别有意思,也是我后来才意识到的:原来的策略调整只看"成功/失败"这个布尔值。一个任务用最便宜的档位糊弄完了(质量分很低),系统会认为"连续成功",反而往下调档——越糊弄越省,越省越糊弄。
改成吃质量分之后:低分完成 ⇒ 升档(换更强的模型/更多步数),高分完成 ⇒ 一律不动。行为立刻正常了。
5. 坑一:Electron ABI 与 Node ABI,互斥
这个坑我至今觉得值得单独写一节。
场景:桌面端要用 better-sqlite3(原生模块),ANS 服务也要用同一个 better-sqlite3。它们是同一个包,被 pnpm workspace 提升到共享位置。
问题来了:
- Electron 需要 Electron ABI 的编译产物;
- 系统 node 需要 Node ABI 的编译产物。
同一份 .node 二进制只能是一个 ABI。于是:
- 先起 ANS(Node ABI),再起桌面端 → 桌面端的 electron-rebuild 把共享模块改写成 Electron ABI → ANS 虽然已经在内存里跑着,源码一改动、watch 重启就崩;
- 反过来,想靠桌面端"按需代起"ANS → 代起用系统 node → 加载 Electron ABI 的模块 → NODE_MODULE_VERSION 不匹配,当场崩。
结论:dev 环境下"桌面端自动拉起 ANS"是不可能成立的。
我的处理是:
- 如实失败,而且快——子进程一 exit 就当轮返回,按 stderr 分因(native-abi-mismatch / child-exited),附上 stderr 尾巴,不把"早就死了"静默拖成 20 秒后的"超时";
- 给用户看得懂的话——检测到 ABI 冲突,直接提示"请先跑 pnpm dev:ans";
- 退出时只杀自己拉起的那个进程,不动用户手动起的服务。
这一条我最大的体会是:"失败快且说人话"是一种可以设计的能力,不是碰运气。默认行为永远是"静默拖到超时",那是最糟的体验。
6. 坑二:IM 入站媒体的"真形状"
我的产品支持用微信 / 钉钉 / 飞书 / 企业微信 / QQ 派活,所以要把用户发来的图片、文件落盘。
我按公开资料和推测,写了一版"字段可能在顶层"的解析:找 url / file_url 之类的候选名。测试全绿。
然后上真实环境——一个件都没落下来。
真实帧长这样(示意):
{ "type": 4,
"file_item": { "media": { "encrypt_query_param": "...", "aes_key": "...", "full_url": "..." },
"file_name": "测试.txt", "md5": "...", "len": "1610" } }
取件地址和钥匙嵌在 media 里,顶层根本没有。而且——密钥有两种写法:一个字段给的是 base64 过的十六进制串,另一个给的是那串十六进制本身,解出来是同一把 AES-128 钥匙。
更关键的是:CDN 上的字节是 AES-128-ECB 密文,不是明文的。所以流程是:取 full_url → 拿到密文 → 用归一后的钥匙解密 → 校验 md5 → 才落盘。
这个坑的教训不是"要看清文档",而是:我的测试样本是我自己按推测形状手写的。于是"取件实现存在"、"五家适配器都调它"、"测试全绿"这三件事同时为真,而件永远到不了盘上。用自造的等价样本做的端到端测试,证明不了端到端。
后来我给这类东西立了规矩:测试夹具必须照抄生产形状(含外键、含真实 DDL),并且要有一个"从 0 变 1"的正面断言——只断言"不抛异常"是捉不住这类问题的。
7. 工程纪律:137 个回归脚本 / 14612 条断言
最后说一个不那么性感但对项目生死攸关的东西。
这个项目长期只有我一个人维护,而且改动的是一个跑在用户机器上、真的要动文件跑命令的引擎。所以我把回归测试做得很重:
- 每个迭代轮次配一个受控回归脚本(rr_*.ts),跑在内存数据库上;
- 全量回归是 137 个脚本 / 14612 条断言,并发跑完约 235 秒;
- 构建标识(build tag)一处真源,其余脚本由代码派生核对——避免"改了版本号但脚本里还抄着旧值"。
还踩过一类很损的坑:哑雷断言。
比如我写过 eq(x, cond ? 'a' : 'b') 这种断言,其中 cond 恒真——它永远通过,且永远通过。测试全绿,但什么都没验证。这类"长得像断言、其实是恒真式"的东西,后来被我做成专项扫描(含反证标本)。
一个人写测试,最容易骗过的人是自己。
8. 现状
- 开源 AGPL-3.0,源码在 GitHub。
- 版本 v0.2.0,63 个工具 / 8 大类,四类记忆(情景 / 语义 / 程序 / 反省),支持 MCP 与多机协同。
- macOS 版(Apple Silicon,macOS 13+)已发布:Developer ID 签名 + Apple 公证,可从 GitHub Releases v0.2.0 下载 dmg / zip(附 SHA-256);Intel Mac 与 Windows 暂未提供,仍需从源码跑(Node ≥ 22 + pnpm ≥ 9,两个终端)。
- 社区版免费;Pro 授权(¥99/月 · ¥899/年)、早鸟终身与 Team 授权包(¥599/月 · ¥5990/年,每包 ≤20 节点)已在面包多开售,桌面端自带 14 天 Pro 试用。授权已开售,安装包也已发布。
如果你对上面任何一个决策有不同做法,欢迎在评论区拍砖——特别是事件溯源的读模型维护成本这一块,我很想听听别人的经验。
- 源码:github.com/Reamy/worke…
- 官网:workerant.cn/
- 授权(Pro / 早鸟):mbd.pub/o/author-bW…
本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
I made an AI Agent engine: 4 architecture decisions and 3 pits in the Electron process
Background: Why write one yourself?
And conclude: I did not make wheels for them.
It's a simple cause -- I want an AI who can handle my "cross-storey chores" but all the solutions I've tried are stuck on the same page: I can't upload my code to someone else's server. The first step of Cloud Agent is to bring your warehouse up.
So there's only one way: the engine runs on my own machine.
As soon as this decision is made, all the structural options behind it are basically locked to death. Here are some of the major decisions I've made and the pits I've stepped on.
1. Decision I: insert the entire engine into the Electron process
This is the most counterintuitive decision.
Normally, the Electron application is structured as a main process that only integrates windows and systems, business logic with a stand-alone back-end service (Node / Go / Python), with HTTP or IPC either.
I did it at first — there were independent working-ant-server (REST, 9411) and working-ant-web (Next.js, 9410). Then I archived them all and moved them all into the Electron process.
The reason is, the problem I'm going to deal with is "doing the real thing on this plane," and the "home" thing is already a civil status.
- Read and write documents, execute orders, access system trays, adjust system notifications - the Main process is naturally located at this location;
- If one more back-end service is added, it will be equal to one more cross-process boundary: each tool call, each event, each production disk, is sequenced;
- More importantly, the complexity of start-up: the user has to start running from the back end to the desktop. One more process and one more failure point.
After the move, only one permanent HTTP service is left: ANS (AI Resource Centre: 27183), responsible for model matching and gateway. Desktop end as client, switch it with HMAC out of station signature.
The structure is probably this:
┌──────────────────────────────────────────────┐
Electron
│ ┌────────────┐ IPC ┌──────────────────┐ │
Renderer – – – main process
· (React) │ Mission Engine/Planning │
{\cHFFFFFF}{\cH00FFFF} {\cHFFFFFF}{\cH00FFFF} {\cHFFFFFF}{\cH00FFFF} {\cHFFFFFF}{\cH00FFFF} {\cHFFFFFF}{\cH00FFFF} {\cHFFFFFF}{\cH00FFFF}
• Memory/knowledge mapping
• IM adaptor
• Delivery of results
│ └────────┬─────────┘ │
└───────────────────────────────────┼───────────┘
• Outpost signature (HMAC)
┌─────────▼─────────┐
ANS Resource Centre: 27183
└─────────┬─────────┘
│
┌─────────▼─────────┐
Your model end.
│ (Open AI compatible) │
└───────────────────┘
What's the price? All time-consuming operations are in the main process, and the interface dies if there's a synchronous blocking. I've worked a lot on this.
2. Decision II: Retroactivity of events rather than a status table
The task is "suspended / recoverable / re-playable / auditable". These four words determine that the data layer cannot use the normal "state table + UPDATE".
If the wa tasks table has only one line of status field, then:
- Can't be replayed - state covered, history lost;
- But the audit can't -- you can't see "what is it now," you can't see "why is it?"
- Well, it's not easy to recover -- the process is in the middle, you know the state is running, you don't know where it is.
So I used the event tracer: all changes first turn into an event-store and then the projector (event-projectors) converts the current status view.
The advantage is that every move leaves a mark and the task can be re-dispatched by the event; the disadvantage is that the model and the writing model are maintained separately and the projector's correctness becomes a new liability (for every event type, every place where it is read is checked).
And then it saved me: "The mission didn't really finish," which is essentially a projection question. I'm sorry. If the completion status is written in each of the multiple exports, sooner or later there will be a "three unmatched". I was forced to condense these exports into the same harvest (see the following section).
3. Decision III: Auto-dismantling of missions + sub-agents
A real task is rarely atom. "Consolidate all warehouse TODOs into a list of issues." At least go through: look for the warehouse and scan each one of them. Scan each one of the polymers to generate drafts.
So the engine automatically disassemblys the mission into subtask and can be assigned to different types of sub-agents:
- Deployment / Exploration / Construction / Validation of four types of specialized sub-agents
- And debate, bees, two modes of collaboration.
Dismantling brings up an easy question: subtask "do what" and "why it" are two things.
I started with a description of the model output subtask (what to do), and the result was a set of subtasks -- it's just doing things, not doing things. Then I added the purpose: each subtask had to carry "why I exist" and the missing entry was rejected at the decryption and the rest was built.
The experience of this article is that it is subject to interpretation and not to subsequent validation. After-censorship means bad data is in the library, and you have to clear it again.
4. Decision IV: "Fixed" must be signed
It's the biggest loss I've ever had.
The early version was simple: Agent said he was done, even after. The result is that it's often done when it's not done.
Then I changed it to three paragraphs:
- Validation of outputs: the mission statement is not authentic (document exists, is not empty, format is correct);
- acceptance check rating: scoring of the product by the engine, not by the implementation model itself;
- Quality clearance: points flow back to dispatch strategy - Low scores cannot be considered "continuous success".
Article 3 was particularly interesting, and I realized it later: the original strategy adjustment was based on the boolean value of "success/fail". When a mission is finished with the cheapest slot (low-quality), the system thinks "continuous success" and moves down -- the more the game goes on and on.
After eating quality points: Low scores completed ⇒ up promotion (more models/steps), high scores completed ⇒ don't move. Behave immediately.
5. Pipe I: Electron ABI and Node ABI, mutually exclusive
I think this pit is worth a separate section.
Scene: The desktop end uses better-sqlite3 (native module) and the ANS service uses the same better-sqlite3. They're the same package, upgraded to a shared location by pnpm workspace.
The question is:
- Electron needs a compiler for Electron ABI;
- System node needs Node ABI compilers.
The same.node binary can only be an ABI. So:
- Start with the ANS (Node ABI), then start with the desktop end Electron-rebuild on the desktop side, recast the shared module into the Electron ABI Z ANS, although already running in the memory, the source code changes, and the watch starts again;
- On the other hand, using the desktop end to replace the system "ANS on a needs basis" with the module "NODE MODULE VERSION" that loads Electron ABI does not match, crashes.
Conclusion: "The desktop-end automatically pulls up ANS" is not possible in the Dev environment.
My treatment is:
- If the failure is real, and fast - an exit returns on the wheel, with a stderr tail attached to the stderr, and does not drag long-dead into "timeout" 20 seconds later;
- Words for users -- ABI conflicts detected, direct hint "Pnpm dev:ans" first;
- Only kill the process that he or she pulls up when he or she exits and does not use the service that the user manually starts.
And the best thing I know about this is that "failure and talk" is a designable ability, not luck. Defaults are always "silent to timeout", which is the worst experience.
6. Pipe II: The real shape of IM access media
My product supports the delivery of WeChat/ Nail/ Flying Book/ Enterprise WeChat/ QQ, so the images and files sent by the user are to be dropped.
I wrote a version of the analysis of "field may be at the top" based on public information and speculation: looking for candidates like url / file url. Test full green.
And then to the real environment -- none of it went down.
The real frame is this:
{ "type": 4,
"file_item": { "media": { "encrypt_query_param": "...", "aes_key": "...", "full_url": "..." },
"file name": "test.txt", "md5": "...", "len": "1610"
The fetching address and key are embedded in media, and there is no top floor. And -- the key is written in two ways: one field is given a hexadecimal string from case 64, the other is given to the hexadecimal itself, which is solved by the same AES-128 key.
More importantly, byte on CDN is an AES-128-ECB secret, not explicitly. So the process is to take the full url and get the secret text and decrypt it with the underlying key and verify the md5 before dropping the disk.
The lesson of this pit is not to "see the file" but: my test sample was written by my own hand. So the three things, "Take the package to exist", "Five adapters to adjust it," and "test the whole green" are true, and they never reach the plate. End-to-end tests with self-built equivalent sample do not prove end-to-end.
And then I set the rules for this type of thing: the test kit has to copy the shape of the production (with external keys, with real DDL) and there's a positive assertion that "from 0 to 1" -- just that "not to leave the anomaly" won't make it.
7. Engineering discipline: 137 regression scripts / 14612 assertions
Finally, something less sexy but vital to the life and death of the project.
This project has long been maintained by me alone and has been modified by an engine that runs on a user machine and really moves a file running order. So I put a lot of pressure on the regression test:
- A controlled re-entry script (rr *.ts) to run the memory database in each iterative rotation;
- Full return was 137 scripts / 14612 claims and ran for approximately 235 seconds;
- Build identification (build tag) is a real source, and the rest of the script is checked by code -- avoid "changed version numbers but old values are copied in the script".
It also stepped on a very damaged pit: dumb thunder claims.
For example, I've written this assertion that cond constant -- it's always passed, always passed. All green, but nothing verified. This kind of thing, which looks like an affirmation, which is a constant form, was then specially scanned by me.
The one who writes the tests is the one who can fool himself.
8. Current status
- Open source AGPL-3.0, source code in GitHub.
- Version v. 2.0.0,63 tools / 8 major categories, four types of memory (scenario/ semantic/ program/ reflection), supporting MCP synergies with multiple machines.
- The MacOS version (Apple Silicon, MacOS 13+) has been signed by publish:Developer ID + Apple notary and can be downloaded from GitHub Releas v. 2.0. dmg / zip (with SHA-256); Intel Mac and Windows are not available for the time being and still need to run from the source code (Node ≥22 + pnpm ≥9, two terminals).
- Free of charge for community editions; Authorization has been opened for sale and installation packages publish.
If you have a different approach to any of the above decisions, I would like to hear about the experience of others when you welcome the building of bricks in the comment area — especially with regard to the maintenance costs of retroactivity models.
- Source: Github.com/Reamy/work...
- Official: workingrkerant.cn/
- Authorization (Pro / Early Bird): mbd.pub/o/author-bW...
Auto-fetched and translated by AI Atlas. All rights belong to the original author.