How Reddit Fixed My App's AI
My RAG pipeline was confidently wrong. I changed the corpus, not the model.
The hint that changed every time
Fermento is an iOS app for tracking kombucha, sourdough, kimchi, kefir, and 36 other ferments. It also has a small AI assistant that runs entirely on the phone. On devices that support it, the app uses Apple’s Foundation Models. Otherwise it downloads Qwen2.5-1.5B. Both models answer over a local retrieval index. There is no server, no API call, and no user data leaving the phone.
The first sign that this was not working was not a benchmark. It was the hint under a running brew.
The hint was supposed to give one short, useful instruction based on the ferment and its current state. Instead it was often vague, wrong, or unrelated. Worse, it seemed to change every time I opened the app. A kombucha jar could get sensible advice once, generic fermentation trivia next, and something meant for a completely different food after that.
My first instinct was to blame the model. It was small, quantized, and running on a phone. Maybe this was simply the quality ceiling of local inference.
That explanation was convenient. It was also wrong.
I needed the real pipeline, not a demo
I could not debug the problem by opening the app several times and taking notes. Each run showed one ferment in one state, with enough variation that almost any explanation felt plausible. Prompt wording, sampling, the selected model, and retrieval could all take the blame.
So I built a harness that runs the app’s own pipeline off-device against the exact models the app downloads. It let me inspect all 40 ferments across many states at once. The first audit covered 76 cases.
The harness did not make the assistant better. It made the failures repeatable. That was much more useful.
The retrieval query was one constant string for every ferment in every state. Seven different brew states returned byte-identical excerpts. The assistant appeared to be adapting to the jar, but the evidence supplied to it was often identical.
Then I looked at what the retriever preferred. In 80 % of cases it returned a paper about Mahewu, a Zimbabwean fermented cereal drink. The paper had no ferment tags, so it was eligible for everything. In 24 % of hints, the model repeated some of that material back to the user.
That produced my favourite failure from the audit. A person making sauerkraut was told to:
“scrub and smoke the fermentation vessel with the traditional plants used in Amakamo… from South-Western Uganda”
The model had not discovered an obscure sauerkraut technique. It had been handed an irrelevant excerpt and had tried to be helpful with it.
A kombucha brewer on day 1 got another instruction:
“feed the jar 25 g of flour”
This one came from an article that really was tagged kombucha. Its title was “Start a Sourdough Culture with Kombucha”. The tag was technically correct, but the article was about using kombucha as an ingredient in a different fermentation. Retrieval saw a match. The user saw nonsense.
There were other failures that looked like model behaviour but were really pipeline behaviour. In a quarter of the cases where no temperature had been recorded, the answer stated one anyway. The prompt said “max two sentences”, but 91 % of answers ignored that instruction. The median answer was 105 words.
At that point, changing the model would have been a distraction. The model could only work with the context the app supplied, and the context was wrong before generation began.
The problem moved from retrieval to the corpus
Fixing the constant query made retrieval more specific, but specificity exposed another problem. The corpus was rich in papers and poor in practical answers.
That distinction matters. An academic abstract can be accurate and still be useless when someone asks why yogurt did not set. A retrieval system does not know that a mechanistic detail is less useful than a plain troubleshooting instruction unless the corpus and ranking give it a way to know.
My RAG pipeline was behaving like a librarian who returned the most scholarly-looking page instead of the page that answered the question. The comparison stops there: the retriever was not judging scholarship or usefulness. It was following the eligibility and ranking rules I had given it.
I needed a set of questions that represented what people actually ask, not what I imagined they might ask while designing the feature.
That is where Reddit changed the project.
Reddit gave me an evaluation set
The communities in r/fermentation, r/Kombucha, r/Kefir, r/Sourdough, r/kimchi, and r/winemaking contain thousands of real problems described in ordinary language. More importantly, the replies often contain corrections from people who have seen the same failure before.
I turned 36 of those threads into evaluation scenarios with known-good answers. Each entry contains a question a user would actually type, the ferment, how common the issue is, its severity, and whether it is actually a problem. Severity is one of cosmetic, salvageable, or discard.
Nine of the 36 scenarios are cases where the right answer is “this is normal”. That flag became the most useful field in the file. A care assistant is not safe merely because it warns people about dangerous food. It also needs to avoid turning normal fermentation into a hazard.
I kept the evaluation set out of the retrieval corpus. Copying the Reddit answers into the app would hand the retriever the answer key and make the score meaningless. Reddit supplied the questions and exposed the knowledge gaps. The replacement articles were written separately from primary sources such as USDA, CDC, the National Center for Home Food Preservation, and university extensions.
The scoring is deterministic keyword coverage. I did not ask one model to grade another model and call the result objective. Each known-good answer defines the concepts that need to appear. The score tells me which of those concepts survived retrieval and generation.
It scored badly, and usefully
The first run reached 22 % mean concept coverage. Zero of 36 scenarios were fully answered. Dairy scored 0 %.
The grounding explained why. Academic abstracts supplied 80 % of the material behind the answers. Practical writing supplied 0 %.
For “My yogurt did not set”, the assistant returned a 19:1 Streptococcus thermophilus to Lactobacillus bulgaricus coculture ratio from a study. It never mentioned incubation temperature. The retrieved fact was real. It was simply not the fact the user needed.
Three questions were refused outright with:
“when in doubt, throw it out”
That included a sourdough starter smelling of acetone, which usually means it is hungry and needs feeding. It also included a vinegar mother forming, which means the process is working.
The refusal was not random. The safety guard required a safety source to be retrieved before it would answer questions involving suspicious smells or growth. The corpus contained no safety sources. The condition could never be satisfied, so the app told people to discard good food.
The evaluation set made that visible. Without the actually a problem field, I might have celebrated conservative refusals as safe behaviour.
The fix that made things worse
I wrote four practical safety articles covering mould versus kahm, botulism risk, fermentation smells, and bottle pressure. In the same change, I added a ranking rule that put safety sources above everything else.
That combined change raised coverage from 22 % to only 28 % and created three new wrong answers.
The worst one came from a jun brewer asking about harmless sediment. The assistant said it was:
“likely due to the presence of botulism spores”
The new articles were good. The ranking rule was not. Four safety articles were winning the retrieval slots for every question, so the botulism article outranked material about jun itself.
I had changed the corpus and ranking together. That made a ranking bug look like a content problem. I could easily have responded by rewriting correct articles or adding more of them.
Instead I limited safety-first ranking to questions that were actually about safety. Coverage reached 35 %, the three new errors went to zero, and the original three refusals also went to zero. On the nine “this is normal” cases, coverage moved from 25 % to 47 %.
That is better than the baseline. It is not a good final score.
For the hint, AI was the wrong source of truth
The evaluation work also forced a more basic decision. The hint under a running brew did not need the model to decide what to say.
Fermento already held the correct temperature bands, pH targets, and phase durations for every ferment in a canonical dataset. Asking retrieval and generation to rediscover those values added latency and created opportunities to invent, omit, or swap facts.
The hint now starts with the canonical instruction. The model has one narrow job: rewrite it warmly in under 40 words. A validator rejects the rewrite if it invents a number, drops one, changes the instruction, or wanders onto another ferment.
About three quarters of rewrites pass. The rest fall back to the original sentence, which was already correct. Median answer length moved from 105 words to 21. Latency moved from 4.2 s to 0.7 s.
The model became more useful after I gave it less authority.
What I would do first now
If I were starting another LLM feature, I would build the evaluation set before polishing the prompt. I would run it through the shipping retrieval and generation path, not a clean notebook example. I would label cases where the correct answer is reassurance, because unnecessary alarm is its own failure mode.
I would also change one layer at a time. Corpus, query construction, ranking, guard behaviour, and generation are separate systems even when they live in one function. Shipping two changes together cost me a clean explanation of the result.
Most of all, I would ask which parts need generation at all. If the product already owns the fact, the model should not be invited to reconstruct it. Let code select the instruction. Let the model handle tone. Validate the boundary between them.
Reddit did not train Fermento’s AI, and its posts were not copied into the app. People in r/fermentation, r/Kombucha, r/Kefir, r/Sourdough, r/kimchi, and r/winemaking gave me something more useful: a public record of the questions my feature needed to survive.
The current chat score is 35 %. There is a backlog of 31 practical articles and a number to beat. That is less satisfying than saying the assistant is fixed, but it is the first version of the project that I can measure honestly.