Aller au contenu
Retour aux projets
Plan d'architectureMVP personnelEn cours10 min de lecture11 sections

Groundline

Questions-réponses documentaires ancrées, qui refusent de deviner

Répond à des questions sur un corpus privé et décline tout ce qu'il ne peut pas citer. L'ingestion, le découpage et le calcul des embeddings s'exécutent en tâches de fond, la recherche repose sur pgvector, et chaque affirmation d'une réponse est vérifiée contre un passage du document source avant qu'un seul token ne soit diffusé au client.

Qualité de la recherche, ancrage imposé et évaluation traitée comme une suite de tests.

  • .NET 10
  • ASP.NET Core
  • Claude API
  • PostgreSQL + pgvector
  • Dapper
  • Hangfire
  • SignalR
  • OpenTelemetry

Projet personnel — pas encore de dépôt public

01

Le problème qui vaut la peine d'être résolu

La génération augmentée par la recherche est facile à démontrer et difficile à faire confiance. Découpez quelques documents, calculez leurs embeddings, entassez les cinq meilleurs passages dans un prompt, et vous obtenez une réponse le plus souvent juste — qui invente de temps à autre une politique qui n'existe pas. Pour tout ce sur quoi une personne va agir, « le plus souvent juste » est la mauvaise cible.

L'exigence est donc inversée. Au lieu de chercher à rendre l'hallucination rare, le système est construit pour qu'une affirmation non étayée ne puisse pas atteindre le client : une affirmation sans citation est un bug que le pipeline attrape, et une question à laquelle le corpus ne peut pas répondre obtient un refus.

Un système qui répond correctement à 80 % des questions et refuse le reste est utile. Un système qui répond correctement à 95 % et devine le reste ne l'est pas, parce qu'on ne peut pas distinguer les deux.

02

Pourquoi ceci, et pas davantage de DeskPilot

DeskPilot place un modèle sur un chemin de requête, sous pression de coût et de latence. Groundline en place un derrière un système de recherche, où le problème difficile se situe entièrement en amont du modèle — si la recherche renvoie les mauvais passages, aucun prompt ne sauve la réponse.

DeskPilotGroundline
Le point durCoût, latence, débitQualité de la recherche et ancrage
Rôle du modèleClasser et rédigerRésumer uniquement ce qu'on lui a remis
Où commence l'échecLe fournisseurLe découpeur
Bon mode de défaillanceRouter vers un humainRefuser de répondre
Forme des donnéesUn ticket courtUn corpus qui change sous vos pieds
03

Le chemin d'ingestion

Document déposéPOST /documents
mettre en file
Tâche Hangfireen arrière-plan
extraire
Texte + mise en pagepar page
Structuretitres
découper
Découpeur sensible à la structureavec recouvrement
vectoriser
Modèle d'embeddingpar lots
stocker
pgvectorvecteurs
PostgreSQL FTSmots-clés
Index de passagesoffsets
L'ingestion est entièrement hors ligne. Rien ici ne se produit pendant que quelqu'un attend.

L'index de passages est ce qui rend la citation possible. Chaque fragment conserve l'identifiant du document, la page et les offsets de caractères dont il provient : une citation résout donc vers une plage exacte dans l'original, et non vers une paraphrase que le lecteur devrait aller vérifier à la main.

04

Le découpage est le plafond de qualité

Presque toutes les mauvaises réponses d'un système comme celui-ci remontent à une frontière de fragment. Coupez au milieu d'un tableau et les chiffres perdent leurs en-têtes. Coupez à nombre de tokens fixe et une clause perd la condition qui la qualifiait. Aucun travail sur les prompts ne réparera un fragment arrivé sans son contexte.

  • Découper d'abord selon la structure du document — titres et sections — avant de se rabattre sur des limites de taille.
  • Ne jamais couper un tableau ; un tableau trop grand devient son propre fragment, sa légende attachée.
  • Chaque fragment porte son chemin de titres en préfixe : lu isolément, il dit encore de quoi il parle.
  • Du recouvrement entre fragments voisins, pour qu'un fait situé près d'une frontière reste retrouvable des deux côtés.
