The chatbot on this site has no vector database

My whole CV is 2,000 tokens and the context window is a million. Measure your corpus before you reach for RAG.

There is a chat button on this site. Ask it about my experience and it answers from my actual CV, not from the model's imagination. It has no vector database, no embeddings, no retrieval step, and no chunking. It did not need any of them, and neither do most of the bots that ship with all four.

The default architecture people reach for is RAG: chunk your documents, embed the chunks, store the vectors, embed the question, retrieve the nearest neighbours, stuff them into the prompt. It is a genuinely good design for the problem it solves.

The problem it solves is having more knowledge than fits in the context window. My entire professional history is about 2,000 tokens. The context window is a million.

~2K

Tokens of knowledge

my whole CV

1M

Context window

of the model

0

Vector DBs needed

500x headroom

The whole architecture

portfolio.tssingle source of truthbuildFacts()at cold startsystem promptsent every requestanswergrounded
No retrieval step, because there is nothing to retrieve from. The facts are simply always present.

The function reads the same TypeScript module the website renders from, flattens it into text, and puts it in the system instruction. That is the retrieval layer. It is a function call.

netlify/functions/chat.mtstypescript
import { EXPERIENCES, PROJECTS, OSS_SUMMARY /* ... */ } from "../../client/src/data/portfolio";

function buildFacts(): string {
  const experience = EXPERIENCES.map(
    (e) => `- ${e.title} at ${e.company}, ${e.period}. ${e.description}`
  ).join("\n");
  // ...projects, education, skills, open source
  return [`EXPERIENCE:\n${experience}`, /* ... */].join("\n");
}

Grounding is a prompt problem here, not a retrieval problem

Since every fact is always in context, the only real question is whether the model stays inside them. Three instructions do most of the work:

The bug that made it look broken

First deploy, every question came back with a 502. The function was running, the key was valid, the upstream call returned 200, and the answer was empty.

I was reading output_text off the response. That field is a convenience property the official SDKs synthesise. The REST API does not return it. The actual text lives at steps[].content[].text.

the fixtypescript
// Reads the documented wire shape, and joins consecutive text blocks.
for (const step of steps) {
  for (const block of step.content ?? []) {
    if (block?.type === "text") chunks.push(block.text);
  }
}

Worth saying out loud: I had read the SDK example and assumed the REST response matched it. Convenience properties are a real trap when you drop down to raw HTTP, because they look exactly like fields.

The unglamorous parts that actually matter

When you should actually reach for RAG

When the knowledge genuinely does not fit, or changes far faster than you deploy, or is large enough that sending all of it on every request costs real money. Those are good reasons and they are common.

"I am building a chatbot" is not one of them. Measure your corpus first. If it fits in the prompt, put it in the prompt, and spend the afternoon you just saved on the system instruction instead.