Harness Engineering-第3章 Agent Loop:心跳与决策循环

《Harness Engineering --- AI Agent 工程方法论》完整目录

第3章 Agent Loop:心跳与决策循环

每一个 AI Agent 的核心都是一个循环------观察、思考、行动、再观察。这个循环的工程质量,决定了 Agent 是一个惊艳的 demo 还是一个可靠的生产系统。

3.1 从 OODA 到 Agent Loop

军事理论家 John Boyd 提出的 OODA 循环(Observe → Orient → Decide → Act)被广泛应用于决策理论。AI Agent 的核心循环本质上是 OODA 的工程化实现:

flowchart TD O["🔍 Observe\n接收输入/工具结果"] --> T["🧠 Think\n模型推理决策"] T --> D{"需要工具?"} D -->|是| A["⚡ Act\n执行工具调用"] A --> F["📥 Feedback\n工具返回结果"] F --> O D -->|否| R["✅ 输出最终回答"] style T fill:#fef3c7,stroke:#f59e0b style A fill:#dbeafe,stroke:#3b82f6 style R fill:#dcfce7,stroke:#22c55e
  • Observe:接收用户输入或上一轮工具执行的结果
  • Think:大模型基于当前上下文进行推理,决定下一步动作
  • Act:调用工具、生成代码、发送请求
  • Feedback:工具返回结果,成为下一轮 Observe 的输入

看起来简单,但魔鬼藏在每一个箭头里。模型可能产生幻觉调用不存在的工具,工具可能超时或失败,循环可能陷入无限重试。一个生产级的 Agent Loop 需要处理所有这些情况。

让我们先看两个真实系统是怎么做的。

3.2 Claude Code 的 Agent Loop 实现

Claude Code 的 Agent Loop 是一个经典的 while 循环实现。剥去日志、遥测等非核心逻辑后,其核心骨架如下:

typescript 复制代码
async function agentLoop(
  userMessage: string,
  context: ConversationContext
): Promise<void> {
  // 将用户消息加入对话历史
  context.messages.push({ role: "user", content: userMessage });

  let shouldContinue = true;

  while (shouldContinue) {
    // 1. Think: 调用模型
    const response = await queryModel({
      messages: context.messages,
      tools: context.availableTools,
      system: context.systemPrompt,
    });

    // 2. 将模型响应加入对话历史
    context.messages.push({ role: "assistant", content: response.content });

    // 3. 检查是否有工具调用
    const toolUses = response.content.filter(
      (block) => block.type === "tool_use"
    );

    if (toolUses.length === 0) {
      // 模型没有调用工具,说明它认为任务完成了
      shouldContinue = false;
      break;
    }

    // 4. Act: 执行工具调用(支持并行)
    const toolResults = await Promise.all(
      toolUses.map(async (toolUse) => {
        const result = await executeTool(toolUse.name, toolUse.input);
        return {
          type: "tool_result",
          tool_use_id: toolUse.id,
          content: result,
        };
      })
    );

    // 5. Feedback: 将工具结果加入对话历史
    context.messages.push({ role: "user", content: toolResults });

    // 6. 检查终止条件
    if (context.messages.length > MAX_TURNS) {
      shouldContinue = false;
    }
  }
}

这段代码揭示了几个关键设计决策:

决策一:循环的驱动力是工具调用。 模型不调用工具 = 任务完成。这是一个优雅的终止条件------不需要额外的"done"信号,模型自己通过行为表达"我做完了"。

决策二:工具并行执行。 Promise.all 意味着同一轮迭代中的多个工具调用是并发的。当模型同时请求读取三个文件时,三次 IO 并行发生,而不是串行等待。这在实践中能将某些迭代的耗时缩短数倍。

决策三:工具结果以 user 角色回传。 这是 Anthropic API 的约定------工具结果被包装成 user 消息,让模型在下一轮能"看到"工具的输出。这保持了对话的 user/assistant 交替结构。

但真实的 Claude Code 远比这复杂。让我们逐一看它处理的边界情况。

3.2.1 查询引擎与消息调度

Claude Code 的查询引擎不是简单地把消息丢给 API。它在发送之前要做大量预处理:

