Agent Loop 状态机与驱动机制¶
如果说 Cordis 是 DSH 的骨架,那么 Agent Loop 就是 DSH 跳动的心脏。
Agent Loop 负责调度 LLM 请求、工具调用执行、多轮对话流转以及异常中断恢复。DSH 对 Agent 的生命周期做出了业界极为清晰严格的数学级划分:Turn(轮次) 与 Step(步骤)。
核心文件速查¶
| 路径 | 职责 | 重要度 |
|---|---|---|
packages/core/agent-loop/src/agent.ts |
ReactLoopAgent 类:核心循环驱动状态机 |
⭐⭐⭐ |
packages/core/agent/src/inbox.ts |
优先级消息邮箱与事件唤醒机制 | ⭐⭐⭐ |
packages/core/agent/src/runtime-types.ts |
Agent 接口定义与生命周期状态类型 | ⭐⭐⭐ |
packages/core/agent/src/dispatch.ts |
Waterfall 事件发射与拦截派发器 | ⭐⭐⭐ |
核心概念:Turn vs Step¶
在很多初学者甚至部分 Agent 库中,"一次用户发问"与"一次大模型调用"经常被混为一谈。在 DSH 中,两者有极其严格的界定:
- Step(步骤):单次模型请求 + 伴随的工具调用执行。
- Turn(轮次):从用户输入到达开始,到所有派生工作(包括连续多步工具执行、多轮思考)全部平息(Settled)为止的完整业务闭环。一个 Turn 包含 0 个、1 个或多个 Step。
User Input Arrival
│
▼
┌───────────┐
│turn/start │
└─────┬─────┘
│
├──────────────────────────────────────────────┐
│ │
▼ (Step Loop) ▼
┌───────────┐ ┌───────────────┐
│step/start │ │ (No work left)│
└─────┬─────┘ └───────┬───────┘
│ │
▼ │
┌──────────────────────────────────────────┐ │
│ 1. 组装 Prompt 与工具 Schema │ │
│ 2. agent/pre-step (Waterfall 拦截与改写) │ │
│ 3. agent/request -> llm/stream │ │
│ 4. 接收 assistant/chunk* 流式输出 │ │
│ 5. assistant/message (落盘) │ │
│ 6. 解析 tool/call* 并执行工具流水线 │ │
│ 7. tools/pre-execute -> execute -> post │ │
│ 8. 记录 tool/result* (落盘) │ │
└─────┬────────────────────────────────────┘ │
│ │
▼ │
┌───────────┐ │
│ step/end │ │
└─────┬─────┘ │
│ │
├────► [工具产生结果,需要模型继续分析] ────────┘
│
▼ (所有工具执行完毕,模型无新增调用,邮箱无待办)
┌───────────┐
│ turn/end │
└───────────┘
逐行精读:ReactLoopAgent.runLoop()¶
让我们打开 packages/core/agent-loop/src/agent.ts,阅读核心执行循环:
// packages/core/agent-loop/src/agent.ts
private async runTurn(): Promise<void> {
// 1. 发射持久化会话事件 turn/start
this.session.append({ type: 'turn/start', turnId })
try {
while (!this.teardown.signal.aborted) {
// 2. 从 Inbox 领取下一批待处理输入(包括用户消息、系统注入提示)
const inputs = this.inbox.claim()
if (!inputs && !this.toolsOweWork) {
break // 无剩余工作,结束当前 Turn
}
// 3. 组装当前 Step 的系统提示词与工具 Schema
const prompt = await this.systemPrompt.assemble(this.ctx)
const schemas = this.tools.getSchemas()
// 4. Waterfall 事件:agent/pre-step
// 允许插件(如安全守卫、上下文注入器)拦截、改写或直接拒绝本次 Step 输入
const preStepResult = await emitAgentEvent(this.ctx, 'agent/pre-step', {
agent: this,
inputs,
prompt,
})
if (preStepResult.status === 'reject') {
// 被守卫拦截,记录拒绝事件,结束该 Step
break
}
// 5. 开始 Step
this.session.append({ type: 'step/start', stepId })
// 6. 派生 LLM 历史消息并请求模型
const history = deriveMessages(this.session.events)
const stream = await this.llm.stream({
messages: history,
system: prompt,
tools: schemas,
})
// 7. 处理流式输出并记录 assistant/chunk / assistant/message
const assistantMessage = await this.consumeLlmStream(stream)
// 8. 如果模型发起了 Tool Calls,并发/串行进入工具执行流水线
if (assistantMessage.toolCalls?.length) {
await this.executeTools(assistantMessage.toolCalls)
this.toolsOweWork = true // 标记工具已产生结果,需要下一次 Step 喂给模型
} else {
this.toolsOweWork = false
}
this.session.append({ type: 'step/end', stepId })
}
} finally {
// 9. 发射 turn/end,完成本轮业务
this.session.append({ type: 'turn/end', turnId })
}
}
优先级消息邮箱:Inbox 机制¶
DSH 设计了一个非阻塞、带优先级的 Inbox 机制(packages/core/agent/src/inbox.ts):
- 普通用户输入(User Message):进入常规队列,立刻唤醒正在休眠的 Agent Loop;
- 后台子任务通知(Subagent Settlement):子智能体执行完毕后以系统通知形式投递到 Inbox;
- 静默注入上下文(Silent Context Injection):一些扩展插件可以向 Inbox 塞入上下文片段,但不主动唤醒 Agent,直到用户下一次发问时一并打包带给模型。
本章思考与自测¶
- 思考题:在
agent/pre-step中,如果守卫插件直接将本次输入改写为空(Empty),DSH 为什么仍然要求落盘一个不包含 Step 的turn/start和turn/end? - 自测题:如果在工具执行期间,用户又向当前 Agent 发送了一条新的消息,DSH 是立刻打断正在执行的工具,还是暂存到 Inbox 中?为什么这样设计?