Agent 一接实时协作文档就开始互相覆盖:从 Presence Lease 到 Block-Level Commit 的工程实战
很多团队把 Agent 接进实时协作文档后,最先暴露的问题不是不会写,而是会把别人刚改完的内容整块盖掉。页面上明明能看到光标和提示,自动化一落笔,旧段落还是被整段回写。问题不在模型理解差,而在执行链路把“拿到当前 DOM”当成“拿到编辑权”。⚠️
协作文档的真实状态并不只在屏幕。一个段落可能已被同事改写,但本地缓存还没刷新;另一个 Agent 可能拿着旧 selection 继续提交。此时如果系统仍按整页 diff 或整段 replace 执行,覆盖事故只会从人工误操作变成自动化误操作。🧭
问题不在改字,而在谁有资格提交
实时协作文档最容易踩的坑,是把 presence 只当 UI 信息。光标在线,不等于当前 Agent 对目标块拥有可提交窗口。没有 lease,写入就可能基于旧快照。📌
另一个误区是直接对整篇文档做 patch。这样实现快,但一旦双方分别改了不同块,系统仍会把无关区域一起带回,造成连带伤害。🧨
更稳的做法,是先给目标块发放短时 Presence Lease,再把提交粒度收窄到 block。只有 lease 未过期、块版本未变化、selection 仍命中原锚点时,写入才允许落地。🔒
一套能落地的三段式防线
第一层是Presence Lease。系统在 Agent 决定修改某个块时,不是立刻提交,而是先记录doc_id、block_id、holder、expires_at和base_version。这样每次提交都能回答:写入是否仍基于同一份世界状态。✅
第二层是Block-Level Commit。提交对象只允许是目标块及其最小上下文,而不是整页 HTML。块级提交让冲突域明显缩小,失败时也只撤销该块,不影响旁边段落、图片和评论流。🧱
第三层是Commit 前回放校验。真正发送 patch 前,系统再抓一次当前块文本与版本,校验 anchor、hash 和编辑人 presence 是否一致。只要其中一项漂移,就转成 rebase 或人工确认,而不是硬写。🛡️
fromdataclassesimportdataclassfromtimeimporttime@dataclassclassLease:block_id:strholder:strbase_version:intexpires_at:floatdefcan_commit(lease:Lease,live_version:int,live_anchor:str,expected_anchor:str)->bool:iftime()>lease.expires_at:returnFalseiflive_version!=lease.base_version:returnFalsereturnlive_anchor==expected_anchor| 方案 | 提交粒度 | 冲突影响面 | 回滚成本 | 适用场景 |
|---|---|---|---|---|
| 整页替换 | document | 极大 | 高 | 单人离线整理 |
| 段落替换 | section | 中 | 中 | 低频协作 |
| 块级提交 + lease | block | 小 | 低 | 多人实时协作、Agent 自动写入 |
实战验证:冲突率下降,不靠更强模型
在一个会议纪要自动回填链路里,团队把“整段 replace + 固定重试”改成“lease + block commit + 回放校验”后,最直接的变化不是生成质量提升,而是覆盖事故从高频偶发变成可拦截异常。旧方案里,只要人工在 3 秒内改过相邻段落,Agent 就可能把旧内容写回;新方案里,这类请求会先被判为版本漂移,转入重抓和重算。📉
更关键的是,失败路径终于可观测。系统能区分 lease 过期、锚点失效、块版本变化和权限抢占,而不是统一报“保存失败”。很多事故不是模型胡写,而是提交前世界已经变了。🔍
真正要治理的是协作语义
笔者认为,协作文档里的 Agent 不是普通输入法,而更像一个带副作用的协作者。系统设计重点不该放在“它能写多快”,而该放在“它何时有资格写、写哪一块、写前如何再确认一次”。如果这三件事没有工程护栏,再强的模型也会在多人协作里放大事故。🤝
未来 3 到 6 个月,这类系统会从“整页生成”走向“块级事务化编辑”。真正有价值的方向,不是让 Agent 更频繁地下笔,而是让它像数据库事务一样,只在拿到正确上下文、租约和版本时提交。你在协作文档自动化里,更难接受的是覆盖冲突,还是人工确认过多?欢迎交流。🚀
