Back to flin
flin

Le moteur d'intentions : requêtes base de données en langage naturel

Comment le moteur d'intentions de FLIN traduit le langage naturel en requêtes de base de données -- le mot-clé ask qui permet aux développeurs d'écrire « users who signed up last week » au lieu de jointures SQL et de clauses WHERE.

Thales & Claude | March 30, 2026 10 min flin
EN/ FR/ ES
flinintentnlpdatabasequeries

SQL est puissant mais verbeux. Écrire SELECT DISTINCT u.* FROM users u JOIN purchases p ON p.user_id = u.id WHERE u.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND u.created_at < NOW() pour répondre à la question « les utilisateurs inscrits la semaine dernière qui ont effectué un achat » est le genre de cérémonie qui fait de l'interaction avec la base de données une corvée.

Les requêtes de style ORM de FLIN améliorent cela : User.where(created_at >= last_week).where(purchases.count > 0). Mieux, mais encore technique. Le développeur doit connaître les noms de champs, les opérateurs de comparaison et la sémantique des jointures.

Le moteur d'intentions va un cran plus loin :

flinusers = ask "users who signed up last week and made a purchase"

Une ligne. Langage naturel. Le moteur d'intentions traduit l'intention en requête de base de données, l'exécute et retourne des entités FLIN typées. Le développeur n'écrit pas de SQL. Il ne rédige pas de requêtes ORM. Il décrit ce qu'il veut, et FLIN trouve comment l'obtenir.

Ce n'est pas une fonctionnalité gadget. C'est la partie la plus ambitieuse de la conception de FLIN -- un pont entre l'intention humaine et l'exécution machine qui rend l'interaction avec la base de données aussi naturelle qu'une conversation.

Le mot-clé ask

Le mot-clé ask est un construit de première classe du langage qui déclenche le moteur d'intentions :

flin// Requêtes simples
users = ask "all active users"
posts = ask "posts from today"
products = ask "products under 100 dollars"

// Requêtes complexes
vips = ask "users who spent more than 1000 in the last month"
trending = ask "most viewed articles this week"
at_risk = ask "customers who haven't ordered in 30 days"

// Agrégations
count = ask "how many users signed up yesterday"
average = ask "average order value this month"

La chaîne à l'intérieur de ask est analysée sémantiquement, pas littéralement. « Users who signed up last week » est compris comme un filtre sur le champ created_at de l'entité User avec une plage de dates des 7 derniers jours. « Products under 100 dollars » correspond à Product.where(price < 100).

Comment fonctionne le pipeline de traduction

Lorsque le runtime FLIN rencontre une expression ask, il exécute un pipeline en cinq étapes :

Étape 1 : ENTRÉE
  ask "users who signed up last week and made a purchase"

Étape 2 : ANALYSE DU SCHÉMA
  Entités disponibles : User, Order, Product
  Champs User : name, email, created_at, active
  Champs Order : user, total, created_at, status

Étape 3 : TRADUCTION IA
  Le LLM génère un plan de requête :
  {
    "entity": "User",
    "filters": [
      {"field": "created_at", "op": ">=", "value": "last_week"},
      {"join": "orders", "condition": "count > 0"}
    ]
  }

Étape 4 : EXÉCUTION DE LA REQUÊTE
  User.where(created_at >= last_week)
      .where(orders.count > 0)

Étape 5 : RÉSULTATS
  [User, User, User, ...]

Étape 1 : Entrée

La chaîne brute en langage naturel est extraite de l'expression ask.

Étape 2 : Analyse du schéma

Le runtime collecte les schémas d'entités de l'application courante. Les noms d'entités, les noms de champs, les types de champs et les relations sont compilés en une description de schéma qui sera envoyée au LLM comme contexte.

rustfn build_schema_context(entities: &HashMap<String, EntitySchema>) -> String {
    let mut context = String::new();
    for (name, schema) in entities {
        context.push_str(&format!("Entity {}:\n", name));
        for field in &schema.fields {
            context.push_str(&format!(
                "  - {}: {} {}\n",
                field.name,
                field.field_type,
                if field.is_relation { "(relation)" } else { "" }
            ));
        }
    }
    context
}

