Appearance
这一篇写我自己做的一个项目:给我们的 AIPPT 产品做一个"聊天改 PPT"的 Agent——用户不用手动点来点去,直接说「把第二页标题改成'年度总结'」「这页太空了,加个三行的表格」,PPT 就自动改好。
听起来是个"自然语言转 JSON"的活儿(我们的 PPT 底层就是一份 JSON,前端靠它渲染,增删页、改文本/图片/表格本质都是在改这份 JSON)。但真上手你会发现,"让大模型把话变成 JSON"这句话里藏着一大堆坑。这篇就复盘我怎么设计、怎么踩坑、又怎么爬出来的。我觉得这套思路是这个项目最值得讲的地方。
先交代背景:我们的 PPT 是一份 JSON
我们整个编辑器是数据驱动的。一份 PPT 大致长这样(简化):
json
{
"slides": [
{
"id": "slide_01",
"elements": [
{ "id": "el_a1", "type": "text", "content": "季度回顾", "fontSize": 40 },
{ "id": "el_a2", "type": "image", "src": "https://.../a.png" }
]
}
]
}前端拿到这份 JSON 就能渲染出页面,用户的每个手动操作(改字、换图、加页)本质都是在改它。所以「聊天改 PPT」= 让 Agent 也能安全、精确地改这份 JSON。目标很明确:说人话,改对地方,别改坏。
我的第一版设计——以及它是怎么翻车的
最直觉的做法,也是我一开始干的:把整份 PPT 的 JSON 塞给模型,让它输出"修改后的完整 JSON"。 我很快就被三个问题按在地上摩擦:
一是又慢又贵。 一份几十页的 PPT,JSON 好几万 token。用户改一个字,我要把整份发进去、再让模型整份吐出来——输入输出双向烧钱,还慢得让人想砸键盘。
二是"保真漂移"。 这是最阴险的。我只想改第二页的标题,模型却把第五页一张图的 URL 悄悄改了、把某个字体大小从 40 变成了 39。因为它是在"重写"整份文档,任何它没注意到的地方都可能被无意改动。用户的 PPT 在你看不见的地方悄悄变形,这是灾难。
三是一个括号就全崩。 让模型生成几万 token 的 JSON,只要中间吐错一个逗号、少一个括号,整份 JSON.parse 直接失败,这次修改全废。文档越大,崩的概率越高。
盯着这三个问题我想了很久,最后想通了一件事,也是整个项目的转折点。
核心洞见:别让模型"生成文档",让它"发出操作"
用户要的从来不是"一份新文档",而是"对现有文档的一个改动"。 那我为什么要让模型重新生成整份 JSON?我应该让它只产出要做什么改动——也就是一串编辑操作(edit operations)。
打个比方:改一篇文章,笨办法是把整篇重抄一遍(还容易抄错、改乱别的段落);聪明办法是发一条批注——「第 2 段第 1 句,改成 XXX」。我要的就是让模型发批注,而不是重抄全文。
这个视角一转,前面三个问题全解了:模型只输出一小段操作(token 极省);它碰不到没提及的元素(保真,不漂移);操作短小、结构固定,还能逐条校验(不怕崩)。
于是整个系统的设计,就围绕"自然语言 → 一串被严格约束的编辑操作 → 安全地应用到 JSON"来展开。
设计一:把"编辑操作"定义成一套协议,用 function calling 产出
我先设计了一套操作协议——把用户可能做的所有改动,抽象成有限的几种带严格结构的操作,比如:
json
[
{ "op": "updateText", "elementId": "el_a1", "content": "年度总结" },
{ "op": "updateImage", "elementId": "el_a2", "src": "https://.../new.png" },
{ "op": "addSlide", "afterSlideId": "slide_01", "layout": "title-content" },
{ "op": "deleteElement","elementId": "el_a2" },
{ "op": "updateStyle", "elementId": "el_a1", "patch": { "fontSize": 48 } }
]然后,我把每一种操作都做成一个带 JSON Schema 的工具(tool),让模型通过 function calling 来"调用"这些操作,而不是自由发挥吐 JSON。这一步很关键:function calling + schema 约束,把模型的输出从"一段可能出错的文本"变成了"结构受控的操作"。 模型只能在我给定的操作和字段里选,天然就规范多了。
这套操作协议,我把它当成整个系统的契约层:前端认它、后端认它、模型也围着它转。三方解耦,各自演进互不影响——这也是后来加新功能(比如加"动画"操作)时改动很小的原因。
设计二:模型怎么知道"第二页的标题"是哪个元素——寻址
有了操作协议,下一个硬骨头是定位:用户说「第二页的标题」,模型得把它对应到 elementId: "el_a1" 才能操作。这里我踩过一个坑:一开始让模型按"第几页第几个元素"来定位,页面一多、顺序一变就错位,改错元素。
我的解法是两条:
① 每个元素有稳定、唯一的 ID。 不靠位置,靠 ID。这样"标题"这个概念始终能被精确指到某个 el_xxx,不受增删顺序影响。
② 不把完整 JSON 喂给模型,而是喂一份"精简的、带 ID 的大纲"。 模型不需要知道每个元素的每个样式细节,它只需要一张"地图"来定位:
json
{
"slide_01": [
{ "id": "el_a1", "type": "text", "role": "title", "preview": "季度回顾" },
{ "id": "el_a2", "type": "image", "preview": "折线图" }
]
}模型看着这份精简大纲,就能判断"第二页的标题 = el_a1",然后发出针对 el_a1 的操作。大纲比完整 JSON 小一两个数量级,又保留了定位所需的全部信息——省 token 又准。PPT 特别大时,我还会先按用户指令只挑相关的页放进大纲(把 RAG 里"只喂相关上下文"那套思想搬了过来),避免上下文爆炸。
设计三:模型永远不直接碰文档——校验层 + 自我修正
我给自己立了一条铁律:模型是"提议者",后端才是唯一的"写入权威"。 模型发出的每一条操作,在真正改动 JSON 之前,都要过一道校验关:
- 结构校验:字段齐不齐、类型对不对(JSON Schema 兜底)。
- 存在性校验:
elementId是不是真存在?(模型偶尔会编一个不存在的 ID。) - 业务规则校验:字号在合理范围吗?图片 URL 合法吗?删的是不是最后一页(不能删空)?
校验不过的操作,我不是直接丢弃,而是把错误信息回喂给模型,让它自我修正、重试一次(比如"el_a9 不存在,请从大纲里重新选")。这套"提议 → 校验 → 失败反馈 → 修正"的小闭环,让系统几乎不会把坏操作应用到用户的文档上。这条"模型只提议、代码来把关"的边界,是我觉得整个设计里最重要的一条。
设计四:拿不准就问、要动大手术就先给预览
用户的话经常是模糊的。「把那个图换掉」——哪个图?「改一下颜色」——改成啥色?早期我让模型"尽力猜",结果经常改错,用户体验很差。后来我加了两条:
歧义澄清:当指代不明确、或有多个候选时,Agent 不猜,而是反问一句「你是指第 3 页的折线图,还是第 5 页的柱状图?」。宁可多问一句,也别改错。
破坏性操作先预览确认:像"删除整页""清空这一页"这种不可逆的操作,我让 Agent 先回一句"我打算删掉第 4 页(标题:竞品分析),确认吗?",用户点确认才真正执行。这其实就是 human-in-the-loop——高风险操作不让 AI 自己拍板。配合操作流天然可逆的特性,我还做了撤销栈,每一步都能一键撤回。
设计五:连续对话里的"它"——多轮上下文
真实对话是这样的:「把标题字大点」→「再大一点」→「颜色换成品牌蓝」。后面这两句全靠上文才知道在说谁。所以我维护了一个会话上下文,记住"当前选中/上次操作的元素"。当用户说"再大一点",Agent 知道这个"它"指的还是刚才那个标题。没有这层记忆,多轮编辑就是一句一断,完全不像"助手"。
设计六:让模型决策,让代码执行
还有个提升稳定性的心法:凡是能用确定性代码算的,就别让模型算。 比如「把所有页的标题都放大 2 号」——我不会让模型逐个去改每个标题的字号(它可能漏、可能算错),而是让模型发出一个高层意图操作 bumpAllTitles(+2),具体"找出所有标题、逐个 +2"由我的代码确定性地执行。
模型负责"听懂意图、选对操作",代码负责"精确、可靠地执行"。 这条分工让批量操作又稳又快,也大幅减少了模型出错的面。
整体流水线
把上面几块串起来,一次"聊天改 PPT"是这样跑的:
我是怎么衡量它好不好的
Agent 这东西,不量化就没法迭代。我攒了一个评测集:几百条「用户指令 + 期望产生的操作(或期望的结果状态)」。每次改提示词、调大纲格式、加新操作,我都跑一遍,看指令解析的准确率(有没有选对操作、定位对元素)涨了还是跌了。这套回归让我敢放心地改系统,而不是每次都提心吊胆手动点几下试试。
踩过的坑(血泪总结)
- 别让模型吐整份文档。 这是我最大的弯路。token 爆炸、保真漂移、一个括号全崩——换成"输出操作"后三个问题一起消失。
- 定位别靠"第几个",要靠稳定 ID。 顺序一变就错位,还特别难查。
- 别把完整 JSON 全喂进去。 精简带 ID 的大纲,省钱又更准;大文档再叠一层"只放相关页"。
- 模型一定会偶尔编字段、编 ID。 所以校验层不是可选项,是生命线;校验失败回喂让它自我修正,比直接报错体验好得多。
- 模糊指令别猜。 宁可反问一句。用户不怕你问,怕你把 PPT 改乱。
- 不可逆操作一定要预览确认 + 撤销。 删错一页用户能记恨你很久。
- 多轮的"它/再/换"很依赖上下文。 记住"当前操作对象",体验立刻从"复读机"变"助手"。
- 能用代码确定性执行的,别让模型算。 模型管意图,代码管执行,稳。
一句话收尾
这个项目让我收获最大的,不是接了个大模型,而是想通了一个视角:把"让 AI 生成结果"重构成"让 AI 发出被严格约束的操作,再由可信的代码去执行"。 一旦守住"模型只提议、代码是唯一写入权威"这条边界,可控性、成本、安全、体验,反而全都拿到了。这套思路我觉得不止适用于 PPT——任何"用自然语言操作一个结构化文档/系统"的场景,都能套。