<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kantemir Satibalov</title>
    <description>The latest articles on DEV Community by Kantemir Satibalov (@kantik001).</description>
    <link>https://dev.to/kantik001</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4009665%2Fb646b101-b5c6-4005-98d8-2fe3930bb3a7.png</url>
      <title>DEV Community: Kantemir Satibalov</title>
      <link>https://dev.to/kantik001</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kantik001"/>
    <language>en</language>
    <item>
      <title>Building an open standard for grounded document assistants</title>
      <dc:creator>Kantemir Satibalov</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:48:38 +0000</pubDate>
      <link>https://dev.to/kantik001/building-an-open-standard-for-grounded-document-assistants-2h6e</link>
      <guid>https://dev.to/kantik001/building-an-open-standard-for-grounded-document-assistants-2h6e</guid>
      <description>&lt;p&gt;Last week I published how I made ~500 horticulture papers queryable without hallucination (&lt;a href="https://dev.to/kantik001/my-father-wrote-the-papers-i-built-a-rag-assistant-so-growers-can-query-them-safely-1hi"&gt;passion project on DEV&lt;/a&gt;). That vertical worked. This post is about what came next: &lt;strong&gt;extracting the repeatable parts into an open platform — and publishing a spec other teams can conform to.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem nobody demos on Twitter
&lt;/h2&gt;

&lt;p&gt;Enterprise teams don't need another ChatGPT wrapper. They need assistants that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Answer &lt;strong&gt;only from internal documents&lt;/strong&gt; (policies, handbooks, runbooks).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cite sources&lt;/strong&gt; — filename + chunk — in every response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refuse&lt;/strong&gt; when retrieval cannot support an answer.&lt;/li&gt;
&lt;li&gt;Run &lt;strong&gt;on infrastructure they control&lt;/strong&gt; (Docker, K8s, private cloud).&lt;/li&gt;
&lt;li&gt;Ship with &lt;strong&gt;measurable quality&lt;/strong&gt; — not "it looked fine in a notebook once."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I learned this building for scientific PDFs. The interesting work was never the chat bubble. It was making archives &lt;strong&gt;answerable without lying&lt;/strong&gt; — and proving retrieval quality &lt;strong&gt;before&lt;/strong&gt; burning LLM tokens.&lt;/p&gt;

