DEV Community

Rayulu Mukku
Rayulu Mukku

Posted on

I Built a RAG System, But I Didn't Want It to Be Another "Chat With Your PDF" App

 # I Built a RAG Workstation Instead of Another "Chat With Your PDF" App

I've been building RAG Studio for a while, and I wanted to do something a little different from the usual RAG projects.

Most RAG demos are basically:

Upload a document → ask a question → get an answer.

That's fine for demonstrating the concept.

But when I started building my own, I kept thinking about everything happening between those three steps.

What happens to the document before it's embedded?

What if the document contains an API key or someone's email address?

Which chunks actually get retrieved?

Does a high similarity score mean the retrieved content is actually useful?

And what happens when the answer isn't in the local documents at all?

I didn't want all of that to be hidden behind a single "Ask AI" button.

So I built RAG Studio — an interactive RAG workstation where I can actually see and inspect what's happening throughout the pipeline.

The project ended up combining three areas I wanted to explore:

ETL + DAG orchestration + Corrective RAG (CRAG).

And one other thing was important to me:

Generate embeddings locally in the browser whenever possible.


What does the architecture look like?

At a high level, the pipeline looks like this:

Document
   ↓
ETL / Cleaning
   ↓
Chunking
   ↓
Local Embedding
   ↓
Vector Index
   ↓
Query Embedding
   ↓
Cosine Similarity
   ↓
CRAG Evaluation
   ↓
 ┌───────────────┐
 │               │
Relevant      Not Relevant
 │               │
 ↓               ↓
Local Context   Web Search
 │               │
 └───────┬───────┘
         ↓
      LLM
Enter fullscreen mode Exit fullscreen mode

Quite a few things are happening here, so I'll go through the interesting parts.


1. I started with ETL because documents are messy

A lot of RAG tutorials start with clean text.

Real documents don't.

A document can contain:

  • headers and footers
  • duplicated text
  • HTML boilerplate
  • advertisements
  • inconsistent whitespace
  • irrelevant navigation content
  • personal information
  • API keys and other secrets

If that content goes straight into chunking and embedding, you're already starting with a problem.

So RAG Studio has a dedicated ETL Studio before the actual RAG pipeline.

Extract

The ingestion layer supports:

  • PDF
  • TXT
  • Markdown
  • JSON
  • HTML/article content

The goal is to turn different input formats into a normalized text representation.

Transform

The cleaning stage then removes unnecessary content and looks for sensitive patterns.

For example:

alex.vance@ai-research.org
            ↓
[REDACTED_EMAIL]
Enter fullscreen mode Exit fullscreen mode
+1-555-019-2834
            ↓
[REDACTED_PHONE]
Enter fullscreen mode Exit fullscreen mode
sk-live-xxxxxxxx
            ↓
[REDACTED_API_KEY]
Enter fullscreen mode Exit fullscreen mode
192.168.1.104
            ↓
[REDACTED_IP]
Enter fullscreen mode Exit fullscreen mode

There is also whitespace normalization and HTML/boilerplate cleanup.

The important part is the ordering:

redaction happens before vectorization.

I wanted sensitive information removed before it ever became part of the embedding/indexing pipeline.


2. I didn't want the ETL process to be invisible

Normally, document cleaning happens somewhere in the backend and the user never sees it.

I wanted the opposite.

RAG Studio has a side-by-side diff viewer showing:

RAW DOCUMENT          CLEANED DOCUMENT

Original text    →    Normalized text
PII              →    [REDACTED]
HTML noise       →    Removed
Extra whitespace →    Cleaned
Enter fullscreen mode Exit fullscreen mode

It also shows information such as the original size, cleaned size and compression ratio.

So you can actually see what the ETL stage did before loading the cleaned corpus into the RAG pipeline.

This turned out to be more useful than I initially expected.

When retrieval produces strange results, I can go back and check whether the problem started with the source data.


3. Then I turned the RAG pipeline into a DAG

Once the document is clean, there are still quite a few stages involved.

Instead of hiding them behind a function like:

await runRAG(query);
Enter fullscreen mode Exit fullscreen mode

I represented the pipeline as a Directed Acyclic Graph.

The current pipeline contains nodes such as:

etl_hub
doc_source
chunking
vectorizer
vector_index
query_source
query_embed
cosine_ranker
crag_grader
web_fallback
context_injector
llm_synthesizer
Enter fullscreen mode Exit fullscreen mode

Visually, it becomes something like:

ETL Hub
   ↓
Document Source
   ↓
Chunking
   ↓
