1. Einführung
Übersicht
In diesem Lab erstellen und stellen Sie eine KI-basierte Full-Stack-Chatanwendung in Google Kubernetes Engine (GKE) bereit. Diese „hybride“ Anwendung demonstriert ein leistungsstarkes Architekturmuster: die Möglichkeit, nahtlos zwischen einem selbst gehosteten offenen Modell (Gemma 3 12B), das direkt in Ihrem Cluster ausgeführt wird, und einem verwalteten KI-Dienst (Gemini 2.5 Flash über Vertex AI) zu wechseln.
+----------------------+
| User (Web Browser) |
+-----------+----------+
|
v
+----------------------------------------------+---------------------------------------------+
| Google Cloud Platform | |
| | +-------+-------+ |
| | | Load Balancer | |
| | +-------+-------+ |
| | v |
| +------------------------------------------+-----------------------------------------+ |
| | Google Kubernetes Engine (GKE) | | |
| | v | |
| | +-----------+-----------+ | |
| | | Gradio Chat App | | |
| | +--+-----------------+--+ | |
| | | | | |
| | (Self-hosted) | | (Managed via SDK) | |
| | v | | |
| | +--------------+---+ | | |
| | | Gemma 3 Model | | | |
| | | (GPU Node) | | | |
| | +------------------+ | | |
| +---------------------------------------------------|--------------------------------+ |
| | |
| v |
| +----------+-----------+ |
| | Vertex AI (Gemini) | |
| +----------------------+ |
| | (Save History) |
| v |
| +----------+-----------+ |
| | Firestore Database | |
| +----------------------+ |
+--------------------------------------------------------------------------------------------+
Sie verwenden Terraform, um die Infrastruktur bereitzustellen, einschließlich eines GKE Autopilot-Clusters und einer Firestore-Datenbank zum Speichern des Chatverlaufs. Anschließend vervollständigen Sie den Python-Anwendungscode, um Unterhaltungen mit mehreren Durchgängen zu verarbeiten, mit beiden KI-Modellen zu interagieren und die endgültige Anwendung mit Cloud Build und Skaffold bereitzustellen.
Lerninhalte
- GKE- und Firestore-Infrastruktur mit Terraform bereitstellen
- Ein Large Language Model (Gemma) in GKE Autopilot mit Kubernetes-Manifesten bereitstellen
- Implementieren Sie eine Gradio-Chatoberfläche in Python, die zwischen verschiedenen KI-Backends wechseln kann.
- Verwenden Sie Firestore, um den Verlauf von Chatsitzungen zu speichern und abzurufen.
- Konfigurieren Sie Workload Identity, um Ihren GKE-Arbeitslasten sicheren Zugriff auf Google Cloud-Dienste (Vertex AI, Firestore) zu gewähren.
Vorbereitung
- Google Cloud-Projekt mit aktivierter Abrechnungsfunktion.
- Grundkenntnisse zu Python, Kubernetes und Standardbefehlszeilentools
- Ein Hugging Face-Token mit Zugriff auf Gemma-Modelle.
2. Projekt einrichten
- Wenn Sie noch kein Google-Konto haben, müssen Sie ein Google-Konto erstellen.
- Verwenden Sie stattdessen ein privates Konto. Bei Arbeitskonten und Konten von Bildungseinrichtungen kann es Einschränkungen geben, die verhindern, dass Sie die für dieses Lab erforderlichen APIs aktivieren.
- Melden Sie sich in der Google Cloud Console an.
- Aktivieren Sie die Abrechnung in der Cloud Console.
- Die Cloud-Ressourcen, die für dieses Lab benötigt werden, sollten weniger als 1 $kosten.
- Sie können die Schritte am Ende dieses Labs ausführen, um Ressourcen zu löschen und so weitere Kosten zu vermeiden.
- Neue Nutzer haben Anspruch auf die kostenlose Testversion mit einem Guthaben von 300$.
- Erstellen Sie ein neues Projekt oder verwenden Sie ein vorhandenes Projekt wieder.
Cloud Shell-Editor öffnen
- Klicken Sie auf diesen Link, um direkt zum Cloud Shell-Editor zu gelangen.
- Wenn Sie heute an irgendeinem Punkt zur Autorisierung aufgefordert werden, klicken Sie auf Autorisieren, um fortzufahren.

- Wenn das Terminal nicht unten auf dem Bildschirm angezeigt wird, öffnen Sie es:
- Klicken Sie auf Ansehen.
- Klicken Sie auf Terminal
.
- Legen Sie im Terminal Ihr Projekt mit diesem Befehl fest:
- Format:
gcloud config set project [PROJECT_ID] - Beispiel:
gcloud config set project lab-project-id-example - Wenn Sie sich nicht an Ihre Projekt-ID erinnern können:
- Sie können alle Ihre Projekt-IDs mit folgendem Befehl auflisten:
gcloud projects list | awk '/PROJECT_ID/{print $2}'