typescript 复制代码
async function queryModelWithPreprocessing(
  context: ConversationContext
): Promise<ModelResponse> {
  // 1. 上下文窗口管理:如果消息太多,截断早期内容
  const messages = truncateToFitContext(context.messages, {
    maxTokens: MODEL_CONTEXT_LIMIT,
    strategy: "keep-recent-and-system",
  });

  // 2. 注入系统提醒(system reminder)
  //    在最后一条 user 消息中附加实时上下文
  const enrichedMessages = injectSystemReminder(messages, {
    currentDir: process.cwd(),
    gitStatus: await getGitStatus(),
    activeFile: context.activeFile,
  });

  // 3. 工具过滤:根据权限模型过滤可用工具
  const tools = filterToolsByPermission(
    context.availableTools,
    context.permissionLevel
  );

  // 4. 发送请求,启用流式响应
  const stream = await anthropic.messages.stream({
    model: context.model,
    messages: enrichedMessages,
    tools,
    system: context.systemPrompt,
    max_tokens: 16384,
  });

  return stream;
}

注意第 2 步的 injectSystemReminder。这是 Claude Code 的一个精妙设计:每次循环迭代都会在消息末尾注入最新的环境状态(当前目录、git 状态等)。这确保模型始终基于最新信息做决策,而不是依赖几轮之前的过时上下文。这个机制我们会在第 8 章"提示词架构"中详细讨论。

3.2.2 流式处理与实时反馈

在生产系统中,模型的推理可能需要 10-30 秒。如果让用户干等一个 loading 动画,体验是灾难性的。Claude Code 通过流式处理解决这个问题:

typescript 复制代码
async function processStreamingResponse(
  stream: MessageStream,
  onText: (text: string) => void,
  onToolStart: (tool: ToolUse) => void,
  onToolEnd: (result: ToolResult) => void
): Promise<ModelResponse> {
  const contentBlocks: ContentBlock[] = [];
  let currentBlock: Partial<ContentBlock> | null = null;

  for await (const event of stream) {
    switch (event.type) {
      case "content_block_start":
        currentBlock = event.content_block;
        if (currentBlock.type === "tool_use") {
          onToolStart(currentBlock as ToolUse);
        }
        break;

      case "content_block_delta":
        if (event.delta.type === "text_delta") {
          // 文本流式输出------用户立刻看到模型的思考过程
          onText(event.delta.text);
        } else if (event.delta.type === "input_json_delta") {
          // 工具参数逐步到达------可以提前展示工具调用意图
          accumulateToolInput(currentBlock, event.delta.partial_json);
        }
        break;

      case "content_block_stop":
        contentBlocks.push(currentBlock as ContentBlock);
        currentBlock = null;
        break;

      case "message_stop":
        return { content: contentBlocks, stopReason: event.stop_reason };
    }
  }
}

流式处理不只是"好看"。它有两个关键工程价值:

  1. 用户感知延迟降低:第一个 token 到达的时间(TTFT)通常在 1-2 秒内,用户立刻知道系统在工作。
  2. 提前执行工具:某些实现会在工具参数完整但消息尚未结束时就开始执行工具,进一步压缩端到端延迟。

3.3 LangGraph 的状态机方法

如果说 Claude Code 的 Agent Loop 是"命令式的 while 循环",那 LangGraph 就是"声明式的状态图"。LangGraph 基于 Pregel 执行引擎,将 Agent Loop 建模为一个有向图的遍历过程。

python 复制代码
from langgraph.graph import StateGraph, END
from typing import TypedDict, Annotated

class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    iteration_count: int

def call_model(state: AgentState) -> dict:
    """Think 节点:调用模型"""
    messages = state["messages"]
    response = model.invoke(messages)
    return {
        "messages": [response],
        "iteration_count": state["iteration_count"] + 1,
    }

def call_tools(state: AgentState) -> dict:
    """Act 节点:执行工具"""
    last_message = state["messages"][-1]
    tool_calls = last_message.tool_calls

    results = []
    for call in tool_calls:
        tool = tool_registry[call["name"]]
        result = tool.invoke(call["args"])
        results.append(ToolMessage(content=result, tool_call_id=call["id"]))

    return {"messages": results}

def should_continue(state: AgentState) -> str:
    """路由函数:决定下一步去哪个节点"""
    last_message = state["messages"][-1]

    # 终止条件 1:模型没有调用工具
    if not last_message.tool_calls:
        return "end"

    # 终止条件 2:超过最大迭代次数
    if state["iteration_count"] >= MAX_ITERATIONS:
        return "end"

    return "tools"

