Aller au contenu
English

Répondre à partir de pages d'orientation publiques

  • Étude de cas 1
  • 240 min
  • Facultative · 1 étude de cas requise pour le certificat

Ce que vous saurez faire

Créez un assistant qui répond aux questions sur les droits au travail à partir de vraies pages d’orientation de GOV.UK. Chaque affirmation cite un passage. Chaque source affiche son adresse et la date de récupération. Quand les pages ne donnent pas de réponse, l’assistant le dit. Vous mesurez la recherche et les réponses sur un ensemble fixe de questions. Vous comparez aussi le coût d’un petit modèle et d’un modèle performant.

Étude de casPièce de portfolio

Durée
Environ 4 h
S’exécute dans
VS Code, Python 3.12+
Compétences utilisées
  • Licences des données publiques
  • Un instantané fixe avec sommes de contrôle
  • Analyse avec provenance
  • Identifiants de segments stables
  • BM25, e5 et reclassement
  • Réponses avec citations et vérifications
  • Abstention
  • recall@k et mesures des réponses
  • Coût et latence

Décompressez le projet de départ, ouvrez le dossier dans VS Code et suivez les étapes ci-dessous. Elles commencent par un nouvel environnement virtuel.

Le projet de départ suit la structure de policy-assistant, le projet du cours. Il contient déjà l’instantané, les questions et les réponses enregistrées. Il contient aussi les éléments que vous avez créés dans le cours : recherche, reclassement, contexte, schéma de réponse, vérifications des citations et fournisseur simulé. Vous écrivez les trois fichiers propres aux pages d’orientation publiques : l’analyseur, le découpeur et le code qui affiche les sources. Le projet terminé présente le résultat, si vous avez besoin d’aide ou voulez comparer. Vous n’avez besoin d’aucun compte : la simulation rejoue les réponses données par de vrais modèles.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 240 minutes

Ce que vous devez savoir

Cette étude de cas applique tout le cours à de nouveaux documents. Voici les leçons les plus utilisées :

Ce qu’il vous faut sur votre ordinateur

  • Python 3.12 ou une version plus récente, VS Code et un terminal, comme dans le cours.
  • Environ 2 Go d’espace disque libre : 0,9 Go pour les paquets et 1 Go pour les deux modèles locaux. Si vous avez suivi le cours, les modèles sont déjà dans votre cache Hugging Face.

Testé sur macOS (puce Apple) avec Python 3.12.13. Les versions sont celles du cours : sentence-transformers 6.1.0, torch 2.14.1, pydantic 2.14.0, pytest 9.1.1. Les commandes Windows suivent celles du cours, mais elles n’ont pas été testées.

Joy Mensah dirige les séances d’un petit centre de conseil dans une ville anglaise. Des bénévoles aident les gens à résoudre un problème au travail : congés refusés, indemnités de maladie interrompues, lettre de licenciement économique. La plupart des réponses se trouvent dans les pages d’orientation de GOV.UK, réparties sur des centaines de pages. Quand des gens attendent, un bénévole ne peut pas lire un guide en huit parties pour chaque question.

Ravi Patel, un développeur bénévole, propose de créer un assistant qui trouve les passages et donne une réponse courte avec ses pages. Joy accepte à trois conditions. Chaque réponse doit montrer sa source et l’ancienneté de la page. Un bénévole doit pouvoir ouvrir la page et la lire avec la personne. Quand les pages d’orientation ne répondent pas, l’assistant doit le dire. Une réponse fausse mais assurée sur un congédiement peut faire perdre à quelqu’un son recours. Et l’assistant ne doit jamais se présenter comme un service gouvernemental ou un avocat.

Joy, Ravi et le centre sont fictifs. Les pages sont réelles, tout comme les questions que les gens posent à leur sujet.

Vous allez créer guidance-assistant, un petit projet Python. L’image montre le parcours complet d’une question.

Un flux de gauche à droite. Un instantané de 137 pages de GOV.UK, récupérées le 9 octobre 2026 et vérifiées avec SHA-256, est analysé et découpé en 504 segments. Chacun conserve un identifiant, l'adresse de la page avec une ancre et des dates. La question d'un conseiller, par exemple Puis-je reporter mes congés inutilisés?, passe à la recherche : BM25 et vecteurs e5, fusion avec RRF, reclassement, cinq meilleurs résultats. Le contexte présente chaque passage avec sa page, sa section, sa date et son adresse. Le modèle écrit des affirmations, chacune avec des identifiants de passages. Le code vérifie que chaque identifiant existe et figurait dans le contexte, puis vérifie les nombres et les mots. Une décision : les pages répondent-elles à la question? Oui : une fiche de réponse avec sources numérotées, adresses, dates et une note précisant qu'il ne s'agit pas de conseils juridiques. Non : les pages ne le précisent pas; consultez Acas ou un conseiller.
La conception de guidance-assistant. La recherche et la réponse viennent du cours; l'instantané, les adresses et les dates de chaque source sont nouveaux.
Description textuelle du diagramme
  1. Un instantané de 137 pages de GOV.UK, récupérées une seule fois le 9 octobre 2026 et vérifiées avec SHA-256, est analysé et découpé en 504 segments. Chaque segment conserve son identifiant, l’adresse de sa page avec une ancre et ses dates.
  2. La question d’un conseiller passe à la recherche du cours : BM25 et vecteurs e5, fusion avec RRF, puis reclassement. Les cinq meilleurs segments sont conservés.
  3. Le contexte présente chaque passage avec sa page, sa section, sa date de récupération et son adresse.
  4. Le modèle répond par des affirmations, et chacune cite des identifiants de passages.
  5. Le code vérifie chaque citation : l’identifiant existe, il figurait dans le contexte, ses nombres et ses mots sont dans le passage.
  6. Si les pages répondent à la question, le conseiller voit une fiche de réponse avec sources numérotées, adresses et dates. Un avis précise qu’il s’agit d’information générale, pas de conseils juridiques. Sinon, la fiche le dit et renvoie vers Acas ou un conseiller.

Trois fichiers sont nouveaux. L’analyseur lit le HTML de GOV.UK, le découpeur donne une adresse Web à chaque segment, et sources.py affiche les sources.

Les pages proviennent de GOV.UK, le site Web du gouvernement britannique. Nous avons choisi un sujet qu’un centre de conseil rencontre chaque jour : les droits au travail en Angleterre, en Écosse et au pays de Galles (congés, indemnités de maladie, congés familiaux, préavis, licenciement économique et congédiement, heures de travail, contrats, salaire minimum). L’instantané contient 38 guides répartis sur 137 pages, soit environ 36 000 mots.

La plupart des contenus de GOV.UK sont protégés par le droit d’auteur de la Couronne sous Open Government Licence v3.0 (OGL). Tout le monde peut les copier, les adapter et les partager, même à des fins commerciales. La licence exige une chose : nommer la source. GOV.UK ne prescrit aucune formulation particulière pour ces pages. Nous utilisons donc le texte par défaut de la licence : « Contient des informations du secteur public sous licence Open Government Licence v3.0. » Le projet l’affiche sous chaque réponse.

La licence ne couvre pas les données personnelles, les logos, les armoiries ni les droits de tiers. Notre instantané contient seulement le corps du texte, sans ces éléments. La licence n’accorde pas non plus le droit « d’utiliser l’information d’une manière qui suggère un statut officiel ». Les conditions de GOV.UK ajoutent que le site ne publie pas de conseils : « Vous devriez obtenir des conseils professionnels ou spécialisés avant d’agir sur la base du contenu. » Ces deux phrases deviennent des règles du projet, dans Utilisation responsable.

