首页 / 文章

在RAG图中对查询、上下文及答案中的PII和PCI信息进行脱敏处理

在RAG图中三个节点处对PII和PCI信息进行遮蔽:搜索前、提示词生成前以及用户界面显示前,并为完整信息、卡片信息及社会安全号码信息设置验证标准。

发布者:REACTAPP.TOP阅读约 8 分钟1908 词

来源: Adding PII/PCI Guardrails to the RAG Pipeline

保护查询与响应内容——个人信息与支付卡信息的遮蔽处理

那些用于索引费用明细、账户文档或支持记录的RAG流程,最终会在用户查询或检索到的上下文中出现社会安全号码、银行卡号以及姓名等信息。防护措施必须覆盖双向:在进入系统的查询中遮蔽敏感信息,在内容被用于提示词之前进行清洗,同时在模型输出结果到达用户界面之前对其进行净化。

好消息

Azure AI Language(及相关个人信息保护功能)能够以较高的准确率识别常见实体类型,可作为第一道防御屏障。再结合应用程序代码中的允许列表字段策略,即可应对大多数演示场景以及许多实际生产环境中的情况——尤其是当Portal Foundry的防护机制提供额外保障时。

与之前实现相比的代码变更

通过三个检查点扩展该流程:

  1. 查询防护——在嵌入或搜索之前检测/屏蔽用户文本中的个人身份信息/支付卡信息
  2. 上下文防护——屏蔽检索到的内容中的相关实体
  3. 答案防护——对生成的回复进行最终审核
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演示中的可选附加功能,而是检索流程不可或缺的一部分。查询、上下文及答案均需有明确的检查点、测试以及日志,以证明敏感信息已被遮蔽且未被保留。

    在集成过程中,对于高风险路径,若检测器出现故障应立即终止操作(返回安全错误信息,而非传输未经筛选的上下文)。对于风险较低的内部工具,则仅在指标异常达到阈值时才终止操作。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则只有在指标异常严重且达到触发阈值时才允许服务保持开启状态。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则只有在指标异常严重且达到触发阈值时才允许服务保持开启状态。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则只有在指标异常严重且达到触发阈值时才允许服务保持开启状态。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经过滤的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经筛选的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。

    在集成过程中,对于高风险路径,若检测器出现故障应立即关闭服务(返回安全的错误信息,而非传输未经筛选的上下文)。对于风险较低的内部工具,则仅在指标异常严重且达到触发阈值时才允许服务继续运行。需记录每个环境所使用的模式。