En juillet, j’ai publié trois décisions pour brancher un RAG sur un SI .NET existant : garder les vecteurs à côté des données, découper selon la structure des documents, laisser l’orchestration dans la couche service. Ce guide remplace cet article. Les trois décisions tiennent toujours, mais le texte restait au niveau des principes. Voici l’architecture complète, avec le SQL et le C#.

Deux choses ont changé depuis. SQL Server 2025, sorti le 18 novembre 2025, stocke les embeddings dans un vrai type de colonne. Et Microsoft Agent Framework 1.0, sorti en avril 2026, a remplacé Semantic Kernel pour la couche agent (voir le guide de migration). Le code de ce guide vient d’un dépôt GitHub public, compilé et testé contre SQL Server 2025 à chaque modification.

Où en est SQL Server 2025 sur les vecteurs

La documentation mélange ce qui est disponible en version finale et ce qui est en préversion, et la différence entre SQL Server installé chez vous et Azure SQL compte. État au 30 septembre 2026 :

FonctionSQL Server 2025 (jusqu’au CU9, 15/09/2026)Azure SQL Database, SQL database dans Fabric
Type VECTOR, jusqu’à 1 998 dimensions en float32DisponibleDisponible
VECTOR_DISTANCE (cosinus, euclidienne, produit scalaire)DisponibleDisponible
Transport binaire depuis .NET (SqlVector<float>)Microsoft.Data.SqlClient 6.1 ou plusIdem
Index CREATE VECTOR INDEX (DiskANN) et VECTOR_SEARCHPréversion, option PREVIEW_FEATURESDisponible
Écriture dans une table indexée, filtres appliqués pendant la rechercheNon : ancien format d’index, table en lecture seule, filtre après coupOui (dernier format d’index)

Retenez la dernière ligne. Sur SQL Server 2025 installé chez vous, l’index vectoriel actuel rend la table en lecture seule une fois créé, et les conditions du WHERE s’appliquent après la recherche approchée. Pour un RAG filtré par habilitations, cela veut dire qu’une requête peut renvoyer zéro résultat alors que des documents autorisés existent. La recherche exacte n’a pas ce défaut, et elle suffit dans la grande majorité des cas (voir plus bas).

L’architecture en un tableau

CoucheCe qui existe déjàCe que le RAG ajoute
DonnéesLa table des documents de l’applicationUne table de fragments avec une colonne VECTOR(1536)
DroitsLa table ou le mécanisme d’habilitations en placeRien : la recherche le réutilise
Service .NETL’identité de l’utilisateur, la journalisation, l’auditUn découpeur, un ingesteur, un moteur de recherche
InterfaceL’applicationUn agent Agent Framework qui répond à partir des fragments autorisés

Une seule table nouvelle, aucun nouveau système. C’est tout l’intérêt de faire le RAG dans SQL Server quand l’application y vit déjà.

Décision 1 : les vecteurs restent à côté des données

La table des fragments pointe vers la table des documents existante. Elle ne copie ni le titre, ni les droits :

CREATE TABLE dbo.DocumentChunks
(
    ChunkId        INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_DocumentChunks PRIMARY KEY CLUSTERED,
    DocumentId     INT                NOT NULL CONSTRAINT FK_DocumentChunks_Documents
                                               REFERENCES dbo.Documents (DocumentId) ON DELETE CASCADE,
    Ordinal        INT                NOT NULL,
    HeadingPath    NVARCHAR(400)      NOT NULL,
    Content        NVARCHAR(MAX)      NOT NULL,
    Embedding      VECTOR(1536)       NOT NULL,
    EmbeddingModel NVARCHAR(100)      NOT NULL
);

Trois détails qui évitent des ennuis plus tard. La clé primaire est un INT en index cluster, condition exigée si vous ajoutez un jour un index vectoriel. La suppression en cascade retire les fragments quand le document disparaît : pas de fragment orphelin qui continue de répondre. Et la colonne EmbeddingModel enregistre le modèle qui a produit chaque vecteur. Deux modèles d’embedding ne sont pas comparables entre eux : le jour où vous changez de modèle, cette colonne dit ce qu’il reste à recalculer, et la recherche filtre dessus pour ne jamais mélanger les deux.

Décision 2 : le découpage suit la structure du document