Ce contexte de schéma est ce qui rend ask conscient de l'application. Le LLM sait quelles entités existent, quels champs elles ont et comment elles sont reliées entre elles.

Étape 3 : Traduction IA

Le contexte du schéma et la requête en langage naturel sont envoyés à un LLM (configurable, par défaut le fournisseur IA défini dans flin.config). Le LLM retourne un plan de requête structuré :

flin// Le prompt envoyé au LLM
// "Given these entities:
//   User: name (text), email (text), created_at (time), active (bool)
//   Order: user (User), total (money), created_at (time), status (text)
//
// Translate this query to a FLIN query plan:
//   'users who signed up last week and made a purchase'
//
// Return JSON with entity, filters, joins, order, and limit."

La réponse du LLM est un plan de requête JSON, pas du SQL brut. Cette représentation intermédiaire est sûre -- elle ne peut référencer que les entités et champs qui existent dans le schéma. Il n'y a aucun moyen pour le LLM de générer du SQL arbitraire ou d'accéder à des données en dehors du modèle d'entités de l'application.

Étape 4 : Exécution de la requête

Le plan de requête est compilé en la représentation de requête interne de FLIN et exécuté contre FlinDB :

rustfn execute_query_plan(
    plan: &QueryPlan,
    db: &ZeroCore,
) -> Result<Vec<Value>, QueryError> {
    let mut query = db.entity(&plan.entity);

    for filter in &plan.filters {
        match filter {
            Filter::Comparison { field, op, value } => {
                query = query.where_clause(field, op, resolve_value(value));
            }
            Filter::Join { entity, condition } => {
                query = query.join(entity, condition);
            }
        }
    }

    if let Some(order) = &plan.order {
        query = query.order(&order.field, &order.direction);
    }

    if let Some(limit) = plan.limit {
        query = query.limit(limit);
    }

    query.execute()
}

Étape 5 : Résultats

Les résultats de la requête sont retournés comme des valeurs FLIN typées -- les mêmes instances d'entités que vous obtiendriez de User.all ou User.where(active == true).

Types de retour

Le mot-clé ask infère son type de retour à partir de la requête :

flin// Les requêtes de liste retournent des tableaux d'entités
users = ask "active users"                    // [User]
orders = ask "orders over 100"               // [Order]

// Les requêtes d'agrégation retournent des scalaires
count = ask "how many users"                  // int
average = ask "average price"                 // float

// Les requêtes à résultat unique retournent des entités optionnelles
newest = ask "most recent user"               // User?

L'inférence de type est basée sur les mots-clés dans la requête : - « how many » ou « count » -> int - « average », « sum », « total » -> float - « most recent », « newest », « first » -> entité optionnelle - Tout le reste -> liste d'entités

Chaînage avec ask

Les résultats de ask peuvent être traités davantage avec les opérations de collection standard de FLIN :

flin// Obtenir les utilisateurs, puis extraire les noms
users = ask "premium users"
names = users.map(u => u.name)

// Filtrer davantage
vips = (ask "active users").where(total_spent > 1000)

// Combiner avec l'interface utilisateur
<div class="dashboard">
    {for user in ask "users who signed up today"}
        <p>{user.name} -- {user.email}</p>
    {/for}
</div>

Mise en cache

Les traductions d'intentions sont mises en cache pour éviter des appels LLM répétés pour la même requête :

flin// Premier appel : traduction IA + exécution (200-500ms)
users = ask "active premium users"

// Même requête plus tard : traduction en cache, juste l'exécution (1-5ms)
users = ask "active premium users"

La clé de cache est le hash de la chaîne de requête plus la version actuelle du schéma. Si le schéma change (une nouvelle entité ou un nouveau champ est ajouté), le cache est invalidé et les requêtes sont re-traduites.

rustpub struct IntentCache {
    translations: HashMap<u64, QueryPlan>,
    schema_version: u64,
}

impl IntentCache {
    pub fn get(&self, query: &str, current_version: u64) -> Option<&QueryPlan> {
        if self.schema_version != current_version {
            return None; // Le schéma a changé, tout invalider
        }
        let key = hash_query(query);
        self.translations.get(&key)
    }
}

