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

推荐订阅源

C
CXSECURITY Database RSS Feed - CXSecurity.com
K
Kaspersky official blog
A
Arctic Wolf
Attack and Defense Labs
Attack and Defense Labs
L
LINUX DO - 热门话题
N
News | PayPal Newsroom
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
L
Lohrmann on Cybersecurity
PCI Perspectives
PCI Perspectives
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Last Watchdog
The Last Watchdog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
W
WeLiveSecurity
Know Your Adversary
Know Your Adversary
博客园 - Franky
T
Tenable Blog
T
Tailwind CSS Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Help Net Security
Help Net Security
WordPress大学
WordPress大学
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
H
Heimdal Security Blog
TaoSecurity Blog
TaoSecurity Blog
S
Security Affairs
J
Java Code Geeks
小众软件
小众软件
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Apple Machine Learning Research
Apple Machine Learning Research
NISL@THU
NISL@THU
O
OpenAI News
The Cloudflare Blog
月光博客
月光博客
Google Online Security Blog
Google Online Security Blog
V
V2EX
罗磊的独立博客
美团技术团队
博客园 - 三生石上(FineUI控件)
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
Cyber Attacks, Cyber Crime and Cyber Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
Hacker News - Newest:
Hacker News - Newest: "LLM"
大猫的无限游戏
大猫的无限游戏

Hacker News: Show HN

