Skip to content

智能体循环(agent loop) 是 harness 对公共 Agent 契约的默认、具体实现。公共 Agent 接口声明的都是契约;本页讲述的是机器本身——packages/core/agent-loop/src/agent.ts 中的 ReactLoopAgent 类。它是构成 core 主干链的包之一,专门放在 agent-loop 里是因为它是随产品发布的循环:扩展插件只依赖 agent(从不直接依赖 agent-loop),因此驱动可以随时替换。

负责
@deepseek-ai/dsh-agentAgent 接口、运行时注册表、agent/* 事件、inbox、发起人作用域(ctx.agents
@deepseek-ai/dsh-agent-loop具体驱动、工具调用调度器、智能体工厂(ctx.agentLoop
@deepseek-ai/dsh-session只追加的 SessionEvent 日志(ctx.sessions
@deepseek-ai/dsh-system-prompt提示组装(.loopCtx.systemPrompt

两个嵌套单元:turn 与 step

循环使用两个嵌套边界,两者的开启与关闭都会写入持久的会话事件。

step(步骤) 是工作单元:一次模型请求加上该请求产生的那些工具执行。它以 step/start 事件开启,以 step/end 关闭。在一个 step 内循环逐块流式接收模型输出,如果助手产出了 tool-call 内容块,就执行这些工具调用。

turn(轮次) 是一次对所接收输入的排空:以 turn/start 开启,然后只要还有续接请求就运行一步或多步,最后以携带 TurnEndReasonturn/end 关闭。当唤醒输入被认领后 turn 开始,当模型不再欠响应(没有活跃的工具调用、也没有新的 steering)或触发终止策略时 turn 结束。ReactLoopAgent 中的阶段状态机是一个三态判别联合:

ts
type Phase =
  | { kind: 'idle'; lastTurn: number }
  | { kind: 'maintenance'; abort: AbortController; lastTurn: number; wakeRequested: boolean }
  | { kind: 'running'; abort: AbortController; turn: number; step: number; wakeRequested: boolean }

statusidlemaintenance 阶段都返回 idle,排空期间返回 running。每次阶段转换都会发布 agent/status

驱动的主流程

kick() 是驱动入口:它不断排空 turn,直到没有续接请求为止,然后回到 idleturn() 开启一个 turn 并运行它的 step 循环。这是来自 agent.ts 的真实主流程:

ts
private async kick(): Promise<void> {
  try {
    while (await this.turn()) {}
  } catch (_error) {
    // Reported failures and cancellation are contained at the driver boundary.
  } finally {
    if (this.phase.kind === 'running') {
      const { turn, wakeRequested } = this.phase
      this.setPhase({ kind: 'idle', lastTurn: turn })
      if (wakeRequested && this.inbox.hasPending) this.wakeDriver()
    }
  }
}

turn() 认领第一个提议的 step,写入 step/start,把认领到的消息记为 user/message 事件,运行 step(),写入 step/end,然后决定是否继续。续接策略与 agent/turn-stopping 拦截出现在该循环的中段:

ts
while (true) {
  signal.throwIfAborted()
  const step = phase.step + 1
  const decision = await this.preStep(target, { turn, step })
  if (decision.kind === 'reject') { turnEnds = { kind: 'blocked' }; return false }
  if (turnEnds && decision.messages.length === 0) break
  if (phase.step === 0 && decision.messages.length === 0) {
    turnEnds = { kind: 'completed' }; return false
  }
  this.session.append('step/start', { turn, step })
  phase.step = step
  try {
    for (const message of decision.messages) {
      this.session.append('user/message', message, { surfaceOp: 'append' })
    }
    const stepEnd = await this.step(decision.assembly)
    if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEnd
  } finally {
    this.session.append('step/end', { turn, step })
  }
  signal.throwIfAborted()
  if (turnEnds && this.inbox.nextStep.length === 0) {
    await this.dispatch.serial('agent/turn-stopping', { turn, signal })
    signal.throwIfAborted()
  }
  if (turnEnds && this.inbox.nextStep.length === 0) break
  target = 'next-step'
}

在哪里调用模型

step(assembly) 渲染系统提示,然后进入请求循环。每次迭代调用 buildRequest(),打开提供方流,把原始块追加到日志,并组装内容块。工具结果以 user/message 上下文的形式追加到 next-step inbox,从而重新进入循环:

ts
while (true) {
  const { request, preparedCall } = await this.buildRequest(
    turn, step, assembly.tools, system, this.session.deriveMessages(), signal,
  )
  const assembler = new BlockAssembler()
  const chunkSeqs: number[] = []
  const stream = preparedCall?.stream(request) ?? this.loopCtx.llm.stream(request)
  for await (const chunk of stream) {
    signal.throwIfAborted()
    chunkSeqs.push(this.session.append('assistant/chunk', { turn, step, chunk }).seq)
    assembler.push(chunk)
  }
  // error/aborted finish → agent/request-error waterfall, maybe retry
  const message = createAssistantMessage({ content: assembler.blocks(), source: { provider: request.provider, model: request.model } })
  this.session.append('assistant/message', { turn, step, message }, { surfaceOp: 'append', sourceEventSeqs: chunkSeqs })
  if (finish.kind === 'max-tokens') return { kind: 'max-tokens' }
  const toolCalls = message.content.filter(block => block.type === 'tool-call')
  if (toolCalls.length === 0) return { kind: 'completed' }
  const { concluded } = await executeToolCalls(this.loopCtx, turn, step, toolCalls, signal,
    context => this.inbox.splice('next-step', this.inbox.nextStep.length, 0, [context]))
  return concluded ? { kind: 'completed' } : null
}

模型调用本身走 ctx.llm.stream() / prepareCall() 接缝(详见 model-selection.md);每个可观测事实都先追加到会话日志,因此回放是纯粹的重新推导。

工具结果如何重新进入

executeToolCalls(位于 packages/core/agent-loop/src/tool-calls.ts)是回到模型历史的桥梁。它为每个助手的 tool-call 块规划一个 ToolExecutionInput,按模型顺序经作用域工具注册表分发,并提交每个结果。提交时把它与对应调用事件关联起来——这正是下一步派生所用的数据:

ts
function appendToolResult(session, turn, step, block, result, callSeq) {
  const message = createToolResultMessage({ callId: block.id, content: result.content, isError: result.isError })
  session.append('tool/result', {
    turn, step, message,
    ...result.error?.info ? { error: result.error.info } : {},
    ...result.meta !== undefined ? { meta: result.meta } : {},
  }, { surfaceOp: 'append', sourceEventSeqs: [callSeq] })
}

sourceEventSeqs: [callSeq] 引用所记录的 tool/call 事件——这种相邻关系正是回放所需要的。工具结果可能携带 concludesTurn,使 turn 在其 step 处结束;否则返回的 additionalContexts 会被拼接到 next-step inbox,让下一步把它们当作 user/message 上下文读取。完整流水线见 tool-presentation.md

循环的终止策略

TurnEndReason 表明结束时,turn 停止。持久的终止原因(来自 packages/core/session/src/types.ts):

原因含义
completed所有 step 正常结束;没有活跃的工具调用,也没有提交新的 steering
max-tokens至少一个 step 达到其输出 token 上限;即使后续 step 正常完成也保持粘滞
aborted取消请求打断了活跃 turn(携带 TurnEndCancelCause
blocked第一个提议的 step 被 agent/pre-step 拒绝
errorstep/turn 失败;永远是结构化的 LlmFailure(非 LlmError 抛错经 errorChain 扁平化为 UNKNOWN 码)
interrupted持久化在重载时关闭了因崩溃而孤儿化的 turn——循环从不主动发出

循环中唯一由数据驱动的提前停止工具循环,而不是策略:携带 concludesTurntool/result 会让 turn 在该 step 处结束。反向钩子是 agent/turn-stopping,一个在边界提交前运行的串行监听器,它可能引入新的 steering 工作。

错误处理

失败都会汇聚到 throwError,先实时报告为 agent/error,再进入驱动包含层。模型请求失败进入 agent/request-error waterfall,监听器可以返回 { kind: 'retry' }(默认 undefined 表示终止)。LlmError 会把结构化的事实原样保留在持久的 turn/end 中;其它错误扁平化为 UNKNOWN 码的文本。取消也被包含:中止时给跳过的工具调用记录合成错误结果,从而保证回放仍然有效。

会话与作用域

循环直接向 this.session(只追加的 SessionEvent 日志)追加事件,并随时通过 this.session.deriveMessages() 派生历史——它自身不保存任何 transcript 状态。它让每个驱动都在 ctx.agents.withInitiator() 内运行,因此从循环内到达的扩展代码都能读到发起智能体。它的作用域是 createScope(loopCtx, this)——活跃的 ReactLoopAgent 对象既充当作用域键又充当发起智能体,每次组装都通过 assembleContextFor 传入 scope: agent

延伸阅读

  • 智能体核心 —— 循环所实现的公共 Agent 句柄、注册表与 agent/* 事件。
  • 会话管理 —— 循环写入并从其派生的 SessionEvent 日志。
  • 作用域系统 —— 循环在其上构建每智能体作用域的两层式作用域注册。
  • 系统提示组装 —— preStep 所渲染的请求前缀。
  • 模型选择 —— 循环如何为每个请求挑选并冻结 provider/model。
  • 仓库源码:packages/core/agent-loop/src/agent.tspackages/core/agent-loop/src/tool-calls.ts