Kurs
Lokale LLMs stehen vor einem Wendepunkt. Dank des langen Kontextfensters, der nativen Multimodalität und Zugänglichkeit von Gemma 4 über Ollama ist es inzwischen praktikabel, einen leistungsfähigen agentischen Coding-Assistenten komplett auf dem eigenen Rechner laufen zu lassen.
In diesem Tutorial zeige ich dir, wie du mit Gemma 4 über Ollama und einer Gradio-Oberfläche einen Coding-Assistenten baust. Die App bietet ein geteiltes Layout mit einem Live-Code-Editor links und einem agentischen Chat rechts. Du kannst Bilder oder Code-Dateien als Kontext hochladen, Tool-Einsatz aktivieren, damit das Modell Code ausführt und validiert, und für knifflige Aufgaben einen Thinking-Mode zuschalten.
Am Ende hast du eine lokale App, die:
- Code in 15+ Sprachen schreibt, erklärt und debuggt
- Python-Code in einem gekapselten Subprozess ausführt und die Ergebnisse zurückgibt
- Bilder und Textdateien als multimodalen Kontext akzeptiert
- Antworten in Echtzeit aus einem lokal laufenden Gemma‑4‑Modell streamt
- Als agentische Schleife läuft, Tools aufruft und auf Basis der Ergebnisse nachfasst
Den vollständigen Code zu diesem Tutorial findest du hier.
Was ist Gemma 4?
Gemma 4 ist die Open-Weights-Modellfamilie von Google DeepMind, entwickelt für lokale Bereitstellung und Forschung. Sie baut auf der Gemma-Reihe auf, mit besserem Instruction-Following, längeren Kontextfenstern sowie nativer Multimodal-Eingabe und basiert auf derselben Forschungsinfrastruktur wie Gemini 3.

Abbildung: Modellleistung vs. Größe (Quelle: Gemma‑4‑Blog)
Modelle wie Gemma‑4‑26B (MOE) und Gemma‑4‑31B erreichen Elo-Werte, die mit deutlich größeren Modellen vergleichbar sind – ein Hinweis auf starke Performance pro Parameter. Das 31B‑Modell rangiert aktuell als drittbestes offenes Modell weltweit auf dem Arena‑AI‑Text‑Leaderboard, das 26B‑Modell belegt Platz 6. Damit eignet sich Gemma 4 besonders gut für lokale und ressourcenbeschränkte Deployments, ohne an Leistungsfähigkeit einzubüßen.
Die Gemma‑4‑Modellfamilie
Gemma 4 erscheint in vier vielseitigen Größen: Effective 2B (E2B), Effective 4B (E4B), 26B Mixture of Experts (MoE) und 31B Dense. Die Familie teilt sich in zwei Tiers je nach Zielumgebung auf:
|
Model |
Architecture |
Total Params |
Active/Effective Params |
Context Length |
Modalities |
|
Gemma-4-31B |
Dense Transformer |
31B |
31B |
256K tokens |
Text, Vision, Video |
|
Gemma-4-26B-A4B |
MoE (128 Experts) |
26B |
3.8B active |
256K tokens |
Text, Vision, Video |
|
Gemma-4-E4B |
Dense Transformer |
7.9B (with embeddings) |
4.5B effective |
128K tokens |
Text, Audio, Vision, Video |
|
Gemma-4-E2B |
Dense Transformer |
5.1B (with embeddings) |
2.3B effective |
128K tokens |
Text, Audio, Vision, Video |
Schauen wir uns die einzelnen Varianten genauer an:

