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 case | Aanbevolen model | Kosten/1M tokens |
|---|---|---|
| Classificatie & tagging | GPT-4o-mini / Claude Haiku | ~$0.25 |
| Samenvatting & extractie | GPT-4o / Claude Sonnet | ~$3-5 |
| Complexe analyse & redenering | GPT-4o / Claude Opus | ~$15-75 |
| Embedding & zoeken | text-embedding-3-small | ~$0.02 |
| Eenvoudige chat | GPT-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:
- Rate limiting per tenant — voorkomt misbruik en onverwachte kosten
- Response caching — identieke queries raken de AI-provider niet opnieuw
- Usage tracking — essentieel voor kostentoewijzing en usage-based billing
- 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:
- Start klein — één feature, één model, bewijs de waarde
- Bouw abstracties — een AI-service layer maakt je flexibel
- Beheer kosten actief — caching, tiered models, token budgets
- Valideer alles — AI-output is onvoorspelbaar, behandel het zo
- 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.