PurrrrrFocus: Pomodoro Timer App - App Store Workflow Engine — Multi-Step Orchestration for Bun RapidPhoto: Pro Photo Editor App - App Store GitHub - DheerG/swarms: Achieve extraordinary results with claude code across a variety of tasks SPICE simulation → oscilloscope → verification with Claude Code — Lucas Gerads Show HN: VCoding – A 5 MB native Windows IDE with no dynamic dependencies Show HN: LLMs don't hallucinate because they're bad at math, it's the format GitHub - Agent-FM/agentfm-core: AgentFM is a peer-to-peer network that turns everyday computers into a decentralized AI supercomputer. AgentFM lets you run massive AI workloads directly across a global mesh of idle CPUs and GPUs. Show HN: Tracking Top US Science Olympiad Alumni over Last 25 Years GitHub - Potarix/agent-hub: One place to talk to all your agents Show HN: Runtime security for AI agents(injection,tool abuse, data exfiltration) GitHub - dubeyKartikay/lazyspotify: Terminal Spotify client for macOS and Linux GitHub - the-banana-tool/king-louie: Easy to use GUI Personal AI Assistant. Win/Linux/Mac. Show HN I made my vacation rental bookable by AI agents–no Airbnb, 0% commission GitHub - basteez/jsf-autoreload: maven plugin to enable hot reload on jsf projects uvm32/hosts/host-gdbstub at main · ringtailsoftware/uvm32 GitHub - labsai/EDDI: Config-driven engine that turns JSON into production-grade AI agents. Multi-agent orchestration, 12+ LLM providers, MCP/A2A protocols, RAG, persistent memory, and enterprise compliance (EU AI Act, GDPR, HIPAA). Built on Quarkus. GitHub - glitchnsec/fortyone-oss: AI Executive Assistant Platform Quickstart | Alien GitHub - muxshed/shed: One stream in, or many. Every destination, simultaneously. No cloud middleman, no per-channel fees, no limits. GitHub - ocrbase-hq/ocrbase: 📄 PDF/IMG ->.MD/JSON Document OCR API for PaddleOCR and GLMOCR. Self-hostable. GitHub - impactjo/home-memory: MCP server that lets your AI assistant remember everything about your home. GitHub - Sets88/dbcls: DbCls is a powerful terminal database client that supports various databases GitHub - neptun2000/heor-agent-mcp GitHub - SeanFDZ/macmind: Single-layer transformer in HyperTalk for the classic Macintosh RollQuation: Math Puzzles - Apps on Google Play GitHub - dropbox/witchcraft Show HN: Agent-cache – Multi-tier LLM/tool/session caching for Valkey and Redis GitHub - opentalon/opentalon: OpenTalon is an open-source platform built from the ground up in Go as a robust alternative to OpenClaw LinkedIn™ 职位抓取工具 - Chrome 应用商店 GitHub - EdoardoBambini/Agent-Armor-Iaga: AI agents are getting tool access — shell, file system, databases, APIs, secrets. But **nobody is governing what they actually do with it**. Frameworks like LangChain, CrewAI, AutoGen, and Claude Code give agents the power to execute. Agent Armor gives you the power to control, audit, and approve every single action before it happens. HN Vibes — Week 15, Apr 7–13 2026 GitHub - chojs23/ec: Easy terminal-native 3-way git mergetool vim-like workflow GitHub - SethPyle376/hiraeth: Local AWS emulator focused on fast integration testing, with SQS support, SQLite-backed state, and a debug-friendly web UI. GitHub - JakOb-dotcom/cloud-sandbox-security-analysis: Technical analysis and Proof of Concept (PoC) regarding environment variable exfiltration in containerized cloud sandboxes via side-channel data leaks. Springboards - Flint Alpha Show HN: A simpler coding agent harness GitHub - audiodude/sudomake-friends GitHub - 256thFission/mini-mythos: OSS clone of Anthropic’s Mythos harness to locate C/C++ memory vulnerabilities Show HN: OpenParallax: OS-level privilege separation for AI agent execution Hacker News Sorted - Chrome 应用商店 Show HN: How to Install Docker on Ubuntu 24.04 LTS: Complete 2026 Guide GitHub - himanshudongre/smriti GitHub - sverrirsig/claude-control: macOS desktop dashboard for monitoring and managing multiple Claude Code sessions GitHub - ory/dockertest: Write better integration tests! Dockertest helps you boot up ephermal docker images for your Go tests with minimal work. Chiral - Chrome 应用商店 Show HN: Two Claudes collaborating through shared memory on a $100 mini-PC GitHub - pmichaillat/latex-cv: Minimalist LaTeX template for academic CVs GitHub - oguzbilgic/posse: A web UI for Anthropic Managed Agents. GitHub - sshiraz/depsly: Dependency risk analysis tool for npm packages ABI Add safari/agent-harness — Safari browser automation via safari-mcp by achiya-automation · Pull Request #212 · HKUDS/CLI-Anything GitHub - Halfblood-Prince/trustcheck: Verify PyPI package attestations and improve Python supply-chain security GitHub - oguzbilgic/kern-ai: Agents that do the work and show it. GitHub - bruits/satteri: High-performance Markdown and MDX processing for the JavaScript ecosystem GitHub - tylergibbs1/feedstock: High-performance web crawler and scraper for TypeScript, powered by Bun and Playwright GitHub - Grimm67123/grimmbot: The self-improving sandboxed and open-source AI agent. With persistent memory and scheduling. GitHub - whitevanillaskies/whitebloom: Local whiteboard that blooms. GitHub - hwdsl2/docker-whisper: Docker image for a self-hosted Whisper speech-to-text server with speaker diarization and OpenAI-compatible transcription and translation APIs. Powered by faster-whisper. Supports all Whisper models, NVIDIA GPU (CUDA) acceleration, JSON/SRT/VTT output, SSE streaming, offline mode, and multi-arch (amd64, arm64). GitHub - yisding/reviewwiggum GitHub - MarwanAlsoltany/serrors: Structured errors for Go: sentinel hierarchies, typed data, custom formatting, and slog integration. GitHub - soatok/age-php GitHub - Luthiraa/markitme GitHub - stagas/rtdiff: realtime git diff gui and AI-assisted commits GitHub - tombedor/excalicharts GitHub - wh1le/excalidraw-edit: Open and edit .excalidraw files from the terminal. Offline, auto-saves to disk. MalExt Sentry - Malicious Extension Scanner - Chrome 应用商店 GitHub - syi0808/asciianimesvg: Generate animated ASCII art SVGs from text. CLI, Rust library, WASM, and web editor. GitHub - zaina-ml/ml_forge: A visual-based graph node editor for training computer vision models. GitHub - anakin87/llm-rl-environments-lil-course: 🌱 A little course on Reinforcement Learning Environments for evaluating and training Language Models GitHub - takaakit/superpowers-uml: Superpowers-UML modifies Superpowers to ensure a software development workflow in which AI agents design through UML modeling. AdriByte Studio - Sviluppo Web e Soluzioni Digitali GitHub - chouligi/angel-copilot: Your personalized Angel Investment Advisor Show HN: MoodSense AI (ML and FastAPI and Gradio, Deployed on Hugging Face) Moodsense Ai - a Hugging Face Space by aman179102 GitHub - agenteractai/lodmem: Level Of Detail Context Management for Agents GitHub - ostefani/subnetlens: A fast, concurrent network scanner with a TUI and plain-text CLI, built in Go. It discovers live hosts on your network, scans their open ports, resolves hostnames, and fingerprints operating systems—delivered. Cyber Pulse: Agentic Intel - Apps on Google Play Whisper API: Self-Hostable Speech to Text Transcription The Agent-Web Protocol Stack: A Research Thesis GitHub - msmarkgu/RelayFreeLLM: A restful API designed to route user prompts to various AI model providers. Show HN: Provepy – A Python decorator that proves your code using Lean and LLMs Show HN: Pardonned.com – A searchable database of US Pardons GitHub - patrickdappollonio/dux: Dux is a terminal UI that lets you run multiple AI coding agents side by side, each in its own git worktree, with full companion terminals, macros, commit generation, and a command palette that knows more tricks than you do. kMC Crystal Simulator Show HN: HyperFlow – A self-improving agent framework built on LangGraph GitHub - stef41/vibescore: 🎵 Grade your vibe-coded project. One command, instant letter grade across security, quality, dependencies, and testing. GitHub - stef41/lmscan: 🔍 Detect AI-generated text and fingerprint which LLM wrote it. Open-source GPTZero alternative. Zero dependencies, works offline. imgur.com GitHub - visionscaper/collabmem: Enabling long-term collaboration with Agentic AI - building up episodic and world model memory over time with in-context awareness 在 Steam 上购买 FriedrichAI: Offline AI 立省 10% GitHub - atripati/ark: AI Runtime Kernel — a context operating system for AI agents. Eliminates tool bloat, loads only what’s needed, and gives LLMs their reasoning space back. GitHub - nowork-studio/toprank: Open-source Claude Code skills for SEO, SEM, Google Ads GitHub - tacomanator/sash: Lightweight macOS menu bar app for reliably cycling through windows of the current application. Appents | Social Media Management for Product-First Teams GitHub - pnhoang/youtube-spam-blocker: Automatically detects and hides spam messages in YouTube Live chat. Set rate limits, keyword filters, and block repeat offenders. GitHub - decisionnode/DecisionNode: CLI + Local MCP - A shared structured memory store across Claude Code, Cursor, Windsurf, Antigravity, and every MCP client. Semantically queryable. GitHub - AvaCodeSolutions/django-email-learning: An open source Django app for creating email-based learning platforms with IMAP integration and React frontend components. The $100K Gap in Kubernetes Security Tooling Function Calling Harness: From 6.75% to 100%
How to build a RAG pipeline with Weaviate and Ollama
edodusi · 2026-04-27 · via Hacker News: Show HN

