Skip to content

attemptApiRequest:流式 LLM 调用与重试

源码版本v4.0.10

职责

attemptApiRequest 是 Task 里负责「跟 LLM provider 通信」那一段的生成器 (generator) 方法。它是一个 async * 函数,yield 出一个个 stream chunk 给上层 recursivelyMakeClineRequests 消费。在 yield 之前,它要做一大堆准备工作:等 MCP 服务器连上、读规则文件、拼系统提示、做上下文截断 (context truncation);在第一个 chunk 出错时,它要决定是自动重试、是问用户、还是直接放弃。

它的位置在 agent loop 的中层。recursivelyMakeClineRequests 每递归一次就调 attemptApiRequest 一次,把流拿到后由外层 while 循环消费 chunk、解析、调 presentAssistantMessage。所以这个方法本身不消费流,它只负责「把流准备好并安全地交出去」。

它的输出类型是 ApiStream,本质是一个异步迭代器。上层先用 iterator.next() 拿第一个 chunk 试水 (first chunk probe:2389)。第一个 chunk 成功就 yield* iterator 把剩余全部转手;失败就走错误分类与重试逻辑。这种「先拿一个 chunk 探活」的设计,把「流前失败」和「流中失败」分开处理,因为前者状态干净,可以无副作用重试,后者可能已经执行了部分工具,不能简单重试。

设计动机

  • MCP 等待有上限:用 pWaitFormcpHub.isConnecting 转假,超时 10 秒就记个 error 继续往下走 (mcp wait:2177)。不让 MCP 卡死整个请求。
  • SystemPromptContext 一锅烩:把 cwd、IDE、provider 信息、规则文件、clineignore、技能、editor tabs、parallel tool calling 等全塞进一个 context 对象 (promptContext:2300)。getSystemPrompt 一次性消费,避免 prompt 拼接逻辑散落。
  • 上下文截断在调用前:contextManager.getNewContextMessagesAndMetadata 负责把对话历史裁到模型上下文窗口内 (context truncate:2354)。如果裁剪过程中发现需要更新删除范围,立刻写回 clineMessages 持久化。
  • 首 chunk 探活策略:用 isWaitingForFirstChunk 标志 + 单独 try/catch 包住 iterator.next(),首 chunk 失败时状态还是干净的,可以安全重试 (first chunk try:2388)。
  • 错误分类决定重试:auth、spend limit、quota、entitlement、ClinePass 限额、insufficient credits 这些「重试也没用」的错误跳过自动重试 (shouldRetry gate:2495);其他错误最多自动重试 3 次,延迟 2s/4s/8s 指数退避 (backoff:2509).

关键文件

数据流

请求从准备到 yield 的主路径是「MCP 等 → 拼 prompt → 截断历史 → 创建流 → 探活 → 转手」。截断历史这一步是关键,它决定模型实际看到什么:

typescript
// apps/vscode/src/core/task/index.ts
const contextManagementMetadata =
    await this.contextManager.getNewContextMessagesAndMetadata(
        this.messageStateHandler.getApiConversationHistory(),
        this.messageStateHandler.getClineMessages(),
        this.api,
        this.taskState.conversationHistoryDeletedRange,
        previousApiReqIndex,
        await ensureTaskDirectoryExists(this.taskId),
        this.stateManager.getGlobalSettingsKey("useAutoCondense") &&
            isNextGenModelFamily(this.api.getModel().id),
    );

if (contextManagementMetadata.updatedConversationHistoryDeletedRange) {
    this.taskState.conversationHistoryDeletedRange =
        contextManagementMetadata.conversationHistoryDeletedRange;
    await this.messageStateHandler.saveClineMessagesAndUpdateHistory();
    // saves task history item which we use to keep track of conversation history deleted range
}

ContextManager 拿完整对话历史 + 上次记录的删除范围 + 上次请求的 token 用量,算出新的截断范围。如果范围有变化,立即把 clineMessages 写盘——因为下一次请求要依赖这个新范围。useAutoCondense 是新模型 (next-gen) 专属的自动压缩开关,开启后会用模型自己生成摘要替代早期消息 (autocondense flag:2362)。

截断完成之后才创建流并探活:

typescript
// apps/vscode/src/core/task/index.ts
const stream = this.api.createMessage(
    systemPrompt,
    truncatedConversationHistory,
    tools,
);

const iterator = stream[Symbol.asyncIterator]();

try {
    // awaiting first chunk to see if it will throw an error
    this.taskState.isWaitingForFirstChunk = true;
    const firstChunk = await iterator.next();
    yield firstChunk.value;
    this.taskState.isWaitingForFirstChunk = false;
} catch (error) {
    // ... 错误分类、自动重试、或问用户
    yield* this.attemptApiRequest(previousApiReqIndex);
    return;
}

isWaitingForFirstChunk 标志告诉外层「我现在在等首 chunk,你别把我当流中状态处理」。首 chunk 失败的 catch 分支按错误类型分流:context window 超长且没自动重试过就调 handleContextWindowExceededError 自动截断重试;其他错误根据 shouldRetry 判断走自动退避重试或者 ask("api_req_failed") 把决定权交给用户。

边界与失败

  • MCP 超时不致命:MCP 10 秒连不上只记 error,继续往下走 (mcp timeout catch:2179)。后续 system prompt 里 MCP 工具可能就缺失,但请求不会因此失败。
  • 上下文超长只自动重试一次:didAutomaticallyRetryFailedApiRequest 标志保证 handleContextWindowExceededError 只调一次 (auto retry flag:2407)。第二次还超长就改走 ask("api_req_failed"),把决定权交给用户。
  • 对话已经很短还超长:如果截断后消息数 ≤ 3,直接告诉用户「context window exceeded, retry to truncate」但不再自动截断 (conversation bricked:2423)。这种情况对话基本废了,得用户介入。
  • api_req_started 状态更新:每次重试都会找最后一个 api_req_started 消息,更新它的 streamingFailedMessageretryStatus (update api_req_started:2433)。UI 靠这个字段展示「重试中」「重试耗尽」状态。
  • error_retry 双重展示去重:自动重试时会先 say("error_retry", ...) 完整信息,然后从 api_req_started 里删掉 streamingFailedMessage 防止同一个错误在 ErrorRow 和 error_retry 里同时显示 (dedupe error display:2548)。
  • 重试计数器手动清零:用户点 yes 重试时把 autoRetryAttempts 清零 (reset counter:2586),给后续失败再留 3 次自动重试空间。
  • 首 chunk 之后失败不在这里处理:首 chunk 成功后续流中失败由 recursivelyMakeClineRequests 的外层 try/catch 接管 (stream-mid failure:3935)。这里只管「流开始之前」的失败。

小结

attemptApiRequest 是「准备 + 探活 + 重试」三件事的合体。准备阶段把规则、技能、MCP、上下文裁剪都跑一遍,探活阶段用首 chunk 决定要不要重试,重试阶段用错误分类 + 指数退避 + 用户兜底三层兜底。流一旦开始就被它 yield* 出去不归它管了。要看流出去之后怎么被消费、怎么切成 block 推进,转 /agent-loop/present-assistant-message;要看更外层的递归驱动,转 /agent-loop/task-class

对照官方资料:Cline 文档 · README