ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Univer 办公运行时:把表格、文档、幻灯片装进 AI Agent 的运行时

Univer 办公运行时:把表格、文档、幻灯片装进 AI Agent 的运行时

1. 为什么“办公运行时”是 Agent 落地的下一个关键战场

1.1 从“会聊天”到“会干活”的鸿沟

过去一年,我接触过不少做 AI Agent 的团队,大家普遍卡在同一个地方:模型能写出一段漂亮的 Python 脚本,能回答“如何做季度汇报”,但真让它打开一个表格、改几个单元格、生成一份带图表的 PPT,立刻就露怯。原因不复杂——大语言模型擅长处理文本 token,而办公场景的核心载体是结构化文档对象:单元格有坐标、公式有依赖关系、幻灯片有图层和母版。这两者之间隔着一层厚厚的“表示鸿沟”。

Agent 要真正在办公场景里干活,必须有一个东西把“文档对象模型”翻译成模型能理解、能操作的形式,再把模型的操作意图翻译回文档的变更指令。这个中间层,就是最近被反复提及的Office Harness,或者更准确地说,是办公运行时。

1.2 univer 切入的到底是什么赛道

Univer 这个项目,早期我对它的印象是“开源版的在线表格”,对标的是表格协作那套东西。但最近重新看它的架构演进,发现它的野心明显不止于此。它把表格、文档、幻灯片三种文档类型统一到一套TypeScript内核之上,对外暴露的 API 是面向对象的、可编程的、细粒度到单元格和文本段的。这意味着什么?意味着一个 AI Agent 不需要去模拟鼠标点击,不需要去解析 HTML 的 DOM 树,它可以直接调用运行时的 API 来读写文档状态。

这就是“把表格、文档、幻灯片全装进 AI Agent 的运行时”这句话的真正含义。Univer 不再只是一个给人用的编辑器,它正在变成一个给 Agent 用的文档操作底座。Agent 在它的运行时里,像操作内存对象一样操作办公文档,执行完再序列化回标准格式。这个定位,恰好踩在了 Agent 从“演示”走向“生产”的必经之路上。

1.3 谁适合关注这个方向

如果你在做 Agent 应用开发,尤其是涉及报表生成、合同起草、数据整理、演示文稿自动化这类场景,那办公运行时的选型会直接决定你的 Agent 能不能落地。如果你是一个 TypeScript 开发者,想切入 AI Agent 赛道但不知道从哪下手,文档运行时这个方向的技术栈匹配度很高——它需要的是扎实的前端工程能力加上对 Agent 编排的理解,而不是纯算法背景。哪怕你只是对“Agent 到底怎么操作办公软件”这件事好奇,理解运行时的设计思路也能帮你判断市面上各种办公 Agent 产品的真实水平。

2. 拆解 univer 的核心设计:为什么是 TypeScript,为什么是运行时

2.1 文档模型与 Agent 操作接口的分离设计

Univer 最核心的设计决策,是把文档数据模型和渲染层彻底分开。表格的单元格数据、公式、样式、合并信息,全部存在一个可序列化的数据结构里,渲染层只负责把这个数据结构画出来。这个设计在传统编辑器里是为了支持协同和撤销重做,但在 Agent 场景下,它产生了一个意想不到的价值:Agent 可以直接操作数据模型,完全绕过 UI 层。

我实测过,通过 Univer 的 Facade API,你可以用类似worksheet.getRange('A1').setValue(100)这样的调用直接改数据,改完之后触发一次重渲染,界面上立刻更新。整个过程不需要模拟任何用户输入事件。对于 Agent 来说,这就是最理想的交互方式——确定性的 API 调用,而不是概率性的 UI 操作。

2.2 TypeScript 在 Agent 运行时里的真实价值

