Видалення персональних даних та даних типу PCI з запитів, контексту та відповідей у графі RAG
Видалення персональних даних та даних типу PCI у трьох місцях у графі RAG: перед пошуком, перед запитом та перед інтерфейсом користувача, із золотими тестами для випадків чистих даних, даних карток та номерів соціального страхування.
Захист запиту та відповіді — маскування персональних даних та даних карток PCI
Пайплайни RAG, які індексують тарифні таблиці, документи про рахунки чи записи дзвінків підтримки, з часом можуть містити номери соціального страхування, номери карток та імена як у запиті користувача, так і в отриманому контексті. Механізми захисту мають діяти в обох напрямках: маскувати конфіденційну інформацію у вхідному запиті, очищувати отримані дані перед їх використанням у запиті до моделі та фільтрувати відповідь моделі перед її відображенням у користувацькому інтерфейсі.
Хороші новини
Azure AI Language (та пов’язані з ним функції обробки персональних даних) може виявляти поширені типи ентитетів з достатньою точністю, що слугує першим рівнем захисту. Поєднання цього з політиками обробки даних у коді додатку дозволяє ефективно керувати більшістю демонстраційних та багатьма продакшн-сценаріями, особливо коли додатковий рівень захисту забезпечують механізми Foundry у порталі.
Зміни в коді порівняно з попередньою реалізацією
Розширте граф із трьома контрольними точками:
- Захист запиту — виявляти та маскувати персональні/кредитні дані в тексті користувача перед вбудовуванням/пошуком
- Захист контексту — маскувати ентитети всередині отриманих уривків
- Захист відповіді — остаточна перевірка сформованої відповіді
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..
Зберігайте структуровані журнали того, що було маскувано (тип ентитета + офсет), але ніколи не зберігайте самі секретні дані, для аудиту.
Виконання: чи справді це працює
Простий запит без даних PII/PCI
Очікуйте, що текст пройде через усі три захисні механізми без змін та отримається звичайна відповідь на пошук.
Запит із фальшивими даними PCI
Встановіть тестовий PAN; переконайтеся, що механізм захисту запитів маскує його перед пошуком, а журнали відображають виявлення класу CreditCard без зберігання цифр.
Запит із фальшивими персональними даними (SSN)
Переконайтеся, що значення у форматі SSN також маскуються аналогічним чином.
Вилучення імені власника рахунку з отриманого контексту
Навіть якщо запит користувача є чистим, отриманий текст PDF може містити імена. Механізм захисту контексту повинен маскувати їх перед тим, як модель чату їх побачить; механізм захисту відповідей запобігає повторному генеруванню імена моделлю.
Оперативні примітки
- Тримайте набір „золотих“ запитів із чистими даними, даними типу PCI та PII у різних варіантах написання
- Віддавайте перевагу маскуванню перед видаленням, щоб структура речень залишалася читабельною
- Ніколи не записуйте сирі дані PAN/SSN „для дебаггінгу“
- Поєднуйте це з правилами порталу Foundry для категорій, пов’язаних із втечею з системи чи шкодою, які не контролюються Python-шаром
Заключення
Механізми захисту PII/PCI не є додатковим елементом для демонстрації RAG — вони є частиною умов отримання інформації. Кожен запит, контекст та відповідь потребують чітких контрольних точок, тестів та журналів, які підтверджують, що інформація була засекречена без збереження секретних даних.
Під час інтеграції необхідно переривати роботу у разі збоїв детектора для маршрутів високого ризику (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів нижчого ризику переривати роботу слід лише за наявності значних показників та досягнення порогу. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значного показника та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значних показників та порогу виклику. Документуйте, який режим використовує кожне середовище.
Під час інтеграції у разі збою детектора на маршрутах високого ризику слід переходити на режим закритих помилок (повертати безпечну помилку замість трансляції неперевіреного контексту). Для внутрішніх інструментів з нижчим ризиком слід переходити на режим відкритих помилок лише за наявності значних показників та порогу виклику. Документуйте, який режим використовує кожне середовище.