如果你需要在自家产品里嵌入一个在线表格、文档或幻灯片,又希望这些文档能被 AI Agent 直接操作,像人一样读写数据、生成报表、完成审批闭环,那 Univer 很可能是目前最值得认真看一遍的开源办公应用 SDK。这篇文章不打算复述官方文档,我会按照实际接入和调优的经验,把 Univer 在 AI Agent 场景下的设计思路、核心接口、权限控制、并发处理等关键问题拆开讲,让拿到这篇文章的开发者可以直接照着做,少走我们踩过的弯路。
先交代一下背景:Univer 是一套基于 TypeScript、Canvas 渲染的开源办公套件 SDK,核心能力是电子表格、文档、演示文稿三大编辑器,同时提供公式引擎、条件格式、数据透视表、协同编辑等完整功能。它本身不是一个给终端用户直接使用的办公软件,而是一个让开发者把办公能力“嵌进自己产品”的组件库。你可以把它理解成“办公界的播放器内核”——界面、交互、数据模型都给你,你只负责提供容器和业务逻辑。这一点和 OnlyOffice、SheetJS、Luckysheet 等方案有本质区别,后面我会逐个对比。
更难得的是,Univer 从架构上就考虑了程序化操作:所有用户操作底层都是命令(Command),数据层和渲染层分离,开发者可以绕过 UI 直接读写数据模型。这就给 AI Agent 留了一扇门——大模型不需要“看见”界面,只需要调用接口,就能完成填表、改数、汇总、生成图表等一系列操作。今天这篇,就是把这扇门彻底打开,看看门后到底有什么。
1. 项目整体思路与定位拆解:为什么是“办公 SDK”而非“办公软件”
1.1 办公应用内核化的三种选择
先解决一个基础问题:项目里要嵌入表格能力,市面上有几条路可走,为什么最终会落到 Univer 这类 SDK 身上。
第一种是直接用商用套件,比如腾讯文档、飞书文档的企业版,或者 OnlyOffice 集成版。优点是功能全、稳定性高,缺点是数据资产和交互逻辑都绑在别人平台上,二次开发自由度低,想做深度定制的 AI 操作往往被限制在官方 API 的范围内。很多企业想做一个带智能分析的内部经营台账,结果发现没办法把大模型直接插进对方的单元格事件里,这就很憋屈。
第二种是用轻量表格库,比如 SheetJS(即 js-xlsx)、Luckysheet、Handsontable 之类。SheetJS 偏纯数据处理,擅长读写 Excel 文件,但渲染和交互很弱;Luckysheet 有过一段活跃期,不过更新节奏和架构相对陈旧;Handsontable 做数据表格没问题,但要支撑多 Sheet、公式引擎、样式合并、协同编辑,基本得自己造一整套轮子。我见过不少团队从 Handsontable 起步,最后维护一个庞大的公式计算模块,崩溃只是时间问题。
第三种就是 Univer 这类企业级开源 SDK,上来就给你完整的办公套件内核。从架构层面它做了一件事:把文档模型设计成可编程、可扩展、可序列化的结构,前端展示层只是模型的一种呈现方式。这意味着 AI Agent 可以直接对数据源编程,而不是模拟键盘鼠标去操作界面。对比下来,如果目标是“让 AI 下地干活”,这几乎是目前最匹配的路线。
1.2 Univer 的核心架构拆解
Univer 的总体架构可以拆成四层,理解这四层对后续接入 AI Agent 非常关键。
第一层是 Core 核心层,负责统一的数据模型和文档结构。不管你是表格、文档还是幻灯片,底层都有一套抽象的 Document / Sheet / Slide 数据模型,增删改查接口是通用的。这层还有个重要模块叫 Command 系统,用户在界面上做的每一次操作,本质上都是生成一个 Command 并执行。Command 可以被拦截、被回放、被序列化,这就为后续的权限管控和 AI 操作提供了基础设施。
第二层是渲染层,负责把模型画到 Canvas 上。Univer 没有走 DOM 表格路线,而是直接用 Canvas 绘制,所以在大数据量下仍然能保持流畅滚动和批量绘制。对 AI Agent 来说,渲染层不重要,重要的是即便 UI 隐藏成后台渲染,模型层依旧可以独立运转。
第三层是插件层,负责按需加载功能模块。公式引擎、条件格式、筛选、数据透视表、协同编辑,都是以插件形式存在。你不需要的功能可以不打包,体积和复杂度都可控。
第四层是接入层,也就是面向开发者的 Facade API 和预设包。默认会提供一批简化后的高等级接口,比如获取工作表、按范围读写值、监听变更等,这些接口天然适合暴露给 AI Agent 调用。我在后面 3.2 小节会专门讲这部分。
1.3 与其他开源方案的横向对比
| 方案 | 渲染方式 | 公式引擎 | 协同编辑 | AI 程序化控制难度 | 适合场景 |
|---|---|---|---|---|---|
| Univer | Canvas | 内置完整引擎 | 支持 | 低,命令+API 双层操作 | 需要深度定制的办公应用、AI Agent 数据入口 |
| OnlyOffice | DOM/Canvas 混合 | 内置完整引擎 | 支持 | 中,依赖官方 API 和插件机制 | 需要完整套件但定制不多 |
| SheetJS | 无渲染 | 只读公式计算 | 不支持 | 中,偏数据读写 | 服务端 Excel 解析导出 |
| Luckysheet | Canvas | 有但更新慢 | 部分 | 低,架构较陈旧 | 轻量在线表格展示 |
| Handsontable | DOM | 弱,需自建 | 不支持 | 中 | 数据录入型表格 |
坦白说,如果你只是要一个能展示和编辑表格的组件,Univer 的优势还不那么明显;但一旦你把“AI Agent 操作办公文档”作为核心目标,Univer 的程序化接口设计就立刻拉开差距了。
2. AI Agent 需要什么样的办公基础设施
2.1 传统办公软件为什么不适合作为 Agent 的手脚
一个很直接的痛点:大模型处理办公文档,最原始的方式是让它生成整份 Excel 或整段 Markdown,再让用户手动下载。这种方式在“一次性产出”的场合勉强可行,比如生成一份周报模板。但凡是涉及“读取现有数据 — 分析 — 回填 — 校验”的闭环场景,比如运营每天用 AI 维护活动排期表,或者财务让 AI 复核报销单并标记风险,一次性的文档生成就完全不够用了。
AI 需要的是一个稳定的、可寻址的数据操作入口。它要知道某个工作簿里有哪些 Sheet、某个单元格的地址是什么、某个范围当前的值是多少。这就像人类用 Excel 时,眼睛看到的是表格,手里操作的是行和列,但大脑里对数据的认知是一套地址系统:A1、B2、Sheet1。传统办公软件没有把这套地址系统公开给外部程序,所以 AI 只能做“写作文”,没法做“编辑手”。
2.2 Univer 为 Agent 提供的三个关键能力
Univer 能解决这个问题,核心是它给了三件东西。
第一,稳定的数据协议。Univer 的数据模型不是简单的 JSON 文件,而是一套结构严谨的文档模型。同一份表格,无论是用户在界面上编辑、还是 AI 通过 API 修改,最终走的都是同一条写入链路——生成 Command、校验、应用、持久化。数据的一致性由框架保证,AI 不再是“外挂”,而是和人类用户平等的操作者。
第二,可编程的命令系统。前面提到所有操作都是 Command。AI Agent 在调用接口时,本质上是生成并执行命令。这意味着你可以对命令做中间件拦截,比如某个单元格受保护、某张表只读、某个操作需要审批,全部可以在命令层拦截,而不是等数据写入后再回头修补。这套机制做企业权限控制非常顺手。
第三,可靠的插件化能力。AI 需要的功能往往不是标准表格功能,比如“从接口拉数回填”“按条件批量加批注”“生成透视表后自动命名”。Univer 的插件机制可以让你把这些能力封装成一个个插件,由 Agent 按需触发,而不是堆在业务代码里到处打补丁。
2.3 一个让 Agent 感知文档结构的“环境描述”设计
在实际项目中,我发现一个非常有效的模式:给 AI 一份“结构上下文”,而不是把整个表格数据塞给它。
比如你的 Agent 要操作一个经营分析工作簿,正确做法是给 LLM 提供如下环境描述:
- 工作簿包含三个 Sheet:销售明细、产品目录、月度汇总
- 销售明细有 4 列:日期、区域、产品、金额
- 数据量约 2000 行,当前数据最后更新于昨日 18:00
- 月度汇总表 B2:B13 分别存放 1-12 月销售额,C 列存放目标值
- AI 可操作的权限范围:只能修改月度汇总表的标记列 D 列,其他区域只读
这个描述通常只有几百个 token,但能精准约束模型的操作空间,避免它胡猜表名和字段。然后配合 tool function calling,AI 每次操作前先读这个上下文,再调用对应接口,就能做到又稳又可控。这是我强烈推荐大家优先落地的方案。
3. 核心功能实操:从嵌入表格到让 AI 写入数据
3.1 五分钟快速接入基础表格
先放一段最简单的初始化代码,把 Univer 的 Sheet 组件跑起来。前端包管理工具我用的是 npm,版本以写这篇文章时官方最新稳定版为准。
npm install @univerjs/presetsimport { Univer } from '@univerjs/presets'; import { UniverSheetsCorePreset, UniverSheetsUIPreset } from '@univerjs/presets/sheets'; const univer = new Univer({ container: '#app', presets: [ new UniverSheetsCorePreset(), new UniverSheetsUIPreset(), ], });这里我用了官方预设包,两行 import 就能同时拉起核心引擎和带 UI 的表格界面。如果你只需要无头模式(headless),可以只引用UniverSheetsCorePreset,渲染层不加载,但数据模型和公式引擎照常可用。这个无头模式对 AI Agent 特别有用:后端 Node.js 环境里完全可以只跑核心层,做批量公式计算和数据校验,不用依赖浏览器。
初始化之后,可以注册一个工作簿数据。比较推荐的做法是直接用univer.createUnit()或者通过命令接口创建一个空工作簿,再通过业务代码填充数据。
3.2 通过 Facade API 读写单元格
Univer 的 Facade 层是专门给外部程序调用的高级接口,比直接操作内部类语义清晰得多。下面是一段典型的读取、写入示例。
// 获取当前激活的工作簿 const workbook = univer.getActiveWorkbook(); const sheet = workbook.getActiveSheet(); // 读取 A1:D10 区域的值 const range = sheet.getRange('A1:D10'); const values = range.getValues(); // 写入 B2 单元格 sheet.getRange('B2').setValue('2026-02-18'); // 批量写入一个区域 sheet.getRange('A1:C3').setValues([ ['区域', '销售额', '目标'], ['华东', 320000, 300000], ['华南', 280000, 300000], ]);注意这里的getRange、getValues、setValues是 Facade 层提供的高等级 API,内部会自动生成并派发命令。对 AI Agent 来说,这层接口就像是给大模型准备的“函数说明书”:参数简单、返回结构确定、错误信息友好,非常适合映射成 function calling 的工具。
3.3 用户自定义表格与单元格保护:让 AI 在规则内干活
热搜里有一个非常典型的需求:“Univer 支持用户定义表格,然后让用户去填写一些单元格,其他的单元格用户无法修改”。这个需求本质上是“表单引擎 + 权限控制”的组合,在做 AI Agent 时尤其重要。你想让 AI 生成一张任务分发表,用户可以回填“完成情况”,但绝不能让用户或 AI 误改“任务编号”“负责人”这类关键字段。
Univer 的工作簿保护和工作表保护是分层的。你可以先保护整张工作表,然后开放指定区域允许编辑。下面是一个概念性的实现思路:
// 保护整个工作表 const sheet = workbook.getActiveSheet(); sheet.protect({ password: 'optional-password', permissions: { editObjects: false, selectLockedCells: false }, }); // 开放 A1:C10 区域允许编辑 const permissionRange = sheet.addPermissionRange('A1:C10', { permission: 'read-write', });把这段逻辑对应到 AI Agent 场景:AI 只拥有“被允许区域”的写入工具。在命令层,Univer 会拦截所有对锁定区域的修改请求,即使 AI 通过程序化接口写入也一样被拒。这让“AI 能做什么”从伦理规则变成了技术规则,上线时心里踏实很多。
补充一个细节:保护参数里的selectLockedCells如果设为 false,用户点击锁定区域时连选中都做不到,体验上会更明确;但程序化写入不受 UI 限制,所以权限控制一定要在命令层或业务层兜底,不能只依赖前端 UI。
3.4 数据校验与公式计算:Agent 的“计算器”和“质检员”
AI Agent 不只是做单元格赋值,更多时候需要计算结果。Univer 自带公式引擎,支持常见的 SUM、AVERAGE、VLOOKUP 等几百个函数,还支持动态数组公式。这意味着你可以让 AI 往某个月度汇总表写入公式,而不是写入硬编码的结果。
// 在 C2 写入公式,后续 A2:B2 变化时自动重算 sheet.getRange('C2').setFormula('=SUM(A2:B2)');这样设计有一个非常大的好处:AI 不需要自己心算结果,即使算错,公式也会按真实数据重新计算。把“人类的计算和判断”交给公式引擎,是降低 AI 幻觉风险的有效手段。我认为在实际项目中,能用公式表达的统计逻辑就不要让模型硬算,模型只负责选参数、摆结构、解释结果。
4. 从 0 到 1 搭建一个 AI 表格助手:真实项目复盘
4.1 整体架构与工具选型
接下来我要完整复盘一个我们团队实际做过的项目:内部销售数据分析 Agent。它要能读懂销售明细、按区域汇总、生成对比报表,并把结论写回一个新的 Sheet。整体架构可以用一张简单的关系图来描述,用文字说明:
前端是浏览器里的 Univer 表格界面,旁边挂一个聊天输入框;后端是 Python 写的 FastAPI 服务,负责接收前端请求,编排 LangChain / LangGraph 的 Agent 流程;Agent 内部通过 function calling 调用一组“表格工具”,每个工具背后对应 Univer Facade API 的对应功能。LLM 不直接碰数据库,也不直接改表格,所有数据交互都通过工具函数完成,这样每一步操作都有日志,出错了可以回溯。
选 LangGraph 而不是纯 LangChain 的原因,是因为这个场景天然是多轮、有状态和可回滚的:Agent 需要“读取数据 — 思考 — 写入 — 再读回来验证”,中间可能还要用户确认。LangGraph 可以把这些步骤建模成一张状态图,每一步的输入输出都结构化存储。一旦某一步发现结果异常,可以回退到上一个检查点重新执行,这对生产环境太重要了。
4.2 核心实现:把表格操作暴露成 Agent 工具
下面这一段是 Python 侧的核心代码。我把 Univer 的读写能力封装成了 FastAPI 的几个接口,并在 LangChain 工具注册表中把它们暴露为函数。
首先是后端接口,负责操作 Univer 数据层。实际部署时,后端 Node.js 服务连接同一个工作簿数据源,Python 侧通过内部 HTTP 或消息队列调用它:
# backend/tools/univer_tools.py import httpx import json UNIVER_SERVICE_URL = "http://univer-service:8001" def get_sheet_data(sheet_name: str, range_addr: str): """从指定工作表读取区域数据。""" resp = httpx.get( f"{UNIVER_SERVICE_URL}/api/sheets/{sheet_name}/range", params={"range": range_addr}, ) return resp.json() def write_sheet_data(sheet_name: str, range_addr: str, values): """向指定工作表区域写入数据。""" resp = httpx.post( f"{UNIVER_SERVICE_URL}/api/sheets/{sheet_name}/range", json={"range": range_addr, "values": values}, ) return resp.json()然后是 LangChain 工具定义,让大模型知道每个函数的功能、参数和返回。
from langchain.tools import StructuredTool tools = [ StructuredTool.from_function( name="get_sheet_data", description="读取表格数据,参数包括 sheet_name 和 range_addr,比如 A1:D10", func=get_sheet_data, args_schema=SheetQuery, ), StructuredTool.from_function( name="write_sheet_data", description="写入数据到表格区域,参数包括 sheet_name、range_addr 和二维数组 values", func=write_sheet_data, args_schema=SheetWrite, ), ]这里我有个经验想分享:工具描述写得越具体,模型越不容易用错。不要只写“读取表格数据”,要写清楚参数格式和示例值。比如描述中可以加一句“range_addr 格式必须为大写字母列号+数字行号,例如 A1:D10”,模型调用时出错的概率会明显降低。
4.3 Agent 编排:让多步操作变成可回溯的流程
Agent 的核心编排逻辑我用 LangGraph 实现。状态会记录三个信息:当前工作簿的 sheet 列表、当前会话内已经执行过的操作记录、最后一次读取的数据快照。这样可以防止模型在前几步出错后仍然沿着错误方向跑。
一个典型的多步流程如下:
- 用户提问:“分析最近三个月各区域销售情况,并和年度目标对比,把结论写到新 Sheet 里”
- 模型先调用
get_sheet_data读取销售明细的范围和字段结构 - 模型判断需要按区域汇总,于是调用
get_sheet_data读取明细全量数据 - 模型生成汇总结果,调用
add_sheet创建新工作表,命名“区域销售对比” - 模型调用
write_sheet_data把汇总表写入新 Sheet - 模型调用
get_sheet_data再读一遍写入后的区域,确认数据和预期一致 - 模型把结论用文字回复给用户,附上数据来源 Sheet 和范围
第 6 步的“写后读校验”是我在实际踩坑后加上的。早期版本没做这步,模型偶尔会自信地写错行列,直接把目标值覆盖成销售值,用户要花很长时间才发现。加上校验步骤之后,脏数据比例显著下降。
4.4 无头模式与服务端渲染:Agent 不一定要有界面
有些场景下 AI Agent 处理表格不一定需要界面,比如定时任务、批量导入导出、自动报表生成。Univer 无头模式在这里能派上大用场。
在 Node.js 服务端,你可以只加载核心 preset,不加载 UI,然后创建一个工作簿实例,把数据灌进去,用公式引擎跑一遍计算,最后导出成 Excel 文件。整个过程完全不依赖浏览器,适合放在定时任务或消息队列里。
// server-side headless example import { Univer } from '@univerjs/presets'; import { UniverSheetsCorePreset } from '@univerjs/presets/sheets'; const univer = new Univer({ presets: [new UniverSheetsCorePreset()], }); // 创建空工作簿、写数据、重算公式、导出 const workbook = univer.createUnit( UniverSheetsCorePreset.getWorkbookData({ sheets: [{ id: 'sheet1', cellData: { A1: { v: 100 }, A2: { v: 200 } } }], }) );我自己经常用这个能力做“AI 生成 Excel 模板并填充数据”的服务:接口收到请求后,在内存里创建工作簿、写入数据、套用样式,然后直接返回 xlsx 文件给客户端下载。这种方式比让 AI 生成一个庞杂的 HTML 表格再转 Excel 稳定得多。
5. 高并发场景下的 Agent 与表格服务设计
5.1 问题拆解:并发瓶颈到底在哪
热搜里有“ai agent 怎么扛并发”,这个问题我展开讲。很多人一听 AI Agent 并发就想到模型 API 限流,但实际系统里瓶颈往往是围绕文档操作的共享状态。
假设你有一个工作簿,多名用户和多个 Agent 并发往同一个 Sheet 写数据,如果没有并发控制,后果很典型:后写覆盖先写,数据互相踩踏,公式重算中间态错乱。所以问题不是“Agent 调用得有多快”,而是“对同一个文档状态的修改如何串行化”。
5.2 实践方案:无状态 Agent + 有状态文档锁
我采用的设计是把 Agent 本身做成无状态,真实状态只存在于工作簿服务和存储层。每个 Agent 任务在执行前,通过一个简短的“租约”机制获取工作表或工作簿的编辑权:
- 编辑权获取:Agent 执行写操作前,调用服务端接口获取一个短时锁,锁的持有者信息、过期时间都记录在 Redis 里
- 操作边界:同一时刻只允许一个写者修改某个工作簿;读操作不受锁限制,可以直接走副本
- 锁释放:操作完成后主动释放,或者超时自动释放,避免 Agent 崩溃后死锁
- 冲突检测:写入时带上版本号,如果版本不一致说明有其他写入发生,本次写操作抛错,由 Agent 重新读取再合并
这套逻辑其实和“乐观锁 + 悲观锁混合”的数据库设计一模一样,只是把锁粒度从行级换成了工作簿级。对大多数办公场景来说,锁粒度不需要太细,毕竟一个工作簿通常不会同时被多个写者高频修改。
5.3 Agent 调用的并发削峰与队列化
另一个常被忽略的点是:Agent 多步任务比单次 API 调用更容易把后端打爆。用户一次提问可能触发 5 到 10 次表格工具调用,如果同时在线 50 个用户,后端瞬间可能收到几百个操作请求。我的建议是给 Agent 任务加一层队列。
用 FastAPI 的话,可以直接基于 Redis 或 Celery 做一个任务队列。前端只负责接收用户请求,返回一个任务 ID;后端 Worker 根据任务 ID 拉取 Agent 流程并逐步执行。这样即使模型服务响应变慢,前端交互也不会卡死,还能顺带获得任务取消、重试、审计等能力。
这段设计中有一个细节值得注意:不要把工作簿服务变成 Agent 的同步调用下游。Agent 每执行一步都要等 Univer 服务返回,整个链路会被人为拉长。更好的做法是让 Agent 先完成数据计算和规划,再把一批写操作一次性提交到工作簿服务。批量提交能显著减少锁竞争和网络开销。
5.4 并发压测的一个简单指标
我自己做压测时,通常会给一个参考指标:单机 Node.js 无头模式处理纯读写操作,每请求耗时目标控制在 10ms 以内;加上公式重算后,视公式复杂度会上升到 20 到 100ms。如果发现单请求写操作超过 200ms,优先排查是不是做了全工作簿序列化,而不是只做增量更新。这个经验帮我排查过不少隐藏的性能问题。
6. 常见问题与排查技巧实录
6.1 初始化失败或白屏
- 版本不匹配:Univer 核心包和 preset 包需要保证同一个大版本。npm 环境里建议直接安装
@univerjs/presets,不要自己混搭零散小包,否则很容易遇到 API 对不上的问题。 - 容器高度:Univer 的容器必须有明确高度,不能是 0 或者
auto。我在项目里见过太多次样式没给高度导致白屏的案例。 - 字体加载:无头模式导出 Excel 时,如果服务端缺少合适字体,生成的 PDF 或图片可能显示乱码。中文场景记得安装中文字体。
6.2 AI 写入数据“沾边但不精确”
- 现象:模型把数据写到了相邻行,或者列位置差一格。
- 原因:工具描述里没有给出表格结构,模型靠猜。
- 解决:每次会话开始时,把 Sheet 名、列头、行数、当前数据预览作为“环境描述”发给模型。宁可多传几十个 token,也好过它写错一整片区域。
6.3 大表格把模型上下文塞爆
- 现象:几万行数据直接通过
get_sheet_data返回,token 超限报错。 - 解决:分层读取。先读列头和前 10 行预览,确定结构;再做聚合查询,比如按区域、月份的汇总结果,而不是读取全量明细。如果确实需要明细,优先让后端先做一次筛选或抽样。
6.4 并发写入覆盖
这个问题我在 5.2 小节已经详细讲过。这里补一个很容易犯的错误:不要在客户端本地用完数据就直接全量写回服务端,务必带上版本号或基于命令做增量更新。全量写回不仅慢,而且会让并发冲突概率急剧上升。
6.5 插件和主题样式冲突
如果你的宿主页面有自己的 CSS,留意 Univer 的弹出菜单、右键菜单、对话框可能被宿主样式干扰。建议把 Univer 容器放在一个独立的、带overflow: auto的容器里,或者使用 Shadow DOM 隔离。实际项目中我们遇到过 UI 框架的全局button样式把 Univer 内部按钮字体改掉的情况,排查了半天。
个人经验收尾
从把 Univer 接入一个内部数据平台,到把它升级成 AI Agent 的“手脚”,我最深的体会是:开源办公 SDK 的核心价值不是省掉你写表格组件的功夫,而是给了你一个可信赖的数据操作底座,让大模型在真实业务里真正“搭得上手”。如果你也想做类似的事情,我的建议是先从一个很小的场景做起,选一个只有两张表、三个操作的工作流跑通,再逐步叠加数据校验、公式重算、并发控制这些高级能力。还有一个小技巧分享给你:给 AI Agent 提供的工具集里,一定要有“写后读校验”这一步,让模型改完数据后自己再读一遍确认。这个动作看着简单,却能挡掉至少一半由模型幻觉导致的脏数据问题。