DeepSeek Harness 05:Agent Loop——控制平面与状态机推进

DeepSeek Harness 05:Agent Loop——控制平面与状态机推进
Asaakii把 Agent 的核心调度逻辑写成几行代码非常简单:
1 | # 简单的玩具代码:隐藏了所有的真实工程隐患 |
这种朴素的循环完全无法抵御真实生产环境的风暴:
- 如果用户在第二轮工具执行到一半时忽然插话:“别跑测试了,赶紧回滚代码”,循环如何打断?
- 如果进程在第 15 步由于系统 OOM 异常退出,重启后凭什么恢复到刚刚执行完的状态?
- 如果模型反复陷入逻辑死循环,如何从外部强制接管?
DeepSeek Harness 的设计把 Agent 推进过程抽象为一个具备严谨状态机语义的控制平面(Control Plane)。
一、控制平面与执行平面的物理分离
在 DSH 中,系统被清晰划分为两个彼此隔离的平面:
- 控制平面(Control Plane):由大语言模型和 Agent Loop 组成。它的核心职责是“提出下一步假说”,它本身绝对不能直接触碰任何现实副作用。
- 执行平面(Execution Plane):由受控的沙箱、文件系统驱动和网络通信管道组成。它负责执行具体命令,并将产出的确定性结果反哺给事实源。
flowchart TD
subgraph ControlPlane ["控制平面 (Control Plane)"]
TurnStart["turn/start: 领取用户指令"] --> PreStep["agent/pre-step: 准入与改写"]
PreStep --> StepStart["step/start: 步进开始"]
StepStart --> Derive["deriveMessages(): 从日志派生上下文"]
Derive --> Freeze["冻结请求体 (Object.freeze)"]
Freeze --> LLMStream["调用模型并捕获流式输出"]
LLMStream --> AppendAssistant["写入 assistant 消息事实"]
end
subgraph ExecutionPlane ["执行平面 (Execution Plane)"]
ToolPipeline["Tool Pipeline 执行与安全拦截"]
RealWorld["产生真实副作用 (写文件 / 运行命令)"]
end
AppendAssistant --> ToolPipeline
ToolPipeline --> RealWorld
RealWorld --> AppendResult["写入 tool/result 事实"]
AppendResult --> StepEnd["step/end: 步进结算"]
StepEnd --> CheckLoop{"任务是否继续?"}
CheckLoop -->|"未完结"| StepStart
CheckLoop -->|"已达成"| TurnEnd["turn/end: 轮次收束"]
在这个闭环中,所有的驱动都以**事实写入日志(Session Log)**为唯一分界线。控制流的任何决策,都不能依赖主进程内存里悄悄维护的临时变量,一切状态变更必须先落盘为不可篡改的事实。
二、三级执行生命周期:Turn、Step 与 Request
为了精准治理不同时间跨度的问题,DSH 将生命周期严格切分为三个层级:
| 生命周期层次 | 时间与逻辑跨度 | 核心治理与拦截职责 |
|---|---|---|
| Turn(轮次) | 从用户提交一条意图开始,直到任务宣告彻底完成或被用户中止 | 绑定用户原始目标、全局 Token 预算上限、最终状态结算 |
| Step(步进) | 一次模型推理与其所触发的一批工具调用的完整执行闭环 | 局部上下文切片、工具并发批次调度、单步取消与重试 |
| Request(物理请求) | 向特定大模型供应商发起的一次具体 HTTP 网络通信 | 供应商路由选择、传输超时控制、网络级重试与退避 |
[!NOTE]
一个关键细节:没有 Step 的 Turn
如果用户的输入在刚进入系统时就被安全策略或预检查拦截(例如输入命中非法敏感词),系统依然会生成一个完整的 Turn 记录,但其中包含 0 个 Step。系统必须如实记录“尝试进入执行但被策略拦截”这一客观事实,绝不能假装这次交互从未发生过。
三、铁律:模型可见历史必须能从日志严格派生
在一些非严谨的 Agent 框架中,存在一种常见的“捷径做法”:在将请求发给模型的前一秒,通过某个拦截钩子悄悄往 messages 数组里硬塞一段临时提示词(例如动态注入当前系统时间或某段上下文)。
DSH 在架构层完全封死了这种偷跑逻辑:
- 模型每一次调用所看到的完整历史,必须严格通过
deriveMessages()从 Session Log 中确定性派生出来。 - Loop 构造完成的请求体,在进入模型适配器前会通过
Object.freeze()进行深度递归冻结。 - 运行时配备了严格的不变量检查器(Invariants Checker):在请求发出前后,校验日志中是否存在当前 Step 记录、请求消息是否与日志投影严格一致。
这一铁律彻底杜绝了“幽灵状态”:在任何时候需要审计、Fork 或恢复任务时,回放系统都能精准重现模型做出决策时所面对的世界。
四、拦截点责任分层机制
为了实现高度的可扩展性,Agent Loop 在不同节点暴露了细粒度的拦截点,但每个拦截点都有其严格的职责红线:
| 拦截接缝 | 拥有的决策权 | 坚决禁止的行为 |
|---|---|---|
agent/pre-step |
准入控制:允许、改写或拒绝刚刚领取的输入 | 直接触发任何具有系统副作用的工具执行 |
agent/request |
决定本次请求所使用的 Provider、模型配置与降级策略 | 绕过日志私自修改已派生的消息历史数组 |
llm/stream |
横切传输包裹:流式性能统计、测试 Mock、内容实时审计 | 破坏 StreamChunk 流式协议的不变量结构 |
agent/request-error |
供应商错误处理:退避重试、路由切换或宣告终止 | 将发生的错误从日志事实源中抹掉 |
tools/* |
工具权限校验、沙箱执行与展示投影转换 | 将模型的推测选择直接等同为用户授权 |
session/event |
只读观察:异步持久化、指标上报、外部监控 | 试图逆向修改已经写入日志的历史事实 |
五、取消与恢复的真实边界
在真实工程中,用户经常需要通过界面打断执行。Agent Loop 采用基于 AbortSignal 的协作式取消机制。
但我们必须清醒地认识到:取消是协作式的终止,绝不是现实副作用的时光倒流!
- 如果模型发起的
rm -rf命令已经提交给了执行平面,即使上层发送了取消信号,已经删除的文件也无法自动复原。 - 当任务被取消后,系统会如实将
aborted状态记入日志。当用户下一次唤醒任务时,Agent Loop 会根据已经落盘的事实重新评估:哪些副作用已经落地,哪些未完成,进而规划后续的补偿或重跑策略。
[!WARNING]
评测视角的警示:模型声明完成 $\neq$ 任务真正完成
在第三方对抗评测(如coding-agent-harness-eval)中发现:尽管 DSH 的状态机完备度很高,但依然存在“模型在turn/end主观声明任务已解决,而实际后端代码根本没有跑通测试”的现象。模型是概率系统,turn-stopping只是它的自我评估。生产系统的最终验收,必须依赖确定的外部自动化测试(如 pytest / npm test)或人工审批进行二次强制校验。











