DeepSeek Harness Agent Loop 全解析:逻辑 · 难点 · 亮点

DeepSeek Harness Agent Loop 全解析:逻辑 · 难点 · 亮点


0. 一句话总览

Agent Loop 是 DSH 中驱动一个会话「思考 → 调用工具 → 产出回复」的日志驱动 React 循环

bash 复制代码
系统提示词 + 上下文 + 用户消息
   ↓ 组装成一次冻结、可回放的 LLM 请求
LLM 流式输出 → assistant/chunk 事件 → 组装成 assistant/message
   ↓ 若含工具调用
按 串行屏障 / 有界并行池 调度执行 → tool/call + tool/result 事件 → 结果放回下一步输入
   ↓
进入下一步(step)或下一轮(turn),直到 completed / blocked / max-tokens / aborted / error

核心原则:一切输入派生自会话日志(session log),一切输出写回会话日志。 没有内存态的「当前对话」,只有可回放的事件流------这是它能在 SW 挂起、重启、暂停、被目标轮次/subagent 反复唤醒续跑的根基。


第一部分 · 全部逻辑

1.1 三个时间尺度:Session / Turn / Step

概念 含义 对应日志事件
Session(会话) 一个 agent 的全部历史:事件日志 + 请求头 + 上下文 request/headerrequest/context
Turn(轮) 「一次输入 → 完整回应」的边界,对应一次任务/提问 turn/startturn/end(带 reason)
Step(步) 一轮内「一次 LLM 请求 + 它产生的全部工具调用」的边界 step/startstep/endassistant/messagetool/calltool/resultassistant/chunk
  • 一轮 turn 常含多个 step:模型先发带 3 个工具调用的消息 → 工具执行 → 结果作为下一步输入 → 模型再发纯文本 → 本轮结束。
  • turn/end.reason 枚举:completed(完成)/ blocked(pre-step 拒绝)/ max-tokens(截断)/ aborted(取消)/ error(异常)。

1.2 Agent 状态机(phase)

ReactLoopAgent.phase 三种状态,各自持有独立 AbortController

scss 复制代码
idle ──wakeDriver()──▶ running(driver:kick() 循环跑 turn)
 ▲                      │
 └─────kick 退出────────┘
idle ──runMaintenance()──▶ maintenance(非 turn 后台任务,如拆卸/维护)
  • idle :等待唤醒;followup() / steer()(带 wakeup)与 inject()(不带)投递消息。
  • runningwakeDriver() 新建 phase(turn=lastTurn, step=0、新 AbortController),在 withInitiator 下执行 kick()kick() 反复 await turn() 直到返回 false。
  • maintenance :仅能从 idle 进入,跑完回 idle;期间到达的 wake 记为 wakeRequested,结束后若 inbox 仍有消息则立即重新拉起 driver。
  • cancel(cause, {keepInbox}):默认清空 inbox 并 abort 当前 phase;abort 后的唤醒消息会被重分类投到 next-turn
  • whenIdle():等待当前 activityDone 完成(用于拆卸前同步)。

1.3 Inbox:下一轮 / 下一步的输入队列(dsh-agent)

两个持久化队列,所有变更以 agent/inbox/spliced 事件写日志

队列 用途 典型来源
next-turn 等待单独开一轮的输入 普通用户消息、goal round 提示、followup()
next-step 等待下一步边界的输入 工具结果上下文、steer()、运行时上下文快照
  • claim(target, turn):先取走 next-step 全部;若 target 为 next-turn 再取队首 1 条(一轮只消费一条轮级消息,可消费任意条步级消息)。
  • 每条 splice 含 removedCount;取消类删除标记 outcome: 'canceled'------这是「输入被消费并运行」与「工作被丢弃未运行」的区分依据(见 2.9)。
  • 会话恢复时 inbox 从日志重建(跳过 seedLength 之前的种子区),validate() 防越界/重复 id。

