cd /news/artificial-intelligence/five-bugs-in-my-llm-app-that-never-t… · home topics artificial-intelligence article
[ARTICLE · art-115506] src=dev.to ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

Five bugs in my LLM app that never threw an error

A developer spent two and a half weeks building an AI agent that reads code review history and distills conventions, but encountered five silent bugs that never threw errors. The bugs included Python's truthiness of non-empty strings, Pydantic rejecting null for string fields, and mismatched wildcard patterns between SQL LIKE and fnmatch, causing the agent to silently ignore most rules. The developer fixed these issues by adding Pydantic validators and converting patterns, ensuring the agent behaves as intended.

read5 min views2 publishedAug 30, 2026

I spent two and a half weeks building an agent that reads a project's code review history and remembers what it decided. 86,321 comments from pandas, distilled into 298 conventions, served out of CockroachDB behind a Lambda.

Every serious bug I hit had the same shape. Nothing raised. Tests passed. The demo worked. And somewhere in the middle, a component was doing absolutely nothing.

Here are five, with the code.

bool("false")

is True

The whole point of the system is that it refuses when it does not know. The answering prompt returns JSON with an answered

field.

result = await chat.complete_json(messages)
if result.get("answered"):
    return result["answer"]

A model that writes "answered": "false"

instead of "answered": false

turns a refusal into an answer, because a non-empty string is truthy. The one behaviour the project exists to guarantee, defeated by a quotation mark.

Now every model response goes through a Pydantic model that fails towards doing nothing:

class AnswerOutput(ModelOutput):
    answered: bool = False   # coerces "false" -> False
    answer: str = ""

An answer becomes a refusal. A drafted rule becomes unusable. A contradiction verdict becomes "compatible", which retires nothing. A garbled response is not evidence for writing to memory.

null

discarded whole responses, six times out of six The fix for bug 1 caused bug 2, which is funnier than it was at the time.

The prompt that turns a maintainer's comment into a rule says:

"rationale": "why, one sentence, only if they gave a reason"

A maintainer who states a convention flatly gets null

back, which is correct JSON for "no reason given". And:

class DraftedRule(BaseModel):
    statement: str = ""
    rationale: str = ""   # null is not a str

Pydantic rejects null

for a plain str

. Validation is all or nothing. So the entire response was thrown away and replaced with the inert default, which downstream reads as the model having declined.

A real maintainer correction became {"status":"ignored","reason":"no convention stated"}

. Six reproductions, six failures, on a payload that was otherwise exactly what I asked for.

class ModelOutput(BaseModel):
    @field_validator("*", mode="before")
    @classmethod
    def _null_text_is_empty(cls, value, info):
        if value is not None:
            return value
        field = cls.model_fields.get(info.field_name)
        return "" if field is not None and field.annotation is str else value

A null

where a string was expected means empty, not malformed. Note the field.annotation is str

check: scope_pattern: str | None

genuinely means None, and coercing that one would turn "applies everywhere" into a pattern matching nothing.

Each rule stores a pattern for which files it applies to. I matched with fnmatch

.

fnmatch("pandas/core/frame.py", "pandas/core/%")   # False

The patterns were SQL LIKE, not globs. pandas/tests/%

, pandas/core/%

, pandas/tests/%/conftest.py

. Nothing asked the extraction prompt for that and nothing documented it. The model just wrote it, presumably because the rules were destined for a database.

%

is not an fnmatch wildcard. So 68 of 79 anchored rules silently matched nothing, and the agent ran entirely on the two rules that happened to be written pandas/**/*.py

, which cover 63% of the repository.

It looked like it was working the entire time. The fix is one line, and the bug was invisible for a week:

cleaned = pattern.strip().strip("`").lstrip("./").replace("%", "*")

The agent comments on pull requests, choosing which conventions apply by embedding a description of the change and taking the nearest rules.

It commented on 38 of 40. I assumed a bad threshold and started tuning. At 1.05 it spoke on 95% of pull requests; at 0.95, on 18%.

Then I measured the distribution instead. For 25 real pull requests, distance to the nearest rule versus the tenth nearest:

query phrasing median nearest median spread, 1st to 10th
title only 0.954 0.097
title plus directories 0.947 0.100
paths and counts 0.958 0.105
written as a question 0.933 0.095

The nearest rule is not meaningfully nearer than the tenth, under any phrasing. Every rule is a general statement about the same codebase, so every rule is similar to any description of a change to it.

There was nothing to threshold. Retrieval now runs on path matching, which is a fact, and similarity only orders what the paths already admitted.

author_association

describes the present I filtered to maintainer comments using GitHub's author_association

field.

It reports whether someone has write access now, not when they wrote the comment. One reviewer wrote 74,077 comments on pandas between 2012 and 2025, more than twice anyone else. He has since left the org, so all 74,077 come back as CONTRIBUTOR

.

Filtering on that field discarded about a third of the corpus, including the most experienced reviewers the project ever had. No error, no warning, just a smaller number that looked plausible.

None of these raised. Four of the five I found by counting something rather than by reading code: how often does it speak, how many rules match anything, how far apart are the distances really.

A library used wrong fails loudly. A model used wrong returns confident, well-formatted output that is quietly detached from what you meant.

So pick a number that should hold if the system works, and go and look at it. Not "does it return results" but "how often does it speak, and is that the rate I intended". Not "did it cite something" but "do the citations resolve".

Code is at github.com/pyarchana/precedent, MIT.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @cockroachdb 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/five-bugs-in-my-llm-…] indexed:0 read:5min 2026-08-30 ·