Personal project · 2026

DatDai

The problem

Vietnam passed a new Land Law in 2024. Decrees in 2025 and 2026 amended the decrees that implement it, and one amending decree has been amended again. Someone asking about land prices or compensation needs the clause that applies today, with an article number they can look up.

A plain retrieval-augmented generation (RAG) chatbot gets this wrong in two ways. It can quote a clause that a later decree replaced, and it can cite an article it never retrieved.

What I built

I built DatDai on my own. It answers from the 2024 Land Law and the ten decrees and resolutions that implement or amend it, split into 4,366 chunks at article boundaries, with long articles split again at each clause.

From a question to a checked answer every stage traced in Langfuse
Rewritea standalone question and search variants
Plana model chooses the lookups
Retrievehybrid search and the amendment graph
Generatea larger model for complex questions
Check citationseach cited article against the retrieved text
The planner can run several lookups at once. The citation check reads the finished answer and makes no model call.

Search runs dense and keyword retrieval together in Qdrant and merges the two rankings with reciprocal rank fusion (RRF). A small Gemini model scores each candidate passage from 0 to 1, and the top passages are expanded to their full article. A follow-up like “case 3” resolves to the third item in the previous answer, using a short state kept for each conversation.

An amendment graph records which decree amends or implements which, and an index links 24 amended articles to the chunks that rewrite them. When a question asks how one decree changed another, the planner fetches those chunks along with the search results.

The citation verifier pulls every article reference out of the answer, with or without Vietnamese diacritics, and checks that each one appears in the retrieved passages. If one doesn’t, it logs the miss and adds a note to the saved answer asking the reader to check the cited provisions.

How I check it

Each request is traced in Langfuse with one span per stage. The trace also records scores that need no model call, such as how many passages came back and the reranker’s top score. Readers can rate an answer thumbs up or down, and the rating goes on the same trace.

For answer quality, a script pulls recent traces and has Gemini 2.5 Flash judge them at temperature 0. It scores each answer from 0 to 1 on grounding in the retrieved passages, relevance and completeness, and writes the scores back to the trace. Each run skips traces that already have scores.

Before launch I audited the deployed service against its design docs. The audit confirmed 6 of the 7 problems in my hardening notes and found 14 more. The table shows the ones that affect uptime, and what the code does now.

What the audit found, and the code now audit of 6 March 2026
FindingAt auditNow
Outside call timeoutsnone15 to 45 s
Postgres connections6 to 8 a requestpooled, max 5
Rate limitnone10 a minute
Memory, concurrency1 GiB, 102 GiB, 5
Health checknonestartup probe
At-audit values come from the audit report. Current values come from the code and the deploy workflow. The rate limit applies per client address.

Next

Next is a fixed set of questions with known answers, scored by the deploy workflow on every push. After that, I want the citation note to show while the answer streams, and the service to refuse to start without its vector store.