écrits/tutorial/2026/07
Tutorial22 juil. 2026·28 min

Intégration de Gemini 3.6 Flash avec TypeScript : Agents pensants et appels d'outils parallèles

Apprenez à intégrer Gemini 3.6 Flash dans des applications TypeScript. Ce tutoriel couvre le mode thinking avec budgets configurables, les appels de fonctions parallèles, le streaming des tokens de réflexion et un agent de recherche complet sous Next.js 15.

Gemini 3.6 Flash est arrivé le 21 juillet 2026 avec trois capacités qui changent la donne pour les développeurs qui construisent des agents : un budget de réflexion configurable qui expose le raisonnement interne du modèle, de véritables appels d'outils parallèles en un seul tour, et une réduction de 17 % des tokens de sortie sur les workflows multi-étapes complexes par rapport à Flash 3.5. La baisse de prix à 1,50 USD par million de tokens d'entrée et 7,50 USD par million de tokens de sortie en fait le modèle pensant le plus rentable de la gamme Google.

Ce tutoriel parcourt chaque couche — complétion de base, configuration du mode thinking, streaming des tokens de réflexion, appels de fonctions parallèles, et un agent de recherche complet construit comme une route SSE Next.js 15.

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 20+ et pnpm installés
  • Un compte Google AI Studio avec une clé API (le niveau gratuit suffit)
  • Une bonne maîtrise des patterns async/await TypeScript
  • Des connaissances de base sur Next.js 15 App Router

Ce que vous allez construire

Un agent de recherche "Market Intel" qui :

  1. Accepte une requête en langage naturel sur une entreprise ou un marché
  2. Utilise le mode thinking de Gemini 3.6 Flash pour planifier son approche de recherche
  3. Exécute trois appels d'outils en parallèle — cours boursiers, actualités récentes et données concurrentielles
  4. Stream le raisonnement du modèle et l'analyse finale vers un client React
  5. Expose le pipeline complet comme une route API Next.js 15 avec Server-Sent Events

À la fin de ce tutoriel, vous disposerez d'un pattern prêt pour la production pour tout agent qui doit réfléchir à ce qu'il doit collecter avant de collecter.

Étape 1 : Configuration du projet

Créez une application Next.js 15 et installez le SDK Google Generative AI :

pnpm create next-app@latest market-intel --typescript --tailwind --app --no-src-dir
cd market-intel
pnpm add @google/genai zod

Créez un fichier .env.local à la racine du projet :

GEMINI_API_KEY=your_key_from_ai_studio

Obtenez votre clé depuis Google AI Studio. Le niveau gratuit inclut 15 requêtes par minute et 1 500 requêtes par jour, largement suffisant pour le développement.

Étape 2 : Configurer le client Gemini

Créez un module client partagé pour que tous les fichiers utilisent la même instance et la même constante de modèle :

// lib/gemini.ts
import { GoogleGenAI } from "@google/genai";
 
if (!process.env.GEMINI_API_KEY) {
  throw new Error("GEMINI_API_KEY is not set in .env.local");
}
 
export const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
 
export const MODEL = "gemini-3.6-flash";

Vérifiez la configuration avec un script rapide avant de construire quoi que ce soit de complexe :

// scripts/test-connection.ts
import { ai, MODEL } from "../lib/gemini";
 
const response = await ai.models.generateContent({
  model: MODEL,
  contents: "Reply with: Gemini 3.6 Flash is online.",
});
 
console.log(response.text);

Lancez-le avec :

npx tsx scripts/test-connection.ts

Vous devriez voir le message de confirmation en une à deux secondes.

Étape 3 : Activer le mode thinking

Gemini 3.6 Flash prend en charge une configuration thinkingBudget qui correspond aux trois niveaux visibles dans l'interface Google Antigravity. Le budget est mesuré en tokens de réflexion — les tokens de raisonnement interne que le modèle utilise avant de générer sa réponse.

Valeur du budgetNiveau AntigravityMeilleur cas d'usage
512–2048Low (faible)Génération CRUD, classification, Q&R simples
4096–8192Medium (moyen)Développement de fonctionnalités, requêtes de recherche, analyse
-1 (illimité)High (élevé)Décisions architecturales, raisonnement complexe multi-étapes
0DésactivéVitesse maximale sans raisonnement visible

Créez un helper qui fait correspondre les noms de niveaux à la valeur de budget correcte :

