Skip to main content

Testing your RAG code

@devilsdev/rag-pipeline-utils/testing has deterministic, offline stand-ins for the parts of a pipeline that normally call a model. Tests that use them need no network and no API keys, and give the same result on every run.

ExportWhat it is
HashEmbedderEmbedder that hashes words into a vector. Texts that share words come out similar, so retrieval ranks sensibly
EchoLLMLLM that answers with the top retrieved document, or an answer you set, and records every call
fixtures.documentsThree short policy documents (vacation, remote work, expenses)
createTestPipeline()A pipeline with HashEmbedder, a MemoryRetriever holding the documents, and EchoLLM

A test pipeline in one call​

import { createTestPipeline } from "@devilsdev/rag-pipeline-utils/testing";

const { pipeline, llm } = await createTestPipeline();
const result = await pipeline.run({
query: "How many vacation days do I get?",
options: { citations: true },
});

console.log(result.results[0].id); // "vacation"
console.log(result.citations.groundednessScore); // 1
console.log(llm.calls.length); // 1

Pass your own documents, an LLM, or llm: null for retrieval only. pipeline takes extra createRagPipeline options, such as a reranker:

import { createTestPipeline } from "@devilsdev/rag-pipeline-utils/testing";

const { pipeline } = await createTestPipeline({
documents: [
{ id: "cats", content: "Cats sleep up to sixteen hours a day." },
{ id: "dogs", content: "Dogs need daily walks." },
],
llm: null,
});

const result = await pipeline.run({ query: "How long do cats sleep?" });
console.log(result.results[0].id); // "cats"
console.log(result.answer); // null

Asserting on what the model received​

EchoLLM keeps every call in calls: the query, the documents and the options (including the signal). Use it to check what your code sends to the model, for example that guardrails removed a document before generation.

import {
EchoLLM,
createTestPipeline,
} from "@devilsdev/rag-pipeline-utils/testing";

const llm = new EchoLLM({ answer: "Twenty days." });
const { pipeline } = await createTestPipeline({ llm });

await pipeline.run({ query: "vacation" });

const [call] = llm.calls;
console.log(call.query); // "vacation"
console.log(call.docs[0].id); // "vacation"

answer can also be a function of the query and documents, (query, docs) => string. EchoLLM streams too (generateStream), word by word.

In your own plugins' tests​

HashEmbedder and EchoLLM pass validatePlugin(stage, plugin, { registry: true }), so they can stand in for real components anywhere a pipeline stage is expected, including checkAnswer, computeSemanticSimilarity and your own plugins' tests.