n4nAI

HIPAA-aware document Q&A: a LangChain tutorial

Hands-on tutorial for building a HIPAA-aware document Q&A system with LangChain: vector search, PHI redaction, and compliant LLM routing for healthcare teams.

n4n Team3 min read567 words

Audio narration

Coming soon — every post will get a voice note here.

Building a hipaa document qa langchain pipeline forces you to treat every retrieved chunk and generated token as potential protected health information. This tutorial walks through a concrete implementation that loads clinical PDFs, embeds them locally, and answers questions through a compliant model endpoint with redaction guardrails.

Prerequisites

  • Python 3.10 or newer
  • Packages: langchain, langchain-community, langchain-openai, chromadb, pypdf, python-dotenv
  • An LLM endpoint covered by a Business Associate Agreement (BAA). If you use a third-party gateway, confirm it forwards to BAA-backed providers only. For example, an OpenAI-compatible gateway like n4n.ai can route across 240+ models with automatic fallback when a provider is degraded, but you remain responsible for the BAA.
  • A test PDF containing synthetic PHI. Never point this code at real patient records without a full compliance review.

Set up a virtual environment and install dependencies:

pip install langchain langchain-community langchain-openai chromadb pypdf python-dotenv

1. Load and split the document

Use PyPDFLoader to pull text, then split into chunks small enough for embedding and retrieval. Keep chunk size conservative to limit PHI exposure per retrieval.

from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

loader = PyPDFLoader("synthetic_clinical_note.pdf")
pages = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", ". ", " "]
)
docs = splitter.split_documents(pages)
print(f"Loaded {len(docs)} chunks")

Expected output:

Loaded 42 chunks

2. Embed and store locally

Embeddings run against your chosen model. Store vectors in Chroma on local disk so PHI never leaves your infrastructure unencrypted. The hipaa document qa langchain pattern depends on this swapability to avoid vendor lock-in.

import os
from dotenv import load_dotenv
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma

load_dotenv()
os.environ["OPENAI_API_BASE"] = os.getenv("LLM_API_BASE")
os.environ["OPENAI_API_KEY"] = os.getenv("LLM_API_KEY")

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(
    documents=docs,
    embedding=embeddings,
    persist_directory="./chroma_db"
)
vectorstore.persist()

If your LLM_API_BASE points to a compliant gateway, the same code works without changes.

3. Build the retriever and chat model

Create a retriever that returns the top 3 chunks. Use a chat model with temperature 0 for deterministic answers.

from langchain_openai import ChatOpenAI

retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

4. Add HIPAA guardrails

Never trust the model to spontaneously omit PHI. Apply regex redaction on both the retrieved context (for logging) and the final answer. This is a baseline; pair it with a human review step in production.

import re

PHI_PATTERNS = [
    (r"\b\d{3}-\d{2}-\d{4}\b", "[SSN]"),
    (r"\b\d{3}-\d{3}-\d{4}\b", "[PHONE]"),
    (r"\b\d{1,2}/\d{1,2}/\d{4}\b", "[DATE]"),
]

def redact(text: str) -> str:
    for pattern, replacement in PHI_PATTERNS:
        text = re.sub(pattern, replacement, text)
    return text

Wrap the LLM call so the answer is scrubbed before it reaches the caller:

from langchain.chains import RetrievalQA

qa = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=retriever,
    return_source_documents=True
)

def ask(query: str) -> str:
    raw = qa.invoke({"query": query})
    answer = redact(raw["result"])
    return answer

5. Run the Q&A loop

Query for a specific patient detail. The system retrieves chunks, generates an answer, and redacts identifiers.

question = "What medication was prescribed for the patient with SSN 123-45-6789?"
answer = ask(question)
print(answer)

Expected output (synthetic data):

The prescribed medication was Lisinopril 10mg daily. The patient's identifier [SSN] has been redacted per policy.

Source documents are available in raw["source_documents"]; redact them before any logging.

6. Conversation memory with isolation

For multi-turn Q&A, use ConversationalRetrievalChain but disable persistent memory on disk. Keep the chat history in memory only and clear it after the session.

from langchain.memory import ConversationBufferMemory
from langchain.chains import ConversationalRetrievalChain

memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True,
    output_key="answer"
)

crc = ConversationalRetrievalChain.from_llm(
    llm=llm,
    retriever=retriever,
    memory=memory
)

def chat(query: str) -> str:
    res = crc.invoke({"question": query})
    return redact(res["answer"])

The hipaa document qa langchain stack must isolate session state. Never write chat_history to a file without encryption and a retention policy.

7. Deployment notes

The pipeline is only compliant if the surrounding infrastructure is. Encrypt the Chroma directory at rest with dm-crypt or cloud KMS. Set OPENAI_API_KEY from a secrets manager, not a .env file in the repo. Disable LangChain verbose logging in production—it prints retrieved documents.

If you use a gateway, confirm it meters per-token usage and forwards provider cache-control hints so you can cache embeddings and common retrievals. That reduces cost and limits repeated PHI transmission.

Finally, add an access control layer: issue short-lived tokens to clinicians, and audit every query. The code above is a starting point, not a turnkey compliant product.

8. Unit test the redaction

Guardrails fail silently if regexes drift. Add a small test to lock behavior:

def test_redact():
    sample = "Patient 123-45-6789 called 555-123-4567 on 01/02/2023."
    cleaned = redact(sample)
    assert "[SSN]" in cleaned
    assert "[PHONE]" in cleaned
    assert "[DATE]" in cleaned
    assert "123-45-6789" not in cleaned

Run pytest test_redact.py. Expected output:

1 passed in 0.02s

What you built

You now have a runnable pipeline that ingests PHI documents, retrieves context, and answers with redaction. The hipaa document qa langchain approach keeps embedding and vector storage local, routes generation to a BAA-covered model, and scrubs output. Extend it with entity recognition for finer redaction and a signed audit log before production use.

Tagslangchainhealthcarehipaadocument-qa

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All framework tutorials: legal & healthcare document q&a posts →