跳转到内容
Yuuki学习手记
GitHub

DeepSeek Harness:怎样保住可复用的请求前缀

专题入口 · 上一篇 DeepSeek 缓存规则 · 下一篇 Pi

模型服务管理 KV,Harness 决定每次提交什么。研究 DeepSeek Harness(dsh)时,先追踪一件具体的事:一次工具调用之后,下一次请求的前部还保留了多少原样内容?

先从你写过的请求函数理解 Harness

Section titled “先从你写过的请求函数理解 Harness”

前端调用接口时,会准备请求数据、等待响应、处理错误,再把新结果写回状态。模型如果还要求读取文件,就需要有人执行工具,把结果放进历史,再发下一次请求。

Harness 可以先理解为把这些运行步骤串起来的外层程序。 模型输出了 read_file 参数,只代表它提出了请求,文件读取仍由程序执行。

用户输入后,Harness 装配规则、历史和工具定义;模型提出 read_file;Harness 执行并把结果加入历史,再进行模型请求

这里先认一个词:装配上下文就是决定这次发哪些消息、规则和工具定义。读图时,只追踪每一步新增了什么内容。

第一停点:模型提议,Harness 执行并接着组织请求,服务端判断缓存命中。 分清三者之后,再看下面三个小选择。

想读源码时:先看一个判断,再沿五个入口追踪

第一眼,先看“不需要更新”的分支

Section titled “第一眼,先看“不需要更新”的分支”

打开 SystemPromptProjection.project。找到“最新有效 system 文本与当前渲染一致,就返回空更新”的判断,再往上看它处理新请求序列的条件。

这一小步要解释:system 文字相同,为什么把它从历史后部移到第一条消息,也可能损失缓存?前缀身份包含此前的内容、位置与序列关系;最终有效指令相同,并不代表提交给模型的序列相同。

源码入口追踪什么与缓存的关系
system-prompt/src/index.tssection、动态 context 和工具装配固定规则与易变内容如何进入请求;工具怎样确定顺序
agent-loop/src/agent.ts每一步装配、投影与提交工具往返和新序列边界何时改变历史
runtime-context.tsSystemPromptProjection、RuntimeContextProjection未变化的提示保留原位;动态快照只在内容变化时产生候选消息
llm-pi-ai/src/adapter.tsprofileOptions把 cacheRetention 等选项传入 Pi 的模型层
llm-pi-ai/src/stream.tsmapUsage把 Pi 的用量转成 Harness 的流式统计

可把这条路径记成:会话与插件贡献 → 装配请求 → 供应商适配 → 模型服务 → 用量回写。其中直接保存和匹配推理 KV 的工作发生在模型服务端。

orderTools 在未指定顺序时按名称排序;配置 toolOrder 时,显式列出的工具按配置排列,其余工具有确定顺序。上游还测试了相反的插件注册顺序能产生相同的工具列表。排序源码、工具顺序测试

这能减少插件加载顺序带来的请求差异。实际命中仍取决于供应商如何编码工具和消息;一个稳定的 JavaScript 数组本身不能证明服务端命中。

2. 动态上下文变化时,再留下新快照

Section titled “2. 动态上下文变化时,再留下新快照”

快照就是某个时刻的一份内容。在这里,程序比较前后两份动态说明,内容相同就不必再追加一份。投影则是从已有记录生成本次需要的表示;可以借用前端从 state 得到视图数据的经验理解。

RuntimeContextProjection.project 比较当前内容与保留的快照,一致时不产生更新。系统提示的注册表也把进程相关路径、端点等列入 runtime context 的职责。动态投影、context 分区

这里可以借鉴的原则是:先判断内容是否真的变化,再决定如何进入上下文。时间、工作区和环境变化仍需正确告知模型;为了缓存省略必要信息,会破坏任务语义。

3. 支持历史内 system 更新的路由,才保留这种布局

Section titled “3. 支持历史内 system 更新的路由,才保留这种布局”

路由在这里包含所选模型接口及其能力。先看简化的消息顺序图:S0 是旧规则版本,S1 是后来的规则版本。

