Skip to main content
Retrieval-augmented generation, or RAG, is one of the most useful patterns for building AI applications that need to answer from your own documents. Instead of asking a model to rely on memory alone, you retrieve relevant source material first, send that context to the model, and ask it to answer with citations. In this tutorial, we’ll build a private RAG bot using Python, Venice for embeddings and chat completions, Qdrant for vector search, and FastEmbed for local re-ranking. By the end, you’ll have the core pieces for a local document assistant that can ingest your files, retrieve relevant chunks, re-rank them, and answer with citations. The RAG bot in action Before we continue: if you want to run the code in this article, you’ll need a Venice API key. Export it as an environment variable:
Interested in the full code implementation? Check out the GitHub repo.

How a Modern RAG Bot Works

A good RAG pipeline is more than “put documents in a vector database.” The basic flow looks like this: The re-ranking step is the upgrade that makes this more useful than a basic RAG demo. Vector search is fast and good at finding semantically similar chunks, but it can still return passages that are adjacent to the topic rather than directly useful. A cross-encoder reads the question and each candidate chunk together, then scores how well that chunk actually answers the question.

Installing the Dependencies

We’ll use the OpenAI Python SDK because Venice exposes an OpenAI-compatible API. We’ll also use Qdrant’s Python client with FastEmbed support:
If you prefer to keep dependencies in a file, create requirements.txt with the same packages:

Choosing the Models

Create a file called rag_bot.py, then start by adding the imports, data structures, API URL, and model names:
The embedding model name is intentionally OpenAI-compatible. Venice maps compatible embedding model names to Venice-hosted embedding models, so existing OpenAI SDK code can usually move over by changing the base_url and API key. You can list available Venice models with:
For chat models:

Creating the Venice and Qdrant Clients

Create one OpenAI-compatible Venice client for both embeddings and chat completions:
For Qdrant, you have three useful modes: For a private local bot, start with an on-disk local Qdrant path:
There’s a few different ways to handle deployment in production. However if you use a remote Qdrant deployment, remember that your document chunks and metadata will be stored there. Venice can keep the inference layer private, but you should still choose the right Qdrant deployment for your data.

Loading and Chunking Documents

For this tutorial, we’ll let the bot ingest local files or folders. Start with .md, .rst, and .txt files:
Once the files are loaded, we need to split the text up by “chunking” it - separating it into chunks of data. A naive strategy might split the chunks evenly. However in most cases, this can lose information at given semantic boundaries which can cause the effectiveness of your RAG system to go down. The chunking strategy we will use prefers paragraph or sentence boundaries so the model gets coherent context:
A starting chunk size of 1000 characters with 150 characters of overlap is a good default for mixed Markdown and text documents. Smaller chunks can improve precision. Larger chunks can preserve more context. The right setting will often on depend on the kinds of documents you are storing.

Embedding Documents with Venice

Once we have chunks, we embed them in batches:
Batching matters. Embedding one chunk at a time is simple, but it adds avoidable latency. Keep the batch size configurable so you can tune throughput based on your workload.

Storing Vectors in Qdrant

Before inserting points, create a Qdrant collection with the right vector size. The easiest way to know the vector size is to embed the first batch, then use len(embeddings[0]).
Each point stores the vector plus payload metadata. The payload includes the original text and a source path so the answer can cite where the context came from:
Use deterministic UUIDs derived from source, chunk_index, and content. That makes repeated ingestion idempotent for unchanged chunks.

Retrieving Candidate Chunks

At question time, the bot embeds the user’s question and asks Qdrant for the top vector matches:
The limit here is the candidate count. It should usually be higher than the number of chunks you plan to send to the model because the next step will re-rank them. A good default is to retrieve 8 candidates and send the best 4 to the chat model.

Re-ranking with FastEmbed

Now we add the part that makes the retrieval feel much smarter.
The important difference between embedding search and cross-encoder re-ranking is how the scoring happens. Embedding search compares one vector for the question against one vector for each chunk. It is fast and scalable. A cross-encoder evaluates the question and chunk together. It is slower, but it can judge relevance more directly. That is why the usual pattern is:
  1. Retrieve a larger candidate set with vector search.
  2. Re-rank only those candidates locally.
  3. Send the top few chunks to the language model.
A good starting point is candidate_k=8 and top_k=4. Increase candidate_k if the right source is often nearby but not making it into the final context.

Answering with Venice Chat Completions

Once the context is selected, format it with source numbers:
Then send the context to a Venice chat model:
Notice the system prompt: the bot is told to answer only from the supplied context. That is a simple but important guardrail. A RAG assistant should not confidently answer from general model knowledge when the retrieved documents do not support the answer.

Running the Bot

Once you assemble the pieces into a script, save it as rag_bot.py. A simple first run can use a few built-in sample documents so you can verify the pipeline before ingesting your own files:
To ingest your own documents:
To keep a local Qdrant collection on disk and start an interactive chat:
The script prints the answer, then prints the sources with both vector and re-ranking scores:
If you want to inspect the actual text passed into the model, add:

Useful CLI Options

Expose the main retrieval knobs as CLI options so you can tune the bot without editing code: For repeated local development, a common flow is:
Then ask follow-up questions without ingesting again:

Privacy Notes

For a private RAG setup, think about each layer separately: The most private default for this tutorial is Venice for inference, local Qdrant on disk, and local FastEmbed re-ranking. That gives you a practical RAG bot without sending your vector database payloads to a third-party vector store.

Common Errors to Handle Up Front

If you change embedding models, recreate the Qdrant collection. Different embedding models can produce vectors with different dimensions, and Qdrant collections expect a fixed vector size.

Where to Go Next

Once you have the baseline running, the highest-impact improvements are usually:
  • Add document-specific loaders for PDFs, HTML, tickets, or internal wiki pages.
  • Store richer metadata such as titles, headings, dates, owners, and URLs.
  • Tune candidate_k, top_k, chunk size, and overlap on real questions.
  • Add evaluation questions so you can measure retrieval quality before and after changes.
  • Stream the final Venice chat completion for a better interactive chat experience.
RAG systems are easy to demo and surprisingly easy to make mediocre. The vector search plus re-ranking pattern is a strong foundation because it keeps retrieval fast while giving the bot a better chance of sending the language model the right context.