Build a RAG Knowledge Pipeline
Seed your knowledge base, configure retrieval parameters, and wire up grounded generation — a complete end-to-end RAG pipeline on LiyaEngine.
Retrieval-Augmented Generation (RAG) is the backbone of every LiyaEngine response. Instead of relying on a model's parametric memory (which can hallucinate), the engine retrieves relevant passages from your knowledge base at query time and uses them as the authoritative source for its answer.
This tutorial covers the full pipeline — from chunking and embedding your documents to serving grounded responses in production.
What you'll build:
- A document ingestion script that chunks, embeds, and stores your content
- A retrieval-augmented API endpoint with configurable relevance thresholds
- A verification layer that detects and flags ungrounded responses
How the pipeline works
User query
↓
Embed query (text-embedding-3-small)
↓
Vector similarity search (pgvector)
↓ top_k chunks above min_relevance
Assemble context block (titled sections)
↓
LLM generation (GPT-4o / Claude 3.5)
↓
Grounding verification
↓
Structured JSON response with citationsEvery step is handled by LiyaEngine. Your job is to seed the right content and tune the retrieval parameters.
Step 1 — Prepare your content
RAG quality depends almost entirely on the quality and structure of your source documents. Before seeding:
Do:
- Break documents into logical sections (one concept per chunk)
- Include headings and titles — they appear in citations
- Use clear, factual prose — avoid marketing fluff in knowledge base docs
Avoid:
- Chunks longer than ~800 tokens (context gets diluted)
- Duplicate content (creates noise in retrieval)
- HTML or markdown with heavy formatting artefacts
A practical chunk structure that works well:
# Section Title
Brief intro sentence.
Key point 1.
Key point 2.
Key point 3.Step 2 — Seed via the API
LiyaEngine's knowledge API accepts raw text chunks with metadata. Each chunk gets embedded and stored against your tenant ID.
// scripts/seed-knowledge.ts
import { PrismaClient } from "@prisma/client";
import { KnowledgeRAGService } from "@liyaengine/core";
import fs from "fs";
import path from "path";
const prisma = new PrismaClient();
const rag = new KnowledgeRAGService(prisma);
const TENANT_ID = process.env.LIYA_TENANT_ID!;
const CONTENT_DIR = path.join(process.cwd(), "knowledge");
function chunkMarkdown(content: string, docId: string) {
const sections = content.split(/\n(?=#{2,3}\s)/);
return sections
.filter(s => s.trim().length > 50)
.map((section, i) => {
const headingMatch = section.match(/^#{2,3}\s(.+)/);
const heading = headingMatch?.[1] ?? `Section ${i + 1}`;
return {
content_text: section.trim(),
source_type: "chat:docs",
metadata: { source_id: docId, document_id: docId, section: heading },
};
});
}
async function seed() {
const files = fs.readdirSync(CONTENT_DIR)
.filter(f => f.endsWith(".md") || f.endsWith(".mdx"));
let total = 0;
for (const file of files) {
const content = fs.readFileSync(path.join(CONTENT_DIR, file), "utf-8");
const docId = file.replace(/\.(mdx|md)$/, "");
const chunks = chunkMarkdown(content, docId);
await rag.upsertChunks(TENANT_ID, chunks);
console.log(`✓ ${docId} — ${chunks.length} chunks`);
total += chunks.length;
}
console.log(`\nDone: ${files.length} files → ${total} chunks`);
}
seed().finally(() => prisma.$disconnect());Run:
LIYA_TENANT_ID=your-tenant-id npx ts-node scripts/seed-knowledge.tsStep 3 — Query with retrieval
Once seeded, every /v1/run call with retrieval configured will automatically embed the user's query, search your knowledge base, and inject the top results into the LLM context.
const response = await fetch("https://api.liyaengine.com/v1/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.LIYA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
pack: "enterprise-chat",
intent: "answer-question",
input: {
message: "What authentication methods does LiyaEngine support?",
user: { id: "user-123", role: "user", permissions: [] },
},
retrieval: {
sources: [], // empty = search all seeded sources
top_k: 6, // return up to 6 chunks
min_relevance: 0.35, // minimum cosine similarity threshold
},
guardrails: {
grounding: { action: "warn_if_ungrounded" },
content: { policy: "professional" },
},
}),
});
const { data } = await response.json();
console.log(data.output.content); // grounded answer
console.log(data.output.sources); // [{doc, section, relevance}]
console.log(data.retrieval_ms); // time spent on vector searchStep 4 — Tune retrieval parameters
The two parameters that most affect response quality:
top_k — how many chunks to retrieve
top_k | Best for |
|---|---|
| 3–4 | Narrow factual questions with one clear answer |
| 6–8 | Broader questions that span multiple sections |
| 10+ | Summarisation tasks over large document sets |
Start with top_k: 6 and adjust based on response quality.
min_relevance — cosine similarity threshold
| Threshold | Effect |
|---|---|
| 0.5+ | Tight — only high-confidence matches. Can miss relevant chunks for vague queries. |
| 0.3–0.45 | Balanced — recommended starting point |
| 0.2–0.3 | Loose — retrieves more context, higher chance of noisy results |
Rule of thumb: If users report "I don't know" responses for questions your docs clearly answer, lower
min_relevance. If responses contain off-topic information, raise it.
Step 5 — Scope retrieval to specific sources
For multi-domain applications, you can restrict retrieval to specific document IDs:
const response = await fetch("https://api.liyaengine.com/v1/run", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.LIYA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
pack: "enterprise-chat",
intent: "answer-question",
input: { message: "What is the parental leave policy?", user: { id: "user-123", role: "user", permissions: [] } },
retrieval: {
sources: ["hr-handbook", "benefits-guide"], // only search these docs
top_k: 5,
min_relevance: 0.35,
},
}),
});
const { data } = await response.json();This is useful when you have separate knowledge bases for different user roles or product areas and want to ensure answers only draw from the appropriate source.
Step 6 — Verify grounding in production
The grounding verification layer runs automatically when you set grounding.action. You can inspect the result in the response metadata:
const { data } = await response.json();
const { output } = data;
if (!output.metadata.grounding_verified) {
console.warn("Response may be ungrounded:", output.metadata.flags);
// flags: ["no_context_retrieved", "ungrounded_phrase:i_believe"]
}
// For high-stakes use cases, block ungrounded responses entirely
if (!output.metadata.grounding_verified) {
return { error: "Could not verify response against knowledge base." };
}Possible flags:
| Flag | Meaning |
|---|---|
no_context_retrieved | Query returned no chunks above min_relevance |
ungrounded_phrase:* | Response contains hedging language ("I believe", "generally speaking") suggesting the model drew on parametric knowledge |
For high-stakes use cases (compliance, legal, medical), handle grounding_verified: false by showing a disclaimer or declining to display the response.
Step 7 — Keep the knowledge base fresh
For production deployments, run the seed script whenever your docs change. A simple approach is a CI/CD step triggered on docs commits:
# .github/workflows/seed-knowledge.yml
on:
push:
paths:
- "knowledge/**"
jobs:
seed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx ts-node scripts/seed-knowledge.ts
env:
LIYA_TENANT_ID: ${{ secrets.LIYA_TENANT_ID }}
DATABASE_URL: ${{ secrets.DATABASE_URL }}For incremental updates, upsertChunks is idempotent — re-seeding the same document replaces existing chunks without creating duplicates.
What's next
- Custom intents — define your own
intentprompts to control exactly how the retrieved context is used - Multi-source retrieval — seed from multiple source types (
pdf,api,database) with differentsource_typelabels and filter at query time - Streaming responses — switch to the
/v1/streamendpoint for long-form answers - Analytics — log
retrieval_ms,sources, andconfidenceto track retrieval quality over time
Ready to build?
Get your API key and start integrating in minutes.