Restore only what this request tokenised. The scoping is the control.
public async Task<string> CompleteAsync(
string conversationId, string userText, CancellationToken ct)
{
var findings = await _detector.AnalyzeAsync(userText, ct: ct);
var d = await _deidentifier.ApplyAsync(conversationId, userText, findings, ct);
var completion = await _model.CompleteAsync(d.Text, ct);
// Scope-limited: only this conversation's map is in play.
var map = await _vault.MapForAsync(conversationId, ct);
var answer = Reidentify(completion, map);
if (TokenPattern.IsMatch(answer))
_logger.LogWarning("Unresolved token in completion for {Conversation}", conversationId);
return answer;
}That last check matters. An unresolved token in the output means the model invented a token name, or your map was incomplete, or the restore missed a reformatted token. Users will report it as gibberish. Catch it yourself first.
Do not log userText, d.Map, or answer in that method. Chapter 16 explains why at length; for now, note that the most natural place to add a debug log is the one place that sees both the tokens and the values.
The detector is a network call to a container. Containers restart, get OOM-killed, and hit their connection limits. What your code does in that two-second timeout is a policy decision, and the default that most code falls into is the wrong one.
try
{
findings = await _detector.AnalyzeAsync(text, ct: ct);
}
catch (Exception ex) when (ex is HttpRequestException or TaskCanceledException)
{
_logger.LogError(ex, "PII detector unavailable");
throw new DetectorUnavailableException(); // do not proceed
}An empty findings list and a failed call are indistinguishable downstream. A catch that logs and continues with zero findings sends the raw text to the model, and it does so precisely when your protection is broken. The system appears healthy, because from the user's perspective the feature is working better than usual.
Failing closed has a cost and you should choose it deliberately. Options, roughly in order of preference:
Reject the request with a clear error. Correct for anything handling sensitive data.
Degrade to deterministic-only and proceed with reduced coverage, logging that you did. Defensible when Chapter 5's layer covers your highest-consequence entities and the remainder is low blast radius. Record the degradation, because it is a fact an auditor will ask about.
Queue for later. Works for asynchronous jobs, useless for interactive chat.
What you never do is proceed as though the check passed. If that means your LLM feature has an availability dependency on your detection container, that is true and you should say so in the design document rather than discovering it during an incident.
One last failure mode, easy to miss and unpleasant in production.
A user says "Sarah Whitfield" in turn one and "Sarah" in turn four. Your detector finds PERSON in both, but the strings differ, so byValue assigns a second token. The model now believes there are two people.
The fix is normalisation at the vault boundary: before assigning a token, check whether the value is a subset or variant of an existing value in this conversation. Surname-only, given-name-only, and initialised forms cover most real cases.
private static bool IsLikelySamePerson(string candidate, string known) =>
known.Contains(candidate, StringComparison.OrdinalIgnoreCase)
|| candidate.Contains(known, StringComparison.OrdinalIgnoreCase);That heuristic is crude and it is better than nothing. Get it wrong in the merging direction and you conflate two people, which is worse than splitting one, so bias it towards splitting and accept the occasional duplicate.
The round trip works, it is reversible, and it keeps real values away from the model. It also, sometimes, makes the model worse at the job you asked it to do.
The implementation details are the author's: replacing from the end of the string, the byValue map for coreference, angle-bracket delimiters surviving paraphrase, the four vault properties, and the multi-turn name-variant heuristic. These are engineering positions, argued in the text rather than cited.
Download the full PDF for free?
Free download — no account required