Vectorizer
   ↓
Vector Index
   ↓
Query Source
   ↓
Query Embedding
   ↓
Cosine Ranker
   ↓
CRAG Grader
   ↓
Web Fallback
   ↓
Context Injector
   ↓
LLM Synthesizer
Enter fullscreen mode Exit fullscreen mode

The graph isn't just there for visualization.

It gives me a way to inspect the actual pipeline state.


4. Why I wanted an interactive DAG

Let's say the final answer is wrong.

It's very easy to immediately think:

"The LLM hallucinated."

But maybe the LLM wasn't the problem.

Maybe the document wasn't cleaned properly.

Maybe the chunk boundaries were bad.

Maybe the relevant chunk wasn't retrieved.

Maybe the retrieved chunk was semantically similar but didn't actually answer the question.

Maybe the system should have searched the web instead.

With the DAG, I can work through those stages.

Each node can expose things like:

  • input payload
  • output payload
  • execution state
  • vector dimensions
  • storage mechanism
  • latency
  • memory information

I also added the ability to jump directly from a node in the graph to the corresponding stage in the application.

So the graph becomes something closer to a debugger for the RAG pipeline.


5. The embedding pipeline runs locally

This was another part of the project I really wanted to experiment with.

Instead of sending every document chunk to an external embedding API, RAG Studio uses Transformers.js and ONNX Runtime to generate embeddings locally in the browser.

The model produces 384-dimensional embeddings.

The basic flow is:

Text
 ↓
Tokenizer
 ↓
Transformer Model
 ↓
384D Vector
 ↓
Local Vector Index
Enter fullscreen mode Exit fullscreen mode

The model execution uses WebAssembly, while the heavier inference work runs inside Web Workers so the main UI thread doesn't get blocked.

So the browser is doing a lot more of the work:

Document
   ↓
Browser
   ↓
Embedding Model
   ↓
Vector
   ↓
Local Index
Enter fullscreen mode Exit fullscreen mode

instead of:

Document
   ↓
Server
   ↓
Embedding API
   ↓
Vector Database
Enter fullscreen mode Exit fullscreen mode

For a large production system, I'd obviously consider a different architecture.

But for this project, running the embedding pipeline locally was an interesting trade-off.

It reduces the need for a separate embedding service and keeps more of the processing on the client.


6. Then comes the math

Once everything is embedded, retrieval is essentially a vector similarity problem.

The query is converted into a vector, and that vector is compared against the document vectors.

The main calculation is cosine similarity:

cosine_similarity(A, B)
=
(A · B) / (||A|| ||B||)
Enter fullscreen mode Exit fullscreen mode

So a retrieval result might look like:

Chunk A    0.91
Chunk B    0.86
Chunk C    0.72
Chunk D    0.31
Enter fullscreen mode Exit fullscreen mode

The higher the score, the more similar the vectors are.

But there is a problem.

Similarity doesn't necessarily mean relevance.


7. A high similarity score doesn't mean the context is good

This is where I wanted to go beyond basic RAG.

Imagine the user asks a question and retrieval returns:

Chunk A → 0.91
Chunk B → 0.87
Chunk C → 0.83
Enter fullscreen mode Exit fullscreen mode

It looks great.

But what if those chunks are only related to the question and don't actually contain enough information to answer it?

If we blindly send them to the LLM, we're basically hoping the model figures it out.

I wanted another check before generation.

That's where Corrective RAG (CRAG) comes in.


8. The CRAG layer acts as a quality gate

After retrieval, the selected chunks are passed to a relevance evaluator.

The evaluator produces a structured result containing:

Verdict
Confidence
Justification
Enter fullscreen mode Exit fullscreen mode

For example:

VERDICT: RELEVANT

CONFIDENCE: 92.4%

The retrieved chunks contain information
directly related to the user's question.
Enter fullscreen mode Exit fullscreen mode

The important thing is that the system now has a decision point.

Instead of:

Retrieve → Generate
Enter fullscreen mode Exit fullscreen mode

the pipeline becomes:

Retrieve
   ↓
Evaluate
   ↓
Is the context good enough?
Enter fullscreen mode Exit fullscreen mode

9. What if the local documents don't have the answer?

This is probably my favorite part of the architecture.

If the retrieved context is relevant enough, the pipeline continues normally:

Retrieved Chunks
       ↓
   CRAG Grader
       ↓
    Relevant
       ↓
 Local Context
       ↓
      LLM
Enter fullscreen mode Exit fullscreen mode