Nous avons aussi examiné les pages d’orientation de Canada.ca. Leurs conditions permettent seulement la reproduction non commerciale sans autorisation. Elles ne respectent donc pas la règle du cours sur les licences. La Licence du gouvernement ouvert – Canada couvre les jeux de données sur open.canada.ca, pas les pages Web.

Un instantané est une copie des pages à un moment précis, conservée avec une empreinte de chaque fichier. Imaginez une photocopie datée d’un tableau d’affichage : le tableau changera, mais pas la copie. Sans elle, les questions, les mesures et les réponses enregistrées ne correspondraient plus dès que GOV.UK modifierait une phrase.

Nous n’avons pas copié les pages Web. GOV.UK propose une Content API officielle, qui renvoie chaque page en JSON avec son contenu en HTML et ses dates. Le script scripts/fetch_snapshot.py a demandé une seule fois 47 guides candidats, à raison d’une requête toutes les 1,2 secondes. Il a d’abord lu le fichier robots.txt de GOV.UK, qui autorise l’API. Nous en avons conservé 38. guidance/pages.jsonl contient une ligne par page, avec :

Champ Exemple
page_id, url statutory-sick-pay/eligibility, https://www.gov.uk/statutory-sick-pay/eligibility
guide_title, part_title Statutory Sick Pay (SSP), Eligibility (indemnités légales de maladie, admissibilité)
public_updated_at 2015-03-09 : date de la dernière modification majeure selon GOV.UK
updated_at 2026-10-05 : date de la dernière modification, quelle que soit son ampleur
retrieved_at 2026-10-09T06:21:10Z : moment de la récupération
api_sha256, body_sha256 empreintes de la réponse de l’API et du corps du texte
body_html corps de la page, exactement comme l’API l’a renvoyé

Nous avons lu l’instantané avant d’écrire la recherche. Quatre constats orientent le projet :

  • La date de « dernière mise à jour » peut tromper. public_updated_at est antérieur à 2020 pour 26 des 38 guides (le plus ancien date du 17 novembre 2014). Pourtant, le guide des indemnités de maladie donne le montant d’avril 2026, soit 123,25 £. updated_at (du 24 septembre au 5 octobre 2026) reflète mieux la réalité, mais aucun champ ne précise quelle phrase a changé. L’assistant affiche donc la date de récupération et updated_at, et demande de vérifier la page.
  • Certaines pages contiennent leur propre historique. La page du salaire minimum contient quatre tableaux : les taux actuels et trois tableaux de taux antérieurs. Certaines règles portent une date sur la page : les soins néonatals concernent les bébés nés à partir du 6 avril 2025. Une règle de préavis pour le congé de paternité concerne les bébés dont la naissance est prévue avant le 25 juillet 2026.
  • L’Irlande du Nord est exclue. Plusieurs pages disent « Les règles sont différentes si vous vivez en Irlande du Nord », sans donner ces règles.
  • Une ligne répétée. Neuf pages contiennent la ligne « Ce guide est aussi disponible en gallois (Cymraeg). » Elle ne répond à aucune question et rend ces pages semblables pour la recherche.

Avant toute recherche, Joy et Ravi définissent une bonne réponse, comme dans Le corpus et les questions. Le fichier questions/questions.jsonl contient 44 questions de six types. Chacune comporte une réponse de référence, les passages pertinents (page et section) et des clés : des expressions régulières auxquelles une bonne réponse doit correspondre, comme dans le cours.

Type Nombre Exemple Bonne réponse
Fait 12 Pendant combien de temps peut-on recevoir les indemnités légales de maladie? Jusqu’à 28 semaines
Condition 14 Ai-je droit à l’indemnité légale de licenciement économique? Normalement oui, si vous êtes salarié avec 2 ans de service
Tableau 3 Quel est le salaire horaire minimum pour une personne de 18 ans? 10,85 £ (à partir d’avril 2026)
Date 5 Quel était le salaire minimum pour une personne de 22 ans en juin 2023? 10,18 £
Calcul 2 Je travaille 4 jours par semaine. À combien de jours de congé légal ai-je droit? 22,4 jours (4 × 5,6)
Non couvert 8 Quel est le montant des indemnités légales de maladie en Irlande du Nord? « Les pages d’orientation ne le précisent pas »

Les questions de condition comptent le plus pour un centre de conseil. En droit du travail, la réponse à « Puis-je…? » est souvent « oui, si… ». Une réponse qui omet la condition est fausse, même si ses nombres sont justes. Les clés vérifient la condition, par exemple « 2 ans ». Les questions non couvertes sont des pièges. Certaines concernent l’Irlande du Nord, une demande un conseil personnel et une invite à prendre un nombre dans la mauvaise règle.

L’auteur de cette étude de cas a rédigé chaque question, réponse et clé en lisant les pages. Aucun modèle n’a rédigé ni étiqueté de question. Un script vérifie que chaque passage existe et que chaque clé correspond à sa réponse de référence. Après avoir lu les premières réponses enregistrées, nous avons modifié deux vérifications, comme nous l’indiquons dans DATASET.md. La clé de Q34 accepte maintenant aussi « not met » (non remplie), car une bonne réponse disait « the birth-date condition is not met » (la condition de date de naissance n’est pas remplie). Un motif must_not de Q41 ne détecte plus « whether you should accept » (si vous devriez accepter). Il avait classé cette abstention comme un conseil. Nous avons ensuite figé l’ensemble, avant toute comparaison. Mesurer les réponses explique pourquoi les données de test doivent être figées avant une comparaison finale.

Les étapes utilisent VS Code et son terminal. Toutes les commandes s’exécutent dans le dossier du projet.

  1. Téléchargez le projet de départ dans l’encadré en haut de cette page. Décompressez-le dans votre dossier de projets. Vous obtenez le dossier guidance-assistant.

  2. Dans VS Code, choisissez File > Open Folder (fichier, ouvrir un dossier), puis ouvrez guidance-assistant. Ouvrez ensuite un terminal : Terminal > New Terminal (terminal, nouveau terminal).

  3. Créez et activez un environnement virtuel :

    Fenêtre de terminal
    python3 -m venv .venv
    source .venv/bin/activate

    L’invite commence par (.venv). Si VS Code demande d’utiliser le nouvel environnement pour ce dossier, choisissez Yes (oui).

  4. Installez les versions du cours. Sur Linux, installez d’abord PyTorch pour processeur seulement : python -m pip install torch==2.14.1 --index-url https://download.pytorch.org/whl/cpu.

    Fenêtre de terminal
    python -m pip install -r requirements.txt
  5. Exécutez les tests et vérifiez l’instantané :

    Fenêtre de terminal
    python -m pytest
    python scripts/check_snapshot.py
Output (end of pytest, then check_snapshot.py)
========================= 5 passed, 9 skipped in 0.46s =========================
OK pages.jsonl SHA-256 b48f97342a68…
OK sources.csv SHA-256 499806c88713…
137 pages from 38 GOV.UK guides, fetched 2026-10-09

Ce que cela nous apprend. Les paquets fonctionnent, et l’instantané est celui que nous avons testé. Chaque chargement compare d’abord les fichiers avec guidance/SHA256SUMS. Un test remplace 123,25 £ par 132,25 £ dans une copie et vérifie que le chargement s’arrête. Les 9 tests ignorés concernent les trois fichiers que vous écrivez. Chacun s’exécute dès que son fichier existe.