# 构建状态图
graph = StateGraph(AgentState)
graph.add_node("model", call_model)
graph.add_node("tools", call_tools)
graph.set_entry_point("model")
graph.add_conditional_edges("model", should_continue, {
    "tools": "tools",
    "end": END,
})
graph.add_edge("tools", "model")

# 编译为可执行的 Runnable
agent = graph.compile()

这种方法和 while 循环在运行时行为上是等价的,但在工程特性上有显著差异:

特性 while 循环(Claude Code) 状态图(LangGraph)
可视化 需要额外日志 图结构天然可视化
中断恢复 需要手动序列化 状态快照内置
分支逻辑 if/else 嵌套 条件边,声明式
调试 断点+日志 节点级别 trace
并发控制 开发者自己管理 Pregel 引擎管理
灵活性 极高 受图结构约束

3.3.1 Pregel 执行引擎

LangGraph 的执行引擎借鉴了 Google 的 Pregel 图计算模型。每个"超级步"(superstep)中,所有就绪的节点并行执行,结果汇入共享状态,然后进入下一个超级步。

python 复制代码
class PregelExecutor:
    def __init__(self, graph, state):
        self.graph = graph
        self.state = state
        self.checkpoint_store = CheckpointStore()

    async def run(self):
        current_node = self.graph.entry_point

        while current_node != END:
            # 1. 保存检查点(支持中断恢复)
            self.checkpoint_store.save(self.state, step=current_node)

            # 2. 执行当前节点
            node_fn = self.graph.nodes[current_node]
            updates = await node_fn(self.state)

            # 3. 合并状态更新
            self.state = merge_state(self.state, updates)

            # 4. 评估出边,决定下一个节点
            edges = self.graph.get_edges(current_node)
            current_node = evaluate_edges(edges, self.state)

        return self.state

检查点机制是 LangGraph 的杀手级特性之一。当 Agent 执行到一半需要人工审批时(比如确认是否执行一个危险的数据库操作),可以保存当前状态、挂起执行、等待人工介入、然后从断点恢复。这种能力在 while 循环模式下需要大量额外工程来实现。

3.4 循环终止:最被低估的工程问题

一个失控的 Agent Loop 能在几分钟内烧掉几十美元的 API 费用,更糟糕的是可能执行数百次无意义的工具调用对外部系统造成副作用。循环终止条件的设计是 Agent Loop 中最被低估的工程问题。

flowchart TD L["循环迭代完成"] --> C1{"模型停止调用工具?"} C1 -->|是| T1["✅ 自然终止"] C1 -->|否| C2{"迭代次数 ≥ 上限?"} C2 -->|是| T2["⛔ 强制终止\n(max iterations)"] C2 -->|否| C3{"Token 预算耗尽?"} C3 -->|是| T3["⛔ 预算终止\n(token budget)"] C3 -->|否| C4{"墙钟超时?"} C4 -->|是| T4["⛔ 超时终止\n(wall time)"] C4 -->|否| C5{"检测到死循环?"} C5 -->|是| T5["⛔ 熔断终止\n(circuit breaker)"] C5 -->|否| N["继续下一轮迭代"] style T1 fill:#dcfce7,stroke:#22c55e style T2 fill:#fee2e2,stroke:#ef4444 style T3 fill:#fee2e2,stroke:#ef4444 style T4 fill:#fee2e2,stroke:#ef4444 style T5 fill:#fee2e2,stroke:#ef4444

3.4.1 终止条件的完整清单

一个生产级的 Agent Loop 至少需要以下终止条件:

typescript 复制代码
function checkTermination(context: LoopContext): TerminationReason | null {
  // 1. 自然终止:模型不再调用工具
  if (context.lastResponse.stopReason === "end_turn") {
    return { reason: "natural", message: "模型认为任务完成" };
  }

  // 2. 迭代上限
  if (context.iterationCount >= context.maxIterations) {
    return {
      reason: "max_iterations",
      message: `达到最大迭代次数 ${context.maxIterations}`,
    };
  }

  // 3. Token 预算耗尽
  if (context.totalTokensUsed >= context.tokenBudget) {
    return {
      reason: "token_budget",
      message: `Token 使用量 ${context.totalTokensUsed} 超出预算`,
    };
  }

  // 4. 时间超限
  if (Date.now() - context.startTime >= context.timeoutMs) {
    return {
      reason: "timeout",
      message: `执行时间超过 ${context.timeoutMs / 1000} 秒`,
    };
  }

  // 5. 循环检测:连续相同的工具调用
  if (detectRepeatingPattern(context.toolCallHistory)) {
    return {
      reason: "loop_detected",
      message: "检测到重复的工具调用模式",
    };
  }

  // 6. 致命错误累积
  if (context.consecutiveErrors >= context.maxConsecutiveErrors) {
    return {
      reason: "error_threshold",
      message: `连续 ${context.consecutiveErrors} 次错误`,
    };
  }

  return null; // 继续执行
}

3.4.2 循环检测的工程实现

循环检测值得单独讨论。最简单的方法是检查连续 N 次工具调用是否完全相同:

typescript 复制代码
function detectRepeatingPattern(
  history: ToolCall[],
  windowSize: number = 3
): boolean {
  if (history.length < windowSize * 2) return false;

  const recent = history.slice(-windowSize);
  const previous = history.slice(-windowSize * 2, -windowSize);

  // 比较最近的 N 次调用和之前的 N 次调用是否相同
  return recent.every(
    (call, i) =>
      call.name === previous[i].name &&
      JSON.stringify(call.input) === JSON.stringify(previous[i].input)
  );
}

但实际中更常见的是"语义循环"------模型不断尝试略有不同的参数调用同一个工具,每次都失败。对付这种情况需要更高级的策略:

typescript 复制代码
function detectSemanticLoop(
  history: ToolCall[],
  windowSize: number = 5
): boolean {
  if (history.length < windowSize) return false;

  const recent = history.slice(-windowSize);

  // 统计最近 N 次中同一个工具被调用的比例
  const toolCounts = new Map<string, number>();
  for (const call of recent) {
    toolCounts.set(call.name, (toolCounts.get(call.name) || 0) + 1);
  }

  // 如果同一个工具占了 80% 以上的调用,且都返回了错误
  for (const [toolName, count] of toolCounts) {
    if (count / windowSize >= 0.8) {
      const recentResults = recent
        .filter((c) => c.name === toolName)
        .map((c) => c.result);
      const allFailed = recentResults.every((r) => r.isError);
      if (allFailed) return true;
    }
  }

  return false;
}

3.5 错误处理:让循环具备韧性

在生产环境中,Agent Loop 中的每一步都可能失败。工具可能超时,API 可能限流,模型可能返回畸形的 JSON。一个健壮的循环需要在不同层次处理错误。

3.5.1 工具级错误处理

最常见的错误发生在工具执行阶段。关键原则是:不要让工具的失败终止循环,而是把错误信息交给模型,让模型决定下一步。

typescript 复制代码
async function executeToolSafely(
  toolName: string,
  toolInput: unknown
): Promise<ToolResult> {
  try {
    // 设置单个工具的执行超时
    const result = await withTimeout(
      executeTool(toolName, toolInput),
      TOOL_TIMEOUT_MS
    );

    return { content: result, isError: false };
  } catch (error) {
    if (error instanceof TimeoutError) {
      return {
        content: `工具 ${toolName} 执行超时(${TOOL_TIMEOUT_MS / 1000}秒)。` +
          `请尝试减小操作范围或使用其他方式。`,
        isError: true,
      };
    }

    if (error instanceof PermissionError) {
      return {
        content: `权限不足:${error.message}。该操作需要用户确认。`,
        isError: true,
      };
    }

    // 未知错误:返回有用的错误信息,但不暴露内部细节
    return {
      content: `工具 ${toolName} 执行失败:${error.message}。` +
        `你可以尝试不同的参数或使用其他工具。`,
      isError: true,
    };
  }
}

注意错误消息的措辞。我们不只是说"失败了",而是给模型提供行动建议:"请尝试减小操作范围"、"你可以尝试不同的参数"。这种设计让模型有机会自我纠正,而不是在相同的错误上反复碰壁。

3.5.2 模型级错误处理

模型本身也可能出错------返回无效的工具调用、产生幻觉工具名、或者 API 请求失败:

typescript 复制代码
async function queryModelWithRetry(
  messages: Message[],
  tools: Tool[],
  maxRetries: number = 3
): Promise<ModelResponse> {
  let lastError: Error | null = null;

  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await queryModel(messages, tools);

      // 验证响应中的工具调用是否合法
      for (const block of response.content) {
        if (block.type === "tool_use") {
          if (!tools.some((t) => t.name === block.name)) {
            // 模型幻觉了一个不存在的工具
            // 不重试,而是将错误信息反馈给模型
            return createErrorFeedback(
              `工具 "${block.name}" 不存在。可用工具:${tools.map((t) => t.name).join(", ")}`
            );
          }
        }
      }

      return response;
    } catch (error) {
      lastError = error;

      if (error instanceof RateLimitError) {
        // 限流:指数退避重试
        const backoffMs = Math.min(1000 * 2 ** attempt, 30000);
        await sleep(backoffMs);
        continue;
      }

      if (error instanceof OverloadedError) {
        // 服务过载:等待更长时间
        await sleep(5000 * (attempt + 1));
        continue;
      }

      // 其他错误不重试
      throw error;
    }
  }

  throw new MaxRetriesError(`模型调用失败 ${maxRetries} 次`, lastError);
}

这里有一个细微但重要的区别:网络层错误(限流、过载)通过重试处理,逻辑层错误(幻觉工具名)通过反馈处理。 重试是对外部环境的容错;反馈是对模型行为的纠正。把它们混为一谈是很多 Agent 系统不稳定的根源。

3.6 并发:单次迭代内的并行执行

Claude Code 支持模型在一次响应中返回多个工具调用,并且并行执行它们。这个能力在实际使用中非常重要------想象模型需要读取 5 个文件来理解一个 bug,如果串行读取需要 5 次 IO,并行只需要 1 次。

但并行执行带来了新的工程问题:

typescript 复制代码
async function executeToolsConcurrently(
  toolUses: ToolUse[],
  concurrencyLimit: number = 10
): Promise<ToolResult[]> {
  // 使用信号量控制并发数
  const semaphore = new Semaphore(concurrencyLimit);
  const results: ToolResult[] = [];

  const promises = toolUses.map(async (toolUse, index) => {
    await semaphore.acquire();
    try {
      const result = await executeToolSafely(toolUse.name, toolUse.input);
      results[index] = {
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: result.content,
        is_error: result.isError,
      };
    } finally {
      semaphore.release();
    }
  });

  // 使用 allSettled 而非 all------一个工具失败不应阻塞其他工具
  await Promise.allSettled(promises);

  return results;
}

关键设计点:

  1. 并发上限:不设上限可能导致系统资源耗尽。Claude Code 默认限制为合理的并发数。
  2. allSettled 而非 allPromise.all 在任一 Promise 失败时就会 reject。但工具 A 的失败不应该影响工具 B 的结果。allSettled 确保所有工具都有机会完成。
  3. 结果顺序保持 :通过 results[index] 保证结果数组的顺序与工具调用数组一致,这对模型理解结果至关重要。

3.6.1 工具间的依赖与冲突

并行执行还要考虑工具之间的语义冲突。比如模型同时调用"写入文件 A"和"读取文件 A"------执行顺序会影响结果的正确性。

Claude Code 的策略是简洁的:信任模型。 如果模型同时发出了两个工具调用,它应该对并行执行有预期。在实践中,模型几乎总是在同一批次中发出无依赖的并行请求(比如同时读取多个不同的文件),很少出现冲突情况。

但如果你在构建自己的 Agent 系统且无法完全信任模型的判断,可以引入工具冲突检测:

typescript 复制代码
function partitionByConflict(toolUses: ToolUse[]): ToolUse[][] {
  const groups: ToolUse[][] = [];
  const resourceLocks = new Map<string, number>();

  for (const toolUse of toolUses) {
    const resources = getAffectedResources(toolUse);
    let conflictGroup = -1;

    for (const resource of resources) {
      if (resourceLocks.has(resource)) {
        conflictGroup = Math.max(conflictGroup, resourceLocks.get(resource)!);
      }
    }

    const targetGroup = conflictGroup + 1;
    if (!groups[targetGroup]) groups[targetGroup] = [];
    groups[targetGroup].push(toolUse);

    for (const resource of resources) {
      resourceLocks.set(resource, targetGroup);
    }
  }

  return groups; // 组内并行,组间串行
}

