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