deepseek-harness 学习手册
Agent 运行时M3 · turn / step 与上下文防御
60 min

turn / step 与上下文防御

turn / step 事件流全图、session log 唯一真相,以及三个可卸载的上下文防御插件。

最后核对于 v0.1.1-rc.2 · commit b150a551b8 · 2026-08-22

M2 讲的是「树怎么长出来」,这一页讲「树跑起来以后发生了什么」。三件事:一次对话在事件层面到底经过哪些环节、为什么整个系统只认一条 append-only 日志、以及三个可以随时卸掉的上下文防御插件。

3.1 turn 与 step:一张事件流全图

先把两个词定死,仓库 glossary 里写得很清楚:

注意「零个」——一个被拒绝的首次认领,仍然会关闭一个持久化的、没花掉任何 step 的 turn,好让日志记下这次尝试。这个细节是整套设计的缩影:宁可日志里多一条「什么都没发生」,也不要日志里缺一条。

图 3.1

一个 turn 的完整事件流

turn/start
  认领 next-step 输入 + 一条排队消息
  装配 prompt sections + 工具 schemas
  → agent/pre-step              reject | enter(messages)
     被拒绝,或首次认领被改写成空 → 关闭 turn,不花 step
     step/start
     把 entered messages 追加为 user/message
     从日志投影出模型历史
     agent/requestllm/stream → assistant/chunk* → assistant/message
     tool/call* → tools/pre-executetools/executetools/post-execute → tool/result*
     step/end
     工具还欠一次请求,或 next-step 输入到了 → 认领 → 下一个 step
  → agent/turn-stopping
turn/end
高亮的是活扩展点,其余是持久 session 事件。前六个是 waterfall(监听器必须调 next() 才委托下去);agent/turn-stopping 是 serial,没有 next()

三个事件域,先选对域再动手

官方 architecture.md 说得直白:事件就是扩展点,选对域是大多数改动的第一个决定。

是什么什么时候用
Session 事件追加进日志、并通过 session/event 广播的持久事实这个事实必须活过一次重载
Agent 事件(agent/*携带一个活的 Agent:inbox、step、status、request、validation、continuation观察或拦截正在进行的工作
Capability 事件fs/*tools/*telemetry/*——把策略和适配器挂到某个 seam 上你不想 import agent loop

agent/pre-step 是最重要的那个:它决定模型看见什么。监听器可以改写被认领的消息,也可以直接拒绝它们。M3 后面讲的三个防御插件,两个挂在 tools/*,一个挂在 agent/pre-step 上。

3.2 Session log:唯一真相

整个系统只认一条 append-only 的 SessionEvent 日志。deriveMessages() 从它投影出模型历史;原始的 assistant/chunk 事件被完整保留,用来保证重放和 UI 的保真度。fork、resume、transcript、遥测、持久化,全部派生自这一条流。

这条不变量还有一个很妙的落地:repeat-tool-reminder 那个循环破除器要给模型塞一句提醒,它没有发明新机制,而是把提醒作为一条「插件来源的 user/message」追加进日志——于是提醒既是模型可见的、又标明了来源、还能从日志重建,一个新的 session 事件都不用加。

格式版本机制:默认「读到必须懂」

SessionEventMap 的成员默认是required-on-read:一个不认识这个事件类型的构建会拒绝这份日志,除非该事件在信封里携带了 ignorable: true。只有结构性的格式变化才动 SESSION_FORMAT_VERSION

而这个版本号现在是 0,仓库明确写着不作任何兼容承诺——见 M10 的风险一节。

3.3 上下文防御三件套

长对话会以三种方式崩坏:历史太长单条工具输出太大模型陷在循环里。dsh 对这三件事各有一个插件,而三个都是可以卸掉的普通插件——卸了 agent loop 照跑。这本身就是对 M2 那条断言的一次检验。

compaction —— 历史太长

一个标准的四包 seam:compaction(定义 + 事件词汇,ctx.compaction)、compaction-basic(token 压力检测 + 摘要后端)、compaction-tool-result-pruner(可选的、不用模型的工具结果裁剪)、command-compact(人类可用的 /compact 命令)。

值得注意的是那个 pruner 被单独切出来了:它是model-free 的,纯规则裁剪旧工具结果,不烧一次模型调用。换句话说,「压缩」这件事被拆成了两个成本级别完全不同的动作,部署方可以只要便宜的那个。

spill —— 单条工具输出太大

三个包:spill 定义存储(ctx.spillStore),spill-local 把溢出文本写进 session 作用域的本地文件,spill-policy 挂在 ctx.tools 上执行「执行后」策略。

做法是:把超大的工具输出落盘,然后把内联结果换成一个有界的预览 + 一个取回定位符。模型看到的不是被粗暴截断的一半内容,而是一段摘要加一个「你可以去拿全文」的句柄。这个设计的关键在于它把决定权留给了模型——需不需要全文,模型自己判断。

guard —— 模型陷在循环里

repeat-tool-reminder 的自我描述里,第一句就是「它不是一个模型可见的工具」。它做的事只有一件:盯住每个 agent 的工具调用流,数连续调用同一个工具且参数规范化后完全相同的次数,在配置的长度上注入一句逐级升级的提醒。

几个细节能看出这个插件被认真想过:

另一个 guard 包 timeout-policy 更简单:给每次工具调用装一个部署级的截止时间。它注册的就是一个普通的 tools/execute 监听器。README 里点明了这两个包的定位——「guard 是核心服务和扩展点的自足消费者,不是一个可替换的能力。」这句区分很重要:不是所有插件都必须是 seam,把不该抽象的东西抽象掉,同样是错误。

目录

本页

M3 · turn / step 与上下文防御