1.4 Turn 循环(turn()

flowchart TD A[turn/start] --> B{preStep: claim + 组装系统提示词 + 运行时上下文投影 + agent/pre-step 瀑布} B -->|reject| X[turnEnds=blocked → return false] B -->|无消息 且 step==0| Y[turnEnds=completed → return false] B -->|有消息| C[step/start + 逐条 user/message surfaceOp=append] C --> D[step: LLM 流式 + 工具执行] D --> E{turnEnds 与 nextStep 空?} E -->|否| F[target=next-step 继续下一步] F --> B E -->|是| G[agent/turn-stopping 串行事件: 插件可追加工作] G --> H{turnEnds 且 nextStep 空?} H -->|否| F H -->|是| I[turn/end 带 reason] I --> J{inbox.hasPending?} J -->|是| K[重置 phase: 新 AbortController, step=0, return true] J -->|否| L[return false, kick 退出]
  • preStepagent/pre-step waterfall 返回 {kind:'enter', messages}{kind:'reject'}:插件可拒绝 本轮(blocked)或增补消息(如注入运行时上下文)。
  • turn 结束判定注意顺序:turnEnds 已存在 && 本次无消息 → 直接结束;step==0 && 无消息 → 空轮 completed。
  • 异常路径:abort → reason aborted;否则 reason errorLlmError 取 failure,其他取 errorChain 根因),先发 agent/error 事件再抛出。
  • 若 inbox 还有 pending → 重置 AbortController 自动开下一轮(这就是 goal round / 排队消息能连续驱动同一会话的原因)。

1.5 Step 循环(step()

flowchart TD A[buildRequest 组装冻结请求] --> B[llm.stream 流式] B --> C[逐 chunk: assistant/chunk + BlockAssembler.push] C --> D{finish} D -->|error/aborted| E[agent/request-error 瀑布: 按 retryPolicy 决定重试] E -->|retry| B E -->|否| F[抛 LlmError] D -->|ok| G[assistant/message: blocks + usage + sourceEventSeqs=chunkSeqs] G --> H{max-tokens?} -->|是| R[返回 max-tokens] H -->|否| I{含 tool-call?} -->|无| S[返回 completed] I -->|有| J[executeToolCalls] J --> K{concluded?} K -->|是| S K -->|否| B[同一步内再发一次 LLM 请求]
  • 流式 chunk 逐块落日志(可回放),BlockAssembler 把 chunk 组装成 block(文本/工具调用/推理等);assistant/messagesourceEventSeqs 关联 chunk 序列。
  • 工具执行产生的额外上下文(快照、指导消息)经 acceptContext 塞回 inbox.next-step

1.6 请求构建(buildRequest

  1. 读持久化 request/header(config + adapterDefaults);requestProposal() 先剔除 adapter 派生的 reasoningEffort/maxTokens,再交给插件。
  2. agent/request waterfall:插件可改写 provider/model 等配置(模型路由扩展点)
  3. llm.prepareCall(config, signal) 绑定适配器;NO_ADAPTER 时降级使用原 config。
  4. 生成 canonicalHeader:首次写 request/header(reason: initial/resume),配置变化时再写(reason: change);provider/model/contextWindow 变化写 request/context
  5. 返回冻结请求markAgentLoopRequest + deepFreeze),携带 sessionIdsignal

1.7 工具调用调度器(executeToolCalls

  • 按每个工具的 executionMode(exec).kind 分组:exclusive(串行屏障)/ parallel(可并行)。
  • exclusive 是屏障:先等当前并行池排空再单独执行,后续调用重新分类。
  • parallel 是有界滚动池maxParallelToolCalls(默认 10,设置键 agent-loop.maxParallelToolCalls),完成一个补一个。
  • 结果按模型顺序 提交(commitReady 游标),与完成顺序无关。
  • 每个调用先写 tool/call(拿 seq),结果 tool/resultsourceEventSeqs:[callSeq] 关联。
flowchart LR A[模型顺序的 toolCalls] --> B{executionMode} B -->|exclusive| C[单调用屏障] B -->|parallel| D[并行池 ≤10] C --> E[按模型顺序 commitReady] D --> E E --> F{还有?} -->|是| B F -->|否| G[返回 concluded]

取消/失败语义:

  • Abort :已启动调用 drain 完并提交真实结果;未启动调用写合成错误(TOOL_ABORTED_BEFORE_DISPATCH)保证回放一致。
  • 调度器内部失败 :停止新派发、drain 已启动调用、抛第一个错误,不伪造结果。
  • 任一工具结果 concludesTurn === true → 本轮 step 结束。

1.8 运行时上下文投影(RuntimeContextProjection)

  • 记住日志里最后一个 source.plugin === dsh-system-prompt 的快照。
  • project(current, sections) 只在渲染结果与保留值不同 时生成新 user/message(source.form:"snapshot" 携带 sections);清空时写 CLEARED 哨兵文本。
  • 快照是未提交候选,由 preStep 的 agent/pre-step 瀑布决定是否纳入本轮。

1.9 工厂与生命周期(AgentLoop Service)

  • 声明式配置maxParallelToolCalls + agents[]id/sessionId/resumeSessionId/provider/model/maxTokens/cwd);launcher 可通过 CONFIGURED_AGENT_IDENTITIES_KEY 覆盖身份。
  • 创建/恢复create() 同步风格;createAgent() 异步 + setup 回调(归调用方 fiber 所有);resume()sessionPersistence 加载;restoreOrCreateConfigured 先恢复、无记录才新建。
  • 所有权(FactoryOwnership) :跟踪所有 live agent teardown 与启动任务;dispose() abort 全部(reason agent loop is not active),先拆 agent 注册再拆 session 注册,等全部 settle。
  • 发布原子性prepare() 先把 teardown 注册进 owner fiber 与 factory(中途卸载可回滚),再 publish()sessions.enter → agents.enter → announce → agent/session-start
  • 系统提示词变量:provider / model / cwd
  • 运行时设置:agent-loop.maxParallelToolCalls 通过 source() 间接引用,不重建服务即可热更新

1.10 不变式(invariant.js

agent-loop-invariantllm/stream 钩子上强制校验每条循环请求:

  1. 请求必须冻结;
  2. 必须带 sessionId 且会话存活;
  3. messages 冻结且逐字节等于 session.deriveMessages()
  4. foldRequestHeader 折叠的 header 一致(model/system/temperature/maxTokens/stop/tools)。

任一条不满足即 fail------这就是「请求可从日志重建」的硬保证。

1.11 与 Goal Round 联动(自动续跑)

dsh-goal-round-driver:goal armed + agent idle + 无竞争输入时,渲染 <goal_round> 提示(Objective + Round N/M),以 source.kind:"goal" 消息 followup 进 inbox → 唤醒开新一轮。轮间做 sessions.flush checkpoint;达 maxGoalRounds → goal 置 blockedround-limit)。


第二部分 · 难点(这些地方为什么容易错,代码如何兜底)

2.1 并发工具调用 vs 严格顺序的日志

:并行池里工具完成顺序不可控,但 tool/result 必须按模型声明顺序落盘,否则回放/前端展示错乱;且 exclusive 调用插在并行组中间时,不能把后面的 parallel 一起并发跑掉。

兜底commitReadycommitted 游标按模型顺序提交(没到序的结果先等);fillPool 在遇到非 parallel 调用处 break,只消费已启动的部分(consumed = started),exclusive 留给下一轮屏障;TOOL_RUNTIME_SCHEDULER 统一 prepare/dispatch/finalize 管道,保证每个调用的生命周期一致。

2.2 取消(abort)与日志一致性

:abort 瞬间有的调用已启动、有的还没启动。若给已启动调用伪造结果会污染真实数据;若不给未启动调用记结果,日志就缺了「这条调用被取消了」的事实,回放时模型会看到「发了调用但没有结果」。

兜底 :abort 时已启动调用 drain 完并提交真实结果 (绝不伪造),未启动调用写合成错误结果TOOL_ABORTED_BEFORE_DISPATCHisError:true),两者语义分明、日志完整。

