乐于分享
好东西不私藏

DeepSeek Harness 源码解读:Agent 执行引擎的设计哲学

DeepSeek Harness 源码解读:Agent 执行引擎的设计哲学

逐行拆解 turn() 方法:从"一次模型调用"到"一个完整的 Agent 回合"


01 一个问题:为什么大多数 AI 只会"接一句"?

你用过很多 AI 助手,有没有发现一个现象:

你问一句,它答一句。然后停了

哪怕它的回答里写了"让我查一下资料",实际上也不会真的去查。哪怕它说"我需要分几步完成",实际上也不会自动继续下一步。

为什么?

因为调用一次大模型 API,和让一个 Agent 自主运转,是完全不同的两件事

前者是"函数调用",后者是"状态机管理"。

DeepSeek Harness 的 turn() 方法,就是解决这个问题的核心答案。今天,我们逐行拆解这段不到 100 行的代码,看看它如何把"一次模型调用"变成"一个完整的 Agent 回合"。


02 先看骨架:一个 Turn,里面套着多个 Step

private async turn(): Promise<boolean> {
  // 回合开始

  const
 turn = phase.turn + 1
  this
.session.append('turn/start', { turn })

  // 核心:步骤循环

  while
 (true) {
    const
 step = phase.step + 1
    const
 decision = await this.preStep(target, { turn, step })
    // ...

    const
 stepEnd = await this.step(decision.assembly)
    // ...

    if
 (turnEnds && inboxEmpty) break
    target = 'next-step'
  }

  // 回合结束

  this
.session.append('turn/end', { turn, reason: turnEnds })
  return
 hasMoreWork
}

这是整段代码最震撼的设计:

一个 Turn 不是一次模型调用,而是一个 while (true) 循环。

什么意思?

普通框架的逻辑是:

用户提问 → 调用模型 → 返回答案 → 结束

Harness 的逻辑是:

Turn 开始
  Step 1: 思考/行动
  Step 2: 发现 inbox 有新消息,继续
  Step 3: 工具返回结果,继续
  Step 4: 模型说"我做完了"
Turn 结束(或进入下一个 Turn)

while (true) 这一行,就是 Agent 从"被动应答"走向"主动运转"的分水岭。


03 拆解三步:决策、执行、收敛

第一步:preStep —— 不是想跑就能跑

const decision = await this.preStep(target, { turn, step })

if
 (decision.kind === 'reject') {
  turnEnds = { kind: 'blocked' }
  return
 false
}

在真正调用模型之前,Harness 先问一层"决策层":

  • • 当前 Agent 有没有权限执行?
  • • 消息队列里有没有有效输入?
  • • 安全策略是否允许继续?

这相当于给模型加了一道**"闸门"**。如果决策层说"不",整个 Turn 直接标记为 blocked,不会浪费一次模型调用。

这是生产环境的关键设计。 实验室里的 Demo 可以跳过这一步,但线上系统必须有。


第二步:step —— 真正调用模型

for (const message of decision.messages) {
  this
.session.append('user/message', message)
}
const
 stepEnd = await this.step(decision.assembly)

这一步才是我们熟悉的"调大模型"。但 Harness 做了两件事,让它不再是简单的 API 调用:

第一,消息被写入了 Session。

不是调完就丢,而是每一条消息都通过 session.append 记录下来。这意味着整个 Agent 的运行过程是完全可观测、可回放、可审计的。

第二,返回值 stepEnd 不是字符串,而是状态。

模型这次输出是正常完成?还是触发了 max-tokens?还是报错了?Harness 用结构化状态记录,而不是靠解析文本猜。


第三步:收敛判断 —— 什么时候停下来?

if (turnEnds && this.inbox.nextStep.length === 0) {
  await
 this.dispatch.serial('agent/turn-stopping', { turn, signal })
  if
 (turnEnds && this.inbox.nextStep.length === 0) break
}

这段代码是整个循环的"刹车系统"

停下来的条件有两个:

  1. 1. 当前 Step 给出了结束信号turnEnds 有值)
  2. 2. 消息队列为空(没有新的外部输入需要处理)

