










packages/core/agent-loop 中的 ReactLoopAgent 是默认实现,它实现 core/agent 定义的 Agent 接口——其他插件只依赖接口、不依赖循环,因此整个循环可以整体换掉。deriveMessages() 从追记式会话日志投影而来。idle | maintenance | running)门控,turn/start、step/start、user/message、assistant/chunk、tool/call、tool/result、step/end、turn/end 等所有边界都写入持久化会话日志——fork、resume、replay、telemetry 全部从同一条事件流派生。agent/pre-step(waterfall)、agent/request(waterfall)、agent/request-error(waterfall)、agent/turn-stopping(serial)。工具执行走三段流水线:tools/pre-execute → tools/execute → tools/post-execute。DeepSeek Harness agent loop 是驱动 Agent 持续工作的引擎:它读取输入、请求模型、执行工具,并判断 Agent 是否还欠模型一次请求。关于它最值得深挖的事实是:循环本身就是一个插件。
在 packages/core/agent-loop 中,默认实现是 ReactLoopAgent,它实现 core/agent 定义的 Agent 接口。其他插件只依赖这个接口,从不依赖具体的循环类。这一架构决策意味着 DeepSeek Harness agent loop 可以被整体替换:挂载一个实现 Agent 接口的其他插件,所有消费者照常工作。接口是契约,循环是实现,Harness 是组合。
这与 DeepSeek Harness「一切皆插件」的整体哲学一脉相承。Agent loop 不是特权核心组件——它是众多插件中的一个,与其它插件共享同一套事件系统。
专业提示: 读代码时,先从
core/agent的Agent接口入手,再读ReactLoopAgent。循环做的一切都是对该契约的响应,而接口正是你自己的循环插件必须满足的东西。
DeepSeek Harness agent loop 把工作组织成两个嵌套层次:turn 与 step。
模型可见的历史从不单独存储,而是通过 deriveMessages() 从追记式会话日志投影而来。模型能看到的一切都必须能从日志重建——这是 docs/architecture.md 明文记载的架构不变量。实际后果是:会话日志是唯一事实来源,模型的上下文窗口永远是它的派生视图。
| 层次 | 定义 | 边界事件 |
|---|---|---|
| turn | 零或多个 step;首条输入打开,模型不再欠回应时关闭 | turn/start、turn/end |
| step | 一次模型请求 + 它调用的工具 | step/start、step/end |
| message | step 内的用户或助手消息 | user/message、assistant/message |
DeepSeek Harness agent loop 的驱动器由一个小型状态机门控:Phase = idle | maintenance | running。
idle — 没有驱动器在跑,Agent 等待输入。maintenance — 驱动器拆除或准备期间的过渡状态。running — 驱动器在跨越多个连续 turn 的整个排空区间内活跃。状态切换发出 agent/status 事件,任何插件都能在不触碰循环的情况下观察其生命周期。这是 DeepSeek Harness agent loop 反复出现的模式:状态变化是事件,事件就是扩展面。
以下是 DeepSeek Harness agent loop 的完整流程,对应 packages/core/agent-loop/src/agent.ts 的实际代码。
输入通过 send / followup / steer / inject 进入,把消息塞进 Inbox 的两条有序队列(next-turn / next-step)。followup 和 steer 还会唤醒驱动器。
wakeDriver() — idle 时占一个 running phase(新 AbortController,turn=上一轮,step=0),在 ctx.agents.withInitiator(this, …) 里跑 kick()。kick() — while (await this.turn()) {}:只要有挂起输入就一直开新 turn。turn() 先 session.append('turn/start', { turn })——持久化开轮边界。然后循环处理每一步。
preStep() 做三件事:
inbox.claim(target, turn) — 认领本步的输入批次(全部 next-step 消息,轮边界时再带一条 next-turn)。认领是纯删除式 splice,每条消息发 agent/inbox/claimed。ctx.systemPrompt.assemble(...) — 组装 prompt 段与工具 schema。dispatch.waterfall('agent/pre-step', …) — 第一个扩展点。监听器可以 reject(不开步,turn 直接以 blocked 结束)或 enter 并改写消息批次;默认 enter 用认领的消息。返回 { reject } 或 { enter, messages, assembly }。特例: 第一步被改写为空 → turn 仍占边界但不花一次模型调用,以
completed结束。
session.append('step/start', { turn, step }) — 持久化。decision.messages 逐条 session.append('user/message', …) — 持久化。认领进来的输入此刻才落成 durable 用户消息。step() 内部循环(支持重试):
4a. 构建请求 buildRequest():
dispatch.waterfall('agent/request', …) — 第二个扩展点。监听器可替换冻结的调用配置(provider/model/reasoningEffort/maxTokens);默认用 agent 选项或已记录的 header。ctx.llm.prepareCall(config, signal) — 绑定到具体 adapter,物化 exact-model 默认值。session.append('request/header', …) 与 request/context(变化时才记)— 持久化,保证日志可重建请求。request(config + deriveMessages() 历史 + system + tools + sessionId + signal)。4b. 流式:
preparedCall.stream(request) ?? ctx.llm.stream(request) — 发请求、读流。for await chunk — 每片 session.append('assistant/chunk', …)(持久化,保原始流保真,供 replay/UI)+ 推进 BlockAssembler。4c. finish 分流 assembler.finish:
error / aborted → dispatch.waterfall('agent/request-error', …) — 第三个扩展点。监听器返回 { kind: 'retry' }(不调 next)则重试本步;默认 undefined 让失败终止(抛 LlmError)。max-tokens → 追加 assistant/message,返回 { kind: 'max-tokens' }。assistant/message({ turn, step, message, usage },sourceEventSeqs 引用对应 chunk)— 持久化。4d. 工具调用:
tool-call 块。没有 → 返回 { kind: 'completed' }(本步结束,模型不欠回应)。executeToolCalls(...)(tool-calls.ts)。finally: session.append('step/end', { turn, step }) — 持久化。turnEnds 非 null)且 inbox 没有 next-step 输入:dispatch.serial('agent/turn-stopping', { turn, signal }) — 第四个扩展点(serial,无 next)。监听器若反对,调 agent.steer(...) 塞 steering,机器重读 inbox → 再开一步;没人反对 → 关 turn。next-step 输入 → target='next-step',回第 2 步再开一个 step(工具延续)。finally: session.append('turn/end', { turn, reason: turnEnds }) — 持久化。reason 是 completed / max-tokens / blocked / aborted / error。step=0,返回 true(开新 turn);否则回 idle。DeepSeek Harness agent loop 的工具调度按 execution mode 分组:互斥调用是 barrier,并行调用用有界滚动池(maxParallelToolCalls)。每个调用走三段流水线,事件挂在 ctx.tools 的 scheduler 上——这是策略/超时/观测的挂载点:
tools/pre-execute → tools/execute → tools/post-execute
prepare(pre-execute)可能短路成直接结果。dispatch(execute)跑工具。finalize / finish(post-execute)收尾。两个细节值得强调。第一,结果按模型顺序提交(commitReady 跨连续槽推进),而不是按完成顺序——模型的世界观保持一致。第二,tool/call 在派发前追加(持久化),tool/result 在 post-execute 后追加(持久化,引用对应 call seq)。结果的 additionalContexts 进 next-step inbox,成为下个 step 边界的上下文;concludesTurn 的结果提前结束本 turn。
返回值是 { concluded }:concluded → { kind: 'completed' };否则返回 null,表示工具还欠一次模型请求——回到 4a 再跑一轮。
DeepSeek Harness agent loop 的整个设计可以浓缩成一张图:持久化事件(记录)与扩展点(接缝)。
turn/start (durable)
认领 inbox + 组装 prompt
─ agent/pre-step (waterfall) reject | enter(messages) ─
step/start (durable)
user/message* (durable)
─ agent/request (waterfall) 换配置 ─
llm/stream → assistant/chunk* (durable) → assistant/message (durable)
tool/call* (durable) → tools/pre-execute → tools/execute → tools/post-execute → tool/result* (durable)
─ agent/request-error (waterfall) 失败时 retry ─
step/end (durable)
工具还欠请求 or 有 next-step 输入 → 再开一步
─ agent/turn-stopping (serial) 没延续就关轮 ─
turn/end (durable)
| 扩展点 | 类型 | 插件能做什么 |
|---|---|---|
agent/pre-step |
waterfall | 拒绝本步,或 enter 并改写消息批次 |
agent/request |
waterfall | 替换冻结的调用配置(provider/model/effort/tokens) |
agent/request-error |
waterfall | 返回 { kind: 'retry' } 重试失败的 step |
agent/turn-stopping |
serial | 通过 steering 反对关轮;循环重读 inbox 再开一步 |
带 ─ ─ 的是插件可挂的扩展点(waterfall 必须 next() 才向下传);其余带 (durable) 的是写进会话日志的持久化事件——fork/resume/转写/telemetry 全从这条流派生。这套设计的意义就是:换掉某个 adapter、加策略、拦截请求/工具/turn,都是挂事件或换 provider,不需要改 loop 本身。
DeepSeek Harness agent loop 是插件这件事不是实现细节,而是产品本身。三个推论:
Agent 的插件即可,而不是维护一个 Harness 的 fork。ctx.tools scheduler 和四个扩展点上,而不是循环代码内部。✅ 最佳实践: 写自定义循环之前,先确认你的需求是不是一个可以挂在现有扩展点上的策略。DeepSeek Harness agent loop 的设计目标就是让大多数定制根本不碰循环。
DeepSeek Harness agent loop 与其它所有能力都是插件,因此围绕它的生态与核心同样重要。DSH Plugins 目录是发现和收录 DeepSeek Harness 插件的社区枢纽——包括循环替换、工具适配器、模型提供商、策略插件。为 DeepSeek Harness agent loop 开发时,把插件收录进去能让社区发现它;动手前先浏览,也能避免重复造轮子。
A: 是的。packages/core/agent-loop 提供 ReactLoopAgent 作为 core/agent 的 Agent 接口的默认实现。其他插件只依赖接口,因此挂载另一个插件即可整体替换循环。
A: turn 是零或多个 step,首条输入到达时打开,模型不再欠回应时关闭。step 是一次模型请求加上它调用的工具。两者在会话日志中都有持久化的开始/结束事件。
A: 不单独存储。DeepSeek Harness agent loop 通过 deriveMessages() 从追记式会话日志投影模型可见的历史。模型能看到的一切都必须能从日志重建。
A: 四个:agent/pre-step(waterfall——拒绝或改写输入批次)、agent/request(waterfall——替换调用配置)、agent/request-error(waterfall——失败重试)、agent/turn-stopping(serial——通过 steering 反对关轮)。
A: 按模型顺序提交(commitReady 跨连续槽推进),而不是按完成顺序,保证模型的世界观一致。
A: DSH Plugins 目录收录了 DeepSeek Harness 的社区插件,包括循环替换、工具适配器、策略插件等。
DeepSeek Harness agent loop 是可替换架构的教科书。通过定义 Agent 接口、把 ReactLoopAgent 作为众多实现之一、把每个边界持久化到追记式日志、并暴露四个 waterfall/serial 扩展点,循环把 agent harness 中最容易僵化的部分变成了最灵活的部分。
对开发者的实际启示:不理解循环也能用它,但理解了才能打开接缝。在 agent/request 拦截请求、在 agent/request-error 重试失败、在 agent/turn-stopping 门控关轮,或者用你自己的 Agent 实现整体替换循环。当你做出值得分享的东西,DSH Plugins 目录就是生态发现它的地方。循环是 DeepSeek Harness 的心脏——而且是一颗可以移植的心脏。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。