很多人看到 TypeScript 第一反应是“类型安全”,但在 Agent 运行时这个场景里,TS 的价值远不止于此。Agent 调用工具时,最怕的是参数传错、返回值结构对不上。Univer 用 TypeScript 写的 API 层,天然带有完整的类型定义文件(.d.ts),这意味着 Agent 框架在做工具注册时,可以直接从类型定义里提取参数 schema,不需要人工写一遍 JSON Schema。

举个例子,Univer 的FRange类型定义了 startRow、startColumn、endRow、endColumn 四个必填字段,Agent 在生成调用参数时,如果漏了任何一个,TypeScript 编译期就会报错。这在 Agent 开发里非常关键——把运行时的参数校验前移到开发期,能省掉大量调试时间。另外,TS 的接口继承机制让 Univer 可以定义一套基础文档接口,表格、文档、幻灯片各自继承扩展,Agent 侧只需要面向基础接口编程,就能统一处理三种文档类型。

2.3 运行时与 Harness 的区别:别把两者混为一谈

热词里有个词叫“harness和agent区别”,这里正好可以借 Univer 说清楚。Agent 是决策者,它决定“要做什么”;Harness 是执行环境,它提供“能做什么”的能力集合;运行时是 Harness 的底层支撑,它保证“做的时候不出错”。Univer 扮演的是运行时加部分 Harness 的角色——它提供了文档操作的能力接口,也管理着文档的状态和生命周期,但具体“先改哪个单元格、再生成什么图表”的决策,仍然由 Agent 来做。

这个分层很重要。我见过一些团队把决策逻辑和执行逻辑混在一起写,结果 Agent 稍微换个任务就要重写大量代码。正确的做法是:Agent 侧只负责生成操作序列,运行时侧保证每个操作原子性执行、可回滚、可序列化。Univer 的事务机制恰好支持这一点,一批操作可以打包成一个事务提交,失败则整体回滚。

3. 实操:把 univer 接入 Agent 工作流的完整路径

3.1 环境搭建与最小可运行示例

先走一遍基础流程。假设你已经有 Node.js 18+ 环境,创建一个新目录,初始化项目:

mkdir univer-agent-demo && cd univer-agent-demo npm init -y npm install @univerjs/core @univerjs/sheets @univerjs/sheets-ui @univerjs/design npm install -D typescript ts-node @types/node

然后创建一个最小的表格实例。这里我选择用 Univer 的 Facade 模式,因为它对 Agent 最友好——所有操作都通过一个统一的 API 入口:

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, }); univer.registerPlugin(UniverSheetsPlugin); univer.registerPlugin(UniverSheetsUIPlugin); const workbook = univer.createUniverSheet({ id: 'agent-demo', sheetOrder: ['sheet1'], sheets: { sheet1: { id: 'sheet1', name: '数据表', rowCount: 100, columnCount: 20, cellData: { 0: { 0: { v: '产品名称' }, 1: { v: '销量' } }, 1: { 0: { v: 'A款' }, 1: { v: 1200 } }, }, }, }, });

这段代码跑起来之后,你就有了一个内存中的表格对象。Agent 后续的所有操作,都围绕这个workbook实例展开。

3.2 用 Facade API 封装 Agent 可调用的工具集

Agent 不会直接写 TypeScript 代码,它需要的是工具描述。我的做法是把常用的文档操作封装成一组工具函数,每个函数有明确的输入输出类型,然后用 Agent 框架的工具注册机制暴露出去。以下是我实际项目里用的几个核心工具:

