Terug naar blog
AILLMSaaS architectuurRAGprompt engineeringkostenbeheerTypeScript

AI-features bouwen in je SaaS: van LLM tot productie

Door SaaS Masters26 maart 202612 min leestijd

AI is niet langer een buzzword — het is een kernonderdeel van moderne SaaS-producten. Van slimme zoekresultaten tot automatische contentgeneratie: je klanten verwachten AI-functionaliteit. Maar hoe bouw je dit verantwoord, schaalbaar en kostenefficiënt in je product?

In dit artikel nemen we je stap voor stap mee: van het kiezen van het juiste model tot productie-klare implementatie met caching, fallbacks en kostenbeheer.

Waarom AI-features in je SaaS?

De markt verschuift snel. SaaS-producten die AI integreren zien:

  • 40-60% hogere engagement door intelligente suggesties
  • Lagere supportkosten door AI-gestuurde selfservice
  • Sterkere retentie — AI-features worden snel onmisbaar
  • Premium pricing mogelijkheden voor AI-tiers

Maar let op: slecht geïmplementeerde AI schaadt je product meer dan het helpt. Hallucinaties, trage responses en onvoorspelbare kosten zijn reële risico's.

De architectuur: waar past AI in je stack?

Optie 1: API-first (aanbevolen voor de meeste SaaS)

[Frontend] → [Je API] → [AI Service Layer] → [OpenAI / Anthropic / etc.]
                              ↓
                        [Cache Layer]
                              ↓
                        [Vector DB]

Dit is de meest flexibele aanpak. Je AI-laag zit achter je eigen API, waardoor je volledige controle hebt over:

  • Rate limiting per tenant
  • Caching van veelvoorkomende queries
  • Model-switching zonder frontend-wijzigingen
  • Kostenallocatie per klant

Optie 2: Edge/Client-side (voor real-time features)

Voor features zoals autocomplete of real-time suggesties kun je kleinere modellen dichter bij de gebruiker draaien. Denk aan WebLLM of ONNX-modellen in de browser.

Het juiste model kiezen

Niet elke feature heeft GPT-4 nodig. Hier is een praktisch framework:

Use caseAanbevolen modelKosten/1M tokens
Classificatie & taggingGPT-4o-mini / Claude Haiku~$0.25
Samenvatting & extractieGPT-4o / Claude Sonnet~$3-5
Complexe analyse & redeneringGPT-4o / Claude Opus~$15-75
Embedding & zoekentext-embedding-3-small~$0.02
Eenvoudige chatGPT-4o-mini / Claude Haiku~$0.25

Pro tip: Start altijd met het kleinste model dat werkt. Upgrade alleen als de kwaliteit onvoldoende is voor je use case.

Implementatie: een AI-service layer bouwen

Hier is een productie-klare opzet in Node.js/TypeScript:

// lib/ai/ai-service.ts
import Anthropic from "@anthropic-ai/sdk";
import { Redis } from "ioredis";
import { createHash } from "crypto";

interface AIRequestOptions {
  prompt: string;
  systemPrompt?: string;
  model?: string;
  maxTokens?: number;
  tenantId: string;
  cacheTtl?: number; // seconds
}

export class AIService {
  private anthropic: Anthropic;
  private redis: Redis;

  constructor() {
    this.anthropic = new Anthropic();
    this.redis = new Redis(process.env.REDIS_URL!);
  }

  async complete(options: AIRequestOptions): Promise<string> {
    const {
      prompt,
      systemPrompt,
      model = "claude-sonnet-4-20250514",
      maxTokens = 1024,
      tenantId,
      cacheTtl = 3600,
    } = options;

    // 1. Check rate limit
    await this.checkRateLimit(tenantId);

    // 2. Check cache
    const cacheKey = this.getCacheKey(prompt, systemPrompt, model);
    const cached = await this.redis.get(cacheKey);
    if (cached) return cached;

    // 3. Call AI provider
    const response = await this.anthropic.messages.create({
      model,
      max_tokens: maxTokens,
      system: systemPrompt || "Je bent een behulpzame assistent.",
      messages: [{ role: "user", content: prompt }],
    });

    const result =
      response.content[0].type === "text" ? response.content[0].text : "";

    // 4. Cache result
    await this.redis.setex(cacheKey, cacheTtl, result);

    // 5. Track usage
    await this.trackUsage(tenantId, response.usage);

    return result;
  }