Examinez les pages et les questions que les pages d’orientation ne couvrent pas :

Fenêtre de terminal
python -m guidance_assistant pages
python -m guidance_assistant questions --kind not_covered
Output (the last lines of each)
1 page 2026-10-05 Payslips: employee rights
1 page 2026-10-05 Pay and work rights helpline and complaints
137 pages from 38 GOV.UK guides | fetched 2026-10-09 | checksums OK
...
Q43 not_covered What is the rate of Statutory Sick Pay in Northern Ireland?
Q44 not_covered How much compensation will I get if I win an unfair dismissal claim?
8 questions

Partie 1 : lire les pages et conserver le chemin vers la source

Section intitulée « Partie 1 : lire les pages et conserver le chemin vers la source »

Le corps d’une page GOV.UK est en HTML : titres avec un id, paragraphes, listes, quelques tableaux et encadrés d’avis. La recherche a besoin de texte propre. Le conseiller a besoin du chemin vers la source : la page, la section et l’adresse de cette section sur GOV.UK. C’est l’idée d’Analyser et normaliser, appliquée à un nouveau format : analyser en blocs. Chaque bloc conserve sa section, l’id de son titre et sa position dans le texte propre. Cet identifiant est l’ancre, la partie d’une adresse après #.

La bibliothèque standard de Python contient un analyseur HTML. Le projet n’a donc besoin d’aucun nouveau paquet. Créez guidance_assistant/parse.py :

guidance_assistant/parse.py
"""Parse a GOV.UK page body (HTML) into clean blocks that remember where they came from.
A block is a heading, a paragraph, a list item, a table or a notice. Each block keeps:
- section: the heading it is under ("" before the first heading),
- anchor: that heading's id, so a citation can link to https://www.gov.uk/<page>#<anchor>,
- start, end: its character positions in the page's clean text.
What the parser removes, and why:
- tags (links keep their words, abbreviations their short form: "SSP");
- the line "This guide (or page) is also available in Welsh (Cymraeg)": it repeats on many
pages, says nothing about the question, and makes those pages look alike to a search.
Tables become one block with one line per row ("cell | cell | cell"): a table is never split.
"""
import html
import re
from dataclasses import dataclass, field
from html.parser import HTMLParser
DROP = re.compile(r"^This (guide|page) is also available in Welsh", re.I)
BLOCK_TAGS = {"p": "paragraph", "li": "item", "h2": "heading", "h3": "heading", "h4": "heading"}
@dataclass(frozen=True)
class Block:
kind: str # heading, paragraph, item, table, notice
text: str
section: str
anchor: str
start: int = 0
end: int = 0
@dataclass
class ParsedPage:
page_id: str
title: str
blocks: list = field(default_factory=list)
text: str = ""
dropped: list = field(default_factory=list) # texts removed as boilerplate
def clean(text: str) -> str:
return re.sub(r"\s+", " ", html.unescape(text)).strip()
class _Reader(HTMLParser):
"""Collects (kind, text, heading id) in reading order."""
def __init__(self):
super().__init__(convert_charrefs=True)
self.out: list[tuple[str, str, str]] = []
self.buffer: list[str] | None = None
self.kind = ""
self.heading_id = ""
self.notice_depth = 0 # inside <div role="note"> (info and help notices)
self.table: list[list[str]] | None = None
self.cell: list[str] | None = None
def handle_starttag(self, tag, attrs):
a = dict(attrs)
if tag == "div" and a.get("role") == "note":
self.notice_depth += 1
if tag == "table":
self.table = []
elif self.table is not None:
if tag == "tr":
self.table.append([])
elif tag in ("td", "th"):
self.cell = []
return
if tag in BLOCK_TAGS and self.buffer is None:
self.buffer, self.kind = [], BLOCK_TAGS[tag]
if self.kind == "heading":
self.heading_id = a.get("id", "")
elif tag == "li" and self.buffer is not None:
self.buffer.append(" ") # a nested list item continues its parent item
if tag == "br" and self.buffer is not None:
self.buffer.append(" ")
def handle_endtag(self, tag):
if self.table is not None:
if tag in ("td", "th") and self.cell is not None:
self.table[-1].append(clean("".join(self.cell)))
self.cell = None
elif tag == "table":
rows = [" | ".join(r) for r in self.table if any(r)]
self.out.append(("table", "\n".join(rows), ""))
self.table = None
return
if tag in BLOCK_TAGS and self.buffer is not None and BLOCK_TAGS[tag] == self.kind:
text = clean("".join(self.buffer))
if text:
kind = "notice" if self.notice_depth and self.kind == "paragraph" else self.kind
self.out.append((kind, text, self.heading_id if kind == "heading" else ""))
self.buffer = None
if tag == "div" and self.notice_depth:
self.notice_depth -= 1
def handle_data(self, data):
if self.cell is not None:
self.cell.append(data)
elif self.buffer is not None:
self.buffer.append(data)
def parse_page(page) -> ParsedPage:
"""page: a snapshot.Page. Returns its blocks and clean text, with positions."""
reader = _Reader()
reader.feed(page.body_html)
reader.close()
out = ParsedPage(page.page_id, page.title)
section, anchor, parts, pos = "", "", [], 0
for kind, text, heading_id in reader.out:
if DROP.search(text):
out.dropped.append(text)
continue
if kind == "heading":
section, anchor = text, heading_id
shown = f"- {text}" if kind == "item" else text
start = pos
parts.append(shown)
pos += len(shown)
out.blocks.append(Block(kind, shown, section, anchor, start, pos))
parts.append("\n")
pos += 1
out.text = "".join(parts)
return out

Trois choix comptent. Un tableau devient un seul bloc, pour qu’aucun segment ne puisse le couper. La ligne sur le gallois est supprimée, et dropped l’indique : un nettoyage visible est un nettoyage vérifiable. Enfin, start et end permettent de retrouver chaque bloc.

Exécutez les tests, puis analysez la page du salaire minimum :

Fenêtre de terminal
python -m pytest
python -m guidance_assistant parse national-minimum-wage-rates
Output (the end of pytest, then part of the blocks)
========================= 8 passed, 8 skipped in 0.05s =========================
National Minimum Wage and National Living Wage rates | https://www.gov.uk/national-minimum-wage-rates
[ 0-90 ] paragraph | The hourly rate for the minimum wage depends on your age an…
[ 275-288 ] heading Current rates | Current rates
[ 289-328 ] paragraph Current rates | The rates change on 1 April every year.
[ 329-417 ] table Current rates | | 21 and over | 18 to 20 | Under 18 | Apprentice April 2026…
...
[ 1095-1139 ] heading Rates between 1 April 202… | Rates between 1 April 2024 and 31 March 2026
[ 1221-1384 ] table Rates between 1 April 202… | | 21 and over | 18 to 20 | Under 18 | Apprentice April 2025…
...
[ 1887-2077 ] table Rates before 1 April 2021 | | 25 and over | 21 to 24 | 18 to 20 | Under 18 | Apprentice…
dropped: This page is also available in Welsh (Cymraeg).
33 blocks, 2373 characters

Ce que cela nous apprend. La page contient quatre tableaux, chacun dans un seul bloc sous son propre titre. Le tableau actuel et les anciens ont les mêmes colonnes et des nombres semblables. Seul le titre indique lequel est actuel. Gardez cela en tête pour la partie 3.

