LLMs  

Enterprise-Grade Privacy in GenAI: PII Anonymization & Differential Privacy in Multi-Agent RAG

As generative AI transitions from experimental pilots to mission-critical enterprise infrastructure, the intersection of utility and privacy has become the primary bottleneck. In regulated industries (Healthcare, Finance, Legal), deploying Large Language Models (LLMs) and Retrieval-Augmented Generation (RAG) pipelines without rigorous privacy guardrails is a compliance violation waiting to happen.

This article provides an end-to-end architectural guide and code implementation for building an Enterprise Multi-Agent LangGraph RAG Pipeline that integrates PII Anonymization and Differential Privacy (DP) across both the fine-tuning and retrieval phases.

1. The Real-World Use Case: "FinServe AI"

Imagine FinServe, a global retail bank. They are deploying an internal AI assistant for loan officers.

  • The Input: A loan officer asks, "What is the current underwriting status for John Doe? His SSN is 123-45-6789, and his account email is [email protected]."

  • The Risk: If this prompt is sent directly to an LLM or stored in a Vector Database, PII is exposed to model memorization, vector DB leakage, and potential prompt injection attacks.

  • The Requirement: The system must redact PII before it touches the LLM or Vector DB. Furthermore, the underlying LLM must be fine-tuned using Differential Privacy to ensure it cannot be reverse-engineered to leak training data, and the RAG embeddings must be noise-injected to prevent membership inference attacks.

2. Architecture Overview

We will use LangGraph to orchestrate a multi-agent pipeline. Unlike simple sequential chains, LangGraph allows us to maintain complex state, manage memory across conversational turns, and route tasks to specialized "Agent Nodes."

The Multi-Agent Pipeline

  • PII Redaction Agent: Intercepts user input, identifies PII using NLP/Regex, and replaces it with pseudonyms (e.g., <PERSON_1>, <SSN_1>). Stores the mapping in the Graph State.

  • DP-Safe Retriever Agent: Queries the Vector DB. To prevent embedding inversion attacks, it applies calibrated Gaussian noise to the retrieved document embeddings (Differential Privacy in RAG).

  • DP-Fine-Tuned Generator Agent: A model trained with DP-SGD (Differential Privacy Stochastic Gradient Descent) generates the response using the sanitized context.

  • PII Resolution Agent: Maps the pseudonyms back to the original PII for the final user-facing response, ensuring the LLM never "saw" the raw PII, but the user gets a natural answer.

3. Code Implementation

Prerequisites

pip install langgraph langchain-openai presidio-analyzer presidio-anonymizer opacus torch numpy faiss-cpu

Step 1: PII Anonymization Engine

We use Microsoft's Presidio for robust, context-aware PII detection.

from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
from presidio_anonymizer.entities import OperatorConfig

class PIIEngine:
    def __init__(self):
        self.analyzer = AnalyzerEngine()
        self.anonymizer = AnonymizerEngine()

    def redact(self, text: str) -> tuple[str, dict]:
        # Analyze PII
        results = self.analyzer.analyze(text=text, language='en')

        # Anonymize using custom placeholders
        anonymized_result = self.anonymizer.anonymize(
            text=text,
            analyzer_results=results,
            operators={
                "PERSON": OperatorConfig("replace", {"new_value": "<PERSON>"}),
                "EMAIL_ADDRESS": OperatorConfig("replace", {"new_value": "<EMAIL>"}),
                "US_SSN": OperatorConfig("replace", {"new_value": "<SSN>"}),
                "DEFAULT": OperatorConfig("replace", {"new_value": "<REDACTED>"})
            }
        )

        # Create a reverse mapping for the final output
        mapping = {
            "<PERSON>": next((e.text for e in results if e.entity_type == "PERSON"), ""),
            "<EMAIL>": next((e.text for e in results if e.entity_type == "EMAIL_ADDRESS"), ""),
            "<SSN>": next((e.text for e in results if e.entity_type == "US_SSN"), "")
        }

        return anonymized_result.text, mapping

    def resolve(self, text: str, mapping: dict) -> str:
        for placeholder, original in mapping.items():
            if original:
                text = text.replace(placeholder, original)
        return text

pii_engine = PIIEngine()

Step 2: Differential Privacy (DP) Mechanisms