Gestion de l'ambiguïté

Le langage naturel est intrinsèquement ambigu. Lorsque le moteur d'intentions rencontre une ambiguïté, il fait des hypothèses raisonnables basées sur des schémas courants :

flin// "recent" -> 7 derniers jours par défaut
recent = ask "recent orders"

// "popular" -> trié par une métrique de popularité (vues, ventes, etc.)
popular = ask "popular products"

// "expensive" -> percentile supérieur par prix
expensive = ask "expensive items"

Ces valeurs par défaut sont documentées et prévisibles. Un développeur qui écrit ask "recent orders" peut s'attendre aux commandes des 7 derniers jours, pas des 30 dernières minutes ou de la dernière année.

Pour les requêtes sensibles à la précision, la syntaxe de style ORM est toujours disponible comme solution de repli :

flin// Si "recent" est ambigu, être explicite
orders = Order.where(created_at >= now - 3.days).order(created_at, "desc")

Cas d'utilisation

Tableau de bord analytique

flinrevenue_today = ask "total revenue today"
new_customers = ask "customers who signed up this week"
top_products = ask "best selling products this month"
at_risk = ask "customers who haven't ordered in 30 days"

<div class="dashboard">
    <Metric label="Revenue" value={revenue_today} />
    <Metric label="New Customers" value={new_customers.count} />
    <List title="Top Products" items={top_products} />
    <Alert title="At Risk" count={at_risk.count} />
</div>

Support client

flinurgent = ask "high priority tickets from this week"
overdue = ask "tickets open for more than 3 days"
unassigned = ask "unassigned tickets"

Gestion de contenu

flindrafts = ask "draft articles by current user"
trending = ask "most viewed articles this month"
scheduled = ask "articles scheduled for tomorrow"

Confidentialité et sécurité

Le moteur d'intentions est conçu avec la confidentialité comme contrainte :

Schéma uniquement, pas de données. Les schémas d'entités (noms et types) sont envoyés au LLM pour la traduction. Les données réelles des entités (e-mails des utilisateurs, mots de passe, dossiers financiers) ne quittent jamais le runtime FLIN.

Exécution paramétrée. Le LLM génère un plan de requête avec des valeurs paramétrées. Les valeurs sont résolues localement, pas par le LLM. Cela empêche l'extraction de données par l'ingénierie de prompts.

Application de liste blanche. Le plan de requête ne peut référencer que les entités et champs qui existent dans le schéma. Le LLM ne peut pas inventer des entités ou accéder aux tables système.

Repli hors ligne

Lorsque le fournisseur IA n'est pas disponible, ask se rabat sur la correspondance de mots-clés :

flin// En ligne : traduction IA complète
users = ask "active users who signed up in January"
// -> User.where(active == true).where(created_at >= january_1).where(created_at < february_1)

// Hors ligne : repli basé sur les mots-clés
users = ask "active users"
// -> User.where(active == true)  (correspond "active" au champ booléen)

Le repli est moins sophistiqué mais gère correctement les requêtes simples. Les requêtes complexes nécessitant la compréhension d'expressions temporelles ou de jointures échoueront avec un message d'erreur clair suggérant au développeur d'utiliser les requêtes de style ORM jusqu'à ce que l'IA soit rétablie.

Le moteur d'intentions est la fonctionnalité la plus visionnaire de FLIN. Il comble le fossé entre ce que les humains pensent (« montre-moi les commandes récentes ») et ce que les bases de données comprennent (SELECT * FROM orders WHERE created_at >= '2026-03-19'). Dans le prochain article, nous explorons l'autre côté de cette intégration IA : la recherche sémantique et le stockage vectoriel.


Ceci est la partie 116 de la série « Comment nous avons construit FLIN », documentant comment un CEO à Abidjan et un CTO IA ont conçu et construit un langage de programmation à partir de zéro.

Navigation de la série : - [115] Gardes personnalisés et middleware de sécurité - [116] Le moteur d'intentions : requêtes base de données en langage naturel (vous êtes ici) - [117] Recherche sémantique et stockage vectoriel - [118] Passerelle IA : 8 fournisseurs, une seule API

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles