こんにちは。
最近ChatGPTが面白すぎて、自宅で一人でいる時は大体YouTubeを見るかAIとわけわかんないこと話しております。
GPTに小説書かせると面白いんですよね。
それはそうと、本記事ではそんなChatGPTの後ろで文章生成を担うAI本体であるGPTを、Rust言語から呼ぶ方法について記事にしてみたいと思います。
Rustって?
Rustというのは汎用のプログラミング言語です。ビデオゲームのことではありません
詳しい説明は省きますがC言語などに匹敵する速度を持ち、比較的安全なプログラムを容易に書くことのできる言語です。
具体的には普通のコードを書く上で、C++11
以降におけるスマート・ポインタ(std::shared_ptrなど)を使用せずともポインタ演算のミスとか、あとは開放済みのメモリの使用などを気にしなくて済みます。
これはCやC++に触れたことがあれば、どれだけありがたいかがわかるはずです。
それを考えるのが楽しいという変態高度技術者は…代わりにC++で作ってみてください。
インストールや文法の具体的な説明は省きます。
Rust Programming Language(公式Webサイト)とかに始め方が乗っていますし、
プログラマーの皆さまがお世話になってきたであろうとほほ様も入門を記載されております。
とほほのRust入門 - とほほのWWW入門
あと正直、平易にGPTを呼びたいならPythonとか使えばいいんだと思います。でも自分はPython詳しゅうないし、おれはRustで書きたいんじゃ。以上。
今回作るもの
CLIでsend {任意のメッセージ}
と入力すると、GPTから文字列が返ってくる仕組みとします。
それならChatGPTを使ったほうが便利だって?
そういうことは言わないの。APIだからこそできることとかもありますので…
あとはAxum
やActix-Web
なんかを活用してWeb UIなどまで作ればもっと便利にできます。
しかしながら、WebSocketとかをハンドリングするJavaScriptを書くのが面倒くさいのでコーディングを簡単にするために今回はOpen AIのAPIを経由してRustから呼び出してCLIでGPTとお話するだけのプログラムを作ってみようと思います。
手順としては以下のような感じになります。
- OpenAIからGPTをアプリケーション経由で呼ぶためのAPIキーを発行する
- OpenAIのアカウントがない場合はそのままではAPIを利用できない為、まずはアカウント登録をする
- Rustでユーザと対話できる簡単なプログラムを作成する
- APIを呼び出すコードと、対話するプログラムをつなげる
① OpenAI API KEYの発行
https://platform.openai.comへアクセスし、右上のログインボタンからあなたのユーザでLog inして、続けて右上のボタンを押してOrganizationをなんかいい感じの名前で作成。
続けて、APIの使用には課金が必要なので左側のメニューのORGANIZATION
配下のBilling
を開いて、お財布と相談して支払情報を設定。
最後に左側メニューのORGANIZATION
配下にあるAPI Keys
を選択し、そこから画面右上のCreate new secret key
を押下。
適当な名前を付け、Create secret key
ボタンを押してキーを発行してください。
キーっぽいものが発行できてればたぶんOKです。
② Rustでユーザと対話できる簡単なプログラムを作成する
まずは適当なディレクトリでRustのプロジェクトを作成してください
cd {適当なディレクトリ}
cargo new rust-gpt
そこからこんな感じでコードを作ってみてください
main.rs
/// API呼び出し処理のためのモジュールの定義
mod call_api;
/// OpenAI APIの呼び出しにreqwestを使う予定のため、非同期でエントリーポイントを定義する
#[tokio::main]
async fn main() {
// OpenAI APIキーをファイルから読み込む
let openai_api_key =
tokio::fs::read_to_string("OPENAI_API_KEY.key")
.await
.expect("OPENAI_API_KEY.keyの読み取りに失敗しました")
.trim()
.to_string();
let mut input = String::new();
loop {
// バッファをクリアして、標準入力を受け取る
input.clear();
std::io::stdin()
.read_line(&mut input)
.expect("標準入力の読み取りに失敗しました");
// 入力をトリムして小文字に変換
let input = input.trim().to_ascii_lowercase();
// 入力をスペースで分割し、各要素をトリムする
let mut input = input.split(' ').map(|s| s.trim());
// 最初の要素を取得
match input.next() {
Some("exit") => break,
Some("send") => {
// 残りの要素を結合してメッセージを作成
let message = input.collect::<Vec<&str>>().join(" ");
// API呼び出しの結果を表示
println!(
"{}",
call_api::call_openai_api(
message.as_str(),
openai_api_key.as_str()
)
.await
);
}
_ => {}
}
}
}
call_api.rs
pub async fn call_openai_api(
message: &str,
api_key: &str,
) -> String {
// ここでOpenAI APIを呼び出す処理を実装する予定
// 現在はダミーのレスポンスを返す
format!("OpenAI APIからのレスポンス(予定): {}", message)
}
Cargo.toml
[package]
name = "rust-gpt"
version = "0.1.0"
edition = "2024"
[dependencies.serde]
version = "1"
features = ["derive"]
[dependencies.tokio]
version = "1"
features = ["full"]
[dependencies.reqwest]
version = "0.12"
features = ["json", "rustls-tls"]
[dependencies]
serde_json = "1"
serde
・tokio
・reqwest
の3つはそれぞれ追々使用する予定なのでとりあえずdependenciesに追加しておきます。
OPENAI_API_KEY.key
{①で発行したAPIキー}
②時点では使用しませんが、③で使う予定であるため作成
動作例
cargo run
...
send こんにちは
OpenAI APIからのレスポンス(予定): こんにちは
exit
こんな感じで動作します。
③ APIを呼び出すコードと、対話するプログラムをつなげる
OpenAI APIの規則に倣って、call_api.rs
からreqwest
とserde
を使用して、エンドポイントにリクエストを送り、レスポンスを取得・デシリアライズして、
結果となる文字列を表示するコードを作成します。
call_api.rs(変更)
use serde::{Deserialize, Serialize};
/// エンドポイントへのリクエストを抽象化するための構造体と列挙型を定義
#[derive(Serialize)]
struct ChatRequest<'a> {
model: &'a str,
messages: Vec<Message<'a>>,
}
/// エンドポイントへのメッセージの役割を定義する列挙型
enum MessageRole {
User,
Assistant,
}
impl Serialize for MessageRole {
/// serde::Serializeをオーバーライドし、
/// MessageRoleをシリアライズして、APIで定義されている文字列に変換する処理の実装
fn serialize<S>(
&self,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let role_str = match self {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
};
serializer.serialize_str(role_str)
}
}
/// エンドポイントへのメッセージを定義する構造体
#[derive(Serialize)]
struct Message<'a> {
role: MessageRole,
content: &'a str,
}
/// エンドポイントからのレスポンスをパースするための構造体
#[derive(Deserialize)]
struct ChatResponse {
choices: Vec<Choice>,
}
/// エンドポイントからのレスポンスにおけるメッセージの選択肢を定義する構造体
#[derive(Deserialize)]
struct Choice {
message: ResponseMessage,
}
/// エンドポイントからのレスポンスメッセージを定義する構造体
#[derive(Deserialize)]
struct ResponseMessage {
content: String,
}
pub async fn call_openai_api(
message: &str,
api_key: &str,
) -> String {
// reqwestクライアントを作成
let client = reqwest::Client::new();
// OpenAI APIのエンドポイント
let url = "https://api.openai.com/v1/chat/completions";
// リクエストボディを作成
let req_body = ChatRequest {
model: "gpt-4.1-mini",
messages: vec![Message {
role: MessageRole::User,
content: message,
}],
};
// API呼び出しを非同期で実行
let resp = client
.post(url)
.bearer_auth(api_key)
.json(&req_body)
.send()
.await
.expect("API呼び出しに失敗しました");
// レスポンスの内容をパース
let resp = resp
.json::<ChatResponse>()
.await
.expect("レスポンスのパースに失敗しました")
.choices
.into_iter()
.next()
.unwrap();
// GPTからのレスポンスを返す
format!(
"OpenAI APIからのレスポンス: {}",
resp.message.content
)
}
動作結果
では一応本記事の目的のコードを作れたので、実行してみましょう。
cargo run
...
send こんにちは!
OpenAI APIからのレスポンス: こんにちは!どうぞよろしくお願いします。今日はどんなことをお手伝いしましょうか?
exit
こんな感じで動作します。
まとめ
これRustじゃなくてよくない?
まぁ私がRustの文法が好きなので、Rustで記事を書きました。
しかしながら今回作成したプログラムの仕様では、前回のユーザとAIのやり取りを記憶していないため、文脈を保持して会話することが出来ません。
send こんにちは!
OpenAI APIからのレスポンス: こんにちは!どうぞよろしくお願いします。今日はどんなことをお手伝いしましょうか?
send ウホウホ!ウホ!ホホーウホ!
OpenAI APIからのレスポンス: ウホウホ!ご機嫌ですね!何か楽しいことがあったんですか?それともゴリラさん気分でしょうか?😊どうぞお話しください!
send ウホ!ウホ!おれゴリはバナナがすきウホ!ウホ!
OpenAI APIからのレスポンス: ウホウホ!ゴリラさん、バナナが好きなんだねウホ!バナナはエネルギーたっぷりでおいしいよねウホ!何本でも食べられちゃうウホ!
send GPTはなにがすき?
OpenAI APIからのレスポンス: 私はAIなので、感情や好みは持っていません。でも、いろいろな質問に答えたり、お話をしたりすることが得意で、それを「好き」と言えるなら、みなさんとコミュニケーションをとることが一番好きです!あなた は何が好きですか?
send おれはカレーが好きウホ!
OpenAI APIからのレスポンス: おお、カレー好きなんですね!ウホウホ!どんなカレーが好きですか?辛口?それとも甘口?野菜たっぷり派ですか?教えてウホ!
exit
そのため、こんな感じであんまり脈絡のない、楽しくないコミュニケーションになってしまいます。
ちなみにChatGPTに同じことを話しかけると、文章として成立します。
これはGPTなどのLLMは基本的に前回のやりとりをAI側では記憶しておらず(ステートレス)、ユーザから過去履歴や、今回は出てきていませんがmessage
のrole
にsystem
を指定したメッセージを付与した指示を配列にまとめてAPIで送って、全部一旦読み込んでから処理をして、結果を返してもらう仕組みとなっているからです。
しかしながら、現在のプログラムではユーザとAIの間でのやり取りがどこにも保存されていないため、このようにコミュニケーションの連続性が確保できないという結果に繋がっています。
まぁ詳しい仕様はこの辺を確認していただければと思います。
なので、もし次回があれば履歴とかそういうのも実装してみようと思います。
おわり