第一章:全景 — AI 数字员工是什么
阅读导航: 本书章节标题暗藏一条线索——我们在造一个"人"。第一章(全景)建立坐标系,第三章(大脑)是技术密度最高的核心章,第四至十二章各拆解一个"器官",可按兴趣跳读,第十三章换视角讲"一个人加 AI 如何构建这个系统"。贯穿全书的设计原则只有一句话:判断归 AI,执行归脚本。你会在每一章看到它的不同表达。
1.1 从 RPA 到 AI Agent 到 AI 数字员工:三代方案的演进
在"AI 数字员工"这个概念出现之前,企业自动化经历了三代方案。三代方案试图解决同一个问题:如何让机器代替人完成重复性工作。每一代都比前一代更进一步,但也都碰上了自己无法逾越的限制。
第一代:RPA——录制回放,规则驱动
RPA(Robotic Process Automation,机器人流程自动化)是一个年规模超百亿人民币的成熟行业。代表厂商 UiPath 已在纽交所上市,Automation Anywhere 估值近 70 亿美元。技术上,RPA 的底层与 Selenium 同源——通过定位 GUI 元素、模拟鼠标点击和键盘输入来驱动应用程序,再将这些操作封装成可视化流程,供不具备编程能力的业务人员使用。
RPA 的存在源于一个普遍的组织困境。大型企业内部往往有几十套独立系统——OA、工单、征信、风控、ERP——分属不同部门,彼此没有 API 互通。一个审批流程可能要跨越三四个系统:从征信系统查询评分,到信贷系统查看申请,到风控系统执行规则校验,再回到信贷系统完成审批操作。要打通这些系统的后台接口,需要跨部门协调,排期往往以月计。而业务人员每天就在这些网页之间重复同样的操作,少则几十次,多则几百次。
RPA 提供了一条不依赖后台改造的路径:在 UI 层模拟人的操作。 录制一遍操作流程,生成脚本,7×24 小时循环执行。不需要 IT 部门配合,不需要系统开放接口,采购 license 后即可部署。银行、保险、证券的运营部门是 RPA 的核心客户群。东方证券曾公开披露,他们通过 RPA 优化了 140 个业务流程,涉及 90 套内外部系统。
在结构固定、界面稳定的场景中,RPA 的表现接近理想。
但它的局限同样明确。RPA 脚本依赖固定的 UI 元素定位——CSS 选择器、XPath 或屏幕坐标。界面一旦改版,元素位置变化,脚本立即失效。页面出现预期之外的弹窗或加载延迟,执行流程中断。更根本的限制在于,RPA 只能执行预定义的规则分支,无法处理需要语义理解的判断——"这笔申请的材料是否完整""这条告警是否需要升级处理",这类问题超出了规则引擎的能力边界。
维护成本是 RPA 在企业落地后面临的最大挑战。每个新流程需要重新录制,每次界面变动需要人工修改脚本。当企业部署上百个自动化流程后,脚本维护量线性增长,最终成为沉重的技术债务——与维护大规模 E2E 测试套件的困境如出一辙。我在文档团队工作时亲眼见过这种情况:几个人专职维护 RPA 脚本,节省的人力与投入的维护成本几乎相抵。
第二代:纯 AI Agent——每次从零推理
2023 年之后,大语言模型的能力突破催生了第二代方案:让 AI 直接操作浏览器。思路很直接——LLM 能理解自然语言指令,能分析页面截图,能推理下一步操作,那么让它像人一样在浏览器中完成任务,理论上可以绕过 RPA 的所有限制。
2024 年上半年,我在公司内部用千问 2.5-72B 开源模型构建了一个通用 Agent 原型——单循环架构,依靠精心设计的提示工程驱动。原型的演示效果令人印象深刻:用户输入"审批今天所有金额小于 500 的工单",Agent 自主打开浏览器,获取页面状态,推理操作步骤,逐一执行,直到任务完成。
但从原型到生产环境,四个问题逐一暴露:
延迟。 每个操作步骤都需要重新获取页面状态、调用 LLM 推理。人工 10 秒能完成的操作,Agent 需要 1-2 分钟。LLM 推理耗时叠加网络往返,使得批量处理场景的总耗时远超人工操作。
不稳定。 每次执行都是独立的推理过程,没有对历史执行路径的记忆。同一个任务今天成功、明天可能失败——页面加载时序的微小变化、元素渲染的延迟、网络波动,都可能导致推理路径偏离。
成本。 每个操作步骤都消耗 LLM Token。一次包含二十步操作的审批流程,Token 消耗可达数千乃至上万。当任务量从演示级别扩展到日常业务规模,成本迅速失控。
不可复用。 Agent 在一次会话中积累的操作经验——哪个按钮在哪、接口返回什么格式——存在于当次对话的上下文中。会话结束,经验消失。另一个用户面对同样的系统,需要从头开始。
这些问题指向一个根本矛盾:LLM 擅长理解和判断,但不擅长可靠地重复执行。 每次执行本质上都是一次独立推理,而独立推理意味着不确定性。对于需要日常稳定运行的业务流程,不确定性是不可接受的。
第三代:AI 数字员工——判断归 AI,执行归脚本
Halo 的设计从一个不同的问题出发:能否让 AI 只做它最擅长的事?
有些事需要判断力:这条工单该不该批?金额没超标,但申请人上周刚被风控标记——这类需要综合多个因素、没有固定答案的决策,是 AI 擅长的。另一些事需要可靠性:登录系统、调用接口、提交表单——步骤固定,要的是每次都做对,一段验证过的脚本比 AI 推理更可靠。前两代方案的问题,本质上都是把这两种能力混在了一起——RPA 完全排除了 AI 的判断能力,纯 AI Agent 又把执行的可靠性也交给了 AI。
第三代方案的核心是将两者分离:
第一代 RPA 第二代 AI Agent 第三代 AI 数字员工
──────────── ──────────────── ──────────────────
纯规则驱动 纯 AI 驱动 AI 判断 + 确定性执行
录制回放 每次从零推理 调用预封装的 Skill
稳定但死板 灵活但不可靠 既灵活又可靠
能力不可共享 经验不可复用 两层市场互驱生长在 Halo 的架构中,AI 负责一件事:根据当前状态和用户设定的规则,决定做什么、什么时候做、做还是不做。"怎么做"交给预先编写并经过验证的 Skill——一段确定性代码,接受配置参数,返回结构化结果,执行过程不依赖 LLM 推理。
这个区分一次性解决了前两代的核心问题。Skill 是确定性的,不存在"推理偏差";执行不经过 LLM,耗时在毫秒级别;一个人编写的 Skill,所有用户都可以安装使用;LLM 仅在判断环节消耗 Token,执行环节的边际成本为零。
回头审视三代方案的演进脉络,区别不在于技术复杂度,而在于对 AI 能力边界的认知。第一代完全不信任 AI,全靠规则;第二代完全依赖 AI,将判断和执行都交给它;第三代找到了正确的切分点——AI 负责判断,确定性脚本负责执行。这个原则贯穿本书后续每一章的设计决策。
一个数字员工的完整工作周期
以一个具体的业务场景说明第三代方案的完整工作方式。
一个运营人员每天登录公司 OA 系统,逐条检查待审批工单,500 元以下的直接批准,超过 500 元的上报主管。这件事他每天花 40 分钟,重复了两年。
在 Halo 中,他用自然语言创建一个数字员工:"每天早上 9 点检查 OA 待审批列表,500 元以下自动批准,超过 500 元通知我。"
这个数字员工不是一段脚本,也不是一次对话。它是一个持续存在的自主 Agent:有自己的工作周期(每天 9 点由调度引擎唤醒),有工作规则(用户用自然语言描述的判断逻辑),有执行能力(已安装的 OA 审批 Skill),有跨次运行的记忆(知道昨天处理到了哪一条),有通知通道(能通过企业微信主动联系用户)。
第二天早上 9 点,调度引擎唤醒它。它调用 Skill 获取待审批列表——这一步是确定性脚本,毫秒级完成,不消耗 LLM。拿到列表后,AI 逐条做判断:金额、类目、申请人历史记录,综合决定批还是不批。12 条自动批准,3 条超过阈值,通过企业微信发送一条汇总消息。整个过程用户没有参与,打开 Halo 可以回看完整的工作日志——每一步调用了哪个 Skill、AI 的判断依据是什么、最终执行了什么操作。
这就是第三代方案的最终产物。RPA 能做到定时执行和自动点击,但无法根据内容做语义判断。纯 AI Agent 能做判断,但每次都从零推理,不稳定且成本高。数字员工将两者的优势整合在一起:AI 负责理解和决策,Skill 负责可靠执行,调度引擎负责按时唤醒,记忆系统负责跨次连续性,通知系统负责主动汇报。
这还只是单个数字员工的工作方式。多个数字员工之间可以互相通信——审批助手发现一笔异常采购,通知风控助手介入核查;风控助手完成核查后,将结论回传给审批助手继续处理。它们各自有独立的工作规则和记忆,但能在需要时协作,形成跨职能的工作流。第九章会展开数字员工之间的通信机制。
驱动这些数字员工的底层引擎与驱动对话的引擎完全相同——同一个 Agent Loop,同一套工具调用机制,同一个 LLM 推理循环。区别仅在于触发方式:对话由用户输入触发,数字员工由调度引擎按时间或事件触发。这意味着数字员工的能力上限不局限于审批、监控这类定时任务——写代码、生成报告、操作浏览器完成复杂调研,任何 AI Agent 能在对话中完成的事,数字员工都可以自主完成。第三章会展开这个通用引擎的完整架构。
后面每一章拆解的,就是这个场景背后的每一个技术环节。
1.2 判断与执行的架构分离
上一节用三代方案的演进引出了一个结论:AI 擅长判断,不擅长可靠地执行。这一节把这个直觉变成一个可操作的架构原则,然后看它怎么落进代码里。
分离确定性操作
判断需要理解力——这条审批单该不该批?这个告警严不严重?没有固定答案,需要语义理解和上下文推理。LLM 是为这个设计的。
执行需要可靠性——调一个接口,填一组字段,点一个按钮。要的是每次都做对,失败了能重试,结果可预期。这是确定性程序的领域。
让 AI 承担执行,等于让一个擅长即兴发挥的演员去当流水线工人。流水线不需要创造性地理解怎么拧螺丝——它需要每次都拧到同样的扭矩。
Halo 数字员工的架构原则由此而来:AI 只管判断,执行交给确定性脚本。
从 Skill 到 browser_skill
Skill 在 Claude Code、Codex 等 Agent 工具中已经是成熟概念。本质上,Skill 是 Agent 的扩展能力包——以文件形式存放于约定目录,运行时加载描述信息进上下文,由 Agent 依据其中的能力描述自主判断何时启用、如何执行。其中的"执行"通常由模型现场生成代码完成:读完描述,写一段 bash 或 Python 丢进沙箱跑。这种通用 Skill,Halo 也直接支持。
Halo 在此之外定义了一种自有形态——browser_skill。它在 SKILL.md 之外多带一段事先写好的脚本(index.js),由内置工具 browser_run 注入到 Halo 内置浏览器的当前页面执行。
// .claude/skills/oa-approvals/index.js
async (params) => {
const resp = await fetch('/api/oa/approvals/pending',
{ credentials: 'include' })
return (await resp.json()).items
}browser_run({ file: ".claude/skills/oa-approvals/index.js" })
模型不再现场生成执行代码,只决定调用哪个 browser_skill、传什么参数;脚本本身是确定性的、版本化的,与用户登录态、cookie 共享同一份 session。browser_skill 作者只管写"做什么","在哪做、以谁的身份做"由 Halo 环境提供。脚本可以人工编写,也可以由 AI 根据用户实际操作自动生成。这套机制依赖 Halo 内置浏览器与脚本运行环境,普通 Claude Code 没有对应承载。第八章会展开这套执行环境。
AI 的角色由此清晰:从 browser_skill 拿到结构化结果,根据用户预设的规则("500 元以下自动批,差旅费一律通知我")做判断,再调用另一个 browser_skill 执行审批。判断走 LLM,执行走脚本。
这个切分解掉了纯 AI Agent 的三个核心问题:执行不再依赖 LLM 推理,不会"发挥失常";速度从秒级降到毫秒级,LLM 只在判断环节消耗;browser_skill 是文件,可被安装、共享、版本化——能力可以跨用户积累。
一个数字员工长什么样
Skill 是执行单元——前面已经看到它的形态(一段 JS 嵌在 .md 描述里)。那判断单元——数字员工本身——长什么样?
一份 YAML。下面是 1.1 那个 OA 审批数字员工的精简定义:
name: OA 审批助手
type: automation
system_prompt: |
每天早上 9 点检查 OA 待审批列表:
- 500 元以下且不是差旅费,自动批准
- 超过 500 元或差旅费,通过企业微信通知我
- 同一申请人 7 天内第 3 次申请,标记复核
subscriptions:
- source:
type: schedule
config: { cron: "0 9 * * *" }
memory_schema:
last_processed_id:
type: string
description: 上次处理到的工单 ID,避免重复
permissions: [ai-browser]一个数字员工的核心定义由这几个部分组成:
system_prompt是它的工作手册。用户用自然语言告诉它该做什么、怎么判断、什么情况找人。运行时这段文本被注入 LLM 的 system message,决定每次唤醒后的行为。subscriptions是它的上班时间。这里是 cron 表达式(每天 9 点);也可以是文件变动、Webhook 调用、关键字订阅等触发源。第六章展开调度引擎。memory_schema声明它需要跨次运行记住的字段。运行结束时由 Agent 自己写回,下次唤醒时自动加载。第七章展开记忆系统。permissions是它的能力清单。声明ai-browser,它就能调用浏览器相关的内置工具与已安装的 Skill。
一段自然语言加几行配置,就构成了一个会思考、有调度、有记忆、能操作企业系统的数字员工。代码里它对应 AutomationSpecSchema(src/main/apps/spec/schema.ts),第五章展开 App Spec 的完整定义。
边界在哪
判断和执行的分离不是一刀切的。实践中总有灰色地带。
比如"从告警页面找出最近 3 天的 P0 告警并按时间排序"。这件事可以完全写在 Skill 里——确定性地抓取、过滤、排序。也可以只让 Skill 抓取原始数据,排序和筛选交给 AI 处理。
但"帮我调研一下市面上关于 XX 的所有方案"就完全不同。搜什么关键词取决于对问题的初步理解,看哪几条结果取决于标题和摘要是否相关,看完之后要不要换个角度再搜取决于第一轮信息是否充分——每一步都是上一步结果的函数,整个执行路径在过程中才浮现。这种事没法写成 Skill,只能交给 AI 在 Agent Loop 里一步步推理。
我们的取舍倾向是:能确定性处理的尽量确定性处理。 AI 只接手那些真正需要语义理解的部分——"这条告警严重吗"、"要不要半夜叫醒用户"、"这条审批该不该批"。
原因很实际:每多一次 LLM 调用,多一份不确定性,多一笔 Token 开销。确定性代码零成本、行为可预测。判断环节省下来的 Token 预算,可以用在更复杂的推理上——比如让 AI 综合多个 Skill 的返回结果做跨系统的决策。
这个边界的设定不是技术问题,是产品判断。把几乎所有逻辑都扔给 AI,Token 消耗会很快失控,运行一次审批流花掉的钱比让人手动审批还贵;把 Skill 写得过度复杂、内部嵌满条件分支,结果跟 RPA 脚本一样脆弱。
正确的切分点是:Skill 处理"怎么做",AI 处理"做不做"和"做哪个"。这条线画在对的位置,整个系统的稳定性上限才能拉上去。
执行端被标准化成独立的 Skill,还带来一个延伸效果:可以安装、版本化、跨用户积累,像 npm 包一样在社区流通。下一节展开两层市场如何互相驱动。
1.3 两个市场
Skill 市场:给 AI 装能力
1.2 介绍了 Skill 的本质——Agent 的扩展能力包,以文件形式存放,运行时加载进上下文。单个 Skill 解决单个问题:拉一个系统的待办列表,提交一条审批,发一条通知。
当 Skill 可以独立于特定数字员工存在,它就具备了被共享的条件。一个工程师花几小时封装了公司 OA 系统的审批接口,发布成 Skill,团队里所有人的数字员工都能直接调用。下一个想自动化同一套 OA 的人,不需要重做这件事。
这和 npm 生态的逻辑一样:门槛低(一份 .md 描述加一段脚本),解决的是自己的问题,顺手发布出去,别人直接装。npm 2010 年上线时几百个包,2015 年超过 10 万个。增长的关键不是某家公司拼命写包,而是任何人都能贡献。
Agent 社区已经围绕 Skill 的共享建立了多个市场——OpenClaw 的 ClawHub、Smithery、Claude Code Skills Registry 等各自收录数千条目,各平台合计已有数万个可安装的 Skill,且仍在快速增长。方向很明确:Skill 应该像 npm 包一样,写一次,到处装。
但这一层解决的问题有明确边界:它给 AI 装能力,让 Agent 能做更多事。谁来决定做不做、什么时候做、做完告诉谁——这不是 Skill 市场管的事。
光有 Skill 还不够
传统 Agent 生态只有 Skill 这一层市场。用户想用 AI 自动化一件事,流程是这样的:找到合适的 Skill → 安装 → 写一段 system_prompt 告诉 Agent 工作规则 → 配置调度(什么时候跑)→ 调试参数直到行为符合预期。
这个过程本身是技术性的。你需要知道哪些 Skill 能组合出你要的效果,需要会写提示词,需要理解调度配置的含义。对工程师来说不算难,但对想用 AI 帮忙处理日常事务的运营、行政、财务人员来说,这和"自己写代码"没有本质区别——门槛换了个形式,还是在那里。
类比一下:npm 让你可以安装任何包,但要把十几个包组装成一个能跑的应用,你还是得会写代码。Skill 市场给 AI 装了能力,但把这些能力组装成一个能持续运行的自动化流程,仍然需要专业知识。
数字员工市场:直接雇一个人
Halo 的设计理念是:组装好的数字员工本身也是可分发的。
以"小红书发布助手"为例。这个数字员工底层依赖多个 Skill(登录小红书、发布帖子、监控评论、回复互动),有一段经过调试的 system_prompt 定义它的工作规则和判断逻辑,有预设的调度配置(每天几点运行),有 memory_schema 记录已处理的内容。所有这些已经组装完成、调试通过。
用户从市场安装这个数字员工,需要做的事只有一件:填几个配置参数(账号偏好、发布频率、内容方向)。不需要知道它依赖哪些 Skill,不需要自己写 system_prompt,不需要理解 cron 表达式。
这个体验的区别,就像去人才市场直接雇一个人,而不是自己去买工具、安装它、从零搭建一套流程。你雇的是一个已经具备相关技能的员工,告诉他你的具体要求,他就能开始工作。你不需要关心他是怎么学会这些技能的。
同一个数字员工模板,不同用户可以有不同的行为。用户 A 告诉审批助手"500 以下自动批",用户 B 告诉同一个助手"采购类一律通知我"——底层调用的 Skill 完全一样,行为由 system_prompt 的自然语言规则决定。
两层如何互相驱动
Skill 市场和数字员工市场不是各自独立的,它们之间有互相驱动的关系。
Skill 越多,数字员工能组合出的场景就越多。今天有人封装了"抓取产品价格"的 Skill,明天就有人基于它构建"竞品价格监控助手"数字员工。一个底层能力的增加,解锁的不是一个场景,而是一类场景。
反过来,当有人想做某个数字员工但发现缺一个特定 Skill——比如"从某个系统导出周报数据"——他去写一个,发布到市场,所有人都能用。数字员工的需求驱动 Skill 的供给。
两层分开,各自积累,互相放大。这和移动平台的模型类似:iOS 提供平台能力(GPS、摄像头、推送),开发者在上面构建 App,App 吸引用户,用户吸引更多开发者。区别在于 Skill 市场的"消费者"不是人,是 AI——给人装能力的是 App Store,给 AI 装能力的是 Skill 市场。
单层做不出这个效果。如果只有数字员工市场、没有 Skill 市场,每个数字员工都得自带所有操作逻辑,无法复用,无法积累。如果只有 Skill 市场、没有数字员工市场,Skill 只是散落的零件,非技术用户无法使用——门槛还是在那里。
这个框架下读后面的章节
后面的章节可以对照这两层来定位:
- 第五章讲 App Spec——"怎么定义一个数字员工"和"怎么定义一个 Skill"的产品契约
- 第六章讲调度引擎——数字员工什么时候醒来做判断
- 第八章讲浏览器工具——Skill 在什么环境里执行
- 第十二章讲 Store 系统——两层的东西怎么分发、搜索、安装
知道你读的技术处于哪一层、服务哪一层,就不会迷路。
1.4 系统全景:五层、30 万行、一个人
前三节讲了"是什么"和"为什么"。这一节讲"长什么样"。
我第一次把 Halo 的模块依赖画出来,是 2025 年 10 月一个周末。那时候代码大概 5 万行,画完发现已经不小了——五个明确的层次,依赖方向自上而下,没有循环。半年后的今天,代码膨胀到了 30 万行 TypeScript,模块从十几个变成数百个,但那张图的骨架没变过。
一个人怎么管理 30 万行代码?答案不在某种巧妙的设计,而在架构分层本身的强约束。边界一旦模糊,AI 几天内就能堆出无法回头的依赖混乱——速度快到人工 review 根本追不上。AI 不怕重复,不怕冗余,它只怕边界模糊。你告诉它"这个模块只能调用下一层",它就严格遵守。你没告诉它,它就到处乱引用,最后改一行崩十处。
给 AI 设计合理的模块层次和依赖方向,是这套纪律能落地的前提。AI 写出 bad 代码是常态——一个文件里的逻辑绕弯、一个模块内的重复实现,这些事会持续发生,没法靠 review 全部拦下。但只要模块边界和依赖方向是清晰的,损坏就被限制在局部。就像一栋设计稳固的房子,一角被砸坏不会让整栋大厦倾倒,受力被各层楼板和承重墙分摊到了局部模块里。层次本身就是风险隔离层。
任何经历过大型项目的工程师都见过同一个规律:几十人的团队维护的百万行代码之所以能迭代不崩,靠的不是个人技术,是架构分层的纪律。现在换成一个人加 AI,纪律更重要——因为 AI 的产出速度比人快十倍,犯错传播的速度也快十倍。
五层
从上到下:
用户交互层(UI)。 React 18 + TailwindCSS,跑在 Electron 主窗口或远程浏览器里。两种宿主共享同一份 UI 代码——一层 Adapter 决定调用走 IPC 还是 HTTP,调用者无感。第二章展开。
数字员工编排层(Apps)。 三个模块:spec 定义"一个数字员工长什么样",manager 管它的生命周期,runtime 触发它运行并记录 Activity。这一层回答"谁在什么时候做什么事"。第五、六章。
服务层(Services)。 Agent 会话、AI 浏览器、多供应商接入、远程 HTTP/WS、健康监测、通知通道——具体能力的实现都在这里。Apps 不直接持有这些能力,通过 Services 调用。第八到第十一章。
平台基础设施层(Platform)。 store、scheduler、event-bus、memory、background。这一层不含业务语义——它不知道"数字员工"是什么,只提供存储、调度、事件、记忆、保活这些通用原语。类比 VS Code 的 platform/:任何上层模块都可以用,它不依赖任何上层。第六、七章。
AI 引擎层。 Halo SDK 提供 Agent Loop,OpenAI Compat Router 把 OpenAI 协议翻译成 Anthropic 协议。第三、四章。
依赖方向只能向下:UI → Apps → Services → 底座。底座有两块互不依赖:Platform 提供存储、调度、事件这些通用原语;AI 引擎提供 Agent Loop 运行时。Apps 既会经 Services 间接用到 AI 引擎,也会直接从 Platform 拿 store、scheduler 这些原语。但数据流不是单向的——调用向下,事件向上。Scheduler(Platform 层)到了 cron 时间,通过 Runtime(Apps 层)启动一次 Run;Run 里 Agent Loop(AI 引擎层)调用浏览器工具(Services 层),拿到结果后写入 Activity Thread(Apps 层),前端(UI 层)通过事件订阅实时渲染。向下是请求,向上是推送。理解这条双向管道,后面十二章的每个模块都能找到它在管道里的位置。没有例外。AI 生成代码的时候,就被这类 skill 规则约束。VS Code 的源码也是类似的分层——workbench → platform → base,单向依赖,几百个 contributor 靠这个纪律共存。Halo 只有一个 contributor(100% AI),人只是架构指导,纪律反而更严,因为没人帮你 review,破坏了结构你自己也不一定能发现。
一条消息怎么流
用户在界面输入"帮我查一下今天的待办",按下发送。
渲染层调 api.sendMessage()。Adapter 判断环境——桌面端走 IPC(window.halo.sendMessage),远程端走 HTTP POST。
主进程收到请求,IPC handler(src/main/ipc/agent.ts)转发给 Agent Service。Service 创建或恢复一个 Session,把消息交给 Halo SDK 的 Agent Loop。
Agent Loop 开始单循环:调用 LLM → 拿到 tool calls → 执行工具 → 把结果塞回上下文 → 再调 LLM → 直到模型说"完成"。每一步都通过事件推送实时同步给前端(IPC 的 webContents.send 或 WebSocket 的 broadcast),UI 用 Zustand store 接收并渲染。
数字员工的路径一模一样——只是入口不是用户手动发送,而是 Scheduler 到了 cron 时间、或 Event Bus 收到了外部事件。从 Agent Loop 往下,对话和自动化共享同一条管道。
工程上这条共享落在一个高度内聚的 services/agent 模块里。模块内封装三个底层引擎——Claude Code SDK、Halo SDK、Codex SDK——经 resolved-sdk 把各自原生事件归一化成同一份会话/流协议。对外只有 createSession、inject-message、send-message 几个入口:UI 的对话、Apps 的定时 Run、IM 进来的外部消息,调到的是同一个服务,底下跑哪个引擎由配置决定,新增引擎不改主流程,只多一个 adapter。
本地优先
所有数据都在本地。
~/.halo/
├── config.json # API Key、主题、远程访问配置
├── halo.db # SQLite:数字员工、调度、Activity
├── spaces/<name>/ # 工作空间(文件 + 对话记录)
└── apps/<appId>/ # 数字员工私有目录(memory.md)没有云数据库,没有账号系统,断网照常运行。这是一个有意识的产品决策,不是偷懒。我在那篇博客里写过原因:"AI 运行时应该是人人都能获得并构建的权利。"如果你的数据存在别人的服务器上,你的 AI 就受制于别人的定价和规则。
代价显而易见:多设备同步需要用户自己处理。我选的解法是远程访问——通过 Cloudflare Tunnel 把你家里的 Halo 暴露到公网,从手机或任何浏览器连进来。数据始终在一台机器上,访问可以在任何地方。第十一章展开。
同一份 UI,两种宿主
五层架构里,UI 层不绑定宿主。同一份 React 代码既能跑在 Electron 主窗口里,也能跑在远程任何浏览器里——区别只在于调用主进程时走的是 IPC 还是 HTTP/WS。Adapter 层(第二章展开)把这个差异封装掉,UI 自身从不关心自己跑在哪个宿主里。
这和 VS Code Remote 是同一个思路:编辑界面可以在本地,也可以在浏览器,但 Language Server、文件系统、终端始终在那台真正干活的机器上。Halo 把这个模型从"远程开发"扩展到"远程 AI 执行"——Agent Loop、浏览器自动化、文件操作、登录态都在你那台 Electron 主机上跑,远程端只是一层渲染。
解耦带来一个直接结果:手机浏览器里看到的 Halo 和桌面上的 Halo 完全一样。不是做了一个移动版,是同一份前端、不同的传输层。新加一个功能,桌面和远程同步可用,不需要为远程端再实现一遍。
这张图和后面的章节
后面每一章对应这张图的一个或多个模块:
- 第二章(地基)= 三端统一的应用骨架:渲染层共享、IPC 安全桥、启动序列、SQLite 命名空间
- 第三章(大脑)= AI 引擎层的 Halo SDK
- 第四章(兼容层)= AI 引擎层的 OpenAI Compat Router
- 第五章(灵魂)= Apps 层的 App Spec 与生命周期
- 第六章(心脏)= 平台层的调度与事件路由 + Apps 层的升级与并发控制
- 第七章(记忆)= 平台层的 memory
- 第八章(双手)= 服务层的 ai-browser
- 第九章(嘴巴)= 服务层的通信体系(含 DHP 对等协议)
- 第十章(面孔)= 用户交互层的 Content Canvas 与流式渲染
- 第十一章(盔甲)= 服务层的 health + remote + mock-bash
- 第十二章(市场)= 平台层的 Store 系统(镜像、安装、Skill 市场)
知道你读的代码属于哪一层,就知道它能调用谁、不能调用谁。这个约束是整个系统 30 万行代码能保持可维护的根本原因。
1.5 零适配接入企业内网:为什么不需要 API 对接
任何成规模的企业里都有几十套系统——OA、工单、知识库、发布平台、监控面板。每套系统都是 Web 页面,每套都有自己的接口,没有哪两套是一样的。
当时如果想自动化任何一个流程,标准做法是找那个系统的后端团队开 API。排期,等。一个月能排上就算快的。企业里有几十套系统,你要做几十次对接。MCP 协议火了之后,换个说法——"给每套系统写个 MCP Server"——本质没变,还是要一个一个对接,写一个一个的适配器。
Halo 数字员工的设定就是基于这类问题产生的:员工每天打开浏览器,登录,操作。浏览器里存着 cookie,系统认的是 cookie,不是人。那如果 AI 也在这个浏览器里操作呢?
cookie 在那里,网络从你自己的电脑发出去,内网系统看到的和员工自己操作完全一样。不需要开 API,不需要写 MCP Server,不需要 IT 部门配合。
你能用浏览器登录的系统,AI 就能操作。
两个浏览器,一个可见一个隐藏
Halo 是 Electron 应用,内置 Chromium。我在架构上做了两个浏览器 context。
Content Canvas 浏览器是用户看得到的那个,嵌在对话界面右侧。用户在这里登录 OA、打开工单系统,cookie 自然留在 session 里。对话场景下 AI 直接在这个浏览器里操作,用户能看到 AI 在做什么。
Daemon 浏览器是隐藏的。daemon-browser.ts 管理着一个后台 BrowserWindow——不弹窗、不打扰,但 Chromium 进程是活的。数字员工在后台 7×24 小时运行时,就是通过这个隐藏浏览器发请求、抓页面、执行 Skill。FIFO 队列管并发,5 分钟超时防死锁。
// src/main/platform/background/partition.ts
// 按域名隔离 session,不同网站的登录态互不干扰
export function extractPartition(url: string): string {
const hostname = new URL(url).hostname
const mainDomain = extractMainDomain(hostname)
return `persist:automation-${mainDomain}`
}persist:automation-jd.com、persist:automation-192.168.1.1——每个域名一个独立 partition。我没有让所有内网系统共享同一个 cookie jar,因为那样会在 session 冲突时出诡异 bug。做过大型 Web 应用的人都知道,cookie 相关的 bug 是最难调的那种。
用户登录一次,之后数字员工就能以同样的身份操作那个系统。权限边界天然清晰:AI 用的是你的账号,做的是你有权限做的事。不可能越权,因为 cookie 代表的就是你自己。
一个 browser_skill 怎么用这个登录态
回到 1.2 那个审批的例子。内网 OA 系统的前端页面打开 F12,你能看到它调的接口——/api/oa/approvals/pending,带着 cookie 发请求,返回 JSON。
browser_skill 做的事就是复用这个接口:
// Skill 在 Halo 内置浏览器中执行
async (params) => {
const resp = await fetch('/api/oa/approvals/pending',
{ credentials: 'include' })
return (await resp.json()).items
}这段代码通过 browser_run 注入到当前页面的 JS 上下文里执行——和你在 F12 Console 里粘进去按回车没有区别。脚本和页面共用同一个 origin 与 session,cookie 自然就在那里。内网系统不知道这个请求来自 AI——它看到的是一个正常的、有权限的、从员工电脑发出的 HTTP 请求。
不需要 IT 配合。不需要系统改造。
反例比比皆是:某个自动化项目花了三个月对接公司内部 5 套系统的 API,接口文档不全,联调来回扯皮,上线后接口变了又改。如果当时有这个思路——打开浏览器能用的接口,AI 直接用——三个月变三天。
Stealth:让 Electron 看起来像真实 Chrome
Electron 有一个问题:它不是真正的 Chrome。带反爬虫机制的站点会检测浏览器指纹:navigator.webdriver 被设成 true、window.chrome 对象缺字段、WebGL 渲染器字符串不对——这些细节暴露了自动化工具的身份。公网站点常见,内网系统里风控、监控类后台也会装。
参考 puppeteer-extra-plugin-stealth 的思路,src/main/services/stealth/evasions/ 下沉淀了 14 个反检测模块。navigator.webdriver.ts 把 webdriver 属性改回 undefined;chrome.app.ts 补上真实 Chrome 的 window.chrome.app 对象;navigator.plugins.ts 伪造插件列表——真实 Chrome 默认有 PDF Viewer 等插件,Electron 没有。
注入方式是 CDP 的 Page.addScriptToEvaluateOnNewDocument——脚本在页面任何 JavaScript 执行之前跑。反检测代码如果在页面脚本之后才注入,就已经晚了,指纹已经被采集。CDP 注入失败时,降级到 did-start-navigation 事件 + dom-ready 时执行,兼容性优先。
// src/main/services/stealth/index.ts
// 预构建脚本,复用,不每次重建
let cachedStealthScript: string | null = null
export function getStealthScript(): string {
if (!cachedStealthScript) {
cachedStealthScript = buildStealthScript()
}
return cachedStealthScript
}14 个模块、一段缓存的脚本、两条注入路径。这不是什么独创——思路来自 puppeteer 社区。但把它集成到 Electron 应用的内置浏览器里,让它对最终用户完全透明,这个工程集成是 Halo 做的。
无障碍树:AI 不需要"看"页面
登录态有了,反检测有了,还有一个问题:AI 怎么知道页面上有什么?
截图是最直觉的方案。OpenAI 的 Operator 走的就是这条路——截图让视觉模型分析。Halo 同样保留了截图工具(browser_screenshot),但没把它作为默认通路。一张截图几千 Token,一次操作截十几张图,成本会迅速积累;视觉定位也存在天然偏差——"第三行第二个按钮"这种描述,模型理解与实际坐标之间总有误差,影响点击的可靠性。
默认通路走 Accessibility Tree,截图留给视觉模型确实更合适的场景——验证码、Canvas 渲染的图表、纯图像内容。
每个现代浏览器都维护一棵无障碍树——本来是给屏幕阅读器用的,描述页面上每个可交互元素的角色、名称、状态。CDP 的 Accessibility.getFullAXTree 一次调用拿到整棵树,Halo 的 snapshot.ts 给每个节点分配一个 UID(格式 snap_1_42),序列化成紧凑文本。
AI 看到的不是像素,是结构:
button "审批" [snap_1_42]
textbox "备注" [snap_1_43]
link "查看详情" [snap_1_44]要点击哪个,AI 直接说 browser_click({ uid: "snap_1_42" })。底层通过 UID 找到 DOM 节点,用 CDP 的 Input.dispatchMouseEvent 执行点击。
结构化文本比像素省 Token,精确度也更高,因为不存在坐标偏差。对页面改版的容忍度也更好——按钮的 DOM 位置变了,只要文字没变,无障碍树里还能找到它。CSS 选择器会因为 class 名重构失效,无障碍树不会。
AT 在 Halo 里承担两件事:探索阶段让 AI 边看边操作,配合 browser_inspect 抓到接口请求,沉淀成 Skill;以及目标系统还没人写过 Skill 时直接兜底完成流程。日常跑业务的主路径是 Skill——AT 不参与,AI 也不看页面。
第八章会展开 Accessibility Tree 的完整实现——20 个交互角色、23 个结构角色的双白名单过滤,单子节点提升减少层级,idToNode Map 保证 O(1) 查找。这里只需要记住一点:Skill 优先,AT 用于探索与兜底,截图留给视觉问题。
边界
"零适配"有明确的边界条件。
要先登录一次。用户在 Halo 内置浏览器里手动登录目标系统,cookie 才会存下来。session 过期了,需要重新登。这和你用真浏览器一样——没有什么系统能永远保持登录态。
Skill 调的是前端页面用的接口,没有版本保证。内网系统升级改了接口,Skill 跟着要改。但接口变更的频率远低于 UI 变更——RPA 脚本因为按钮位置挪了就崩,Skill 只在接口路径或参数格式变了才需要动。
权限等于用户权限。AI 不能做你做不了的事。这既是限制,也是安全保障。
从三个月到三分钟
传统企业内网对接的痛苦人尽皆知。每接一个内网系统,就要拉后端同学开会、对文档、联调、上灰度……一套流程下来几周算快的。
现在呢?找到内网系统的登录页面,在 Halo 内置浏览器里登录一次,写一个 Skill 调那个系统的前端接口。从"能不能做"变成"要不要做"。门槛从"需要 IT 部门配合"降到"会用浏览器就行"。
这也解释了 1.4 里我为什么选 Electron。不是因为它轻量——它很臃肿。但它给了一个完整的 Chromium 实例,这个 Chromium 实例就是打开企业内网的那扇门。Web 应用做不到本地浏览器的 cookie 隔离,原生 GUI 框架做不到内嵌 Chromium。
后面第八章和第十一章会展开更多细节——14 个浏览器工具的完整实现、远程访问如何让你在手机上控制家里电脑的 Halo。但零适配接入这件事的核心就一句话:你的浏览器能登录什么,AI 就能操作什么。
本章回顾
本章五节画了一张地图。
三代方案的演进给出了第一个结论:AI 擅长判断,不擅长可靠地重复执行。RPA 完全排除了 AI 的判断能力,纯 AI Agent 又把执行的可靠性也交给了 AI。这个判断不是画架构图得出来的,是早期那一轮纯 AI Agent 原型在延迟、不稳定、Token 失控这三件事上反复栽跟头,最后逼出来的。
把"做不做"和"怎么做"拆成两个独立决策,整个系统的可靠性、成本、可复用性同时解锁。Skill 做执行,数字员工做判断,两层通过文件系统解耦——不需要新协议,也不需要新运行时。
Skill 市场给 AI 装能力,数字员工市场把这些能力组织成可调度的工作单元。两层互相驱动,缺一不可:只有 Skill 而没有数字员工,非技术用户用不起来;只有数字员工而没有 Skill,能力无法跨场景积累。
UI、Apps、Services、Platform、AI 引擎,依赖只能向下。30 万行代码一个人加 AI 维护得住,靠的不是个人能力,是这条规则被严格执行——边界一旦模糊,AI 生成代码的速度会反过来压垮自己。
零适配接入是这套架构最直接的落地姿势。复用用户浏览器的登录态,反检测让 Electron 在内网系统面前等同真实 Chrome,无障碍树让 AI 用极小的 Token 成本理解页面。企业里几十套内网系统,不需要一个一个对接——你能登录的,AI 就能操作。
这五件事构成后面所有技术章节的坐标系。读每一章时,可以对照定位:这个模块属于哪一层,处理的是判断还是执行,让能力还是让调度更进一步。
下一章拆开工程地基。一套前端代码同时跑在桌面端、手机端和浏览器——我在医院病床上掏出手机连回家里那台 Mac,看到的界面和桌面上一模一样。这个"一模一样"背后,是把传输层从渲染层彻底剥离的一层薄薄的 Adapter。