PII & Output Validation
Key Points
- PII detection: scrub sensitive data before sending to LLM and from responses before display/storage.
- Tools: Microsoft Presidio (Python; called from .NET), Azure AI Language PII detection, regex for simple cases.
- Output validation: schema, constraints, content moderation. Model can hallucinate JSON; verify.
- Schema-strict mode (OpenAI) helps but doesn't replace runtime validation.
PII categories
- Direct identifiers: name, email, phone, address, SSN, credit card.
- Indirect: IP, device ID, location patterns.
- Health (PHI): medical conditions, prescriptions.
- Financial: account numbers, balances.
Pre-LLM redaction
public string Redact(string input)
{
input = Regex.Replace(input, @"[\w.+-]+@[\w-]+\.[\w-]+", "[EMAIL]");
input = Regex.Replace(input, @"\b\d{3}-\d{2}-\d{4}\b", "[SSN]");
input = Regex.Replace(input, @"\b(?:\d[ -]*?){13,19}\b", "[CARD]");
input = Regex.Replace(input, @"\b\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}\b", "[PHONE]");
return input;
}
For richer:
// Azure AI Language PII
var client = new TextAnalyticsClient(uri, cred);
var pii = await client.RecognizePiiEntitiesAsync(input);
var redacted = pii.RedactedText;
Or Microsoft Presidio (Python) via subprocess / HTTP.
Tokenize for reversible
If the use case needs original PII back:
public class PiiTokenizer
{
public (string redacted, Dictionary<string, string> map) Process(string input)
{
var map = new Dictionary<string, string>();
var output = new StringBuilder(input);
// detect entities, replace with tokens, store mapping
return (output.ToString(), map);
}
public string Restore(string output, Dictionary<string, string> map)
{
foreach (var (tok, original) in map) output = output.Replace(tok, original);
return output;
}
}
LLM sees [USER_EMAIL_1]; final output restores actual email.
Output validation
LLM output → validate before use:
public bool TryParseAddress(string output, out Address addr)
{
addr = null;
try
{
addr = JsonSerializer.Deserialize<Address>(output);
}
catch { return false; }
return !string.IsNullOrEmpty(addr.Street)
&& addr.Country?.Length == 2
&& IsValidPostalCode(addr.Country, addr.PostalCode);
}
Don't trust the LLM's "I have validated" claims.
Schema-strict mode
OpenAI's structured output:
new ChatOptions
{
ResponseFormat = ChatResponseFormat.ForJsonSchema(
JsonSchema.FromType<MyType>(),
schemaName: "Address",
strict: true)
};
Enforces JSON Schema. Still validate semantic constraints in code.
Content moderation
Azure AI Content Safety:
var safety = new ContentSafetyClient(uri, cred);
var result = await safety.AnalyzeTextAsync(new() { Text = output });
if (result.Categories.Any(c => c.Severity > 4)) // 0-7 scale
return null;
Categories: Hate, SelfHarm, Sexual, Violence. Tunable thresholds.
Hallucination detection
// Check answer against retrieved sources
public bool IsGrounded(string answer, IList<string> sources)
{
var sentences = SplitIntoSentences(answer);
return sentences.All(s => sources.Any(src => SemanticOverlap(s, src) > 0.7));
}
Or use eval frameworks (Ragas faithfulness).
Output sanitization for display
If displaying LLM output as HTML: escape! LLMs can output <script> tags.
Or use markdown renderer that strips dangerous HTML.
File output validation
If LLM generates filenames / paths:
var safe = Path.GetFileName(filename); // strip path
if (Path.GetExtension(safe) is ".exe" or ".dll" or ".ps1") /* reject */;
Code execution validation
If LLM generates code (code interpreter): - Sandbox. - Resource limits. - Network policy. - No filesystem access outside scratch.
Senior considerations
- Pre + post: redact before LLM; validate after.
- Defense in depth: multiple layers.
- Schema-first: define types; validate against schema.
- Trust nothing from LLM: assume hallucination, malicious crafting (via injection).
- Compliance: PII handling must meet GDPR/HIPAA/etc.
Anti-patterns
- ❌ Send raw user PII to external LLM API.
- ❌ Trust LLM-generated SQL / code.
- ❌ Display unsanitized LLM output as HTML.
- ❌ Skip output validation because "LLM said it's correct".