LLM's integreren in je webshop — RAG, embeddings en API's
Terug naar blog

LLM's integreren in je webshop — RAG, embeddings en API's

AuthorRuthger Idema
5 mei 202613 min leestijd

RAG, vector databases, OpenAI API's — de technologie bestaat. Maar hoe bouw je dit daadwerkelijk in je webshop? Een technische deep-dive met concrete kosten en implementatiekeuzes.

LLM's integreren in je webshop — RAG, embeddings en API's

De meeste artikelen over AI in e-commerce beschrijven wat mogelijk is. Dit artikel beschrijft hoe je het bouwt — met concrete architectuurbeslissingen, werkende code-voorbeelden, kostencalculaties en de valkuilen die je onderweg tegenkomt.

Dit is een technische deep-dive. Als je liever begint met de businesscase, lees dan eerst ons artikel over AI-gedreven semantic search voor webshops.

Wat een LLM-integratie inhoudt

Een Large Language Model (LLM) is een AI-model getraind op grote hoeveelheden tekst dat zowel tekst begrijpt als genereert. GPT-4, Claude 3, Gemini — dit zijn allemaal LLM's beschikbaar via API.

Integreren in je webshop betekent dat je het model aanroept via een HTTP-request, je eigen data meegeeft als context en het resultaat gebruikt in je applicatie. De drie hoofdtoepassingen voor webshops:

  1. Generatieve toepassingen — productbeschrijvingen schrijven, marketing-emails opstellen, FAQ-antwoorden genereren op basis van je kennisbasis
  2. Begripstoepassingen — zoekopdrachten begrijpen, klantvragen categoriseren, sentiment analyseren op reviews
  3. Conversatietoepassingen — chatbots, productadviseurs, configuratiewizards die door opties leiden

De meest interessante combinatie voor webshops — en ook de meest complexe om te bouwen — is een LLM gekoppeld aan je eigen productdata via RAG.

RAG — Retrieval-Augmented Generation uitgelegd

Een LLM weet veel, maar niet wat er in jouw catalogus staat. GPT-4 weet niet dat jij momenteel een blauwe winterjas in maat M hebt staan voor €129, dat hij 600 gram weegt en waterafstotend is.

De naïeve aanpak is je hele catalogus in de prompt stoppen. Dat werkt niet. Een prompt met 50.000 producten past niet in een context window, kost een fortuin en maakt het model traag en onnauwkeurig. Hoe groter de context, hoe meer het model de neiging heeft relevante informatie te negeren.

RAG is de architectuuroplossing. Het principe is eenvoudig maar de implementatie vereist aandacht.

De RAG-cyclus in vier stappen:
  1. Indexeer — Zet je productdata om naar vector embeddings en sla op in een vector database (eenmalig, daarna incrementeel bij updates)
  2. Retrieve — Wanneer een gebruiker een vraag stelt, zoek de meest relevante producten via vector similarity search
  3. Augment — Voeg die relevante producten als context toe aan de LLM-prompt
  4. Generate — Het LLM genereert een antwoord op basis van die specifieke, actuele context

Het resultaat: een LLM dat accuraat antwoord geeft op basis van jouw catalogus, zonder alles in de prompt te stoppen en zonder dat het model dingen verzint die niet in je assortiment staan.

Technische architectuur

INDEXERINGSPIJPLIJN (eenmalig + incremental updates):

Productdata (Magento/Shopify)
        ↓
Tekst constructie (naam + beschrijving + attributes)
        ↓
Embedding model (OpenAI text-embedding-3-small)
        ↓
Vector database (pgvector / Pinecone)

QUERY-PIJPLIJN (per gebruikersinteractie):

Gebruikersquery
        ↓
Embedding model → query vector
        ↓
Vector similarity search → top-K producten
        ↓
Prompt builder → query + productcontext
        ↓
LLM API (Claude / GPT-4) → antwoord
        ↓
Response parser → geformatteerde output
        ↓
Gebruiker

Code — indexeringsservice in Laravel

php
<?php

namespace App\Services\AI;

use App\Models\Product;
use Illuminate\Support\Facades\DB;
use OpenAI\Laravel\Facades\OpenAI;

class ProductEmbeddingService
{
    /**
     * Genereer en sla embedding op voor één product.
     */
    public function indexProduct(Product $product): void
    {
        $text = $this->buildProductText($product);
        $embedding = $this->generateEmbedding($text);

        // Opslaan in pgvector (PostgreSQL extensie)
        DB::statement(
            'INSERT INTO product_embeddings (product_id, embedding, indexed_at)
             VALUES (?, ?::vector, NOW())
             ON CONFLICT (product_id)
             DO UPDATE SET embedding = EXCLUDED.embedding, indexed_at = NOW()',
            [$product->id, '[' . implode(',', $embedding) . ']']
        );
    }

