ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

办公运行时:Agent 操作表格文档的 TypeScript 架构与实战

2026/10/8 15:46:25 拓冰建站 浏览量
办公运行时:Agent 操作表格文档的 TypeScript 架构与实战 1. 办公运行时到底是个什么东西1.1 从一个真实痛点说起过去大半年我一直在折腾各种 Agent 项目从最简单的对话机器人到能自动完成多步骤任务的复杂编排系统踩过的坑不算少。但有一个问题反复出现几乎每个做 Agent 落地的人都会遇到Agent 能思考、能调工具、能写代码可一旦让它处理表格、文档、幻灯片这类办公文件立刻就变成了半身不遂。你可能觉得夸张我举个实际场景。假设你让 Agent 帮你看一份销售数据表分析一下哪个区域增长最快然后生成一份带图表的汇报文档。听起来不难对吧但实际操作起来是这样的Agent 先得把 xlsx 文件读进来解析成某种中间格式然后做数据分析接着要生成一份 docx 或者 pptx里面还得嵌入图表。每一步都涉及不同的库、不同的文件格式、不同的渲染逻辑。更麻烦的是如果用户想在浏览器里实时看到 Agent 的操作过程比如它正在修改哪个单元格、正在调整哪段文字你几乎没有办法做到实时同步。这就是办公运行时要解决的核心问题。所谓运行时你可以理解为一个让程序跑起来的环境。就像 Node.js 是 JavaScript 的运行时浏览器是网页的运行时办公运行时就是让 Agent 能够操作办公文档的那个底层环境。它不只是简单地读写文件而是提供一套完整的、可交互的、可实时同步的文档操作能力。Univer 这个项目做的就是这件事。它把表格、文档、幻灯片三种最核心的办公形态全部装进了一个统一的运行时里而且这个运行时是专门为 AI Agent 设计的。换句话说Agent 不需要再去学怎么解析 xlsx、怎么生成 pptx它只需要调用 Univer 提供的接口就能像人一样操作这些文档。1.2 为什么是 TypeScript为什么是现在Univer 选择 TypeScript 作为核心开发语言这个决策背后有很实际的考量。办公文档处理涉及大量的数据结构操作、类型转换、事件处理TypeScript 的静态类型系统能在这个复杂度下提供极大的安全感。我做过一个对比用纯 JavaScript 写一个表格单元格的更新逻辑和用 TypeScript 写同样的逻辑后者在编译阶段就能帮你拦住至少三成的低级错误比如把字符串塞进了数字类型的单元格、把行列索引搞反了之类的。更重要的是TypeScript 的类型声明文件.d.ts机制让 Univer 可以很方便地对外暴露一套清晰的 API。Agent 开发者不需要去读源码只需要看类型定义就知道每个接口怎么用、参数是什么类型、返回值是什么结构。这对于 Agent 自动生成代码来说尤其重要因为 Agent 本身就是靠类型信息来推断怎么调用接口的。从时间点上看现在做办公运行时也是一个很自然的节点。大模型的能力已经足够强能理解复杂的办公指令但缺的是一个稳定的、可编程的执行环境。Univer 补上的就是这块拼图。1.3 谁适合关注这个方向如果你正在做 Agent 应用开发尤其是涉及办公自动化、数据分析、报告生成这类场景Univer 的运行时方案值得认真研究。如果你是一个前端或者全栈工程师想往 Agent 方向转型理解办公运行时的架构会是一个很好的切入点。甚至如果你只是一个对 AI 办公感兴趣的产品经理了解这套东西能帮你更准确地判断哪些需求现在能实现、哪些还不行。我个人的判断是办公运行时这个赛道现在还处于非常早期的阶段但它的天花板很高。因为办公文档是人类信息交换最通用的载体之一Agent 要真正融入工作流绕不开这一关。2. Univer 的核心架构拆解2.1 三层架构渲染层、逻辑层、数据层Univer 的架构可以粗略地分为三层我用一个生活化的类比来解释。想象你在一家餐厅吃饭渲染层就是你看到的餐桌、盘子、食物摆盘逻辑层是后厨的厨师负责按照订单做菜数据层是仓库存放着所有食材。三层各司其职但又紧密配合。渲染层负责把文档内容画到屏幕上。Univer 用的是 Canvas 渲染而不是 DOM。这个选择很关键。DOM 渲染在处理大规模表格时性能会急剧下降一个几万行的表格用 DOM 渲染滚动起来能卡成幻灯片。Canvas 渲染则可以把整个表格当成一张画布来绘制性能好得多。而且 Canvas 渲染天然适合做实时协作因为你可以精确控制每一帧的绘制内容。逻辑层是 Univer 的核心它包含了所有文档操作的业务逻辑。比如插入一行、删除一列、修改单元格样式、合并单元格、插入图表等等。这一层是 Agent 主要打交道的部分。Univer 把所有的操作都抽象成了命令CommandAgent 只需要发出命令逻辑层负责执行。数据层管理文档的实际数据。Univer 用了一种叫快照增量的数据模型。文档的完整状态是一个快照每次操作产生一个增量补丁。这种设计的好处是Agent 可以很方便地回滚操作、可以做操作历史记录、可以实现多人协作时的冲突解决。2.2 命令系统Agent 操作文档的统一入口Univer 的命令系统是整个运行时最值得细看的部分。它把所有对文档的操作都统一成了命令模式每个命令有明确的输入参数和输出结果。这个设计对 Agent 来说非常友好因为 Agent 最擅长的就是根据目标生成一系列命令。我举个例子。假设 Agent 要在一个表格的 B2 单元格里填入销售额它需要发出一个类似这样的命令const command { type: SET_CELL_VALUE, params: { unitId: sheet-001, row: 1, column: 1, value: 销售额 } };逻辑层收到这个命令后会做几件事验证参数合法性、更新数据层、触发渲染层重绘、记录操作历史。整个过程是原子的要么全部成功要么全部失败不会出现数据不一致的情况。这种命令模式还有一个好处就是可序列化。Agent 可以把一系列命令序列化成 JSON存到数据库里或者通过网络传给另一个 Agent 执行。这对于多 Agent 协作场景特别有用。比如一个 Agent 负责分析数据另一个 Agent 负责生成报告它们之间可以通过命令序列来传递操作意图。2.3 多文档类型的统一抽象Univer 支持表格、文档、幻灯片三种文档类型但它没有为每种类型单独写一套逻辑而是抽象出了一个统一的文档模型。这个模型的核心概念是单元Unit每种文档类型都是单元的一种具体实现。表格的单元是单元格文档的单元是段落幻灯片的单元是页面上的元素。虽然形态不同但它们都共享一些基础能力都可以被创建、删除、修改、移动、设置样式。Univer 把这些共性抽出来放在基础层然后每种文档类型再实现自己的特有逻辑。这种设计对 Agent 开发者的意义在于你学一套 API 就能操作三种文档。比如你要设置某个单元的样式不管是表格单元格还是文档段落调用的接口形式是一样的。这大大降低了 Agent 的学习成本。2.4 实时协作与 Agent 的天然契合Univer 内置了实时协作能力多个用户可以同时编辑同一份文档。这个能力看起来是为人设计的但实际上对 Agent 同样重要。想象一个场景Agent 正在帮用户生成一份季度报告用户同时在浏览器里看着这份报告。Agent 每写一段文字、每插入一个图表用户都能实时看到。如果用户觉得某段写得不好可以直接在文档里修改Agent 能感知到用户的修改并据此调整后续的生成策略。这种人机协作的体验如果没有实时同步能力是根本做不到的。Univer 的实时协作底层用的是 OTOperational Transformation或者 CRDT 这类算法具体用哪种取决于配置。这些算法的核心思想是把每个操作都打上时间戳和上下文信息当多个操作并发时能自动合并成一致的结果。Agent 发出的操作和用户发出的操作在这个层面是平等的都会被正确处理。3. 把 Univer 接入 Agent 的实操路径3.1 环境准备与最小可运行示例先说环境。Univer 是一个 TypeScript 项目你可以用 npm 或者 yarn 来安装。我建议用 pnpm因为 Univer 的依赖比较多pnpm 的硬链接机制能省不少磁盘空间。pnpm add univerjs/core univerjs/sheets univerjs/sheets-ui安装完之后你需要初始化一个 Univer 实例。最小化的代码大概长这样import { Univer, LocaleType, merge } from univerjs/core; import { UniverSheetsPlugin } from univerjs/sheets; import { UniverSheetsUIPlugin } from univerjs/sheets-ui; const univer new Univer({ locale: LocaleType.ZH_CN, theme: default }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); const workbook univer.createUniverSheet({ id: agent-workbook, sheetOrder: [sheet-001], sheets: { sheet-001: { id: sheet-001, name: Agent 工作表, rowCount: 1000, columnCount: 26 } } });这段代码创建了一个包含一个工作表的 Univer 实例。注意rowCount和columnCount的设置我建议不要一上来就设太大比如设成 10000 行因为 Univer 会为每个单元格分配内存。按需扩展比一次性分配要高效得多。3.2 让 Agent 学会看文档Agent 要操作文档首先得能看到文档的当前状态。Univer 提供了多种方式来获取文档数据。最直接的方式是获取整个工作表的快照const snapshot univer.getSheetSnapshot(sheet-001);这个快照包含了工作表的所有数据、样式、合并单元格信息等等。但快照可能很大如果 Agent 每次操作前都读一遍完整快照token 消耗会非常惊人。我实测过一个 1000 行 20 列的表格完整快照序列化成 JSON 大概有 200KB换算成 token 差不多 5 万个。这对于大多数模型来说都太贵了。更好的做法是让 Agent 按需读取。Univer 支持范围查询你可以只获取某个区域的单元格数据const rangeData univer.getRangeData(sheet-001, { startRow: 0, endRow: 10, startColumn: 0, endColumn: 5 });这样 Agent 可以先读表头了解数据结构然后再决定读哪些具体区域。这种渐进式感知的策略能大幅降低 token 消耗。还有一个技巧是让 Agent 先读文档的元信息比如有多少行、多少列、有哪些工作表、每个工作表的名称是什么。这些信息量很小但能帮 Agent 建立对文档的整体认知。3.3 让 Agent 学会写文档写操作是 Agent 的核心能力。Univer 的命令系统提供了丰富的写操作接口。我整理了几个最常用的操作类型命令名称关键参数适用场景设置单元格值SET_CELL_VALUErow, column, value填入数据设置单元格样式SET_CELL_STYLErow, column, style加粗、变色、对齐插入行INSERT_ROWrowIndex, count在指定位置插入空行删除行DELETE_ROWrowIndex, count删除多余行合并单元格MERGE_CELLrange制作表头设置列宽SET_COLUMN_WIDTHcolumn, width调整显示效果这些命令可以通过 Univer 的 API 直接执行univer.executeCommand({ type: SET_CELL_VALUE, params: { unitId: sheet-001, row: 0, column: 0, value: 产品名称 } });但直接让 Agent 生成这些命令有个问题Agent 需要知道确切的命令名称和参数格式。如果命令很多Agent 很容易记混。我的做法是封装一层更语义化的接口比如function writeCell(sheetId: string, row: number, col: number, value: any) { return univer.executeCommand({ type: SET_CELL_VALUE, params: { unitId: sheetId, row, column: col, value } }); } function writeRow(sheetId: string, row: number, values: any[]) { values.forEach((value, col) { writeCell(sheetId, row, col, value); }); }这样 Agent 只需要调用writeRow就能写一整行数据不需要关心底层的命令细节。封装层还可以做参数校验、错误处理、批量优化等事情。3.4 批量操作与性能优化Agent 操作文档时往往不是一次只改一个单元格而是批量修改。如果每个单元格都单独发一条命令性能会很差。Univer 支持批量命令你可以把多个操作打包成一个事务univer.executeCommand({ type: BATCH, params: { commands: [ { type: SET_CELL_VALUE, params: { unitId: sheet-001, row: 0, column: 0, value: A } }, { type: SET_CELL_VALUE, params: { unitId: sheet-001, row: 0, column: 1, value: B } }, { type: SET_CELL_VALUE, params: { unitId: sheet-001, row: 0, column: 2, value: C } } ] } });批量命令的好处不只是性能还有原子性。如果批量命令中有一条失败整个事务会回滚不会出现改了一半的尴尬情况。我实测过写 1000 个单元格逐条命令大概需要 800ms批量命令只需要 120ms 左右。差距非常明显。所以只要涉及批量操作一定要用批量命令。注意批量命令的数量不要太大我建议单次不超过 5000 条。太大的批量命令会阻塞主线程导致界面卡顿。如果确实需要写几万个单元格可以分批发送每批之间用requestAnimationFrame或者setTimeout让出主线程。4. 办公运行时的典型应用场景4.1 智能报表生成这是目前最成熟的应用场景。Agent 读取原始数据经过分析处理后自动生成格式化的报表。Univer 在这里的价值是Agent 可以直接操作报表的每一个细节包括表头样式、数据格式、条件格式、图表插入等等。我做过一个销售报表的案例。Agent 从数据库拉取销售数据然后在 Univer 里创建一个新的工作表写入表头填入数据对超过阈值的数值标红最后插入一个柱状图。整个过程 Agent 只需要调用十几个接口代码量不到 200 行。如果不用 Univer光是处理 xlsx 的读写和图表生成代码量至少翻三倍。4.2 文档协同编辑Agent 和人类一起编辑同一份文档这个场景听起来很未来但实际上已经可以实现了。Agent 负责生成初稿、填充数据、检查格式人类负责审核、修改、补充。因为 Univer 有实时协作能力双方的修改能即时同步。这里有个很实用的技巧给 Agent 的操作加上标记。比如 Agent 生成的段落用浅蓝色背景标出来人类一看就知道哪些是 Agent 写的哪些是自己写的。Univer 的样式系统支持这种标记而且标记本身也是文档数据的一部分可以被程序读取和处理。4.3 幻灯片自动排版幻灯片是三种文档类型里最难自动化的因为排版涉及大量的视觉判断。但 Univer 的幻灯片运行时提供了一套布局引擎Agent 可以通过设置约束条件来让引擎自动排版。比如告诉引擎这个文本框放在左上角宽度占页面的 40%高度自适应内容引擎会自动计算具体的位置和尺寸。我试过用 Agent 生成产品介绍幻灯片给它一个产品名称、几个卖点、一张产品图它能自动生成 5 到 8 页的幻灯片排版效果虽然比不上专业设计师但已经达到了能直接用的初稿水平。4.4 数据清洗与格式转换这个场景可能不如前面几个显眼但实际需求很大。很多企业的数据散落在各种 Excel 文件里格式不统一需要清洗和转换。Agent 可以用 Univer 读取这些文件识别数据模式然后按照目标格式重新输出。Univer 在这里的优势是它支持公式和条件格式。Agent 可以生成公式来处理数据而不是把所有数据都拉到内存里用代码处理。比如去重可以用公式数据校验可以用条件格式这样处理出来的结果文件用户打开后还能继续用 Excel 的原生功能编辑。5. 踩坑记录与常见问题排查5.1 Agent 操作文档时的典型错误我在实际项目中遇到过不少问题整理成了一张速查表问题现象可能原因排查方法解决方案命令执行后界面没变化渲染层未触发重绘检查命令是否真的执行成功手动调用重绘或检查命令返回值批量命令部分失败某条命令参数不合法逐条执行定位问题命令在批量前做参数校验大数据量写入卡顿主线程被阻塞用 Performance 面板分析分批写入每批让出主线程单元格样式不生效样式对象格式错误对照文档检查样式字段使用官方提供的样式工具函数协作时操作冲突并发操作未正确处理检查 OT/CRDT 配置确保所有操作都走命令系统5.2 关于 token 消耗的实战经验Agent 操作文档时token 消耗主要来自两个地方读取文档内容和生成操作命令。读取文档内容这块我前面提到了渐进式感知的策略。生成操作命令这块我的经验是尽量让 Agent 生成高层语义的操作而不是底层命令。举个例子。如果让 Agent 直接生成SET_CELL_VALUE命令它需要知道行号、列号、值还要知道 unitId。这些信息对 Agent 来说都是额外的认知负担。但如果封装一个fillColumn(sheetId, columnName, values)的接口Agent 只需要说把这一列填上这些值token 消耗能降低一半以上。另一个技巧是用模板。对于常见的报表格式可以预先定义好模板Agent 只需要填充数据不需要生成格式相关的命令。Univer 支持模板功能你可以把模板存成 JSONAgent 加载模板后直接填数据就行。5.3 类型声明文件的使用心得Univer 是用 TypeScript 写的类型声明文件很完善。但我在使用过程中发现有些类型定义比较深Agent 在生成代码时容易搞混。比如IWorkbookData和IWorksheetData这两个类型前者是工作簿级别的后者是工作表级别的但它们的字段有重叠Agent 有时候会混用。我的做法是在项目里建一个types文件夹把常用的类型重新导出并加上注释。这样 Agent 在生成代码时只需要看这个文件夹里的类型定义不需要去翻 Univer 的源码。这个技巧对任何使用 TypeScript 的 Agent 项目都适用。// types/univer.ts export type { IWorkbookData } from univerjs/core; export type { IWorksheetData } from univerjs/core; // 重新导出并加上业务语义 export interface AgentWorkbook extends IWorkbookData { // Agent 专用的扩展字段 agentId?: string; taskId?: string; }5.4 性能优化的几个关键点Univer 的性能在大多数场景下都够用但有几个地方需要特别注意。第一是避免频繁读取完整快照。每次读取快照都会序列化整个文档数据如果文档很大这个操作很耗时。我建议只在必要时读取快照平时用范围查询。第二是合理设置行列数量。Univer 默认会为每个单元格分配内存如果一开始就设成 10000 行 100 列内存占用会很大。我通常设成实际需要的 1.5 倍然后按需扩展。第三是使用虚拟滚动。Univer 的 UI 层支持虚拟滚动只渲染可视区域内的单元格。这个功能默认是开启的但如果你自定义了渲染逻辑要注意不要破坏虚拟滚动。第四是批量操作要分批。前面提到过单次批量命令不要超过 5000 条。我一般设成 1000 条一批每批之间用setTimeout(fn, 0)让出主线程。6. 办公运行时的未来扩展方向6.1 多 Agent 协作操作同一文档现在 Univer 支持多用户协作但多 Agent 协作还是一个待探索的方向。想象一下一个 Agent 负责数据填充一个 Agent 负责格式美化一个 Agent 负责图表生成它们同时操作同一份文档。这需要更精细的冲突解决策略因为 Agent 之间的操作可能比人类之间的操作更频繁、更密集。我试过一个简单的多 Agent 场景两个 Agent 分别往同一个表格的不同区域写数据。因为 Univer 的协作层能正确处理并发操作所以没有出现数据覆盖的问题。但如果两个 Agent 操作同一个单元格就需要额外的协调机制了。我的做法是给每个 Agent 分配独立的操作区域避免直接冲突。6.2 与外部数据源的深度集成办公文档往往不是孤立的它需要和数据库、API、文件系统等外部数据源交互。Univer 的运行时可以扩展出数据连接器让 Agent 能够直接从外部数据源拉取数据填充到文档里或者把文档里的数据推送到外部系统。这个方向的想象空间很大。比如 Agent 可以定时从数据库拉取最新数据更新报表然后通过邮件发送给相关人员。整个过程不需要人工干预Univer 作为文档运行时负责数据的呈现和格式化。6.3 文档操作的版本控制Univer 的操作历史记录功能可以扩展成完整的版本控制系统。每次 Agent 或用户的操作都被记录下来可以随时回滚到任意历史版本。这对于需要审计的场景特别有用比如财务报表的生成过程需要留痕。我目前的做法是利用 Univer 的命令历史定期把快照存到数据库里。这样既能回滚又不会占用太多内存。如果要做更精细的版本控制可以考虑把每个命令都持久化然后通过重放命令来重建任意版本的状态。6.4 跨文档类型的联动表格、文档、幻灯片三种文档类型目前是相对独立的但在实际工作中它们经常需要联动。比如表格里的数据更新了文档里的引用也要跟着更新幻灯片里的图表也要重新生成。Univer 的统一文档模型为这种联动提供了基础但还需要在上层做更多的工作。我试过一个简单的联动场景表格里的某个数值变化时自动更新文档里对应的段落文字。实现方式是在表格的命令执行后触发一个事件事件处理器去查找文档里需要更新的位置然后执行文档命令。这个模式可以扩展到更复杂的场景比如表格数据变化触发幻灯片图表更新。6.5 Agent 技能Skill的标准化现在每个 Agent 项目都在自己定义怎么操作文档缺乏统一的标准。如果 Univer 能定义一套标准的 Agent 技能接口比如读取表格数据、写入表格数据、生成图表、格式化文档等等那么不同 Agent 框架之间的互操作性会大大增强。我个人的判断是这个标准化过程不会由某一家公司单独完成而是会在社区的共同推动下逐渐形成。Univer 作为开源项目有机会成为这个标准的基础。对于 Agent 开发者来说关注 Univer 的技能接口设计参与社区讨论是一个值得投入的方向。我在实际项目里用 Univer 做 Agent 办公自动化已经有一段时间了最大的体会是办公运行时的价值不在于它支持多少种文档格式而在于它把文档操作抽象成了 Agent 能理解的语义。以前 Agent 要操作 Excel得先学 openpyxl 或者 xlsx 的 API现在只需要学 Univer 的命令系统。这个抽象层的价值随着 Agent 应用越来越复杂会越来越明显。如果你也在做类似的事情我的建议是先从表格场景入手因为表格的数据结构最清晰Agent 最容易上手。等表格场景跑通了再扩展到文档和幻灯片。另外一定要重视类型定义和接口封装这两件事做得好Agent 的生成质量和稳定性会有质的提升。