Side project · Case study

Traffic Law RAG

A question-answering tool for Taiwan's traffic laws. Ask in plain Chinese and get an answer grounded only in the statute text, with citations for every claim or a clear refusal.

  • Python / FastAPI
  • Chroma
  • Voyage embeddings
  • Claude API
  • SSE
  • React / TypeScript
  • pytest
  • Vitest
  • Playwright
  • Docker
  • GitHub Actions
Try the live demoSource code

It might need a few seconds to load on first visit due to cold start

Role
Solo, end to end (spec, retrieval and evaluation, backend, frontend, deployment)
articles across five regulations
462
evaluation questions
40
automated tests
442
Architecture Decision Records
9
01

Why I built it

Laws are written in legal language; people ask in everyday words. 「紅燈右轉罰多少?」 shares almost no keywords with the article that answers it. A general chatbot may answer anyway, sometimes with an outdated or invented fine. I wanted each answer to be checkable: the source article sits beside the answer, every number must appear in the cited text, and “these laws don’t answer that” is an acceptable result.

Coming from frontend, I also wanted to own what I knew least: retrieval, evaluation, and keeping a public demo affordable when every question is billed to my own API keys.

02

What it does

  • Streams answers in Traditional Chinese, with a citation badge such as 道交處罰條例 §53 after each claim
  • Lists retrieved articles beside the answer; clicking one opens the full text and links to the official law database
  • Shows the five pipeline steps: rewrite, retrieve, relevance check, generate, and verify, with status and timing
  • Refuses out-of-scope questions and explains why, including the similarity score against the threshold
  • Publishes evaluation metrics and all 40 expandable test questions
  • Exposes the same pipeline to Claude Code as an MCP server with three read-only tools
03

Architecture

One Docker image runs FastAPI and serves the built React app, so the SSE stream needs no CORS. Law data comes from Taiwan’s open-data portal because the official site forbids scraping; it is indexed offline in Chroma.

flowchart LR
  Data["data.gov.tw open data"] -.->|"parse, chunk, embed"| Index["Chroma index<br/>(GitHub Release)"]
  UI["React SPA"] -->|"POST /api/ask"| Ret["Vector search, top 5"]
  Index --> Ret
  Ret --> Gate{"Top similarity ≥ 0.40?"}
  Gate -- No --> Refuse["Refusal event"]
  Gate -- Yes --> Gen["Claude reads whole articles"]
  Gen -->|"[[REFUSE]]"| Refuse
  Gen --> Val["Verify citations and numbers"]
  Val -->|"SSE events"| UI
  Refuse -->|"SSE events"| UI
  • Legal boundaries define chunks. Each article is one chunk. Long articles split only at paragraph or item boundaries, so every chunk still maps to an article number.
  • The index is versioned outside the repository. Each data version is a GitHub Release. CD downloads the latest one, deploys it, and smoke-tests that the live data version and chunk count match.
04

Engineering highlights

05

How I worked

I wrote a 13-step implementation spec with acceptance criteria for each step, recorded nine ADRs, and built with Claude Code. AI wrote most of the code and tests; I chose the scope and data source, verified the evaluation answers, made the trade-offs, and accepted each step. Rather than reviewing backend code line by line, I used 442 tests, CI, and evaluation reruns after every prompt or retrieval change as the gate. A separate reviewer agent’s observation that the out-of-scope questions were too easy reshaped the evaluation set.