Couper tous les 500 tokens est simple, et coupe une clause, un tableau ou une étape de procédure en deux. Le modèle reçoit la moitié d’une règle et répond à partir de cette moitié. Les documents métier portent déjà leur structure ; le découpeur du dépôt la respecte :

  • un fragment par section, délimitée par les titres ;
  • une section trop longue est coupée entre deux paragraphes, jamais au milieu d’un ;
  • un tableau ou un bloc de code reste entier, même s’il dépasse la taille cible ;
  • chaque fragment garde le chemin de ses titres, par exemple « Politique de frais > Déplacements > Hôtels ».

Ce chemin est ajouté au texte envoyé au modèle d’embedding. Un fragment qui dit seulement « plafond : 120 euros par nuit » ne dit pas de quoi il parle ; avec son chemin, la question « combien pour un hôtel ? » le retrouve :

public sealed record DocumentChunk(int Ordinal, string HeadingPath, string Content)
{
    // The heading path travels with the text into the embedding.
    public string EmbeddingText => $"{HeadingPath}\n\n{Content}";
}

Le découpeur du dépôt travaille sur du Markdown. Pour des PDF ou des fichiers Word, la conversion en amont compte autant que le découpage : c’est le rôle de votre chaîne d’extraction existante, ou de la bibliothèque Microsoft.Extensions.DataIngestion de Microsoft, encore en préversion, qui propose aussi un découpage par titres.

Décision 3 : les habilitations s’appliquent dans la requête

C’est la décision qui compte le plus, et celle que les tutoriels sautent. La recherche filtre sur la table de droits de l’application, dans la même requête que le calcul de distance :

SELECT TOP (@top)
    c.ChunkId, c.DocumentId, d.Title, d.SourceUri, c.HeadingPath, c.Content,
    VECTOR_DISTANCE('cosine', c.Embedding, @query) AS Distance
FROM dbo.DocumentChunks AS c
INNER JOIN dbo.Documents AS d ON d.DocumentId = c.DocumentId
WHERE c.EmbeddingModel = @model
  AND EXISTS (
      SELECT 1
      FROM dbo.DocumentAccess AS a
      INNER JOIN OPENJSON(@roles) WITH (RoleName NVARCHAR(100) '$') AS r
          ON r.RoleName = a.RoleName
      WHERE a.DocumentId = c.DocumentId)
ORDER BY Distance;

Côté .NET, le vecteur de la question part en binaire, sans passer par du JSON :

// Fail closed: a caller without any role sees nothing, rather than everything.
if (caller.Roles.Count == 0)
{
    return [];
}

var queryVector = await embeddingGenerator.GenerateVectorAsync(question, cancellationToken: cancellationToken);

command.Parameters.Add("@roles", SqlDbType.NVarChar, -1).Value = JsonSerializer.Serialize(caller.Roles);
command.Parameters.Add("@query", SqlDbTypeExtensions.Vector).Value = new SqlVector<float>(queryVector);

Deux règles derrière ce code. Les rôles viennent de l’identité côté serveur (jeton, session, claims), jamais d’un paramètre que le modèle ou le client pourrait fournir. Et un utilisateur sans rôle ne voit rien : en cas de doute, la recherche échoue fermée. Dans votre application, la table DocumentAccess est remplacée par votre modèle de droits réel, qu’il s’agisse de rôles, de services, d’une sécurité au niveau des lignes ou d’une vue qui les combine. Le principe ne change pas : le filtre vit là où vivent déjà les droits.

Recherche exacte ou index vectoriel ?

La recherche exacte calcule la distance avec chaque fragment candidat. Microsoft la recommande tant que la recherche porte sur moins de 50 000 vecteurs environ, comptés après les conditions du WHERE. Avec un filtre d’habilitations, ce seuil est rarement atteint : un utilisateur a le droit de lire une fraction des documents, pas tous.

CritèreRecherche exacte (VECTOR_DISTANCE)Index DiskANN sur SQL Server 2025Index DiskANN sur Azure SQL
StatutDisponiblePréversionDisponible
RésultatsExactsApprochésApprochés
Filtre d’habilitationsAppliqué avant, résultats completsAppliqué après, résultats manquants possiblesAppliqué pendant la recherche
Écritures dans la tableNormalesTable en lecture seule (sauf option ALLOW_STALE_VECTOR_INDEX)Normales
Volume adaptéJusqu’à environ 50 000 vecteurs après filtrageGrands volumes, données stablesGrands volumes

Commencez en recherche exacte, mesurez le temps de réponse sur vos volumes réels, et ne passez à l’index que si la mesure l’exige. Le dépôt contient un script séparé pour l’index, avec ces réserves en tête.

Brancher l’agent

