ChatGPT mit Rust zu nutzen, erfordert eine HTTP-Kommunikation kombiniert mit asynchroner Verarbeitung. Es gibt dafür spezielle Schnittstellenbibliotheken, alternativ stehen in Rust Standardpakete wie Reqwest und Tokio zur Verfügung. Damit erstellen Sie sogar einen Chatbot.
Vor über einem Jahr besuchte mich Tobias, ein guter Bekannter und freier Programmierer. Er zeigte sich total begeistert von ChatGPT: “Das Ding erstellt jetzt alle meine Shell-Skripte – einfach so!” Da ich bereits seit 20 Jahren im Bereich des maschinellen Lernens tätig bin, aber bis dato mit ChatGPT direkt nicht in Kontakt gekommen war, dachte ich mir: “Manchen kann man schon mit wenig eine Freude machen.” Tobias holte seinen Laptop heraus und tippte eine Anfrage nach der nächsten ein. Mit jeder Antwort von ChatGPT wuchs meine Überraschung – einen solchen Quantensprung in diesem Bereich der KI hatte ich nicht erwartet.
Seitdem habe ich mich intensiv mit der in ChatGPT und ähnlichen Programmen verwendeten Technologie beschäftigt: Large Language Models (LLM). In mir reifte der Plan, ChatGPT in eigene Rust-Programme zu integrieren. Damit war eine neue Herausforderung geboren: einen eigenen einfachen Chatbot mit Rust zu erstellen.
ChatGPT-API
Alle Dienste im Web, egal, ob es sich um einen HTML-Server, eine Cloud-SQL-Datenbank oder eben ChatGPT handelt, funktionieren nach demselben Schema: Sie senden eine Anfrage und erhalten ein Ergebnis zurück.
Die Hersteller bieten ihre Dienste nicht kostenlos an. Damit Sie mit einem Programm eine Anfrage schicken können, benötigen Sie einen Schlüssel (API-Key), über den der Anbieter abrechnet. Den API-Key erhalten Sie für ChatGPT unter https://platform.openai.com/apps. Jede Anfrage kostet abhängig vom Umfang und der verwendeten ChatGPT-Version einige Cents. Da ältere Versionen von ChatGPT günstiger sind, ergibt es Sinn, zunächst damit zu entwickeln und erst später auf aktuelle Versionen umzusteigen.
Um keine Verwirrung zu stiften, konzentriere ich mich hier auf OpenAI mit ChatGPT (Abbildung 1). Der Zugriff auf andere LLMs funktioniert ähnlich. Am Ende des Artikels zeige ich Ihnen, wie Sie mit der freien Software Ollama einen eigenen Server lokal aufsetzen, etwa für Llama 3 von Meta. Solche Aufrufe kosten gar nichts, doch der verwendete Computer muss die entsprechende Rechenleistung mitbringen.

