Lesson 2 · from concept to a shippable pipeline

Building the RAG system

How the concierge actually answers a Thai car buyer — the architecture decision that prevents hallucination, the pgvector schema, hybrid retrieval, reranking, grounding, and the model that runs each step.

Postgres + pgvector Gemini stack tools vs RAG hybrid + rerank + grounding
Decision 1 · the most important one

Inventory is a tool. Policy is a document. Never let the model be the database.

Before any retrieval, split knowledge into two classes and route every message to the right one. This single decision stops the worst failure mode: the LLM inventing a price, a mileage, or an "accident-free" claim a customer could sue over.

Class A — live, one-of-one

→ Tools (DB function calls)

Each used car is a unique unit, not a SKU with a stock count. Prices, mileage, financing tables, availability change daily. Queried exactly, live — never embedded.

  • search_inventory(make, model, year, price_max…)
  • get_vehicle(listing_id) · check_availability(id)
  • estimate_installment(price, down, term) — deterministic math
Class B — prose, slow-changing

→ RAG (vector + BM25)

Dealer knowledge that is explanation, not rows. Semantic match over chunks.

  • Financing process & required documents
  • Trade-in / ownership-transfer (โอนรถ) steps & costs
  • Warranty, insurance (พรบ./ประกัน), inspection grading
  • Branch FAQs, buying guidance

The router, live — click a real message

A cheap, fast classifier reads each turn and dispatches it. Most real messages are mixed — the orchestrator runs several and composes one reply.

The data map

Where each kind of knowledge lives

KnowledgeStorageWhy
Vehicle listings (unit, specs, price, status)Postgres cars + GIN trigramExact, live, one-of-one; faceted query; status lifecycle
Financing tables, down-payment rulesPostgres / rules configDeterministic math; legal exactness
Promotions (eligibility, dates)Postgres + rulesTool-validated; expiry-aware; never invented
Policies, processes, FAQs (prose)pgvector + BM25 (RAG)Natural-language explanation; semantic match
Past resolved conversations (curated)RAG (optional)Reuse proven phrasings — only approved ones

Every RAG chunk also carries mandatory metadata — tenant_id, branch_id, locale, effective_date, expiry_date, access_level, topic_tags — which powers tenant isolation, branch-specific answers, stale-promo filtering, and never leaking internal notes to customers.

Step 1 · the schema

The pgvector table for Class B

One table holds the chunk text, its dense vector, a full-text vector for BM25, and the metadata. Two indexes — HNSW for vectors, GIN for text — make hybrid search fast.

Migration
Index choice
Why halfvec
CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE policy_chunks (
    id           bigserial PRIMARY KEY,
    tenant_id    bigint      NOT NULL,
    source_id    bigint      NOT NULL,         -- which document
    doc_version  int         NOT NULL,
    content      text        NOT NULL,         -- the chunk itself
    embedding    halfvec(1536) NOT NULL,       -- Gemini Embedding 001 (Matryoshka-truncated)
    search_tsv   tsvector    NOT NULL,         -- BM25 / lexical (newmm-segmented Thai)
    branch_id    bigint,                       -- NULL = applies to all branches
    locale       text        NOT NULL DEFAULT 'th',
    effective_date date,
    expiry_date  date,                         -- filter dead promos at query time
    access_level text        NOT NULL DEFAULT 'customer',  -- vs 'internal'
    topic_tags   text[]      NOT NULL DEFAULT '{}'
);

-- Dense vector search (cosine). HNSW = fast, graceful updates.
CREATE INDEX ON policy_chunks
    USING hnsw (embedding halfvec_cosine_ops);

-- Lexical search for BM25 / exact-term recall.
CREATE INDEX ON policy_chunks USING gin (search_tsv);

-- Cheap metadata filters applied before ranking.
CREATE INDEX ON policy_chunks (tenant_id, branch_id, locale);
Step 2 · retrieval