La couche agent utilise Microsoft Agent Framework. La recherche se branche comme fournisseur de contexte : TextSearchProvider l’exécute avant chaque appel au modèle, avec l’utilisateur capturé côté serveur :

public static AIAgent Create(IChatClient chatClient, ChunkRetriever retriever, ICallerContext caller) =>
    chatClient.AsAIAgent(new ChatClientAgentOptions
    {
        ChatOptions = new() { Instructions = Instructions },
        AIContextProviders =
        [
            new TextSearchProvider(
                (query, cancellationToken) => SearchAsync(retriever, caller, query, cancellationToken),
                new TextSearchProviderOptions
                {
                    SearchTime = TextSearchProviderOptions.TextSearchBehavior.BeforeAIInvoke,
                }),
        ],
    });

L’agent se crée à chaque requête, avec l’utilisateur de la requête : c’est un objet léger autour du client de chat partagé. Si vous choisissez plutôt le mode où le modèle appelle lui-même un outil de recherche, gardez les rôles hors des paramètres de l’outil. Sinon, c’est le modèle qui décide de ce que l’utilisateur a le droit de lire.

Ce que les tests vérifient

Les tests du dépôt tournent contre un vrai SQL Server 2025 à chaque modification, avec un générateur d’embeddings déterministe qui ne demande aucune clé d’API. Ils vérifient :

  • qu’un salarié retrouve le plafond d’hôtel dans la politique de frais, avec sa source ;
  • qu’un salarié ne récupère jamais un fragment du document RH sur les salaires, même avec une question qui en reprend les mots ;
  • qu’un utilisateur avec plusieurs rôles voit l’union de ses droits, et un utilisateur sans rôle, rien ;
  • qu’une nouvelle ingestion d’un document remplace ses fragments au lieu de les dupliquer ;
  • et surtout, en faisant tourner l’agent complet devant un faux modèle qui enregistre tout ce qu’il reçoit, que le texte d’un document interdit n’arrive jamais dans le prompt.

Ce dernier test est celui que je recommande de reprendre chez vous. Vérifier ce que renvoie la requête ne suffit pas : ce qui compte, c’est ce qui atteint le modèle.

Checklist avant la production

  1. Identifier la source des droits. Où vit la règle « qui peut lire quel document » aujourd’hui ? C’est elle que la recherche doit interroger, pas une copie.
  2. Choisir le modèle d’embedding et sa dimension. La colonne VECTOR(n) en dépend. Pour une profession réglementée, un modèle hébergé dans l’Union européenne, voire sur site.
  3. Prévoir le changement de modèle. Colonne EmbeddingModel, procédure de recalcul, et filtre sur le modèle dans la recherche.
  4. Relire vingt fragments. Sur vos vrais documents, avant tout réglage : un découpage raté se voit à l’œil nu.
  5. Constituer un jeu de questions de référence. Une trentaine de questions réelles, avec les documents attendus et ceux qui ne doivent jamais sortir pour chaque profil.
  6. Échouer fermé. Pas de rôle, pas de résultat. Une erreur de droits, pas de résultat.
  7. Traiter les embeddings comme les documents. Un vecteur dérive du texte : même niveau de confidentialité, même durée de conservation, même suppression.
  8. Mesurer avant d’indexer. Temps de réponse et nombre de fragments candidats par profil ; l’index vectoriel seulement si ces mesures le justifient.

Pour aller plus loin

Le dépôt GitHub contient le schéma, le découpeur, l’ingestion, la recherche, l’agent, les tests et un exemple qui tourne sans clé d’API. Il vérifie aussi, à chaque modification, que le code publié dans le guide Semantic Kernel ou Microsoft Agent Framework compile toujours. Pour l’écart entre une démonstration qui marche et un système qui tient, voir ce qui tient en production. Et si vous voulez rendre vos documents métier interrogeables depuis vos applications existantes, c’est le cœur de l’intégration IA dans les applications .NET.

Sources et références

RAGSQL Server.NETAgent FrameworkArchitecture

À lire aussi

Aller plus loin

Vos documents métier sont-ils interrogeables depuis vos applications, sans ouvrir un second système de droits ?

Pour les ETI et les éditeurs dont le SI vit en .NET et SQL Server : l'IA développée dans votre applicatif (extraction documentaire, agents, classification) avec vos règles métier, votre architecture, votre code. Pas un outil de plus à côté du SI : une capacité nouvelle dedans. Je travaille dans votre repo, avec vos développeurs, aux standards de votre équipe.

Parlons de votre SI →