2.3 调度器内部失败:不伪造、不死锁

:调度器(工具注册/准备)自己挂了时,既不能像 abort 那样写合成结果(那是用户取消,不是系统故障),也不能无限等待在途调用。

兜底schedulerFailure 记下第一个错误 → 停止新派发 → Promise.allSettled drain 在途调用 → 抛错。已记录的 tool/call 事件保留,但不伪造 任何 tool/result

2.4 可回放性:请求必须与日志严格一致

:LLM 请求的 messages 是从「内存里攒的对话」推导的,而会话日志是唯一真相。一旦某插件偷偷改请求、或日志重建路径与派发路径脱节,回放出的就是另一个请求------会话恢复后行为漂移。

兜底 :请求冻结 (deepFreeze)+ 携带 sessionId + invariant.jsllm/stream 上逐字节比对 deriveMessages() 与折叠 header,任何脱节立即 fail。这是「日志即真相」的强制执法。

2.5 运行时上下文注入的噪声控制

:把系统提示词里动态渲染的「当前运行时上下文」(文件策略、技能清单等)每轮每步都整体塞进请求,既费 token 又污染历史,还会让模型注意力涣散。

兜底RuntimeContextProjection 记录「最后保留的快照」,只在内容变化时 生成新 user/message;清空时写 CLEARED 哨兵。不变就不注入。

2.6 SW 挂起 / 重启 / 会话恢复

:MV3 SW 随时可能被挂起,agent loop 的内存态(phase、inbox 投影)会丢;恢复后若 inbox 重建错位,消息会重复消费或丢失。