&lt;p&gt;That discipline became &lt;strong&gt;&lt;a href="https://github.com/kantik001/grounded-llm" rel="noopener noreferrer"&gt;Grounded LLM&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm trying to standardize (and what I'm not)
&lt;/h2&gt;

&lt;p&gt;I'm not building Dify, LangGraph, or a visual agent constructor.&lt;/p&gt;

&lt;p&gt;I'm standardizing &lt;strong&gt;one narrow class of systems&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Document-grounded assistants&lt;/strong&gt; — internal Q&amp;amp;A with citations, numeric verification, and regression-tested retrieval.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The positioning line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Open standard for document-grounded assistants with citations, numeric verify, and measurable retrieval quality — deployable on your infrastructure.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Non-goals (by design):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Arbitrary tool/agent graphs&lt;/li&gt;
&lt;li&gt;General chat without a knowledge base&lt;/li&gt;
&lt;li&gt;Cloud-only lock-in&lt;/li&gt;
&lt;li&gt;Feature parity with Glean or Microsoft Copilot SaaS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We compete on &lt;strong&gt;trust + reproducible quality + conformance&lt;/strong&gt; — not feature count.&lt;/p&gt;




&lt;h2&gt;
  
  
  Five pillars of the standard
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pillar&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;Today in the repo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Spec &amp;amp; conformance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Published rules + tests anyone can run&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kantik001/grounded-llm/blob/main/docs/en/spec/GROUNDED_SPEC_v1.md" rel="noopener noreferrer"&gt;Grounded Spec v1&lt;/a&gt;, &lt;code&gt;python -m conformance&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Quality science&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Numbers, not demos&lt;/td&gt;
&lt;td&gt;89 retrieval cases, &lt;strong&gt;retrieval gate in CI&lt;/strong&gt;, adversarial pack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Reference deploy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reproducible install&lt;/td&gt;
&lt;td&gt;Docker, Helm, Terraform (AWS/GCP/Azure)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. Template marketplace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Growth without forking core&lt;/td&gt;
&lt;td&gt;HR, IT Support, Legal FAQ packs + registry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5. Governance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standard outlives one author&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kantik001/grounded-llm/blob/main/docs/en/RFC.md" rel="noopener noreferrer"&gt;RFC process&lt;/a&gt;, &lt;a href="https://github.com/kantik001/grounded-llm/blob/main/docs/en/rfcs/RFC-0001-grounded-compatible.md" rel="noopener noreferrer"&gt;RFC-0001 Grounded-compatible&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Horizon 1 success metric: &lt;strong&gt;any engineer runs conformance on a fresh deploy in under 15 minutes.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What the platform is today (not a slide deck)
&lt;/h2&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://kantik001.github.io/grounded-llm/" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;kantik001.github.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;a href="https://github.com/kantik001/grounded-llm/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;Grounded LLM v0.1.0&lt;/a&gt; is the first tagged release — reference implementation of Grounded Spec v1 with hybrid retrieval (BM25 + dense + RRF), pgvector/Chroma/Qdrant backends, 89 retrieval eval cases in CI, and published GHCR images (&lt;code&gt;ghcr.io/kantik001/grounded-llm-*:0.1.0&lt;/code&gt;). Landing page: &lt;a href="https://kantik001.github.io/grounded-llm/" rel="noopener noreferrer"&gt;https://kantik001.github.io/grounded-llm/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After 11 delivery phases merged to &lt;code&gt;main&lt;/code&gt;, this is a &lt;strong&gt;working reference implementation&lt;/strong&gt;, not a manifesto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Clients (Web / API / Telegram / embed widget)
        ↓
Go server — auth, sessions, LLM, verify, admin, quotas, OIDC/RBAC
        ↓ POST /rag/context
Python RAG — hybrid BM25 + dense + RRF; Chroma / Qdrant / pgvector
        ↓
data/{tenant}/{domain}/  +  Postgres (sessions, audit; pgvector optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Split on purpose:&lt;/strong&gt; Go owns trust boundaries and orchestration; Python owns retrieval only.&lt;/p&gt;

&lt;h3&gt;
  
  
  What ships out of the box
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Citations in every answer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Audit trail for HR/legal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Numeric verify layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dosages, vacation days, SLA numbers must match retrieved context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retrieval eval gate in CI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Catches silent RAG regressions on every PR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-tenant API&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X-Tenant-ID&lt;/code&gt;, API keys, OpenAPI v1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise admin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;RBAC, OIDC SSO, audit log, async reindex, analytics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Template packs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python scripts/init_pack.py install hr&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ingest connectors&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SharePoint, Google Drive, Confluence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Conformance CLI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Offline spec check + live HTTP check against any deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embeddable widget&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Intranet embed, not only Telegram&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Quick start
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kantik001/grounded-llm.git
&lt;span class="nb"&gt;cd &lt;/span&gt;grounded-llm
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; conformance/requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; conformance spec          &lt;span class="c"&gt;# offline OpenAPI contract&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; conformance check &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your product is &lt;strong&gt;Grounded-compatible&lt;/strong&gt;, these tests should pass without forking my codebase.&lt;/p&gt;




&lt;h2&gt;
  
  
  From one vertical to a platform (the story arc)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Horticulture proof&lt;/th&gt;
&lt;th&gt;Grounded LLM platform&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Repo&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kantik001/grounded_horticulture_en" rel="noopener noreferrer"&gt;grounded_horticulture_en&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/kantik001/grounded-llm" rel="noopener noreferrer"&gt;grounded-llm&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Domain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Apple rootstocks, disease IDs&lt;/td&gt;
&lt;td&gt;Any internal documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retrieval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tuned on ~500 papers, eval 68/68&lt;/td&gt;
&lt;td&gt;Hybrid BM25+RRF + 89 eval cases, CI gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deliverable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Vertical proof + passion story&lt;/td&gt;
&lt;td&gt;v0.1.0 spec + conformance + packs + deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The horticulture project answered: &lt;em&gt;"Can we make scientific PDFs queryable safely?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Grounded LLM answers: &lt;em&gt;"Can we ship the next assistant in days without rebuilding auth, verify, eval, and deploy?"&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Media: what to show
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Screenshot 1 — Chat with citations
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvce50a5646p4npt00hnm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvce50a5646p4npt00hnm.png" alt=" " width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dp1a9mzkrk2lt22y4k7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4dp1a9mzkrk2lt22y4k7.png" alt=" " width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshot 2 — Conformance CLI
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F721hejjnb4z5jyqsthy5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F721hejjnb4z5jyqsthy5.png" alt=" " width="800" height="722"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📸 Terminal recording:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; conformance spec
python &lt;span class="nt"&gt;-m&lt;/span&gt; conformance check &lt;span class="nt"&gt;--url&lt;/span&gt; http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Screenshot 3 — CI retrieval gate
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxhpwdtkik21noh384n9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvxhpwdtkik21noh384n9.png" alt=" " width="799" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshot 4 — Template packs
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuve16kgr0oktmgmie7r5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuve16kgr0oktmgmie7r5.png" alt=" " width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshot 5 — Admin panel
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwkal8bd36feq0izk1c4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkwkal8bd36feq0izk1c4.png" alt=" " width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx98mds7n3hds7kv9huw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpx98mds7n3hds7kv9huw.png" alt=" " width="800" height="797"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh580fwq2dohi3d76el0w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh580fwq2dohi3d76el0w.png" alt=" " width="799" height="604"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Where this sits in the industry (including Google)
&lt;/h2&gt;

&lt;p&gt;Big tech is solving &lt;strong&gt;adjacent&lt;/strong&gt; problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product / area&lt;/th&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Grounded LLM difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;NotebookLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Research / consumer grounding on uploads&lt;/td&gt;
&lt;td&gt;We target &lt;strong&gt;enterprise on-prem&lt;/strong&gt;, API contract, CI gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vertex AI Search&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed cloud retrieval&lt;/td&gt;
&lt;td&gt;We target &lt;strong&gt;self-hosted&lt;/strong&gt;, MIT core, conformance badge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemini + Workspace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SaaS copilot inside Google&lt;/td&gt;
&lt;td&gt;We target &lt;strong&gt;any LLM endpoint&lt;/strong&gt;, any infra&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I'm not competing with Google on consumer UX. I'm saying: &lt;strong&gt;when procurement asks "is your internal assistant grounded and testable?" — there should be a published spec and CLI answer, not a vendor slide.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you work on &lt;strong&gt;enterprise RAG, OSS standards, or ML platform conformance&lt;/strong&gt; — I'd genuinely value feedback on &lt;a href="https://github.com/kantik001/grounded-llm/blob/main/docs/en/rfcs/RFC-0001-grounded-compatible.md" rel="noopener noreferrer"&gt;RFC-0001&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Call to action
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Try v0.1.0:&lt;/strong&gt; &lt;a href="https://github.com/kantik001/grounded-llm/releases/tag/v0.1.0" rel="noopener noreferrer"&gt;Release notes&lt;/a&gt; · &lt;code&gt;docker compose up&lt;/code&gt; or pull GHCR &lt;code&gt;:0.1.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Star / watch&lt;/strong&gt; the repo if enterprise grounding interests you: &lt;a href="https://github.com/kantik001/grounded-llm" rel="noopener noreferrer"&gt;github.com/kantik001/grounded-llm&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run conformance&lt;/strong&gt; on your deploy and open an issue if something should be in Spec v2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute an eval case&lt;/strong&gt; when you fix a retrieval bug — see &lt;a href="https://github.com/kantik001/grounded-llm/blob/main/GOOD_FIRST_ISSUES.md" rel="noopener noreferrer"&gt;GOOD_FIRST_ISSUES.md&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Building in horticulture or another vertical?&lt;/strong&gt; The passion repo is still the deep retrieval story; this platform is the generalization&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What is it?&lt;/td&gt;
&lt;td&gt;Open platform + Spec v1 for cited, verified document assistants&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is it not?&lt;/td&gt;
&lt;td&gt;Agent builder, ChatGPT clone, Glean competitor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why now?&lt;/td&gt;
&lt;td&gt;Vertical proof worked; standard + conformance is the multiplier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What's next?&lt;/td&gt;
&lt;td&gt;External conformance adopters, Spec v2 feedback, RFP-grade positioning&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I started with my father's papers. I want to end with a &lt;strong&gt;checkable standard&lt;/strong&gt; any team can implement — and prove — on their own infrastructure.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>opensource</category>
      <category>rag</category>
      <category>architecture</category>
    </item>
    <item>
      <title>My father wrote the papers — I built a RAG assistant so growers can query them safely</title>
      <dc:creator>Kantemir Satibalov</dc:creator>
      <pubDate>Fri, 10 Jul 2026 13:02:56 +0000</pubDate>
      <link>https://dev.to/kantik001/my-father-wrote-the-papers-i-built-a-rag-assistant-so-growers-can-query-them-safely-1hi</link>
      <guid>https://dev.to/kantik001/my-father-wrote-the-papers-i-built-a-rag-assistant-so-growers-can-query-them-safely-1hi</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gardener's Assistant&lt;/strong&gt; — a grounded RAG chat for horticulture.&lt;/p&gt;

&lt;p&gt;My father spent decades as a plant breeder and researcher (Doctor of Agricultural Sciences, North Caucasus mountain horticulture institute). His articles — and his colleagues' — live in journal PDFs, not in anything a generic LLM can cite reliably. I built an assistant so growers and agronomists can &lt;strong&gt;ask questions and get answers grounded in those papers&lt;/strong&gt;, with numbers verified before they reach the user.&lt;/p&gt;

&lt;p&gt;The intended goal: make scientific horticulture &lt;strong&gt;queryable without hallucination&lt;/strong&gt;. If retrieval cannot support an answer, the bot refuses. It does not invent rootstock codes, spray rates, or cultivar names.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does today:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Text chat over ~500 scientific articles (apple, pear, plum) via hybrid retrieval&lt;/li&gt;
&lt;li&gt;Telegram Mini App + browser client (Docker, one &lt;code&gt;compose up&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;68-question retrieval regression suite — the gate I run before trusting any LLM output&lt;/li&gt;
&lt;li&gt;Numeric verifier in Go — dosages in the answer must appear in retrieved context&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;Gardener's Assistant chat: three horticulture questions in Russian receive grounded answers from scientific articles via RAG, with streaming response in a Telegram-style UI:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fba44n12cot4xzy32nt7z.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fba44n12cot4xzy32nt7z.gif" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Admin panel: login, upload horticulture articles into the RAG corpus, trigger reindex, and review 👍/👎 answer ratings — filter by likes or dislikes, see totals, and inspect each rated Q&amp;amp;A with crop, timestamp, and session/message ID to trace who gave feedback:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhl62e4sg5fe641lehv3.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnhl62e4sg5fe641lehv3.gif" alt=" " width="760" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GIF:&lt;/strong&gt; see above — &lt;strong&gt;Russian UI and Russian source articles&lt;/strong&gt; (working demo today). English corpus and UI copy are being rolled out next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run locally:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/kantik001/grounded_horticulture_en
&lt;span class="nb"&gt;cd &lt;/span&gt;grounded_horticulture_en
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env   &lt;span class="c"&gt;# LLM_API_KEY required&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;→ &lt;strong&gt;Chat:&lt;/strong&gt; &lt;a href="http://localhost/" rel="noopener noreferrer"&gt;http://localhost/&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ &lt;strong&gt;Admin (article upload):&lt;/strong&gt; &lt;a href="http://localhost/admin.html" rel="noopener noreferrer"&gt;http://localhost/admin.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three questions from the recording&lt;/strong&gt; (Russian — matches the indexed corpus):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;Какие признаки парши на листьях яблони?&lt;/em&gt; — disease + glossary expansion&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Как густота посадки влияет на Айдаред на подвое СК 4?&lt;/em&gt; — exact identifier (BM25)&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Какие подвои подходят для интенсивного сада на склоне?&lt;/em&gt; — semantic + lexical blend&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;English equivalents&lt;/strong&gt; (for the upcoming EN launch — same retrieval paths, translated articles):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;em&gt;What are signs of apple scab on leaves?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;How does planting density affect Aidared on SK 4 rootstock?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;What rootstocks work for intensive orchards on slopes?&lt;/em&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The public repo ships &lt;strong&gt;demo articles only&lt;/strong&gt; (EN samples for quick start); the full journal corpus stays local for licensing. The pipeline, eval harness, and Docker stack are all there.&lt;/p&gt;


&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/kantik001" rel="noopener noreferrer"&gt;
        kantik001
      &lt;/a&gt; / &lt;a href="https://github.com/kantik001/grounded_horticulture_en" rel="noopener noreferrer"&gt;
        grounded_horticulture_en
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Grounded RAG horticulture assistant (English public portfolio, demo data only).
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🍏 grounded-horticulture — horticulture assistant&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Grounded RAG&lt;/strong&gt; for horticulture: answers grounded in scientific articles with fact checking, not LLM hallucinations. Telegram Mini App and browser chat with API key.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kantik001/grounded_horticulture_en/LICENSE" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/a549a7a30bacba7bfceebdc207a8e86c3f2c02995a2527640dca30048fd2b64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d417061636865253230322e302d626c75652e737667" alt="License: Apache 2.0"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kantik001/grounded_horticulture_en/server/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/42a8119239952bdf2ce6b26d1b1c896936c9f84e80b36c23fa133857bd2a0cd2/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f476f2d312e32332d3030414444383f6c6f676f3d676f266c6f676f436f6c6f723d7768697465" alt="Go"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kantik001/grounded_horticulture_en/api/" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/e7716dbbb101baaff85724da7435b8059c4585b3988b6f7f91a9c29f5d278a7f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f507974686f6e2d332e31312d3337373641423f6c6f676f3d707974686f6e266c6f676f436f6c6f723d7768697465" alt="Python"&gt;&lt;/a&gt;
&lt;a href="https://github.com/kantik001/grounded_horticulture_en/docker-compose.yml" rel="noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/de2ad857b3516d3115839ef96382af475fa06bcb7e17392806216470fc7f7a38/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446f636b65722d436f6d706f73652d3234393645443f6c6f676f3d646f636b6572266c6f676f436f6c6f723d7768697465" alt="Docker"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Demo&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Chat: question → RAG answer&lt;/th&gt;
&lt;th&gt;Admin: articles and 👍/👎&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/kantik001/grounded_horticulture_en/docs/assets/demo-chat.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkantik001%2Fgrounded_horticulture_en%2FHEAD%2Fdocs%2Fassets%2Fdemo-chat.gif" alt="Chat demo"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/kantik001/grounded_horticulture_en/docs/assets/demo-admin.gif"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fkantik001%2Fgrounded_horticulture_en%2FHEAD%2Fdocs%2Fassets%2Fdemo-admin.gif" alt="Admin demo"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/kantik001/grounded_horticulture_en/docs/assets/demo-chat.mp4" rel="noopener noreferrer"&gt;▶ Full chat recording (MP4)&lt;/a&gt; · &lt;a href="https://github.com/kantik001/grounded_horticulture_en/docs/assets/demo-admin.mp4" rel="noopener noreferrer"&gt;▶ Full admin recording (MP4)&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it is&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;An assistant for gardeners and agronomists: &lt;strong&gt;text&lt;/strong&gt; → hybrid search over articles → LLM answer with &lt;strong&gt;verification&lt;/strong&gt; of numbers and dosages; &lt;strong&gt;photo&lt;/strong&gt; → CV + recommendation (beta, no production weights in this repo).&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Go&lt;/strong&gt; (&lt;code&gt;server/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Auth, Postgres sessions, RAG+LLM orchestration, verify, rate limit, &lt;code&gt;/metrics&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Python&lt;/strong&gt; (&lt;code&gt;api/&lt;/code&gt;, &lt;code&gt;rag/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Hybrid retrieval (Chroma + BM25 + reranker), CV &lt;code&gt;/classify&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Web&lt;/strong&gt; (&lt;code&gt;webapp/&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Chat, article upload admin, nginx in Docker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Access:&lt;/strong&gt; Telegram &lt;code&gt;initData&lt;/code&gt; or browser &lt;code&gt;X-API-Key&lt;/code&gt; (see &lt;code&gt;.env.example&lt;/code&gt;).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Public repository:&lt;/strong&gt; git contains demo data only (&lt;code&gt;data/demo_hr/&lt;/code&gt;, &lt;code&gt;data/apple/sample_*.txt&lt;/code&gt;). Full article…&lt;/p&gt;
&lt;/blockquote&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/kantik001/grounded_horticulture_en" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
Key paths:

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rag/&lt;/code&gt; + &lt;code&gt;api/&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Hybrid retrieval (Chroma, BM25, RRF, reranker)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;server/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Go orchestration, SSE chat, numeric verifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eval/*.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;68 retrieval regression questions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scripts/run_rag_eval.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One-command eval runner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;webapp/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser chat UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Passion → engineering constraint
&lt;/h3&gt;

&lt;p&gt;The personal motivation came first: my father's horticulture papers should be queryable, not buried in PDF archives. The engineering rule followed: &lt;strong&gt;I don't trust the LLM until retrieval is measurable.&lt;/strong&gt; Before tuning models or UI, I wrote a 68-question JSONL eval suite — rootstock codes, diseases, out-of-scope refusals — and made it a regression gate.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why hybrid retrieval (not "better embeddings")
&lt;/h3&gt;

&lt;p&gt;Pure vector search understood &lt;em&gt;topic&lt;/em&gt; but missed &lt;em&gt;tokens that matter&lt;/em&gt; — cultivar names, rootstock codes like &lt;code&gt;SK 4&lt;/code&gt;, OCR-noisy spellings.&lt;/p&gt;

&lt;p&gt;Example eval question: &lt;em&gt;"How does planting density affect Aidared on SK 4 rootstock?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Vector-only returned paragraphs about rootstocks but not the &lt;code&gt;Aidared&lt;/code&gt; token:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fig4hlepohk2cirhxqge4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fig4hlepohk2cirhxqge4.png" alt=" " width="797" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; per-crop hybrid pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query → glossary expansion (domain synonyms)
     → Chroma (multilingual-e5-small) top-16
     → BM25 top-16
     → RRF merge
     → conditional cross-encoder rerank (rootstock / disease / variety only)
     → diversify (≤2 chunks per article) → top-8 to the LLM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: &lt;strong&gt;68/68&lt;/strong&gt; on the retrieval suite:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkyi9ap5lugqjfqjnhqr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgkyi9ap5lugqjfqjnhqr.png" alt=" " width="800" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Re-verify anytime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python scripts/run_rag_eval.py &lt;span class="nt"&gt;--suite&lt;/span&gt; all &lt;span class="nt"&gt;--in-process&lt;/span&gt; &lt;span class="nt"&gt;--fast&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Decisions worth calling out:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RRF over score normalization&lt;/strong&gt; — BM25 and cosine similarities live on different scales; ranks merge cleanly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Category-gated reranker&lt;/strong&gt; — the cross-encoder helps dense technical questions but costs CPU; "when should I water?" skips it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eval retrieval separately from generation&lt;/strong&gt; — no LLM tokens, ~20s locally, catches regressions before users do.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Go + Python split
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; (&lt;code&gt;rag/&lt;/code&gt;, &lt;code&gt;api/&lt;/code&gt;): embeddings, Chroma, BM25, reranker, &lt;code&gt;POST /rag/context&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; (&lt;code&gt;server/&lt;/code&gt;): auth (Telegram + API key), Postgres sessions, SSE streaming, &lt;strong&gt;numeric verifier&lt;/strong&gt; (numbers in the answer must appear in retrieved context)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If retrieval is weak, Go short-circuits before paying for generation.&lt;/p&gt;

&lt;h3&gt;
  
  
  What broke (and what saved me)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Eval caught it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chunking split tables from headers&lt;/td&gt;
&lt;td&gt;Apple pass_rate −7&lt;/td&gt;
&lt;td&gt;Yes — reverted in minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BM25 not rebuilt after corpus update&lt;/td&gt;
&lt;td&gt;Exact-code questions failed&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glossary entry too aggressive&lt;/td&gt;
&lt;td&gt;MRR dropped, pass_rate unchanged&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Passion projects still need discipline. The interesting work wasn't the chat bubble — it was making scientific archives answerable without lying.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Passion without measurement ships fairy tales. A 20-second eval run changed how I work more than any embedding upgrade.&lt;/li&gt;
&lt;li&gt;Hybrid search (BM25 + vectors + RRF) beat "just use a better model" for scientific text with rare codes.&lt;/li&gt;
&lt;li&gt;The hard problem was never the chat UI — it was making PDF archives answerable without lying.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: assistant output is informational; field decisions require local experts and compliant product labels. CV classification is beta.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>ai</category>
      <category>rag</category>
    </item>
    <item>
      <title>Vector search kept missing rootstock codes, so I went hybrid</title>
      <dc:creator>Kantemir Satibalov</dc:creator>
      <pubDate>Fri, 10 Jul 2026 07:48:43 +0000</pubDate>
      <link>https://dev.to/kantik001/vector-search-kept-missing-rootstock-codes-so-i-went-hybrid-37li</link>
      <guid>https://dev.to/kantik001/vector-search-kept-missing-rootstock-codes-so-i-went-hybrid-37li</guid>
      <description>&lt;h2&gt;
  
  
  Grounded RAG in production (8 Part Series)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/kantik001/i-stopped-trusting-generic-llms-for-horticulture-so-i-built-a-grounded-assistant-on-500-46p9"&gt;I stopped trusting generic LLMs for horticulture — so I built a grounded assistant on ~500 scientific articles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kantik001/68-questions-before-a-single-token-eval-first-rag-1g6g"&gt;68 questions before a single token: eval-first RAG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector search kept missing rootstock codes, so I went hybrid&lt;/strong&gt; — &lt;em&gt;you are here&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Scientific articles aren't FAQ-shaped: chunking a 500-article corpus — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Gate the answer, not just the retrieval: verifying LLM output against sources — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Go for the product, Python for the models: anatomy of a two-service RAG — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Hardening a side project like it's production (and the outage that caused) — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;One RAG platform, swappable domains: what 500 articles taught me about product shape — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;When the eval suite first ran against pure vector search, the failures clustered in one place: &lt;strong&gt;exact identifiers&lt;/strong&gt;. Rootstock codes like "SK-4", cultivar names, dosage lines. Embeddings are great at "this paragraph is about slope planting" and terrible at "this exact token matters more than the topic."&lt;/p&gt;

&lt;p&gt;A typical miss looked like this — vector search returned paragraphs &lt;em&gt;about&lt;/em&gt; rootstocks, but not the cultivar token the eval expected:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5pmuqn33req3qqxu9uf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv5pmuqn33req3qqxu9uf.png" alt=" " width="797" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix wasn't a bigger model. It was accepting that I needed two retrievers with opposite failure modes, and a principled way to merge them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query
  └─ glossary expansion (synonyms appended)
       ├─ Chroma vector search (multilingual-e5-small), top-16, filtered by crop
       └─ BM25 per-crop index, top-16
             └─ RRF merge → candidates
                   └─ cross-encoder rerank (bge-reranker-base) — only for some categories
                         └─ per-article diversification → top-8 fragments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every stage exists because a specific eval failure demanded it. Layer by layer:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embeddings: &lt;code&gt;intfloat/multilingual-e5-small&lt;/code&gt;.&lt;/strong&gt; Multilingual because the corpus is Russian and queries can be either language. One non-obvious gotcha: e5 models &lt;em&gt;require&lt;/em&gt; &lt;code&gt;query:&lt;/code&gt; / &lt;code&gt;passage:&lt;/code&gt; prefixes at query and index time. Without them similarity scores quietly degrade — no error, just worse ranking. I subclassed the embedding wrapper so the prefixes can't be forgotten.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BM25, one index per crop.&lt;/strong&gt; Classic lexical scoring over tokenized chunks. This is what catches "SK-4" — the exact token is either in the chunk or it isn't. The indexes are built at reindex time and persisted alongside the vector store, so a container restart doesn't silently drop the lexical half (an early bug the eval caught: exact-code questions failing while everything else passed).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RRF merge.&lt;/strong&gt; Reciprocal Rank Fusion is embarrassingly simple — each list contributes&lt;br&gt;
&lt;code&gt;1 / (k + rank)&lt;/code&gt; per document, sum, sort (k=60):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rrf_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rankings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ranking&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rankings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk_id&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ranking&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I tried score normalization schemes first. RRF won because it needs no calibration between BM25 scores and cosine similarities — it only trusts &lt;em&gt;ranks&lt;/em&gt; — and it's ten lines you can hold in your head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conditional reranking.&lt;/strong&gt; A cross-encoder (&lt;code&gt;BAAI/bge-reranker-base&lt;/code&gt;) reads the query and each candidate together and re-scores the top candidates. It measurably improves ranking for dense technical questions — and costs real CPU time. So it's category-gated: the question classifier (rule-based, config-driven) tags questions as &lt;code&gt;rootstock&lt;/code&gt;, &lt;code&gt;disease&lt;/code&gt;, &lt;code&gt;variety&lt;/code&gt;, &lt;code&gt;fertilizer&lt;/code&gt;, &lt;code&gt;relief&lt;/code&gt;, or &lt;code&gt;general&lt;/code&gt;, and only the complex categories pay the reranker tax. "When should I water?" doesn't need a cross-encoder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Glossary expansion.&lt;/strong&gt; A curated JSON of domain synonyms; if the query contains a known term, its synonyms are appended to the search string. This is where user vocabulary meets literature vocabulary — the colloquial disease name pulls in &lt;em&gt;Marssonina&lt;/em&gt; spellings the embeddings alone ranked too low. Curated beats automatic here: the glossary is small, reviewable, and each entry exists because a real query missed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diversification.&lt;/strong&gt; Top-ranked chunks tend to come from the same article. The last step caps fragments per source before returning the top-8, so the LLM sees multiple studies instead of one article shredded eight ways.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I didn't do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fine-tune embeddings.&lt;/strong&gt; Tempting, but the eval said ranking was mostly fine once lexical search covered the identifier cases. Not worth the MLOps overhead yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM query rewriting.&lt;/strong&gt; Adds latency and a failure mode to the &lt;em&gt;cheap&lt;/em&gt; half of the system. The glossary covers the actual observed misses deterministically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A vector DB migration.&lt;/strong&gt; Chroma with a persistent local directory is unglamorous and entirely sufficient at ~14.5k chunks. The interesting problems were above the storage layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;With the full stack, the 68-question suite passes at 100% retrieval, and — the part I care about more — hit_rate@3 stays high enough that the LLM's context isn't padded with near-misses (apple suite: MRR 0.938, hit@3 0.953). With &lt;code&gt;--fast&lt;/code&gt; (reranker off) it still passes 68/68 today, which tells me the reranker is currently insurance rather than load-bearing. I keep it because insurance is what you want the week a new corpus batch lands.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>python</category>
    </item>
    <item>
      <title>68 questions before a single token: eval-first RAG</title>
      <dc:creator>Kantemir Satibalov</dc:creator>
      <pubDate>Mon, 06 Jul 2026 08:06:01 +0000</pubDate>
      <link>https://dev.to/kantik001/68-questions-before-a-single-token-eval-first-rag-1g6g</link>
      <guid>https://dev.to/kantik001/68-questions-before-a-single-token-eval-first-rag-1g6g</guid>
      <description>&lt;h2&gt;
  
  
  Grounded RAG in production (8 Part Series)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://dev.to/kantik001/i-stopped-trusting-generic-llms-for-horticulture-so-i-built-a-grounded-assistant-on-500-46p9"&gt;I stopped trusting generic LLMs for horticulture — so I built a grounded assistant on ~500 scientific articles&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;68 questions before a single token: eval-first RAG&lt;/strong&gt; — &lt;em&gt;you are here&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kantik001/vector-search-kept-missing-rootstock-codes-so-i-went-hybrid-37li"&gt;Vector search kept missing rootstock codes, so I went hybrid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Scientific articles aren't FAQ-shaped: chunking a 500-article corpus — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Gate the answer, not just the retrieval: verifying LLM output against sources — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Go for the product, Python for the models: anatomy of a two-service RAG — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Hardening a side project like it's production (and the outage that caused) — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;One RAG platform, swappable domains: what 500 articles taught me about product shape — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;In Part 1 I promised the decision that changed everything. Here it is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I don't touch the LLM until a fixed suite of domain questions passes retrieval.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "the demo looked good." Not "I asked it five things and it answered." A versioned file of questions with expected evidence, run as a regression suite — the same way you'd treat unit tests. Today that suite is 68 questions: 45 apple, 8 pear, 10 plum, and 5 for an HR-policy sandbox that proves the pipeline isn't hard-coded to orchards.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3leuoretfq7gvhtad8kn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3leuoretfq7gvhtad8kn.png" alt=" " width="800" height="112"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why retrieval, not answers
&lt;/h2&gt;

&lt;p&gt;A RAG pipeline fails in two places: the right passage never reaches the prompt, or the model mangles a passage that did. The first failure is cheap to detect and free to test — no API key, no tokens, no flaky LLM in the loop. So the default eval mode stops at retrieval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"crop_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"What are signs of scab?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expect_contains"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"scab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"spot"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expect_context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expect_out_of_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disease"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runner sends the question to the retrieval service (&lt;code&gt;POST/rag/context&lt;/code&gt;) and checks that every &lt;code&gt;expect_contains&lt;/code&gt; substring appears in the combined retrieved context. A few details that turned out to matter more than I expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expect_contains_any&lt;/code&gt;&lt;/strong&gt; for synonyms. The literature writes &lt;em&gt;Marssonina&lt;/em&gt;; users write the colloquial disease name. Either counts as evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Light stemming.&lt;/strong&gt; &lt;code&gt;rootstock&lt;/code&gt; should match &lt;code&gt;rootstocks&lt;/code&gt;. Without it you either overfit the expected strings to one article's phrasing or get false failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;expect_out_of_scope: true&lt;/code&gt;&lt;/strong&gt; questions. A question about, say, car maintenance must return &lt;em&gt;weak or no&lt;/em&gt; context. This catches the embarrassing failure mode where vector search happily returns "closest" chunks for any string whatsoever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The metrics that survived
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pass_rate&lt;/code&gt; alone saturates: once you hit 68/68, it can't tell you whether a refactor made ranking &lt;em&gt;worse&lt;/em&gt; as long as the evidence still sneaks into position 8. So the runner also reports ranking metrics per suite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MRR&lt;/strong&gt; (mean reciprocal rank) of the first relevant fragment,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;hit_rate@1 / @3 / @5&lt;/strong&gt; — did relevant evidence appear in the top-1/3/5 fragments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A fragment counts as relevant when it contains at least one expected substring. That's a single-relevant proxy — the baselines don't carry ground-truth chunk ids — and I'm fine with it: it's cheap, stable, and moves in the right direction when I break something.&lt;/p&gt;

&lt;p&gt;There's also a &lt;code&gt;--full&lt;/code&gt; mode that does call the LLM and reports two more numbers:&lt;br&gt;
&lt;code&gt;verify_pass_rate&lt;/code&gt; (do the numbers in the answer actually appear in the retrieved context — more on that verifier in Part 5) and &lt;code&gt;answer_contains_rate&lt;/code&gt; (does the answer mention the expected terms). Out-of-scope questions skip the LLM entirely, mirroring the production short-circuit: if retrieval finds nothing, we refuse before paying for tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it cheap enough to actually run
&lt;/h2&gt;

&lt;p&gt;An eval nobody runs is documentation. The full HTTP run over 68 questions takes about 4 minutes; that was too slow for "run after every change," so the runner grew flags:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--in-process&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Import the retrieval module directly, skip HTTP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--fast&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disable the cross-encoder reranker (~15× faster; still 68/68 on the current set)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--workers 2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Parallel requests against one retrieval worker&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The ~20-second &lt;code&gt;--in-process --fast&lt;/code&gt; combination is what I run reflexively. The full run with reranking is for before releases and after reindexing. In CI, unit tests run on every PR and the complete eval is a manual GitHub Actions workflow — model downloads are too heavy to justify on every push.&lt;/p&gt;

&lt;p&gt;Results land in &lt;code&gt;eval/results/&amp;lt;timestamp&amp;gt;_&amp;lt;suite&amp;gt;.json&lt;/code&gt;, so "did Tuesday's chunking change hurt pear questions?" is a diff, not an argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the suite caught (a sample)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A chunking change that split experiment tables from their headers — apple &lt;code&gt;pass_rate&lt;/code&gt; dropped 7 points, nothing else moved. Reverted in minutes.&lt;/li&gt;
&lt;li&gt;BM25 index not rebuilt after a corpus update — vector search masked it for common questions, but exact-code questions (rootstock "SK-4") failed instantly.&lt;/li&gt;
&lt;li&gt;A glossary entry that expanded a term too aggressively and pushed the right article out of the top-5 for two questions: visible as an MRR drop with &lt;code&gt;pass_rate&lt;/code&gt; unchanged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsswfjdfv0uh2u047c55o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsswfjdfv0uh2u047c55o.png" alt=" " width="797" height="84"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of these would have been caught by "chat with the bot for a while." All of them would have shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell you to steal
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Write the eval file &lt;strong&gt;before&lt;/strong&gt; tuning retrieval. Even 20 questions change how you work.&lt;/li&gt;
&lt;li&gt;Test retrieval separately from generation. It's the cheap 80%.&lt;/li&gt;
&lt;li&gt;Add out-of-scope questions early. Refusing well is a feature.&lt;/li&gt;
&lt;li&gt;Make the fast path under 30 seconds, or you'll stop running it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Part 3 is the payoff: what it actually took to get those 68 questions passing — hybrid search, RRF, and why "just use a better embedding model" wasn't the answer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: assistant output is informational; field decisions require local experts and compliant product labels.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>testing</category>
    </item>
    <item>
      <title>I stopped trusting generic LLMs for horticulture — so I built a grounded assistant on ~500 scientific articles</title>
      <dc:creator>Kantemir Satibalov</dc:creator>
      <pubDate>Tue, 30 Jun 2026 13:23:58 +0000</pubDate>
      <link>https://dev.to/kantik001/i-stopped-trusting-generic-llms-for-horticulture-so-i-built-a-grounded-assistant-on-500-46p9</link>
      <guid>https://dev.to/kantik001/i-stopped-trusting-generic-llms-for-horticulture-so-i-built-a-grounded-assistant-on-500-46p9</guid>
      <description>&lt;h2&gt;
  
  
  Grounded RAG in production (8 Part Series)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;I stopped trusting generic LLMs for horticulture — so I built a grounded assistant on ~500 scientific articles&lt;/strong&gt; - &lt;strong&gt;you are here&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kantik001/68-questions-before-a-single-token-eval-first-rag-1g6g"&gt;68 questions before a single token: eval-first RAG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kantik001/vector-search-kept-missing-rootstock-codes-so-i-went-hybrid-37li"&gt;Vector search kept missing rootstock codes, so I went hybrid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Scientific articles aren't FAQ-shaped: chunking a 500-article corpus — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Gate the answer, not just the retrieval: verifying LLM output against sources — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Go for the product, Python for the models: anatomy of a two-service RAG — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Hardening a side project like it's production (and the outage that caused) — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;One RAG platform, swappable domains: what 500 articles taught me about product shape — &lt;em&gt;coming soon&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrv75rk1jrufkngalzry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsrv75rk1jrufkngalzry.png" alt=" " width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last year I kept seeing the same pattern in agtech and “AI assistant” demos: a chatbot wrapped around a generic model, a handful of PDFs, and a disclaimer nobody reads.&lt;/p&gt;

&lt;p&gt;I'm a developer, not an agronomist. But I'm working on two related projects — a grounded RAG platform (grounded-llm, private repo) and its first production-shaped domain pack: a horticulture assistant built on hundreds of articles from the Russian journal Plodovodstvo i vinogradstvo Yuga Rossii (apple, pear, plum — on the order of ~500 source articles, not five blog posts).&lt;/p&gt;

&lt;p&gt;I didn't want another “ChatGPT for gardeners.”&lt;br&gt;
I wanted answers that behave like someone who actually read the literature — and admits when the literature doesn't cover the question.&lt;/p&gt;

&lt;p&gt;That gap turned into months of engineering. I'm sharing the story in public; the full corpus and codebase stay private.&lt;/p&gt;

&lt;p&gt;What broke first: “sounds right” ≠ “is right”&lt;br&gt;
Early experiments failed in boring, repeatable ways.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Domain language doesn't match generic retrieval&lt;/strong&gt;&lt;br&gt;
Russian horticulture is full of synonyms and notation variants: rootstock labels, disease names, regional cultivars. A user writes марссониоз; the literature may use Marssonina, abbreviations, or OCR-noisy spellings. Naive retrieval misses; the model fills the gap confidently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Scientific text isn't FAQ-shaped&lt;/strong&gt;&lt;br&gt;
Articles contain experiment sections, tables, and “brief for the grower” blocks. One chunk size for everything → right article, wrong paragraph → fluent wrong answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Generation is the wrong place to fix retrieval&lt;/strong&gt;&lt;br&gt;
If the right passage never reaches the prompt, no system prompt saves you. I separated concerns early:&lt;/p&gt;

&lt;p&gt;Python service → retrieval only (/rag/context)&lt;br&gt;
Go server → sessions, LLM calls, answer cleanup, guardrails&lt;br&gt;
Not because microservices are fashionable — because I needed to change and measure retrieval without redeploying the whole product.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I built: two layers, one product&lt;/strong&gt;&lt;br&gt;
| Layer | What it is |&lt;br&gt;
|-------|------------|&lt;br&gt;
| &lt;strong&gt;Platform core&lt;/strong&gt; (grounded-llm) | Auth, Postgres sessions, orchestration |&lt;br&gt;
| &lt;strong&gt;Domain pack&lt;/strong&gt; (horticulture) | Corpus, crop config, prompts, eval baselines |&lt;/p&gt;

&lt;p&gt;There's also a non-agricultural sandbox (demo_hr) — HR policy docs, same pipeline — to show the platform isn't hard-coded to apple diseases.&lt;/p&gt;

&lt;p&gt;The horticulture pack indexes on the order of ~14,500 text chunks from the journal corpus. At this scale, “vector search only” and “we'll fix it in the prompt” stop being credible.&lt;/p&gt;

&lt;p&gt;I'm not open-sourcing the full article texts (rights + focus). I am sharing architecture lessons, failure modes, and metrics — and offering controlled demos when it's worth someone's time.&lt;/p&gt;

&lt;p&gt;One question that kept me honest&lt;br&gt;
Which rootstocks and training systems show up in slope / terrace planting research for our region?&lt;/p&gt;

&lt;p&gt;Generic LLMs invent varieties and numbers.&lt;br&gt;
A grounded system either retrieves relevant experimental context — rootstocks, spacing, relief, regional trials — or should refuse to answer.&lt;/p&gt;

&lt;p&gt;That requirement ruled out most tutorial RAG stacks I'd seen. It also ruled out marketing photo → disease as the hero feature before a model is actually trained on disease imagery. Vision is on the roadmap; text grounded in papers is what's production-shaped today.&lt;/p&gt;

&lt;p&gt;What I deliberately didn't optimize for (yet):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-tenant SaaS billing&lt;/li&gt;
&lt;li&gt;Viral B2C Telegram growth&lt;/li&gt;
&lt;li&gt;Claiming diagnosis-grade vision from an ImageNet backbone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I optimized for:&lt;br&gt;
1.Retrieval you can regression-test&lt;br&gt;
2.Answers you can gate before users see them&lt;br&gt;
3.A platform you can re-pack for another vertical in days, not months&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next (Part 2)&lt;/strong&gt;&lt;br&gt;
Part 1 was the why.&lt;/p&gt;

&lt;p&gt;Part 2 is the decision that changed everything: I don't trust the pipeline until a fixed suite of domain questions passes retrieval — today 68 questions across apple, pear, plum, and the HR sandbox — before we pay for a single generated token.&lt;/p&gt;

&lt;p&gt;Spoiler: getting there wasn't “use a bigger embedding model.” It was unglamorous engineering — chunking, hybrid search, reranking, glossary expansion — I'll unpack one layer per post.&lt;/p&gt;

&lt;p&gt;If this resonates&lt;br&gt;
I'm building in public through writing, not through dumping the entire corpus on GitHub.&lt;/p&gt;

&lt;p&gt;Follow on Dev.to for Part 2&lt;br&gt;
Comment if you've hit similar RAG failure modes in regulated or scientific domains&lt;br&gt;
Reach out (GitHub / email in bio) for a short demo: HR sandbox or limited horticulture preview&lt;br&gt;
Disclaimer: assistant output is informational; field decisions require local experts and compliant product labels.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>llm</category>
      <category>go</category>
    </item>
  </channel>
</rss>
