1. 为什么“AI Agent 的 Office 运行时”是个真问题
1.1 从一个具体场景说起:Agent 要改表格,但它没有“手”
我最早意识到这个问题的严重性,是在做一个财务对账类 Agent 的时候。需求听起来很简单:让 Agent 读取一张 Excel 报表,核对几个科目的数字,把差异写进备注列,再导出一份新的文件。用 Python 的 openpyxl 或者 pandas 当然能做,但问题在于——Agent 不是跑在脚本里的,它跑在一个对话式的工作流里,用户随时可能说“把第三行那个数改成 500”“帮我把这一列标红”“在 B 列插入一列写说明”。这些操作如果每次都靠后端重新生成整个文件,体验会碎成一地。
更麻烦的是权限。真实业务里,一张表往往不是所有人都能随便改的。财务给的模板,可能只允许填“本期发生额”这一列,其他单元格是公式、是锁定区域、是别人负责的部分。你让一个 Agent 去操作,它必须知道“哪些能碰、哪些不能碰”,而不是拿到一个文件就全量重写。
这就是 Univer 这类项目要解决的核心矛盾:AI Agent 需要一个能理解、能操作、能约束的 Office 运行时,而不是一个只能读写的文件解析库。
Univer 的定位,用一句话说,它是一个开源的、可嵌入的 Office 套件运行时,支持表格、文档、幻灯片等形态,并且从设计之初就把“可编程”和“可约束”放在了很重要的位置。它不是一个桌面软件的替代品,而是一个可以被 Agent 调用的、有状态的计算环境。你可以把它理解成:给 Agent 发了一台“虚拟办公桌”,桌上摆着表格和文档,Agent 可以按规则去动这些东西,而不是每次都在后台重新打印一份。
1.2 热搜词背后的真实需求:从“能跑”到“可控”
看这次的热搜词,很有意思。一边是Univer、AI Agent、Office、SDK、Node.js这些技术词,另一边是office安装包安卓、office永久激活、office破解版下载、office卸载不彻底这些非常“民间”的词。这两组词放在一起,其实暴露了一个现实:大量做 Agent 的人,并不是从 Office 生态里长出来的,他们是从 Web、从 Node.js、从 AI 应用层切进来的。
他们不关心 Office 安装包怎么激活,他们关心的是:我怎么在我的 Node.js 服务里,让 Agent 操作一个表格,而且这个表格还能在浏览器里给用户看到、让用户接着改。传统的做法是前端用某个表格组件,后端用 Excel 解析库,两边状态同步靠轮询或者 WebSocket 硬扛,一旦涉及公式、格式、合并单元格、权限控制,就开始各种补丁。
Univer 的思路是把这套东西做成一个统一的运行时。前端有 Canvas 渲染的界面,后端有 Node.js 侧的计算和持久化能力,中间通过一套 SDK 暴露 API。Agent 不需要去模拟鼠标点击,它直接调用运行时的方法,比如“设置某个区域的单元格值”“锁定某个工作表”“读取某个范围的计算结果”。这就把“操作 Office”从 UI 自动化变成了 API 调用,稳定性和可控性完全不是一个量级。
1.3 谁适合看这篇:三类人可以对号入座
第一类,是正在做 AI Agent 应用、需要让 Agent 处理表格和文档的开发者。你可能已经试过让 Agent 生成 CSV 或者 Markdown 表格,但发现一旦用户要“接着改”,整个链路就断了。Univer 提供的是一种“可交互的中间态”,Agent 改完的东西,用户可以在同一个运行时里继续操作。
第二类,是前端或全栈工程师,想找一个能嵌入自己产品的 Office 组件。你不需要从零实现一个表格引擎,也不需要被某个商业组件绑死。Univer 是开源的,你可以自己部署、自己扩展,甚至改它的渲染逻辑。
第三类,是对“AI Agent 怎么扛并发”这个问题感兴趣的人。热搜词里出现了这个,说明大家已经开始从 demo 走向生产了。Univer 的运行时模型,天然适合做多用户、多 Agent 的并发协作,因为它把状态管理和计算逻辑放在了服务端,前端只是视图。这个架构选择,后面我会详细拆。
2. Univer 的整体设计与核心思路拆解
2.1 不是“另一个在线表格”,而是“Office 运行时”
很多人第一次看到 Univer,会下意识地把它和在线表格工具做对比。这个对比方向不太对。在线表格工具是一个产品,Univer 是一个运行时。产品是给最终用户用的,运行时是给开发者用的。Univer 的官方定位里,“SDK”和“可嵌入”是关键词,它不指望你直接打开一个网站就用它,而是希望你把它集成到自己的应用里。
这个定位决定了它的架构。一个在线表格产品,可以把所有逻辑都放在前端,后端只做存储。但一个运行时,必须考虑“谁来调用”“调用的时候状态在哪里”“多个调用者怎么不打架”。所以 Univer 把核心的计算引擎、状态管理、权限模型都做成了可以在 Node.js 侧运行的东西。前端渲染层通过一套协议和服务端通信,Agent 也是通过同一套协议或者更底层的 API 来操作。
这就带来一个很实际的好处:Agent 和用户看到的是同一个状态。用户在前端改了一个单元格,Agent 在服务端能立刻感知到;Agent 改了一个区域,前端也会实时更新。不需要做两套状态同步,也不需要担心“Agent 改的是旧版本”。
2.2 为什么选 Node.js 作为服务端运行时
热搜词里Node.js出现了很多次,包括node.js安装教程、centos 7.9 node.js安装部署、node.js是干什么的。这说明很多人的技术栈是围绕 Node.js 建的。Univer 把服务端运行时放在 Node.js 上,是一个很务实的选择。
第一,前端和后端可以共享同一套逻辑。Univer 的核心计算引擎是用 TypeScript 写的,在浏览器里能跑,在 Node.js 里也能跑。这意味着公式计算、数据校验、权限判断这些逻辑,不需要写两遍。对于 Agent 场景来说,这一点尤其重要:Agent 在服务端做的操作,和用户在前端做的操作,走的是同一套规则,不会出现“前端能改、后端改不了”或者反过来。
第二,Node.js 的异步模型适合处理大量并发的轻量操作。Agent 操作表格,往往是很多个小操作:读一个范围、写一个单元格、查一个公式结果。这些操作如果每个都开一个线程,开销太大。Node.js 的事件循环模型,配合 Univer 运行时的状态管理,可以比较自然地处理这种并发。
第三,生态成熟。做 AI Agent 的人,大概率已经在用 Node.js 或者至少熟悉 JavaScript/TypeScript。让 Agent 通过一个 npm 包来操作 Office 运行时,比让它去调一个 C++ 编译的二进制或者走复杂的 RPC 要顺手得多。
2.3 权限模型:让 Agent“只能填该填的格子”
热搜词里有一句很具体的话:“univer 支持用户定义表格,然后让用户去填写一些单元格,其他的单元格用户无法修改”。这个描述其实点到了 Univer 在 Agent 场景下最有价值的一个能力:细粒度的单元格权限控制。
传统的 Excel 文件权限,要么是文件级的(能不能打开),要么是工作表级的(能不能编辑这个 sheet),最多到“保护工作表”加密码。但真实业务里,权限往往是单元格级的:这一列是财务填的,那一列是业务填的,公式列谁都不能动,汇总行只有经理能改。
Univer 的运行时模型里,权限是作为状态的一部分来管理的。你可以定义一个范围,设置它的编辑权限,然后把这个权限和用户身份或者 Agent 身份绑定。Agent 在操作的时候,运行时会先检查权限,如果越权,操作会被拒绝,而不是悄悄改掉。这对于“让 AI 真的下地干活”来说,是一个安全底线。没有这个,你不敢让 Agent 碰生产数据。
2.4 和传统方案的对比:为什么不是 openpyxl + 前端表格组件
我试过用 openpyxl 做后端、用某个前端表格组件做展示的方案。问题出在“状态一致性”上。openpyxl 读进来的是一个 Python 对象,前端表格组件维护的是另一套状态。Agent 改了 Python 对象,要序列化成文件,前端再重新加载,才能看到变化。如果用户同时在前端改,两边就冲突了。
Univer 的做法是只有一个状态源。这个状态源可以在服务端,也可以在前端,但同一时刻只有一个地方是“权威”的。Agent 和用户都通过 API 去改这个状态源,改完之后,所有订阅者收到更新。这就把“同步问题”变成了“权限和冲突解决问题”,而后者是可以通过版本号、操作变换(OT)或者 CRDT 来处理的。
对于 Agent 场景,这个架构还有一个隐藏好处:Agent 的操作可以被记录、被回放、被审计。因为所有操作都是通过 API 走的,你可以把 Agent 的每一步操作记下来,出了问题可以追溯。如果 Agent 是直接改文件,你很难知道它到底改了哪里。
3. 核心细节解析与实操要点
3.1 环境准备:Node.js 版本和依赖管理
Univer 的服务端运行时对 Node.js 版本有要求。根据我实际部署的经验,建议用 Node.js 20 LTS 或更高版本。热搜词里出现了node.js 22.12+,如果你要用最新的特性,22.x 也可以,但生产环境我倾向于用 LTS。
安装方式很简单,但有几个坑要注意:
# 用 nvm 管理版本,避免系统自带的 Node.js 太旧 nvm install 20 nvm use 20 # 初始化项目 mkdir univer-agent-demo && cd univer-agent-demo npm init -y # 安装 Univer 核心包 npm install @univerjs/core @univerjs/sheets @univerjs/sheets-formula注意:Univer 的包名是
@univerjs/开头,不要装成univer或者univerjs,那些可能是旧版本或者第三方包。另外,如果你在 CentOS 7.9 上部署,系统自带的 glibc 版本可能比较低,Node.js 20 需要 glibc 2.28 以上。如果遇到GLIBC_2.28 not found,要么升级系统,要么用 Docker 跑。
3.2 初始化一个可被 Agent 操作的表格实例
在服务端创建一个 Univer 实例,核心是拿到Univer对象和对应的Workbook。下面是一个最小化的例子:
import { Univer, LocaleType } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; // 创建 Univer 实例 const univer = new Univer({ locale: LocaleType.ZH_CN, logLevel: 'error', }); // 注册插件 univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); // 创建一个工作簿 const workbook = univer.createUniverSheet({ id: 'agent-demo', sheetId: 'sheet-001', name: '对账表', rowCount: 100, columnCount: 20, }); // 拿到工作表 const worksheet = workbook.getActiveSheet();这段代码跑起来之后,你手里就有一个可以在内存里操作的表格对象了。Agent 可以通过这个对象的方法来读写单元格。
3.3 单元格读写:Agent 最常用的两个操作
读单元格:
const cellValue = worksheet.getRange(0, 0).getValue(); // 或者读一个范围 const rangeValues = worksheet.getRange(0, 0, 5, 3).getValues();写单元格:
worksheet.getRange(0, 0).setValue('本期发生额'); worksheet.getRange(1, 0).setValue(1000);看起来很简单,但有几个细节要注意。第一,行列索引是从 0 开始的,不是从 1 开始。第二,setValue之后,公式不会自动重算,需要手动触发计算或者等下一次计算周期。第三,如果单元格有数据验证或者权限限制,setValue可能会失败,需要检查返回值或者捕获异常。
实操心得:在 Agent 场景里,我建议把“读”和“写”分开成两个工具函数,读的函数返回纯数据,写的函数返回操作结果(成功/失败/被拒绝)。这样 Agent 在调用的时候,能清楚地知道自己的操作有没有生效,而不是改完之后发现没变化。
3.4 权限控制:定义“哪些格子 Agent 不能碰”
这是 Univer 在 Agent 场景下最值得花时间配置的部分。假设你有一个预算表,A 列是科目名称(只读),B 列是预算数(只读),C 列是实际发生数(Agent 可写),D 列是差异(公式,只读)。
// 锁定 A 列和 B 列 worksheet.getRange(0, 0, 100, 2).setLocked(true); // 锁定 D 列(公式列) worksheet.getRange(0, 3, 100, 1).setLocked(true); // C 列不锁定,Agent 可以写 worksheet.getRange(0, 2, 100, 1).setLocked(false);设置锁定之后,如果 Agent 尝试写 A 列,运行时会拒绝这个操作。你可以捕获这个拒绝,然后给 Agent 返回一个明确的错误信息,让它知道“这个区域不可写”。
注意:锁定只是运行时层面的控制,不是加密。如果 Agent 直接操作底层数据对象,还是能绕过去。所以权限检查要放在 Agent 调用 API 的那一层,而不是只依赖单元格的 locked 属性。
3.5 公式计算:让 Agent 写进去的数字自动算出结果
Univer 的公式引擎支持大部分常用的 Excel 公式。Agent 写了一个数字到 C 列,D 列的公式会自动重算。这个能力对于 Agent 来说很重要,因为 Agent 不需要自己实现计算逻辑,它只需要把原始数据填进去,公式会处理剩下的。
// 在 D 列设置公式 worksheet.getRange(0, 3).setFormula('=C1-B1'); // 触发重算 univer.getFormulaManager().calculate();实测下来,公式重算在数据量不大的时候很快,几百行基本无感。但如果你的表格有几千行公式,建议批量写入之后再触发一次计算,而不是每写一个单元格就重算一次。
4. 实操过程与核心环节实现
4.1 从零搭建一个 Agent 可操作的表格服务
这一节我把完整流程走一遍。目标:搭一个 Node.js 服务,暴露 HTTP 接口,Agent 通过接口来读写表格,同时有权限控制。
第一步,初始化项目并安装依赖:
mkdir univer-agent-server && cd univer-agent-server npm init -y npm install @univerjs/core @univerjs/sheets @univerjs/sheets-formula express npm install -D typescript ts-node @types/express @types/node npx tsc --init第二步,创建 Univer 运行时管理模块。因为一个服务可能同时处理多个表格,所以需要一个管理器来维护多个 Univer 实例:
// univer-manager.ts import { Univer, LocaleType } from '@univerjs/core'; import { UniverSheetsPlugin } from '@univerjs/sheets'; import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'; interface SheetInstance { univer: Univer; workbook: any; worksheet: any; } const instances = new Map<string, SheetInstance>(); export function createSheet(id: string, rowCount = 100, columnCount = 20): SheetInstance { const univer = new Univer({ locale: LocaleType.ZH_CN, logLevel: 'error', }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsFormulaPlugin); const workbook = univer.createUniverSheet({ id, sheetId: `${id}-sheet`, name: 'Sheet1', rowCount, columnCount, }); const worksheet = workbook.getActiveSheet(); const instance = { univer, workbook, worksheet }; instances.set(id, instance); return instance; } export function getSheet(id: string): SheetInstance | undefined { return instances.get(id); }第三步,暴露 HTTP 接口:
// server.ts import express from 'express'; import { createSheet, getSheet } from './univer-manager'; const app = express(); app.use(express.json()); // 创建表格 app.post('/sheet', (req, res) => { const { id, rowCount, columnCount } = req.body; const instance = createSheet(id, rowCount, columnCount); res.json({ ok: true, id }); }); // 读单元格 app.get('/sheet/:id/cell/:row/:col', (req, res) => { const instance = getSheet(req.params.id); if (!instance) return res.status(404).json({ error: 'sheet not found' }); const value = instance.worksheet .getRange(Number(req.params.row), Number(req.params.col)) .getValue(); res.json({ value }); }); // 写单元格 app.post('/sheet/:id/cell', (req, res) => { const instance = getSheet(req.body.id); if (!instance) return res.status(404).json({ error: 'sheet not found' }); const { row, col, value } = req.body; try { instance.worksheet.getRange(row, col).setValue(value); res.json({ ok: true }); } catch (e) { res.status(403).json({ error: 'write rejected', detail: String(e) }); } }); app.listen(3000, () => console.log('listening on 3000'));这个服务跑起来之后,Agent 就可以通过 HTTP 请求来操作表格了。当然,生产环境还需要加认证、加限流、加操作日志,但核心链路就是这些。
4.2 参数选择:行数、列数和内存占用
Univer 在创建表格的时候需要指定rowCount和columnCount。这个不是随便填的,因为它会影响内存占用。我实测过,一个 1000 行 x 50 列的表格,在 Node.js 里大概占 20-30MB 内存。如果你有 100 个这样的表格实例,就是 2-3GB。所以行数列数要按实际需要来,不要一上来就开 10000 行。
实操心得:如果你的表格是动态增长的,可以先用一个较小的初始值,比如 200 行,然后在 Agent 写入超出范围的时候,动态扩展。Univer 支持
insertRows和insertColumns,但频繁插入会影响性能,建议批量插入。
4.3 并发处理:多个 Agent 同时操作一张表
热搜词里有“ai agent 怎么扛并发”,这个问题在 Univer 场景下可以拆成两个层面:一是多个 Agent 同时写同一张表,二是多个表格实例同时被操作。
对于第一种,Univer 的运行时本身不是线程安全的,因为它是单线程的 JavaScript 对象。如果你有多个 Agent 同时写,需要加锁。最简单的做法是在 Node.js 层面用一个队列,把写操作串行化:
const writeQueue: Array<() => Promise<void>> = []; let processing = false; async function enqueueWrite(task: () => Promise<void>) { writeQueue.push(task); if (!processing) { processing = true; while (writeQueue.length > 0) { const next = writeQueue.shift()!; await next(); } processing = false; } }对于第二种,多个表格实例之间是独立的,可以并行处理。但要注意 Node.js 的单线程模型,如果计算量很大,会阻塞事件循环。这时候可以考虑用 Worker Threads 把计算密集型的操作放到单独的线程里。
4.4 持久化:Agent 改完之后,数据存哪里
Univer 的运行时状态是在内存里的,服务重启就没了。所以需要持久化。最简单的做法是定期把工作簿序列化成 JSON,存到数据库或者文件系统。
const snapshot = instance.workbook.save(); // snapshot 是一个 JSON 对象,可以存到任何地方恢复的时候:
const workbook = univer.createUniverSheet(snapshot);注意:
save()出来的快照包含公式、格式、权限等所有状态,但不包含未计算完的中间结果。恢复之后需要重新触发一次计算。
5. 常见问题与排查技巧实录
5.1 公式不重算:Agent 写了值但结果没变
这是最常见的问题。原因通常是写入之后没有触发计算。Univer 的公式计算是懒执行的,setValue只是改了单元格的值,不会自动重算依赖它的公式。
解决方法:在批量写入之后,显式调用univer.getFormulaManager().calculate()。如果你用的是setFormula,公式本身会被注册,但结果也需要计算之后才可见。
5.2 权限拒绝没有报错:Agent 以为写成功了
有时候 Agent 写了一个锁定的单元格,setValue没有抛异常,但值也没变。这是因为权限检查可能是在更上层做的,底层 API 不一定抛错。
排查方法:写完之后立刻读一次,对比值是否变化。如果没变,说明被拒绝了。更好的做法是在写之前先检查isLocked()。
5.3 内存泄漏:长时间运行后内存持续增长
Univer 实例如果不再使用,需要手动销毁。否则事件监听、公式依赖图等会一直占着内存。
univer.dispose(); instances.delete(id);实操心得:我建议给每个表格实例加一个最后访问时间,超过一定时间没有操作的实例自动销毁。对于 Agent 场景,很多表格是一次性的,用完就可以释放。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 公式结果不更新 | 未触发计算 | 检查是否调用 calculate | 写入后显式触发计算 |
| 写入被拒绝但无报错 | 单元格锁定 | 读回值对比 | 写前检查 isLocked |
| 内存持续增长 | 实例未销毁 | 监控 heapUsed | 定期 dispose 无用实例 |
| 并发写入冲突 | 无锁机制 | 检查写入顺序 | 用队列串行化写操作 |
| 快照恢复后格式丢失 | 快照不完整 | 对比 save 前后 | 确保 save 包含所有插件状态 |
5.5 一个容易忽略的坑:Node.js 版本和 TypeScript 配置
Univer 的包是 ESM 和 CJS 双发布的,但如果你用 TypeScript,moduleResolution要设成node16或者bundler,否则可能找不到类型定义。另外,tsconfig.json里的target建议至少ES2020,因为 Univer 用了一些较新的语法。
如果你在 CentOS 7.9 上部署,Node.js 20 需要 glibc 2.28,而 CentOS 7.9 默认是 2.17。解决办法是用 Docker 跑一个 Node.js 20 的镜像,或者升级到 CentOS 8 以上。这个坑我踩过,当时报了一堆GLIBC not found,排查了半天才发现是系统版本问题。
6. 这套运行时还能怎么扩展
6.1 接入更多 Agent 框架
Univer 的 SDK 是纯 TypeScript 的,不绑定任何 Agent 框架。你可以把它封装成 LangChain 的 Tool,也可以封装成某个 Agent 平台的插件。核心就是暴露几个方法:读范围、写范围、设置公式、检查权限。Agent 框架负责决定什么时候调用这些方法,Univer 负责执行和约束。
6.2 多文档类型:从表格到文档和幻灯片
Univer 不只支持表格,还有文档和幻灯片的运行时。如果你的 Agent 需要生成报告,可以先在表格里算数据,然后把结果写到文档里,最后生成幻灯片。这些都可以在同一个 Univer 实例里完成,状态是打通的。
6.3 审计和回放:记录 Agent 的每一步操作
因为所有操作都走 API,你可以在 API 层加一个中间件,把每次操作记下来:谁、什么时候、改了哪个范围、改成了什么。这个日志对于排查问题和满足合规要求都很有用。更进一步,你可以用这个日志做回放,看看 Agent 是怎么一步步把表格改成最终状态的。
我个人在实际操作中的体会是,Univer 这类运行时的价值,不在于它现在支持多少公式、渲染多好看,而在于它把“Office 操作”从文件层面提升到了状态层面。对于 AI Agent 来说,文件是死的,状态是活的。Agent 需要的是一个能理解上下文、能约束行为、能实时反馈的环境,而不是一个每次都要重新解析和序列化的文件。这个方向,我觉得是对的。