05

Recherche hybride, puis reclassement

La recherche vectorielle est bonne sur le sens et mauvaise sur les tokens exacts. Demandez un code d'erreur, une référence de pièce ou un numéro de clause et les embeddings renverront volontiers quelque chose de sémantiquement voisin et littéralement faux. La recherche par mots-clés a la défaillance inverse. Exécuter les deux et fusionner n'est pas une précaution : c'est couvrir deux faiblesses distinctes et bien comprises.

ÉtapeOutilCe qu'elle rattrape
Recherche densepgvector, cosinus sur les embeddingsParaphrases et recouvrement conceptuel
Recherche creuseRecherche plein texte de PostgreSQLIdentifiants exacts, codes, termes rares
FusionReciprocal rank fusionFusionne les deux sans régler un poids par requête
ReclassementCross-encodage des meilleurs candidatsLa précision dans la petite fenêtre qui atteint le modèle

pgvector plutôt qu'une base vectorielle dédiée, parce que le corpus se mesure en centaines de milliers de fragments, pas en milliards. Garder les vecteurs dans PostgreSQL, c'est un seul entrepôt de données, une seule stratégie de sauvegarde, une seule transaction — et filtrer par locataire ou par document devient une clause WHERE au lieu d'une demande de fonctionnalité de filtrage sur métadonnées. Cette réponse change à une échelle que ce MVP n'atteindra pas.

06

L'ancrage est imposé, pas demandé

Demander à un modèle de citer ses sources produit des citations. Cela ne produit pas des citations correctes, et une citation fabriquée est pire qu'aucune, parce qu'elle a l'air d'un travail sérieux. Le contrôle est donc mécanique, et il s'exécute après la génération.

  1. 1Répondre en affirmations, pas en proseLe modèle renvoie une liste d'affirmations via un schéma d'outil, chacune accompagnée des identifiants de fragments dont elle provient — et non un paragraphe auquel on aurait greffé des notes de bas de page.
  2. 2Résoudre chaque citationChaque identifiant de fragment cité doit exister dans l'ensemble effectivement retrouvé pour cette question. Une citation vers autre chose est une fabrication et invalide la réponse entière.
  3. 3Vérifier l'appuiUne seconde passe, peu coûteuse, confronte chaque affirmation au seul passage qu'elle cite et répond à une question : ce texte étaie-t-il cette affirmation ? Les affirmations non étayées sont écartées.
  4. 4Refuser plutôt qu'amincirSi écarter des affirmations laisse la réponse incomplète, celle-ci devient un « impossible à répondre à partir de ces documents » explicite, avec les passages retrouvés les plus proches affichés. Le refus est une fonctionnalité.
07

Diffuser une réponse que l'on n'a pas fini de vérifier

Vérifier après la génération et diffuser pendant la génération sont en conflit direct : on ne peut pas diffuser un token que l'on risque de retirer ensuite. C'est la tension la plus vive de la conception, et la résolution consiste à diffuser la part déjà sûre.

  • Les sources retrouvées sont diffusées en premier, immédiatement : le lecteur voit quels documents sont utilisés pendant que la réponse s'écrit encore.
  • Les affirmations sont diffusées une par une, à mesure qu'elles passent la vérification, au lieu que la réponse entière arrive d'un bloc.
  • Une affirmation écartée n'apparaît jamais : rien n'est donc visiblement retiré en cours de diffusion.
  • SignalR transporte le tout, et un client qui se reconnecte récupère la réponse terminée plutôt que de reprendre un flux partiel.

Le résultat est plus lent à produire la première phrase complète qu'un système non ancré, et plus rapide à produire le premier signal utile, parce que les sources arrivent presque instantanément. L'échange est meilleur qu'il n'y paraît : savoir quels documents sont consultés est souvent l'essentiel de ce que le lecteur voulait.

