
字节笔记本
2026年10月6日 · 约 5 分钟读完
DeepSeek Harness 附件接缝:图片不进会话日志
DeepSeek Harness(dsh)是 DeepSeek 开源的 agent 框架,一切皆插件,底层由 Cordis 驱动,MIT 协议,目前处于开发者预览阶段。它的仓库 docs/subsystems 目录里,每个文件讲一个子系统的设计约定。attachment 是其中篇幅最短的几篇之一,讲的却是所有多模态 agent 都绕不开的问题:用户拖进来的图、模型生成的图,到底应该存在哪。

为什么图片不能直接进会话日志
把图片塞进会话日志,常见做法有四种:转成 base64 直接写进事件流;存一个宿主机上的临时文件路径;存浏览器的 object URL;存模型服务商返回的临时 URL。attachment 文档开篇就把这四条路全部堵死:会话事件和模型可见的 ImageBlock 里,只允许出现内容寻址引用和元数据,浏览器 object URL、宿主临时路径、provider URL、base64 载荷一概不许出现。
道理不复杂。base64 会让日志体积膨胀数倍,重放历史时全是无意义的大字段;临时路径和 object URL 的生命周期都短于会话本身,恢复会话、分叉会话时必然失效;provider URL 则把用户数据留在了第三方手里,过期之后同样读不回来。会话日志要的是可以长期重放的确定性记录,图片字节这种大而不稳定的负载必须挪出去,attachment 接缝因此把二进制图片的所有权从会话日志中整体剥离。
先落盘,再追加事件
这个子系统的核心约束是一条顺序规则:先持久化,后入库。宿主一旦接受用户消息,消息里的图片必须先落到 <DSH_HOME>/attachments/v1 目录之下,然后这条用户事件才允许追加进日志;模型的结构化图片输出也遵循同样的规则。
规则只留了一个口子:浏览器里还没点发送的草稿可以留在内存里,原生客户端可以先放在操作系统的临时目录暂存。这些属于未提交状态,一旦消息被接受,就立即转入先落盘的流程。
不透明引用:能存,不能用
图片落盘之后,服务返回一个 AttachmentId。它是带品牌标记的不透明字符串,本地后端目前生成 sha256:<digest> 这种形式,但文档反复强调:消费方不许解析这个字符串的内部结构,也不许从它推导文件系统路径。内容寻址的格式属于实现细节,哪天换成别的摘要算法,所有客户端代码都不需要改。
伴随引用一起持久化的还有一份元数据 ImageAttachmentRef:从存储字节验证出的 mediaType、精确字节数、图片的原始宽高,外加一个去掉了本地路径信息的可选显示名。记录宽高和字节数的目的,是让客户端不解码图片就能排版历史记录;但元数据只是加速读取的缓存,每一次权威读取仍然要重新核对摘要、媒体签名、尺寸和元数据是否与对象一致。
三个入口:先验证、再提交、后读回
附件服务通过 ctx.attachments 暴露三个方法,分工非常清楚。
validateImage 只做准入检查:把编码字节完整解码,核对调用方声明的媒体类型,检查大小、尺寸和像素上限,全程不落盘。批量上传的场景有一条专门的要求:先把每个成员都 validate 一遍,全部通过之后再逐一 saveImage,这样任何一项被拒绝时都不会留下半成品对象。
saveImage 执行同样的检查,通过后原子提交,在所属会话事件追加之前返回持久引用。readImage 则从授权的会话路径拿到引用,完整性校验通过才返回字节,校验失败直接抛存储错误,同时支持用 AbortSignal 中途取消。
这一层共用一套部署侧配置 ImageAttachmentLimits:单图字节上限、单条消息图片数量上限、单条消息图片总字节上限、像素总量上限,以及允许的格式列表 image/png、image/jpeg、image/webp、image/gif。

回收为什么故意延后
文档里有一个容易被忽略的表态:附件服务对留存策略保持中性。因为恢复出来的会话和分叉出来的会话可能共享同一个图片对象,引用感知的垃圾回收被明确推迟,而不是绑定在某个会话的删除动作上。换句话说,删除一个会话不会顺手删掉它引用过的图片,哪些对象已经没有任何引用指向,需要一套独立的扫描机制来回答。
值得抄走的三点
即使不基于 dsh 做开发,这篇短文档的设计取舍也值得借鉴。其一,大二进制永远不进事件流,日志里只放引用,这是会话可恢复、可分叉的前提。其二,先持久化再入库的顺序约束写在接口契约里,由服务端保证,而不是指望调用方自觉。其三,元数据可以冗余存放来换取排版速度,但权威读取必须逐项复核,信任落盘的字节而不是落盘的说明。



