惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
aimingoo的专栏
aimingoo的专栏
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
Engineering at Meta
Engineering at Meta
N
Netflix TechBlog - Medium
Blog — PlanetScale
Blog — PlanetScale
大猫的无限游戏
大猫的无限游戏
Vercel News
Vercel News
D
DataBreaches.Net
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
L
LangChain Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Fortinet All Blogs
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
J
Java Code Geeks
Recent Announcements
Recent Announcements
Jina AI
Jina AI
G
Google Developers Blog
腾讯CDC
博客园_首页
博客园 - 【当耐特】

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Building a RAG Pipeline From Scratch: What SmartQueue Tau...
ambarish pathak · 2026-06-18 · via DEV Community

When I set out to add an AI assistant to SmartQueue, a distributed task queue I'd already built in Go for handling IT support tickets, the obvious move was to bolt on an LLM and call it done. Type a question, get an answer. But a generic LLM doesn't know your company's password reset procedure, your P1 outage runbook, or that refunds need manager approval above $500. It needed grounding in actual internal knowledge. That's the job retrieval-augmented generation (RAG) is built for: pull the relevant facts out of your own documents first, then hand them to the model as context instead of trusting it to know your business.

This post walks through how that pipeline actually works, the architectural decision I reversed midway through (and why), the numbers I picked for things like retrieval depth and temperature, and an honest take on whether any of it counts as "real" RAG.

What the assistant actually does

SmartQueue Bot lives inside the Queue Health and AI Bot tabs of the dashboard. An agent picks a ticket, asks a question like "what are the immediate steps for this database outage," and the bot streams back an answer token by token, grounded in a small internal knowledge base of IT runbooks. The request flow looks like this:

agent question
|
v
prompt-injection check (regex guardrails)
|
v
BM25 search over 10 runbooks --> top 4 matches
|
v
system prompt assembled: ticket context + runbook excerpts
|
v
Groq (LLaMA 3.3 70B) streamed via SSE, with last 10 turns of session history
|
v
response streamed to client + written back to Redis session memory

Three things happen before any text reaches the model: the user's message is checked for prompt injection attempts, the message is used as a query against the knowledge base, and the top matches get woven into a system prompt alongside the ticket's category, priority, and description. The model never sees raw documents without that framing. It sees a structured brief.

The decision I reversed: ChromaDB, then BM25

The first version of the knowledge base used ChromaDB with its default ONNX embedding function: proper vector search, no torch dependency, queried through a thread pool so it wouldn't block the event loop. That's the textbook RAG setup, and it worked locally. It fell apart the moment I tried to deploy the whole stack as a single container on Hugging Face Spaces.

The deployment used supervisord to run Redis, the Go API, two Go worker replicas, and the FastAPI AI service all inside one container, and originally a separate ChromaDB process alongside them. That's five long-running processes competing for a small amount of memory and CPU in a free-tier container, with supervisord responsible for starting them in the right order and keeping them alive. ChromaDB was the one that kept causing startup races and silent failures. After enough commits with messages like "fix: remove ChromaDB from supervisord" and "fix: replace ChromaDB with in-memory BM25 search," I made the call to rip it out entirely.

The replacement is about 50 lines of pure Python, with no embedding model, no external process, and no network call:

def _bm25_score(query_tokens, doc_tokens, k1=1.5, b=0.75):
    avg_dl = sum(len(d) for d in _CORPUS) / len(_CORPUS)
    tf = Counter(doc_tokens)
    score = 0.0
    for term in query_tokens:
        if term not in tf:
            continue
        idf = _idf(term, _CORPUS)
        dl = len(doc_tokens)
        score += idf * (tf[term] * (k1 + 1)) / (tf[term] + k1 * (1 - b + b * dl / avg_dl))
    return score

This is the standard Okapi BM25 formula, computed fresh against the in-memory runbook corpus on every query. No index to build, no daemon to keep alive, no embedding latency on cold start. The trade-off is real: BM25 only matches on term overlap, so a query phrased very differently from the runbook's wording (synonyms, paraphrasing) won't score well. But for a fixed set of 10 short, keyword-dense IT runbooks where users are typically searching with the same vocabulary the runbooks use ("VPN," "password reset," "outage"), that weakness barely shows up in practice. The thing that mattered more than retrieval quality at this scale was that the service now starts reliably every single time.

