Über Web, Tech, Games, Art,
Musik, Code & Design

10. März 2026

KI-Chatbot mit eigenen Inhalten

Bereits vor einigen Monaten habe ich versucht mit in das Thema „KI mit eigenen Inhalten“ einzuarbeiten. Eigentlich dachte ich, dass das ein sehr naheliegender Anwendungsfall ist. Als ich recherchiert habe, wie ich so eine Anwendung mit Chat-GPT umsetzen kann, habe ich schnell wieder aufgegeben. Auch mit dem Pro-Account ist es nicht ohne weiteres möglich. Knackpunkt hierbei ist, dass KI eine Vektordatenbank benötigt. Zwar gibt es hier auch externe Anbieter, aber das erstellen mehrerer kostenpflichtiger Accounts bei verschiedenen Anbietern erschien mir sehr umständlich.

Recht komfortabel scheint das ganze Procedere bei Chatbase umgesetzt zu sein – allerdings ist der Anbieter mit ca. 150$ / Monat nicht gerade günstig.

Google´s NotebookLM setzt diesen Anwendungsfall eigentlich perfekt um. Leider aber nur innerhalb der eigenen Web-Anwendung. Eine API für externe Anwendungengibt es – für mich völlig unverständlich – leider nicht.

Tatsächlich scheint es, nach wie vor, keinen Anbieter zu geben, der diese Funktion komfortabel, benutzerfreundlich und für einen fairen Preis zur Verfügung stellt.

Die Lösung!

Mit Hilfe von Google Gemini habe ich jetzt hinbekommen einen „eigenen“ Chatbot zu erstellen, der ausschließlich auf eigene Inhalte zugreift. Das können PDFs, Docs oder auch Websites sein. Die Einrichtung ist zwar ziemlich umständlich und kompliziert, aber folgende Schritt-für-Schritt-Anleitung sollte hierbei eine große Hilfe sein.

Das wird benötigt

  • Ein aktives Rechnungskonto bei Google
  • Ein Google Cloud Projekt
    • Die Vertex AI API
    • Die Discovery Engine API
  • Ein „Bucket“ (die Wissensbasis)
  • Eine AI Application (in der Google Cloud)
  • Ein Skript, dass die Anfragen annimmt und die Antworten ausgibt

Die API-Funktionen bei Google sind kostenpflichtig. Deshalb muss man hier zunächst ein Rechnungskonto einrichten. Die Kosten hierfür sind glücklicherweise sehr überschaubar und bewegen sich in den meisten Fällen im Cent-Bereich. 

https://console.cloud.google.com/billing

Wenn das erledigt ist, folgen die projektbezogenen Schritte:

Schritt 1: Projekt anlegen

Erstelle in der Cloud-Console ein Projekt, z.B. „Chatbot“.

https://console.cloud.google.com

Aktiviere im Menü unter „APIs & Services“ folgende APIs:

  • Vertex AI API
  • Discovery Engine API 

Schritt 2: Dateien hochladen (Wissensbasis)

Damit der Bot auf deine Daten zugreifen kann, müssen diese in einen „Bucket“ geladen werden:

  1. Suche nach Cloud Storage -> Buckets oder klicke hier: https://console.cloud.google.com/storage/overview  und anschließend auf „Erstellen“.
  2. Gib dem Bucket einen Namen und lade deine Dateien (z.B. PDFs) dort hoch.

Schritt 3: Data Store & App erstellen

Jetzt verknüpfen wir die Dateien mit der KI:

  1. Suche nach Vertex AI oder klicke hier: https://console.cloud.google.com/vertex-ai/dashboard
  2. Klicke links auf Vertex AI Search und bei „Benutzerdefinierte Suche (allgemein)“ auf „Erstellen“ und folge den Anweisungen
  3. Data Store erstellen:
    • Wähle Cloud Storage als Quelle.
    • Wähle „Unstructured documents“ (für PDFs/Docs).
    • Gib den Pfad zu deinem Bucket an.
  4. Warte kurz, bis Google die Dokumente indexiert hat (bei wenigen Dateien dauert das ca. 10–30 Minuten).