Abbildung: Visuelle Übersicht zu Gemma 4 (Quelle)
- 31B Dense ist das Flaggschiffmodell, optimiert für Rechenzentrums‑Deployments und komplexe Reasoning‑Workloads. Es unterstützt ein Kontextfenster mit 256K Token und einem Sliding Window von 1024 Token für effiziente Langkontext-Verarbeitung.
- 26B-A4B (MoE) ist Gemmas erstes MoE‑Modell, das Token über 128 Experts routet, wobei pro Forward‑Pass nur 3,8B Parameter aktiv sind. So erreicht es nahezu 31B‑Qualität bei einem Bruchteil der Rechenkosten pro Token – ideal für High‑Throughput‑Serving.
- E4B und E2B sind die On‑Device‑ und Mobile‑Tier. Anders als die größeren Varianten unterstützen sie native Audio‑Eingaben für Spracherkennung neben Vision und Video – damit sind sie die multimodal stärksten Modelle der Familie für Edge‑Deployments.
Alle vier Modelle stehen unter der Apache‑2.0‑Lizenz und lassen sich lokal über Ollama, vLLM, llama.cpp oder Unsloth bereitstellen.
Für Coding‑Aufgaben glänzt Gemma 4 bei:
- Komplettem, strukturiertem Code mit Erklärungen
- Reasoning über bestehende Codebasen, wenn sie als Kontext gegeben sind
- Tool‑Einsatz und agentische Workflows in Kombination mit einer Orchestrierungsschicht
- Umgang mit Bildern neben Code (z. B. UI‑Screenshot lesen und passendes HTML generieren)
In diesem Tutorial verwenden wir die Variante gemma4:e4b (9,6 GB) über Ollama – eine quantisierte Version, die sich gut für lokale Inferenz auf Consumer‑Hardware eignet.
Gemma 4 über Ollama ausführen
Ollama übernimmt Download, Quantisierung, Serving und stellt eine OpenAI‑kompatible HTTP‑API bereit. In diesem Tutorial dient Ollama als Inferenz‑Backend, und unsere App kommuniziert darüber via localhost:11434.
So holst und startest du das Modell:
curl -fsSL https://ollama.com/install.sh | sh
ollama pull gemma4:e4b
ollama serve
Die obigen Befehle installieren Ollama lokal über das offizielle Installationsskript. Anschließend laden wir die Modellvariante gemma 4:e4b lokal für On‑Device‑Inferenz. Zum Schluss startet ollama serve den Ollama‑Server, sodass unsere App Anfragen an das Modell senden kann.
Sobald der Server läuft, verbindet sich die Gradio‑App automatisch.
Gemma‑4‑Demo: Baue einen Coding‑Assistenten mit Ollama
In diesem Abschnitt bauen wir den Code‑Assistenten Schritt für Schritt. Auf hoher Ebene erledigt die App Folgendes:
-
Nimmt natürliche Sprache im Chatpanel entgegen – optional mit Bild- oder Dateianhängen
-
Injiziert den aktuellen Editor‑Code als Kontext für das Modell
-
Sendet eine Streaming‑Anfrage an Gemma 4 über Ollamas
/api/chat‑Endpoint -
Ruft optional Tools (Codeausführung, Matheauswertung) in einer agentischen Schleife auf
-
Übernimmt erkannte Codeblöcke aus der Antwort direkt in den Live‑Editor
Lass es uns Schritt für Schritt aufbauen.