import { IWorkbookData, FRange } from '@univerjs/core'; // 工具1:读取指定区域的数据 function readRange(workbook: IWorkbookData, sheetId: string, range: FRange) { const sheet = workbook.sheets[sheetId]; const result: any[][] = []; for (let r = range.startRow; r <= range.endRow; r++) { const row: any[] = []; for (let c = range.startColumn; c <= range.endColumn; c++) { row.push(sheet.cellData?.[r]?.[c]?.v ?? null); } result.push(row); } return result; } // 工具2:批量写入数据 function writeRange(workbook: IWorkbookData, sheetId: string, startRow: number, startColumn: number, values: any[][]) { const sheet = workbook.sheets[sheetId]; if (!sheet.cellData) sheet.cellData = {}; values.forEach((row, ri) => { if (!sheet.cellData![startRow + ri]) sheet.cellData![startRow + ri] = {}; row.forEach((val, ci) => { sheet.cellData![startRow + ri][startColumn + ci] = { v: val }; }); }); return { success: true, rowsWritten: values.length }; } // 工具3:设置公式 function setFormula(workbook: IWorkbookData, sheetId: string, row: number, col: number, formula: string) { const sheet = workbook.sheets[sheetId]; if (!sheet.cellData) sheet.cellData = {}; if (!sheet.cellData[row]) sheet.cellData[row] = {}; sheet.cellData[row][col] = { f: formula }; return { success: true }; }

这三个工具覆盖了 Agent 在表格场景下 80% 的操作需求。注意每个工具都返回结构化的结果,而不是抛异常——Agent 需要根据返回值判断下一步动作,异常会打断它的推理链。

3.3 参数计算与范围选择:一个真实的数据汇总案例

假设 Agent 收到的任务是“把 A 款和 B 款的销量汇总,写到第三行”。它需要先读取数据,计算汇总值,再写入。这个过程涉及几个关键参数的计算:

  • 读取范围:从第 2 行到第 N 行(N 由 Agent 通过读取总行数确定)
  • 汇总列:第 2 列(索引为 1)
  • 写入位置:第 3 行第 2 列

Agent 生成的工具调用序列大致是这样的:

[ { "tool": "readRange", "args": { "sheetId": "sheet1", "range": { "startRow": 1, "startColumn": 0, "endRow": 2, "endColumn": 1 } } }, { "tool": "writeRange", "args": { "sheetId": "sheet1", "startRow": 3, "startColumn": 0, "values": [["合计", 2700]] } } ]

这里有个细节值得注意:Agent 在生成endRow时,需要知道表格实际有多少行数据。如果它盲目传一个很大的值,读取工具会返回大量空值,浪费 token。我的经验是,在工具实现里加一个“自动截断”逻辑——遇到连续三行全空就停止读取,这样既安全又高效。

3.4 事务与回滚:保证 Agent 操作的可恢复性

Agent 操作文档最怕的是“改了一半出错”。比如它先删了旧数据,准备写新数据时模型调用超时了,文档就处于一个不一致的状态。Univer 的事务机制可以解决这个问题。我把一批相关的写操作包在一个事务里:

function executeTransaction(workbook: IWorkbookData, operations: Array<() => void>) { const snapshot = JSON.parse(JSON.stringify(workbook)); try { operations.forEach(op => op()); return { success: true }; } catch (e) { Object.assign(workbook, snapshot); return { success: false, error: String(e) }; } }

这个快照回滚的方案虽然简单,但在实际项目里非常管用。Agent 的每一步操作都通过事务包装,失败就整体回退,然后 Agent 可以重新规划。实测下来,这比让 Agent 自己去处理部分失败的情况要可靠得多。

4. 常见问题与排查技巧实录

4.1 Agent 调用工具时参数类型不匹配

这是最高频的问题。Agent 生成的 JSON 里,数字经常被写成字符串,比如"startRow": "1"而不是"startRow": 1。Univer 的 API 对类型敏感,传字符串进去可能不报错但行为异常。我的解决办法是在工具入口做一层类型强制转换:

function normalizeRange(raw: any): FRange { return { startRow: Number(raw.startRow), startColumn: Number(raw.startColumn), endRow: Number(raw.endRow), endColumn: Number(raw.endColumn), }; }

同时在工具描述里明确标注每个参数的类型,Agent 框架通常会把类型信息注入到提示词里,能显著降低这类错误。

4.2 公式计算结果不更新