But if the evaluator decides the context isn't sufficient:

Retrieved Chunks
       ↓
   CRAG Grader
       ↓
  Not Relevant
       ↓
Optimized Search Query
       ↓
    Web Search
       ↓
 Additional Context
       ↓
      LLM
Enter fullscreen mode Exit fullscreen mode

So the local vector index isn't treated as the absolute source of truth.

It's one knowledge source.

If it can't provide enough evidence, the system has another route.

This is the main idea behind the corrective layer:

Don't force the LLM to answer from context that the system already knows is insufficient.


10. The UI lets me watch the whole thing happen

This is really what separates RAG Studio from a normal RAG demo for me.

I can run the pipeline step by step instead of waiting for one final response.

I can inspect the intermediate state.

I can look at the vectors.

I can see the retrieval scores.

I can inspect the CRAG decision.

I can see whether the web fallback was triggered.

And I can inspect what eventually gets assembled into the LLM context.

So instead of only seeing:

Question → Answer
Enter fullscreen mode Exit fullscreen mode

I can see:

Question
   ↓
Query Embedding
   ↓
Similarity Search
   ↓
Retrieved Chunks
   ↓
Relevance Evaluation
   ↓
Routing Decision
   ↓
Context Assembly
   ↓
LLM
   ↓
Answer
Enter fullscreen mode Exit fullscreen mode

That makes debugging much easier.


The technology behind it

The main stack is:

Technology Where I use it
TypeScript 5+ Main application, state, APIs and vector operations
Next.js Application framework
React / JSX UI
Zustand Client-side state management
Tailwind CSS Styling and responsive UI
SVG Interactive DAG and architecture visualizations
Transformers.js Browser-side model inference
ONNX Runtime Local model execution
WebAssembly Fast browser-side inference
Web Workers Running embedding workloads away from the UI thread

One thing I particularly enjoyed was building the DAG visualization myself rather than treating it as just an image.

The nodes, connections and interactions are part of the application.


What I actually wanted to build

I wasn't trying to build another chatbot.

I wanted to make the RAG pipeline itself something you can explore.

A normal RAG application might look like:

Upload
  ↓
Embed
  ↓
Search
  ↓
LLM
Enter fullscreen mode Exit fullscreen mode

RAG Studio is closer to:

                 ┌──────────────┐
                 │   Document   │
                 └──────┬───────┘
                        ↓
                 ┌──────────────┐
                 │     ETL      │
                 │ Clean/Redact │
                 └──────┬───────┘
                        ↓
                    Chunking
                        ↓
                Local Embeddings
                        ↓
                   Vector Index
                        ↓
                     Query
                        ↓
                 Similarity Search
                        ↓
                  CRAG Evaluation
                    ↙        ↘
               Relevant    Insufficient
                  ↓             ↓
             Local Context   Web Search
                    ↘        ↙
                     Context
                        ↓
                       LLM
Enter fullscreen mode Exit fullscreen mode

There are more moving parts, but I think that's actually the point.

When something goes wrong, there are more places to look.


What I learned building it

The biggest thing I took away from this project is that RAG isn't just retrieval.

There are a lot of decisions happening before the LLM ever sees the prompt.

The source data matters.

The cleaning matters.

The chunking matters.

The embeddings matter.

The retrieval strategy matters.

And even after retrieving something that looks relevant, you still need to ask whether it is actually sufficient.

That's why I started thinking about RAG less as:

"Search some vectors and ask an LLM."

and more as:

"Build a pipeline that can inspect its own intermediate state and recover when one path isn't good enough."

That's what I wanted RAG Studio to explore.

Not just getting an answer.

But being able to look at the pipeline and understand how the system got there.


Try it

I've made the project available here:

RAG Studio → rayulumukku.com/projects/rag-studio/

If you're building RAG systems yourself, I'd be interested in hearing how you're handling retrieval evaluation, document preprocessing, and fallback strategies.

The interesting part of RAG isn't just the final answer. It's everything that happens before it.

Top comments (1)

Collapse
 
marcusykim profile image
Marcus Kim

Redacting emails and API keys before vectorization is the right boundary, because deleting them from the source later does not remove what has already reached an embedding index. Making the DAG inspectable down to chunk payloads, 384-dimensional vectors, latency, and the CRAG verdict turns the system into a debugger instead of a chatbot wrapper. The next product-level move is to persist those traces as an evaluation dataset-especially cases that trigger web fallback-while recording source provenance, since better recovery can otherwise trade a visible retrieval failure for a harder-to-detect grounding problem.