// lib/thinking.ts
export type ThinkingTier = "low" | "medium" | "high" | "disabled";
 
export function thinkingConfig(tier: ThinkingTier) {
  const budgets: Record<ThinkingTier, number> = {
    low: 1024,
    medium: 8192,
    high: -1,
    disabled: 0,
  };
  return { thinkingBudget: budgets[tier] };
}

Conseil : Commencez chaque nouvel agent avec le niveau "medium". Exécutez la même tâche dix fois avec "medium" et "high" et comparez la qualité. Dans la plupart des cas pratiques, medium est indiscernable de high pour environ la moitié du coût en tokens de réflexion.

Étape 4 : Streamer les tokens de réflexion

Les modèles pensants émettent deux catégories de contenu dans le flux : les parties thought (le raisonnement interne, coloré en ambre dans l'interface Antigravity) et les parties text (la réponse visible). Votre application peut afficher les deux.

// lib/stream-agent.ts
import { ai, MODEL } from "./gemini";
import { thinkingConfig } from "./thinking";
 
export type StreamChunk =
  | { type: "thinking"; text: string }
  | { type: "response"; text: string };
 
export async function* streamWithThinking(
  prompt: string,
  tier: "low" | "medium" | "high" = "medium"
): AsyncGenerator<StreamChunk> {
  const stream = await ai.models.generateContentStream({
    model: MODEL,
    contents: prompt,
    config: {
      thinkingConfig: thinkingConfig(tier),
    },
  });
 
  for await (const chunk of stream) {
    const parts = chunk.candidates?.[0]?.content?.parts ?? [];
    for (const part of parts) {
      if (part.thought && part.text) {
        yield { type: "thinking", text: part.text };
      } else if (part.text) {
        yield { type: "response", text: part.text };
      }
    }
  }
}

Testez le flux dans un petit script :

// scripts/test-thinking.ts
import { streamWithThinking } from "../lib/stream-agent";
 
for await (const chunk of streamWithThinking(
  "Comparez PostgreSQL et SQLite pour une application mobile-first.",
  "medium"
)) {
  if (chunk.type === "thinking") process.stdout.write("[T] " + chunk.text);
  else process.stdout.write("[R] " + chunk.text);
}

Les lignes [T] montrent le modèle qui évalue les compromis avant de s'engager sur la réponse finale — un raisonnement qui serait autrement invisible.

Étape 5 : Définir les déclarations d'outils

Gemini 3.6 Flash utilise le même format de déclaration de fonctions que les versions Gemini précédentes. Définissez chaque outil comme une FunctionDeclaration avec un objet de paramètres JSON Schema :

// lib/tools.ts
import type { FunctionDeclaration } from "@google/genai";
 
export const researchTools: FunctionDeclaration[] = [
  {
    name: "get_stock_quote",
    description:
      "Get the current stock price, market cap, and key financial ratios for a ticker symbol.",
    parameters: {
      type: "object",
      properties: {
        symbol: {
          type: "string",
          description: "Stock ticker e.g. NVDA, MSFT, GOOGL",
        },
      },
      required: ["symbol"],
    },
  },
  {
    name: "search_news",
    description: "Search recent news articles for a company or topic.",
    parameters: {
      type: "object",
      properties: {
        query: { type: "string", description: "News search query" },
        days: {
          type: "integer",
          description: "Number of past days to search. Default is 7.",
        },
      },
      required: ["query"],
    },
  },
  {
    name: "get_competitors",
    description:
      "Return a list of direct competitors and approximate market share for a company.",
    parameters: {
      type: "object",
      properties: {
        company: {
          type: "string",
          description: "Company name or ticker symbol",
        },
      },
      required: ["company"],
    },
  },
];

Étape 6 : Implémenter les gestionnaires d'outils

Dans ce tutoriel, les gestionnaires retournent des données fictives. En production, vous remplacerez chaque fonction par un vrai appel API :

// lib/tool-handlers.ts
export async function executeToolCall(
  name: string,
  args: Record<string, unknown>
): Promise<unknown> {
  switch (name) {
    case "get_stock_quote":
      return getStockQuote(args.symbol as string);
    case "search_news":
      return searchNews(args.query as string, (args.days as number) ?? 7);
    case "get_competitors":
      return getCompetitors(args.company as string);
    default:
      throw new Error(`Unknown tool: ${name}`);
  }
}
 
function getStockQuote(symbol: string) {
  const data: Record<string, object> = {
    NVDA: { price: 1247.5, change: "+3.2%", marketCap: "3.1T", peRatio: 48.2 },
    MSFT: { price: 512.3, change: "+0.8%", marketCap: "3.8T", peRatio: 35.1 },
    GOOGL: { price: 198.45, change: "+1.5%", marketCap: "2.4T", peRatio: 28.7 },
  };
  return data[symbol.toUpperCase()] ?? { error: `Symbol ${symbol} not found` };
}
 
function searchNews(query: string, days: number) {
  return {
    articles: [
      {
        title: `${query}: Key Developments`,
        summary:
          "Les dépenses en infrastructure IA continuent d'accélérer...",
        source: "Reuters",
        publishedAt: new Date().toISOString(),
      },
    ],
    totalResults: 1,
    daysSearched: days,
  };
}
 
function getCompetitors(company: string) {
  return {
    company,
    competitors: [
      { name: "AMD", marketShare: "18%", focus: "GPU, CPU" },
      { name: "Intel", marketShare: "12%", focus: "CPU, GPU" },
      { name: "Qualcomm", marketShare: "8%", focus: "Edge AI, Mobile" },
    ],
  };
}

Étape 7 : Construire la boucle d'agent parallèle

L'insight crucial lorsque vous travaillez avec les appels d'outils de Gemini 3.6 Flash : quand le modèle retourne plusieurs parties functionCall dans un seul tour de réponse, il demande explicitement une exécution parallèle. Les exécuter séquentiellement ajoute une latence inutile — utilisez Promise.all pour déclencher tous les appels simultanément, puis envoyez tous les résultats dans un seul tour utilisateur :

// lib/agent-loop.ts
import type { Content, FunctionCall, Part } from "@google/genai";
import { ai, MODEL } from "./gemini";
import { thinkingConfig } from "./thinking";
import { researchTools } from "./tools";
import { executeToolCall } from "./tool-handlers";
 
export interface AgentResult {
  thoughts: string[];
  answer: string;
  toolCalls: Array<{ name: string; args: Record<string, unknown> }>;
}
 
export async function runResearchAgent(query: string): Promise<AgentResult> {
  const history: Content[] = [
    { role: "user", parts: [{ text: query }] },
  ];
  const thoughts: string[] = [];
  const toolCalls: AgentResult["toolCalls"] = [];
 
  for (let turn = 0; turn < 6; turn++) {
    const response = await ai.models.generateContent({
      model: MODEL,
      contents: history,
      config: {
        tools: [{ functionDeclarations: researchTools }],
        thinkingConfig: thinkingConfig("medium"),
      },
    });
 
    const parts = response.candidates?.[0]?.content?.parts ?? [];
 
    for (const part of parts) {
      if (part.thought && part.text) {
        thoughts.push(part.text);
      }
    }
 
    const fnCalls = parts.filter(
      (p): p is Part & { functionCall: FunctionCall } => !!p.functionCall
    );
 
    if (fnCalls.length === 0) {
      const answer = parts
        .filter((p) => !p.thought && p.text)
        .map((p) => p.text ?? "")
        .join("");
      return { thoughts, answer, toolCalls };
    }
 
    history.push({ role: "model", parts });
 
    for (const { functionCall } of fnCalls) {
      toolCalls.push({
        name: functionCall.name,
        args: (functionCall.args ?? {}) as Record<string, unknown>,
      });
    }
 
    // Exécuter tous les appels d'outils en parallèle
    const toolResults = await Promise.all(
      fnCalls.map(async ({ functionCall }) => {
        const result = await executeToolCall(
          functionCall.name,
          (functionCall.args ?? {}) as Record<string, unknown>
        );
        return {
          functionResponse: {
            name: functionCall.name,
            response: { output: result },
          },
        };
      })
    );
 
    history.push({ role: "user", parts: toolResults });
  }
 
  return {
    thoughts,
    answer: "Recherche terminée — nombre maximum d'itérations atteint.",
    toolCalls,
  };
}

Étape 8 : Route API Next.js 15 avec SSE

Exposez l'agent comme un endpoint Server-Sent Events en streaming. Le client recevra des mises à jour en temps réel pour les tokens de réflexion, l'exécution des outils et la réponse finale :

// app/api/research/route.ts
import { NextRequest } from "next/server";
import type { Content, FunctionCall, Part } from "@google/genai";
import { ai, MODEL } from "@/lib/gemini";
import { thinkingConfig } from "@/lib/thinking";
import { researchTools } from "@/lib/tools";
import { executeToolCall } from "@/lib/tool-handlers";
 
export const maxDuration = 120;
 
export async function POST(req: NextRequest) {
  const { query } = await req.json() as { query: string };
  const encoder = new TextEncoder();
 
  const stream = new ReadableStream({
    async start(controller) {
      const send = (data: object) =>
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        );
 
      const history: Content[] = [
        { role: "user", parts: [{ text: query }] },
      ];
 
      for (let turn = 0; turn < 6; turn++) {
        const response = await ai.models.generateContent({
          model: MODEL,
          contents: history,
          config: {
            tools: [{ functionDeclarations: researchTools }],
            thinkingConfig: thinkingConfig("medium"),
          },
        });
 
        const parts = response.candidates?.[0]?.content?.parts ?? [];
 
        for (const part of parts) {
          if (part.thought && part.text) {
            send({ type: "thinking", text: part.text });
          }
        }
 
        const fnCalls = parts.filter(
          (p): p is Part & { functionCall: FunctionCall } => !!p.functionCall
        );
 
        if (fnCalls.length === 0) {
          const answer = parts
            .filter((p) => !p.thought && p.text)
            .map((p) => p.text ?? "")
            .join("");
          send({ type: "result", text: answer });
          break;
        }
 
        history.push({ role: "model", parts });
        send({ type: "tools_start", names: fnCalls.map((f) => f.functionCall.name) });
 
        const toolResults = await Promise.all(
          fnCalls.map(async ({ functionCall }) => {
            const result = await executeToolCall(
              functionCall.name,
              (functionCall.args ?? {}) as Record<string, unknown>
            );
            send({ type: "tool_done", name: functionCall.name });
            return {
              functionResponse: {
                name: functionCall.name,
                response: { output: result },
              },
            };
          })
        );
 
        history.push({ role: "user", parts: toolResults });
      }
 
      controller.close();
    },
  });
 
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Étape 9 : Composant client React

Construisez un tableau de bord simple qui affiche le flux SSE en temps réel :

// app/page.tsx
"use client";
 
import { useState } from "react";
 
type Event =
  | { type: "thinking"; text: string }
  | { type: "tools_start"; names: string[] }
  | { type: "tool_done"; name: string }
  | { type: "result"; text: string };
 
export default function ResearchPage() {
  const [query, setQuery] = useState("");
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(false);
 
  async function runResearch() {
    setEvents([]);
    setLoading(true);
 
    const res = await fetch("/api/research", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ query }),
    });
 
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
 
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
 
      for (const line of decoder.decode(value).split("\n")) {
        if (!line.startsWith("data: ")) continue;
        try {
          const event = JSON.parse(line.slice(6)) as Event;
          setEvents((prev) => [...prev, event]);
        } catch {
          // ignorer les chunks incomplets
        }
      }
    }
 
    setLoading(false);
  }
 
  return (
    <main className="max-w-3xl mx-auto p-8 space-y-6">
      <h1 className="text-2xl font-bold">Agent Market Intel</h1>
      <p className="text-gray-600 text-sm">
        Propulsé par Gemini 3.6 Flash avec mode thinking et exécution parallèle des outils.
      </p>
 
      <div className="flex gap-2">
        <input
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && !loading && query && runResearch()}
          placeholder="ex : paysage concurrentiel de NVIDIA dans les puces IA"
          className="flex-1 border rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          onClick={runResearch}
          disabled={loading || !query.trim()}
          className="bg-blue-600 text-white px-5 py-2 rounded-lg disabled:opacity-50 hover:bg-blue-700 transition-colors"
        >
          {loading ? "Recherche…" : "Rechercher"}
        </button>
      </div>
 
      <div className="space-y-2">
        {events.map((event, i) => (
          <div
            key={i}
            className={`rounded-lg p-3 text-sm ${
              event.type === "thinking"
                ? "bg-amber-50 border border-amber-200 text-amber-900 font-mono text-xs"
                : event.type === "tools_start"
                ? "bg-blue-50 border border-blue-200 text-blue-800"
                : event.type === "tool_done"
                ? "bg-emerald-50 border border-emerald-200 text-emerald-800"
                : "bg-white border shadow-sm whitespace-pre-wrap"
            }`}
          >
            {event.type === "thinking" && (
              <span>
                <span className="font-semibold">Réflexion : </span>
                {event.text}
              </span>
            )}
            {event.type === "tools_start" && (
              <span>
                <span className="font-semibold">Appels parallèles : </span>
                {event.names.join(", ")}
              </span>
            )}
            {event.type === "tool_done" && (
              <span>
                <span className="font-semibold">Terminé : </span>
                {event.name}
              </span>
            )}
            {event.type === "result" && (
              <div>
                <p className="font-semibold mb-2">Analyse :</p>
                {event.text}
              </div>
            )}
          </div>
        ))}
      </div>
    </main>
  );
}