The numbers, and why those numbers

A few of the constants in this pipeline were deliberate tuning decisions rather than defaults I left untouched. None of this is a RAGAS-style evaluation with precision/recall/faithfulness scores. There's no eval harness here, just systems-level tuning based on the constraints I was working under (a free-tier LLM provider, a single demo container, and a knowledge base that doesn't change).

Constant Value Why
Retrieved docs (k) 4 Enough runbook context to usually cover the right answer without bloating the prompt against the 800-token response budget
BM25 k1 / b 1.5 / 0.75 Standard Robertson defaults, since with only 10 documents there isn't enough signal to meaningfully tune these per-corpus
Bot temperature 0.2 Troubleshooting answers should be literal and repeatable, not creative
Classifier temperature 0.1 Output is parsed as JSON; near-deterministic reduces malformed responses
Recommender temperature 0.3 Slightly more room since it's reasoning over queue state, not just extracting fields
Bot max_tokens 800 Long enough for multi-step troubleshooting guidance, short enough to keep streaming snappy
Classifier max_tokens 250 The schema is small, just eight short fields and no prose
Session history window last 10 turns, capped at 20 stored, 1-hour TTL in Redis Enough continuity for a real troubleshooting conversation without memory growing unbounded
Rate limit 30 requests/minute per session Protects the free Groq quota from being burned by a single runaway client
LLM client retries 0, with a 10s timeout Every caller already has its own fallback (keyword classifier, rule-based recommender, canned bot response), so retrying into the same failure just adds latency before falling back anyway

That last one is worth dwelling on. Every AI-backed endpoint in this system has a non-LLM fallback path. If Groq is rate-limited or down, the classifier falls back to keyword matching, the recommender falls back to threshold-based rules on queue depth, and the bot falls back to a templated response built from the same retrieved runbook excerpts. The system was designed to degrade, not fail, which matters a lot more when you're running on a free API tier than it would on a paid, SLA-backed one.

Is this actually "RAG," and is it better?

Strictly, yes: it retrieves before it generates, and the generation is conditioned on what's retrieved. But it's a narrow slice of what RAG can mean. There's no chunking (each runbook is embedded as one flat document), no re-ranking step, no hybrid retrieval, and no evaluation loop telling me whether the right runbook actually got surfaced for a given question. It's RAG sized correctly for the problem: a small, static, keyword-friendly knowledge base where the cost of building anything more elaborate would have outweighed the benefit.

Whether BM25-over-ChromaDB was "better" depends on what you're optimizing for. For retrieval quality on a larger, more varied corpus, an embedding-based approach would win, since BM25 degrades once questions stop reusing the document's own vocabulary. But for this deployment, with this knowledge base size and this hosting constraint, dropping the vector store was unambiguously the right call: it eliminated an entire class of deployment failures and removed a dependency for a problem that ten short documents don't actually need solved with embeddings.

If I were extending this rather than rebuilding it, the next real upgrades would be a basic retrieval eval (even just "did the correct runbook end up in the top 4 for a labeled set of test questions"), splitting the longer runbooks into smaller chunks so the model gets more relevant text per retrieved slot, and a hybrid approach once the knowledge base grows past roughly fifty documents. Somewhere around that scale, pure keyword overlap stops being enough to catch the paraphrased queries vector search handles for free.

That's also roughly the direction I went on a separate project, AskMyDoc, where I paired BM25 with ChromaDB in a hybrid retriever, added HyDE-style query rewriting to bridge the vocabulary gap, and built a RAGAS-based evaluation harness to actually measure retrieval quality instead of eyeballing it. SmartQueue's BM25-only pipeline was the right tool for a ten-document, single-container helpdesk demo. It's not the pipeline I'd reach for if the knowledge base were a thousand documents instead of ten, but knowing the difference, and being able to justify it with a real deployment failure rather than a hunch, is the actual lesson this project taught me.