支持历史内 system 更新时,有效规则 S1 不变可以保留消息原位;把 S1 搬到开头会改变原有前缀

图里文字的最终含义可以相同,输入顺序却变了。就像前端缓存的依赖变了,不能仅凭最后显示的一行文字相同,断言所有中间结果还有效。

SystemPromptProjection 根据路由的 inHistory 能力、当前提示与请求序列边界,选择追加、保持原位或重建头部。不能把“system 一律往后追加”作为所有接口通用的优化。

上游有一份很适合研究的已实现修复记录:局部历史替换开始新序列时,未变化的 system 文本曾被搬回头部,导致前部差异扩大。修复让最新有效文本相同时保持原位。它是上游记录的故障与修复,本仓库没有复现其运行数据。

这一站完成了:工具顺序、动态快照和规则位置,都可能改变请求前缀。 下次从 Pi 怎样翻译接口用量继续。

选读:dsh 与 Pi 的关系,以及两个适配层的字段

与 Pi 的关系:模型适配层可以复用

Section titled “与 Pi 的关系:模型适配层可以复用”

适配器可以先理解成一段「翻译接口」的代码:把应用的数据变成供应商需要的请求,再把响应变回应用使用的格式。

@deepseek-ai/dsh-llm-pi-ai 是 dsh 的模型适配包,声明依赖 @earendil-works/pi-ai;同仓也保留 llm-deepseek 专用实现。适配包声明

这一关系允许我们沿接口继续读 Pi,但不能据此把整个 DeepSeek Harness 当作 Pi 的 Agent 循环。也不能认为这份 dsh 快照实际安装的 Pi 依赖,必然等于下一篇单独选取的 Pi 主干快照。

mapUsage 的对应关系很直接:

Pi 用量dsh 用量当前转换行为
inputinputTokens未缓存输入
outputoutputTokens输出
cacheReadcacheReadTokens大于 0 时写入
cacheWritecacheWriteTokens大于 0 时写入
totalTokenstotalTokens保留 Pi 的总量

dsh 的 TokenUsage 契约把输入与缓存桶定义为互斥计数。Pi 适配层会省略值为 0 的缓存字段,所以看到字段缺失时,还要回查适配器,不能只凭缺失判断供应商没有提供缓存能力。

另一个协议差异也值得保留:同快照 llm-deepseek 的 translate.ts处理 Messages 风格的 cache_read_input_tokens 等字段;本仓库 Yuuki 使用 Chat Completions。研究某个响应时,先确定走的是哪个适配器和协议。

选读:上游测试、证据边界与带回 Yuuki 的小研究

上游怎样验证,本文验证到了哪一步

Section titled “上游怎样验证,本文验证到了哪一步”

request-cache.e2e.ts包含需要 DEEPSEEK_API_KEY 的真实服务测试:制造一次工具往返和后续用户轮次,从 assistant/message 事件读取 usage,断言首个请求之后出现 cacheReadTokens > 0。

证据能说明什么还不能说明什么
排序与投影源码、对应测试断言上游如何设计和检查请求稳定性该版本在本机已经通过测试
真实 API 测试代码上游准备从哪里观察真实缓存命中本次已经发请求并观测到命中
单次服务端命中字段该请求报告命中了多少输入所有请求必然命中,或端到端延迟必然降低

测试中的旧缓存粒度注释不能代替当前 DeepSeek 持久化规则。即使请求构造稳定,也要保留缓存构建、淘汰和尽力命中的条件。

先只做一次源码对照:在 Yuuki 的 completion 中指出 system、tools、history 和 usage 的入口,再标出缺少的缓存字段。下一步实现可从单次请求的原始缓存用量开始,验收后再研究前缀布局与摘要策略。

继续阅读Pi 怎样使用和观测缓存,比较同一组 token 数怎样跨过两个项目的接口。

来源与核对范围

核对日期:2026-10-24。本文静态阅读官方仓库的 22ddee379240 快照,未安装运行,也未执行上游测试或真实 API 实验。项目在该快照仍标为 developer preview。项目说明