Skip to content

attemptApiRequest : appel LLM en streaming et retries

源码版本v4.0.10

Responsabilités

attemptApiRequest est la méthode générateur (generator) de Task responsable de « communiquer avec le provider LLM ». C'est une fonction async * qui yield les chunks du flux un à un au recursivelyMakeClineRequests appelant. Avant de yield, elle doit accomplir toute une série de préparatifs : attendre que les serveurs MCP soient connectés, lire les règles, assembler le system prompt, procéder à la troncature du contexte (context truncation) ; et en cas d'erreur sur le premier chunk, décider s'il faut retry automatiquement, demander à l'utilisateur, ou abandonner.

Elle se situe au niveau intermédiaire de l'agent loop. recursivelyMakeClineRequests appelle attemptApiRequest à chaque récursion, récupère le flux, puis c'est la boucle while externe qui consomme les chunks, les parse et appelle presentAssistantMessage. La méthode elle-même ne consomme pas le flux ; elle ne fait que « préparer le flux et le remettre de façon sûre ».

Son type de sortie est ApiStream, essentiellement un itérateur asynchrone. La couche appelante utilise d'abord iterator.next() pour tâter le premier chunk (first chunk probe:2389). Si le premier chunk réussit, on yield* iterator pour relayer tout le reste ; en cas d'échec, on passe à la classification d'erreur et à la logique de retry. Cette approche « tâter le premier chunk » sépare « échec avant le flux » et « échec pendant le flux », car le premier a un état propre et peut être retenté sans effet de bord, tandis que le second peut déjà avoir exécuté des outils partiels et ne peut pas être retenté simplement.

Motivation de conception

  • Attente MCP bornée : on utilise pWaitFor pour attendre que mcpHub.isConnecting passe à faux, avec un timeout de 10 secondes au-delà duquel on logge une error et on continue (mcp wait:2177). MCP ne doit pas bloquer la requête entière.
  • SystemPromptContext tout-en-un : on met cwd, infos IDE, provider, règles, clineignore, skills, onglets de l'éditeur, parallel tool calling, etc. dans un seul objet context (promptContext:2300). getSystemPrompt le consomme en une fois, pour éviter que la logique d'assemblage du prompt soit dispersée.
  • Troncature du contexte avant l'appel : contextManager.getNewContextMessagesAndMetadata est chargé de ramener l'historique de conversation dans la fenêtre de contexte du modèle (context truncate:2354). Si la troncature modifie la plage supprimée, on réécrit immédiatement clineMessages pour persister.
  • Stratégie de sondage du premier chunk : un flag isWaitingForFirstChunk + un try/catch dédié autour de iterator.next() ; en cas d'échec du premier chunk, l'état est encore propre et on peut retry sans risque (first chunk try:2388).
  • Classification des erreurs pour décider du retry : auth, spend limit, quota, entitlement, limite ClinePass, insufficient credits — toutes ces erreurs où « retry ne sert à rien » court-circuitent le retry automatique (shouldRetry gate:2495) ; les autres erreurs sont retry au plus 3 fois avec un backoff exponentiel 2s/4s/8s (backoff:2509).

Fichiers clés

Flux de données

Le chemin principal de la requête, de la préparation au yield, est « attendre MCP → assembler le prompt → tronquer l'historique → créer le flux → sonder → relayer ». L'étape de troncature de l'historique est clé, c'est elle qui détermine ce que le modèle voit réellement :

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
}

Le ContextManager prend l'historique complet de conversation + la plage supprimée précédemment enregistrée + la consommation en tokens de la requête précédente, et calcule la nouvelle plage de troncature. Si la plage a changé, on écrit immédiatement clineMessages sur disque — parce que la prochaine requête dépendra de cette nouvelle plage. useAutoCondense est le commutateur de condensation automatique réservé aux nouveaux modèles (next-gen) ; quand il est activé, le modèle génère lui-même un résumé qui remplace les premiers messages (autocondense flag:2362).

Une fois la troncature faite, on crée le flux et on sonde :

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) {
    // ... classification d'erreur, retry automatique, ou ask à l'utilisateur
    yield* this.attemptApiRequest(previousApiReqIndex);
    return;
}

Le flag isWaitingForFirstChunk signale à la couche externe « j'attends le premier chunk, ne me traite pas comme étant en milieu de flux ». En cas d'échec du premier chunk, le catch route selon le type d'erreur : si la fenêtre de contexte est dépassée et qu'on n'a pas déjà retenté automatiquement, on appelle handleContextWindowExceededError pour tronquer et retry ; pour les autres erreurs, shouldRetry décide entre retry automatique avec backoff ou ask("api_req_failed") qui laisse la décision à l'utilisateur.

Limites et échecs

  • Timeout MCP non fatal : si MCP ne se connecte pas dans les 10 secondes, on se contente de logger une error et on continue (mcp timeout catch:2179). Les outils MCP peuvent manquer dans le system prompt, mais la requête n'échoue pas pour autant.
  • Dépassement de contexte retenté une seule fois : le flag didAutomaticallyRetryFailedApiRequest garantit que handleContextWindowExceededError n'est appelé qu'une fois (auto retry flag:2407). Au deuxième dépassement, on bascule sur ask("api_req_failed") et on laisse l'utilisateur décider.
  • Conversation déjà courte mais toujours trop longue : si après troncature le nombre de messages est ≤ 3, on dit à l'utilisateur « context window exceeded, retry to truncate » mais on ne retente pas la troncature automatique (conversation bricked:2423). La conversation est essentiellement fichue dans ce cas, l'utilisateur doit intervenir.
  • Mise à jour de l'état api_req_started : à chaque retry, on retrouve le dernier message api_req_started et on met à jour streamingFailedMessage et retryStatus (update api_req_started:2433). L'UI s'appuie sur ce champ pour afficher « retry en cours » / « retry épuisé ».
  • Dédoublonnage de l'affichage error_retry : pendant un retry automatique, on fait d'abord say("error_retry", ...) avec l'info complète, puis on retire streamingFailedMessage de api_req_started pour éviter que la même erreur ne s'affiche à la fois dans ErrorRow et dans error_retry (dedupe error display:2548).
  • Remise à zéro manuelle du compteur de retry : quand l'utilisateur clique yes pour réessayer, on remet autoRetryAttempts à zéro (reset counter:2586), pour offrir 3 nouveaux retry automatiques en cas d'échec ultérieur.
  • Échec après le premier chunk non géré ici : un échec en milieu de flux après le premier chunk est capté par le try/catch externe de recursivelyMakeClineRequests (stream-mid failure:3935). Ici on ne gère que les échecs « avant le début du flux ».

Résumé

attemptApiRequest réunit trois choses : préparation, sondage, retry. La phase de préparation enchaîne règles, skills, MCP et troncature du contexte ; le sondage utilise le premier chunk pour décider s'il faut retry ; le retry s'appuie sur trois niveaux — classification d'erreurs, backoff exponentiel, décision utilisateur. Une fois le flux lancé, il est yield* à l'extérieur et n'est plus de son ressort. Pour voir comment le flux est consommé et découpé en blocs, aller à /agent-loop/present-assistant-message ; pour voir le pilote récursif au-dessus, aller à /agent-loop/task-class.

Voir la documentation officielle : documentation Cline · README.