Hybrid search — dense + BM25, fused

Dense vectors catch meaning; BM25 catches exact terms embeddings miss (model codes, the word "พรบ."). For Thai, hybrid is non-negotiable. Toggle the knobs and watch the SQL the app would run.

retrieval mode
metadata filters

generated SQL — what the retrieval service runs

        

:q = the query embedding (from Gemini Embedding 001) · :text = the newmm-segmented query · 60 in 1/(60+rank) is the RRF damping constant. Inventory search (Class A) is a separate faceted query with price/year filters — this builder is the policy (Class B) path.

Step 3 · rerank

The cheapest big quality win

Hybrid search casts a wide net — the top 20 are plausibly relevant. A cross-encoder reranker scores each query–chunk pair jointly and reshuffles them, so only the genuinely-best 3–5 reach the LLM. Vendors cite +20–35% answer accuracy. Click to rerank.

fused candidates (top 20, by RRF)
after cross-encoder → top 5 to the LLM

Cross-encoder reads query + chunk together (slow, accurate); the dense/BM25 stage scored them independently (fast, approximate). You rerank only ~20, so the cost is tiny — Cohere Rerank is $2 per 1,000 searches. Self-host the BGE reranker to make it free at volume.

Step 4 · grounding

Answer only from the chunks — or don't answer

The LLM is told to use only the reranked chunks for policy facts. And a confidence gate decides: if the top rerank score is weak, the bot asks one clarifying question or escalates to a human. It never guesses. Drag to see the behavior.

top rerank score
0.00 — irrelevant0.821.00 — perfect
gate decision

Class A/B separation + this confidence gate are the core anti-hallucination mechanism. Also filter chunks past expiry_date at retrieval time so dead promos never surface.

Step 5 · model routing

Don't use a frontier model for every step

Route by task. A cheap model classifies and extracts; deterministic code does the math (free); the strong model only writes the final Thai reply. Set your volume and watch the monthly bill.

50,000

Prices verified Jun 2026: Gemini Flash $0.30/$2.50, Gemini Pro $1.25/$10, Gemini Embedding 001 $0.15/M (per 1M tokens); Cohere Rerank $2/1k searches. Prompt-cache the persona + system prefix to cut Pro input cost ~90%.

Step 6 · evaluation

Measure retrieval separately from generation

RAG quality = retrieval quality. If the right chunk isn't in the top-k, the LLM can't use it. Build a golden set of real Thai questions labelled with the correct chunk, and track recall@k — before and after rerank. Drag k.

k (chunks retrieved)
k = 5
recall@k — hybrid only
—
recall@k — hybrid + rerank
—
  • Retrieval hit-rate — right chunk in top-k? (the chart on the left)
  • Routing accuracy — tools vs RAG vs persona
  • Groundedness — flag any price/mileage not traceable to a tool
  • Persona/particle consistency — a ค่ะ-bot never emits ครับ, no stray English
  • Clarify-vs-guess behavior on low-confidence queries
  • Resolution, not containment — did the need actually get met?

Toy curve for intuition: rerank lifts the low-k end most — that's the whole point of paying for it. Real numbers come from your golden set.

Build order

Sequence to good Thai replies, fastest

  1. Class A inventory tools + Thai normalization — answers the highest-volume question correctly, hallucination-free.
  2. Persona + reply composer + style checker — consistent voice, LINE Flex cards, grounded numbers, one CTA.
  3. RAG pipeline (Class B) — ingest → newmm chunk → embed → hybrid → rerank → gate.
  4. Dialogue state + slot-filling — multi-turn discovery, returning-customer memory.
  5. Financing estimate tool + booking flow — deterministic math, viewing booking.
  6. Confidence-gated human handoff.
  7. Eval harness + golden set — stood up early enough to gate every later change.

The hardest part isn't the pipeline — it's surviving messy Thai input. That's lesson 3.