3.7 心跳机制:让用户知道"我还活着"

Agent 执行复杂任务可能需要几分钟。在这段时间里,用户最大的焦虑来源不是"慢",而是"不知道在干什么"。心跳机制(heartbeat)解决的就是这个问题。

3.7.1 多层级的进度反馈

一个完善的心跳系统应该提供多个层级的信息:

typescript 复制代码
interface HeartbeatSystem {
  // 第一层:循环级别------当前是第几轮迭代
  onIterationStart(iteration: number, totalEstimate?: number): void;

  // 第二层:工具级别------正在执行什么工具
  onToolStart(toolName: string, toolInput: unknown): void;
  onToolEnd(toolName: string, result: ToolResult): void;

  // 第三层:文本级别------模型正在生成什么内容
  onTextDelta(text: string): void;

  // 第四层:元信息------资源消耗情况
  onMetrics(metrics: {
    tokensUsed: number;
    elapsedMs: number;
    toolCallCount: number;
  }): void;
}

Claude Code 的实现是终端 UI 驱动的。每次工具开始执行时,界面上会显示一个旋转的 spinner 和工具名称;执行完成后显示结果摘要。模型生成的文本则实时流式输出。这看起来是 UI 细节,但它本质上是 Agent Loop 的一部分------反馈回路不只包括给模型的反馈,也包括给用户的反馈。

3.7.2 进度估算

心跳的一个进阶需求是进度估算。用户不仅想知道"在干什么",还想知道"大概还要多久"。

typescript 复制代码
class ProgressEstimator {
  private history: { taskType: string; iterations: number; durationMs: number }[] = [];

  estimateProgress(
    currentIteration: number,
    taskType: string
  ): { percent: number; remainingMs: number } | null {
    // 基于历史数据估算
    const similar = this.history.filter((h) => h.taskType === taskType);
    if (similar.length < 3) return null; // 数据不足,不估算

    const avgIterations = similar.reduce((s, h) => s + h.iterations, 0) / similar.length;
    const avgDuration = similar.reduce((s, h) => s + h.durationMs, 0) / similar.length;

    const percent = Math.min((currentIteration / avgIterations) * 100, 95);
    const remainingMs = Math.max(
      avgDuration * (1 - currentIteration / avgIterations),
      0
    );

    return { percent, remainingMs };
  }

  recordCompletion(taskType: string, iterations: number, durationMs: number): void {
    this.history.push({ taskType, iterations, durationMs });
    // 只保留最近 100 条记录
    if (this.history.length > 100) this.history.shift();
  }
}

注意 Math.min(..., 95) 这个细节。永远不要告诉用户"99% 完成"然后让他们再等 2 分钟------这比不给进度条更让人抓狂。上限设为 95%,直到真正完成才跳到 100%。

3.8 完整的生产级 Agent Loop

把前面讨论的所有机制组合起来,一个生产级的 Agent Loop 长这样:

typescript 复制代码
async function productionAgentLoop(
  userMessage: string,
  context: ConversationContext,
  heartbeat: HeartbeatSystem
): Promise<AgentResult> {
  context.messages.push({ role: "user", content: userMessage });

  const startTime = Date.now();
  let iterationCount = 0;

  while (true) {
    iterationCount++;
    heartbeat.onIterationStart(iterationCount);

    // ---- Think ----
    let response: ModelResponse;
    try {
      response = await queryModelWithRetry(
        preprocessMessages(context),
        getPermittedTools(context),
        3 // max retries
      );
    } catch (error) {
      return {
        status: "error",
        message: `模型调用失败:${error.message}`,
        iterations: iterationCount,
      };
    }

    // 流式输出模型的文本内容
    for (const block of response.content) {
      if (block.type === "text") {
        heartbeat.onTextDelta(block.text);
      }
    }

    context.messages.push({ role: "assistant", content: response.content });

    // ---- 提取工具调用 ----
    const toolUses = response.content.filter(
      (block) => block.type === "tool_use"
    );

    // ---- 检查终止条件 ----
    const termination = checkTermination({
      lastResponse: response,
      iterationCount,
      startTime,
      maxIterations: context.config.maxIterations ?? 50,
      timeoutMs: context.config.timeoutMs ?? 600_000,
      totalTokensUsed: context.totalTokens,
      tokenBudget: context.config.tokenBudget ?? 1_000_000,
      toolCallHistory: context.toolCallHistory,
      consecutiveErrors: context.consecutiveErrors,
      maxConsecutiveErrors: 5,
    });

    if (termination) {
      heartbeat.onMetrics({
        tokensUsed: context.totalTokens,
        elapsedMs: Date.now() - startTime,
        toolCallCount: context.toolCallHistory.length,
      });
      return {
        status: termination.reason === "natural" ? "success" : "terminated",
        message: termination.message,
        iterations: iterationCount,
      };
    }

    // ---- Act: 并行执行工具 ----
    for (const toolUse of toolUses) {
      heartbeat.onToolStart(toolUse.name, toolUse.input);
    }

    const toolResults = await executeToolsConcurrently(toolUses);

    for (let i = 0; i < toolUses.length; i++) {
      heartbeat.onToolEnd(toolUses[i].name, toolResults[i]);
      context.toolCallHistory.push({
        name: toolUses[i].name,
        input: toolUses[i].input,
        result: toolResults[i],
      });

      // 更新连续错误计数
      if (toolResults[i].is_error) {
        context.consecutiveErrors++;
      } else {
        context.consecutiveErrors = 0;
      }
    }

    // ---- Feedback: 工具结果回传 ----
    context.messages.push({ role: "user", content: toolResults });
  }
}