兜底 :inbox 的所有变更都以 agent/inbox/spliced 事件持久化,恢复时从日志重建(跳过 seedLength 种子区);validate() 拒绝越界 splice 与重复消息 id;resume()sessionPersistence.prepare,取消用 raceAbortCall 释放被弃的 preparation([Symbol.dispose])。

2.7 生命周期所有权与「中途卸载」

:agent 的 setup 还没跑完,owner fiber 或 factory 就被拆了------此时 teardown 逻辑还没注册完,容易泄漏;发布到一半(session 已 enter、agent 还没 announce)崩溃,注册表就脏了。

兜底prepare()teardown 先于发布注册disposedisposing ??= 记忆化,幂等);machineReady promise 让 dispose 能等待未完成的构造;assertLive() 在 publish 每一步之间校验,任一失败走统一 dispose 回滚;owner effect 在 setup 期 abort 时主动 dispose(true)

2.8 模型路由与 adapter 默认值的拉扯

:持久化的 request/header 里可能有 adapter 写入的默认 reasoningEffort/maxTokens;插件又要在 agent/request 瀑布里改 provider/model。若把 adapter 默认值当用户配置交给插件,路由会乱。

兜底requestProposal()剥离 adapter 派生值 再给插件提案;prepareCall 绑定后生成 canonicalHeader,按 initial/resume/change 三档记录 header 变更;provider/model/contextWindow 变化单独记 request/contextNO_ADAPTER 优雅降级。

2.9 空轮 / 边界 / max-tokens 语义

:一轮 turn 在第一步之前就停下时,turn/end 的形状和「被拒绝/空 claim」完全一样------只看 turn 记录,分不清「输入被消费后没跑完」和「根本没干活」。

兜底dsh-agent 的 consumed-work 记账(foldConsumedWork / accountsForClaim)把 inbox 的 removedCountoutcome:'canceled' 纳入判定:completed 不算记账、blocked/aborted/interrupted/error 都算------取消时任何人都能只读日志得出「丢了多少未运行的工作」。

max-tokensturnEnds 一旦为 max-tokens粘住(后续 step 不覆盖),即使后续步骤跑完,本轮仍按截断记账------语义明确。

2.10 多输入源竞争与身份冲突

:同一会话可能同时有用户消息、goal round、steer 注入;多个配置 agent 可能撞同一个 sessionId。

兜底 :goal-round-driver 用 competingQueued / needsCheckpoint 栅栏避免与用户输入撞车;丢弃自己轮次时 restoreOtherClaimed 把其他已 claim 消息放回 inbox;validateConfiguredAgents 拒绝 sessionIdresumeSessionId 并存、拒绝重复身份;waitForDrainingConfiguredIdentity 等同一 id 的旧生命周期拆完再建。


第三部分 · 亮点(值得借鉴的设计)

3.1 日志即真相(Log as Source of Truth)

一切状态(消息、inbox、请求头、上下文、turn/step 记账)都从会话日志派生或写回,没有旁路内存态。这带来三个免费收益:崩溃可恢复、行为可回放、任何人(包括外部调用方)都能只读日志推导出准确状态------包括「取消后丢了哪些未运行的工作」(见 2.9)。

3.2 冻结请求 + 不变式执法

deepFreeze 的请求对象 + markAgentLoopRequest 标记 + invariant.js 在 LLM 流入口逐字节比对。不是「尽量保持一致」,而是不变量本身成为运行时护栏:任何破坏可回放性的改动当场 fail。这是把架构原则变成可测试约束的范例。

3.3 模型顺序提交 × 有界并行池

并发执行(吞吐)与严格顺序落盘(正确性)解耦 :工具随便并行跑,日志严格按模型声明顺序提交;exclusive 屏障与并行池复用同一 runGroup 管道。默认 10 并发且可运行时热调,兼顾安全与效率。

3.4 取消语义完备且分层

abort(用户取消)与 scheduler failure(系统故障)走两套不同语义:前者补合成错误结果保日志完整,后者坚决不伪造。取消瞬间「已启动/未启动」两类调用各有明确归宿,不会出现悬空调用。

3.5 上下文按 diff 注入

运行时上下文投影只在变化时生成消息(含 CLEARED 哨兵),省 token、少噪声、历史干净。这是长会话下系统提示词动态部分的标准解法。

3.6 事件驱动插件扩展点(waterfall 矩阵)

