Entity resolution — deciding that "Anvi Innovate Pvt Ltd, Vizag" and "ANVI INNOVATE PRIVATE LIMITED, Visakhapatnam" are the same customer — is classic MDM pain with a new toolkit.
Tier: Advanced · Workflow area: Unstructured data processing
The modern pattern is two-stage: embeddings do the cheap, wide work of blocking (finding candidate pairs worth comparing), and an LLM does the expensive, careful work of adjudicating each candidate pair with a structured verdict and confidence. A human review band sits between auto-merge and auto-reject. Done right, this reaches accuracy hand-tuned rule systems struggle to match, at a fraction of the rule-maintenance burden.
Don't LLM-adjudicate what deterministic keys resolve — if a GSTIN/PAN/tax-ID match exists, that's the verdict. The LLM is for the residue.
Normalize, then embed every entity record. Concatenate the identity-bearing fields into a canonical string ("name | address | city | phone-last-4") and embed once per record, storing vectors alongside IDs:
text = f"{r.name} | {r.address} | {r.city} | {r.phone[-4:] if r.phone else ''}"
Any solid embedding model works; store in pgvector so blocking is a SQL query.
Block via similarity search. For each record, retrieve its top-k neighbors above a loose similarity floor:
SELECT b.entity_id, 1 - (a.emb <=> b.emb) AS sim
FROM entities a JOIN entities b ON a.entity_id < b.entity_id
WHERE a.entity_id = :id
ORDER BY a.emb <=> b.emb
LIMIT 10;
Tune the floor on a labeled sample for recall, not precision — a true duplicate pair missed at blocking is unrecoverable, while a false candidate just costs one LLM call. Blocking should collapse an O(n²) comparison space into a candidate list ~10–50x the entity count.
Define the verdict schema. Structured output with an explicit decision, confidence, and cited evidence:
from pydantic import BaseModel
from typing import Literal
class MatchVerdict(BaseModel):
decision: Literal["same_entity", "different_entity", "unsure"]
confidence: float
evidence: str # which fields drove the decision
Adjudicate candidate pairs in a loop over the API. Give the model both records verbatim and domain rules that matter (abbreviation conventions, city synonyms, what a legal-suffix difference means):
import anthropic
client = anthropic.Anthropic()
for pair in candidate_pairs: # checkpointed loop; skip already-judged pairs
response = client.messages.parse(
model="claude-opus-4-8",
max_tokens=1024,
thinking={"type": "adaptive"},
messages=[{
"role": "user",
"content": (
"Are these two records the same real-world company?\n"
f"A: {pair.a.to_json()}\nB: {pair.b.to_json()}\n"
"Rules: legal suffixes (Pvt Ltd / Private Limited) are equivalent; "
"'Vizag' and 'Visakhapatnam' are the same city; different GSTINs "
"mean different entities regardless of name similarity. "
"If evidence is thin, answer 'unsure' rather than guessing."
),
}],
output_format=MatchVerdict,
)
save_verdict(pair, response.parsed_output)
Expected output per pair: a MatchVerdict like {"decision": "same_entity", "confidence": 0.94, "evidence": "identical address and phone; name differs only in legal suffix"}.
Apply the gate: thresholds and the review band. Schema is enforced by parse(); decisions route three ways — same_entity with confidence ≥ 0.9 auto-merges, different_entity ≥ 0.9 auto-rejects, and everything else (including all unsure) lands in the human-review queue. Reviewer decisions append to a labeled-pairs table that becomes your calibration set and few-shot pool.
Cluster and survive. Pairwise merges compose into clusters via connected components (watch for chains: A=B, B=C, but A≠C — flag such clusters for review rather than transitively merging). Survivorship — which record's values win in the golden record — stays a deterministic, documented rule set, not an LLM decision.
Measure against the labeled sample. Hold out 200 human-labeled pairs (stratified across easy/hard). Report precision and recall of auto-decisions and the review-band rate. Tune the two confidence thresholds on this set — precision of auto-merge is the number that protects you from wrongly merging two customers' billing.
False merges are far more damaging than missed merges — un-merging entities after downstream systems consumed the golden record is an incident, not a fix. Set the auto-merge threshold for near-perfect precision and let the review queue absorb the ambiguity.
Entity masters are concentrated PII — names, addresses, phone numbers — so this workflow needs explicit data-processing approval before any external API sees a record pair. Prefer running adjudication inside the client's perimeter: warehouse-native AI functions, or a self-hosted model via vLLM for the verdict step (the schema, thresholds, and review band are identical; budget for lower accuracy and a wider review band). Minimize what each prompt carries — identity fields only, never full account history. The labeled-pairs set and review queue stay client-side, and the deliverable includes the threshold calibration report and survivorship rules doc so the client's MDM team can re-tune as their data evolves.