08

L'évaluation, découpée en trois

Un chiffre unique de justesse est inutile pour un système comme celui-ci, parce qu'il ne dit pas quelle étape a régressé. La recherche et la génération sont mesurées séparément, et le refus est mesuré pour lui-même.

Ce qui est mesuréMétriqueCe qu'elle dit
RechercheRappel@k sur des paires question/fragment annotéesLe bon passage est-il entré dans la fenêtre, tout simplement
AncragePart des affirmations disposant d'un passage d'appui vérifiéLe modèle reste-t-il dans ce qu'on lui a remis
RefusComportement sur un jeu de questions volontairement sans réponseDécline-t-il au lieu d'improviser

C'est le jeu de refus qui compte le plus, et c'est celui qui manque d'habitude. Sans lui, toute modification qui rend le système plus enclin à répondre ressemble à une amélioration, et la régression n'apparaît que le jour où quelqu'un agit sur une invention formulée avec assurance.

09

Un corpus qui change sous vos pieds

Les documents sont révisés, remplacés, retirés. Un passage cité qui n'existe plus transforme une citation en lien mort, et une réponse ancrée dans une politique retirée est pire qu'une absence de réponse.

  • Les documents sont versionnés ; les fragments appartiennent à une version, pas à un nom de fichier.
  • La réingestion est incrémentale — seules les sections modifiées sont revectorisées, parce que recalculer chaque nuit les embeddings d'un corpus inchangé est du pur coût.
  • Les versions remplacées restent interrogeables mais sont signalées, pour qu'une ancienne réponse puisse encore être expliquée après coup.
  • Chaque réponse enregistre les versions de documents qu'elle a utilisées : c'est ce qui rend un audit possible, tout simplement.
10

La stack, et la raison de chaque brique

BesoinChoixPourquoi celui-ci
Accès au modèleMicrosoft.Extensions.AIUne seule abstraction IChatClient pour la génération et la vérification, avec un pipeline de middlewares pour la télémétrie
VecteursPostgreSQL + pgvectorUn seul entrepôt de données, de vraies transactions, le filtrage par locataire comme clause WHERE
LecturesDapperLa recherche hybride est par nature du SQL écrit à la main ; un ORM n'y apporte rien
ÉcrituresEF Core 10Gestion des documents et des versions, là où le suivi des changements se rentabilise
IngestionHangfireDes tâches longues, réessayables et observables, sans utilisateur qui attend
RésilienceMicrosoft.Extensions.Http.ResiliencePipeline standard first-party plutôt que des stratégies Polly assemblées à la main
StreamingSignalRLes sources d'abord, puis les affirmations vérifiées — uniquement côté client
Documentation d'APIScalar + OpenAPI intégréASP.NET Core génère lui-même le document ; pas besoin de Swashbuckle
ObservabilitéSerilog + OpenTelemetryRappel de la recherche, taux d'ancrage et taux de refus comme métriques, pas comme lignes de logs
TestsxUnit v3 + TestcontainersUn vrai PostgreSQL avec pgvector ; simuler un index vectoriel ne teste rien
11

Les compromis que j'accepte

DécisionCoût
Passe de vérificationDeux appels de modèle par réponse : plus lent, et environ le double du coût
Refuser quand rien n'étaieUn taux de réponse plus faible, et des utilisateurs qui voulaient une estimation la trouveront inutile
pgvector plutôt qu'une base vectorielleUn plafond de taille de corpus qu'un déploiement plus vaste atteindrait
Recherche hybride plus reclassementTrois étapes à régler et à mesurer au lieu d'une
Des affirmations plutôt que de la proseDes réponses qui se lisent de façon plus hachée qu'un paragraphe fluide

Envie d'en discuter la conception ?

C'est une conception : elle est donc encore discutable. Si vous pensez que l'un des compromis ci-dessus est mauvais, je préfère l'entendre maintenant plutôt qu'après la construction.