The redact phi langchain llm pattern is mandatory when you process medical records with third-party models. If you forward raw patient names, MRNs, or dates of birth to an LLM API, you create direct HIPAA exposure and weaken your breach posture. This guide builds a concrete pipeline that strips protected health information locally before any token leaves your infrastructure.
Step 1: Install local PHI detection tooling
Cloud NLP services that “detect PHI for you” defeat the purpose—you already sent the PHI. Run detection on your own hardware. Microsoft Presidio is the pragmatic choice: it wraps spaCy and rule-based recognizers, and it runs fully offline.
pip install presidio-analyzer presidio-anonymizer langchain langchain-openai langchain-text-splitters spacy
python -m spacy download en_core_web_lg
The en_core_web_lg model gives Presidio the entity recognition backbone. For a production service, pin versions and pre-build a container image so you never fetch models at request time.
Step 2: Write a deterministic redaction function
Presidio splits work into an analyzer (finds spans) and an anonymizer (replaces them). The default anonymizer swaps each entity for its type label, e.g., <PERSON>. That is enough to break direct identifiers while preserving grammatical structure for the LLM.
from presidio_analyzer import AnalyzerEngine
from presidio_anonymizer import AnonymizerEngine
analyzer = AnalyzerEngine()
anonymizer = AnonymizerEngine()
def redact_text(text: str) -> str:
# Detect PII/PHI entities in the supplied language
results = analyzer.analyze(text=text, language="en")
if not results:
return text
# Replace detected spans with entity-type placeholders
anonymized = anonymizer.anonymize(text=text, analyzer_results=results)
return anonymized.text
Call it on a sample:
raw = "Patient John Doe, MRN 123456, DOB 1980-01-01, lives at 5 Elm St."
print(redact_text(raw))
# -> "Patient <PERSON>, MRN <US_SSN_OR_ITIN>, DOB <DATE_TIME>, lives at <LOCATION>"
Presidio mislabels MRN as SSN sometimes; add a custom recognizer for MRN patterns (\b\d{6,10}\b) if your corpus uses them. The redact phi langchain llm pattern lives or dies on recall for your specific field formats.
Step 3: Wrap redaction as a LangChain Runnable
LangChain’s RunnableLambda lets you drop the function into any composition without custom classes.
from langchain_core.runnables import RunnableLambda
redact_runnable = RunnableLambda(redact_text)
Now you can pipe documents or queries through it: redact_runnable.invoke(raw). Because it is a Runnable, it works natively with RunnableParallel, retries, and batching.
Step 4: Redact documents at ingestion time
Embeddings are not anonymous. If you embed raw PHI, the vector store becomes a regulated artifact. Redact before splitting and before embedding.
from langchain_text_splitters import RecursiveCharacterTextSplitter
raw_doc = """
Clinical note: John Doe (MRN 998122) presented on 2023-04-12 with chest pain.
Address: 12 Oak Rd. Prior history of hypertension.
"""
splitter = RecursiveCharacterTextSplitter(chunk_size=200, chunk_overlap=20)
chunks = splitter.split_text(raw_doc)
redacted_chunks = [redact_text(c) for c in chunks]
# redacted_chunks now contain "<PERSON> (MRN <US_SSN_OR_ITIN>) presented..."
Push redacted_chunks into your vector store. The original raw_doc should stay in your compliant primary datastore, not in the LLM-adjacent cache.
Step 5: Redact user queries before retrieval
Engineers often redact the corpus and forget the query. A user asking “What did John Doe’s labs show?” just leaked a name to the gateway. Redact the question string with the same function.
user_question = "What did John Doe's labs show on 2023-04-12?"
redacted_question = redact_text(user_question)
# -> "What did <PERSON>'s labs show on <DATE_TIME>?"
The retriever then searches using the redacted question. Semantic match quality stays high because entity replacements still carry syntactic role.
Step 6: Assemble the full chain
Use RunnableParallel to redact both context and question, then call the model. Below is a minimal Q&A composition.
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
prompt = ChatPromptTemplate.from_messages([
("system", "Answer only from the provided context. Context is redacted."),
("human", "Context:\n{context}\n\nQuestion: {question}")
])
# Standard OpenAI client; point base_url at any OpenAI-compatible gateway.
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
chain = (
{
"context": redact_runnable, # assumes context passed as string
"question": redact_runnable,
}
| prompt
| llm
)
response = chain.invoke({
"context": raw_doc,
"question": user_question
})
If you route through n4n.ai, the same ChatOpenAI client works against its OpenAI-compatible endpoint and you get automatic fallback when a provider is rate-limited or degraded—but the redaction above is still your control point, not the gateway’s.
Step 7: Verify success
Verification is not optional. You need three checks:
- Unit test on known PHI
def test_redaction():
raw = "John Doe MRN 123456 DOB 1980-01-01"
out = redact_text(raw)
assert "John Doe" not in out
assert "123456" not in out
assert "1980-01-01" not in out
-
Payload logging in staging Log the exact JSON sent to the LLM client. Grep for known test identifiers. If they appear, your Runnable ordering is wrong.
-
Negative regex sweep Run a regex for your MRN format against the redacted corpus. Any match is a recall failure.
A green build on all three means the redact phi langchain llm pattern is actually enforced.
Step 8: Handle operational edge cases
Presidio will miss rare formats. Add custom recognizers for facility-specific codes. Keep a deny-list of high-risk tokens (staff names) as a second pass.
Do not rely on the LLM to “forget” PHI. Prompt instructions like “ignore PII” are not compliance. The redaction must be structural, not advisory.
Cache the redacted chunks, not the raw ones. If you use a provider cache-control hint via the gateway, set cache_control on the redacted prompt only.
Why this pattern holds up
The redact phi langchain llm pattern separates compliance from model choice. Because redaction is a local Runnable, you can swap the LLM provider, use a different embedding model, or move from RAG to fine-tuning without re-auditing data flow. The PHI never enters the request object.
In healthcare deployments, that boundary is the difference between a defensible architecture and an incident report. Build the redaction step first, treat it as load-bearing code, and test it like you would auth.