Découpez les blocs en segments comme dans Découper de façon réfléchie. Chaque section forme un segment d’au plus 200 mots, sans couper un tableau, avec le titre de la page au début. Deux éléments sont nouveaux. L’identifiant du segment utilise le hachage de la page, de son updated_at et des positions, comme dans Conserver les métadonnées. Une page modifiée par GOV.UK reçoit de nouveaux identifiants : une ancienne citation ne peut donc pas pointer vers un nouveau texte. Chaque segment conserve aussi son adresse Web avec l’ancre et ses trois dates. Une citation peut ainsi mener à la section exacte.

Créez guidance_assistant/chunk.py :

guidance_assistant/chunk.py
"""Structure-aware chunks with stable IDs, as in the course's policy assistant.
One chunk per section (from a heading to the next heading; the text before the first heading is a
section too). A section longer than MAX_WORDS is split between blocks, never inside a table.
Each chunk starts with the page title (and the section heading when a part does not start with it),
so a chunk keeps its context: "Exceptions" alone does not say which guide.
The chunk ID is the first 12 hexadecimal characters of SHA-256 over "page_id|updated_at|start|end":
the same text of the same page gives the same ID every time, a changed page (a new updated_at)
gives new IDs, and adding a page changes no other ID.
"""
import hashlib
from dataclasses import asdict, dataclass
from .parse import ParsedPage
MAX_WORDS = 200
def chunk_id(page_id: str, updated_at: str, start: int, end: int) -> str:
return hashlib.sha256(f"{page_id}|{updated_at}|{start}|{end}".encode("utf-8")).hexdigest()[:12]
@dataclass(frozen=True)
class Chunk:
chunk_id: str
page_id: str
title: str # the page title: "Guide: Part"
section: str # the heading ("" = the top of the page)
anchor: str # the heading's id on GOV.UK
position: int
start: int
end: int
text: str # what is indexed and shown to the model
url: str # the page, with #anchor when the section has one
public_updated_at: str
updated_at: str
retrieved_at: str
def to_dict(self) -> dict:
return asdict(self)
def structure(parsed: ParsedPage, page, max_words: int = MAX_WORDS) -> list[Chunk]:
sections: list[list] = []
for b in parsed.blocks:
if b.kind == "heading" or not sections:
sections.append([])
sections[-1].append(b)
chunks: list[Chunk] = []
for blocks in sections:
part: list = []
for b in blocks:
words = sum(len(x.text.split()) for x in part)
if part and words + len(b.text.split()) > max_words:
chunks.append(_make(parsed, page, part, len(chunks)))
part = []
part.append(b)
if part:
chunks.append(_make(parsed, page, part, len(chunks)))
return chunks
def _make(parsed: ParsedPage, page, blocks: list, position: int) -> Chunk:
start, end = blocks[0].start, blocks[-1].end
section, anchor = blocks[0].section, blocks[0].anchor
prefix = page.title if blocks[0].kind == "heading" or not section else f"{page.title}\n{section}"
return Chunk(
chunk_id=chunk_id(page.page_id, page.updated_at, start, end), page_id=page.page_id, title=page.title,
section=section, anchor=anchor, position=position, start=start, end=end,
text=f"{prefix}\n{parsed.text[start:end]}", url=page.url + (f"#{anchor}" if anchor else ""),
public_updated_at=page.public_updated_at[:10], updated_at=page.updated_at[:10],
retrieved_at=page.retrieved_at[:10])

Exécutez les tests, construisez l’index et examinez les segments d’une page :

Fenêtre de terminal
python -m pytest
python -m guidance_assistant ingest
python -m guidance_assistant chunks statutory-sick-pay/eligibility
Sortie
======================== 18 passed, 1 skipped in 0.14s =========================
index index.sqlite: 504 chunks from 137 pages | embeddings intfloat/multilingual-e5-small | 31.5 s
0a1832934dc9 #0 chars 0-221 | 48 words | https://www.gov.uk/statutory-sick-pay/eligibility
daa76305e45b #1 chars 222-449 | 49 words | https://www.gov.uk/statutory-sick-pay/eligibility#telling-your-employer
da9155fa4f2f #2 chars 450-580 | 28 words | https://www.gov.uk/statutory-sick-pay/eligibility#exceptions
261d90e47a19 #3 chars 581-887 | 63 words | https://www.gov.uk/statutory-sick-pay/eligibility#linked-periods-of-sickness
a09f0d23000a #4 chars 888-1270 | 72 words | https://www.gov.uk/statutory-sick-pay/eligibility#fit-notes-and-asking-for-proof
e668f89c2ed3 #5 chars 1271-1566 | 56 words | https://www.gov.uk/statutory-sick-pay/eligibility#other-proof-of-sickness
1229b76afc00 #6 chars 1567-2114 | 110 words | https://www.gov.uk/statutory-sick-pay/eligibility#if-youre-not-eligible-or-your-ssp-ends
7 chunks

Ce que cela nous apprend. Chaque section forme un segment, et chaque segment possède une adresse qui ouvre GOV.UK à cette section. Le segment médian contient 72 mots; le plus long en contient 211, dans un seul bloc impossible à diviser. ingest a pris 31,5 secondes sur notre Mac avec 2 fils d’exécution. L’essentiel du temps servait à charger le modèle e5 et à créer les plongements des 504 segments. Le seul test ignoré concerne sources.py.

L’index contient les quatre méthodes de recherche du cours, sans modification. Mesurez-les sur les 36 questions ayant des passages pertinents (les 8 questions non couvertes n’en ont aucun) :

Fenêtre de terminal
python -m guidance_assistant eval --method bm25
python -m guidance_assistant eval --method dense
python -m guidance_assistant eval --method hybrid
python -m guidance_assistant eval --method rerank
Sortie
bm25, top 5: hit@5 0.778 | recall@5 0.701 | precision@5 0.244 | MRR 0.549 (36 questions with relevant passages)
dense, top 5: hit@5 0.972 | recall@5 0.861 | precision@5 0.300 | MRR 0.867 (36 questions with relevant passages)
hybrid, top 5: hit@5 0.889 | recall@5 0.829 | precision@5 0.289 | MRR 0.785 (36 questions with relevant passages)
rerank, top 5: hit@5 0.944 | recall@5 0.859 | precision@5 0.294 | MRR 0.872 (36 questions with relevant passages)

Le graphique ajoute le rappel à d’autres valeurs de k, comme dans Mesurer la récupération.

  • bm25 (dashed)
  • dense
  • hybrid
  • rerank
recall@k des quatre méthodes de recherche sur les 36 questions ayant des passages pertinents (instantané du 9 octobre 2026, segments structurés, e5, modèle de reclassement du cours).
Description textuelle du graphique

Quatre courbes montrent le rappel pour k = 1, 3, 5 et 10. BM25 : 0,301, 0,595, 0,701, 0,843. Dense : 0,535, 0,792, 0,861, 0,956. Hybride : 0,484, 0,755, 0,829, 0,914. Reclassement : 0,604, 0,729, 0,859, 0,951. BM25 a le rappel le plus faible pour chaque k. Le reclassement a le plus élevé pour k = 1; la recherche dense et le reclassement sont proches à 5 et 10.