Patterns d'optimisation des coûts

Gemini 3.6 Flash est déjà le modèle pensant le moins cher de sa catégorie, mais plusieurs patterns réduisent encore les coûts :

Dimensionner le budget de réflexion. Pour les requêtes simples, réglez thinkingBudget à 512 voire 0. Les tokens de réflexion comptent comme des tokens de sortie à 7,50 USD/M.

Mettre en cache le prompt système. Si vous ajoutez une instruction système avec du contexte ou des données de fond, ce texte se répète à chaque tour. Placez-le dans le premier message avec un indicateur de cache pour ne le payer qu'une seule fois.

Court-circuiter avec un mémo. Dans les boucles d'agents où le même outil peut être appelé avec des arguments identiques, maintenez une Map indexée par name + JSON.stringify(args) et retournez le résultat mis en cache plutôt que d'effectuer un second appel.

Grouper les requêtes liées. La fenêtre de contexte d'un million de tokens permet d'envoyer dix requêtes d'entreprises dans un seul appel avec un prompt JSON structuré et d'obtenir dix réponses, plutôt que dix requêtes API séparées.

Résolution des problèmes courants

401 API_KEY_INVALID — Vérifiez que la clé provient de Google AI Studio et non d'un compte de service Vertex AI ; les deux systèmes d'authentification sont distincts.