Welcome to the final part of our fascinating journey into AI-assisted search. This article is the last in a three-part series designed to demystify vector databases.

  • Part 1: The “What & Why”: a conceptual introduction to what a vector database is, why it's a game-changer, and its core use cases.
  • Part 2: The “How”: a practical, hands-on tutorial where we built a project to demonstrate the technology in action.
  • Part 3: The “Real World” (you are here): a hands-on guide to building a retrieval-augmented generation (RAG) pipeline: hybrid search, intelligent chunking, and turning search results into AI-generated answers.

From lists to answers

Part 1 explored the world of vector embeddings: turning content into coordinates on a meaning map. Part 2 focused on building a local semantic search engine with Weaviate and Node.js.

This part goes one step further to deliver a better user experience.

The thing about search results is that they're a list. The user asks a question, and gets links to pages that include the search term. That's useful, but it's how search worked in 2018. What they actually want is an answer.

To close that gap, turn your search engine into an AI assistant that answers questions grounded in relevant content, and does it locally: no API keys, no cloud dependencies.

Here's what you’ll build:

  • A hybrid search retrieval function (an upgrade to the pure vector search)
  • A prompt template that grounds the LLM in your content
  • A streaming generation pipeline with Ollama
  • Intelligent chunking that guarantees quality answers
  • Relevance thresholds that prevent hallucination

Hint:

Already familiar with RAG and ready to start building? Skip right to the coding part.

The two librarians

Before you write any code, let me paint a picture.

Imagine you walk into a library in search of knowledge. The librarian greets you. They’ve read thousands of books over the years and memorized all of them by heart. You ask, “What's the best framework for server-side rendering?” The librarian smiles confidently and answers from memory. They even email you the framework’s name and URL. Later, you log into your work device, type in the URL, and, well, nothing. Turns out that the knowledgeable librarian confidently recommended a framework that doesn't exist. They weren’t lying; they genuinely believed they were right. They just...misremember things.

That's an LLM without RAG: remarkable recall, questionable accuracy.

