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:
- Suche nach Cloud Storage -> Buckets oder klicke hier: https://console.cloud.google.com/storage/overview und anschließend auf „Erstellen“.
- 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:
- Suche nach Vertex AI oder klicke hier: https://console.cloud.google.com/vertex-ai/dashboard
- Klicke links auf Vertex AI Search und bei „Benutzerdefinierte Suche (allgemein)“ auf „Erstellen“ und folge den Anweisungen
- Data Store erstellen:
- Wähle Cloud Storage als Quelle.
- Wähle „Unstructured documents“ (für PDFs/Docs).
- Gib den Pfad zu deinem Bucket an.
- 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)
Nächster Artikel
