La Stack TypeScript de l'IA fiable : Zod et Vercel AI SDK
Cet article explique comment implémenter la Représentation Intermédiaire (IR) en TypeScript avec Zod et Vercel AI SDK. Il révèle le mécanisme du “Constrained Decoding” qui garantit 100% de conformité au schéma, transformant l’IA probabiliste en composant déterministe.
Série “De l’Idée au Code : IA Fiable” Partie 1 : Pourquoi le ‘Prompt-to-UI’ ne scale pas Partie 2 : La Représentation Intermédiaire (IR) Partie 3 : Stack TypeScript : Zod & Vercel AI SDK Partie 4 : Validation multi-niveaux Partie 5 : Génération de code déterministe
Le problème d’implémentation
Dans l’article précédent, on a établi que la Représentation Intermédiaire (IR) est le pattern architectural pour fiabiliser l’IA. L’idée est de forcer le LLM à générer un “plan” (un JSON structuré de type SPEC) avant de générer le code final.
La question est : comment ?
Si on se contente de demander au LLM “Donne-moi un JSON qui suit ce format…”, on n’a aucune garantie. Le LLM peut halluciner, oublier un champ, ou générer un JSON mal formé. On est toujours dans le stochastique.
La solution d’ingénierie n’est pas de demander. C’est de contraindre.
L’impact quantifiable
Les chiffres parlent d’eux-mêmes :
- Prompt engineering classique : ~35,9% de fiabilité pour respecter le format
- Structured outputs (constrained decoding) : 100% de conformité garantie
Cette transformation radicale est rendue possible par une avancée technique fondamentale : le “Constrained Decoding”.
Le pilier technique : Le “Constrained Decoding”
La vraie avancée s’appelle le “Constrained Decoding” (ou structured outputs).
Plutôt que de laisser le LLM choisir librement le prochain token (mot/symbole) à générer, ce processus manipule activement la distribution de probabilités à chaque étape de génération.
À chaque token, le système :
- Analyse le JSON Schema requis
- Détermine quels tokens sont syntaxiquement valides à cette position
- Force la probabilité à 0 pour tous les tokens invalides
- Ne permet la sélection que parmi les tokens conformes
Le résultat : une garantie à 100% que la sortie sera syntaxiquement conforme. C’est le mécanisme qui permet de passer d’un outil probabiliste à un composant déterministe.
Cette approche est au cœur de systèmes comme SpecifyUI, qui génère des interfaces utilisateur via une IR de type SPEC (Structured, Parameterized, Hierarchical).
La Stack TypeScript : Zod + Vercel AI SDK
Dans l’écosystème TypeScript/Node.js, le duo gagnant pour implémenter ce pattern est Zod + Vercel AI SDK.
1. Zod : Le “Contrat” (Le Schéma)
Zod est la bibliothèque de référence pour la validation de données type-safe en TypeScript. Dans notre architecture, c’est lui qui définit le “contrat” de l’IR.
On définit un schéma Zod qui décrit la structure attendue. C’est notre “source de vérité”.
import { z } from 'zod';
// Schéma pour un composant UI individuel
const ComponentUI = z.object({
name: z.string().describe("Nom du composant, ex: 'Button', 'Header'"),
type: z.enum(['button', 'input', 'container', 'text']),
properties: z.record(z.any()).describe("Props du composant"),
children: z.array(z.lazy(() => ComponentUI)).optional()
});
// Schéma de l'IR complète (type SPEC)
const PageIRSchema = z.object({
// Global specifications
name: z.string(),
layout: z.enum(['colonne', 'grille', 'flex']),
// Parameterized fields (déterministes)
colors: z.object({
primary: z.string().regex(/^#[0-9A-F]{6}$/),
background: z.string().regex(/^#[0-9A-F]{6}$/)
}),
spacing: z.object({
base: z.enum(['4px', '8px', '16px']),
multiplier: z.number().min(1).max(4)
}),
// Hierarchical structure
components: z.array(ComponentUI)
});
type PageIR = z.infer<typeof PageIRSchema>;
Points clés :
- Type-safety complète : Le type
PageIRest automatiquement inféré - Validation runtime : Zod valide les données à l’exécution
- Descriptions intégrées : Servent de contexte pour le LLM
- Structure hiérarchique :
childrenpermet la composition récursive (essence de SPEC) - Champs paramétrés : Les couleurs et espacements sont contraints par des valeurs déterministes
2. Vercel AI SDK : L’Orchestrateur
Le Vercel AI SDK s’est imposé comme le standard pour orchestrer les LLMs avec des structured outputs. Il fournit des fonctions comme generateObject ou streamObject.
On lui donne notre schéma Zod, et il se charge de tout :
- Génère le JSON Schema à partir du schéma Zod
- Pilote le LLM en mode “structured outputs” pour forcer le constrained decoding
- Valide la sortie du LLM contre le schéma Zod
- Gère les retries automatiquement si la validation échoue
import { generateObject } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
async function generatePageIR(userPrompt: string): Promise<PageIR> {
const { object } = await generateObject({
model: anthropic('claude-3-5-sonnet-20241022'),
schema: PageIRSchema, // On passe le schéma Zod
prompt: `Génère une IR de type SPEC pour cette demande : ${userPrompt}
Respecte ces contraintes :
- Les couleurs doivent être des hex codes valides
- L'espacement doit suivre le système de base (4/8/16px)
- La hiérarchie des composants doit être cohérente`,
});
// 'object' est GARANTI être conforme au schéma PageIRSchema
// Et TypeScript le sait (type-safety complète)
return object;
}
// Usage
const ir = await generatePageIR("Un dashboard avec header, sidebar et 3 cartes de stats");
// TypeScript autocomplete fonctionne parfaitement
console.log(ir.colors.primary); // Type-safe !
console.log(ir.components[0].name); // Type-safe !
3. Streaming pour l’expérience utilisateur
Pour les IU complexes, on peut streamer la génération de l’IR :
import { streamObject } from 'ai';
async function streamPageIR(userPrompt: string) {
const { partialObjectStream } = await streamObject({
model: anthropic('claude-3-5-sonnet-20241022'),
schema: PageIRSchema,
prompt: userPrompt,
});
// Stream les objets partiels au fur et à mesure
for await (const partialObject of partialObjectStream) {
console.log('IR partielle:', partialObject);
// Mettre à jour l'UI en temps réel
}
}
Ce que ça change concrètement
Cette approche est une révolution dans notre façon de travailler avec les LLMs.
Le LLM cesse d’être un “générateur de code” imprévisible. Il devient un “remplisseur de schéma” structuré.
Nous ne lui demandons plus de créer la logique UI, nous lui demandons de structurer l’information selon nos règles (le schéma Zod).
Les bénéfices immédiats :
- 100% de conformité : Fini les JSON mal formés ou les champs manquants
- Type-safety de bout en bout : De la génération à la consommation
- Validation métier intégrée : Les règles sont dans le schéma (regex, enums, ranges)
- Déterminisme contrôlé : Même prompt = même structure d’IR
- Debuggabilité : L’IR est inspectable, versionnable, testable
Nous reprenons le contrôle total sur la pipeline d’ingénierie.
C’est la clé de l’industrialisation.
Exemple réel : SpecifyUI
Cette stack exacte est utilisée par SpecifyUI, un système de recherche qui génère des interfaces utilisateur via une IR de type SPEC (Structured, Parameterized, Hierarchical).
Le workflow :
- Input : Intention utilisateur (texte + image de référence)
- Phase 1 : LLM génère une IR SPEC (via Zod + constrained decoding)
- Validation : Multi-niveaux (schéma + métier + sémantique)
- Phase 2 : Générateur déterministe transforme SPEC → React components
- Output : Interface production-ready
Résultats mesurés :
- Contrôle créatif significativement supérieur vs génération directe
- Designs jugés plus cohérents et corrects par des experts UI
- Itérations 10x plus rapides (modifications ciblées sur l’IR)
La suite : La validation multi-niveaux
Maintenant que nous avons le problème (Partie 1), le pattern IR (Partie 2) et la stack technique (Partie 3), une question demeure : comment valider cette IR générée par le LLM ?
Avoir une IR conforme au schéma Zod est un excellent début, mais ce n’est pas suffisant. Il faut également valider :
- Les règles métier (couleurs du design system, contraintes d’espacement)
- La cohérence sémantique (hiérarchie des composants logique ?)
- La faisabilité technique (ce design est-il implémentable ?)
Dans le prochain article, nous verrons l’architecture de validation multi-niveaux qui garantit la qualité de l’IR avant toute génération de code.
Lire la série : ⬅️ Partie 2 : La Représentation Intermédiaire (IR) | Partie 4 : Validation multi-niveaux ️