Ollama: Eine ChatGPT-ähnliche Webanwendung erstellen
In diesem Kapitel erstellen wir eine ChatGPT-ähnliche Anwendung: Umschalten zwischen mehreren Sitzungen, Verlaufspersistenz in einer Datenbank, eine vollständige Frontend-Oberfläche und drei Bereitstellungsmethoden.
Funktionsplanung und Datenmodell
Ein nutzbares Chat-Produkt benötigt mindestens vier Fähigkeiten: Multi-Session-Isolation, Verlaufspersistenz, Streaming-Antworten und Sitzungsverwaltung (Erstellen und Löschen).
Das Datenbankmodell, das diese Fähigkeiten unterstützt, benötigt nur zwei Tabellen:
chats speichert die Sitzungsliste, und messages speichert jede Nachricht gruppiert nach Fremdschlüssel. Beim Wechseln von Sitzungen werden die entsprechenden Nachrichten in das messages-Array von Ollama zusammengestellt, um den vollständigen Kontext wiederherzustellen.
Der Technologie-Stack folgt dem Minimal-Abhängigkeits-Prinzip: Flask + sqlite3 (Python-Standardbibliothek) + Vanilla-JavaScript-Frontend, keine Frontend-Frameworks.
Installiere flask und die ollama-Erweiterung:
pip install flask ollama
Lade das Modell:
ollama pull qwen3.5:4b
Backend: Vollständige API für Sitzungen und Nachrichten
Das Backend hat klare Zuständigkeiten: die beiden Tabellen verwalten und jede Streaming-Antwort von /api/chat einzeln in die Datenbank schreiben.
Beispiel
import json
import sqlite3
from flask import Flask, request, Response, jsonify, stream_with_context
from ollama import chat
app = Flask(__name__)
DB = 'chats.db'
def db():
"""Öffnet eine Datenbankverbindung; Zeilen werden als Dictionaries zurückgegeben"""
conn = sqlite3.connect(DB)
conn.row_factory = sqlite3.Row
return conn
def init_db():
with db() as conn:
conn.executescript('''
CREATE TABLE IF NOT EXISTS chats(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS messages(
id INTEGER PRIMARY KEY AUTOINCREMENT,
chat_id INTEGER NOT NULL,
role TEXT NOT NULL,
content TEXT NOT NULL
);
''')
init_db()
# Sitzungsliste
@app.get('/api/chats')
def list_chats():
with db() as conn:
rows = conn.execute(
'SELECT id, title, created_at FROM chats ORDER BY id DESC'
).fetchall()
return jsonify([dict(r) for r in rows])
# Neue Sitzung erstellen
@app.post('/api/chats')
def create_chat():
title = (request.json or {}).get('title', 'Neue Sitzung')
with db() as conn:
cur = conn.execute('INSERT INTO chats(title) VALUES (?)', (title,))
return jsonify({'id': cur.lastrowid, 'title': title})
# Sitzung löschen (zusammen mit ihren Nachrichten)
@app.delete('/api/chats/<int:cid>')
def delete_chat(cid):
with db() as conn:
conn.execute('DELETE FROM messages WHERE chat_id=?', (cid,))
conn.execute('DELETE FROM chats WHERE id=?', (cid,))
return jsonify({'ok': True})
# Verlauf einer Sitzung lesen
@app.get('/api/chats/<int:cid>/messages')
def get_messages(cid):
with db() as conn:
rows = conn.execute(
'SELECT role, content FROM messages WHERE chat_id=? ORDER BY id',
(cid,)
).fetchall()
return jsonify([dict(r) for r in rows])
Backend-Kern: Streaming-Dialog und Verlauf-Rückschreibung
Die Chat-Oberfläche erledigt drei Dinge: Verlauf abrufen und Kontext aufbauen, Generierung streamen und die vollständige Antwort zurück in die Datenbank schreiben.
Beispiel
@app.post('/api/chat')
def do_chat():
data = request.get_json()
cid, user_input = data['chat_id'], data['message']
# Verlauf abrufen (einschließlich der gerade eingefügten Benutzernachricht) als Modellkontext
with db() as conn:
conn.execute(
'INSERT INTO messages(chat_id, role, content) VALUES (?,?,?)',
(cid, 'user', user_input)
)
history = [dict(r) for r in conn.execute(
'SELECT role, content FROM messages WHERE chat_id=? ORDER BY id',
(cid,)
)]
messages = [{'role': 'system',
'content': 'Du bist der EXAMPLE-Programmierassistent. Antworte präzise und knapp.'}]
messages += history
def generate():
reply = ''
stream = chat(model='qwen3.5:4b',
messages=messages, stream=True)
for chunk in stream:
reply += chunk.message.content
# Jeden Chunk als NDJSON an das Frontend senden
yield json.dumps(
{'delta': chunk.message.content},
ensure_ascii=False) + '\n'
# Wichtig: die vollständige Antwort zurück in die Datenbank schreiben, um persistenten Speicher zu bilden
with db() as conn:
conn.execute(
'INSERT INTO messages(chat_id, role, content) VALUES (?,?,?)',
(cid, 'assistant', reply))
yield json.dumps({'done': True}, ensure_ascii=False) + '\n'
return Response(
stream_with_context(generate()),
mimetype='application/x-ndjson')
# Startseite: die Frontend-Seite zurückgeben
@app.get('/')
def index():
return open('index.html', encoding='utf-8').read()
if __name__ == '__main__':
app.run(port=5000)
Frontend: Chat-Oberfläche in Vanilla JS
Das Frontend ist funktionsvollständig und einfach genug: Sitzungsliste links, Nachrichtenbereich plus Eingabefeld rechts, mit Fetch-Streaming-Rendering.
Beispiel
<!-- Dateipfad: index.html (gleiches Verzeichnis wie server.py) -->
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>Lokales ChatGPT</title>
<style>
body { display:flex; height:100vh; margin:0; font-family: sans-serif; }
aside { width:220px; border-right:1px solid #ddd; overflow-y:auto; }
aside button { display:block; width:100%; text-align:left;
padding:10px; border:0; background:none; cursor:pointer; }
aside button:hover { background:#f2f2f2; }
main { flex:1; display:flex; flex-direction:column; }
#log { flex:1; overflow-y:auto; padding:20px; }
.msg { margin:8px 0; line-height:1.6; white-space:pre-wrap; }
form { display:flex; border-top:1px solid #ddd; }
input { flex:1; padding:12px; border:0; }
</style>
</head>
<body>
<aside>
<button onclick="newChat()">+ Neue Sitzung</button>
<div id="list"></div>
</aside>
<main>
<div id="log"></div>
<form id="f">
<input id="q" placeholder="Geben Sie eine Frage ein, drücken Sie die Eingabetaste zum Senden" autocomplete="off">
</Form>
</main>
<script>
let chatId = null;
const log = document.getElementById('log');
const list = document.getElementById('list');
// Sitzungsliste laden
async function loadChats() {
const chats = await (await fetch('/api/chats')).json();
list.innerHTML = '';
for (const c of chats) {
const b = document.createElement('button');
b.textContent = c.title;
b.onclick = () => openChat(c.id);
list.appendChild(b);
}
}
// Sitzung öffnen und Verlauf wiederherstellen
async function openChat(id) {
chatId = id;
log.innerHTML = '';
const msgs = await (await fetch(`/api/chats/${id}/messages`)).json();
for (const m of msgs) addMsg(m.role, m.content);
}
// Neue Sitzung erstellen
async function newChat() {
const c = await (await fetch('/api/chats', {method:'POST'})).json();
await loadChats();
openChat(c.id);
}
// Eine Nachricht an die Oberfläche anhängen
function addMsg(role, text) {
const div = document.createElement('div');
div.className = 'msg';
div.textContent = (role === 'user' ? 'Du: ' : 'Assistent: ') + text;
log.appendChild(div);
log.scrollTop = log.scrollHeight;
return div;
}
// Antwort senden und als Stream rendern
document.getElementById('f').onsubmit = async e => {
e.preventDefault();
const input = document.getElementById('q');
const q = input.value.trim();
if (!q || !chatId) return;
input.value = '';
addMsg('user', q);
const div = addMsg('assistant', '');
const resp = await fetch('/api/chat', {method:'POST',
headers:{'Content-Type':'application/json'},
body: JSON.stringify({chat_id: chatId, message: q})});
const reader = resp.body.getReader();
const dec = new TextDecoder();
let buf = '';
while (true) {
const {done, value} = await reader.read();
if (done) break;
buf += dec.decode(value, {stream:true});
const lines = buf.split('\n');
buf = lines.pop();
for (const line of lines) {
if (!line) continue;
const obj = JSON.parse(line);
if (obj.delta) {
div.textContent += obj.delta;
log.scrollTop = log.scrollHeight;
}
}
}
};
newChat();
loadChats();
</script>
</body>
</html>
Starten und aufrufen:
python server.py
Öffnen Sie http://localhost:5000 in Ihrem Browser, um die generierte Seite zu sehen.
Überprüfe drei Kernfunktionen: Der Verlauf wird beim Wechseln der Sitzungen links vollständig wiederhergestellt; neue Sitzungen beeinträchtigen sich nicht gegenseitig; der Verlauf ist auch nach dem Neustart von server.py weiterhin vorhanden (in chats.db persistiert).
Bereitstellung: Drei Stufen
| Stufe | Ansatz | Hinweise |
|---|---|---|
| Lokale persönliche Nutzung | python server.py direkt ausführen | Lauscht standardmäßig auf 127.0.0.1, sicher |
| Intranet-Freigabe | app.run(host='0.0.0.0'), Mitglieder greifen auf http://server-IP:5000 zu | Keine Authentifizierung; füge unbedingt Nginx Basic Auth hinzu (siehe Kapitel zur privaten Bereitstellung) |
| Langfristiger Betrieb auf einem Cloud-Server | systemd-Dienstregistrierung + Nginx-Reverse-Proxy + Domainname | Führe zuerst ollama pull <model> auf dem Server aus |
Verwende systemd auf dem Cloud-Server, um die Anwendung dauerhaft am Laufen zu halten:
Beispiel
[Unit]
Description=Beispiel-Chat-Web-App
After=network.target ollama.service
[Service]
WorkingDirectory=/opt/example-chat
ExecStart=/usr/bin/python3 server.py
Restart=always
RestartSec=3
[Installation]
WantedBy=multi-user.target
Langfristigen Betrieb einrichten:
sudo systemctl daemon-reload sudo systemctl enable --now example-chat
Andere ErweiterungenDie vollständige Sicherheits-Checkliste für die externe Bereitstellung (Loopback-Port, Reverse-Proxy-Authentifizierung, Selbstprüfung der Angriffsfläche) befindet sich im Kapitel Sicherheit und Compliance; gehen Sie sie unbedingt durch, bevor Sie live gehen.