- Sie können alle Ihre Projekt-IDs mit folgendem Befehl auflisten:
- Format:
- Es sollte folgende Meldung angezeigt werden:
Wenn SieUpdated property [core/project].
WARNINGsehen undDo you want to continue (Y/n)?gefragt werden, haben Sie die Projekt-ID wahrscheinlich falsch eingegeben. Drücken Sien, dannEnterund versuchen Sie, den Befehlgcloud config set projectnoch einmal auszuführen.
Repository klonen
Klonen Sie in Ihrem Cloud Shell-Terminal das Projekt-Repository und wechseln Sie zum Projektverzeichnis:
git clone https://github.com/GoogleCloudPlatform/devrel-demos.git
cd devrel-demos/containers/gradio-chat-gke
Sehen Sie sich die Projektstruktur an:
gradio-chat-gke/
├── app/
│ ├── app.py # Main application logic (you will edit this)
│ ├── requirements.txt # Python dependencies
│ └── themes.py # UI theming
├── deploy/
│ ├── chat-deploy.yaml # Kubernetes deployment for the chat app
│ ├── Dockerfile # Container definition for the chat app
│ └── gemma3-12b-deploy.yaml# Kubernetes deployment for Gemma model
├── infra/
│ └── main.tf # Terraform infrastructure definition
└── skaffold.yaml # Skaffold configuration for building/deploying
Umgebungsvariablen festlegen
Richten Sie Umgebungsvariablen für Ihre Projekt-ID und Projektnummer ein. Diese werden von Terraform und nachfolgenden Befehlen verwendet.
export GOOGLE_CLOUD_PROJECT=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format="value(projectNumber)")
export REGION=us-central1
Cloud Resource Manager API aktivieren
Für Terraform muss die Cloud Resource Manager API aktiviert sein, damit die Ressourcen Ihres Projekts verwaltet werden können. Daher müssen wir sie zuerst aktivieren. Später stellen wir unsere Chatanwendung mit Skaffold bereit. Skaffold verwendet Cloud Build, um unser Container-Image zu erstellen. Wir aktivieren jetzt die Storage API und erstellen den für Cloud Build erforderlichen Bucket. Wir verwenden Terraform selbst, um die restlichen APIs zu aktivieren, die für dieses Projekt erforderlich sind.
gcloud services enable cloudresourcemanager.googleapis.com storage-api.googleapis.com
Cloud Build-Staging-Bucket erstellen
Skaffold verwendet Google Cloud Build, wofür ein Cloud Storage-Bucket zum Bereitstellen Ihres Quellcodes erforderlich ist.
Erstellen Sie sie jetzt, damit sie vorhanden ist:
gcloud storage buckets create gs://${GOOGLE_CLOUD_PROJECT}_cloudbuild
Wenn Sie eine Fehlermeldung erhalten, dass der Bucket bereits vorhanden ist, können Sie sie ignorieren.
3. Infrastruktur mit Terraform bereitstellen
Wir verwenden Terraform, um die erforderlichen Google Cloud-Ressourcen einzurichten. So wird eine reproduzierbare und konsistente Umgebung geschaffen.
- Rufen Sie das Infrastrukturverzeichnis auf:
cd infra
In dieser Datei werden die zusätzlichen APIs definiert, die für dieses Projekt erforderlich sind: cloudbuild, artifactregistry, container (gke), firestore und aiplatform (vertexai). Sehen Sie sich die Datei oder unten an, um zu sehen, wie die APIs über Terraform aktiviert werden:
resource "google_project_service" "cloudbuild" {
service = "cloudbuild.googleapis.com"
disable_on_destroy = false
project = var.project_id
}
resource "google_project_service" "artifactregistry" {
service = "artifactregistry.googleapis.com"
disable_on_destroy = false
project = var.project_id
}
resource "google_project_service" "container" {
service = "container.googleapis.com"
disable_on_destroy = false
project = var.project_id
}
resource "google_project_service" "firestore" {
service = "firestore.googleapis.com"
disable_on_destroy = false
project = var.project_id
}
resource "google_project_service" "vertexai" {
service = "aiplatform.googleapis.com"
disable_on_destroy = false
project = var.project_id
}
GKE-Cluster definieren
Öffnen Sie infra/main.tf in einem Editor. Sie sehen mehrere # TODO-Kommentare. Sie können die Datei manuell öffnen oder mit diesem Befehl im Editor öffnen:
cloudshell edit main.tf
Zuerst müssen wir unseren Kubernetes-Cluster definieren. Wir verwenden GKE Autopilot, das sich ideal für KI-Arbeitslasten eignet, da die Knotenverwaltung automatisch erfolgt.
Suchen Sie nach # TODO: Create a GKE Autopilot Cluster und fügen Sie den folgenden Block darunter ein:
# Create a GKE Autopilot Cluster
resource "google_container_cluster" "primary" {
name = var.cluster_name
location = var.region
project = var.project_id
# Enable Autopilot mode
enable_autopilot = true
deletion_protection = false
# Networking
network = "default"
subnetwork = "projects/${var.project_id}/regions/${var.region}/subnetworks/default"
# Timeout for cluster creation
timeouts {
create = "30m"
update = "30m"
}
depends_on = [google_project_service.container]
}
Hinweis: enable_autopilot = true Durch diese eine Zeile müssen wir keine Knotenpools, kein Autoscaling und kein Bin-Packing unserer GPU-Arbeitslasten verwalten.
Firestore-Datenbank definieren
Als Nächstes benötigen wir einen Ort, an dem wir unser Chatprotokoll speichern können. Firestore ist eine serverlose NoSQL-Datenbank, die sich dafür perfekt eignet.
Suchen Sie nach # TODO: Create a Firestore Database und fügen Sie Folgendes hinzu:
resource "google_firestore_database" "database" {
project = var.project_id
name = "chat-app-db"
location_id = "nam5"
type = "FIRESTORE_NATIVE"
depends_on = [google_project_service.firestore]
}
Suchen Sie nach dem Hinzufügen der Datenbankressource nach # TODO: Create an initial Firestore Document und fügen Sie den folgenden Block hinzu. Mit dieser Ressource wird ein erstes Platzhalterdokument in unserer Sammlung erstellt, was für die Initialisierung der Datenbankstruktur hilfreich ist.
resource "google_firestore_document" "initial_document" {
project = var.project_id
collection = "chat_sessions"
document_id = "initialize"
fields = <<EOF
EOF
depends_on = [google_firestore_database.database]
}
Workload Identity definieren
Schließlich müssen wir die Sicherheit konfigurieren. Unsere Kubernetes-Pods sollen auf Vertex AI und Firestore zugreifen können, ohne dass wir Secrets oder API-Schlüssel verwalten müssen. Dazu verwenden wir Workload Identity.
Wir gewähren dem Kubernetes-Dienstkonto (KSA), das von unserer App verwendet wird, die erforderlichen IAM-Rollen.
Hinweis:Das in diesen Bindungen referenzierte Kubernetes-Dienstkonto (gradio-chat-ksa) ist noch nicht vorhanden. Sie wird später erstellt, wenn wir unsere Anwendung im Cluster bereitstellen. Es ist völlig in Ordnung (und gängige Praxis), diese IAM-Bindungen vorab bereitzustellen.
Suchen Sie nach # TODO: Configure Workload Identity IAM bindings und fügen Sie Folgendes hinzu:
locals {
ksa_principal = "principal://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${var.project_id}.svc.id.goog/subject/ns/default/sa/gradio-chat-ksa"
}
resource "google_project_iam_member" "ksa_token_creator" {
project = var.project_id
role = "roles/iam.serviceAccountTokenCreator"
member = local.ksa_principal
}
resource "google_project_iam_member" "ksa_vertex_user" {
project = var.project_id
role = "roles/aiplatform.user"
member = local.ksa_principal
}
resource "google_project_iam_member" "ksa_datastore_user" {
project = var.project_id
role = "roles/datastore.user"
member = local.ksa_principal
}
Konfiguration anwenden
Nachdem wir unsere Infrastruktur definiert haben, stellen wir sie nun bereit.
- Zuerst müssen wir einige Variablen für Terraform festlegen. Dazu verwenden wir Umgebungsvariablen:
export TF_VAR_project_id=$(gcloud config get-value project)
export TF_VAR_project_number=$(gcloud projects describe $TF_VAR_project_id --format="value(projectNumber)")
export TF_VAR_region="us-central1"
- Initialisieren Sie Terraform:
terraform init
- Mit
terraform plankönnen Sie sich eine Vorschau der Ressourcen ansehen, die erstellt werden.
terraform plan
- Wenden Sie die Konfiguration an. Geben Sie bei Aufforderung zur Bestätigung
yesein.
terraform apply
Hinweis: Die Bereitstellung eines GKE-Clusters kann 10 bis 15 Minuten dauern. Während Sie warten, können Sie mit der Überprüfung des Anwendungscodes im nächsten Abschnitt fortfahren.
- Konfigurieren Sie nach Abschluss des Vorgangs
kubectlfür die Kommunikation mit Ihrem neuen Cluster:
gcloud container clusters get-credentials gradio-chat-cluster --region us-central1 --project $TF_VAR_project_id
4. Self-Hosted Gemma in GKE bereitstellen
Als Nächstes stellen wir das Modell Gemma 3 12B direkt in Ihrem GKE-Cluster bereit. Dies ermöglicht eine Inferenz mit niedriger Latenz und die vollständige Kontrolle über die Umgebung für die Modellausführung.
Hugging Face-Anmeldedaten konfigurieren
Damit das Gemma-Modell heruntergeladen werden kann, muss Ihr Cluster bei Hugging Face authentifiziert werden.
- Sie benötigen ein Hugging Face-Token.
- Erstellen Sie ein Kubernetes-Secret mit Ihrem Token. Ersetzen Sie [YOUR_HF_TOKEN] durch Ihr tatsächliches Token:
kubectl create secret generic hf-secret --from-literal=hf_api_token=[YOUR_HF_TOKEN]
Modell bereitstellen
Wir verwenden ein Standard-Kubernetes-Deployment, um das Modell auszuführen. Das Manifest befindet sich unter deploy/gemma3-12b-deploy.yaml. Sie können die Datei manuell öffnen oder mit diesem Befehl im Editor öffnen:
cd ../deploy
cloudshell edit gemma3-12b-deploy.yaml
Sehen Sie sich diese Datei einmal genauer an. Beachten Sie den Abschnitt resources:
resources:
requests:
nvidia.com/gpu: 4
nodeSelector:
cloud.google.com/gke-accelerator: nvidia-l4
Das ist die deklarative KI-Infrastruktur. Wir teilen GKE Autopilot mit, dass für diesen bestimmten Pod 4 NVIDIA L4-GPUs erforderlich sind. Autopilot sucht oder stellt einen Knoten bereit, der genau diese Anforderungen erfüllt. Wenn ein Knoten nicht für die Bereitstellung verfügbar ist, wird der Vorgang so lange wiederholt, bis ein Knoten verfügbar ist, der die Anforderungen erfüllt.
- Wenden Sie das Bereitstellungsmanifest an:
Dadurch wird der Prozess zum Herunterladen der Modellgewichte und zum Starten des Inferenzservers gestartet. In der Regel kann dies einige Minuten dauern. Für diese Gemma-Bereitstellung werden GPUs verwendet, die möglicherweise nicht immer verfügbar sind. Wenn keine GPUs verfügbar sind, bleibt der Gemma-Pod im Status „Ausstehend“, bis GPUs verfügbar sind. In der Google Cloud Console wird dann ein Fehler wie „Cannot schedule pods: Preemption is not helpful for scheduling.“ (Pods können nicht geplant werden: Die Präemption ist für die Planung nicht hilfreich.) und/oder „Cannot schedule pods: node(s) didn't match Pod's node affinity/selector.“ (Pods können nicht geplant werden: Knoten stimmen nicht mit der Knotenaffinität/dem Knotenselektor des Pods überein.) angezeigt. Das bedeutet, dass GKE noch keine GPUs für Sie bereitstellen konnte. Das System versucht es so lange, bis es GPUs erhält. Das kann je nach Verfügbarkeit der GPU einige Minuten oder Tage dauern. Sie können den Status mit folgendem Befehl prüfen:cd .. kubectl apply -f deploy/gemma3-12b-deploy.yaml Sie können mit der Bereitstellung der Anwendung fortfahren, auch wenn derkubectl get podsgemma-Pod noch nicht ausgeführt wird. Die Chat-App stellt eine Verbindung zum Gemma-Dienst her, sobald dieser verfügbar ist. Sie können erst dann über Ihre Chat-App mit Gemma interagieren, wenn dergemma-Pod den StatusRunningund1/1anzeigt. In der Zwischenzeit können Sie aber mit Gemini chatten.
5. Chatanwendung erstellen
Jetzt vervollständigen wir die Python-Anwendung. Öffnen Sie app/app.py im Cloud Shell-Editor. Sie finden mehrere # TODO-Blöcke, die ausgefüllt werden müssen, damit die Anwendung funktioniert.
cloudshell edit app/app.py
Schritt 1: Unterhaltungsverlauf verarbeiten
LLMs benötigen einen speziell formatierten Konversationsverlauf, damit sie verstehen, wer was gesagt hat.
Das Muster „Universal Translator“:Wir schreiben zwei verschiedene Funktionen, um denselben Chatverlauf zu verarbeiten. Dies ist ein wichtiges Muster in Anwendungen mit mehreren Modellen.
- Die „Source of Truth“ (Gradio): In unserer App wird der Verlauf in einem einfachen, generischen Format gespeichert:
[[user_msg1, bot_msg1], ...]. - Ziel 1 (Gemma): Muss in einen einzelnen Raw-String mit bestimmten speziellen Tokens umgewandelt werden.
- Ziel 2 (Gemini): Muss in eine strukturierte Liste von API-Objekten umgewandelt werden.
Indem wir den generischen Verlauf bei jedem Zug in das Zielformat umformatieren, können wir nahtlos zwischen Modellen wechseln. Wenn Sie später ein anderes Modell hinzufügen möchten, müssen Sie eine neue Verarbeitungsfunktion für das entsprechende Format schreiben.
Für Gemma (selbst gehostet)
Chatvorlagen:Wenn Sie Ihre eigenen offenen Modelle hosten, müssen Sie den Prompt in der Regel manuell in einen bestimmten String formatieren, der vom Modell als Konversation erkannt wird. Das wird als „Chatvorlage“ bezeichnet.
Suchen Sie in app.py nach der Funktion process_message_gemma und ersetzen Sie sie durch den folgenden Code:
# This function takes a user's message and the conversation history as input.
# Its job is to format these elements into a single,
# structured prompt that can be understood by the language model (LLM).
# This structured format helps the LLM maintain context and generate more relevant responses.
def process_message_gemma(message, history):
user_prompt_format = "User's Turn:\n>>> {prompt}\n"
assistant_prompt_format = "Assistant's Turn:\n>>> {prompt}\n"
history_message = ""
for user_turn, assistant_turn in history:
history_message += user_prompt_format.format(prompt=user_turn)
history_message += assistant_prompt_format.format(prompt=assistant_turn)
# Format the new user message
new_user_message = user_prompt_format.format(prompt=message)
# Create a new aggregated message to be used as a single flat string in a json object sent to the LLM
aggregated_message = (
history_message + new_user_message + assistant_prompt_format.format(prompt="")
)
return aggregated_message
Für Gemini (verwaltet)
Bei verwalteten Diensten werden strukturierte Objekte oft gegenüber Rohstrings bevorzugt. Wir benötigen eine separate Funktion, um den Verlauf in types.Content-Objekte für das Gemini SDK zu formatieren.
Suchen Sie nach process_message_gemini und ersetzen Sie es durch:
def process_message_gemini(message, history):
contents = []
for user_turn, model_turn in history:
contents.append(
types.Content(role="user", parts=[types.Part.from_text(text=user_turn)])
)
contents.append(
types.Content(role="model", parts=[types.Part.from_text(text=model_turn)])
)
contents.append(
types.Content(role="user", parts=[types.Part.from_text(text=message)])
)
return contents
Schritt 2: Self-Hosted Gemma Model aufrufen
Wir müssen unseren formatierten Prompt an den Gemma-Dienst senden, der in unserem Cluster ausgeführt wird. Wir verwenden eine standardmäßige HTTP POST-Anfrage an den internen DNS-Namen des Dienstes.
Suchen Sie die Funktion call_gemma_model und ersetzen Sie sie durch:
# Construct the request, send it to Gemma, return the model's response
# aggregated_message = current user message + history
def call_gemma_model(aggregated_message, model_temperature, top_p, max_tokens):
json_message = {
"prompt": aggregated_message,
"temperature": model_temperature,
"top_p": top_p,
"max_tokens": max_tokens,
"stop": ["User's Turn:"],
}
# Log what will be sent to the LLM
print("*** JSON request: " + str(json_message))
# Send the constructed json with the user prompt to the model and put the model's response in the json_data variable
json_data = post_request(json_message)
# The response from the model is a list of predictions. We'll take the first result.
raw_output = json_data["predictions"][0]
# The vLLM server returns the full prompt in the response. We need to extract
# just the newly generated text from the model.
assistant_turn_marker = "Assistant's Turn:\n>>>"
marker_pos = raw_output.rfind(assistant_turn_marker)
if marker_pos != -1:
output = raw_output[marker_pos + len(assistant_turn_marker) :]
else:
output = raw_output
# Clean up potential over-generation
stop_marker = "User's Turn:"
stop_pos = output.lower().find(stop_marker.lower())
if stop_pos != -1:
output = output[:stop_pos]
return output.strip()
Schritt 3: Vertex AI-Gemini-Modell aufrufen
Für das verwaltete Modell verwenden wir das Google GenAI SDK. Das ist viel einfacher, da die Netzwerkanrufe für uns abgewickelt werden.
Suchen Sie die Funktion call_gemini_model und ersetzen Sie sie durch:
# Send a request to Gemini via the VertexAI API. Return the model's response
# contents = list of types.Content objects
def call_gemini_model(contents, model_temperature, top_p, max_tokens):
gemini_model = "gemini-2.5-flash"
response = client.models.generate_content(
model=gemini_model,
contents=contents,
config={
"temperature": model_temperature,
"max_output_tokens": max_tokens,
"top_p": top_p,
},
)
return response.text
Schritt 4: Main Inference Interface implementieren
Schließlich benötigen wir die Hauptorchestratorfunktion, die von Gradio aufgerufen wird. Diese Funktion muss:
- Initialisieren Sie den Verlauf, wenn er leer ist.
- Verarbeite die Nachricht.
- Leiten Sie die Anfrage an das ausgewählte Modell (Gemma oder Gemini) weiter.
- Speichern Sie die Interaktion in Firestore.
- Gibt die Antwort an die Benutzeroberfläche zurück.
Gradio und Statusverwaltung:Die ChatInterface-Funktion von Gradio verarbeitet den Status auf Sitzungsebene (Anzeigen von Nachrichten im Browser) automatisch. Es bietet jedoch keine integrierte Unterstützung für externe Datenbanken.
Um den Chatverlauf langfristig zu speichern, verwenden wir ein Standardmuster: Wir hängen uns in die Funktion inference_interface ein. Wenn wir request: gr.Request als Argument akzeptieren, übergibt Gradio uns automatisch die Sitzungsdetails des aktuellen Nutzers. Damit erstellen wir ein eindeutiges Firestore-Dokument für jeden Nutzer, um sicherzustellen, dass Unterhaltungen in einer Umgebung mit mehreren Nutzern nicht verwechselt werden.
Suchen Sie die Funktion inference_interface und ersetzen Sie sie durch:
# This is the primary chat function. Every time a user sends a message, gradio calls this function,
# which sends the user's input to the appropriate AI (as indicated on the user interface), updates
# the chat history for future use during this session, and records the chat history in Firestore.
def inference_interface(
message,
history,
model_name,
model_temperature,
top_p,
max_tokens,
request: gr.Request,
):
# set history to empty array
if history is None:
history = []
# Get or create session document
session_hash = request.session_hash
doc_id = f"session-{session_hash}"
doc_ref = db.collection("chat_sessions").document(doc_id)
# Create the session document if it doesn't exist
if not doc_ref.get().exists:
doc_ref.set({"Session start": datetime.datetime.now()})
# Log info
print("Model: " + model_name)
print("LLM Engine: " + llm_engine)
print("* History: " + str(history))
# Pass the message and history to the appropriate model, as indicated by the user via the ui
if model_name == "Gemma3 12b it":
aggregated_message = process_message_gemma(message, history)
output = call_gemma_model(
aggregated_message, model_temperature, top_p, max_tokens
)
elif model_name == "Gemini":
gemini_contents = process_message_gemini(message, history)
output = call_gemini_model(
gemini_contents, model_temperature, top_p, max_tokens
)
else:
# Handle the case where no valid model is selected
output = "Error: Invalid model selected."
interaction = {"user": message, model_name: output}
# Log the updated chat history
print("* History: " + str(history) + " " + str(interaction))
# Save the updated history to Firestore
save_chat_history(interaction, doc_ref)
return output
6. Datei „app.py“ prüfen
Jetzt sollte Ihre auf Gradio basierende Chatanwendung bereit für die Bereitstellung sein. Achten Sie darauf, dass sie genau der folgenden vollständigen Datei entspricht.
Fehlerbehebung:Wenn Sie Ihre Anwendung bereitstellen und beim Versuch, eine Verbindung zu ihr herzustellen, die Fehlermeldung „Verbindung abgelehnt“ oder „Diese Website ist nicht erreichbar“ erhalten, wiederholen Sie die Schritte ab diesem Punkt. Kopieren Sie dazu zuerst diese gesamte Datei und fügen Sie sie in Ihre Datei „app.py“ ein.
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import google.auth
import google.cloud
import gradio as gr
import requests
import themes
from google import genai
from google.cloud import firestore
from google.genai import types
## Do one-time initialization things
## grab the project id from google auth
_, project = google.auth.default()
print(f"Project: {project}")
# Set initial values for model
llm_engine = "vllm"
host = "http://gemma-service:8000"
context_path = "/generate"
# initialize vertex for interacting with Gemini
client = genai.Client(
vertexai=True,
project=project,
location="global",
)
# Initialize Firestore client
db = firestore.Client(database="chat-app-db")
# This is the primary chat function. Every time a user sends a message, gradio calls this function,
# which sends the user's input to the appropriate AI (as indicated on the user interface), updates
# the chat history for future use during this session, and records the chat history in Firestore.
def inference_interface(
message,
history,
model_name,
model_temperature,
top_p,
max_tokens,
request: gr.Request,
):
# set history to empty array
if history is None:
history = []
# Get or create session document
session_hash = request.session_hash
doc_id = f"session-{session_hash}"
doc_ref = db.collection("chat_sessions").document(doc_id)
# Create the session document if it doesn't exist
if not doc_ref.get().exists:
doc_ref.set({"Session start": datetime.datetime.now()})
# Log info
print("Model: " + model_name)
print("LLM Engine: " + llm_engine)
print("* History: " + str(history))
# Pass the message and history to the appropriate model, as indicated by the user via the ui
if model_name == "Gemma3 12b it":
aggregated_message = process_message_gemma(message, history)
output = call_gemma_model(
aggregated_message, model_temperature, top_p, max_tokens
)
elif model_name == "Gemini":
gemini_contents = process_message_gemini(message, history)
output = call_gemini_model(
gemini_contents, model_temperature, top_p, max_tokens
)
else:
# Handle the case where no valid model is selected
output = "Error: Invalid model selected."
interaction = {"user": message, model_name: output}
# Log the updated chat history
print("* History: " + str(history) + " " + str(interaction))
# Save the updated history to Firestore
save_chat_history(interaction, doc_ref)
return output
# Construct the request, send it to Gemma, return the model's response
# aggregated_message = current user message + history
def call_gemma_model(aggregated_message, model_temperature, top_p, max_tokens):
json_message = {
"prompt": aggregated_message,
"temperature": model_temperature,
"top_p": top_p,
"max_tokens": max_tokens,
"stop": ["User's Turn:"],
}
# Log what will be sent to the LLM
print("*** JSON request: " + str(json_message)) # Log the JSON request
# Send the constructed json with the user prompt to the model and put the model's response in the json_data variable
json_data = post_request(json_message)
# The response from the model is a list of predictions.
# We'll take the first result.
raw_output = json_data["predictions"][0]
# The vLLM server returns the full prompt in the response. We need to extract
# just the newly generated text from the model. The prompt ends with
# "Assistant's Turn:\n>>>", so we find the last occurrence of that and
# take everything after it.
assistant_turn_marker = "Assistant's Turn:\n>>>"
marker_pos = raw_output.rfind(assistant_turn_marker)
if marker_pos != -1:
# Get the text generated by the assistant
output = raw_output[marker_pos + len(assistant_turn_marker) :]
else:
# Fallback in case the marker isn't found
output = raw_output
# The model sometimes continues the conversation and includes the next user's turn.
# The 'stop' parameter is a good hint, but we parse the output as a safeguard.
stop_marker = "User's Turn:"
stop_pos = output.lower().find(stop_marker.lower())
if stop_pos != -1:
output = output[:stop_pos]
# The model also sometimes prefixes its response with "Output:". We'll remove this.
output = output.lstrip()
prefix_marker = "Output:"
if output.lower().startswith(prefix_marker.lower()):
output = output[len(prefix_marker) :]
return output.strip()
# Send a request to Gemini via the VertexAI API. Return the model's response
# contents = list of types.Content objects
def call_gemini_model(contents, model_temperature, top_p, max_tokens):
gemini_model = "gemini-2.5-flash"
response = client.models.generate_content(
model=gemini_model,
contents=contents,
config={
"temperature": model_temperature,
"max_output_tokens": max_tokens,
"top_p": top_p,
},
)
output = response.text # Extract the generated text
# Consider handling additional response attributes (safety, usage, etc.)
return output
def process_message_gemini(message, history):
contents = []
for user_turn, model_turn in history:
contents.append(
types.Content(role="user", parts=[types.Part.from_text(text=user_turn)])
)
contents.append(
types.Content(role="model", parts=[types.Part.from_text(text=model_turn)])
)
contents.append(
types.Content(role="user", parts=[types.Part.from_text(text=message)])
)
return contents
# This function takes a user's message and the conversation history as input.
# Its job is to format these elements into a single,
# structured prompt that can be understood by the language model (LLM).
# This structured format helps the LLM maintain context and generate more relevant responses.
def process_message_gemma(message, history):
user_prompt_format = "User's Turn:\n>>> {prompt}\n"
assistant_prompt_format = "Assistant's Turn:\n>>> {prompt}\n"
history_message = ""
for user_turn, assistant_turn in history:
history_message += user_prompt_format.format(prompt=user_turn)
history_message += assistant_prompt_format.format(prompt=assistant_turn)
# Format the new user message
new_user_message = user_prompt_format.format(prompt=message)
# Create a new aggregated message to be used as a single flat string in a json object sent to the LLM
aggregated_message = (
history_message + new_user_message + assistant_prompt_format.format(prompt="")
)
return aggregated_message
# Function to save chat history to Firestore
def save_chat_history(interaction, doc_ref):
timestamp_str = str(datetime.datetime.now())
# Save the chat history, merging with existing data
doc_ref.update({timestamp_str: interaction})
print("Chat history saved successfully!") # Optional: Log success
# Send the json message to the model and return the model's response. This is used for Gemma but not Gemini. It could also be used for other models.
def post_request(json_message):
print("*** Request" + str(json_message), flush=True)
# Set a timeout and check for HTTP errors. This will raise an exception on a bad status code (4xx or 5xx).
response = requests.post(host + context_path, json=json_message, timeout=60)
response.raise_for_status()
json_data = response.json()
print("*** Output: " + str(json_data), flush=True)
return json_data
# custom css to hide default footer
css = """
footer {display: none !important;} .gradio-container {min-height: 0px !important;}
"""
# Add a dropdown to select the model to chat with
model_dropdown = gr.Dropdown(
["Gemma3 12b it", "Gemini"],
label="Model",
info="Select the model you would like to chat with.",
value="Gemma3 12b it",
)
# Make the model temperature, top_p, and max tokents modifiable via sliders in the GUI
model_temperature = gr.Slider(
minimum=0.1, maximum=1.0, value=0.9, label="Temperature", render=False
)
top_p = gr.Slider(minimum=0.1, maximum=1.0, value=0.95, label="Top_p", render=False)
max_tokens = gr.Slider(
minimum=1, maximum=4096, value=1024, label="Max Tokens", render=False
)
# Call gradio to create the chat interface
app = gr.ChatInterface(
inference_interface,
additional_inputs=[model_dropdown, model_temperature, top_p, max_tokens],
theme=themes.google_theme(),
css=css,
title="Chat with AI",
)
app.launch(server_name="0.0.0.0", allowed_paths=["images"])
7. Chat-Anwendung bereitstellen
Wir verwenden Skaffold, um unser Container-Image zu erstellen und im Cluster bereitzustellen. Skaffold ist ein Befehlszeilentool, das den Prozess zum Erstellen, Übertragen und Bereitstellen von Anwendungen in Kubernetes orchestriert und automatisiert. Dadurch wird der Entwicklungsablauf vereinfacht, da Sie den gesamten Prozess mit einem einzigen Befehl auslösen können. Das ist ideal, um Ihre Anwendung zu iterieren.
Hinweis: Dadurch wird auch das Kubernetes-Dienstkonto bereitgestellt, das für Workload Identity erforderlich ist. Die Definition finden Sie in der Datei „deploy/chat-deploy.yaml“. Hier finden Sie die Definition:
apiVersion: v1
kind: ServiceAccount
metadata:
name: gradio-chat-ksa
Führen Sie Skaffold aus, um die Anwendung zu erstellen und bereitzustellen:
skaffold run --default-repo=us-central1-docker.pkg.dev/$GOOGLE_CLOUD_PROJECT/chat-app-repo
Skaffold verwendet Cloud Build, um das Container-Image zu erstellen, es in die von Terraform erstellte Artifact Registry zu übertragen und dann die Kubernetes-Manifeste auf Ihren Cluster anzuwenden.
8. Anwendung testen
- Warten Sie, bis dem Chat-Anwendungsdienst eine externe IP-Adresse zugewiesen wurde:
Sobald sichkubectl get svc gradio-chat-service --watchEXTERNAL-IPvonpendingin eine tatsächliche IP-Adresse ändert, drücken SieCtrl+C, um die Überwachung zu beenden. - Öffnen Sie einen Webbrowser und rufen Sie
http://[EXTERNAL-IP]:7860auf. - Probieren Sie es aus! Die App ist so konfiguriert, dass Sie standardmäßig mit Ihrem lokal gehosteten Gemma-Modell chatten. Wenn Sie mit Gemini chatten möchten, ändern Sie das Modell im Drop-down-Menü „Zusätzliche Eingaben“. Fragen Sie die KI zum Beispiel: „Erzähl mir einen Kubernetes-Witz.“
Fehlerbehebung:
- Wenn Sie eine Fehlermeldung wie „Diese Website ist nicht erreichbar“ oder „[EXTERNAL-IP] hat die Verbindung abgelehnt“ erhalten, ist möglicherweise ein Problem mit Ihrer Datei „app.py“ aufgetreten. Kehren Sie zum Schritt „app.py-Datei prüfen“ zurück und wiederholen Sie die Schritte ab dort.
- Die Benutzeroberfläche verwendet standardmäßig das Modell „Gemma3 12b it“. Wenn Sie sofort eine Fehlermeldung erhalten, ist der Gemma-Pod wahrscheinlich noch nicht bereit. Tipp:Sie können das Drop-down-Menü auf „Gemini“ umstellen, um die Interaktion mit der Chatanwendung zu testen, während Sie auf die Initialisierung von Gemma warten.
Gemma testen:Achten Sie darauf, dass im Drop-down-Menü „Gemma3 12b it“ ausgewählt ist, und senden Sie eine Nachricht (z.B. „Erzähl mir einen Witz über Kubernetes“).
Gemini testen:Stellen Sie das Drop-down-Menü auf „Gemini“ um und stellen Sie eine weitere Frage, z. B. „Was ist der Unterschied zwischen einem Pod und einem Knoten?“
Verlauf prüfen:Nachdem Sie erfolgreich mit einem Modell (Gemma oder Gemini) in der Chat-App gechattet haben, können Sie sich die Chatprotokolle in Ihrer Datenbank „chat-app-db“ in Firestore ansehen. Wenn Sie mit beiden Modellen chatten konnten, wird der Unterhaltungsverlauf auch beim Wechsel zwischen den Modellen beibehalten.
9. Weitere Informationen
Nachdem Sie nun eine funktionierende hybride Chatanwendung haben, können Sie sich mit den folgenden Herausforderungen auseinandersetzen, um Ihr Wissen zu vertiefen:
- Benutzerdefinierte Persona:Ändern Sie die Funktionen
process_message_gemmaundprocess_message_geminiso, dass am Anfang ein „System-Prompt“ enthalten ist. Sagen Sie den Modellen zum Beispiel: „Du bist ein hilfsbereiter Piratenassistent.“ und sehen Sie, wie sich die Antworten dadurch ändern. - Dauerhafte Nutzeridentität:Derzeit wird für jede Sitzung eine neue zufällige UUID generiert. Wie würden Sie ein echtes Authentifizierungssystem (z. B. Google Log-in) einbinden, damit ein Nutzer seinen bisherigen Unterhaltungsverlauf auf verschiedenen Geräten sehen kann?
- Modell ausprobieren:Versuchen Sie, den Schieberegler
temperaturein der Benutzeroberfläche zu ändern. Wie wirkt sich eine hohe Temperatur (nahe 1,0) im Vergleich zu einer niedrigen Temperatur (nahe 0,1) auf die Kreativität und Genauigkeit der Antworten aus?
10. Fazit
Glückwunsch! Sie haben eine hybride KI-Anwendung erstellt. Sie haben Folgendes gelernt:
- Terraform für Infrastructure-as-Code in Google Cloud verwenden
- Hosten Sie Ihre eigenen LLMs mit offenem Gewicht in GKE, um die vollständige Kontrolle zu behalten.
- Integrieren Sie verwaltete KI-Dienste wie Vertex AI, um flexibel zu sein.
- Zustandsorientierte Anwendung mit Firestore für die Persistenz erstellen
- Arbeitslasten mit Workload Identity schützen
Bereinigen
Löschen Sie die erstellten Ressourcen, um Gebühren zu vermeiden:
cd infra
terraform destroy -var="project_id=$GOOGLE_CLOUD_PROJECT" -var="project_number=$PROJECT_NUMBER" -var="region=$REGION"