Univer 的公式计算是异步的,写入公式后立刻读取单元格,拿到的可能是旧值或空值。我踩过这个坑:Agent 写完 SUM 公式后马上读取结果,发现是 undefined,然后它以为公式没生效,又写了一遍,导致数据错乱。正确的做法是在写入公式后,等待一个计算周期再读取,或者在工具返回值里明确告诉 Agent“公式已写入,结果将在下次读取时可用”。

4.3 大数据量下的性能瓶颈

当表格行数超过一万行时,逐单元格读写会明显变慢。我的优化方案是批量操作——Univer 的setValue支持传入二维数组,一次调用写入整个区域,比循环单格写入快一个数量级。另外,Agent 在读取时尽量只读需要的列,不要全表扫描。如果确实需要全量数据,考虑先导出为 CSV 再让 Agent 处理,处理完再批量写回。

4.4 常见问题速查表

问题现象可能原因排查方向解决手段
工具调用返回 undefined参数类型错误检查 Agent 生成的 JSON 类型入口做类型转换
写入后读取不到数据异步计算未完成确认是否涉及公式延迟读取或返回提示
大批量操作超时逐格操作开销大统计操作次数改为批量 API
文档状态不一致部分操作失败检查是否有事务保护加快照回滚
Agent 反复操作同一区域工具返回值不明确检查返回结构返回操作摘要和当前状态

4.5 一个容易被忽略的坑:Sheet ID 的稳定性

Agent 在多轮对话中可能会引用之前创建的 sheet,但如果中间发生了 sheet 重命名或删除,ID 可能变化。我的做法是在工具层维护一个“逻辑名称到物理 ID”的映射,Agent 始终用逻辑名称操作,运行时负责解析。这样即使底层 ID 变了,Agent 的上下文也不会断裂。

5. 办公运行时的扩展方向与个人实践体会

5.1 从表格扩展到文档和幻灯片

Univer 的文档和幻灯片模块目前成熟度不如表格,但架构思路是一致的。文档场景下,Agent 需要操作的是段落和文本段,API 粒度更细;幻灯片场景下,Agent 需要操作的是页面和元素,涉及布局计算。我目前的做法是先在表格场景把 Agent 工作流跑通,沉淀出一套工具封装模式,再逐步迁移到文档和幻灯片。三种文档类型共享同一套事务和快照机制,迁移成本比想象中低。

5.2 多 Agent 协作下的文档锁

当多个 Agent 同时操作同一份文档时,需要引入锁机制。我的方案是在运行时层加一个简单的乐观锁:每个 Agent 操作前先读取文档版本号,提交时检查版本号是否变化,变了就拒绝并让 Agent 重新读取。这个机制在 Univer 的数据模型上很容易实现,因为整个文档状态是可序列化的,版本号就是一个递增整数。

5.3 我个人的几条实操建议

第一,不要试图让 Agent 直接生成 Univer 的底层数据结构。我试过让模型输出完整的 cellData JSON,结果格式错误率极高。正确的做法是提供高层工具函数,Agent 只负责填参数。

第二,工具的数量要克制。我一开始封装了二十多个工具,结果 Agent 选择困难,经常调错。后来精简到八个核心工具,准确率明显提升。工具描述要写清楚“什么时候用这个工具”,而不是只写“这个工具能做什么”。

第三,给 Agent 提供文档的“当前状态摘要”。每次工具调用返回时,附带一个简短的文档状态描述,比如“当前表格有 5 行数据,A 列是产品名,B 列是销量”。这样 Agent 不需要反复读取就能知道上下文,省 token 也更稳定。

第四,日志要记全。Agent 的每一次工具调用、参数、返回值、耗时,全部落盘。出问题的时候,回放日志比重新跑一遍快得多。我用一个简单的 JSONL 文件记录,每行一条,排查效率很高。

这套东西目前还在快速迭代中,Univer 的 API 也在变。但核心思路是稳定的:把文档操作抽象成确定性的工具,让 Agent 做它擅长的决策,让运行时做它擅长的执行。这个分工一旦理顺,办公场景的 Agent 落地就会顺畅很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进