LiyaEngine
All tutorials

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 citations

Every 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.ts

Step 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 search

Step 4 — Tune retrieval parameters

The two parameters that most affect response quality:

top_k — how many chunks to retrieve

top_kBest for
3–4Narrow factual questions with one clear answer
6–8Broader 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

ThresholdEffect
0.5+Tight — only high-confidence matches. Can miss relevant chunks for vague queries.
0.3–0.45Balanced — recommended starting point
0.2–0.3Loose — 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:

FlagMeaning
no_context_retrievedQuery 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 intent prompts to control exactly how the retrieved context is used
  • Multi-source retrieval — seed from multiple source types (pdf, api, database) with different source_type labels and filter at query time
  • Streaming responses — switch to the /v1/stream endpoint for long-form answers
  • Analytics — log retrieval_ms, sources, and confidence to track retrieval quality over time

Ready to build?

Get your API key and start integrating in minutes.