Développement PostgreSQL dans un workflow IA qui doit fonctionner

Le développement PostgreSQL n'est pas intéressant en soi. Ce qui le rend intéressant, c'est le contexte dans lequel il s'inscrit, et le contexte auquel je revenais constamment était celui-ci : un workflow IA où une mauvaise réponse ne fait pas seulement mauvaise impression, elle coûte à quelqu'un de l'argent, une demande de visa, ou une opportunité d'emploi. Cette contrainte a changé chaque décision que j'ai prise à la couche base de données.
En 2026, le consensus de l'industrie s'était déjà éloigné des appels LLM à invite unique. Systèmes IA composites, workflows agentiques, exécution multi-agents avec état utilisant des outils comme LangGraph et CrewAI. Le modèle est un nœud dans un graphe. La base de données en est un autre. Le problème est que le nœud modèle est probabiliste et le nœud base de données doit être déterministe. Faire coopérer ces deux choses sans laisser l'incertitude du modèle s'infiltrer dans la couche de persistance est le vrai problème d'ingénierie.
La contrainte qui a façonné chaque décision de schéma
Sur Fursa, un outil d'éligibilité de route de visa couvrant 170+ pays de destination, le modèle produisait des évaluations d'éligibilité structurées. Ces évaluations devaient être stockées, versionnées et auditables. Un utilisateur devait pouvoir revenir six mois plus tard et voir exactement ce que le modèle avait dit à l'époque, sous exactement quel ensemble de règles, avec exactement quels documents source cités. Ce n'est pas un problème de cache. C'est un problème de conception de schéma.
J'ai modélisé le résultat d'éligibilité comme un enregistrement immuable. Pas de mises à jour. Chaque réévaluation écrivait une nouvelle ligne, avec une clé étrangère pointant vers la version de règle et l'instantané source qui était actif au moment de la requête. La sortie du modèle était stockée en tant que JSONB validé, pas du texte brut, pas un blob. Les sorties structurées de l'API étaient validées contre un schéma Pydantic avant de jamais toucher la base de données. Si la validation échouait, le workflow s'arrêtait. L'enregistrement n'était jamais écrit.
Cette seule décision, valider avant de persister, a éliminé une catégorie entière de corruption qui aurait été invisible jusqu'à ce qu'un auditeur la demande.
Les sorties structurées ne sont aussi bonnes que le schéma qui les applique
Les API de modèles frontier garantissent maintenant nativement les sorties JSON structurées. C'est utile. Ce n'est pas suffisant. Le JSON natif du modèle doit toujours être validé contre votre schéma de domaine, parce que le modèle ne connaît pas votre schéma de domaine. Il connaît ce que vous lui avez dit dans l'invite système, et les invites système dérivent.
Le motif auquel je me suis arrêté dans plusieurs constructions :
- Définir la forme canonique comme un modèle Pydantic, pas dans l'invite
- Passer le schéma JSON dérivé de ce modèle au paramètre de sortie structurée de l'API
- Valider la réponse contre le même modèle Pydantic à la réception
- Écrire dans PostgreSQL seulement après que la validation réussisse
- Enregistrer les échecs de validation avec la réponse brute du modèle attachée, pour le débogage
Cela m'a donné deux choses. Premièrement, la base de données restait propre indépendamment de ce que le modèle produisait. Deuxièmement, les échecs de validation devenaient un signal. Un pic d'échecs signifiait que le comportement du modèle avait dérivé, ou que l'invite avait changé, ou qu'une nouvelle version de modèle avait été déployée en amont. La base de données n'était pas juste du stockage. C'était un canari.
Concurrence et le problème du workflow agentique
Sur Job Hunter, un tableau d'affichage d'emploi quotidien et un moteur de sensibilisation explorant 185+ pages de carrière, plusieurs agents s'exécutaient en parallèle. Un agent explorait et normalisait les offres d'emploi. Un autre les notait par rapport à un profil de candidat. Un troisième rédigeait des messages de sensibilisation. Les trois pouvaient écrire sur le même enregistrement de candidat en même temps.
Le verrouillage au niveau des lignes de PostgreSQL gérait le cas évident. Le problème plus subtil était l'idempotence. Si l'agent de notation s'arrêtait à mi-chemin et redémarrait, il devait savoir quels enregistrements il avait déjà notés et lesquels il n'avait pas. J'ai utilisé une colonne de statut avec un enum contraint, combinée avec un motif de requête FOR UPDATE SKIP LOCKED. L'agent sélectionnait le prochain lot non traité, verrouillait ces lignes, les traitait, et mettait à jour le statut dans la même transaction. Si l'agent mourait, le verrou était libéré et une autre instance reprenait le travail. Pas de double-traitement. Pas de lacunes.
C'est un motif bien connu dans les systèmes basés sur les files d'attente. Ce qui le rendait intéressant ici, c'est que le « travail » effectué était un appel de modèle, qui est lent et coûteux. Je ne voulais pas réexécuter un appel de modèle pour lequel j'avais déjà payé. J'ai donc aussi stocké la sortie brute du modèle aux côtés du résultat traité. Si une étape en aval échouait, je pouvais rejouer à partir de la sortie stockée sans frapper l'API à nouveau. Cela a réduit à la fois le coût et la latence sur les tentatives.
Les portes d'approbation humaine et ce qu'elles exigent de la base de données
Certains workflows ne doivent pas être entièrement automatisés. Sur OptimalTax, un outil de déclaration fiscale automatisé avec une précision de calcul de 99%, le modèle générait une déclaration brouillon. Un humain l'examinait avant la soumission. Cette étape d'examen n'est pas un détail UX. C'est une exigence de conformité, et elle doit être représentée dans la base de données.
J'ai ajouté une table d'approbation avec une clé étrangère vers le brouillon, un identifiant d'examinateur, un horodatage, et une colonne de décision. Le processus de soumission vérifiait un enregistrement approuvé avant de procéder. Pas d'enregistrement approuvé, pas de soumission, appliqué à la couche application et à la couche base de données avec une contrainte de vérification. Deux points d'application parce qu'un n'est pas suffisant quand les enjeux sont une déclaration fiscale.
La piste d'audit que cela a créée n'était pas optionnelle. C'était la fonctionnalité. Tout régulateur demandant qui a approuvé quoi et quand obtenait une réponse précise d'une seule requête. Le modèle était une partie du workflow. L'humain en était une autre. La base de données enregistrait les deux.
Ce que la mise en cache des invites a changé à propos de la stratégie de données
La mise en cache des invites, maintenant supportée nativement par Anthropic et OpenAI, réduit les coûts des jetons d'entrée jusqu'à 50% pour les applications riches en contexte. Cela semble être une préoccupation API. Cela a changé ma stratégie de données.
Quand vous mettez en cache une grande invite système ou un corpus de documents, vous arrêtez d'envoyer ce contenu à chaque appel. Mais vous devez toujours savoir contre quel contexte mis en cache une réponse de modèle donnée a été générée. Si le contexte mis en cache change, les anciennes réponses peuvent ne plus être valides. J'ai commencé à traiter la clé de cache comme une métadonnée de première classe, stockée aux côtés de chaque réponse de modèle dans la base de données. Cela a rendu possible d'invalider ou de signaler les réponses quand le contexte sous-jacent était mis à jour, sans réexécuter chaque enregistrement historique.
C'est le genre de décision qui semble inutile jusqu'à ce que le contexte change et que vous ayez besoin de savoir quels enregistrements réévaluer. Ensuite, cela semble évident.
Où regarder si vous voulez voir le tableau d'ingénierie complet
Les motifs ci-dessus ne sont pas théoriques. Ils ont fonctionné en production sur plusieurs produits IA, chacun avec un mode de défaillance différent que la couche base de données devait absorber. La perspective d'ingénierie sur ce travail entre dans l'architecture, les choix de stack, et les compromis en plus de détails. Si vous évaluez si l'approche est solide avant un engagement, c'est le bon endroit pour commencer.
Si vous avez déjà une construction spécifique en tête, le service d'audit technique est délimité pour exactement cette situation : deux à trois semaines, une portée fixe, et une réponse claire sur si l'architecture tiendra.
Envie d'en discuter ?
Parlons-en.