Skip to content
Trần Tuấn Anh
beginwhoknowledge_searchstackllmwhat i buildcategorizeexperiencehttpprojectsagentragentamessagecontact

agent graph · drag to rotate · click a node

01Software engineer · Hà Nội · open to work

I build the parts of AI products that have to be right: retrieval that cites the real passage, agents that survive a deploy, and the ledger that bills for it once.

02Ragenta

How a document becomes a cited answer

Ragenta is a multi-tenant RAG + agents SaaS I built at NYB AI. A workspace uploads its documents, asks questions, and gets answers that point at the exact passage they came from. Every model call is paid from a credit ledger, and every ingestion job has to survive a deploy in the middle of it.

Below is the path one document takes, from upload to a [[n]] citation in a chat reply. The six stage ids are the same ones the backend uses; the diagram follows the card you are reading.

Ragenta pipeline: from upload to cited answerSix stations in a row — ingest, parse, chunk, embed, retrieve, cite. The station for the stage you are reading is highlighted.Ingest — upload queued as a BullMQ jobpending1 ingestParse — sections with page rangesparsing2 parseChunk — 512-token pieces with overlapsummarychunking3 chunkEmbed — vectors into Qdrant, text in Postgresqdranttsvector4 embedRetrieve — hybrid dense + lexical searchhybridtop-k5 retrieveCite — passages frozen onto the message[[1]] [[2]]6 cite
status pending ·
  1. 01

    Ingest

    Upload queues a BullMQ job whose payload is only {documentId, workspaceId}; everything else is read from the DB when it runs. The job id is ingest:{documentId}:{attempt} and the credit charge is keyed on it — a retry never bills twice. Three attempts, not BullMQ's default: a document that failed twice usually fails a third time, and each try is a paid provider call.

  2. 02

    Parse

    Format-specific extractors (unpdf, mammoth, exceljs, email, hierarchy) produce sections with page ranges. A scanned PDF has no text layer and fails with a reason a human can act on, instead of silently indexing nothing. No OCR in the path — the failure reason says so.

  3. 03

    Chunk

    A port of RAGFlow's naive_merge: split at sentence delimiters (including CJK 。;!?, so a Vietnamese or Chinese base doesn't become one giant chunk), merge back up to 512 tokens, prefix each chunk with the tail of the previous one. Optional RAPTOR summaries are indexed as kind = 'summary' chunks and cited like any passage.

  4. 04

    Embed

    100 inputs per call (a provider limit), truncated at 8 000 tokens rather than failing the document. The embedding model is frozen per knowledge base at creation — changing it cannot re-embed what already exists. Text stays in Postgres with a tsvector; the vector goes to Qdrant keyed by the chunk row id. Ranges already embedded and paid for are reused on retry.

  5. 05

    Retrieve

    Hybrid: Qdrant cosine (in [0,1]) and Postgres ts_rank_cd (unbounded, normalised against the best hit) fused with a vectorWeight, cut at a threshold. Modes hybrid · vector · keyword; a switched-off half skips the call rather than weighting it to zero. An optional reranker runs over the fused candidates only, never the corpus.

  6. 06

    Cite

    The model cites with [[n]]; the server freezes the matching passages onto the message row, so rendering is a lookup by index — no post-hoc similarity match that can drift to the wrong paragraph. In agent runs a CitationCollector numbers passages once per run, so [[1]] is the same paragraph whichever tool call found it.

Four decisions I'd defend in a review

Citations are frozen server-side.
Rendering is a lookup by index, never a similarity match that can drift to the wrong paragraph. Why not re-match at render time: less code, but a similarity match can land on a neighbouring paragraph, and a wrong citation is worse than none.
Streaming state is local, not in the query cache.
A setQueryData per token re-renders every subscriber. The stream writes local state; the cache is invalidated once at the end and the server's rows — with citations, model and cost — win.
Poll only while something is in flight.
The documents list stops the moment every row is ready or failed. Why not a fixed interval: an idle page would keep hitting the API for nothing.
Refusals happen before the stream opens.
No credits, a model outside the plan, a missing base: a normal 4xx with a message, never an error frame after the UI has already switched to "answering".

Read the full case study — architecture, the agent flow editor, all seven decisions →

03Experience

Run log

  1. 03

    NYB AI

    Software Engineer · ragenta

    checkpointPromo-code and credit feature end to end — admin APIs, console UI and the redeem flow — plus timezone-aware webinar scheduling and a hardened Google One Tap sign-in.

    TypeScript · Hono · Next.js · PostgreSQL · Qdrant · Redis · BullMQ · Better Auth · Stripe · MinIO · Docker · Google One Tap · PostHog · GitOps

    06/202609/2026 · 4 months
  2. 02

    Global AI

    Fullstack Developer · tradewize

    checkpointFrontend and backend features for TradeWize, a US educational trading simulator — web and app modules, APIs, dashboards and user flows, working directly with BAs and testers.

    TypeScript · Node.js · PostgreSQL · Docker

    12/202506/2026 · 7 months
  3. 01

    Avada Group

    Node.js Intern · shopify apps

    checkpointThree demo Shopify apps in three months — auth, webhooks, APIs and store interaction, plus event-driven work on Firebase Pub/Sub and Cloud Functions.

    Koa.js · Shopify Polaris · Firebase · Liquid

    09/202511/2025 · 3 months
  4. 00

    Hanoi University of Industry

    B.Eng. Software Engineering

    10/202208/2026
04Projects

Spec sheets

Ragenta AI — screenshot
Ragenta workspace — grounded chat with cited sources
project · 012026

Ragenta AI

Multi-tenant RAG + configurable agents. Workspaces on Better Auth organisations, an ingestion worker, hybrid retrieval with exact citations, a credit ledger fed by Stripe webhooks, and a 16-node agent flow editor.

role
fullstack — RAG pipeline, agent runner, billing
stack
TypeScript · Hono · Next.js · PostgreSQL · Qdrant · Redis · BullMQ · Better Auth · Stripe · MinIO · Docker
scale
16 node types · 3 ingest attempts · 512-token chunks
links
case studystaging (opens in a new tab)github (opens in a new tab)
E-commerce microservices — screenshot
Stationery storefront, homepage
project · 022026

E-commerce microservices

Auth, catalogue, orders, payments and notifications as independent microservices talking over gRPC and Kafka; Stripe webhooks; an OpenAI-backed product chatbot with retrieval over the catalogue.

arch
microservices · gRPC · Kafka
stack
TypeScript · NestJS · Next.js · PostgreSQL · Stripe · OpenAI · Docker
links
demo (opens in a new tab)github (opens in a new tab)
Job Portal API — screenshot
Job Portal admin dashboard
project · 032024

Job Portal API

REST backend for a recruitment platform: employers manage postings, candidates search and apply.

arch
monolith REST · JWT
stack
Node.js · Express · TypeScript · PostgreSQL · Prisma · JWT
links
github (opens in a new tab)