3.9 方法论提炼

回顾本章内容,我们可以提炼出关于 Agent Loop 设计的几条核心方法论:

方法论一:终止条件是首要设计对象。 大多数人设计 Agent Loop 时先想"怎么跑起来",但生产系统中最先爆出问题的是"怎么停下来"。你的终止条件清单至少应该包括:自然终止、迭代上限、token 预算、时间超限、循环检测、错误累积阈值。

方法论二:错误分层处理,不要一刀切。 网络层错误用重试解决,逻辑层错误用反馈解决,资源层错误用终止解决。把工具失败的信息交给模型而不是直接抛异常,这是让 Agent 具备自愈能力的关键。

方法论三:心跳不是锦上添花,是基础设施。 对于任何执行时间超过 5 秒的 Agent 操作,都应该有进度反馈机制。流式输出、工具执行状态、迭代计数------这些信息让用户从"等待黑盒"变成"观察同事工作"。

方法论四:并发是性能的乘数,但要有控制。 并行工具执行可以显著缩短端到端延迟,但需要设置并发上限、使用 allSettled 容错、保持结果顺序。在无法确保工具间无冲突时,按资源分组串行执行。

方法论五:循环和状态图是两种等价的表达方式,选择取决于你的需求。 如果需要中断恢复、可视化调试、复杂的分支逻辑,状态图(LangGraph)更合适。如果追求极致的灵活性和最小的抽象开销,while 循环(Claude Code)更直接。没有"更好"的方案,只有"更适合"的方案。

下一章我们将讨论 Agent Loop 中最关键的输入------上下文。如何在有限的上下文窗口中塞入最有用的信息,是决定 Agent 能力上限的核心工程问题。

相关推荐
山间小僧16 分钟前
「AI学习笔记」Agent Memory(二)跨会话记忆:从聊天记录到可演化的长期记忆
aigc·agent·vibecoding
dong_junshuai7 小时前
每天一个开源项目#99 OpenResearch:2.2K星的本地研究Agent工作台
开源·github·agent
花椒技术7 小时前
把 AI 视频接进直播:提前生成、按次生成、持续生成怎么选?
agent·音视频开发
用户8082598666877 小时前
用 LangGraph 重写自己手写的 Agent:框架替你解决了什么,什么它不管
agent
能不能静下心来看7 小时前
从 RAG 到 Agent:跑通两个 Demo 后,我终于分清了这两个词
agent
ZGi.ai7 小时前
知识库权限变了,怎样避免答错?
agent·权限管理·知识库·工作流·zgi·客服运营
大模型真好玩8 小时前
DeepSeek Harness 入门很简单(四)——DeepSeek Harness接入插件
人工智能·agent·deepseek
掰头战士8 小时前
MCP、Skill、Plugin,都是给agent拓展能力,到底有何区别?
typescript·llm·agent
嘟嘟嘟95279 小时前
AI Agent 的边缘困境
人工智能·架构·agent
HYDtomako9 小时前
Agent checkpoint设计
ai·agent·checkpoint·claude code