Aller au contenu

Intégration LangChain

Vous voulez un agent LangGraph qui fonctionne, où le LLM ne voit jamais que des jetons, où un outil reçoit quand même les vraies valeurs dont il a besoin, et où la détection tourne sur un vrai modèle NER plutôt que sur une liste de valeurs figée. L'assemblage ci-dessous va de bout en bout. Il réunit un détecteur GLiNER2, un ThreadAnonymizationPipeline, PIIAnonymizationMiddleware, un system prompt qui apprend au modèle à traiter les jetons comme des données, et un outil qui cherche une personne par son nom.

Pour la version minimale avec un détecteur bouchon, commencez par le tutoriel Middleware LangChain. La suite en reprend la forme avec un vrai modèle et un system prompt.

1. Construire le pipeline sur un détecteur GLiNER2

Gliner2Detector enrobe un modèle GLiNER2. Passez l'identifiant du modèle sous forme de chaîne et il se charge à la construction. Passez labels pour lui indiquer quels types d'entités interroger. L'anonymiseur par défaut utilise LabelCounterPlaceholderFactory, qui émet le jeton délimité <<PERSON:1>> que le middleware sait retrouver.

from piighost.components.detector.ner import Gliner2Detector
from piighost.pipeline import ThreadAnonymizationPipeline

detector = Gliner2Detector(
    model="fastino/gliner2-multi-v1",
    labels=["PERSON", "LOCATION"],
    threshold=0.5,
)
pipeline = ThreadAnonymizationPipeline(detector)

2. Déclarer un outil qui a besoin de la vraie valeur

Un outil qui cherche une personne par son nom a besoin de Patrick, pas de <<PERSON:1>>. Écrivez-le contre les vraies valeurs. Sous ToolCallStrategy.FULL, le middleware restaure l'argument avant l'appel, puis dé-identifie le résultat.

from langchain.tools import tool


@tool
def lookup_city(person: str) -> str:
    """Return the city where a person lives."""
    directory = {"Patrick": "Paris"}
    return directory.get(person, "unknown")

3. Dire au modèle que les jetons sont des données

Le modèle raisonne sur <<PERSON:1>> au lieu d'un nom. Un court system prompt l'empêche de commenter le jeton ou de refuser de le passer à un outil.

SYSTEM_PROMPT = """\
You are a helpful assistant. Some inputs contain placeholders like <<PERSON:1>> \
that stand in for real values withheld for privacy.

Treat each placeholder as if it were the real value. Never comment on its \
format, never say it is a token, and pass it to tools unchanged as an argument. \
If the user asks about the content of a placeholder, say the data is withheld \
and you cannot reveal it.
"""

4. Enrober le pipeline et créer l'agent

PIIAnonymizationMiddleware prend le pipeline. tool_strategy=ToolCallStrategy.FULL restaure les arguments de l'outil à l'entrée et dé-identifie le résultat de l'outil à la sortie. L'outil travaille ainsi sur les vraies valeurs, pendant que le modèle continue de ne voir que des jetons.

from langchain.agents import create_agent

from piighost.integrations.langchain import (
    PIIAnonymizationMiddleware,
    ToolCallStrategy,
)

agent = create_agent(
    model="openai:gpt-5.6-terra",
    system_prompt=SYSTEM_PROMPT,
    tools=[lookup_city],
    middleware=[
        PIIAnonymizationMiddleware(
            pipeline=pipeline,
            tool_strategy=ToolCallStrategy.FULL,
        )
    ],
)

5. Exécuter un tour

Le thread_id va dans la config LangGraph, sous configurable. Le middleware l'y lit et rattache chaque jeton à cette conversation.

import asyncio


async def main() -> None:
    result = await agent.ainvoke(
        {"messages": [{"role": "user", "content": "Where does Patrick live?"}]},
        config={"configurable": {"thread_id": "thread-42"}},
    )
    print(result["messages"][-1].content)


asyncio.run(main())

La réponse est restaurée pour l'affichage, donc elle se lit avec les vraies valeurs. Sa formulation dépend du modèle, par exemple :

Patrick lives in Paris.

Qui voit quoi

GLiNER2 marque Patrick comme PERSON dans le message entrant. À partir de là, chaque frontière du tour remplace dans un seul sens, soit la valeur par son jeton, soit le jeton par sa valeur.

  • abefore_model fait passer le message dans pipeline.anonymize, si bien que le LLM reçoit Where does <<PERSON:1>> live?.
  • Le modèle appelle lookup_city(person="<<PERSON:1>>"). Sous ToolCallStrategy.FULL, awrap_tool_call restaure l'argument en Patrick avant d'exécuter l'outil, puis dé-identifie à nouveau le résultat texte de l'outil.
  • aafter_model restaure la réponse pour l'utilisateur.

Le thread_id garde <<PERSON:1>> lié à Patrick à chaque étape.

Voir aussi

  • Pour choisir un autre comportement d'outil, INPUT seul, OUTPUT seul ou PASSTHROUGH, voir Stratégies d'appel d'outil.
  • Pour remplacer GLiNER2 par spaCy, un pack regex ou votre propre détecteur, voir Étendre piighost.
  • Pour exécuter le pipeline hors du processus contre un serveur partagé, voir Client distant.