DP must be applied in two places: Fine-Tuning (to protect the model weights) and RAG Retrieval (to protect the Vector DB).

A. DP Fine-Tuning (Opacus)

When fine-tuning your enterprise LLM (e.g., Llama-3-8B), you must use DP-SGD to bound the influence of any single training example.

import torch
from opacus import PrivacyEngine
from transformers import AutoModelForCausalLM, AutoTokenizer

def dp_fine_tune_step(model, dataloader, optimizer, epsilon=1.0, delta=1e-5):
    """
    Conceptual implementation of DP-SGD for LLM Fine-Tuning.
    In production, this runs over the entire dataset.
    """
    privacy_engine = PrivacyEngine()

    # Wrap model, optimizer, and dataloader with Opacus
    model, optimizer, dataloader = privacy_engine.make_private(
        module=model,
        optimizer=optimizer,
        data_loader=dataloader,
        noise_multiplier=1.1, # Calibrated for Epsilon=1.0
        max_grad_norm=1.0,
    )

    # Standard training loop
    for batch in dataloader:
        outputs = model(**batch)
        loss = outputs.loss
        loss.backward()
        optimizer.step()
        optimizer.zero_grad()

    return privacy_engine.get_epsilon(delta=delta)

B. DP in RAG (Embedding Noise Injection)

To prevent adversaries from querying the Vector DB to deduce if a specific sensitive document exists (Membership Inference), we add calibrated Laplace/Gaussian noise to the retrieved embeddings before passing them to the LLM context.

import numpy as np

def add_dp_noise_to_embeddings(
    embeddings: np.ndarray,
    epsilon: float,
    sensitivity: float = 1.0
) -> np.ndarray:
    """
    Applies the Gaussian Mechanism to embeddings for Differential Privacy.
    Prevents embedding inversion attacks in the RAG pipeline.
    """
    # Sigma calculated based on Epsilon, Delta, and Sensitivity
    delta = 1e-5
    sigma = (sensitivity * np.sqrt(2 * np.log(1.25 / delta))) / epsilon

    noise = np.random.normal(0, sigma, embeddings.shape)
    noisy_embeddings = embeddings + noise

    # Normalize to maintain cosine similarity utility
    norms = np.linalg.norm(noisy_embeddings, axis=1, keepdims=True)
    return noisy_embeddings / norms

Step 3: LangGraph Multi-Agent Orchestration

Now, we tie it all together using LangGraph. We define a State that persists across the graph and conversational turns (Memory).

from typing import TypedDict, Annotated, Sequence
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
import operator

# 1. Define the State
class AgentState(TypedDict):
    # The conversation history
    messages: Annotated[Sequence[BaseMessage], operator.add]

    # PII mapping for the current turn
    pii_mapping: dict

    # User ID for memory isolation
    user_id: str

# 2. Define the Nodes (Agents)

def pii_redactor_node(state: AgentState):
    """Intercepts user input and redacts PII."""
    last_message = state["messages"][-1].content
    redacted_text, mapping = pii_engine.redact(last_message)

    # Replace the last message with the redacted version for downstream agents
    redacted_msg = HumanMessage(content=redacted_text)

    return {
        "messages": [redacted_msg],
        "pii_mapping": mapping
    }

def dp_rag_retriever_node(state: AgentState):
    """Retrieves documents and applies DP noise to embeddings."""
    query = state["messages"][-1].content

    # Mock Vector DB Retrieval
    # In production: docs = vectorstore.similarity_search(query)
    mock_docs = [
        "Underwriting for <PERSON> is currently in Stage 2. Requires financial verification.",
        "Standard SLA for loan approval is 48 hours."
    ]

    # Mock Embeddings + DP Noise Injection
    mock_embeddings = np.random.rand(2, 768)
    noisy_embeddings = add_dp_noise_to_embeddings(mock_embeddings, epsilon=5.0)

    context = "\n".join(mock_docs)

    # Inject context into the state as a system message
    context_msg = BaseMessage(
        content=f"CONTEXT:\n{context}\n\nQuery: {query}",
        type="system"
    )

    return {"messages": [context_msg]}

