Maîtriser l'API Gemini pour les architectures d'agents intelligents
L’API Gemini ne se limite pas à générer du texte. Bien utilisée, elle permet de construire des agents capables de raisonner, de planifier et d’agir sur des systèmes externes — un dépôt GitHub, une base de connaissances, un board de tickets.
C’est exactement ce que j’ai mis en place pour BB-PO, mon assistant intégré à l’application de gestion de projet Citios. Cet article détaille l’architecture sous-jacente : appels API, ingénierie des prompts, output structuré, et surtout la boucle d’outils qui permet à Gemini d’explorer un repo pour diagnostiquer un bug ou cadrer un ticket.
Partie 1 — Fondations de l’API Gemini
Modèles et capacités
Gemini repose sur une architecture de transformeurs multimodale. En pratique, pour un agent conversationnel métier, on s’appuie surtout sur les modèles de la famille Gemini Pro : bon équilibre entre qualité de raisonnement, latence et coût.
Ce qui change la donne pour les agents, ce n’est pas seulement la qualité du texte généré — c’est la capacité du modèle à décider d’appeler des fonctions (tools) quand il lui manque une information, puis à intégrer le résultat dans sa réponse.
Authentification et environnement
L’API exige une clé, stockée côté serveur (variables d’environnement, secrets manager). Jamais côté client. Dans mon cas, l’agent tourne en fonction serverless ; les headers d’auth sont construits à chaque requête :
const resp = await fetch(`${GEMINI_OPENAI_BASE}/chat/completions`, {
method: "POST",
headers: geminiAuthHeaders(),
body: JSON.stringify(body),
});
Deux modes d’appel : streaming et JSON
Selon le besoin, j’utilise deux wrappers :
| Fonction | Mode | Usage |
|---|---|---|
callGemini | Streaming (ReadableStream) | Conversation interactive, affichage progressif |
callGeminiJson | Réponse complète (stream: false) | Output structuré, boucle d’outils |
Le streaming convient à l’UI conversationnelle. La boucle d’outils, elle, a besoin d’une réponse complète pour inspecter les tool_calls avant d’exécuter quoi que ce soit :
async function callGeminiJson(
body: Record<string, unknown>,
): Promise<
| { ok: true; message: GeminiCompletionMessage }
| { ok: false; status: number; text: string }
> {
const resp = await fetch(`${GEMINI_OPENAI_BASE}/chat/completions`, {
method: "POST",
headers: geminiAuthHeaders(),
body: JSON.stringify({ ...body, stream: false }),
});
if (!resp.ok) {
return { ok: false, status: resp.status, text: await resp.text() };
}
const data = await resp.json();
const message = data.choices?.[0]?.message as GeminiCompletionMessage | undefined;
if (!message) {
return { ok: false, status: 500, text: JSON.stringify(data) };
}
return { ok: true, message };
}
Partie 2 — Ingénierie des prompts et output structuré
Le system prompt : plus qu’une persona
Un agent utile ne part pas d’un prompt générique. Le message système est assemblé dynamiquement à partir de plusieurs couches :
- Persona — nom et posture de l’assistant
- Instructions métier — règles du projet, vocabulaire, périmètre
- Contexte RAG — extraits de documents uploadés
- Mémoires — faits persistés sur l’utilisateur ou le projet
- Hints d’outils — quand et comment utiliser GitHub, le calculateur, etc.
let systemMessage =
personaHeader +
(system_prompt || defaultPrompt) +
agentsMdContext +
githubToolsHint +
ragContext +
memoryContext;
const requestMessages: ChatMessage[] = [
{ role: "system", content: systemMessage },
...messages,
];
Plus le contexte est précis, moins le modèle invente. Sur BB-PO, chaque projet embarque un fichier de contexte produit : l’assistant sait de quoi on parle sans que l’utilisateur doive tout réexpliquer.
Contexte conversationnel
Gemini ne « se souvient » de rien entre deux requêtes. C’est à vous d’envoyer l’historique complet — messages utilisateur, réponses assistant, et résultats d’outils — à chaque tour. Sans ça, l’agent perd le fil dès qu’il a appelé un tool.
Output structuré : générer un ticket fiable
Pour automatiser la création de tickets, on ne demande pas un paragraphe libre : on impose un schéma JSON strict dans le system prompt, et on passe par callGeminiJson.
const ticketSystemPrompt = `
You are an expert project manager bot. Extract information from the user's
request and generate a technical support ticket as JSON only.
Schema:
{
"title": "string",
"description": "string",
"priority": "High | Medium | Low",
"assigned_to": "string",
"steps_to_reproduce": ["string"]
}
No markdown, no text outside the JSON object.
`;
Résultat : un artefact directement injectable dans un board ou une issue GitHub, sans parsing fragile d’un pavé de markdown.
Partie 3 — Architecture des agents avec les Tools
Le principe
Un tool, c’est une fonction que vous déclarez au modèle. Gemini décide quand l’appeler, avec quels arguments. Votre backend exécute la fonction, renvoie le résultat, et le modèle continue — jusqu’à une réponse textuelle finale, ou jusqu’à une limite de tours.
Sans tools, Gemini ne connaît que son entraînement. Avec tools, il peut lire un fichier du repo, chercher une erreur dans le code, explorer une arborescence.
Déclaration des outils
Chaque outil est décrit avec un nom, une description (critique pour que le modèle sache quand l’utiliser) et un schéma de paramètres :
const TOOL_DEFINITIONS = [
{
type: "function",
function: {
name: "search_github_code",
description: "Search for code in the project's linked GitHub repository",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search terms or code snippet" },
max_results: { type: "number", description: "Max results 1–10 (default 8)" },
},
required: ["query"],
},
},
},
{
type: "function",
function: {
name: "read_github_file",
description: "Read the text content of a file from the linked GitHub repository",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Path relative to repo root" },
ref: { type: "string", description: "Optional branch, tag, or commit SHA" },
},
required: ["path"],
},
},
},
{
type: "function",
function: {
name: "list_repo_tree",
description: "List files and directories at a path in the linked GitHub repository",
parameters: {
type: "object",
properties: {
path: { type: "string", description: "Directory path (empty string for root)" },
ref: { type: "string", description: "Optional branch, tag, or commit SHA" },
},
required: ["path"],
},
},
},
];
La boucle d’agent : runToolAgentLoop
Cœur du système. À chaque round :
- Envoyer system + historique + définitions d’outils
- Recevoir une réponse (texte et/ou
tool_calls) - S’il n’y a pas d’appel d’outil → réponse finale
- Sinon → exécuter chaque outil, pousser les résultats dans la conversation, recommencer
async function runToolAgentLoop(opts: {
chatModel: string;
systemMessage: string;
messages: ChatMessage[];
toolCtx: ToolExecutionContext;
}): Promise<ToolAgentResult> {
const conversation: ChatMessage[] = [...opts.messages];
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const geminiResp = await callGeminiJson({
model: opts.chatModel,
messages: [
{ role: "system", content: opts.systemMessage },
...conversation,
],
tools: TOOL_DEFINITIONS,
tool_choice: "auto",
});
if (!geminiResp.ok) throw new AiServiceError(geminiResp.status, geminiResp.text);
const content = geminiResp.message.content ?? "";
const roundToolCalls = normalizeToolCalls(geminiResp.message.tool_calls, round);
if (roundToolCalls.length === 0) {
return { content: content || "I couldn't generate a response.", tool_rounds: round, tool_calls: 0 };
}
conversation.push({
role: "assistant",
content: content || null,
tool_calls: roundToolCalls,
});
for (const tc of roundToolCalls) {
const result = await executeToolCall(tc, opts.toolCtx);
conversation.push({
role: "tool",
tool_call_id: tc.id,
content: result,
});
}
}
return {
content: "Maximum tool rounds reached. Please try a more specific question.",
tool_rounds: MAX_TOOL_ROUNDS,
tool_calls: 0,
};
}
tool_choice: "auto" laisse Gemini décider. MAX_TOOL_ROUNDS évite les boucles infinies (et la facture qui va avec).
Normaliser les appels d’outils
Les tool_calls bruts ne sont pas toujours propres (id manquant, arguments déjà parsés). Une étape de normalisation avant exécution évite des crashs silencieux :
function normalizeToolCalls(
raw: GeminiCompletionMessage["tool_calls"],
round: number,
) {
if (!raw || !Array.isArray(raw)) return [];
return raw
.map((tc, i) => {
const args = tc.function?.arguments ?? "{}";
return {
id: tc.id || `call_${round}_${i}`,
type: "function" as const,
function: {
name: tc.function?.name ?? "",
arguments: typeof args === "string" ? args : JSON.stringify(args),
},
};
})
.filter((tc) => tc.function.name);
}
Scénario concret : diagnostiquer un bug via GitHub
Voici le type de flux que la boucle produit quand un utilisateur décrit une erreur :
- Utilisateur — « La connexion échoue avec
InvalidCredentialsError. » - Round 1 —
search_github_code(query="InvalidCredentialsError")→ localise les fichiers concernés - Round 2 —
read_github_file(path="src/auth/service.ts")→ lit le service d’auth - Round 3 — réponse textuelle : explication + pistes (dépendance externe, validation des credentials, etc.)
L’agent n’invente pas le chemin du fichier : il le découvre. C’est la différence entre un chatbot qui « a l’air de savoir » et un assistant qui vérifie dans le code.
Partie 4 — Déploiement et bonnes pratiques
Serverless
L’endpoint tourne en fonction serverless (Supabase Functions / Deno). Le handler parse la requête, construit le system prompt, et bascule vers runToolAgentLoop ou le streaming selon le flag use_tools.
Logging
Sans logs structurés, une boucle d’outils est opaque. Je journalise systématiquement : modèle, usage des tools, nombre de rounds, durée, taille de réponse, erreurs.
function logChatInvocation(summary: ChatInvocationLog): void {
const parts = [
"[chat]",
`status=${summary.status}`,
`model=${summary.model}`,
`tools=${summary.use_tools}`,
`project=${summary.project_id ?? "none"}`,
`msgs=${summary.messages}`,
`duration_ms=${summary.duration_ms}`,
];
if (summary.tool_rounds != null) parts.push(`tool_rounds=${summary.tool_rounds}`);
if (summary.error) parts.push(`error=${summary.error}`);
console.log(parts.join(" "));
}
Ce que je retients en production
- Tokens et coût — l’historique + le contexte RAG + les résultats d’outils grossissent vite. Tronquez, résumez, limitez
MAX_TOOL_ROUNDS. - Prompts précis — rôle, schéma, exemples few-shot quand le format compte.
- Validation — parser et valider le JSON de sortie avant de l’injecter dans un système aval.
- Sécurité — clé API serveur uniquement ; tokens GitHub à scopes restreints (lecture repo, pas admin) ; valider les chemins demandés par les tools.
- Permissions minimales — un outil qui lit un fichier n’a pas besoin d’écrire une issue.
En résumé
| Brique | Rôle |
|---|---|
callGemini / callGeminiJson | Streaming UI vs réponses structurées / tools |
| System prompt dynamique | Persona + contexte projet + RAG + hints tools |
TOOL_DEFINITIONS | Contrat entre le modèle et votre backend |
runToolAgentLoop | Orchestration multi-tours jusqu’à la réponse finale |
| Outils GitHub | Explorer, lire, chercher dans le dépôt lié |
Gemini devient intéressant pour le métier le jour où il cesse d’être un générateur de texte isolé et commence à agir — avec des garde-fous, des logs, et des tools dont vous contrôlez le périmètre.
Si vous voulez voir ça en action côté produit, BB-PO en est l’illustration concrète. Et si vous voulez en discuter pour votre stack, contactez-moi.
Vous cherchez un CTO à la demande pour votre startup ou votre PME ?
Discutons de votre projet →