第 1 卷
张嘉熙 · DeepSeek Harness 前沿工程实践



你好,我是张嘉熙。
核心篇讲到这里,能力、工具和会话都齐了——只差最后一条边界没划:模型眼前那份上下文,到底该装什么、又该把什么留在外面?
其中最占地方的是两类:工具吐出的大段输出、用户拖进来的图片。装不下之后怎么腾地方, 第 10 讲我们聊过了,本讲看的则是更前面一步——该不该让它们直接整份进去:
工具一次吐出一大段文本,该整份进上下文吗?
用户拖进来的一张截图,也该整份进上下文吗?
这两类,dsh 给它们各开了一条外置通道——下面两节各讲一条。
(@deepseek-ai/dsh-spill / -local / -policy)此前只写在依赖里,本讲才第一次真正用上。
两点提醒:本项目用 --legacy-peer-deps 安装,peer 依赖必须显式写进 package.json,漏了就报 ERR_MODULE_NOT_FOUND;附件后端还要用到原生模块
sharp(装的是预编译包,不用本地编译)。
关于示例:npm run start:ch12:spill 跑溢出存储(超大的工具输出怎么被换成预览和指引),npm run start:ch12:attachment 跑图片附件(存进去、看字节落在哪、
看日志里留下什么、再取回字节),npm run start:ch12:admission 跑准入(超限和不合规怎么被拒)。
工具输出的外部持久化:溢出存储
第一个问题:工具一次吐出一大段文本,该整份进上下文吗?
不该。这样一大段输出不该原样整段进下一次请求(官方原话是不应完整占用下一次模型请求),但它后面可能还有用。另外,日志只追加、不改写(第 8 讲),一旦写进去就再也
删不掉了。
那这些内容放到哪儿?存成文件,日志里只留预览和指引,这就是溢出存储(spill),分两步:完整存成文件,然后让进日志的那份只留“头尾预览 + 一句去哪儿读全文”。
两个“溢出”要分清,它们只是同名,不是一回事:本讲的“溢出存储”是当场定的,工具一跑完,在写进日志之前判定单条输出的字节数(跟部署配的 maxInlineBytes 比);第 10
讲的“溢出”是请求被拒之后才确认的,判断的是整个请求超没超模型的窗口。
落到实现上,动手的是溢出策略插件 dsh-spill-policy——它本质是个结果转换器:工具跑出来的结果,超了阈值的就被它换掉,原文一份不丢。它注册两个监听器,挂在两处:一是
工具执行之后(tools/post-execute),结果超了阈值就整份存走;二是 Code Mode (第 6 讲)的 tools/code-dispatch-log:Code Mode 里,run_code 程序会调别
库不替你定;不配,就什么都不发生,工具输出照旧整份进上下文。
名字分清楚了,还有一处更容易混:溢出存储跟第 10 讲的压缩、裁剪都在对付“装不下”的问题,但管的不是同一件事。先把它们摆开:第 10 讲对应的是裁剪,它和溢出存储一样,管的
都是单条过大;压缩管的则是“历史整体太长”。而压缩、裁剪都是事后在 surface 上动手、只往后追加、不改已有事件,溢出存储却在写进日志之前就分流,这是两讲最根本的分工:
第 10 讲裁剪本讲溢出存储
窗口有压力或已溢出时,才回头裁一次时机(CompactionTrigger 就这两种;裁剪自己不注册监每次工具跑完,当场判
听器)
事后在 surface 上腾地方,已有事件一条不改(只往动的是哪一层在写进日志之前就把内后追加)
先看是不是纯文本(源看,有一个块不是文本
判据字符数 > thresholdChars(默认 8192)
> maxInlineBytes(没阈值,多大的输出都整
头尾预览 + 一句"(省替换结果头 + 一行标记 + 尾位符 >,< 检索提示
| 中间部分 | 还在日志里(旧事件一条不丢) | 在文件里,日志里只剩 |
|---|---|---|
| 回头还能查吗 | 取不回——不浮到surface上,也没有取回的入口 | 能取回——文件还在 |
预览 + 提示,整体不结果长度定死的:头 + 标记 + 尾,三者相加就超了,就不外置)