    /**
     * Verwerk een batch producten efficiënt via de Batch API.
     */
    public function indexBatch(array $productIds): void
    {
        $products = Product::whereIn('id', $productIds)
            ->with(['category', 'attributes'])
            ->get();

        // OpenAI ondersteunt batch embedding — efficiënter en goedkoper
        $texts = $products->map(fn($p) => $this->buildProductText($p))->toArray();

        $response = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $texts,
        ]);

        foreach ($response->embeddings as $index => $embeddingData) {
            $product = $products[$index];
            $embedding = '[' . implode(',', $embeddingData->embedding) . ']';

            DB::statement(
                'INSERT INTO product_embeddings (product_id, embedding, indexed_at)
                 VALUES (?, ?::vector, NOW())
                 ON CONFLICT (product_id)
                 DO UPDATE SET embedding = EXCLUDED.embedding, indexed_at = NOW()',
                [$product->id, $embedding]
            );
        }
    }

    /**
     * Bouw een rijke tekstrepresentatie van het product voor betere embeddings.
     */
    private function buildProductText(Product $product): string
    {
        $parts = [
            'Productnaam: ' . $product->name,
            'Categorie: ' . ($product->category->name ?? ''),
            'Beschrijving: ' . strip_tags($product->description ?? ''),
        ];

        // Voeg attributes toe als sleutel-waarde paren
        foreach ($product->attributes as $attribute) {
            $parts[] = $attribute->label . ': ' . $attribute->value;
        }

        return implode("\n", array_filter($parts));
    }

    private function generateEmbedding(string $text): array
    {
        $response = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $text,
        ]);

        return $response->embeddings[0]->embedding;
    }
}

Code — retrieval en RAG query service

php
<?php

namespace App\Services\AI;

use App\Models\Product;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use OpenAI\Laravel\Facades\OpenAI;

class ProductAdvisorService
{
    public function __construct(
        private ProductEmbeddingService $embeddingService
    ) {}

    /**
     * Beantwoord een klant-vraag op basis van de catalogus via RAG.
     */
    public function answer(string $userQuestion, int $contextProducts = 5): string
    {
        // Stap 1: relevante producten ophalen
        $relevantProducts = $this->retrieveRelevantProducts($userQuestion, $contextProducts);

        if ($relevantProducts->isEmpty()) {
            return 'Ik kon geen passende producten vinden voor jouw vraag. '
                . 'Kun je je zoekopdracht anders formuleren?';
        }

        // Stap 2: productcontext bouwen
        $productContext = $this->buildProductContext($relevantProducts);

        // Stap 3: antwoord genereren via Anthropic Claude
        return $this->generateAnswer($userQuestion, $productContext);
    }

