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.
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.
| Finding | At audit | Now |
|---|---|---|
| Outside call timeouts | none | 15 to 45 s |
| Postgres connections | 6 to 8 a request | pooled, max 5 |
| Rate limit | none | 10 a minute |
| Memory, concurrency | 1 GiB, 10 | 2 GiB, 5 |
| Health check | none | startup probe |
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.