Abbildung 1: Neben der gewohnten Weboberfläche bietet ChatGPT eine API an, mit der Programme direkt Anfragen stellen.
Für die Programmiersprachen Python und Javascript gibt es offizielle API-Umsetzungen für ChatGPT vom Hersteller. Auf der zentralen Webseite für Rust-Bibliotheken, Crates.io, finden Sie dafür unterschiedliche Implementierungen. Da es in diesem Artikel primär um die HTML-Kommunikation mit einem Server geht, nutze ich nicht diese inoffiziellen Rust-Bibliotheken, sondern steuere den Server direkt auf der untersten Ebene an [1].
HTTP-Kommunikation
Eine einfache Möglichkeit zur Datenübertragung per HTTP an einen Server bietet das Kommandozeilenprogramm Curl. In Listing 1 stellt es eine Frage an den ChatGPT-Server.
Als erster Parameter fungiert die Adresse des ChatGPT-Servers (der Endpoint) für Chat-Aufrufe. Die beiden nächsten Zeilen mit »-H« davor enthalten die Header-Informationen des Datenpakets. Der Content-Type »application/json« steht für den Datentyp des Paketinhalts im JSON-Format. Die zweite Zeile des Headers enthält den in der Shell-Variablen »OPENAPI_API_KEY« hinterlegten Zugriffsschlüssel.
Listing 1
Abfrage via Curl
$ curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{ "model": "gpt-3.5-turbo", \
"messages": [ \
{ \
"role": "system", \
"content": "You are a helpful assistant." \
}, \
{ \
"role": "user", \
"content": "What is the best programming language?" \
} \
]}'
Nach »-d« folgt das eigentliche Datenpaket. Der Slot »model« enthält die Version von ChatGPT, mit der Sie arbeiten möchten. Im Slot »messages« befindet sich die gesamte Chat-Kommunikation. Das »system« (Zeile 7), hier ChatGPT, hat zunächst die Nachricht »You are a helpful assistant« bekommen. Der »user« stellt in Zeile 11 die Gretchenfrage nach der besten Programmiersprache.
Nach dem Absenden verstreicht etwas Zeit, bis ChatGPT den Text aus Listing 2 an Curl zurückgibt, ebenfalls im JSON-Format. Im Mittelpunkt steht dabei der Slot »content« mit der eigentlichen Antwort. Den ersten Schritt zu einem eigenen Chatbot in Rust bildet ein Programm, das in etwa dasselbe tut wie der Curl-Aufruf: eine Anfrage an einen Server versenden und die Antwort verarbeiten.
Listing 2
ChatGPT-Ausgabe
{
"id": "chatcmpl-9GPcrEW4R6JwpMJQZoe4z8QZFexf5",
"object": "chat.completion",
"created": 1713698781,
"model": "gpt-3.5-turbo-0125",
...
"content": "The best programming language depends on the specific needs and requirements of a project.
Different programming languages have different strengths and are suited to different types of tasks.
Some popular programming languages include Python, Java, JavaScript, C++, and Ruby.
It's recommended to choose a programming language based on the particular project you are working on
and your own familiarity with the language."
...
}
Reqwest
Das meistverwendete Rust-Paket (Crate) für den Zugriff per HTML auf Server ist Reqwest. Listing 3 zeigt, wie Rust damit den Inhalt einer Webseite liest und sie in der Konsole ausgibt.
Die komplette Kommunikation erfolgt über das zentrale Reqwest-Objekt »Client« (Zeile 3). Mit der Methode »client.get(“URL“)« in der nächsten Zeile definiert das Programm den Server, auf den es zugreifen will. Die Methode »send()« stößt anschließend die Übertragung an. Da sie schiefgehen kann, kommen Sie nicht um eine passende Fehlerbehandlung herum. Die einfachste Möglichkeit dazu bietet die Funktion »unwrap()«: Sie bricht bei einem Fehler schlicht das Programm ab.
Geht alles gut, wandelt das Programm das zurückgelieferte Ergebnis in einen String um. Dieser Vorgang kann ebenfalls fehlschlagen, weswegen Sie erneut auf »unwrap()« zurückgreifen. Das Ergebnis landet schließlich in der Variablen »result«, die das Programm anschließend ausgibt (vorletzte Zeile).
Listing 3
Reqwest.rs – HMTL-Seite auslesen
use reqwest::blocking::Client;
fn main() {
let client = Client::new();
let result = client.get("https://www.rust-lang.org")
.send().unwrap()
.text().unwrap();
println!("{}",result);
}
Als kritisch erweist sich bei der Kommunikation mit Servern das Warten auf die Antwort. Hier stellt sich die Frage, was das Client-Programm währenddessen tun soll. In unserem Beispiel dreht es Däumchen – es ist blockiert (»blocking«, erste Zeile). Besäße es eine Oberfläche, käme sie zum Stehen. Die Alternative zu diesem als synchron bezeichneten Vorgehen wäre eine asynchrone Verarbeitung. Sie wartet nicht auf die Rückmeldung des Servers, sondern arbeitet bis zum Eintreffen der Antwort weiter und kümmert sich dann darum.
Asynchron mit Tokio
Für die asynchrone Verarbeitung in Rust gibt es die Direktive »async« und die Funktion »await()« (Listing 4). Dabei zeigt »async« an, dass Rust eine Funktion asynchron verarbeiten soll, und »await()« markiert die Stelle, an der das Programm auf etwas wartet.
Listing 4
Asynchrone Verarbeitung
async fn get_result() {
Client.get(...).await().unwrap()
}
Eine asynchrone Funktion ist in Rust schnell definiert – schwierig wird es, wenn Sie sie ausführen möchten. Rufen Sie sie auf wie jede andere Funktion, führt Rust sie nicht aus, sondern liefert stattdessen eine Datenstruktur zurück, die auf die asynchrone Funktion verweist.
Obwohl Rust standardmäßig »async« und »await()« anbietet, fehlt die Information, wie solche Funktionen auszuführen sind. Der Hintergrund: Da Rust auf den unterschiedlichsten Plattformen vom Mikrochip bis hin zum Multiprozessorrechner agieren kann, gibt es keine optimale Art, mit asynchroner Verarbeitung umzugehen. Die Details übernehmen Bibliotheken wie Tokio [2], Async-std [3] oder Smol [4]. Ich habe mich für die Library Tokio entschieden, da sie derzeit auf Desktop-Rechnern am häufigsten zum Einsatz kommt.
Beim Arbeiten mit Tokio müssen Sie im ersten Schritt eine Runtime erzeugen, die sich um das Ausführen der asynchronen Funktionen kümmert (Listing 5). Mit der Methode »block_on()« erhält Tokio die asynchrone Funktion und führt sie aus. Sie sparen sich das explizite Erzeugen der Laufzeitumgebung und den Aufruf der Methode »block_on()«, indem Sie die Funktion »main()« so definieren wie in den ersten beiden Zeilen von Listing 6. Zusätzlich zeigt das Listing, wie das Lesen einer HTML-Seite beim asynchronen Zugriff aussieht.
Listing 5
Tokio
let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(meine_async_fn());
Listing 6
Reqwest_async.rs
use reqwest::Client;
#[tokio::main]
async fn main() {
let client = Client::new();
let result = client.get("https://www.rust-lang.org")
.send().await.unwrap()
.text().await.unwrap();
println!("{}",result);
}
Der große Vorteil von Tokio zeigt sich, wenn ein Programm viele asynchrone Aufrufe möglichst gleichzeitig verarbeiten soll (Listing 7). Durch die Methode »spawn()« weiß »tokio«, dass es die asynchrone Funktion »get_html()« möglichst effizient ausführen soll. Während es beispielsweise auf den ersten Server wartet, führt es den Aufruf für den zweiten Server aus. Der Befehl »join« wartet, bis alle Tasks abgeschlossen sind.
Tokio bietet zahlreiche weitere Methoden, mit denen Sie Funktionen nahezu gleichzeitig ausführen können, ohne sich dabei um viel kümmern zu müssen.
Listing 7
Parallelverarbeitung
let _ = tokio::join!(
tokio::spawn(get_html("http://test.com")),
tokio::spawn(get_html("http://test1.com""))
);
Fehlerbehandlung
Das Programm »openai.rs« – Sie finden es im Download-Bereich zu diesem Artikel – erledigt dasselbe wie der Curl-Aufruf am Anfang des Artikels. Auf den ersten Blick sieht es etwas kompliziert aus, aber einige Hintergrundinformationen sorgen für mehr Verständnis.
Zu den großen Themen bei der Kommunikation via Web zählt die Fehlerbehandlung. In Rust stehen dazu grundsätzlich zwei Wege offen: Das Programm selbst kümmert sich entweder um den Fehler (Recoverable Errors) oder bricht einfach ab (»panic!«). Dass ein Programm auf den Fehler reagiert, ist der grundsätzliche Anspruch an professionelle Software, doch die Fehlerbehandlung macht alles etwas unübersichtlicher.
Übernimmt hingegen eine Funktion in einem Rust-Programm die Fehlerbehandlung, liefert sie ein Ergebnis vom Typ »Result« zurück (Listing 8). Geht alles gut, weist das Ergebnis den Wert »Ok(…)« auf, anderenfalls erhalten Sie »Err(…)« mit dem entsprechenden Fehler.
Listing 8
Result
enum Result<T, E> {
Ok(T),
Err(E),
}
Soll das Programm einfach abbrechen, genügt die Methode »unwrap()« (Listing 9, erste Zeile). Sie ruft im Fehlerfall den Befehl »panic!« auf (Zeile 4), was in etwa dem »match«-Statement entspricht. Will sich die aufrufende Funktion des Fehlers annehmen, kann sie im Zweig »Err« des »match«-Statements etwas Sinnvolles tun oder den Fehler als Rückgabewert weiterreichen (Zeilen 6 bis 9).
Listing 9
Abbrechen
function_with_error().unwrap;
match function_with_error() {
Ok(value) => value,
Err(e) => panic!("Error: {:?}", e)
}
match function_with_error() {
Ok(value) => value,
Err(e) => return Err(e)
}
function_with_error()?
Allerdings machen Sie es sich noch etwas einfacher, indem Sie den Operator »?« verwenden. Er erledigt genau dasselbe wie das »match«-Statement aus Listing 9. Die Funktion »start()« gibt jeden Fehler an die Funktion »main()« zurück (Listing 10).
Listing 10
Main
async fn start() -> Result<(), Box<dyn Error>> {
...
Ok(())
}
Da in Rust Fehler unterschiedliche Datentypen aufweisen können, wirkt der Rückgabewert der Funktion im ersten Moment etwas unübersichtlich. Dem Enum »Result« werden bei der Definition zwei Datentypen mitgegeben: Beim ersten handelt es sich um den, den das »Ok(…)« zurückliefert, beim zweiten um den Fehlerdatentyp von »Err(…)«.
Im »Ok«-Fall liefert die Funktion »start()« nichts zurück, was in Rust so aussieht: »()«. Bei einem Fehler kann es sich um jeden beliebigen Fehlertyp handeln. Deswegen steht dort kein konkreter Typ, sondern die Schnittstelle (Trait) »std::error::Error«. Sie implementiert jeden Fehlertyp.
Anfrage senden
Das Paket Reqwest verwendet bei der Anfrage an OpenAI die Methode »post()« (Listing 11, Zeilen 28 bis 32), da es etwas absendet, bevor es etwas zurück erhält. Im ersten Beispiel kam dagegen die Methode »get()« zum Einsatz, da Reqwest einfach einen Seiteninhalt abrief.
Die Methode »send()« schickt die Anfrage ab, und das Programm wartet asynchron mit »await« auf die Antwort. Der verwendete »request_string« ist als »r#”…”#;« definiert (Listing 11, Zeilen 18 bis 26). Es handelt sich dabei um einen Raw String, der sich über mehrere Zeilen erstrecken kann und automatisch die Zeilenumbrüche mit in den Inhalt aufnimmt. Das kommt der Übersichtlichkeit zugute. Sobald das Ergebnis des Aufrufs zurückkommt, liest das Programm die Antwort mit der Methode »text()« aus (Zeile 33).
Listing 11
Openai.rs
use std::error::Error;
use std::fs;
use reqwest::{Client, Url};
#[tokio::main]
async fn main() {
match start().await {
Ok(_) => println!("Bye!"),
Err(e) => println!("Error: {e}")
}
}
fn get_openai_key() -> String{
let key = fs::read_to_string("./secret.txt").unwrap();
key
}
async fn start() -> Result<(), Box<dyn Error>> {
let client = Client::new();
let url = Url::parse("https://api.openai.com/v1/chat/completions")?;
let request_string = r#"
{
"model": "gpt-3.5-turbo",
"messages": [{
"role": "user",
"content": "What is the best programming language?"
}]
}
"#;
let result = client
.post(url)
.header("Content-Type", "application/json")
.bearer_auth(get_openai_key())
.body(request_string)
.send().await?;
let body = result.text().await?;
println!("Body: {body}");
Ok(())
}
Lokales LLM mit Ollama
Möchten Sie die Beispielprogramme ohne Webservices lokal verwenden, bietet die Software Ollama [5] eine einfache Möglichkeit dazu. Sie installieren sie schlicht entsprechend der Anleitung des Projekts auf Ihrem Linux-System. Danach lädt das Kommando »ollama pull llama3« beispielsweise das LLM Llama 3 [6] von Meta auf den Rechner. Mit dem Befehl »ollama serve« starten Sie es. In den Beispielprogrammen müssen Sie lediglich die aufgerufene URL in http://localhost:11434/v1/chat/completions ändern.
Ausblick
Den kompletten Chatbot »openai_console.rs« finden Sie im Download-Bereich zu diesem Artikel. Er verwendet einige Konsolenkomponenten, die die Bedienung vereinfachen. Wenn Sie mehr über die asynchrone Verarbeitung in Rust wissen möchten, empfehle ich Ihnen als Einstieg ein Video von Herbert TheBracket Wolverson auf Youtube [7]. (csi)
Infos
- ChatGPT-API für Rust: https://platform.openai.com/docs/api-reference/introduction
- Tokio: https://tokio.rs
- Async-std: https://async.rs
- Smol: https://github.com/smol-rs/smol
- Lokale KI-Chatbots: Erik Bärwaldt, “Frei und unabhängig”, LM 09/2024, S. 44, https://www.lm-online.de/50080
- Llama 3: https://ollama.com/library/llama3
- “1 Hour Dive into Asynchronous Rust”: https://www.youtube.com/watch?v=0HwrZp9CBD4






