Skip to content
All posts

Swelv· Part 1 of 2

A RAG without vectors: the model finds its own books

Every month, five books picked for each reader. The model invents no title: it queries the catalogue with tools, and code checks everything.

6 min readLire en français

Every month, Swelv builds each reader a selection of five books, from what they read, love and give up on. Each book comes with a tagline and a “why” that names their own reads.

9:41

A selection that reflects you

Léa's monthly selection October 2026

Alexandre DumasThe Count of Monte Cristo

The Count of Monte Cristo

Alexandre Dumas

Classics · Adventure · Historical

Camille, Léo liked it

Because you loved The Three Musketeers

Why this is for you

You loved The Three Musketeers: the same pace, the same Dumas, and a patient revenge that holds for a thousand pages.

+ Add to my list
  1. 1A book that existsFound in the catalogue, never quoted from memory.
  2. 2An axisOut of five: three safe, one adjacent, one discovery.
  3. 3A framed taglineA few set phrasings, always in a friendly voice.
  4. 4A why you can checkIt names a book from the profile, and a real link between the two.
The “A selection that reflects you” screen, rebuilt with the app's colors and layout, on a sample profile. Three of the five books, one per axis.

RAG: retrieve, then generate

A language model writes very well, but it does not know our catalogue, and it can confidently quote a book that does not exist. RAG (Retrieval-Augmented Generation) fixes that: before answering, you fetch the right information, and the model writes from it.

The best-known version uses embeddings: texts become vectors, and you look for the ones closest to the question. It is ideal to find a passage in documentation. To recommend a book, much less so: the real constraints are exact (the right language, not a book already read, not the same author twice), and vector proximity guarantees none of them.

So Swelv does the opposite: the model gets tools, and searches the catalogue itself, the way you would query a database. This is function calling: the model does not answer directly, it asks to call a function, the server runs it, and hands back the result.

Classic RAGembeddings
The question
Turned into a vector
The nearest passages
The model answers
  • + ideal to find free text
  • − no guarantee on an exact rule
RAG with toolstools
The reader's profile
The model queries the database↻ in a loop
Code checks every pick
The model writes
  • + exact filters, verifiable results
  • − more round trips, so slower
Two ways to fetch information before generating. When the constraints are exact (the language, a book already read, an author already proposed), tools win.

The challenge. Leave the model what it does well (understanding taste, forming hypotheses, writing) and keep in code everything that must be exact. A model can forget an instruction; an SQL clause, never.

The loop

The model gets the reader's profile, then searches again and again: it forms a hypothesis (“they love big historical adventures”), searches, intersects the results, eliminates. When it proposes its five books, code rereads each one, and sends the model back to work if one does not pass.

The profile
  • loved books, favourites
  • books given up, and why
  • recent activity
  • languages read

Never the private notes.

The loopAI
modela filterbookscatalogue

It forms hypotheses, searches, intersects lists, eliminates.

The checksSQL
  • does the book exist?
  • already read or proposed?
  • right language?
  • same author twice?

✗ → the model is sent back

5 books
3 safe1 adjacent1 discovery
The model searches the catalogue itself, with tools the server turns into queries. Then code rereads every pick, and sends the model back to work until each book passes.

Each search is a small filter the model writes, which the server turns into SQL. The server always adds what the model should not have to handle: the books already read, and the reader's languages.

The model writes
search({  genre: "adventure",  themes: ["revenge", "betrayal"],  keep_as: "like_dumas"})
The server runs
SELECT title, author, id FROM catalogueWHERE genre = $1  AND summary ~* $2  AND id NOT IN (:already_read)  AND language IN (:reader_languages)ORDER BY rating DESC LIMIT 20
+ added by the server, to every search

The model gets → a handful of books, each with an id to copy as is, and the list kept under a name to intersect it later.

One tool call, with example names. The model writes a short filter; the server turns it into parameterised SQL and adds what the model should not have to handle.

Language, learned the hard way

The feature's first incidents all had the same cause: language. Each one moved a rule from the prompt into code.

Three gates

  1. 01On every searchNarrowed by default to the reader's languages.
  2. 02Inside the loopAn off-language pick is refused, the model asked again.
  3. 03At validationLast net: the book is dropped, then replaced.
A quota per selection, from recent reads: the main language keeps at least three slots out of five.

What taught it

  1. 1A French-only reader gets books in other languages.→ Language was only a hint in the prompt: it becomes a filter, at three levels.
  2. 2The tagline in one language, the explanation in another.→ The output language is decided in code, and enforced on the model.
  3. 3Editions mislabelled by the data source.→ A language quota per selection, and the language cross-checked.
Each incident moved a rule from the prompt into code: a model can forget an instruction, not a WHERE clause.

The right model is not the cheapest

Before switching models, we replay real selections on the same profiles and compare: cost, time, number of corrections, and above all the quality of the copy.

Cost per selection, relative

The previous model× 1the reference
The new modelchosen÷ 3faster, with the richest copy: names the plot, cites the awards
The cheapest÷ 30no corrections needed, but shorter, more generic “whys”

The system prompt is the same for every reader: the provider serves almost all of it from its cache.

Replays of real selections on the same profiles, before switching models. The cheapest did not win: its copy is what the reader reads.

What I take from it

  • Give tools, not memories. The model only proposes what it found.
  • What matters goes into code. A prompt instruction always ends up forgotten.
  • Pick the model on what the user reads, not only on the bill.

Maxime Burri

Working on something similar? Let's talk.

Send an email