def dp_generator_node(state: AgentState):
    """Generates response using a DP-Fine-Tuned LLM."""
    # In production, this calls an LLM that was trained using the Opacus DP-SGD method
    context = state["messages"][-2].content
    query = state["messages"][-1].content

    # Mock LLM Generation (Using a DP-Fine-Tuned model)
    response_text = (
        "Based on the secure records, the underwriting for the user is in "
        "Stage 2. The standard SLA is 48 hours. "
        "(Processed via DP-Safe LLM)."
    )

    return {"messages": [AIMessage(content=response_text)]}

def pii_resolver_node(state: AgentState):
    """Maps pseudonyms back to real PII for the end-user."""
    ai_response = state["messages"][-1].content
    mapping = state.get("pii_mapping", {})

    final_response = pii_engine.resolve(ai_response, mapping)

    # Overwrite the AI message with the resolved version
    return {"messages": [AIMessage(content=final_response)]}

# 3. Build the Graph
workflow = StateGraph(AgentState)

workflow.add_node("pii_redactor", pii_redactor_node)
workflow.add_node("dp_retriever", dp_rag_retriever_node)
workflow.add_node("dp_generator", dp_generator_node)
workflow.add_node("pii_resolver", pii_resolver_node)

workflow.set_entry_point("pii_redactor")
workflow.add_edge("pii_redactor", "dp_retriever")
workflow.add_edge("dp_retriever", "dp_generator")
workflow.add_edge("dp_generator", "pii_resolver")
workflow.add_edge("pii_resolver", END)

# 4. Compile with Memory (Checkpointer)
# In Enterprise, use PostgresSaver for distributed memory
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

Step 4: Execution & Real-Time Flow

Let's run the pipeline with our FinServe use case. Notice how we pass a thread_id to maintain conversational memory.

config = {"configurable": {"thread_id": "loan_officer_42"}}

# Initial Query containing heavy PII
initial_query = (
    "What is the status of John Doe's loan? "
    "His SSN is 123-45-6789 and email is [email protected]."
)

print("--- Running Enterprise Privacy RAG Pipeline ---")

result = app.invoke(
    {
        "messages": [HumanMessage(content=initial_query)],
        "user_id": "loan_officer_42"
    },
    config
)

print("\n[Final User-Facing Response]:")
print(result["messages"][-1].content)

4. Enterprise Considerations & Best Practices

Implementing this in a production environment in 2026 requires attention to several architectural nuances.

1. Memory & State Management

In the code above, we used MemorySaver for simplicity. In a true enterprise deployment, you must use PostgresSaver or RedisSaver. This ensures that the pii_mapping and conversational history are durable, distributed, and can be audited for compliance (e.g., GDPR Right to be Forgotten).

2. The "DP-Fine-Tuned" Imperative

It is crucial to understand that PII Redaction at inference time is not enough. If an LLM was trained on raw PII, it has memorized it. An adversary could use prompt extraction techniques to leak it.

  • Action: All enterprise base models must be fine-tuned using Opacus (DP-SGD) or trained via Private Aggregation of Teacher Ensembles (PATE) before being deployed to the RAG generator node.

3. Utility vs. Privacy Trade-off (The Epsilon Slider)

Differential Privacy introduces noise. In our RAG pipeline, we added noise to the embeddings (epsilon=5.0).

  • A lower Epsilon (e.g., 1.0) means higher privacy but more noise, which degrades retrieval accuracy.

  • A higher Epsilon (e.g., 10.0) means lower privacy but higher utility.

  • Enterprise Strategy: Implement a dynamic Epsilon slider based on data classification. Public data gets ϵ=10, highly sensitive financial data gets ϵ=1.0.

4. Evaluation and Red Teaming

You must continuously evaluate the pipeline. Use frameworks like Garak or PyRIT to run adversarial PII extraction and membership inference attacks against your deployed LangGraph pipeline to ensure the DP bounds hold up in the real world.

Conclusion

Building enterprise GenAI is no longer just about achieving the highest benchmark scores; it is about building trustworthy, mathematically provable privacy guarantees. By combining Presidio for deterministic PII redaction, Opacus for DP-SGD fine-tuning, Gaussian Noise Injection for RAG vector security, and LangGraph for stateful multi-agent orchestration, organizations can deploy powerful AI assistants that respect user privacy and comply with the strictest global regulations.