| 事件 | 作用 |
|--------------------------------------------------------|-----------------------------|-----------|--------|
| agent/pre-step | 拒绝本轮(blocked)或增补消息 |
| agent/request | 改写 provider/model,实现模型路由 |
| agent/request-error | 按 retryPolicy 决定是否重试 LLM 请求 |
| agent/turn-stopping | 轮末追加工作(可阻止 turn 结束) |
| `agent/inbox/inserted | discarded | claimed` | 观察输入流转 |
| agent/status / agent/error / agent/session-start | 状态广播与错误上报 |

循环本身不写死任何业务,全部通过瀑布开放,这是「通用驱动 + 可插拔策略」的教科书结构。

3.7 生命周期原子发布与有序拆除

teardown 先注册后发布dispose 记忆化幂等、publish 每步 assertLive、工厂 teardown 有统一 reason------把「中途卸载」这个最容易泄漏的角落做成了确定性的回滚路径。

3.8 Inbox 持久化 + 严格校验

队列变更即事件、恢复即重放、splice 带 removedCount/outcome:'canceled'validate() 防越界与重复 id。连「待办队列」都是可回放日志的一部分,这比内存队列高一个量级的健壮性。

3.9 与 Goal Round / Subagent 的天然配合

目标轮次、任务面板、subagent 都只是往 inbox 投消息 + 唤醒,agent loop 自动开新一轮------驱动方不需要理解循环内部 ,只依赖公开的 followup() 与持久化语义。接口边界清晰,扩展成本极低。

3.10 运行时设置热更新

maxParallelToolCalls 通过 source() 间接引用 + 设置 section 的 setSource 回调,改设置不需要重建 AgentLoop 服务。把「配置」与「运行时值」解耦的轻量做法。


附录 A · 术语表

术语 含义
ReactLoopAgent 驱动一个会话的循环主体(turn/step 状态机)
AgentLoop 工厂服务:create/resume、所有权、声明式配置
Inbox next-turn / next-step 持久化输入队列
phase idle / running / maintenance 三态 + AbortController
waterfall / serial Cordis 事件两种模式:瀑布(可改结果、可短路)/ 串行(顺序执行)
BlockAssembler 把流式 chunk 组装成 assistant 内容块
deriveMessages 从会话日志推导当前应发给模型的消息序列
request/header 持久化的请求配置头(provider/model/温度等)
RuntimeContextProjection 运行时上下文快照的 diff 投影器
consumed-work 从日志折叠出「已消费未运行的工作」的记账

附录 B · 源码速查

位置 内容
dsh-agent-loop/lib/index.js ReactLoopAgent、AgentLoop、工具调度、上下文投影、常量
dsh-agent-loop/lib/invariant.js 请求重建不变式插件
dsh-agent/lib/index.js Inbox、agentEvents、assembleContextFor、consumed-work 记账
dsh-llm BlockAssembler、消息工厂、markAgentLoopRequest、LlmError
dsh-session 事件日志、deriveMessages、requestHeader、foldRequestHeader
dsh-system-prompt 系统提示词 section 组装/渲染、运行时上下文
dsh-goal-round-driver 目标轮次自动续跑、轮间 checkpoint
相关推荐
YOLO数据集集合1 小时前
一站式AI数据自动化标注与训练平台:零门槛玩转YOLO全系列模型
人工智能·深度学习·yolo·ai·自动化·数据集·标注软件
lucky_syq1 小时前
第5篇 · S1·下:前沿架构:MoE、Reasoning 模型、长上下文、多模态、SSM/Mamba 与模型谱系
人工智能·学习·架构
阿里云大数据AI技术1 小时前
基于阿里云 Milvus 复刻“高德扫街榜”
人工智能
秦先生在广东1 小时前
Archify:让 AI Agent 直接在对话中生成可交互、可验证架构图的 Skill
人工智能
゛凌乱的记忆づ2 小时前
50元从零到成品:一套AI辅助的嵌入式实战入门教程——基础工程篇
人工智能
无凭2 小时前
DeerFlow 的可观测性(一):RunJournal 如何记录 Agent 运行过程
人工智能·开源
迷迭香yy2 小时前
行业板块轮动因子实战从板块资金到因子建模的本地化Python全流程
数据库·人工智能·python
牛奶咖啡132 小时前
AI助力运维——AIGC运维应用实践—Deepseek的介绍与本地部署选型
运维·人工智能·deepseek·deepseek能做什么·deepseek本地部署配置·本地部署选型避坑原则·本地部署的典型方案
阿里云大数据AI技术2 小时前
阿里云 Milvus 知识库开启邀测,助力客户构建企业级 Agent
人工智能·agent