Schritt 1: Abhängigkeiten installieren
Die App benötigt einige wenige Bibliotheken für UI, Bildverarbeitung und HTTP‑Kommunikation. Da das Modell lokal über Ollama läuft, gibt es keine Cloud‑SDK‑Abhängigkeiten.
pip install gradio requests pillow
In diesem Projekt verwenden wir:
-
gradiofür den geteilten Editor und die Chat‑UI -
requestsfür die Kommunikation mit der Ollama‑HTTP‑API -
pillowfür die Bildverarbeitung in der UI
Damit bleibt die Umgebung schlank und läuft auf jeder Maschine mit Ollama – inklusive macOS mit Apple Silicon, Linux und Windows via WSL.
Schritt 2: Konfiguration und Konstanten
Bevor wir Logik schreiben, definieren wir Imports, den Modellnamen, die Ollama‑Basis‑URL, unterstützte Sprachen und einen Standard‑Systemprompt. Diese Konstanten steuern das Verhalten der gesamten App.
import base64
import json
import math
import os
import re
import subprocess
import tempfile
from pathlib import Path
import gradio as gr
import requests
OLLAMA_BASE = "http://localhost:11434"
MODEL = "gemma4:e4b"
LANGUAGES = [
"python", "javascript", "typescript", "bash", "sql",
"rust", "go", "java", "c", "cpp", "html", "css",
"json", "yaml", "markdown", "plaintext",
]
TEXT_EXTS = {
".py", ".js", ".ts", ".jsx", ".tsx", ".html", ".css",
".json", ".yaml", ".yml", ".toml", ".md", ".txt",
".csv", ".sql", ".sh", ".bash", ".rs", ".go",
".java", ".c", ".cpp", ".h", ".hpp", ".rb", ".php",
}
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp"}
DEFAULT_SYSTEM = """\
You are an expert coding assistant. When you write code:
- Always wrap it in a markdown code block with the language tag
- Write complete, working code — not fragments
- Briefly explain what the code does
You have a code-runner tool. Use it to validate logic when helpful.\
"""
Die Sets TEXT_EXTS und IMAGE_EXTS steuern zwei unterschiedliche Anhangsverhalten. Text‑/Code‑Dateien werden gelesen und als Kontext in den Prompt injiziert, während Bilddateien base64‑kodiert und im Images‑Feld für Gemma 4s Vision‑Fähigkeiten übergeben werden.
Der DEFAULT_SYSTEM‑Prompt beeinflusst den Stil der Codegenerierung und fordert explizit vollständige Codeblöcke mit Sprach‑Tags an, da die App diese parst, um Code in den Editor zu übernehmen.
Schritt 3: Agentische Tools definieren
Der Assistent kann in einem agentischen Modus arbeiten, in dem er während der Inferenz Tools aufruft. Wir definieren zwei Tools im Standard‑Funktionsaufruf‑Schema, das Ollama unterstützt:
TOOLS = [
{
"type": "function",
"function": {
"name": "run_code",
"description": (
"Execute Python code in a sandboxed subprocess and return "
"stdout + stderr. Use this to validate, test, or demonstrate code."
),
"parameters": {
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "Python code to run (max ~50 lines, 5 s timeout).",
}
},
"required": ["code"],
},
},
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "Evaluate a mathematical expression precisely.",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Python-compatible math expression, e.g. 'math.sqrt(2) * 100'",
}
},
"required": ["expression"],
},
},
},
]
Der obige Codeschnipsel definiert das Tool run_code, das Python in einer temporären Datei via Subprozess mit 5‑Sekunden‑Timeout ausführt und sowohl stdout als auch stderr erfasst. So kann das Modell Logik validieren, Beispiele ausführen oder Live‑Output erzeugen – nicht nur statischen Text.
Das Tool calculate wertet Mathe‑Ausdrücke in einem eingeschränkten Namensraum aus – nur mit dem Modul math und sicheren Built‑ins. So erhält das Modell präzise numerische Resultate, ohne einen kompletten Python‑Prozess zu starten.
Schritt 4: Toolausführung
Die Ausführungsschicht mappt Tool‑Namen auf ihre Implementierungen. Sie wird in der agentischen Schleife aufgerufen, sobald das Modell einen Block tool_calls zurückgibt.
def _run_python(code: str) -> str:
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(code)
tmp = f.name
try:
r = subprocess.run(
["python3", tmp],
capture_output=True, text=True, timeout=5,
)
out = (r.stdout + r.stderr).strip()
return out[:3000] if out else "(no output)"
except subprocess.TimeoutExpired:
return "⏱ Timed out (>5 s)"
except Exception as e:
return f"Error: {e}"
finally:
os.unlink(tmp)
def _calculate(expr: str) -> str:
ns = {k: getattr(math, k) for k in dir(math) if not k.startswith("_")}
ns.update({"abs": abs, "round": round})
try:
return str(eval(expr, {"__builtins__": {}}, ns)) # noqa: S307
except Exception as e:
return f"Error: {e}"
def execute_tool(name: str, args: dict) -> str:
if name == "run_code":
return _run_python(args.get("code", ""))
if name == "calculate":
return _calculate(args.get("expression", ""))
return f"Unknown tool: {name}"
Einige erwähnenswerte Sicherheitsaspekte der Tool‑Ausführung:
-
_run_python()schreibt in eine temporäre Datei und räumt sie auf – so bleiben keine Dateien zurück, selbst bei Fehlern -
Output ist auf 3.000 Zeichen begrenzt, damit der Kontext nicht von riesigen Print‑Dumps überflutet wird
-
Die Funktion
_calculate()nutzteval()mit leerem__builtins__, wodurch der Zugriff auf Python‑Built‑ins außerhalb der explizit erlaubtenmath‑Funktionen verhindert wird -
Das Timeout ist strikt auf 5 Sekunden gesetzt – lang genug für die meisten Validierungen, kurz genug, um ausufernde Prozesse zu verhindern
Schritt 5: Helfer‑Utilities
Bevor wir die Kern‑Chatlogik bauen, benötigen wir einige Utility‑Funktionen für Pfade, Bildkodierung und Code‑Extraktion. Diese Helfer werden durchgängig in der Streaming‑Pipeline genutzt.
def encode_image(path: str) -> str | None:
if not path:
return None
try:
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()
except Exception:
return None
def file_as_context(path: str) -> str | None:
if not path:
return None
p = Path(path)
if p.suffix.lower() not in TEXT_EXTS:
return None
try:
content = p.read_text(encoding="utf-8", errors="replace")[:8000]
lang = p.suffix.lstrip(".")
return f"\n\n**Attached file — {p.name}:**\n```{lang}\n{content}\n```"
except Exception:
return None
def resolve_gradio_path(val) -> str | None:
if val is None:
return None
if isinstance(val, Path):
s = str(val)
return s if s.strip() else None
if isinstance(val, str):
s = val.strip()
return s if s else None
if isinstance(val, dict):
p = val.get("path")
if isinstance(p, str) and p.strip():
return p.strip()
nested = val.get("file")
if isinstance(nested, dict):
np = nested.get("path")
if isinstance(np, str) and np.strip():
return np.strip()
return None
name = getattr(val, "name", None)
if isinstance(name, str) and name.strip():
return name.strip()
return None
def is_image_path(path: str | None) -> bool:
return bool(path) and Path(path).suffix.lower() in IMAGE_EXTS
def extract_last_code_block(text: str) -> tuple[str | None, str]:
blocks = re.findall(r"```(\w*)\n(.*?)```", text, re.DOTALL)
if blocks:
lang, code = blocks[-1]
return code.strip(), lang.strip() or "python"
return None, "python"
def ollama_ok() -> bool:
try:
requests.get(f"{OLLAMA_BASE}/", timeout=2)
return True
except Exception:
return False
def _gradio_content_to_text(content) -> str:
if content is None:
return ""
if isinstance(content, str):
return content
if isinstance(content, list):
parts: list[str] = []
for block in content:
if isinstance(block, dict):
if block.get("type") == "text":
parts.append(str(block.get("text", "")))
elif "text" in block:
parts.append(str(block["text"]))
elif isinstance(block, str):
parts.append(block)
return "".join(parts)
return str(content)
def _append_chat_turn(
history: list | None, user_text: str, assistant_text: str
) -> list:
base = list(history) if history else []
return base + [
{"role": "user", "content": user_text},
{"role": "assistant", "content": assistant_text},
]
def run_code_btn(code: str) -> str:
if not code.strip():
return "Nothing to run."
result = _run_python(code)
return result
Diese Utilities ermöglichen einen reibungslosen Umgang mit multimodalen Eingaben und strukturierten Ausgaben:
-
encode_image()‑Funktion: Sie wandelt Bilddateien in einenbase64‑String um, der imimages‑Feld von Ollama übergeben wird. So kann Gemma Bilder zusammen mit Text für multimodales Reasoning verarbeiten. -
file_as_context()‑Funktion: Sie liest Text‑/Code‑Dateien und injiziert sie direkt als formatierten Markdown‑Block in den Prompt. Das erspart ein separates Retrieval, da alles inline als Kontext übergeben wird. -
resolve_gradio_path()‑Funktion: Sie normalisiert unterschiedliche Dateiformate, die Gradio zurückgibt, zu einem nutzbaren Pfad. Das verhindert Edge‑Case‑Bugs beim Dateihandling. -
is_image_path()‑Funktion: Schneller Check, ob eine Datei ein Bild ist – wichtig für korrektes Routing von Bildeingaben. -
extract_last_code_block()‑Funktion: Extrahiert den letzten Codeblock aus der Modellantwort. -
ollama_ok()‑Funktion: Prüft, ob der lokale Ollama‑Server läuft, bevor Anfragen gesendet werden. -
_gradio_content_to_text()und_append_chat_turn(): Flatten Gradio‑Nachrichten ins Plain‑Text‑Format und pflegen die Chat‑Historie. -
run_code_btn()‑Funktion: Führt den generierten Code aus, wenn du im UI den Run code‑Button klickst.
Mit den Helpers an Bord implementieren wir nun die Kernfunktion chat().
Schritt 6: Kern‑Chat‑Streaming‑Generator
Diese Funktion steuert die komplette Interaktion – von der Input‑Vorbereitung über Streaming‑Antworten bis zur Tool‑Ausführung.
Auf hoher Ebene erledigt chat() Folgendes:
- Baut die Gesprächshistorie auf
- Anreichern der aktuellen Nutzereingabe mit Kontext (Code, Dateien, Bilder)
- Senden einer Streaming‑Anfrage an Ollama
- Optionales Ausführen von Tools (agentischer Modus)
- Yield von Teilantworten für Live‑Updates in der UI
def chat(
message, history, image_path, file_path,
editor_code, language, system_prompt,
agentic, thinking, temperature,
):
Die Funktion folgt dieser Abfolge:
Schritt 1: Nachrichtenhistorie aufbauen
Wir konvertieren zuerst vorherige Chat‑Turns in Ollamas Nachrichtenformat. Der System‑Prompt wird vorangestellt, um das Modell zu steuern und die volle Gesprächshistorie vor der Antwort zu geben.
for h in history or []:
if not isinstance(h, dict):
continue
role = h.get("role")
if role not in ("user", "assistant"):
continue
messages.append(
{"role": role, "content": _gradio_content_to_text(h.get("content"))}
)
content = message
Schritt 2: Nutzer‑Turn komponieren
Als Nächstes bauen wir die aktuelle Nutzernachricht, indem wir zusätzlichen Kontext injizieren. Arbeitet der Nutzer mit Code, hängen wir den Editor‑Inhalt an:
if editor_code.strip() and editor_code.strip() != STARTER_CODE.strip():
content += (
f"\n\n**Current code in editor ({language}):**\n"
f"```{language}\n{editor_code}\n```"
)
Hochgeladene Dateien werden als Inline‑Kontext ergänzt, Bilder werden base64‑kodiert und über das Images‑Feld übergeben. So kann das Modell Text, Code und Bilder gemeinsam verarbeiten.
Schritt 3: Ollama‑Request senden
Das Payload enthält Modellname, Nachrichtenverlauf, Streaming‑Flag und Optionen:
payload = {
"model": MODEL,
"messages": messages,
"stream": True,
"options": options,
}
if agentic:
payload["tools"] = TOOLS
Ist der Thinking‑Mode aktiviert, wird options["think"] = True gesetzt – das aktiviert erweitertes Chain‑of‑Thought‑Reasoning in unterstützten Modellen.
Schritt 4: Tool‑Aufrufe handhaben (agentischer Modus)
Im agentischen Modus kann das Modell statt Text Tool‑Aufrufe zurückgeben. Jedes Tool wird dann ausgeführt, die Ergebnisse werden dem Gespräch hinzugefügt und eine Folgeanfrage setzt die Generierung fort.
if agentic and msg.get("tool_calls"):
for tc in msg["tool_calls"]:
fn_name = tc["function"]["name"]
fn_args = tc["function"]["arguments"]
result = execute_tool(fn_name, fn_args)
if fn_name == "run_code" and fn_args.get("code"):
new_code = fn_args["code"]
messages.append(msg)
messages.append({"role": "tool", "content": result})
resp2 = requests.post(f"{OLLAMA_BASE}/api/chat", ...)
So entsteht eine agentische Ein‑Turn‑Schleife: Das Modell ruft ein Tool auf, sieht das Ergebnis und generiert dann die finale Antwort weiter. Tool‑Logs werden für den Nutzer im Chat vorangestellt.
Schritt 5: Code extrahieren und Editor aktualisieren
Nach Abschluss des Streamings extrahieren wir den letzten Codeblock aus der Antwort:
extracted, _ = extract_last_code_block(full_response)
if extracted:
new_code = extracted
Das Modell schreibt also Code, der Editor aktualisiert sich automatisch – ganz ohne Copy & Paste.
Schritt 7: Gradio‑UI‑Layout
Mit der Kernlogik an Ort und Stelle gestalten wir das UI mit Gradio. Ziel ist ein Layout, das Coding‑Workflows und KI‑Interaktion nebeneinander unterstützt.
Wir nutzen gr.Blocks für ein Zwei‑Spalten‑Layout:
- Links steht der Code‑Editor mit Ausführung im Fokus
- Rechts laufen Chat, Anhänge und Modellsteuerung
with gr.Blocks(title="Gemma 4 · Code Assistant") as demo:
with gr.Row(equal_height=False):
# LEFT: Code Editor
with gr.Column(scale=11):
with gr.Row():
lang_sel = gr.Dropdown(choices=LANGUAGES, value="python", label="Language")
run_btn = gr.Button("Run Code", elem_classes=["run-btn"])
clear_ed = gr.Button("Clear")
code_editor = gr.Code(
value=STARTER_CODE,
language="python",
label="Editor",
lines=24,
interactive=True,
)
run_output = gr.Textbox(
label="Output",
lines=6,
interactive=False,
elem_id="run-output",
)
# RIGHT: Chat
with gr.Column(scale=9):
chatbot = gr.Chatbot(value=[], elem_id="chatbot", height=430)
with gr.Row():
image_upload = gr.Image(label="Image (vision)", type="filepath")
file_upload = gr.File(label="Code / text file")
with gr.Row():
msg_input = gr.Textbox(placeholder="Ask the agent...", scale=6)
send_btn = gr.Button("Send", variant="primary")
with gr.Row():
agentic_cb = gr.Checkbox(label="Enable Agentic", value=True)
thinking_cb = gr.Checkbox(label="Enable Thinking", value=False)
clear_chat = gr.Button("Clear chat")
with gr.Accordion("Settings", open=False):
sys_prompt = gr.Textbox(value=DEFAULT_SYSTEM, label="System prompt")
temperature = gr.Slider(minimum=0.0, maximum=2.0, value=0.7)
So greift das UI mit den beschriebenen Komponenten ineinander:
- Code‑Workspace: Das linke Panel bietet Sprachwahl, Editor sowie Ausführen/Zurücksetzen. Die Ausgaben erscheinen in einer hervorgehobenen Textbox.
- Chat und Interaktion: Das rechte Panel enthält den Chatbot, das Eingabefeld für Fragen sowie Uploads – Bilder für multimodales Reasoning, Dateien zur Kontextinjektion. Zusätzlich schalten Toggles Tool‑Einsatz und optionales Reasoning frei.
- Layout und Usability: Zwei Spalten mit ca. 55/45‑Aufteilung – der Editor hat Priorität, der Chat bleibt schnell zugänglich. Das sorgt für einen flüssigen Workflow zwischen Coding, KI‑Hilfe und Ausführung.
Schritt 8: Event‑Verkabelung
Senden und Submit nutzen dieselben Inputs/Outputs – Buttonklick und Enter im Textfeld lösen identisches Verhalten aus:
_inputs = [
msg_input, chatbot, image_upload, file_upload,
code_editor, lang_sel,
sys_prompt, agentic_cb, thinking_cb, temperature,
]
_outputs = [chatbot, code_editor, msg_input]
send_btn.click(fn=respond, inputs=_inputs, outputs=_outputs)
msg_input.submit(fn=respond, inputs=_inputs, outputs=_outputs)
Der Wrapper respond() wandelt Gradio‑Pfadobjekte in String‑Pfade um, bevor er sie an den Streaming‑Generator übergibt. Danach wird bei jedem Chunk (history, code, "") ausgegeben, sodass das leere Feld die Eingabe direkt beim ersten Yield leert.
Schritt 9: Theme und CSS
Zur besseren Usability und Konsistenz ergänzen wir ein Custom‑Theme und CSS auf Basis des Gradio‑Standard‑Styles. So erhält die App einen polierten, GitHub‑ähnlichen Look – mit Light‑ und Dark‑Mode. Optional, aber empfehlenswert.
Wir definieren zunächst ein Basistheme mit gr.themes.Base:
THEME = gr.themes.Base(
primary_hue = "blue",
secondary_hue = "slate",
neutral_hue = "slate",
font = [gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"],
font_mono = [gr.themes.GoogleFont("JetBrains Mono"), "monospace"],
)
Das sorgt für eine konsistente Farbpalette und eine klare Typografie (Inter) mit entwicklerfreundlicher Monospace‑Schrift (JetBrains Mono). Danach überschreiben wir zentrale CSS‑Variablen für das Erscheinungsbild:
gradio-app {
--body-background-fill: #f0f3f7;
--block-background-fill: #ffffff;
--body-text-color: #1f2328;
--button-primary-background-fill: #1a7f37;
}
body.dark gradio-app {
--body-background-fill: #0d1117;
--block-background-fill: #1c2128;
--body-text-color: #e6edf3;
--button-primary-background-fill: #238636;
}
Der erste Block definiert das helle Theme, der zweite überschreibt Werte für den Dark Mode. Auch wenn optional, steigern Theme und CSS die User Experience deutlich.
Mit der strukturierten und gestylten UI kommen wir zum letzten Schritt: dem Start der Anwendung.
Schritt 10: Start
Zum Schluss startest du die App mit:
if __name__ == "__main__":
print(f"Model : {MODEL}")
print(f"Ollama : {OLLAMA_BASE}")
if not ollama_ok():
print("Ollama not detected — run ollama serve before chatting")
demo.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
theme=THEME,
css=CSS,
)
Mit server_name="0.0.0.0" ist die App im lokalen Netzwerk erreichbar, und share=False hält alles lokal. Der Check ollama_ok() sendet einen schnellen GET an localhost:11434 und warnt beim Start, falls Ollama nicht läuft – statt erst beim ersten Senden einer Nachricht einen Fehler zu zeigen.
Öffne dann http://localhost:7860, sobald die App läuft.
So sieht der komplette Ablauf für Nutzer aus:

-
Im Chatpanel eine Nachricht tippen, optional Bild oder Code‑Datei hochladen und Agentic/Thinking umschalten
-
Beim Absenden baut
respond()die vollständige Ollama‑Anfrage – inklusive Editor‑Code und Anhängen als Kontext -
Die Antwort streamt Token für Token, Teilantworten werden live im Gradio‑Chatbot dargestellt
-
Ist der agentische Modus an und das Modell ruft ein Tool auf, läuft es lokal, Ergebnisse werden angehängt, und eine Folgeanfrage streamt die finale Antwort
-
Nach dem Streaming wird der letzte fenced Codeblock extrahiert und automatisch in den Editor übernommen
Fazit
In diesem Tutorial haben wir mit Gemma 4, Ollama und Gradio einen vollständig lokalen KI‑Coding‑Assistenten gebaut. Die App unterstützt multimodale Eingaben, echten Tool‑Einsatz, Streaming‑Antworten und einen Live‑Code‑Editor – alles auf deinem Rechner, ohne externe API.
Der Whole‑Context‑Ansatz, den Editor‑Code in jeden Prompt zu injizieren, ist für Single‑File‑Workflows simpler als RAG und eignet sich besonders gut fürs Erklären, Refactoren oder Erweitern des Codes, an dem du gerade arbeitest.
Darauf kannst du aufbauen:
- Einen Dateibaum hinzufügen, um Multi‑File‑Projekte zu unterstützen und ausgewählte Dateien als Kontext zu injizieren
- Weitere Ollama‑Modelle per Dropdown unterstützen, damit Nutzer zwischen Gemma 4, Llama 3 und anderen wechseln können
- Chat‑Historie auf Disk persistieren, damit Sessions Neustarts überleben
Ich bin Google Developers Expertin für ML (Gen AI), dreifache Kaggle-Expertin und Women-Techmakers-Botschafterin mit über drei Jahren Erfahrung in der Tech-Branche. 2020 habe ich ein Health-Tech-Startup mitgegründet und absolviere derzeit einen Master in Informatik an der Georgia Tech mit Schwerpunkt Machine Learning.
Gemma 4 FAQs
Braucht das eine GPU?
Nicht zwingend. Gemma 4 e4b ist ein quantisiertes Modell, das auf der CPU läuft – eine GPU beschleunigt die Inferenz aber deutlich.
Worin unterscheidet sich der agentische Modus vom normalen Chat?
Im normalen Modus streamt das Modell nur Text. Im agentischen Modus kann es run_code oder calculate aufrufen, die Ergebnisse prüfen und sie vor dem Abschluss der Antwort einarbeiten.
Wie aktualisiert sich der Editor automatisch, wenn der Agent Code schreibt?
Die Helper‑Funktion extract_last_code_block() scannt die Antwort des Assistenten nach dem letzten fenced Codeblock und übergibt ihn nach Abschluss des Streamings an den Editor. Darum weist der System‑Prompt das Modell an, Code stets in fenced Blöcken mit Sprach‑Tag zu kapseln.
Was passiert, wenn Ollama nicht läuft?
Der Check ollama_ok() fängt das beim Start und bei jeder Chat‑Eingabe ab. Ist Ollama nicht erreichbar, gibt der Chat eine formatierte Fehlermeldung aus, statt abzustürzen.