但注意,这里触发了一个事件 agent/turn-stopping。这意味着**"准备停车"是一个可被拦截的过程**——其他模块可以监听这个事件,做收尾工作,甚至阻止停车。

这不是"一刀切"的结束,而是**"优雅收敛"**。


04 两个魔鬼细节:Sticky 和 Abort

细节一:Sticky max-tokens

if (turnEnds === null || turnEnds.kind !== 'max-tokens') {
  turnEnds = stepEnd
}

这段代码很短,但极其深刻

它的意思是:一旦某个 Step 触发了 max-tokens(输出被截断),整个 Turn 的最终状态就必须保持为 max-tokens,后续正常完成的 Step 不能把它"覆盖"掉。

为什么?

因为 max-tokens 不是普通的结束,它意味着模型还没说完。如果你让后面的 Step 把它覆盖成 completed,你就丢失了一个关键信号:这个 Turn 的输出是不完整的。

这就是"粘性状态"设计。 它体现了 Harness 对 LLM 行为边界的深刻理解。


细节二:AbortSignal 贯穿始终

const { signal } = phase.abort
signal.throwIfAborted()
// ... 循环中多次检查 ...

if
 (signal.aborted) {
  turnEnds = { kind: 'aborted', reason: signal.reason }
  throw
 error
}

整个 Turn 从始至终都绑定了一个 AbortSignal

这意味着:外部可以在任何时刻优雅地取消这个 Turn——不是暴力杀进程,而是让循环自然走到一个安全点,记录 aborted 状态,然后干净退出。

对于需要长时间运行的 Agent 来说,这是生死攸关的设计


05 错误处理:不是 try-catch,是结构化归因

turnEnds = {
  kind
: 'error',
  error
: error instanceof LlmError
    ? error.failure
    : { message: errorChain(error), code: 'UNKNOWN' },
}
this
.throwError(error)

Harness 不把错误当成"异常"来恐慌,而是当成**"状态"来记录**。

  • • 如果是 LlmError(模型侧错误),保留原始结构,方便上游重试或降级。
  • • 如果是未知异常,统一包装为 UNKNOWN,附带完整的错误链。

在生产环境里,"知道为什么失败"和"成功"一样重要。


06 收尾:一次状态重置,准备下一次奔跑

if (!this.inbox.hasPending) return false

phase.abort = new AbortController()
phase.wakeRequested = false
phase.step = 0
return
 true

如果 inbox 没有待处理消息,返回 false——告诉调度器:"我干完了,别叫我了。"

如果还有消息,则重置状态

  • • 新建一个 AbortController(旧的 signal 已失效)
  • • 清除唤醒标志
  • • Step 计数归零

然后返回 true——"请继续调度我。"

这不是结束,是下一次开始的准备。


07 总结:这段代码到底做对了什么?

普通 LLM 调用
Harness 的 turn()
一次请求,一次响应
一个 Turn,多个 Step
出错就崩
结构化归因,优雅降级
无法中断
AbortSignal 全链路可控
黑盒运行
Session 事件流全程可观测
输出长度超限被忽略
Sticky max-tokens 状态保留
被动应答
主动收敛,自主判断何时结束

08 写在最后

很多人以为,Agent 的核心是"模型能力"——参数够不够大,上下文够不够长。

但看完这段代码你会发现:

真正让 Agent 从"玩具"变成"生产工具"的,不是模型,而是运行时。

while (true) 这一行,写起来只要几秒,但它背后是对状态机、可观测性、容错、并发控制的深刻理解。

DeepSeek Harness 的 turn() 方法,就是 Agent 运行时的教科书级实现。

下次当你写 Agent 框架时,不妨问问自己:

我的 Agent,有真正的"回合"概念吗?能在一次任务里连续思考多步吗?出错了能优雅降级吗?能被随时安全取消吗?

如果答案都是"是",恭喜你,你的 Agent 真正"活"过来了。


本文基于 DeepSeek Harness 开源代码分析,如有理解偏差,欢迎指正。