  private async checkRateLimit(tenantId: string): Promise<void> {
    const key = \`rate:${tenantId}:${Math.floor(Date.now() / 60000)}\`;
    const count = await this.redis.incr(key);
    if (count === 1) await this.redis.expire(key, 120);
    if (count > 100) {
      throw new Error("AI rate limit exceeded");
    }
  }

  private getCacheKey(...parts: (string | undefined)[]): string {
    const hash = createHash("sha256")
      .update(parts.filter(Boolean).join("|"))
      .digest("hex");
    return \`ai:cache:${hash}\`;
  }

  private async trackUsage(
    tenantId: string,
    usage: { input_tokens: number; output_tokens: number }
  ): Promise<void> {
    const month = new Date().toISOString().slice(0, 7);
    const key = \`usage:${tenantId}:${month}\`;
    await this.redis.hincrby(key, "input_tokens", usage.input_tokens);
    await this.redis.hincrby(key, "output_tokens", usage.output_tokens);
  }
}

Wat deze service doet:

  1. Rate limiting per tenant — voorkomt misbruik en onverwachte kosten
  2. Response caching — identieke queries raken de AI-provider niet opnieuw
  3. Usage tracking — essentieel voor kostentoewijzing en usage-based billing
  4. Model-abstractie — wissel van provider zonder codewijzigingen

Prompt engineering: de sleutel tot betrouwbare output

Slechte prompts = slechte resultaten. Hier zijn bewezen patronen:

Structured Output met JSON

const systemPrompt = \`Je bent een product-categorisatie engine.
Analyseer de productbeschrijving en retourneer UITSLUITEND valid JSON:

{
  "category": "string (één van: software, hardware, service, consultancy)",
  "confidence": "number (0-1)",
  "tags": ["string array met relevante tags"],
  "summary": "string (max 100 woorden)"
}

Geen extra tekst buiten het JSON-object.\`;

Few-shot examples

const prompt = \`Classificeer de volgende supporttickets:

Voorbeeld 1:
Input: "Ik kan niet inloggen sinds de update"
Output: { "category": "authentication", "priority": "high", "sentiment": "frustrated" }

Voorbeeld 2:
Input: "Is er een API voor bulk imports?"
Output: { "category": "feature_request", "priority": "medium", "sentiment": "neutral" }

Nu jouw beurt:
Input: "${ticketText}"
Output:\`;

Guardrails inbouwen

function validateAIResponse<T>(
  response: string,
  schema: z.ZodSchema<T>
): T | null {
  try {
    // Strip eventuele markdown code blocks
    const cleaned = response
      .replace(/\`\`\`json?\n?/g, "")
      .replace(/\`\`\`/g, "")
      .trim();
    const parsed = JSON.parse(cleaned);
    return schema.parse(parsed);
  } catch {
    return null; // Fallback naar handmatige verwerking
  }
}

RAG: je AI slimmer maken met je eigen data

Retrieval-Augmented Generation (RAG) is dé manier om AI-features te bouwen die je eigen productdata gebruiken — zonder het model te fine-tunen.

Stap 1: Embeddings genereren

import OpenAI from "openai";

const openai = new OpenAI();

async function generateEmbedding(text: string): Promise<number[]> {
  const response = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: text,
  });
  return response.data[0].embedding;
}

Stap 2: Opslaan in een vector database

Met pgvector (PostgreSQL extensie) hoef je geen aparte database te draaien:

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  tenant_id TEXT NOT NULL,
  content TEXT NOT NULL,
  embedding vector(1536),
  metadata JSONB DEFAULT '{}'
);

CREATE INDEX ON documents
  USING ivfflat (embedding vector_cosine_ops)
  WITH (lists = 100);

Stap 3: Zoeken en context meegeven

async function ragQuery(tenantId: string, question: string): Promise<string> {
  const questionEmbedding = await generateEmbedding(question);

  // Zoek relevante documenten
  const docs = await db.query(
    \`SELECT content, 1 - (embedding <=> $1::vector) as similarity
     FROM documents
     WHERE tenant_id = $2
     ORDER BY embedding <=> $1::vector
     LIMIT 5\`,
    [\`[${questionEmbedding.join(",")}]\`, tenantId]
  );

  // Bouw context op
  const context = docs.rows.map((d) => d.content).join("\n\n---\n\n");

  return aiService.complete({
    tenantId,
    systemPrompt: \`Beantwoord de vraag op basis van de volgende context.
Als het antwoord niet in de context staat, zeg dat eerlijk.

Context:
${context}\`,
    prompt: question,
  });
}

Kostenbeheer: de verborgen uitdaging

AI-kosten kunnen exploderen als je niet oplet. Hier zijn concrete strategieën:

1. Tiered model routing

function selectModel(complexity: "low" | "medium" | "high"): string {
  switch (complexity) {
    case "low":
      return "claude-haiku-4-20250414"; // $0.25/1M
    case "medium":
      return "claude-sonnet-4-20250514"; // $3/1M
    case "high":
      return "claude-opus-4-20250514"; // $15/1M
  }
}

// Automatische complexiteitsdetectie
function estimateComplexity(prompt: string): "low" | "medium" | "high" {
  const wordCount = prompt.split(/\s+/).length;
  if (wordCount < 50) return "low";
  if (wordCount < 200) return "medium";
  return "high";
}

2. Token budgets per tenant

async function checkBudget(tenantId: string): Promise<boolean> {
  const month = new Date().toISOString().slice(0, 7);
  const usage = await redis.hgetall(\`usage:${tenantId}:${month}\`);
  const totalTokens =
    parseInt(usage.input_tokens || "0") +
    parseInt(usage.output_tokens || "0");

  const plan = await getTenantPlan(tenantId);
  return totalTokens < plan.monthlyTokenLimit;
}

3. Aggressive caching

  • Cache identieke queries (zoals hierboven)
  • Cache embeddings — herbereken niet bij elke request
  • Gebruik semantic caching: als een nieuwe query >95% lijkt op een gecachte query, gebruik het gecachte antwoord

Productie-checklist

Voordat je AI-features live zet, loop deze checklist af:

Betrouwbaarheid

  • Fallback wanneer AI-provider down is
  • Retry-logica met exponential backoff
  • Circuit breaker voor provider-outages
  • Timeout configuratie (AI-calls kunnen traag zijn)

Veiligheid

  • Input sanitization (geen prompt injection)
  • Output validatie (check op ongewenste content)
  • PII-filtering vóór het versturen naar externe APIs
  • Audit logging van alle AI-interacties

Kosten

  • Rate limiting per tenant
  • Token budget alerts
  • Automatische model-downgrade bij hoog gebruik
  • Maandelijkse kostenrapportage per feature

UX

  • Streaming responses voor lange outputs
  • Duidelijke loading states
  • "AI-generated" labels waar nodig
  • Feedback-mechanisme (thumbs up/down)

Streaming responses implementeren

Niets is zo frustrerend als 10 seconden naar een spinner staren. Streaming maakt AI-features responsief:

// Next.js API Route met streaming
import { Anthropic } from "@anthropic-ai/sdk";

export async function POST(req: Request) {
  const { prompt, tenantId } = await req.json();

  const anthropic = new Anthropic();

  const stream = anthropic.messages.stream({
    model: "claude-sonnet-4-20250514",
    max_tokens: 2048,
    messages: [{ role: "user", content: prompt }],
  });

  // Return als Server-Sent Events
  return new Response(
    new ReadableStream({
      async start(controller) {
        for await (const event of stream) {
          if (
            event.type === "content_block_delta" &&
            event.delta.type === "text_delta"
          ) {
            controller.enqueue(
              new TextEncoder().encode(\`data: ${JSON.stringify({ text: event.delta.text })}\n\n\`)
            );
          }
        }
        controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
        controller.close();
      },
    }),
    {
      headers: {
        "Content-Type": "text/event-stream",
        "Cache-Control": "no-cache",
        Connection: "keep-alive",
      },
    }
  );
}

Conclusie

AI-features bouwen in je SaaS is geen raketwetenschap, maar het vereist wel degelijke engineering. De sleutel is:

  1. Start klein — één feature, één model, bewijs de waarde
  2. Bouw abstracties — een AI-service layer maakt je flexibel
  3. Beheer kosten actief — caching, tiered models, token budgets
  4. Valideer alles — AI-output is onvoorspelbaar, behandel het zo
  5. Meet en itereer — track welke features waarde leveren

De SaaS-producten die AI het beste integreren zijn niet degene met de meeste features, maar degene die de juiste features betrouwbaar en betaalbaar aanbieden.

Begin vandaag met één concrete use case — en bouw van daaruit.