浏览器端智能体操作系统:本地优先架构与技能系统深度解析
1. 项目概述一个在浏览器中运行的持久化智能体操作系统如果你和我一样对当前大多数AI应用感到厌倦——它们要么是功能单一的聊天机器人要么是依赖云端、缺乏连续性的工具——那么“OS Loop AI”这个项目可能会让你眼前一亮。这不是又一个套着聊天界面的GPT包装器而是一个真正意义上的智能体操作系统。它运行在你的浏览器里拥有独立的身份、长期记忆、自主技能并且能在多个会话中与你保持连续的关系。最核心的是它的运行时完全在本地执行远程服务如同步、市场、账户是可选的附加功能而非强制依赖。这意味着你的数据、你的对话、你的智能体其主权完全归属于你。这个项目的技术栈相当现代且务实Next.js 15作为全栈框架TypeScript确保类型安全状态管理用Zustand本地数据库是IndexedDB通过Dexie操作向量搜索和嵌入模型Xenova/all-MiniLM-L6-v2跑在Web Worker里加密用Web Crypto API而LLM则支持OpenAI、Gemini和Claude。整个架构设计清晰地划分了层次从UI到状态管理再到领域逻辑和持久化存储每一层都职责分明。对于任何想要深入理解如何构建一个复杂、状态丰富、且能在浏览器端独立运行的AI应用的开发者来说这个项目都是一个绝佳的学习范本。它不仅展示了技术可能性更体现了一种“本地优先、用户主权”的设计哲学。2. 核心架构与设计哲学拆解2.1 何为“智能体操作系统”传统AI应用尤其是基于聊天的其状态往往是短暂且会话隔离的。你关闭标签页对话历史就消失了你换个设备一切从头开始。智能体Agent的概念则要求连续性、记忆性和主动性。OS Loop AI试图在浏览器这个看似“轻量”的环境中构建一个满足这些要求的完整运行时环境。2.1.1 持久化身份与记忆系统的基石是八种智能体文档它们定义了智能体的核心从公开资料、身份定义、灵魂原则到用户模型、策略集和运行时设置。这些文档在首次启动时被创建并持久化到IndexedDB中构成了智能体不可变的“人格”基础。更重要的是长期记忆系统它不仅仅是聊天记录的堆砌。系统会将对话内容通过本地嵌入模型转化为向量存入memoryEntries表。当智能体需要上下文时它能基于语义相似度进行检索从而实现跨越会话的“记忆”能力。这种设计让智能体更像一个持续成长的伙伴而非每次重启都失忆的机器。2.1.2 技能系统与工具执行这是智能体“能动性”的关键。技能Skill在这里被定义为可执行的、模块化的能力单元。与简单的函数调用不同技能拥有完整的生命周期从生成通过LLM根据描述创建TypeScript模块、安装、配置、到执行和更新。技能可以请求用户批准、访问网络资源需遵循声明的网络规则、甚至集成OAuth流程。系统内置了一个技能市场的抽象允许从配置的代码仓库如官方的os-loop-skills仓库发现和安装技能。所有已安装的技能在运行时通过一个沙盒化的executeSkillModule引擎来执行确保了安全隔离。2.1.3 本地优先与可选同步整个架构的巧妙之处在于其“本地优先”原则。所有核心数据——对话、记忆、技能、配置——都默认存储在浏览器的IndexedDB中。远程服务如Supabase后端被设计为可选的“插件”。用户可以选择启用同步将数据备份到自己的云端或在不同设备间同步。但即使没有网络核心功能也完全可用。这种设计将数据控制权交还给用户也避免了因服务中断导致应用瘫痪的风险。加密保险库Vault进一步强化了这一点用户的LLM API密钥等敏感信息使用Web Crypto API在本地加密存储永远不会发送到项目方的服务器。2.2 状态管理与运行时事件流对于一个复杂的交互式应用状态管理是命脉。OS Loop AI采用Zustand作为中心化状态库但它的设计远不止于此。2.2.1 基于事件的响应式UI智能体的运行过程是异步且多步骤的思考、调用工具、执行技能、等待批准、流式输出回复。为了清晰地呈现这些状态项目定义了一套包含92种类型的运行时事件系统定义在src/types/events.ts。当运行时引擎执行时它会通过一个内部事件总线RuntimeEventBus发射这些结构化事件。前端的useRuntimeEvents钩子会订阅这些事件并根据当前活跃的会话和运行ID进行过滤和转换最终驱动UI的更新。例如thinking.started事件会让UI显示一个脉动的蓝点tool.started和tool.completed事件会在活动面板中显示工具执行的详情而assistant.message.delta事件则负责将LLM流式输出的文本实时拼接到聊天界面。这种事件驱动的架构将复杂的运行时状态与UI渲染解耦使得前端可以灵活、精确地响应后端智能体的每一个动作。2.2.2 聊天流程的状态闭环用户发送一条消息这个动作会触发一系列状态变更和异步操作sendMessage动作被调用它首先清理掉上一次运行的UI残留activeEvents,pendingAssistantText然后将runStatus设为running。接着它调用ChatRuntimeRunner.run()将用户消息和会话ID传递给真正的本地运行时引擎。运行时引擎开始工作它可能先进行思考thinking事件然后决定调用一个工具tool事件或技能skill事件在这个过程中可能会请求用户批准approval事件。最终LLM生成回复通过generateResponseStream产生流式文本assistant.message.delta事件被不断触发前端实时渲染。运行结束时run.completed事件触发UI状态重置并从数据库重新加载完整的对话历史以确保一致性。这个闭环确保了即使运行时在后台进行复杂的多步推理和操作前端的交互始终是响应且状态一致的。3. 核心模块深度解析与实操要点3.1 数据库层IndexedDB与Dexie的工程化实践在浏览器中构建一个拥有40张表的复杂应用数据库IndexedDB是唯一的选择。但直接操作IndexedDB的API是繁琐且容易出错的。OS Loop AI选择了Dexie.js这个封装库并在此基础上构建了一套清晰的仓储模式。3.1.1 模式定义与迁移项目的数据库模式定义在src/lib/db/schema.ts中使用Dexie的声明式API。版本管理通过MIGRATIONS对象实现这是处理数据模型演化的关键。// 示例数据库版本与迁移 export const MIGRATIONS: Recordnumber, (db: Dexie) Promisevoid { 10: async (db: Dexie) { // 迁移10完整的仓库模型转型 await db.transaction(rw, db.repositorySources, db.skillPackages, async () { // 1. 更新 repositorySources 表结构 // 2. 迁移 installedSkillRecords将 marketplaceId 转换为 skillSlug // 3. 清理 skillPackages 中废弃的字段 // ... 具体的数据转换逻辑 }); }, 11: async (db: Dexie) { // 迁移11为现有技能草案回填 forkedFrom 字段 await db.skillPackages.where(lifecycleState).equals(draft).modify({ forkedFrom: null }); }, };每次应用启动Dexie会检查当前数据库版本与代码中定义的版本。如果版本较低它会按顺序执行缺失的迁移。这种机制使得数据结构的升级变得可控且可回溯。实操心得在设计IndexedDB迁移时务必在事务内完成所有相关表的操作以保证原子性。同时迁移脚本应该具备幂等性即多次执行不会产生错误或重复数据。对于复杂的迁移可以先在开发环境用备份数据充分测试。3.1.2 仓储模式与类型安全为了避免业务代码直接耦合Dexie项目抽象了一层仓储接口。例如对于智能体文档的操作// 仓储接口定义 export interface IAgentDocumentRepository { getById(id: UUIDv7): PromiseAgentDocument | null; getByDocumentType(type: AgentDocumentType): PromiseAgentDocument[]; create(doc: OmitAgentDocument, id | createdAt | updatedAt): PromiseAgentDocument; update(id: UUIDv7, changes: PartialOmitAgentDocument, id | documentType): PromiseAgentDocument; // ... 其他方法 } // Dexie实现 export class DexieAgentDocumentRepository implements IAgentDocumentRepository { constructor(private table: Dexie.TableAgentDocument, string) {} async getByDocumentType(type: AgentDocumentType): PromiseAgentDocument[] { // 使用Dexie查询但返回的是领域类型 return this.table.where(documentType).equals(type).toArray(); } // ... 其他方法的实现 }业务层通过接口来使用仓储这使得底层数据库实现现在是Dexie未来可能是其他可以替换也极大地提升了代码的可测试性。所有数据类型都从src/types/集中导入确保了整个应用类型定义的一致性避免了“类型漂移”。3.2 技能系统从生成到执行的完整生命周期技能系统是OS Loop AI最复杂也最强大的部分之一。它允许用户或智能体自身通过自然语言描述来创造新的功能模块。3.2.1 技能生成与OAuth智能解析当用户要求“创建一个能读取我Google日历的技能”时skill_generate_local工具会被调用。其内部流程堪称精妙LLM提示工程skill-generation-service会构建一个结构化的提示词其中包含清晰的技能元数据名称、描述、权限和TypeScript模块代码的示例。为了避免JSON转义问题它使用一种分隔符格式---SKILL_META---/---SKILL_MODULE---来引导LLM输出。多格式解析解析器会首先尝试分隔符格式如果失败则回退到直接解析JSON、Markdown代码块甚至大括号匹配。这种鲁棒性设计很重要因为不同LLM的输出格式可能不稳定。OAuth提供者解析这是亮点。如果LLM在技能清单中指定了OAuth授权但只提供了一个providerId如google系统不会报错。oauth-provider-resolution-service内置了一个已知提供者注册表覆盖了Google、GitHub、Microsoft等15个常见服务。它会自动补全authorizationUrl、tokenUrl、scopes等详细信息。对于未知的提供者它会返回一个needs_user_input响应用人类可读的问题如“请提供该服务的授权端点URL”来引导用户补充信息而不是直接失败。验证与持久化解析后的输出会经过严格的类型验证。验证通过后技能会作为“草案”被持久化到数据库并通过skill-lifecycle-service自动激活立即出现在技能页面和运行时注册表中。3.2.2 技能执行与沙箱环境生成的技能本质是一段TypeScript代码。为了安全地执行它项目实现了一个沙箱化的executeSkillModule引擎。能力宿主技能在执行时会被注入一个host对象这个对象提供了有限的、受控的API比如访问网络需遵循声明的networkRules、调用LLM、读写技能专属的工作空间状态等。技能无法直接访问DOM、localStorage或其他浏览器敏感API。超时控制每个技能执行都有超时限制防止恶意或 buggy 的技能无限运行。事件发射技能执行过程中的开始、进度、完成或失败都会通过运行时事件总线发出相应事件从而在UI的活动面板中可视化。3.2.3 技能工作空间与状态持久化技能可以拥有工作空间这是一个与特定技能实例关联的、持久化的状态存储。想象一个“文件管理器”技能它需要记住用户最后浏览的目录。这个状态就可以保存在工作空间中并且通过skillWorkspaceArtifacts表存储结构化的产出物。工作空间的状态是隔离的不同技能实例、不同用户会话之间的数据不会混淆。3.3 运行时引擎与工具调用模型智能体的“大脑”是运行时引擎它负责协调LLM、工具、技能和记忆完成用户的任务。3.3.1 规划与执行循环运行时引擎的核心是一个循环规划根据当前对话历史、记忆、可用工具和技能LLM规划器决定下一步该做什么。这被封装在一个PlannerContext对象中。执行执行规划出的动作可能是调用一个工具如web_search执行一个技能或者直接生成文本回复。观察将执行结果作为新的上下文信息再次送入规划步骤。循环重复此过程直到任务完成或达到最大迭代次数默认50次可配置。这个过程在src/lib/runtime/中实现它处理了工具调用的参数绑定可以从加密保险库中获取API密钥、技能的分发执行、以及运行时状态的持久化agentRuns和agentRunSteps表记录了每一次运行的详细步骤用于回放和调试。3.3.2 提供者抽象与本地回退项目支持多个LLM提供商OpenAI, Gemini, Claude。运行时在每次执行开始时会根据智能体设置和加密保险库中的密钥来解析使用哪个提供商。如果未配置或保险库被锁定它会优雅地回退到一个本地回显提供者该提供者只会重复用户的输入。这种设计确保了应用在缺少关键配置时仍能基本运行而不是直接崩溃。4. 前端工程与状态同步实践4.1 基于Next.js App Router的应用结构项目采用Next.js 15的App Router这是一种基于文件系统的路由方式。src/app/目录下的每个文件夹代表一个路由段。src/app/ ├── layout.tsx # 根布局包含侧边栏和主内容区 ├── page.tsx # 首页 (/)即聊天视图 ├── skills/ │ ├── page.tsx # 技能列表页 │ └── [slug]/ │ └── page.tsx # 特定技能详情页 ├── settings/ │ └── page.tsx # 设置页包含10个标签页 ├── memory/ │ └── page.tsx # 记忆浏览页 ├── vault/ │ └── page.tsx # 保险库管理页 └── ... # 其他路由4.1.1 布局与侧边栏AppLayout组件包裹了所有路由提供了一个固定的两栏布局左侧是260px宽的侧边栏右侧是灵活的主内容区。侧边栏在所有页面都可见包含了导航的核心入口新建聊天、会话列表、收件箱、技能、工作空间、审批、记忆、保险库、历史记录、调度器和设置。这种设计提供了全局的上下文和快速导航能力。4.1.2 流式渲染与React Server Components由于大量状态聊天消息、技能列表等存储在客户端的IndexedDB中主要的页面交互逻辑都在客户端组件中。但是Next.js的App Router允许在服务端渲染静态部分如布局框架并结合React的流式渲染特性可以提供更快的初始页面加载体验。项目在需要与服务端交互的地方如可选的同步API路由使用了Next.js的API Routes。4.2 Zustand状态管理领域存储与UI状态分离项目没有使用一个庞大的全局Store而是根据领域模型划分了多个独立的Zustand StorechatStore,skillStore,settingsStore,memoryStore,vaultStore等。4.2.1 存储结构与数据流以chatStore为例它管理着所有与会话相关的UI状态sessions: 从IndexedDB加载的活跃会话列表。activeSessionId: 当前选中的会话ID。messages: 当前活跃会话的消息列表。pendingAssistantText: 流式响应过程中累积的文本。runStatus: 当前运行状态空闲、运行中、完成、失败。activeEvents: 当前运行时活动的结构化事件列表用于驱动活动面板。一个关键原则是组件永远不直接调用仓储或数据库。所有数据操作都通过Store的Action来发起这些Action内部会调用对应的领域服务如session-service,message-service。这保证了数据变更的逻辑集中并且能自动触发UI更新。4.2.2 运行时事件与UI的绑定useRuntimeEvents这个自定义Hook是连接运行时引擎和UI状态的桥梁。它订阅了全局的RuntimeEventBus。当事件发生时Hook会检查事件的runId和sessionId是否与当前Store中活跃的运行和会话匹配。只有匹配的事件才会被处理并转换为Store的更新。例如一个tool.started事件会被转换为向activeEvents数组添加一个代表工具开始的活动卡片。而assistant.message.delta事件则会被用于追加pendingAssistantText实现打字机效果。这种设计使得多会话、多任务并行成为可能——每个会话只关心自己的事件流。4.3 技能市场与仓库源的可扩展设计技能市场不是一个中心化的服务器而是一个基于Git仓库的去中心化发现机制。4.3.1 仓库源配置repositorySources表存储了所有配置的仓库源。默认会种子一个官方源https://github.com/CristianBB/os-loop-skills。用户可以添加任意公开的GitHub仓库URL作为自定义源。每个源都有trustMode信任模式如official、manual_review和priority优先级属性。4.3.2 技能发现与安装当用户在技能市场浏览或搜索时系统会遍历所有已启用的仓库源获取其skills.json清单文件并合并结果。安装一个技能本质上是将远程仓库中该技能目录下的代码skill.json清单和index.ts主模块克隆到本地的skillPackages表中并创建一个installedSkillRecord记录关联到其源仓库ID和技能Slug。4.3.3 更新与信任系统可以检查已安装技能是否有更新。信任模式决定了更新的行为官方源技能可以自动更新而手动审查源的技能可能需要用户确认。这种设计在开放性和安全性之间取得了平衡。5. 开发、测试与部署指南5.1 本地开发环境搭建开始贡献代码或单纯想体验这个项目第一步是搭建本地环境。# 1. 克隆仓库 git clone https://github.com/CristianBB/os-loop-ai.git cd os-loop-ai # 2. 安装依赖 (项目使用 pnpm但 npm 也可用) npm install # 3. 配置环境变量 cp .env.example .env.local # 编辑 .env.local填入必要的变量如NEXTAUTH_SECRET如果启用同步功能 # 4. 启动开发服务器 npm run dev打开浏览器访问http://localhost:3000。首次访问时系统会自动在IndexedDB中创建数据库并种子默认的智能体文档你会看到一个拥有基本人格的智能体界面。注意事项.env.local中的变量大多以NEXT_PUBLIC_开头这意味着它们会被打包到客户端代码中。因此绝对不要将真正的LLM API密钥放在这里。LLM密钥应该通过应用内的加密保险库功能进行设置它们会被安全地存储在本地。5.2 测试策略单元、集成与端到端项目配备了完善的测试套件体现了对质量的重视。单元/集成测试 (Vitest React Testing Library MSW)运行npm run test。这些测试专注于独立的函数、组件和模块。MSW用于模拟网络请求使得测试不依赖外部API。测试覆盖度运行npm run test:coverage可以生成覆盖度报告帮助识别未测试的代码路径。端到端测试 (Playwright)运行npm run test:e2e。E2E测试模拟真实用户操作例如导航、发送消息、安装技能等。e2e/helpers/目录下提供了工具函数用于在测试中初始化IndexedDB数据或模拟服务器发送事件这使得测试更稳定、可重复。类型检查与代码质量npm run type-check进行TypeScript严格类型检查npm run lint和npm run format:check用于保证代码风格一致。5.2.1 编写可测试的代码由于核心业务逻辑与UI、数据库解耦良好编写测试相对直接。例如测试一个技能生成服务import { describe, it, expect, vi } from vitest; import { generateSkillFromDescription } from /lib/skills/skill-generation-service; import { mockLLMProvider } from ../mocks/providers; describe(skill generation service, () { it(should parse a valid skill description with OAuth, async () { // 1. 模拟LLM返回一个结构化的技能描述 const mockResponse ---SKILL_META--- {name: Fetch Calendar, description: ..., oauth: {providerId: google}} ---SKILL_MODULE--- // TypeScript code here ---END_SKILL---; mockLLMProvider.generateText.mockResolvedValue(mockResponse); // 2. 调用待测试函数 const result await generateSkillFromDescription(a calendar skill, claude-3-sonnet); // 3. 断言结果 expect(result.status).toBe(success); expect(result.skillManifest.name).toBe(Fetch Calendar); expect(result.skillManifest.oauth?.providerId).toBe(google); // 4. 断言OAuth解析器被调用并补全了信息 expect(mockOAuthResolver).toHaveBeenCalledWith(google); }); });5.3 构建与部署考量这是一个Next.js应用因此部署选项与任何Next.js应用相同Vercel、Netlify、AWS等。但是由于其“本地优先”的特性需要特别注意几点静态资源与浏览器API应用大量使用浏览器特有的API如IndexedDB、Web Crypto API、Web Workers。这意味着它必须运行在浏览器环境中不能进行静态生成或服务端渲染核心功能页面。npm run build会生成一个混合渲染的应用但聊天等核心页面必然是客户端组件。环境变量确保生产环境变量正确配置。特别是如果启用了可选的同步/认证功能需要正确设置NEXTAUTH_URL、SUPABASE_URL等。数据库迁移当发布新版本且包含数据库模式变更时用户的本地IndexedDB数据库会自动运行迁移脚本。务必确保迁移脚本是向前兼容的并且能妥善处理旧数据。在MIGRATIONS对象中添加新版本时要编写详细的、测试过的迁移逻辑。体积优化嵌入模型xenova/transformers和Dexie等库会增加最终打包体积。利用Next.js的代码分割和动态导入来优化初始加载时间。考虑将一些重型依赖如特定技能设置为按需加载。6. 常见问题、排查与扩展思路6.1 开发与调试中常见问题6.1.1 数据库迁移失败症状应用打开白屏浏览器控制台报错“Migration failed”。排查检查src/lib/db/schema.ts中的MIGRATIONS对象。确认当前数据库版本和目标版本。在开发者工具的Application - IndexedDB中可以手动删除该网站的数据来重置数据库注意这会丢失所有本地数据。解决迁移脚本必须考虑边界情况。例如在重命名字段时要先检查旧字段是否存在。对于生产环境考虑在迁移前在控制台打印详细日志或者提供一种“安全模式”来绕过有问题的迁移。6.1.2 技能执行超时或失败症状技能卡住UI显示执行失败活动面板有错误信息。排查检查技能清单中声明的networkRules是否允许访问目标URL。检查技能代码中是否有无限循环或长时间阻塞的操作。在开发者工具的Console或Network面板查看是否有错误输出。检查skillPackages表中该技能的代码是否有语法错误。解决技能沙箱应该有更详细的错误日志。可以扩展运行时事件增加skill.error事件包含堆栈信息。对于网络请求失败技能应能优雅处理并返回用户友好的错误信息。6.1.3 内存检索不准确症状智能体似乎“忘记”了之前的重要对话。排查检查memoryEntries表中是否有预期的条目。对话压缩服务可能将旧消息总结后存为了记忆。检查嵌入模型是否成功加载并运行在Web Worker中。检查向量检索时的相似度阈值设置在AgentSettings中可配置。解决可以调整记忆提取策略和压缩阈值。也可以为记忆条目增加手动标记重要性的功能。6.2 性能优化点IndexedDB操作批量化频繁的单条读写会影响性能。对于批量操作如保存一系列消息应使用Dexie的事务或批量写入方法。向量搜索性能384维的向量余弦相似度计算在JavaScript中可能成为瓶颈尤其是在记忆条目很多时。确保向量搜索在Web Worker中进行避免阻塞主线程。可以考虑引入近似最近邻搜索算法如HNSW虽然实现复杂但能大幅提升检索速度。LLM上下文管理随着对话和记忆的增长发送给LLM的上下文窗口会越来越大导致成本增加和速度变慢。对话压缩服务是关键它需要智能地将历史消息总结成更紧凑的检查点。可以探索更积极的压缩策略或分层记忆结构。技能包懒加载所有已安装技能的代码都存储在IndexedDB中但并非所有技能都需要在启动时加载到内存。可以实现一个按需加载的机制只有当技能第一次被调用时才解析和执行其模块。6.3 项目扩展与自定义方向6.3.1 添加新的LLM提供商项目已经抽象了LLM提供商接口。要添加新的提供商如本地运行的Ollama在src/lib/providers/下创建一个新的适配器类实现LlmProvider接口。在提供商解析逻辑中注册这个新类。在UI的设置页面添加对应的配置选项。更新类型定义ProviderType。6.3.2 开发自定义技能这是参与生态的主要方式。你可以在本地通过“生成技能”功能快速原型化一个想法。手动在src/lib/skills/examples/下编写一个完整的技能模块包含清单文件和TypeScript代码。清单文件定义了技能的元数据、权限和配置。将技能提交到官方技能仓库或自己的仓库供他人发现和安装。6.3.3 集成外部系统MCP项目提到了MCPModel Context Protocol服务器的支持。MCP是一种让LLM安全访问外部工具和数据的协议。你可以实现一个MCP服务器例如连接到公司的内部数据库或API然后在OS Loop AI中配置这个服务器。智能体就能通过MCP协议安全地调用你服务器上定义的工具极大地扩展了其能力边界。6.3.4 主题与UI定制UI基于shadcn/ui和Tailwind CSS定制主题相对容易。你可以修改tailwind.config.ts中的颜色和样式或者创建自己的UI组件来替换默认的。由于状态管理与UI组件解耦替换组件不会影响核心逻辑。这个项目像一个精心设计的乐高套装提供了坚实的基础模块和清晰的接口。无论是想学习现代前端架构、探索AI智能体实现还是想构建一个真正属于自己、能持续学习和进化的数字助手它都提供了极高的起点和无限的可能性。我最欣赏的是它对“本地优先”和“用户主权”的坚持这在这个数据普遍被平台掌控的时代显得尤为珍贵。