DeepSeek Harness 核心 Agent Loop:Turn、Step、Inbox 与取消收敛
JotaroRD

Turn/Step 流程

模块定位

packages/core/agent-loop 是 DeepSeek Harness 里唯一包含具体 loop driver 的包。它的职责不是实现所有 Agent 能力,而是驱动 Agent 生命周期:领取输入、组装请求、调用模型、执行工具、判断是否继续下一步。

相关源码入口:

  • packages/core/agent-loop/src/agent.ts
  • packages/core/agent-loop/src/tool-calls.ts
  • packages/core/agent-loop/src/runtime-context.ts
  • packages/core/agent-loop/src/index.ts
  • packages/core/agent/
  • packages/core/session/

项目文档反复强调:新行为优先挂插件,不要改 loop。原因是 loop 是 spine,插件才是行为扩展面。

Turn 与 Step

Turn 是外层工作单元,通常对应用户可感知的一轮交互。Step 是一次模型请求加它触发的工具执行。一个 turn 可以包含多个 step,因为模型可能需要多轮“请求工具 → 看结果 → 继续请求”。

简化流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sequenceDiagram
participant User as User/Input
participant Loop as Agent Loop
participant Prompt as SystemPrompt/Tools
participant LLM as LLM Adapter
participant Tools as ToolRuntime
participant Log as Session Log

User->>Loop: followup / steer / inject
Loop->>Log: turn/start
Loop->>Prompt: assemble prompt + schemas
Loop->>Log: user/message
Loop->>LLM: stream request
LLM-->>Loop: assistant chunks
Loop->>Log: assistant/chunk*, assistant/message
Loop->>Tools: execute tool calls
Tools-->>Loop: normalized results
Loop->>Log: tool/call*, tool/result*
Loop->>Log: step/end, turn/end

Inbox 的三个入口

Agent 输入不是直接拼进 messages,而是进入 inbox。核心入口可以理解为三种:

方法 进入队列 是否唤醒 loop 用途
followup() next-turn FIFO 是 普通用户新输入
steer() next-step inbox 是 立刻影响当前/下一 step
inject() next-step inbox 否 等下一次自然唤醒时附加上下文

这种设计让“用户新问题”“运行中引导”“静默上下文注入”有不同语义。Loop 在 turn 边界会领取 pending next-step input 加一个 queued prompt;step 之间只领取 next-step input。

agent/pre-step 是关键关口

Loop 领取输入后不会立刻发给模型。它会先触发 agent/pre-step waterfall。Listener 可以:

  • 接受输入。
  • 改写进入 step 的消息。
  • 拒绝这次 step。

如果第一批输入被拒绝或改写为空,loop 仍然会关闭一个 durable turn,只是没有实际 step。这让“尝试过但没有进入模型”的事实也能被记录。

请求如何形成

每个 step 的模型请求来自:

  • system prompt sections。
  • 当前 agent 可见 tool schemas。
  • session log 派生出的 message history。
  • provider/model/maxTokens/reasoning 等 request config。

agent/request waterfall 可以调整请求。然后 loop 调 ctx.llm.prepareCall(),让 adapter 拥有的默认值被具体 materialize,并把 request/header 写入 log,保证后续可重建。

工具调用与并发

模型返回 tool calls 后,loop 会交给 ToolRuntime 执行。工具有并发分类:

  • parallel:连续安全调用可以进入有上限的 rolling pool。
  • exclusive:作为 barrier,前后调用不能重叠。

注意:并发只覆盖 dispatch/body;策略、持久结果、结果上下文仍保持模型顺序。这样既能提高读类工具吞吐,又不会让 session log 顺序失真。

取消与收敛

取消不是简单 throw。Loop 有一个当前 admission/turn 的 signal。取消时:

  • 清理 pending work,除非调用方要求保留 inbox。
  • 协作式 abort 当前 signal。
  • 已开始工具要 drain,保留 finalized result context。
  • 未派发 tool call 会生成 synthetic result。
  • durable turn/end 记录 aborted 或 disposed。

一个关键细节是 wake latch:如果取消收敛过程中又来了唤醒输入,loop 会在收敛边界重放唤醒,避免丢输入。

插件应该挂在哪里

不要为了功能直接改 loop。常见扩展点:

需求 扩展点
改模型请求 agent/request
入模前过滤/压缩输入 agent/pre-step
工具审批 tools/pre-execute
工具超时/指标 tools/execute
工具结果附加上下文 tools/post-execute
回合停止策略 agent/turn-stopping
UI 观察 session/event + agent/status
持久化 session/event + session/flush

常见坑

第一,agent/pre-step 是 waterfall。要委托后续 listener,就必须调用 next()。

第二,插件异常会结束当前 turn,但不一定杀死整个 loop。恢复逻辑要看异常发生在哪个阶段。

第三,模型可见的新信息不能只存在内存。要能从 session log 还原。

第四,工具取消是协作式的。工具 body 必须观察或转发 AbortSignal。

继续读源码

  • packages/core/agent-loop/README.md
  • packages/core/agent-loop/src/agent.ts
  • packages/core/agent-loop/src/tool-calls.ts
  • docs/agent-lifecycle.md
  • docs/subsystems/core.md