WICHTIG!: Wenn später Dokumente im Bucket ergänzt werden, müssen sie erneut in den Cloud Speicher importiert werden. (https://console.cloud.google.com/gen-app-builder/data-stores)

Schritt 4: Das PHP-Skript

Mit folgendem PHP-Skript kann auf das Projekt zugegriffen werden. 

<?php

function getGoogleAccessToken($keyFilePath) {
    if (!file_exists($keyFilePath)) return null;
    $keyData = json_decode(file_get_contents($keyFilePath), true);
    $header = json_encode(['alg' => 'RS256', 'typ' => 'JWT']);
    $iat = time();
    $exp = $iat + 3600;
    $payload = json_encode([
        'iss' => $keyData['client_email'],
        'scope' => 'https://www.googleapis.com/auth/cloud-platform',
        'aud' => 'https://oauth2.googleapis.com/token',
        'exp' => $exp, 'iat' => $iat
    ]);
    $base64UrlHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
    $base64UrlPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload));
    $signatureInput = $base64UrlHeader . "." . $base64UrlPayload;
    openssl_sign($signatureInput, $signature, $keyData['private_key'], "SHA256");
    $base64UrlSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));
    $jwt = $signatureInput . "." . $base64UrlSignature;
    $ch = curl_init('https://oauth2.googleapis.com/token');
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['grant_type' => 'urn:ietf:params:oauth:grant-type:jwt-bearer', 'assertion' => $jwt]));
    $response = curl_exec($ch);
    $data = json_decode($response, true);
    return $data['access_token'] ?? null;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['ajax'])) {
    header('Content-Type: application/json');
    $projectId = 'xxxxxxxxxxxx-xxxxxx-xx';
    $location  = 'global'; 
    $engineId  = 'xxxxxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxx'; 
    $keyFile   = __DIR__ . '/credentials.json';

    $accessToken = getGoogleAccessToken($keyFile);
    if (!$accessToken) { echo json_encode(['error' => 'Auth fail']); exit; }

    $frage = htmlspecialchars($_POST['frage'] ?? '');
    $url = "https://discoveryengine.googleapis.com/v1/projects/{$projectId}/locations/{$location}/collections/default_collection/engines/{$engineId}/servingConfigs/default_serving_config:search";

    $data = [
        "query" => $frage,
        "pageSize" => 3,
        "contentSearchSpec" => [
            "summarySpec" => [
                "summaryResultCount" => 3,
                "includeCitations" => true,
                "ignoreAdversarialQuery" => true
            ]
        ]
    ];

    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer ' . $accessToken, 'Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    $response = curl_exec($ch);
    $result = json_decode($response, true);
    echo json_encode(['antwort' => $result['summary']['summaryText'] ?? "Keine Antwort gefunden."]);
    exit;
}
?>

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ARTBOT | Optimized</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
    <style>
        :root { --primary: #4f46e5; --bg: #f3f4f6; --text: #1f2937; }
        body { font-family: 'Inter', sans-serif; background: var(--bg); margin: 0; display: flex; justify-content: center; height: 100vh; }
        .chat-container { width: 100%; max-width: 700px; background: white; display: flex; flex-direction: column; box-shadow: 0 10px 15px rgba(0,0,0,0.1); }
        header { padding: 15px; border-bottom: 1px solid #eee; text-align: center; color: var(--primary); font-weight: 600; font-size: 1.2rem; }
        
        .messages { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; background: #fff; }
        .msg { padding: 12px 18px; border-radius: 15px; font-size: 15px; max-width: 85%; line-height: 1.6; position: relative; }
        
        .user { align-self: flex-end; background: var(--primary); color: white; border-bottom-right-radius: 2px; }
        
        .bot { align-self: flex-start; background: #f3f4f6; color: var(--text); border-bottom-left-radius: 2px; }
        .bot strong { color: #000; font-weight: 600; }
        .bot ul { margin: 10px 0; padding-left: 20px; }
        .bot li { margin-bottom: 5px; }
        
        .cite { 
            display: inline-block; background: #e0e7ff; color: #4338ca; 
            font-size: 10px; px; padding: 1px 4px; border-radius: 4px; 
            margin-left: 3px; vertical-align: super; font-weight: bold;
        }

        .loading { font-size: 12px; color: #999; display: none; padding: 10px 20px; font-style: italic; }
        footer { padding: 15px; border-top: 1px solid #eee; display: flex; gap: 10px; background: white; }
        input { flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 10px; outline: none; font-family: inherit; }
        button { background: var(--primary); color: white; border: none; padding: 10px 20px; border-radius: 10px; cursor: pointer; font-weight: 600; }
        button:hover { background: #4338ca; }
    </style>
</head>
<body>

<div class="chat-container">
    <header>ARTBOT</header>
    <div class="messages" id="box">
        <div class="msg bot">Hallo! Frag mich etwas zu Deinen Inhalten.</div>
    </div>
    <div id="loader" class="loading">Überprüfe Dokumente...</div>
    <footer>
        <input type="text" id="input" placeholder="Nachricht..." autocomplete="off">
        <button id="btn">Senden</button>
    </footer>
</div>

<script>
    const box = document.getElementById('box');
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');
    const loader = document.getElementById('loader');

    function formatAIResponse(text) {
    text = text.trim();

    text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
    text = text.replace(/\[(\d+(?:,\s*\d+)*)\]/g, '<span class="cite">$1</span>');

    let lines = text.split('\n');
    let htmlResult = "";
    let inList = false;

    lines.forEach((line) => {
        let trimmedLine = line.trim();

        if (trimmedLine.startsWith('* ')) {
            if (!inList) {
                htmlResult += '<ul>';
                inList = true;
            }
            let content = trimmedLine.substring(2);
            htmlResult += `<li>${content}</li>`;
        } 
        else {
            if (inList) {
                htmlResult += '</ul>';
                inList = false;
            }

            if (trimmedLine.length > 0) {
                htmlResult += trimmedLine + '<br>';
            } else {
                htmlResult += '<div style="margin-bottom:10px;"></div>'; 
            }
        }
    });

    if (inList) {
        htmlResult += '</ul>';
    }

    htmlResult = htmlResult.replace(/<br><ul>/g, '<ul>');

    return htmlResult;
}

    async function send() {
        const text = input.value.trim();
        if(!text) return;

        const u = document.createElement('div'); 
        u.className = 'msg user'; 
        u.innerText = text; 
        box.appendChild(u);
        
        input.value = '';
        box.scrollTop = box.scrollHeight;
        loader.style.display = 'block';
        btn.disabled = true;

        const fd = new FormData();
        fd.append('ajax', '1');
        fd.append('frage', text);

        try {
            const res = await fetch(window.location.href, { method: 'POST', body: fd });
            const data = await res.json();
            
            const b = document.createElement('div'); 
            b.className = 'msg bot';
            
            if (data.antwort) {
                b.innerHTML = formatAIResponse(data.antwort);
            } else {
                b.innerText = "Fehler: " + (data.error || "Keine Antwort.");
            }
            
            box.appendChild(b);
        } catch(e) {
            console.error(e);
        } finally {
            loader.style.display = 'none';
            btn.disabled = false;
            box.scrollTop = box.scrollHeight;
        }
    }

    btn.onclick = send;
    input.onkeypress = (e) => { if(e.key === 'Enter') send(); };
</script>
</body>
</html>Code-Sprache: HTML, XML (xml)

In diesem Skript müssen noch ProjectID, EngineID und ein KeyFile ergänzt werden. 

Die ProjectID findet sich in der Cloud Console oben links in der Projekt-Liste: https://console.cloud.google.com/

Die EngineID ist hier zu finden: https://console.cloud.google.com/gen-app-builder/engines


Das KeyFile ist ein JSON-File muss zunächst generiert werden muss. Die Datei sorgt dafür, dass die (kostenpflichtigen) APIs auch nur vom Eigentümer genutzt werden können! Damit die Datei erstellt werden kann, muss zunächst ein Dienstkonto eingerichtet werden: https://console.cloud.google.com/iam-admin/serviceaccounts

Klicke anschließend rechts auf die 3 Punkte, „Schlüssel verwalten“ und erstelle einen JSON-Schlüssel. Speichere die Datei als „credentials.json“ in deinem Projektverzeichnis.

WICHTIG! Damit diese Datei nicht öffentlich auslesbar ist, muss sie UNBEDINGT via htaccess geschützt werden!

<Files "credentials.json">
    Order allow,deny
    Deny from all
</Files>
Code-Sprache: HTML, XML (xml)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

FRTG - Traces
Jetzt bei Apple Music & Spotify.