在RAG图中对查询、上下文及答案中的PII和PCI信息进行脱敏处理
在RAG图中三个节点处对PII和PCI信息进行遮蔽:搜索前、提示词生成前以及用户界面显示前,并为完整信息、卡片信息及社会安全号码信息设置验证标准。
保护查询与响应内容——个人信息与支付卡信息的遮蔽处理
那些用于索引费用明细、账户文档或支持记录的RAG流程,最终会在用户查询或检索到的上下文中出现社会安全号码、银行卡号以及姓名等信息。防护措施必须覆盖双向:在进入系统的查询中遮蔽敏感信息,在内容被用于提示词之前进行清洗,同时在模型输出结果到达用户界面之前对其进行净化。
好消息
Azure AI Language(及相关个人信息保护功能)能够以较高的准确率识别常见实体类型,可作为第一道防御屏障。再结合应用程序代码中的允许列表字段策略,即可应对大多数演示场景以及许多实际生产环境中的情况——尤其是当Portal 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..
为审计目的,需记录被屏蔽的内容的结构化信息(实体类型及起始位置),绝不能保存原始敏感数据。
实际运行效果:是否真的有效
不含个人身份信息/支付卡信息的简单查询
经过三个防护环节后文本应保持不变,同时返回正常的检索结果。
包含虚假支付卡信息的查询
植入测试用的PAN卡片;确认查询防护机制在搜索前已对其进行遮蔽,且日志中显示为CreditCard类检测结果而未存储具体数字。
包含虚假个人身份信息(社会安全号码)的查询
确认类似社会安全号码格式的值也得到了相应遮蔽处理。
从检索到的内容中提取账户持有人姓名
即便用户输入的内容是干净的,检索到的PDF文本仍可能包含姓名。上下文防护机制必须在聊天模型看到相关内容前将其遮蔽;回复防护机制则用于防止模型重新生成这些姓名。
操作注意事项
- 维护包含干净格式、PCI标准格式、个人身份信息以及混合格式的黄金提示词包
- 优先选择遮蔽处理而非删除,以保持句子结构可读性
- 绝不可为“调试”目的而记录原始的PAN卡片/社会安全号码
- 针对Python层无法处理的越狱或危害类场景,需结合Foundry门户的防护机制使用
总结
PII/PCI防护措施并非RAG演示中的可选附加功能,而是检索流程不可或缺的一部分。查询、上下文及答案均需有明确的检查点、测试以及日志,以证明敏感信息已被遮蔽且未被保留。
在集成过程中,对于高风险路径,若检测器出现故障应立即终止操作(返回安全错误信息,而非传输未经筛选的上下文)。对于风险较低的内部工具,则仅在指标异常达到阈值时才终止操作。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则只有在指标异常严重且达到触发阈值时才允许服务保持开启状态。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则只有在指标异常严重且达到触发阈值时才允许服务保持开启状态。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则只有在指标异常严重且达到触发阈值时才允许服务保持开启状态。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经筛选的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。
在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经筛选的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。