Ce que cela nous apprend. La tendance observée dans la collection de Larkfield se maintient sur des pages réelles. Les mots-clés manquent beaucoup de questions, car les gens n’utilisent pas les mêmes mots que les pages. Les vecteurs en trouvent davantage. La recherche hybride avec RRF reste inférieure à la recherche dense seule, pour la raison expliquée dans Recherche lexicale, dense et hybride. Le modèle de reclassement place le bon passage en premier le plus souvent (recall@1 de 0,604, MRR de 0,872). Cela a un coût : environ 340 ms par question sur notre Mac, contre 5,5 ms pour la recherche dense. Avec 36 questions, une question représente 0,028. La recherche dense et le reclassement ne se distinguent donc pas clairement. Nous gardons le reclassement, le choix par défaut du cours. Chaque méthode manque une des trois questions portant sur des tableaux, et cet échec est le plus instructif.

La question Quel est le salaire horaire minimum pour une personne de 18 ans? mène à une page GOV.UK contenant quatre tableaux. Taux actuels, avril 2026, 18 à 20 ans : 10,85 £ : rang 6 après reclassement, hors des 5 premiers. Taux du 1er avril 2024 au 31 mars 2026, 18 à 20 ans : 10 £ et 8,60 £ : rang 4. Taux du 1er avril 2021 au 31 mars 2024 : 7,49 £, 6,83 £, 6,56 £ : rang 7. Taux avant le 1er avril 2021 : 6,45 £ et 6,15 £ : rang 3. Les 5 premiers passages envoyés au modèle contiennent deux anciens tableaux, mais pas le tableau actuel. chat-small s'est abstenu : les pages d'orientation ne donnent pas le taux à la date de récupération.
Un échec réel (Q27, reclassement). La page contient son propre historique, et le modèle de reclassement ne distingue pas le tableau actuel des anciens.
Description textuelle du diagramme

La question demande le salaire horaire minimum d’une personne de 18 ans. Elle mène à une page contenant quatre tableaux de même forme. Le modèle de reclassement place le tableau des taux d’avant le 1er avril 2021 au rang 3. Il place celui de 2024 à 2026 au rang 4. Le tableau actuel, avec 10,85 £, arrive au rang 6, juste hors des 5 premiers. Le tableau de 2021 à 2024 arrive au rang 7. Le modèle reçoit donc deux anciens tableaux, mais pas le tableau actuel. chat-small s’est abstenu : « Les pages d’orientation ne précisent pas le salaire horaire minimum d’une personne de 18 ans à la date de récupération. »

Chez Larkfield, une ancienne politique était un document distinct avec des dates d’entrée en vigueur. Un filtre l’excluait. Ici, une page contient son propre historique : les taux actuels et trois tableaux antérieurs, sous des titres avec des dates. Pour un modèle de reclassement, « taux pour les 18 à 20 ans » se ressemble en 2020 et en 2026. BM25 réussit mieux sur cette question (le tableau actuel au rang 2). La recherche dense le place au rang 6, comme le modèle de reclassement.

Deux éléments ont rendu cet échec sûr. Le modèle a reçu le titre du tableau avec chaque passage (« section : taux avant le 1er avril 2021 »). De plus, la règle 3 de la requête interdit de deviner. chat-small n’a donc pas présenté un ancien taux comme le taux actuel. Il s’est abstenu. Pour corriger la récupération elle-même, attribuez à chaque section historique les dates de son titre comme dates d’entrée en vigueur. Filtrez ensuite par date, comme dans Recherche lexicale, dense et hybride. C’est une bonne prochaine étape pour votre version.

Avant de compter sur les pages, mesurez ce que le modèle sait sans elles. C’est l’examen à livre ouvert de Ce qu’est la génération augmentée par récupération, mais sans le livre. Le projet peut poser chaque question à chat-small sans passages :

Fenêtre de terminal
python -m guidance_assistant ask Q33 --closed-book
Sortie
Question: My baby is due on 1 July 2026 and I joined my employer 10 weeks ago. Do I need to give 15 weeks' notice for paternity leave?
The model alone, without the guidance:
No—you must notify your employer **by the end of the 15th week before the expected week of childbirth**; it is not 15 weeks’ notice counted from when you joined. For a due date of 1 July 2026, that deadline was around 21 March 2026, so if you missed it, tell your employer as soon as possible. Since paternity leave became a day-one right for babies due from 6 April 2026, your short service should not prevent leave, though statutory paternity pay has separate eligibility rules.
gpt-6-luna-2026-09-22 | 75 tokens in, 1152 out | 7.874 s

Cette réponse est assurée et fausse. La page contient une règle pour ce cas précis. Pour un bébé attendu avant le 25 juillet 2026, une personne ayant moins de 26 semaines de service n’a pas à donner un préavis de 15 semaines. Le modèle dit que la date limite « était autour du 21 mars 2026 » et conseille à la personne de se dépêcher.

Sur l’ensemble des questions, le modèle seul a mieux réussi que prévu : selon les corrigés, 31 des 36 questions couvertes ont reçu une bonne réponse. Nous avons lu les 5 réponses signalées comme incorrectes. Trois disent la bonne chose autrement, une omet la principale limite (Q15), et une est fausse (Q33). L’exactitude n’est donc pas la raison pour laquelle Joy a besoin des pages. Voici les raisons :

  • Aucune source ni date. Aucune réponse ne peut être vérifiée en ouvrant une page.
  • Il répond à ce qu’il ne peut pas savoir à partir de GOV.UK. Sur les 8 questions non couvertes, il a répondu à 7 à partir de ses propres connaissances. Pour l’Irlande du Nord, il a dit « 123,25 £ par semaine pour 2026/27 ». Pour l’entente de règlement, il a donné un conseil personnel : « Ne l’acceptez pas et ne la signez pas avant… ». Une partie peut être vraie, mais rien ne vient des pages d’orientation, et rien ne l’indique.
  • Les réponses fausses et justes se ressemblent. La réponse à Q33 a le même ton assuré que les 31 bonnes réponses.

Comme le cours l’a montré dans Ce qu’est la génération augmentée par récupération, les pages apportent des preuves et des dates. Elles ne font pas que réduire les inventions.

Le pipeline de réponse est la fonction ask() du cours. Il manque ce que la personne voit. Le modèle donne seulement des affirmations et des identifiants de passages. L’application écrit les sources, en cherchant chaque identifiant cité dans l’index. Une ligne de source correspond donc toujours à une vraie page, avec son adresse et ses dates, même si le modèle se trompe. L’avis et l’attribution exigée par la licence sont des textes fixes, sous chaque réponse.

Créez guidance_assistant/sources.py :

