RAG
Ground the agent in your documents — agentic retrieval as a tool (default) or always-on prompt injection, with citations flowing through the tool-call mechanism.
Retrieval-Augmented Generation grounds the agent's answers in your own documents: retrieve the
passages relevant to a question, and let the model answer from them. The
@dudousxd/nestjs-agent-rag package ships the batteries — chunking,
ingestion, an embedding-backed retriever, and vector stores — while the retrieval seam itself lives
in -core, so the agent runtime stays storage-agnostic.
The pieces
EmbeddingProvider(core SPI) — text → vectors.aiSdkEmbedding(model)implements it over any Vercel AI SDK embedding model.VectorStore(-rag) —upsert+search+remove+listDocuments. ShipsMemoryVectorStore(in-JS cosine) andPgVectorStore(pgvector). The extension point for Qdrant/Pinecone/etc.Retriever(core SPI) —retrieve(query) → Passage[].EmbeddingRetrieverbridges an embedder + a vector store into one.
import { EmbeddingRetriever, MemoryVectorStore, ingestDocuments } from '@dudousxd/nestjs-agent-rag';
import { aiSdkEmbedding } from '@dudousxd/nestjs-agent-ai-sdk';
import { openai } from '@ai-sdk/openai';
const embedder = aiSdkEmbedding(openai.embedding('text-embedding-3-small'));
const store = new MemoryVectorStore();
// chunk → embed → upsert. Chunk ids are `${doc.id}#${n}`, so re-ingesting overwrites in place.
await ingestDocuments(
[{ id: 'refunds', text: refundPolicyText, source: 'docs/refunds' }],
{ embedder, store, chunkSize: 800 },
);
const retriever = new EmbeddingRetriever(embedder, store);Two modes
The retriever becomes a search_knowledge(query) tool. Each turn the model decides whether and
what to search — no wasted embedding on "hi, thanks!", refined queries, multiple searches when it
needs them. This is the modern default.
import { createRetrievalTool } from '@dudousxd/nestjs-agent-rag';
import { provideAgentTool } from '@dudousxd/nestjs-agent';
@Module({
imports: [AgentModule.forRoot({ /* … */ })],
providers: [provideAgentTool(createRetrievalTool(retriever))],
})
export class AppModule {}Best for chat assistants and mixed conversations, and when you're running a capable model that tool-calls reliably.
Before the model runs, the loop embeds the user's message verbatim, retrieves the top-K passages, and folds them into the system prompt. The model always sees retrieved context — no decision, guaranteed grounding.
AgentModule.forRoot({
// …model, store, actorResolver…
retrieval: { mode: 'inject', retriever, topK: 5 },
});Best for "answer strictly from these docs" bots (support KBs, document Q&A), weaker models that won't reliably call a tool, or when you must guarantee retrieval every turn. The cost: it embeds and searches on every message, using the raw message as the query.
Which one?
Rule of thumb: tool = the model searches when it decides it needs to, with the query it forms;
inject = the library always searches, using the user's message as the query, before generating.
Default to tool; reach for inject when you need deterministic grounding.
Citations
Retrieved passages carry a source, and they surface as citations through the tool-call
mechanism — no new message field:
- Agentic mode: the
search_knowledgetool's output is the passages. It persists as a tool call and renders as a tool part in the chat UI, and shows up in the-telescopeAgent tab. - Inject mode: the loop records a synthetic
auto_executedretrievetool call (output = passages) against the assistant message it informed — the same surface, so citations render identically.
Either way, each retrieval also emits an aviary:agent:retrieved diagnostics event
({ runId, query, count }) — subscribe for retrieval metrics without instrumenting your code.
Better relevance: hybrid + reranking
Because every retriever — vector, keyword, fused, reranked — is a core Retriever, they compose.
Two standard precision upgrades:
Hybrid search fuses dense vector similarity with lexical BM25 (so exact keyword matches aren't
lost to fuzzy embeddings). HybridRetriever uses Reciprocal Rank Fusion — no score normalization
needed between the two scales. Feed both retrievers the same chunks so their ids line up:
import {
EmbeddingRetriever, KeywordRetriever, HybridRetriever,
chunkDocuments, ingestChunks,
} from '@dudousxd/nestjs-agent-rag';
const chunks = chunkDocuments(docs, { chunkSize: 800 });
await ingestChunks(chunks, { embedder, store }); // dense
const keyword = new KeywordRetriever();
keyword.add(chunks); // lexical BM25 (same chunk ids)
const hybrid = new HybridRetriever([new EmbeddingRetriever(embedder, store), keyword]);Reranking over-fetches cheap candidates, then reorders them with a stronger model (a
cross-encoder / Cohere / Voyage rerank endpoint behind the Reranker SPI):
import { RerankingRetriever } from '@dudousxd/nestjs-agent-rag';
const retriever = new RerankingRetriever(hybrid, reranker, { fetchTopK: 20 });
// fetch 20 candidates → rerank → return the top few. Still just a `Retriever`.Any of these drops straight into createRetrievalTool(retriever) or forRoot({ retrieval }).
Production storage: pgvector or Redis
MemoryVectorStore is for tests and small corpora. For scale, two shipped adapters — both take an
injected client (no driver dependency) and expose ensureSchema():
pgvector (Postgres):
import { Pool } from 'pg';
import { PgVectorStore } from '@dudousxd/nestjs-agent-rag';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const store = new PgVectorStore(
{ query: (sql, params) => pool.query(sql, params).then((r) => r.rows) },
{ dimensions: 1536 }, // match your embedding model
);
await store.ensureSchema(); // vector extension + table + HNSW cosine indexRedis (RediSearch — Redis Stack / Redis 8+), the ecosystem-native option if you already run Redis:
import { createClient } from 'redis';
import { RedisVectorStore } from '@dudousxd/nestjs-agent-rag';
const client = createClient({ url: process.env.REDIS_URL });
await client.connect();
const store = new RedisVectorStore(
{ sendCommand: (args) => client.sendCommand(args) },
{ dimensions: 1536, filterableFields: ['owner'] }, // TAG fields you can filter by
);
await store.ensureSchema(); // FT.CREATE — HNSW cosine indexSwap MemoryVectorStore for either in the wiring above; nothing else changes. For any other backend
(Qdrant, Pinecone, Weaviate), implement the VectorStore SPI — upsert + search + remove + listDocuments.
Ingesting uploads automatically
If your app already stores files with @dudousxd/nestjs-media, the
@dudousxd/nestjs-agent-rag-media package turns every upload into
searchable knowledge with no glue: attach a file and it's extracted, chunked, embedded, and indexed —
scoped to its owner, and removed again on delete.
import { AgentMediaIngestionModule } from '@dudousxd/nestjs-agent-rag-media';
AgentMediaIngestionModule.forRoot({
store, // the same VectorStore you retrieve from
embedder,
collections: ['knowledge-base'],
readFile: (disk, path) => media.disk(disk).get(path),
});It couples to media purely through the aviary:media:* diagnostics channel (no hard dependency), and
stamps each chunk with the owner from the server-side attach record. That last part is what makes
per-user RAG a real boundary — wrap the retriever with FilteredRetriever:
import { EmbeddingRetriever, FilteredRetriever } from '@dudousxd/nestjs-agent-rag';
const forThisUser = new FilteredRetriever(new EmbeddingRetriever(embedder, store), {
ownerId: actor.id,
});See the rag-media package reference for custom extractors (PDF/DOCX), durable ingestion, and the emitted diagnostics.
Related
@dudousxd/nestjs-agent-rag— the package reference- Tools —
readtools, which is what agentic retrieval rides on - Governed SQL — the other read seam; a
VectorStorecan ride the same-datapool
Multi-agent
Declare named agents as @Agent classes, let an orchestrator hand off to sub-agents via handoff, and run each delegation as a durable child run.
Cost & Governance
The usage ledger and read-model — spend per model and actor, a gateway's reported cost preferred over a cache-aware estimate, daily quota, and the console that surfaces it all.