Your AI analyst is doing regex
Ask a model to classify sentiment and it will quietly write keyword-matching code and present it as semantic analysis. The labels look right, nobody checks them, and every row that needed judgement is wrong. I measured how wrong, and what fixes it.
AI data analysis has two failure modes, and only one of them gets talked about. The loud one is numeric: hallucinated totals, invented percentages, a model asked to add a column and confidently making the answer up. It gets the attention because it gets caught; a wrong total is checkable against the source, and someone eventually checks.
The quiet one lives in text. Ask a model to classify sentiment or categorise free-text responses and it will, left to its own devices, write keyword-matching code and present the result as semantic analysis. The output is a tidy column of labels. It looks exactly like the work.
Nobody audits it, because auditing 3,000 rows of sentiment labels is the job the AI was hired to do. And it is bad in a specific, structural way that stays invisible until a decision built on it goes wrong.
I measured this on my own product this week. hey anna is an AI data analyst, so the semantic lane is core business, and I wanted the failure quantified rather than suspected.
The methodology is deliberately checkable: a synthetic dataset of 150 customer reviews with planted ground-truth labels, deterministic seed, split into slices. Ninety plain controls (twenty of them neutral), 28 negation traps (“can’t complain”, “not worth the money”), 20 sarcasm traps (“Money well spent.” two sentences after describing the defect), and 12 mixed-sentiment reviews.
The failure is invisible because the easy rows look perfect
Start with the baseline: a naive keyword classifier, the kind of code models actually write. It scores 67.3% overall. The composition of that number is the whole story. On the plain controls it scores 100%. On the mixed-sentiment reviews, 11 of 12. On negation, 0%. On sarcasm, 0%.
That 100% is what makes the failure mode invisible. Spot-check a handful of rows and you will land on the easy ones, and the easy ones are flawless. The classifier is perfectly right on everything obvious and systematically wrong on everything that requires reading.
“Can’t complain” files as negative because it contains a negative word. The customer who wrote “Money well spent.” about a table that arrived snapped files as delighted. Every row that most needed judgement got string matching instead, and the aggregate looks plausible enough to ship.
The code performs competence
The obvious defence is “read the code the model wrote.” So here is the actual code from the eval, written by a 9B model (Qwen3.5-9B) given a Python runtime and asked for a sentiment breakdown:
# Strong negative indicatorsnegative_keywords = ['snapped', 'wobbles', 'wobbly', 'scratched', 'badly finished', 'disappointment', 'terrible', 'poor quality', "don't buy", 'worst', 'broken', 'broke', 'waste', 'hate', 'hated']# Strong positive indicatorspositive_keywords = ['love', 'amazing', 'fantastic', 'great', 'perfect', 'exceeded', 'beautiful', 'premium', 'durable', 'happy', 'pleased', 'favourite']# Check for sarcasm/contradiction patternshas_negative_structure = any( pattern in text_lower for pattern in ['if by', 'if you mean', 'nothing like', 'not', 'would not say', 'not happy', 'not pleased', 'not great'])Look at the third block. It has a section labelled “sarcasm/contradiction patterns”. It contains a bare 'not' substring check. This code scored 20% on the sarcasm slice.
The comments perform awareness of the exact problem the code fails at. A reviewer skimming it sees diligence, structure, even sophistication. Reading the code doesn’t help, because the code is fluent in the same way the prose is. It looks like the work.
Same model, one tool, 24 points
The interesting result is the controlled comparison. Same 9B model, same data, same brief, two harness configurations.
Code-only, where the model has a Python runtime and nothing else, which is the default configuration for this class of product: 63.3% overall. Below the keyword baseline, because its handwritten heuristics misfired on rows plain keywords got right. Sarcasm 20%, negation 43%.
With one addition to the harness, a first-class semantic-classification tool (in hey anna it’s an =AI() column: a prompt evaluated per row, so judgement is a legitimate bulk operation rather than something to be faked in code): 87.3% overall. Sarcasm 100%. Negation 100%. Same model. One tool difference.
Do the arithmetic on 87.3% and there is a trade to own. Both trap slices are perfect, so every remaining miss sits in the plain and mixed rows, the easy ones, where the keyword baseline was nearly flawless. The tool configuration gave back some easy-row accuracy to buy all of the trap-row accuracy. I’d take that trade every time: it swapped systematic misses on the rows that required reading for occasional misses on rows anyone can check.
The other obvious defence is “just use a bigger model.” A fully local 27B with the same tool scored 100% across every slice, ahead of cloud Sonnet’s 98%; Sonnet’s only misses were three neutral reviews drifting to positive. All three models, 9B to frontier, chose the semantic tool unprompted when it existed. The steering lives in the tool description, not in the model’s size.
And the misses that remain, on the 9B and Sonnet runs alike, concentrate in one place: bland, informational reviews drifting positive. Neutral is the hardest class, and it needs an explicit rubric rather than trust.
I’ve argued that AI should be a dumb renderer: compute everything that has a correct answer deterministically, and let the model narrate. This is that argument’s other half. Sentiment has no SQL answer; it is a judgement call per row, and it belongs on the generative side of the line. The failure here is the model smuggling a judgement task across to the deterministic side, reducing it to string matching so it can be code. The harness has to offer a legitimate way to do judgement at scale, or the model will fake determinism instead.
The problem nobody will pay to solve
Here is the uncomfortable commercial shape of all this. The loud failure mode sells its own fix; a hallucinated number burns someone visibly, and they go looking for a product that shows its working. The quiet one leaves no crime scene. The labels looked fine, someone made the decision, and whatever went wrong later never got traced back to a 'not' substring check. Benchmarks measure the loud lane for the same reason buyers audit it: it is the one you can see.
Which means the differentiator in AI products right now is harness design, and the hardest part of selling it is that the problem it solves is invisible to the customer by construction. This post is proof of both halves: the eval is hey anna’s, and hey anna is shutting down anyway. The harness held. The distribution did not.
A demo does not surface an invisible problem. You have to show people the failure they already have, the way an eval with planted ground truth showed it to me. For this class of problem, market education is the product work.