    /**
     * Vector similarity search via pgvector.
     */
    private function retrieveRelevantProducts(string $query, int $limit): Collection
    {
        // Zet query om naar vector
        $queryEmbedding = OpenAI::embeddings()->create([
            'model' => 'text-embedding-3-small',
            'input' => $query,
        ])->embeddings[0]->embedding;

        $vectorStr = '[' . implode(',', $queryEmbedding) . ']';

        // Cosine distance search — haal alleen voorradige producten op
        $results = DB::select("
            SELECT p.id, 1 - (pe.embedding <=> ?::vector) as similarity
            FROM product_embeddings pe
            JOIN products p ON p.id = pe.product_id
            WHERE p.is_active = true
              AND p.qty > 0
              AND (1 - (pe.embedding <=> ?::vector)) > 0.6
            ORDER BY pe.embedding <=> ?::vector
            LIMIT ?
        ", [$vectorStr, $vectorStr, $vectorStr, $limit]);

        $productIds = collect($results)->pluck('id');

        return Product::whereIn('id', $productIds)
            ->with(['category'])
            ->get()
            ->sortBy(fn($p) => array_search($p->id, $productIds->toArray()));
    }

    private function buildProductContext(Collection $products): string
    {
        return $products->map(fn($product) => implode("\n", [
            '---',
            'Product: ' . $product->name,
            'Prijs: €' . number_format($product->price, 2, ',', '.'),
            'Categorie: ' . ($product->category->name ?? 'Onbekend'),
            'Beschrijving: ' . substr(strip_tags($product->description ?? ''), 0, 300),
            'Op voorraad: Ja',
            'URL: /product/' . $product->url_key,
        ]))->join("\n");
    }

    private function generateAnswer(string $question, string $context): string
    {
        // Anthropic Claude via HTTP client
        $response = \Illuminate\Support\Facades\Http::withHeaders([
            'x-api-key' => config('services.anthropic.api_key'),
            'anthropic-version' => '2023-06-01',
            'content-type' => 'application/json',
        ])->post('https://api.anthropic.com/v1/messages', [
            'model' => 'claude-3-haiku-20240307',
            'max_tokens' => 600,
            'system' => 'Je bent een behulpzame productadviseur. '
                . 'Beantwoord vragen uitsluitend op basis van de meegeleverde producten. '
                . 'Als geen enkel product past bij de vraag, zeg dat eerlijk. '
                . 'Verzin geen producten of specificaties die niet in de context staan. '
                . 'Schrijf in het Nederlands. Wees concreet en bondig.',
            'messages' => [
                [
                    'role' => 'user',
                    'content' => "BESCHIKBARE PRODUCTEN:\n{$context}\n\nKLANT VRAAG:\n{$question}",
                ],
            ],
        ]);

        return $response->json('content.0.text') ?? 'Er is een fout opgetreden. Probeer het opnieuw.';
    }
}

Model keuze en kostenvergelijking

De keuze voor een LLM-provider heeft grote impact op kosten, snelheid en kwaliteit.

OpenAI

GPT-4o (flagship):
  • Input: $2,50 per 1M tokens | Output: $10,00 per 1M tokens
  • Beste voor: complexe redenering, code generatie, meertalige content
  • Latency: 1-3 seconden
GPT-4o mini (efficiënt):
  • Input: $0,15 per 1M tokens | Output: $0,60 per 1M tokens
  • Beste voor: hoog volume, eenvoudigere taken, real-time toepassingen
  • Latency: <1 seconde

Anthropic (Claude)

Claude 3.5 Sonnet:
  • Input: $3,00 per 1M tokens | Output: $15,00 per 1M tokens
  • Beste voor: lange context, complexe instructie-opvolging
  • Sterk in: weigeren van hallucinated content bij beperkende prompts
Claude 3 Haiku (snel en goedkoop):
  • Input: $0,25 per 1M tokens | Output: $1,25 per 1M tokens
  • Beste voor: real-time chatbot interacties, hoog volume, lage latency
  • Latency: <0,5 seconde

Kostencalculatie — productadviseur in productie

Aanname: 10.000 gesprekken per maand, gemiddeld 4 berichten per gesprek, gemiddeld 1.000 tokens per bericht (inclusief productcontext die ~600 tokens is).

10.000 gesprekken × 4 berichten × 1.000 tokens = 40.000.000 tokens/maand

Met Claude 3 Haiku (60% input, 40% output):
  Input:  24M tokens × $0,25/1M  = $6,00
  Output: 16M tokens × $1,25/1M  = $20,00
  Totaal:                          $26,00/maand

Met GPT-4o mini:
  Input:  24M tokens × $0,15/1M  = $3,60
  Output: 16M tokens × $0,60/1M  = $9,60
  Totaal:                          $13,20/maand

Met GPT-4o (voor complexe use cases):
  Input:  24M tokens × $2,50/1M  = $60,00
  Output: 16M tokens × $10,00/1M = $160,00
  Totaal:                          $220,00/maand

Voor productadviseur en chatbot-toepassingen is Claude 3 Haiku of GPT-4o mini de juiste keuze. Gebruik grotere modellen alleen waar de kwaliteitswinst de kostenstijging rechtvaardigt.

Embedding kosten

OpenAI text-embedding-3-small: $0,02 per 1M tokens

Catalogus van 50.000 producten (gemiddeld 250 woorden per product):
  50.000 × 250 woorden × 1,33 tokens/woord ≈ 16.625.000 tokens
  Kosten volledige herindexering: $0,33

Dagelijkse updates (500 gewijzigde producten):
  500 × 250 × 1,33 ≈ 166.250 tokens/dag
  Dagelijkse kosten: $0,003
  Jaarlijkse kosten voor updates: $1,10

Embeddings zijn vrijwel gratis. De kosten zitten in LLM inference.

Infrastructuur — wat je nodig hebt in productie

Vector database keuze

pgvector (PostgreSQL extensie)
  • Kosten: gratis, self-hosted
  • Wanneer: je gebruikt al PostgreSQL, catalogus tot ~1M producten
  • Voordeel: geen extra service, transactionele consistentie met productdata
  • Nadeel: minder geoptimaliseerd voor puur vector-werk dan dedicated oplossingen
Pinecone (managed)
  • Kosten: $70/maand starter, schaalt met volume
  • Wanneer: hoog query-volume, meerdere indexes, geen PostgreSQL
  • Voordeel: managed, goed geconfigureerd voor vector-werk
  • Nadeel: externe dependency, kosten schalen
Weaviate (open source)
  • Kosten: gratis self-hosted, managed variant beschikbaar
  • Wanneer: hybride search (vector + keyword) belangrijk is
  • Voordeel: ingebouwde hybride search, rijke filtering
  • Nadeel: complexere setup, meer onderhoud

Voor de meeste Magento- en Shopify-integraties via Laravel is pgvector de pragmatische keuze. Eén service minder, geen vendor lock-in, goede performance tot honderdduizenden producten.

Caching strategie

LLM-responses cachen bespaart 30-50% op API-kosten bij herhalende vragen.

php
// Cache populaire vragen — 4 uur geldig
$cacheKey = 'product_advisor_' . md5($userQuestion);

return Cache::remember($cacheKey, 14400, function () use ($userQuestion) {
    return $this->productAdvisorService->answer($userQuestion);
});

Wees voorzichtig met caching bij tijdsgevoelige informatie: voorraad en prijs mogen niet gecached worden als die real-time moeten zijn.

Rate limiting

Zonder rate limiting ben je kwetsbaar voor kostenoverschrijdingen. Stel limieten in per gebruiker en per IP.

php
// Laravel throttle middleware voor de AI-endpoints
Route::post('/api/product-advisor', [ProductAdvisorController::class, 'answer'])
    ->middleware('throttle:20,1'); // 20 requests per minuut per IP

Monitoring

Log elke LLM-aanroep. Minimaal: prompt hash (niet de volledige prompt vanwege privacy), response tijdsduur, token count, kosten en of er een escalatie-trigger was.

Zonder monitoring heb je geen inzicht in kwaliteitsproblemen, misbruik of onverwacht hoge kosten.

Architectuur — Laravel als AI-sidecar

Voor Magento en Shopify bouwen wij de AI-laag als aparte Laravel-service. Dit geeft:

  • Scheiding van concerns — Magento blijft Magento. De AI-logica is los en testbaar
  • Onafhankelijke schaling — de AI-service kan horizontaal schalen zonder dat Magento dat doet
  • Herbruikbaarheid — dezelfde AI-service kan meerdere frontends bedienen
  • Technologische flexibiliteit — andere LLM-provider proberen vereist aanpassing in één service

De communicatie verloopt via REST API of GraphQL. Magento of Shopify roept de Laravel AI-service aan, die op zijn beurt de vector database en LLM-API aanspreekt.

Valkuilen die je wilt vermijden

Hallucinaties in productcontext

LLM's verzinnen antwoorden als ze geen goede context hebben. Beperk het model expliciet tot de meegeleverde context: "Antwoord alleen op basis van de bovenstaande producten. Als geen enkel product past, zeg dat expliciet." Test dit actief door te vragen naar producten die je niet verkoopt.

Stale embeddings

Als je productdata wijzigt maar de embeddings niet update, krijg je verouderde of verkeerde zoekresultaten. Implementeer een event-driven update via Magento/Shopify webhooks of product-save hooks.

Te grote context

Meer context is niet altijd beter. Stuur nooit meer dan 8-10 producten als context. Met meer producten neemt de kans toe dat het model relevante informatie negeert. Filter scherp in de retrieval-stap.

Ontbrekende similarity-drempel

Zonder een minimum similarity score stuur je ook producten als context die nauwelijks relevant zijn. Hanteer een drempel van 0,55-0,65 cosine similarity. Producten onder die drempel zijn ruis.

Geen fallback

Wat gebeurt er als de OpenAI API down is? Als je vector database te traag reageert? Bouw altijd een fallback: terugvallen op reguliere zoekresultaten of een melding dat de AI-assistent tijdelijk niet beschikbaar is.

Wanneer beginnen

De technologie is toegankelijk. Een werkend prototype van een RAG-gebaseerde productadviseur bouw je in een dag. Een productierijpe implementatie met monitoring, caching, rate limiting, CI/CD en foutafhandeling kost 4-8 weken.

De businesscase is er voor webshops met meer dan 1.000 producten en een van de volgende situaties:

  • Klanten bellen of mailen veel over "welk product past bij mijn situatie?"
  • De zoekfunctie levert regelmatig nul of irrelevante resultaten
  • Productbeschrijvingen schrijven kost te veel tijd

Heb je een concrete use case? Wij bouwen de AI-integratie als onderdeel van je Magento- of Shopify-architectuur, met Laravel als AI-laag. Neem contact op voor een technisch gesprek.


Dit artikel maakt deel uit van onze AI-serie. Lees ook: AI-chatbots voor klantenservice en semantic search voor webshops. Code-voorbeelden zijn vereenvoudigd voor leesbaarheid; productie-implementaties vereisen aanvullende foutafhandeling, security en logging.
Ruthger Idema

Geschreven door Ruthger Idema

15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.

Meer over ons team →
Deel dit artikel:

Wil je jouw e-commerce naar het volgende niveau?

Plan een vrijblijvend gesprek met onze experts over Magento, Shopify of Laravel maatwerk.

Plan een Tech Check