Now imagine a different librarian. Same question. But instead of answering from memory, this one says, "Let me check." They walk over to the shelves, pull out three relevant books, flip to the right chapters, read them, and then answer your question, citing specifically relevant pages.

That's an LLM with RAG. The shelves are your vector database from part 2. In this part, you bring in the librarian.

What is RAG, really?

As the name suggests, retrieval-augmented generation is a three-step technique:

  1. Retrieval: search your vector database for content relevant to the question
  2. Augmentation: insert that content into the LLM's prompt as context
  3. Generation: enable the LLM to answer based on this fresh context, not the outdated training data

RAG doesn't make the LLM smarter, it makes it informed. And that's a crucial distinction. LLMs are frozen in time at their training cutoff, but your content changes constantly, maybe daily. RAG bridges the gap by giving the model up-to-date, relevant context at query time.

The beauty of this architecture is its simplicity. Each step is independent: this tutorial uses Weaviate for retrieval, Qwen 3.5 on Ollama for generation, and Node.js to wire it all together and run the JavaScript scripts. But you can swap your vector database, change your prompt template, or upgrade your LLM (if you have enough VRAM, and given today's market, I doubt it), all without rebuilding the pipeline from scratch.

Case in point, you could skip Weaviate and use Ollama for embeddings. However, since this tutorial builds directly on the previous part, Weaviate is already set up with text2vec-transformers and the relevant data. Switching would mean re-ingesting all articles with different vectors, so we keep what works and add what's missing.

  • Weaviate still handles embeddings with the same text2vec-transformers model that turns content and queries into vectors for search.
  • Ollama's job is to run LLMs that generate natural language answers from retrieved context.

Two models, two roles, one pipeline.

Let's get to it.

Required:

This tutorial builds directly on the previous part, so you need the Weaviate instance and ingested articles from that tutorial. The complete code is available on GitHub.

Set up Ollama

In part 2, you kept everything local with Docker. Same philosophy here: Ollama lets you run LLMs on your machine with zero cloud dependencies.

Why not use an API like Claude or GPT? You're absolutely right! For production, that's often the right call. For learning, local is better: you manage every piece, control every parameter, and don't worry about rate limits or costs while experimenting. Also, maybe you have sensitive data that you don't want to send to a cloud platform, and you can set up an on-premises solution with the same tools.

Install Ollama following the steps for your platform, then pull the Qwen 3.5 4b model:

ollama pull qwen3.5:4b

Qwen 3.5 4b is a 3.4GB model that runs on a laptop, handles 256K tokens of context, and produces solid answers from retrieved content. We'll upgrade to a larger model later to see what changes.

Verify it's running:

ollama list
# NAME          ID              SIZE      MODIFIED
# qwen3.5:4b    2a654d98e6fb    3.4 GB    Just now

Connect to Ollama from Node.js (full version):

./scripts/0-test-ollama.js

import ollama from "ollama";

const response = await ollama.chat({
  model: "qwen3.5:4b",
  messages: [{ role: "user", content: "Say hello in one sentence." }],
});

console.log(response.message.content);

If you see a greeting in the console, you're good.

Hint:

If your content lives in Storyblok, you can fetch articles via the Content Delivery API and feed them into the same pipeline. As we explain later in this tutorial, the component-based structure gives you even better chunking out of the box.

Build the RAG pipeline

This is the core of the article, five steps with working examples. The snippets below are extracted from the complete pipeline in scripts/5-ask.js. Treat the snippets as a reference implementation: understand how each piece works, then write a version tailored to your data and use case.

1. The retrieval function

To implement a great search engine that gives useful answers, you first need to improve the retrieval from the vector database.

The earlier solution used nearText for pure vector search. It works, but it has a blind spot: specific terms. Try searching for an error code like ERR_MODULE_NOT_FOUND or a version number like v4.14.0. Vector search treats these as meaningless noise because they carry no semantic weight.

The fix is a hybrid search: combine vector similarity with keyword matching (BM25). Weaviate supports this natively with a single alpha parameter that controls the balance: 0 means pure keywords, 1 means pure vectors, and anything in between blends both signals.

Check the following snippet:

./scripts/5-ask.js

import weaviate from "weaviate-client";
import ollama from "ollama";

const client = await weaviate.connectToLocal();
const collection = client.collections.get("Article");

async function retrieve(question, limit = 5) {
  const results = await collection.query.hybrid(question, {
    limit,
    alpha: 0.75, // That's the balance control
    returnMetadata: ["score"],
    returnProperties: ["title", "body", "description"],
  });

  return results.objects;
}

The alpha: 0.75 leans toward semantic understanding but doesn't ignore keywords. In practice, this catches both conceptual matches ("How do I set up internationalization?") and exact matches ("ERR_MODULE_NOT_FOUND").

Here's the same query ("v4", imagine a user is just using this keyword) with both approaches to demonstrate the difference:

./scripts/4-search.js

const query = process.argv[2] || "How do I set up Storyblok with Next.js?";

// Pure vector search (Part 2 approach)
const vectorResults = await collection.query.nearText(query, {
  limit: 3,
  returnMetadata: ["distance"],
  returnProperties: ["title"],
});

// Hybrid search (Part 3 approach)
const hybridResults = await collection.query.hybrid(query, {
  limit: 3,
  alpha: 0.75,
  returnMetadata: ["score"],
  returnProperties: ["title"],
});

Run it with a keyword-heavy query:

node scripts/4-search.js "v4"
--- nearText (pure vector) ---
Query: "v4"

  📄 You're Not Building Netflix: Stop Coding Like You Are
  📄 Nobody Writes Clean Code. We All Just Pretend
  📄 Developers vs AI: Are We Becoming AI Managers Instead of Coders?

--- hybrid (vector + BM25) ---
Query: "v4"

  📄 Introducing Storyblok CLI v4
  📄 You're Not Building Netflix: Stop Coding Like You Are
  📄 Nobody Writes Clean Code. We All Just Pretend

The vector search is completely lost: "v4" carries no semantic meaning, so it returns irrelevant articles. Meanwhile, the hybrid search detects the literal keyword match and surfaces "Introducing Storyblok CLI v4" at the top. That's BM25 doing its job with the vector component still contributing to the overall ranking.

2. The prompt template

Retrieval gives you context. Now you need to package it for the LLM. Like the imaginary librarian, you literally take the sources from the vector database and augment the prompt, turning it into a powerful command that guides the LLM to craft a meaningful answer for your users.

This is the augmentation step, and the system prompt is arguably the most important piece of code in the entire pipeline. Get it wrong, and the model ignores the context and hallucinates. Get it right, and you have an assistant that stays grounded, cites its sources, and admits when it doesn't know:

./scripts/5-ask.js

function buildPrompt(question, sources) {
  const context = sources
    .map(
      (source, i) =>
        `[Source ${i + 1}: ${source.properties.title}]\n${source.properties.body.slice(0, 2000)}`
    )
    .join("\n\n---\n\n");

  return {
    system: `You are a helpful assistant that answers questions based ONLY on the provided context.
If the context does not contain enough information to answer the question, say so clearly.
When you use information from a source, cite it by number (e.g., [Source 1]).
Keep answers concise and practical.`,
    user: `Context:\n${context}\n\nQuestion: ${question}`,
  };
}

Every word in that system prompt is there for a reason. Let's break it down:

  • “Based ONLY on the provided context”: this is the anti-hallucination instruction. Without it, the model happily fills in gaps from its training data, which defeats the entire purpose of RAG.
  • Numbered sources: formatting the context with clear [Source N] labels lets the model cite its references. This makes every claim in the answer verifiable.
  • The “say so” escape hatch: explicitly telling the model to say "I don't know" reduces its inherent behavior to fabricate answers. LLMs are trained to be helpful, which means they'll invent something rather than stay silent. You need to give it explicit permission to decline.

After the system prompt, concatenate the sources with the user’s question. This is the magic of augmentation.

3. The generation call

Now it’s time to send the assembled prompt to Ollama and get an answer back. Let's start simple, with a blocking call, then upgrade to streaming:

./scripts/5-ask.js

import ollama from "ollama";

async function generate(prompt) {
  const response = await ollama.chat({
    model: "qwen3.5:4b",
    messages: [
      { role: "system", content: prompt.system },
      { role: "user", content: prompt.user },
    ],
  });

  return response.message.content;
}

This works, but the experience is terrible. You wait several seconds staring at a blank screen, then the entire answer emerges at once. Streaming transforms the engine into something that simulates an AI assistant:

./scripts/5-ask.js

import ollama from "ollama";

 async function generate(prompt) {
 async function generateStream(prompt, model = "qwen3.5:4b") {
  const response = await ollama.chat({
  const stream = await ollama.chat({
    model: "qwen3.5:4b", 
    model,
    messages: [
      { role: "system", content: prompt.system },
      { role: "user", content: prompt.user },
    ],
    think: false,
    stream: true,
  });

  return response.message.content;
  for await (const chunk of stream) {
    process.stdout.write(chunk.message.content);
  }
  console.log();
}

Notice the most important changes here: stream: true and a for await loop. The tokens flow in as the model generates them, exactly like ChatGPT, but running on your laptop.

4. Assemble all the pieces

Let's wire retrieval, prompt building, and generation into a single function:

./scripts/5-ask.js

async function askQuestion(question, model) {
  console.log(`\n🔍 Searching for: "${question}"\n`);

  // Step 1: Retrieve
  const sources = await retrieve(question, 5);
  console.log(
    `📚 Found ${sources.length} sources:`,
    sources.map((s) => s.properties.title)
  );

  // Step 2: Augment
  const prompt = buildPrompt(question, sources);

  // Step 3: Generate
  console.log(`\n💬 Answer (${model}):\n`);
  await generateStream(prompt, model);
}

// Read question + model from CLI args, with sensible defaults
const question = process.argv[2] || "How do I set up Storyblok with Next.js?";
const model = process.argv[3] || "qwen3.5:4b";

await askQuestion(question, model);
client.close();

Question in, grounded answer out. Three steps, clearly separated, easy to debug. The second CLI argument lets us swap models without touching the code, which we'll use in the model comparison section below.

5. Run it

Test your implementation with a real question. Remember that LLMs are non-deterministic, so the answer you’ll get could be different.

node scripts/5-ask.js "How do I set up Storyblok with Next.js?"
🔍 Searching for: "How do I set up Storyblok with Next.js?"

📚 Found 5 sources: [
  'Announcing React SDK v4 with full support for React Server Components',
  'Announcing Official Storyblok Richtext Support in our Frontend SDKs',
  'Announcing Stable Live Preview for Storyblok’s Astro SDK',
  'Introducing Storyblok CLI v4',
  'Announcing Storyblok Svelte SDK v5: now fully compatible with Svelte 5'
]

💬 Answer (qwen3.5:4b):

Based on the provided context, here's how to set up Storyblok with Next.js:

1. Install the SDK: `npm install @storyblok/react@latest` [Source 2]
2. Initialize the SDK with a `getStoryblokApi()` function in `lib/storyblok.js` [Source 1]
3. For App Router with React Server Components, ensure you're on v4.0.0+ which provides full RSC support with live preview [Source 1]

The answer is much more useful: it cites specific sources, draws from multiple articles to compose a complete response, and gets everything right, because it's reading your content, not guessing from training data.

But it can be even better.

Chunking: where most RAG pipelines fail

Look back at the answer we got in step 5. It's grounded in the right articles, cites sources, and isn’t hallucinating. But why does it feel…generic?

The model retrieved the correct articles, but the embeddings averaged across 3,000 words, diluting the specific setup steps. You’re forced to keep probing instead of getting started with integrating Storyblok into your Next.js project.

The problem is embedding dilution. When you vectorize an entire 3,000-word article, the result represents the average meaning of the whole piece. It's like describing a book by its most frequent word. The specific paragraph about cache TTL gets buried under the other 2,900 words about API authentication, rate limits, and SDK setup.

The fix is chunking: break documents into smaller, focused pieces before vectorizing them.

Naive chunking: the random way

Most tutorials suggest splitting text every 500 tokens (or every X tokens). Fast, simple, and useless. It slices mid-sentence, mid-paragraph, mid-thought. The resulting vectors are semantic smoothies, blended beyond recognition.

Heading-based chunking: the practical approach

Our data is in Markdown (DEV.to articles from part 2), so we can do better: split by headings. Each section under an ## or ### heading is a coherent chunk with a clear topic (full version):

./scripts/3-ingest-chunks.js

function chunkByHeaders(article) {
  const sections = article.body.split(/(?=^#{2,3}\s)/m);

  return sections
    .filter((section) => section.trim().length > 50)
    .map((section, i) => ({
      title: article.title,
      chunkIndex: i,
      body: section.trim(),
      originalUrl: article.url,
      author: article.author,
    }));
}

Now re-ingest the chunked content into Weaviate. The script creates a fresh ArticleChunk collection, flattens every article into its chunks, and batches the inserts so we don't spam the DB one object at a time:

./scripts/3-ingest-chunks.js

import { readFileSync } from "fs";

const data = JSON.parse(readFileSync("./data/articles.json", "utf-8"));
const chunks = data.flatMap(chunkByHeaders);
console.log(`Chunked ${data.length} articles into ${chunks.length} chunks`);

const objects = chunks.map((chunk) => ({ properties: chunk }));

// Batch insert
const batchSize = 20;
for (let i = 0; i < objects.length; i += batchSize) {
  const batch = objects.slice(i, i + batchSize);
  await client.collections.get("ArticleChunk").data.insertMany(batch);
}

Run the chunking script:

node scripts/3-ingest-chunks.js
Created ArticleChunk collection
Chunked 20 articles into 297 chunks
Ingested 297 chunks

20 articles became 297 focused chunks. To experience the difference, use a helper file: 6-compare-chunking.js. It's a trimmed variant of 5-ask.js that runs the same question through both collections (Article and ArticleChunk) and prints both answers back-to-back.

Let's run it:

node scripts/6-compare-chunking.js "How do I set up Storyblok with Next.js?"

The whole articles’ answer comes out first and looks close to the one from step 5: correct at a high level, but vague. Now look at the chunked articles’ answer:

💬 Answer (qwen3.5:4b  chunked articles):

1. Install the SDK: `npm install @storyblok/react@latest` [Source 5]

2. Initialize the SDK in `lib/storyblok.js`:

   import { apiPlugin, storyblokInit } from '@storyblok/react/rsc';

   export const getStoryblokApi = storyblokInit({
     accessToken: 'YOUR_ACCESS_TOKEN',
     use: [apiPlugin],
     components: { teaser: Teaser, page: Page },
   });

   [Source 1]

3. Wrap your app with `StoryblokProvider` in `app/layout.jsx` [Source 1]

4. Create the Provider as a client component with `'use client'` [Source 1]

5. Fetch and render content using `getStoryblokApi()` in server components
   with `StoryblokStory` [Source 1]

Same model, same question, but the chunked version features storyblokInit, StoryblokProvider, the 'use client' directive, server component patterns with StoryblokStory, and actual working setup steps pulled from focused sections, not averaged across entire articles.

The chunk-level embeddings captured the precise meaning of each section instead of diluting it.

Structure-aware chunking: the CMS advantage

For plain Markdown, heading-based splitting works well. But if your content lives in a CMS with a component model, you can do much better.

Consider how content is structured in Storyblok: a typical article has a title component, an introduction block, multiple section blocks (each with its own heading and body), code blocks, and a conclusion.

Each component is a self-contained semantic unit with clear boundaries. No need to invent a structure for chunking—the content architecture is the semantic structure.

Strategy

Pros

Cons

Best for

Fixed-size (every N tokens)

Simple, predictable

Ignores context boundaries

Prototypes only

Heading-based

Respects document structure

Misses nested semantics

Markdown, flat files

Structure-aware (CMS components)

Preserves semantic boundaries

Requires content model knowledge

Structured CMS content

This is one of the underappreciated advantages of structured content in the AI era: your component model gives you chunking for free.

Model swap: when bigger isn't better

You might be thinking, “Sure, chunking helped, but what if I just use a bigger model with the whole articles? More parameters must generate better answers.”

To test that, keep the whole-article retrieval (no chunks) and upgrade the model instead:

ollama pull qwen3.5:9b

Ask the bigger model the same question:

node scripts/5-ask.js "How do I set up Storyblok with Next.js?" "qwen3.5:9b"
💬 Answer (qwen3.5:9b  whole articles):

1. Install the SDK: `npm install @storyblok/react@latest` [Source 1][Source 2]
2. Initialize the SDK: Create a file (e.g., `lib/storyblok.js`) to initialize the SDK and export the `getStoryblokApi()` function.
3. Use Rich Text Components: render rich text fields using the `StoryblokRichText` component [Source 2].
4. For Live Preview: version 4.0.0+ provides full support for live preview functionality [Source 1].

The qwen3.5:9b took noticeably longer to generate and used much more memory. And the result? Roughly the same generic answer as the qwen3.5:4b from step 5. Compare that with the chunked version that included storyblokInit, StoryblokProvider, the 'use client' directive, proper file paths, and server component fetching with StoryblokStory.

The bigger model still relies on diluted whole-article embeddings. It can't synthesize what isn't in the context.

In RAG, the most impactful improvements come from how you prepare and retrieve context, not from throwing a bigger model at the problem. Chunking strategy, hybrid search tuning, and prompt design are all human engineering decisions that outweigh model size. You don't need the most expensive model. You need smarter retrieval, better chunking, and a well-crafted system prompt.

Hint:

This is equally true for cloud APIs. You don't need to call the most expensive frontier model if your retrieval pipeline is solid. A smaller, cheaper model with great context will outperform a giant model with mediocre context, and cost a fraction of the price.

Start small, invest in your retrieval pipeline, and upgrade the model only when you've exhausted the engineering improvements.

From local to production

The pipeline works. Before shipping it, add one critical safety net and survey additional production requirements.

Relevance thresholds

The most dangerous failure mode in RAG isn't a wrong answer, it's a confident wrong answer based on irrelevant context. If someone asks an off-topic question and the retrieval returns vaguely related noise, the model will dutifully synthesize an answer from that noise.

The fix is a relevance threshold: if nothing in the database is close enough to the question, say so instead of guessing:

./scripts/5-ask.js — updated with threshold

async function retrieve(question, limit = 5, threshold = 0.3) {
  const results = await collection.query.hybrid(question, {
    limit,
    alpha: 0.75,
    returnMetadata: ["score"],
    returnProperties: ["title", "body", "description"],
  });

  // Filter out results below the relevance threshold
  const relevant = results.objects.filter(
    (obj) => obj.metadata.score >= threshold
  );

  if (relevant.length === 0) {
    return null; // Signal: nothing relevant found
  }

  return relevant;
}

Hybrid search scores combine vector similarity and BM25 keyword matching, but don't map directly to cosine similarity. A threshold of 0.3 filters out noise while keeping genuinely relevant results. The exact value depends on your data and model, so test and adjust based on the results.

Update the main pipeline:

// ./scripts/5-ask.js — updated

async function askQuestion(question, model) {
  const sources = await retrieve(question, 5);

  if (!sources) {
    console.log("I don't have enough information to answer that question.");
    client.close();
    return;
  }

  const prompt = buildPrompt(question, sources);
  await generateStream(prompt, model);
}

Finally, test it with an off-topic question:

node scripts/5-ask.js "What's the best pizza in Naples?"
🔍 Searching for: "What's the best pizza in Naples?"

📚 Found 5 sources: [
  "Mastering Scheduling and Gantt Charting: A Recap of Bryntum's Guide",
  "DEV's Worldwide Show and Tell Challenge Presented by Mux",
  'Will WebAssembly Kill JavaScript? Let's Find Out',
  'Building a Custom Calendar with React + Storyblok',
  "You're Not Building Netflix: Stop Coding Like You Are"
]

💬 Answer (qwen3.5:4b):

The provided context does not contain enough information to answer the question.
None of the sources discuss food, restaurants, or pizza in Naples.

No hallucinations ensure that users trust the assistant because it knows when to stay quiet.

What to explore next

A production RAG system has more aspects than one tutorial can cover.

Here's a roadmap:

  • Reranking with cross-encoders: a second retrieval stage that re-scores the top results with much higher precision. Weaviate's reranker-transformers module supports this natively for self-hosted instances. This is the single biggest quality improvement you can add after chunking.
  • Context window management: LLMs have token limits (though Qwen 3.5's 256K context window gives you plenty of room). When your retrieval returns more context than the model can handle, you need a truncation strategy that preserves the most relevant content.
  • Caching common queries: if the same questions come up repeatedly, cache the retrieval results (or even the full answers) to reduce latency and computation.
  • Error handling: what happens when Ollama is slow, returns garbage, or the context window is exceeded? Production systems need graceful degradation.

Takeaways

In part 1, you learned what vectors are. In part 2, you built a search engine. In this part, you turned that search engine into an AI assistant that runs locally.

The key lessons:

  • Hybrid search beats pure vector search: keywords and semantics complement each other.
  • Chunking matters more than model: an efficient retrieval pipeline with a small model outperforms a mediocre pipeline with a frontier model.
  • RAG makes LLMs informed, not smarter: the quality of the answers depends on the quality of the retrieval.
  • Know when to say “I don't know": relevance thresholds prevent the most dangerous failure mode: confident wrong answers.
  • Structured content is a RAG superpower: if your content lives in a CMS with a component model (like Storyblok), the content architecture is the semantic structure. Your components give you chunking for free. This is the idea behind Strata, Storyblok's AI-ready content layer, currently in preview.

The pipeline you built is linear: retrieve, then generate. While the industry keeps progressing—agentic retrieval, graph RAG, evaluation frameworks like RAGAS—the foundation of hybrid search, smart chunking, and grounded generation is the starting point.

Your content already has the answers; you just need to tell the AI where to look.