thinkingConfig sans effet — Confirmez que le modèle est gemini-3.6-flash. La variante Flash-Lite (gemini-3.6-flash-lite) ne supporte pas le mode thinking et ignore le champ thinkingBudget.

Parties thought vides — Les tokens de réflexion nécessitent que thinkingBudget soit supérieur à 0. Régler thinkingBudget: 0 désactive entièrement la réflexion pour une vitesse maximale. Utilisez au moins 512 pour voir la sortie de raisonnement.

Boucle d'agent sans fin — Ajoutez une limite stricte de tours comme dans l'exemple ci-dessus. Si la boucle persiste, loguez history à chaque itération et cherchez un appel d'outil qui retourne systématiquement une erreur.

Erreurs CORS dans le navigateur — N'appelez jamais l'API Gemini directement depuis JavaScript côté client ; cela expose votre clé API. Routez toujours via une route API Next.js ou une Server Action comme montré à l'étape 8.

Prochaines étapes

  • Remplacez les gestionnaires d'outils fictifs par Alpha Vantage pour les données boursières réelles et Tavily pour les actualités vérifiées
  • Remplacez la boucle SSE manuelle par useChat de Vercel AI SDK 7 avec un adaptateur de fournisseur google
  • Explorez l'outil code_execution intégré de Google pour l'analyse de données Python dans la boucle d'agent
  • Essayez l'API Live de Gemini pour les requêtes vocales en temps réel
  • Routez Gemini via votre propre gateway pour la visibilité des coûts et la limitation de débit : tutoriel LiteLLM Proxy

Conclusion

Gemini 3.6 Flash réunit le raisonnement en mode thinking, l'exécution parallèle des outils et une fenêtre de contexte d'un million de tokens à un prix qui rend les agents multi-étapes économiquement viables en production. La réduction de 17 % en tokens de sortie par workflow complexe n'est pas qu'une métrique marketing — elle se traduit directement par moins de tours d'agent, des factures allégées et des réponses plus rapides. Avec les patterns couverts dans ce tutoriel — configuration du thinkingBudget, boucle d'outils parallèle avec Promise.all, et streaming des tokens de réflexion vers un client React — vous disposez des fondations pour construire des agents qui réfléchissent explicitement à ce qu'ils doivent collecter avant de commencer à collecter.