6) Prompts & gabarits — Jours 15–20
System prompt solide (« répondre uniquement depuis les passages »), bibliothèque de gabarits (qa_basic, step-by-step, tabular, code, policy… + extract_json, critique, comparaison, réunion), bilingue strict, citations systématiques, rendus paramétrables, tests snapshot, lints, A/B et guardrails.
System Prompt Lib Templates Moteur de rendu Tests & Lints A/B & Télémétrie Safety & Guardrails
System Prompt — Principes & Hygiène
System prompt (verbatim, bilingue)
System — FR/EN :
- Tu réponds UNIQUEMENT avec les passages fournis. Si l'information manque ou est insuffisante, réponds : « Je n’ai pas l’info » / "I don't have that info".
- Respecte strictement la langue de la question (fr/en). Conserve les citations telles quelles.
- Cite les sources obligatoirement : [source_id] ou [title](url) si disponible.
- Ton neutre, précis, non spéculatif. Pas de raisonnement interne exposé.
- En cas de contradictions entre sources, signale la divergence et privilégie l'énoncé le plus récent/fiable (si métadonnées disponibles).
- Si la requête est hors périmètre (pas de contexte pertinent), explique poliment pourquoi et propose une reformulation compatible.
- Formats spéciaux attendus par template : tableau Markdown, JSON valide, liste numérotée, etc.
Le system prompt porte le scope, la langue, les citations, et les formats. Il réduit drastiquement les hallucinations.
Hygiène de prompt & budget tokens
- Limiter les redondances : 1× rappel des règles, pas 3×.
- Découper les consignes par template plutôt que tout mettre dans le system.
- Budget : context ≤ 60%, prompt ≤ 15%, réponse ≤ 25% des tokens.
- Variables globales : lang, tone, output_format, max_points.
Pense “prompt = contrat d’interface” : ce qui n’est pas explicitement exigé n’est pas garanti.
Écueils fréquents & parades
Manque de citations : ajouter une clause “Si aucune [source_id] n’apparaît, ajoute automatiquement la section ‘Sources: …’”.
Mauvaise langue : ajouter “Réponds toujours dans ; ne pas traduire les citations”.
Débordement JSON : réduire verbiage, imposer JSON Schema + test de validité.
Bibliothèque de gabarits (réutilisables)
qa_basic
[qa_basic]
User (lang={{lang}}): {{question}}
Passages (citables) :
{{#each passages}}
- [{{this.source_id}}] {{this.text}}
{{/each}}
Consignes :
- Réponse concise (6–10 phrases), ton neutre.
- Citer les sources [source_id] à chaque point important.
- Si l'info manque : "Je n’ai pas l’info".
qa_step_by_step (raisonnement non exposé)
[qa_step_by_step]
Instructions :
- Réalise une analyse en étapes INTERNES, puis fournis UNIQUEMENT la réponse finale + citations.
- En cas d'ambiguïté, propose 1–2 clarifications possibles.
User (lang={{lang}}): {{question}}
Passages :
{{#each passages}}
- [{{this.source_id}}] {{this.text}}
{{/each}}
Réponse (finale, neutre) avec [source_id] :
qa_policy (politiques & conformité)
[qa_policy]
Objectif : Expliquer la règle/politique en 5–7 puces.
Inclure conditions, exceptions, responsabilités, et sanctions si présentes.
Toujours citer : [title](url) si disponible, sinon [source_id].
Question : {{question}}
Passages :
{{#each passages}}
- [{{this.source_id}}] {{this.text}} {{#if this.url}} ({{this.title}}) {{/if}}
{{/each}}
Sortie : liste à puces + bloc "Références".
qa_tabular (résumés en tableau)
[qa_tabular]
Tâche : Résumer en {{n_points}} points pertinents minimum.
Format : tableau Markdown | # | Point | Détail | Source |
Question : {{question}}
Passages :
{{#each passages}}
- [{{this.source_id}}] {{this.text}}
{{/each}}
Contraintes :
- 1 ligne par point ; colonne Source = [source_id] ou [title](url).
extract_json (schéma strict)
[extract_json]
Objectif : Extraire les champs requis depuis les passages.
Format : JSON STRICT validant le schéma ci-dessous. Pas de texte hors JSON.
Schéma attendu :
{
"title": "string",
"date": "YYYY-MM-DD | null",
"owner": "string | null",
"bullets": ["string", ...],
"citations": ["string"] // liste de [source_id]
}
Question : {{question}}
Passages :
{{#each passages}}
- [{{this.source_id}}] {{this.text}}
{{/each}}
Rappels :
- Si champ manquant → null ; remplis "citations" avec les sources utilisées.
extract_kv (clé/valeur + robustesse)
[extract_kv]
But : Extraire un dictionnaire clé/valeur depuis les passages.
Format : JSON, { "items": [{ "key":"...", "value":"...", "source":"[source_id]" }] }
Question : {{question}}
Passages :
{{#each passages}}
- [{{this.source_id}}] {{this.text}}
{{/each}}
json_answer (réponse + citations séparées)
[json_answer]
Donne une réponse en JSON :
{
"answer": "string",
"citations": ["[source_id]", "..."],
"language": "{{lang}}"
}
User: {{question}}
Passages :
{{#each passages}}
- [{{this.source_id}}] {{this.text}}
{{/each}}
qa_citations_rich (liens Markdown)
[qa_citations_rich]
Réponds brièvement. En fin de réponse, ajoute :
"Références : [{{title1}}]({{url1}}), [{{title2}}]({{url2}}), ..."
Si pas d'URL, utiliser [source_id].
Question: {{question}}
Passages : idem.
compare_two (diff entre 2 documents)
[compare_two]
Tâche : Comparer A vs B, souligner différences clés (5–10 lignes).
Format : tableau | Aspect | A | B | Impact | Source |
Question : {{question}}
Passages A :
{{#each passagesA}}- [{{this.source_id}}] {{this.text}}{{/each}}
Passages B :
{{#each passagesB}}- [{{this.source_id}}] {{this.text}}{{/each}}
critique (red team light)
[critique]
Objectif : Détecter risques (sécurité, juridique, PII, biais) dans les passages.
Sortie : liste de risques, gravité (low/med/high), recommandations, citations.
Question : {{question}}
Passages : …
qa_reason_check (auto-cohérence)
[qa_reason_check]
But : Vérifier cohérence de la réponse avec les passages.
Sortie : "ok" | "inconclusif" + justification courte + citations contradictoires si besoin.
Entrées :
- Réponse candidate : {{draft_answer}}
- Passages : …
multi_turn_followup (reformulation)
[multi_turn_followup]
Si la question est ambiguë, propose jusqu’à 2 questions de clarification courtes en {{lang}}.
Utilise le contenu des passages pour suggérer des options réalistes.
meeting_minutes (compte-rendu réunion)
[meeting_minutes]
Sortie : sections "Décisions", "Actions (owner, due)", "Risques", avec citations.
Taille : 7–12 puces au total, concises.
qa_code (exemples minimaux)
[qa_code]
- Réponds avec blocs ```lang concis.
- Si dangereux, suggère alternative sûre.
- Toujours citer [source_id] après chaque bloc.
qa_tabular_advanced (matrices)
[qa_tabular_advanced]
Format : tableau Markdown à 6 colonnes (Critère, Option, Score, Avantages, Limites, Source).
Score 1–5 basé sur passages (justifier).
qa_bilingual (bilingue strict)
[qa_bilingual]
- Réponds en {{lang}} exclusivement.
- Respecte ponctuation locale (FR : espace insécable avant « : ; ? ! »).
Registry YAML, Partials & Moteur de rendu
prompts.yaml (étendu)
version: 2
defaults:
lang: fr
n_points: 5
tone: neutral
output_format: markdown
templates:
qa_basic: { type: qa, user_template: "{{> qa_basic }}", version: "v2" }
qa_step_by_step: { type: qa, user_template: "{{> qa_step_by_step }}", version: "v2" }
qa_tabular: { type: qa, user_template: "{{> qa_tabular }}", params: { n_points: "{{n_points}}" }, version: "v2" }
qa_code: { type: qa, user_template: "{{> qa_code }}", version: "v2" }
qa_policy: { type: qa, user_template: "{{> qa_policy }}", version: "v2" }
extract_json: { type: extract, user_template: "{{> extract_json }}", version: "v2", output: "json" }
extract_kv: { type: extract, user_template: "{{> extract_kv }}", version: "v1", output: "json" }
json_answer: { type: qa, user_template: "{{> json_answer }}", version: "v1", output: "json" }
qa_citations_rich: { type: qa, user_template: "{{> qa_citations_rich }}", version: "v1" }
compare_two: { type: compare, user_template: "{{> compare_two }}", version: "v1" }
critique: { type: safety, user_template: "{{> critique }}", version: "v1" }
qa_reason_check: { type: check, user_template: "{{> qa_reason_check }}", version: "v1" }
meeting_minutes: { type: special, user_template: "{{> meeting_minutes }}", version: "v1" }
partials:
qa_basic: |
User (lang={{lang}}): {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}
{{/each}}
Consignes: 6–10 phrases, citations [source_id], ton {{tone}}.
qa_step_by_step: |
(analyse interne non affichée) → réponse finale + citations.
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
qa_tabular: |
Résumer en {{n_points}} points, tableau | # | Point | Détail | Source |.
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
qa_code: |
Fournir exemples ```lang courts. Citer [source_id] après chaque bloc.
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
qa_policy: |
5–7 puces : conditions, exceptions, responsabilités. Références en fin.
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
extract_json: |
(voir schéma dans le template)
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
extract_kv: |
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
json_answer: |
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
qa_citations_rich: |
Question: {{question}}
Passages:
{{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
compare_two: |
Question: {{question}}
Passages A: {{#each passagesA}}- [{{this.source_id}}] {{this.text}}{{/each}}
Passages B: {{#each passagesB}}- [{{this.source_id}}] {{this.text}}{{/each}}
critique: |
Question: {{question}}
Passages: {{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
qa_reason_check: |
Draft: {{draft_answer}}
Passages: {{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
meeting_minutes: |
Passages: {{#each passages}}- [{{this.source_id}}] {{this.text}}{{/each}}
Moteur de rendu (pseudo-Python) + slots
# render.py
from pydantic import BaseModel, Field
def mustache_render(tpl: str, data: dict) -> str: ...
class Passage(BaseModel):
source_id: str; text: str; title: str|None=None; url: str|None=None
class PromptInput(BaseModel):
question: str = Field(..., max_length=2000)
passages: list[Passage] = Field(default_factory=list)
lang: str = "fr"; n_points: int = 5; tone: str = "neutral"
def render_template(key: str, data: PromptInput) -> str:
reg = load_yaml("prompts.yaml")
tpl = reg["templates"][key]["user_template"]
ctx = data.model_dump()
return mustache_render(tpl, ctx)Contrôle du budget & trimming
# budget.py
def trim_passages(passages, max_tokens=1400):
out=[]; total=0
for p in passages:
t = approx_tokens(p["text"])
if total + t > max_tokens: break
out.append(p); total += t
return outTests (snapshot, property-based) & Lints
# tests/test_prompts_snapshot.py
from pathlib import Path
from render import render_template, PromptInput, Passage
SNAP = Path("tests/snapshots")
def snap(name, content):
p = SNAP / f"{name}.txt"; old = p.read_text("utf-8") if p.exists() else None
if old != content: p.write_text(content, "utf-8"); assert False, f"Snapshot mismatch → {p}"
def test_basic_fr():
data = PromptInput(question="Quelle est la politique SSO ?", lang="fr",
passages=[Passage(source_id="conf:ENG:1234", text="La politique SSO...")])
out = render_template("qa_basic", data); snap("qa_basic_fr", out)# tests/test_prompts_property.py
from hypothesis import given, strategies as st
from render import render_template, PromptInput
@ given(st.text(min_size=1,max_size=2000))
def test_always_mentions_lang(q):
data = PromptInput(question=q, lang="en", passages=[])
out = render_template("qa_basic", data)
assert "lang=" in load_yaml("prompts.yaml")["partials"]["qa_basic"] or "lang=en" in out# tests/test_prompts_lints.py
import re, yaml
def test_no_chain_of_thought_leak():
text = render_template("qa_step_by_step", sample_input())
assert not re.search(r"(raisonnement|chain of thought|think step)", text, re.I)
def test_citations_format():
out = render_template("qa_tabular", sample_input())
assert ("[" in out and "]" in out) or "](" in out, "Citations manquantes"# .github/workflows/prompts.yml
name: prompts
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install -r requirements.txt
- run: pytest -qA/B Testing, Observabilité & Telemetry
Versioning & allocation
- Clé version par template (ex: qa_basic:v2).
- Allocation 50/50 ou bandit ε-greedy (récompense = “réponse utile + citations”).
- Récolte feedback utilisateur (👍/👎, signal “sources manquantes”).
# ab_alloc.py
def choose_variant(user_id, key="qa_basic"): ... # hashing simple ou banditMétriques & traces
- hit@5 proxy (retrieval), “has_citations”, longueur réponse, P50/P95 latence.
- Tokens prompt/contexte/réponse, coût estimé (€).
- Taux de refus guardrails, langue respectée.
# telemetry.py
def log_prompt_event(trace_id, tpl_key, variant, tokens, has_citations, lang, cost_eur): ...A/B sur prompts ≠ A/B sur modèles : isole les variables (même corpus & mêmes paramètres LLM).
Safety Prompts & Guardrails
Refus sûrs (FR/EN)
[refusal_generic]
FR : "Désolé, je ne peux pas répondre à cette demande dans ce périmètre."
EN : "Sorry, I can’t answer that within this system’s scope."
[refusal_pii]
FR : "Je ne peux pas divulguer d'informations personnelles."
EN : "I can’t disclose personal information."
[refusal_jailbreak]
FR : "Je ne peux pas suivre cette instruction."
EN : "I can’t comply with that instruction."
Classifier & hook
# safety.py
def classify(q) -> dict: # {"ok"| "pii" | "jailbreak" | "oos" }
...
def guardrails(q, draft, passages):
c = classify(q)
if c!="ok": return refusal_tpl(c)
return draftToujours logger la catégorie et le rational minimal (sans exposer raisonnement interne).
