Édition des données PII et PCI dans la requête, le contexte et la réponse au sein d’un graphe RAG
Masquage des données PII et PCI en trois étapes dans un graphe RAG : avant la recherche, avant la demande et avant l’interface utilisateur, avec des tests de validation pour les cas de cartes bancaires, de numéros de sécurité sociale et de données propres.
Protéger à la fois la requête et la réponse — Masquage des données PII et PCI
Les pipelines RAG qui indexent les tarifs, les documents de compte ou les transcriptions d’assistance finiront par rencontrer des numéros de sécurité sociale, des PAN de cartes et des noms, que ce soit dans la requête de l’utilisateur ou dans le contexte récupéré. Des mesures de protection doivent s’appliquer des deux côtés : masquer les éléments sensibles dans la question reçue, nettoyer les fragments récupérés avant qu’ils ne soient intégrés à la demande, et purifier la réponse du modèle avant qu’elle n’atteigne l’interface utilisateur.
Bonnes nouvelles
Azure AI Language (ainsi que les fonctionnalités relatives aux données PII) peut détecter des types d’entités courants avec une précision suffisante pour constituer une première ligne de défense. En combinant cela avec des politiques de champ autorisés dans le code d’application, on couvre la plupart des cas de démonstration ainsi que de nombreux cas en production — surtout lorsque les mesures de protection de Foundry ajoutent une seconde couche de sécurité.
Changements de code par rapport à l’implémentation précédente
Étendez le graphe avec trois points de contrôle :
- Guard de requête — détecter/masquer les données PII/PCI dans le texte de l’utilisateur avant insertion/recherche
- Guard de contexte — masquer les entités présentes dans les extraits récupérés
- Guard de réponse — vérification finale de la réponse générée
mkdir AISearchGRManualVectorization && cd AISearchGRManualVectorization && \
uv init .
uv add langgraph langchain-core langchain-openai langchain-text-splitters \
azure-ai-documentintelligence azure-core \
langchain-azure-ai azure-search-documents \
azure-ai-textanalytics \
python-dotenv
# Add all files from git repo or clone repository
@dataclass
class RedactionResult:
text: str
pii_found: bool = False
pci_found: bool = False
categories: List[str] = field(default_factory=list)
error: Optional[str] = None
def redact_text(text: str) -> RedactionResult:
if not text or not text.strip():
return RedactionResult(text=text)
client = _get_client()
results = client.recognize_pii_entities([text], language="en")
result = results[0]
if result.is_error:
return RedactionResult(text=text, error=result.error.message)
categories = [e.category for e in result.entities]
return RedactionResult(
text=result.redacted_text,
pii_found=len(categories) > 0,
pci_found=any(c in PCI_CATEGORIES for c in categories),
categories=categories,
)
def redact_query_node(state: ChatFileSearchState) -> ChatFileSearchState:
if not state.get("query"):
return state
result = guardrails.redact_text(state["query"])
if result.pii_found:
kind = "PCI" if result.pci_found else "PII"
print(f"[guardrail] {kind} detected and redacted in query")
state["query"] = result.text
return state
def redact_response_node(state: ChatFileSearchState) -> ChatFileSearchState:
if not state.get("answer"):
return state
result = guardrails.redact_text(state["answer"])
if result.pii_found:
kind = "PCI" if result.pci_found else "PII"
print(f"[guardrail] {kind} detected and redacted in response")
state["answer"] = result.text
return state
def generate_node(state: ChatFileSearchState) -> ChatFileSearchState:
if not state.get("query"):
state["answer"] = None
return state
if not state["retrieved"]:
context = "No relevant content was found in the uploaded files."
else:
context = "\n\n---\n\n".join(
f"[{doc.metadata.get('file_name', 'unknown')}] {doc.page_content}"
for doc in state["retrieved"]
)
context = guardrails.redact_text(context).text # <-- checkpoint 2
prompt = (
"Answer the question using only the context below, which comes from "
"files the user uploaded to this chat. If the answer isn't in the "
"context, say you don't know - don't make anything up.\n\n"
"Format the answer in markdown: use a short bolded summary line "
"first, then bullet points or a table for any figures, dates, or "
"line items. Keep prose minimal - prefer structure over paragraphs.\n\n"
f"Context:\n{context}\n\n"
f"Question: {state['query']}"
)
response = llm.invoke(prompt)
state["answer"] = response.content
return state
graph.add_edge("ingest", "redact_query")
graph.add_edge("redact_query", "retrieve")
graph.add_edge("retrieve", "generate")
graph.add_edge("generate", "redact_response")
graph.add_edge("redact_response", END)
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ ┌─────┐
│ ingest ├─────►│ redact_query ├─────►│ retrieve ├─────►│ generate ├─────►│ redact_response ├─────►│ END │
└──────────┘ └──────────────┘ └──────────┘ └──────────┘ └─────────────────┘ └─────┘
uv run python -m aisearchgrmanualvectorization.main \
--session demo1 \
--query "What's the overdraft protection fee?"
Q: What's the overdraft protection fee?
A: **Overdraft protection transfer fee: $10.00 per occurrence**
- Overdraft protection transfers from a linked savings account are subject to a $10.00 fee per occurrence (as stated in the Overdraft Protection disclosure).
- If no linked savings account, insufficient funds fee: $34.00 per item (up to four items per day).
Sources:
- sample_bank_statement.pdf (chunk 7): #### Important Account Disclosures ##### Overdraft Protection If a transaction is presented that ...
- sample_bank_statement.pdf (chunk 8): ##### Interest Rate & Annual Percentage Yield This account earns interest at a variable rate, curre...
- sample_bank_statement.pdf (chunk 9): ##### Monthly Maintenance Fee A $12.00 monthly maintenance fee applies unless one of the following ...
- sample_bank_statement.pdf (chunk 3): <td>-$10.44</td> <td>$8,096.52</td> </tr> <tr> <td>02/07/2026</td> <td>Greenfield Grocers</td> <td>D...
uv run python -m aisearchgrmanualvectorization.main \
--session demo1 \
--query "My card number is 4111 1111 1111 1111, what's the late payment fee on my loan?"
[guardrail] PCI detected and redacted in query
Q: My card number is 4111 1111 1111 1111, what's the late payment fee on my loan?
A: **Short answer: Late payment fee is $35.00 or 5% of the payment, whichever is less.**
- Late Payment Fee: $35.00 or 5% of the payment amount, whichever is less
- Delinquent threshold: payment not received within 15 calendar days of its Scheduled Payment Date
- Default threshold: payment unpaid 30 calendar days past Scheduled Payment Date
Sources:
- sample_loan_agreement.pdf (chunk 7): ### 6. Late Payments and Default A payment not received within 15 calendar days of its Scheduled Pa...
- sample_loan_agreement.pdf (chunk 1): ## 2. Key Loan Terms <table> <tr> <th>Principal Amount</th> <th>$18,000.00</th> <th>Loan Term</th>...
- sample_loan_agreement.pdf (chunk 6): A complete 48-month amortization schedule is available from the Lender upon request. Figures above a...
- sample_bank_statement.pdf (chunk 9): ##### Monthly Maintenance Fee A $12.00 monthly maintenance fee applies unless one of the following ...
uv run python -m aisearchgrmanualvectorization.main \
--session demo1 \
--query "My SSN is 123-45-6789 - can you also tell me the monthly payment on the loan?"
[guardrail] PII detected and redacted in query
Q: My SSN is 123-45-6789 - can you also tell me the monthly payment on the loan?
A: **Monthly payment is $454.37**
- Monthly Payment: $454.37
Sources:
- sample_loan_agreement.pdf (chunk 1): ## 2. Key Loan Terms <table> <tr> <th>Principal Amount</th> <th>$18,000.00</th> <th>Loan Term</th>...
- sample_loan_agreement.pdf (chunk 3): "Scheduled Payment Date" means the 15th calendar day of each month during the Loan Term, or the next...
- sample_loan_agreement.pdf (chunk 6): A complete 48-month amortization schedule is available from the Lender upon request. Figures above a...
- sample_loan_agreement.pdf (chunk 4): <table> <tr> <th>Payment #</th> <th>Payment Amount</th> <th>Interest</th> <th>Principal</th> <th>Rem...
uv run python -m aisearchgrmanualvectorization.main \
--session demo1 \
--query "Who is the account holder on the bank statement?"
[guardrail] PII detected and redacted in query
Q: Who is the account holder on the bank statement?
A: **I don't know — the statement redacts that name.**
- The document shows "**************" in place of the name (redacted).
- No unredacted name or identifying information for that field appears in the provided context.
Sources:
- sample_bank_statement.pdf (chunk 0): # SAMPLE STATEMENT - SYNTHETIC TEST DATA - NOT A REAL FINANCIAL DOCUMENT # Meridian Trust Bank (fi...
- sample_bank_statement.pdf (chunk 6): <!-- PageBreak --> <table> <tr> <th>Date</th> <th>Description</th> <th>Type</th> <th>Amount</th> <...
- sample_bank_statement.pdf (chunk 10): ##### Error Resolution & Your Rights In case of errors or questions about electronic transfers, tel...
- sample_bank_statement.pdf (chunk 8): ##### Interest Rate & Annual Percentage Yield This account earns interest at a variable rate, curre..
Gérez des journaux structurés indiquant ce qui a été masqué (type d’entité + emplacement), jamais les données secrètes brutes, aux fins d’audit.
Exécution : fonctionne-t-il réellement ?
Une requête simple sans données PII/PCI
On s’attend à ce que le texte reste inchangé après les trois contrôles et à obtenir une réponse de recherche normale.
Requête contenant des données PCI fictives
Plantez un PAN de test ; assurez-vous que le mécanisme de protection des requêtes le masque avant la recherche, et que les journaux montrent une détection de type CreditCard sans en stocker les chiffres.
Requête contenant des informations personnelles falsifiées (SSN)
Vérifiez que les valeurs ayant la forme d’un SSN soient également masquées de la même manière.
Récupération du nom d’un titulaire de compte à partir du contexte obtenu
Même si la question de l’utilisateur est propre, le texte PDF récupéré peut contenir des noms. Le mécanisme de protection du contexte doit les masquer avant que le modèle de chat ne voie ce fragment ; le mécanisme de protection des réponses intercepte les tentatives du modèle de générer à nouveau ces noms.
Notes opérationnelles
- Gardez un ensemble de prompts parfaits comprenant des exemples propres, conformes aux normes PCI et PII, ainsi que des variantes en majuscules et minuscules
- Préférez le masquage à la suppression afin que la structure des phrases reste lisible
- N’ayez jamais recours à l’enregistrement de PAN ou de SSN bruts « pour le débogage »
- Combinez cela avec les règles de sécurité du portail Foundry pour gérer les catégories de jailbreak ou de dommages que la couche Python ne contrôle pas
Clôture
Les mesures de protection des PII/PCI ne sont pas une simple touche finale pour une démonstration RAG : elles font partie intégrante du contrat de récupération des informations. Chaque requête, contexte et réponse nécessitent un point de contrôle explicite, des tests et des journaux prouvant que la suppression a eu lieu sans conserver le secret.
Lors de l’intégration, en cas de panne du détecteur sur les parcours à haut risque, il faut générer une erreur de sécurité plutôt que de diffuser un contexte non filtré. Pour les outils internes à faible risque, la défaillance ne doit survenir qu’en cas de seuil critique atteint, accompagnée d’une alerte sonore. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, adoptez un mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, n’adoptez un mode d’échec ouvert qu’en présence d’une métrique significative et d’un seuil de notification. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, activez le mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, activez le mode d’échec ouvert uniquement en cas de valeur métrique élevée et lorsqu’un seuil de notification est atteint. Documentez le mode utilisé par chaque environnement.
Lors de l’intégration, en cas de panne des détecteurs sur les routes à haut risque, activez le mode d’échec fermé (retournez une erreur sécurisée plutôt que de diffuser du contexte non filtré). Pour les outils internes à faible risque, activez le mode d’échec ouvert uniquement en cas de valeur métrique élevée et lorsqu’un seuil de notification est atteint. Documentez le mode utilisé par chaque environnement.