A RAG chatbot that streams and cites sources has four stages. Skip the source plumbing and you get answers nobody can verify.

Stage 1: embed the query.

import { embed } from 'ai';

const { embedding } = await embed({ model: 'your-embedding-model-id', value: query });

Stage 2: retrieve. Query your vector store with the embedding and take the top chunks. Keep chunk ids and URLs alongside the text.

Stage 3: generate with context.

const result = streamText({
  model: 'your-model-id',
  instructions: 'Answer using only the provided context. Cite sources by id.',
  messages: await convertToModelMessages(messages),
  prompt: undefined, // or append a final user message with the context
});

Build the final prompt as: instructions + retrieved context + conversation. Put the context in the last user message or a dedicated context block, not in instructions.

Stage 4: stream sources. Attach source parts so the client can render citations: use the messageMetadata callback or data parts carrying { id, url, title } per chunk, and render them from message parts on the client.

Rules:
1. Embed with the SDK's embed (renamed from textEmbedding in v6; the codemod is v6/rename-text-embedding-to-embedding). Do not hand-roll the embedding call.
2. Cap retrieved context to the model's window minus headroom for the answer. More chunks is not better; irrelevant chunks degrade answers.
3. Never send raw database rows. Project to { id, title, url, text } before prompt construction.
4. If chunks contain untrusted content, instruct the model to treat context as data, not instructions. Prompt injection via retrieved docs is the classic RAG vulnerability.
5. Cache embeddings for repeated queries. Embedding is a billable call like any other.
6. Verify citations: click through a few source links in the UI and confirm the quoted text exists in the chunk.