guidance_assistant/sources.py
"""Show an answer the way a person should see it: every claim numbered, every source with its address
and its dates, and what the assistant is not.
The application writes the sources and the notice, not the model: the model gives only claims and
passage IDs, and this code looks up each ID in the index. So a source line is always a real page,
with the date we fetched it, even when the model is wrong.
"""
from .snapshot import ATTRIBUTION
NOTICE = ("This is what GOV.UK guidance says, not legal advice. This assistant is not a government service. "
"Pages fetched on {fetched}: check the page for changes.")
def sources_for(answer, store) -> list:
"""The cited chunks, in the order of first citation, without repeats."""
chunks = [store.chunk(cid) for cid in answer.cited()]
return [c for c in chunks if c is not None]
def render(answer, checks: list, store) -> str:
if not answer.answerable:
lines = [f"Answer: {answer.answer}", "", "The pages in the snapshot do not answer this question.",
"Ask the Acas helpline or an adviser."]
return "\n".join(lines + ["", NOTICE.format(fetched=_fetched(store)), ATTRIBUTION])
sources = sources_for(answer, store)
number = {c.chunk_id: i for i, c in enumerate(sources, 1)}
lines = [f"Answer: {answer.answer}", ""]
for claim, check in zip(answer.claims, checks):
refs = "".join(f"[{number[i]}]" for i in claim.chunk_ids if i in number) or "[no source]"
status = "" if check.ok else " (check: " + "; ".join(check.problems) + ")"
lines.append(f"- {claim.text} {refs}{status}")
lines += ["", "Sources:"]
for i, c in enumerate(sources, 1):
section = f', section "{c.section}"' if c.section else ""
lines.append(f"[{i}] {c.title}{section}")
lines.append(f" {c.url}")
lines.append(f" fetched {c.retrieved_at}; GOV.UK last updated {c.updated_at}")
return "\n".join(lines + ["", NOTICE.format(fetched=_fetched(store)), ATTRIBUTION])
def _fetched(store) -> str:
dates = sorted({c.retrieved_at for c in store.chunks()})
return dates[-1] if dates else "?"

Exécutez les tests, puis posez une question enregistrée :

Fenêtre de terminal
python -m pytest
python -m guidance_assistant ask Q14
Output (recorded answer, chat-small)
============================== 20 passed in 0.19s ==============================
Question: I have worked for my employer for 18 months and was just dismissed. Can I claim unfair dismissal?
Passages: 59652cdb4e72, f4443fade2e6, f44be5705d22, 407a19ade544, 023fd2596ef3 (1100 tokens, estimated)
Answer: The guidance says you must be classed as an employee and normally have 2 years’ service; with 18 months, you would not meet that usual qualifying period. In Northern Ireland, the usual period is 1 year, so 18 months meets the service-length requirement; there is no qualifying period if you were dismissed since 25 June 2013 because of your political opinions or affiliation. Any unfair dismissal claim must be made to a tribunal within 6 months of dismissal.
- You must be classed as an employee and normally have 2 years’ service to qualify, so 18 months does not meet the usual qualifying period. [1]
- In Northern Ireland the usual qualifying period is 1 year, and there is no qualifying period for a dismissal since 25 June 2013 because of political opinions or affiliation. [1]
- An unfair dismissal claim must be made to a tribunal within 6 months of dismissal. [1]
Sources:
[1] Dismissal: your rights: What to do if you're dismissed, section "Qualifying period to claim unfair dismissal"
https://www.gov.uk/dismissal/what-to-do-if-youre-dismissed#qualifying-period-to-claim-unfair-dismissal
fetched 2026-10-09; GOV.UK last updated 2026-10-05
This is what GOV.UK guidance says, not legal advice. This assistant is not a government service. Pages fetched on 2026-10-09: check the page for changes.
Contains public sector information licensed under the Open Government Licence v3.0.
gpt-6-luna-2026-09-22 | 1413 tokens in, 570 out | 4.761 s

Ce que cela nous apprend. La réponse conserve les conditions : statut d’employé, « normalement » 2 ans, l’exception et la limite de 6 mois. Elle dit ce que les pages d’orientation disent, plutôt que « vous ne pouvez pas déposer de demande ». C’est ce qu’exige la règle 5 de la requête. Toutes les affirmations ont passé les vérifications, et la personne qui conseille peut ouvrir la section avec la personne concernée.

Fenêtre de terminal
python -m guidance_assistant ask Q35
Sortie (extrait)
- Almost all people classed as workers are entitled to 5.6 weeks’ paid holiday a year. [1]
- At 4 working days a week, 5.6 weeks is 22.4 working days. [1][2] (check: number_not_found:22.4)

La réponse est juste : 4 × 5,6 = 22,4. Mais 22,4 n’est pas écrit sur la page, donc la vérification des nombres le signale. C’est la limite présentée dans Répondre et citer. Le code peut vérifier qu’un nombre figure dans le passage, mais pas qu’un calcul est juste. Sur les 44 réponses, les vérifications ont signalé 4 affirmations sur 70, et toutes les 4 sont justes. Deux sont des calculs (Q35, Q36), et deux emploient d’autres mots que le passage (Q25, Q34). Aucune réponse n’a cité un identifiant inexistant ou absent de ses passages.

Fenêtre de terminal
python -m guidance_assistant ask Q43
Sortie (extrait)
Answer: The guidance does not say what rate of Statutory Sick Pay applies in Northern Ireland.
The pages in the snapshot do not answer this question.
Ask the Acas helpline or an adviser.

Les passages sur les indemnités de maladie, avec 123,25 £, figuraient dans le contexte. Mais aucun passage ne dit qu’ils s’appliquent à l’Irlande du Nord. C’est l’abstention de Traiter les preuves difficiles; le modèle seul avait donné un montant.

Erreur courante

La ligne de source dit une chose, la page en dit une autre

Symptôme. Une personne qui conseille ouvre une adresse citée, et la section dit autre chose que l’affirmation. Ou l’adresse mène à une page GOV.UK qui ne figure pas du tout dans l’instantané.

Cause. Le modèle a écrit les lignes de source (la requête lui demandait d’« inclure l’URL »). Un modèle peut écrire une adresse qui semble réelle pour une page qu’il n’a jamais vue.

Correction. Laissez le modèle donner seulement les identifiants des passages. Cherchez chaque identifiant dans l’index, et rejetez ceux qui ne figurent pas dans le contexte. Écrivez le titre, l’adresse et les dates à partir de l’index, comme le fait sources.py. La page affichée est alors toujours celle d’où vient le passage.

Toutes les exécutions ci-dessous utilisent les mêmes 44 questions, le même instantané, la requête answer_v1 et les 5 premiers passages. Le simulateur rejoue les réponses enregistrées sur Azure. D’abord, la question principale du cours : une meilleure récupération améliore-t-elle les réponses?

Fenêtre de terminal
python -m guidance_assistant eval --answers --method rerank
python -m guidance_assistant eval --answers --method bm25
Sortie
chat-small, rerank, top 5, answer_v1: 44 questions
answerable, correct: 34/36
answerable, abstained: 2/36
not covered, abstained: 8/8
shown but wrong: 0 (none)
claims flagged by the checks: 4 of 70
tokens in / out (median): 1235 / 206
latency median / p95: 2.26 / 5.04 s (recorded)
cost per question (median): US$0.00023
chat-small, bm25, top 5, answer_v1: 44 questions
answerable, correct: 28/36
answerable, abstained: 8/36
not covered, abstained: 7/8
shown but wrong: 1 (Q40)
claims flagged by the checks: 3 of 59
tokens in / out (median): 1260 / 182
latency median / p95: 2.12 / 4.02 s (recorded)
cost per question (median): US$0.00022

La mesure la plus importante pour Joy est « affichée mais fausse » : une réponse qui n’est ni une abstention ni une bonne réponse. Une personne qui conseille la lirait comme si elle venait des pages d’orientation. Avec le reclassement, cette mesure est à zéro. Une meilleure récupération a donné 6 bonnes réponses de plus (28 → 34), avec le même modèle et le même coût. Avec BM25, le modèle s’est abstenu sur 8 questions couvertes. Sept fois, le bon passage n’avait pas été trouvé; une fois (Q32), il l’avait été. La seule réponse BM25 affichée mais fausse, Q40, est peu grave. La question demandait ce que verse le régime de maternité propre à un employeur. La réponse donnait la règle selon laquelle un régime ne peut pas verser moins que le montant légal. Elle aurait dû dire que les pages d’orientation ne le précisent pas.

Avec le reclassement, les deux échecs sont des échecs de récupération. Le passage pertinent ne figurait pas parmi les 5 premiers, et le modèle s’est abstenu. Q27 correspond aux quatre tableaux de la partie 3. Pour Q19 (indemnités de maternité après 3 mois dans un emploi), la section sur l’admissibilité n’a pas été trouvée. C’est l’analyse par étape de Trouver où ça échoue : aucune réponse fausse ne venait du modèle.

Ensuite, le modèle. Les réponses de chat-strong ont été enregistrées sur un échantillon : les 22 questions à numéro pair, qui incluent tous les types. Comparez-le à chat-small sur les mêmes 22 questions :

Fenêtre de terminal
python -m guidance_assistant eval --answers --ids Q02,Q04,Q06,Q08,Q10,Q12,Q14,Q16,Q18,Q20,Q22,Q24,Q26,Q28,Q30,Q32,Q34,Q36,Q38,Q40,Q42,Q44
python -m guidance_assistant eval --answers --ids Q02,Q04,Q06,Q08,Q10,Q12,Q14,Q16,Q18,Q20,Q22,Q24,Q26,Q28,Q30,Q32,Q34,Q36,Q38,Q40,Q42,Q44 --model chat-strong
Sur les 22 questions à numéro pair chat-small chat-strong
Couvertes, bonnes réponses 18/18 18/18
Non couvertes, abstentions 4/4 4/4
Affichées mais fausses 0 0
Affirmations (signalées) 37 (2) 51 (3)
Latence, médiane / p95 (enregistrée) 2,35 / 4,28 s 3,25 / 10,37 s
Coût par question (médiane) 0,00023 $ US 0,00417 $ US
Coût pour 1 000 questions environ 0,25 $ US environ 4,65 $ US

Ce que cela nous apprend. Sur cet échantillon, le modèle plus puissant n’est pas plus juste. Il écrit plus d’affirmations (51 contre 37), est plus lent et coûte environ 18 fois plus par question. Pour quelques centaines de questions par mois, les deux coûtent très peu. Sans différence dans les mesures, chat-small est le choix à retenir. Un échantillon de 22 questions ne peut pas révéler une petite différence. Enregistrez donc les versions et mesurez de nouveau plus tard, comme dans La passation opérationnelle.

Avec le modèle de reclassement (environ 0,34 seconde sur un processeur) et l’appel au modèle (environ 2,3 secondes), l’attente est d’environ 3 secondes.

La conception finale : les 5 premiers passages après reclassement, chat-small, la requête answer_v1 et les sources écrites par l’application. Voici une question du début à la fin. Elle contient une date, et le modèle seul y avait mal répondu :

Fenêtre de terminal
python -m guidance_assistant ask Q33
Output (recorded answer, chat-small)
Question: My baby is due on 1 July 2026 and I joined my employer 10 weeks ago. Do I need to give 15 weeks' notice for paternity leave?
Passages: 96d822f2d09f, dbdb4b6565f7, 1d938f602c0b, e30d70b6e089, 6f79065eed0a (1075 tokens, estimated)
Answer: Your baby’s due date is before 25 July 2026. You do not need to give 15 weeks’ notice if you’ll have been employed for less than 26 weeks up to any day in the qualifying week, which is the 15th week before the baby is due; you’ll still need to give 28 days’ notice of when you want leave to start and whether you want 1 or 2 weeks.
- A baby due on 1 July 2026 is due before 25 July 2026. [1]
- For a baby due before 25 July 2026, 15 weeks’ notice is not required if the employee will have been employed for less than 26 weeks up to any day in the qualifying week. The qualifying week is the 15th week before the baby is due. [1]
- The employee must still give 28 days’ notice of when they want leave to start and whether they want 1 or 2 weeks. [1]
Sources:
[1] Paternity pay and leave: How to claim, section "If your baby is due before 25 July 2026"
https://www.gov.uk/paternity-pay-leave/how-to-claim#if-your-baby-is-due-before-25-july-2026
fetched 2026-10-09; GOV.UK last updated 2026-10-05
This is what GOV.UK guidance says, not legal advice. This assistant is not a government service. Pages fetched on 2026-10-09: check the page for changes.
Contains public sector information licensed under the Open Government Licence v3.0.
gpt-6-luna-2026-09-22 | 1496 tokens in, 649 out | 5.048 s

La recherche a trouvé la seule section écrite pour ce cas. La réponse applique la date de la question à celle de la page, conserve la condition et cite la section.

Pour poser votre propre question, vous avez besoin d’un modèle actif. Le simulateur connaît seulement les questions enregistrées et le précise : « Aucun enregistrement pour cette requête ». Copiez .env.example vers .env. Choisissez ensuite un modèle local gratuit avec Ollama, ou votre propre clé, comme dans le cours. Vos réponses différeront des réponses enregistrées; les vérifications et les sources fonctionnent de la même façon.

  • Les pages apportent des preuves et des dates plus que de l’exactitude. Modèle seul : 31 sur 36 selon les corrigés, aucune source, et 7 questions non couvertes sur 8 ont quand même reçu une réponse. Avec les pages : 34 sur 36, 8 abstentions sur 8, aucune réponse affichée mais fausse.
  • La récupération comptait plus que le modèle : +6 bonnes réponses de BM25 au reclassement; aucune de chat-small à chat-strong, pour un coût 18 fois supérieur.
  • Les deux échecs étaient des échecs de récupération, et les deux abstentions étaient sûres.
  • Les pages contiennent leur propre historique, et la date de changement majeur de GOV.UK remontait à plusieurs années sur des pages affichant les taux de 2026.
  • Limites. 44 questions écrites par une seule personne; 36 avec des passages, donc une question fait varier une mesure d’environ 0,03. Un seul instantané d’un seul sujet. Les réponses ont été enregistrées une fois; une autre exécution du même modèle peut différer. Les vérifications automatiques ne détectent pas tout, donc nous avons lu chaque réponse.
  • Pas de conseils juridiques. GOV.UK dit lui-même qu’il ne publie pas de conseils. L’assistant dit ce que les pages d’orientation disent, pas ce qu’une personne devrait faire (règle 5 de la requête). L’avis le précise sous chaque réponse. Une personne ayant un litige réel a besoin de conseils, d’Acas ou d’un avocat. Les délais comptent : les pages donnent des limites de 6 mois pour plusieurs demandes au tribunal.
  • Pas un service officiel. La licence ne permet pas de suggérer un statut officiel. GOV.UK exige une autorisation avant que quiconque affirme avoir son approbation. N’utilisez pas le nom, la couronne ou le style visuel de GOV.UK dans le nom ou les écrans de l’assistant. L’avis dit : « Cet assistant n’est pas un service gouvernemental. »
  • Affichez la source et la date de chaque page. Les pages d’orientation changent au moins chaque avril. Chaque réponse affiche la date de récupération et la dernière mise à jour de GOV.UK, et invite à vérifier la page. Actualisez l’instantané, puis exécutez les tests et l’évaluation avant de remplacer l’ancien.
  • Dites où cela ne s’applique pas. Les pages concernent l’Angleterre, l’Écosse et le pays de Galles. Les questions d’Irlande du Nord nécessitent nidirect et la Labour Relations Agency. L’assistant s’abstient, et la personne qui conseille devrait savoir pourquoi.
  • Données personnelles. Les personnes qui conseillent saisiront des situations réelles : noms, employeurs, santé. Gardez les questions hors des journaux (logs). Ne les envoyez pas à un modèle hébergé sans consentement et sans contrat couvrant cet usage. Seul l’appel au modèle quitte l’ordinateur.
  • Attribution. Conservez la ligne d’attribution exigée par la licence partout où vous affichez ou publiez le texte des pages, y compris dans votre dépôt.

Ce projet montre ce que les employeurs recherchent dans un travail de RAG. Il comprend une source de données sous licence, la provenance, une récupération mesurée, des citations vérifiées et l’abstention. Il offre aussi une comparaison honnête incluant le coût.

Quoi mettre dans le dépôt :

guidance-assistant/
README.md what it is, what it is not, how to run and check it, the results
DATASET.md source, licence, attribution, fetch date, checksums, limits
LICENSE MIT, for the code
guidance/ the snapshot (OGL v3.0, with the attribution line in README and DATASET.md)
guidance_assistant/ the code, including parse.py, chunk.py and sources.py
questions/ the 44 questions with keys and passages
recordings/ the recorded answers, so that anyone can replay them
scripts/ check_snapshot.py, fetch_snapshot.py, build_snapshot.py
tests/ pytest tests (20, no model download needed)

Un modèle de README :

README.md (outline)
# guidance-assistant
## What it is
A cited Q&A assistant over 137 GOV.UK pages on rights at work (snapshot of 2026-10-09),
for volunteer advisers. Not legal advice; not a government service.
## Results (44 fixed questions, recorded 2026-10-09)
- Search: rerank hit@5 0.944, MRR 0.872 (BM25 0.778, 0.549).
- Answers, chat-small + rerank: 34/36 right, 8/8 abstained, 0 shown but wrong.
- The model alone: 31/36 by the keys, no sources, answered 7 of 8 uncovered questions.
- chat-strong: same scores on a 22-question sample, about 18 times the cost.
## Design choices
- Official Content API, one fetch, SHA-256 checked snapshot; OGL v3.0 attribution.
- Chunks keep the GOV.UK address with #anchor and the dates.
- The model gives passage IDs; the application writes the sources.
## Limits
(one snapshot, one topic, one author's questions, England, Scotland and Wales only)

Comment le décrire en entrevue, en deux phrases : « J’ai créé un assistant avec citations à partir d’un instantané vérifié de 137 pages GOV.UK; le code vérifie chaque citation, et l’application écrit chaque source avec sa date. Sur 44 questions fixes, il a répondu correctement à 34 des 36 questions couvertes et s’est abstenu sur les 8 autres. »

Allez plus loin : chacune de ces pistes est une bonne prochaine étape, avec un changement principal.

  • Des dates sur les sections. Convertissez les dates des titres, comme « Taux entre le 1er avril 2024 et le 31 mars 2026 », en dates d’entrée en vigueur. Filtrez selon la date de la question, puis mesurez de nouveau Q27 et les questions contenant une date.
  • Une actualisation. Récupérez les mêmes chemins dans quelques mois. Comparez les sommes de contrôle, listez les pages modifiées et exécutez l’évaluation sur les deux instantanés. Quelles réponses enregistrées sont maintenant périmées?
  • Une deuxième nation. Ajoutez les pages d’Irlande du Nord de nidirect (vérifiez d’abord leur licence) avec une étiquette « nation ». Filtrez selon cette étiquette, comme le cours filtrait selon l’accès.
  • Un modèle actif. Exécutez les 44 questions avec un modèle local dans Ollama. Comparez la mesure « affichée mais fausse » à celle des modèles enregistrés.

Vérifiez votre compréhension

Un collègue dit : « Le modèle seul a donné 31 bonnes réponses sur 36. Les pages ajoutent seulement 3 bonnes réponses. La recherche ne vaut donc pas le travail nécessaire. » Qu'oublie-t-il? Utilisez les chiffres de cette étude de cas.

Afficher la réponse

Il compte seulement les bonnes réponses. Sans les pages, aucune réponse n’a de source ni de date. Un conseiller ne peut donc pas distinguer les 31 bonnes réponses de la mauvaise; Q33 semblait tout aussi certaine. Pour les 8 questions non couvertes par les pages d’orientation, le modèle seul a répondu à 7 selon ses propres connaissances. Deux exemples : un taux d’indemnité de maladie pour Northern Ireland et des conseils personnels sur une entente de règlement. Avec les pages et le reclassement, il s’est abstenu pour les 8 questions, sans afficher de mauvaise réponse. Il a aussi lié chaque affirmation à une section datée. Pour un centre de conseil, une réponse vérifiable et un honnête « les pages d’orientation ne le précisent pas » comptent davantage que trois bonnes réponses supplémentaires.

Vérifiez votre compréhension

Le tableau actuel du salaire minimum arrivait au 6e rang, après deux anciens tableaux de la même page. Pourquoi aucun mauvais taux n'a-t-il atteint le conseiller, et quel changement corrigerait la recherche elle-même?

Afficher la réponse

Chaque passage parvenait au modèle avec son titre de section, par exemple « Taux avant le 1er avril 2021 ». La requête interdit de deviner. Sans tableau actuel, chat-small s’est abstenu plutôt que de citer un ancien taux. Pour corriger la recherche, attribuez à chaque section historique les dates de son titre comme dates d’entrée en vigueur. Filtrez ensuite selon la date de la question avant de sélectionner les 5 premiers résultats, comme pour les versions des politiques dans le cours. Mesurez ensuite de nouveau les résultats des questions datées et des questions sur les tableaux. Les questions datées doivent utiliser leur propre date.

Vérifiez votre compréhension

Pourquoi sources.py cherche-t-il chaque identifiant cité dans l'index au lieu de demander au modèle d'écrire l'adresse de la page?

Afficher la réponse

Un modèle peut écrire une adresse qui semble réelle, mais mène à une autre page ou à une page absente de l’instantané. Les identifiants sont vérifiables : les contrôles des citations rejettent tout identifiant inexistant ou absent du contexte. L’application écrit le titre, l’adresse et les dates à partir de l’index. Chaque ligne de source correspond donc à la page d’origine du passage, avec ses dates réelles. Le modèle reste responsable des affirmations. Les contrôles signalent celles dont les nombres ou les mots ne figurent pas dans le passage.

Récapitulatif

  • Vérifiez la licence des pages exactes et ses conditions : attribution et aucune suggestion de statut officiel.
  • Récupérez les pages une seule fois avec l’API officielle. Conservez un instantané avec les sommes de contrôle et la date de récupération de chaque page.
  • Conservez le chemin vers la source : blocs avec section et ancre, segments avec identifiants et adresses stables.
  • Les pages contiennent leur propre historique : les titres et les dates doivent accompagner chaque passage.
  • Le modèle fournit les affirmations et les identifiants; l’application écrit les sources, les dates et l’avis.
  • Une meilleure récupération a donné 6 bonnes réponses supplémentaires; le modèle puissant n’en a ajouté aucune, pour un coût 18 fois supérieur.

Vous avez créé un assistant dont un bénévole peut vérifier les réponses page par page. La prochaine étude de cas porte sur la recherche dans une documentation technique, où les noms exacts et les reformulations comptent.

Leçon suivante : Chercher dans une documentation technique