💬

AI といっしょに Rust でリアルタイムチャット作ってみた

に公開

こんにちは。今回は「食わず嫌い」していた AI コードエディタ( 今回は Cursor )を使い、Rust で WebSocket サーバー(チャットシステム)を作った開発体験をゆるっと共有しようと思います。AI に補完してもらうだけでなく、ある程度“ラリー”を楽しみたかったので、ちょっと腰を据えて取り組みました。

きっかけ

  • 食わず嫌いしていた AI コードエディタに手を出したかった
    もともと AI コードエディタには興味があったのですが、「どう使うかよくわからない」「結局自分でやる方が確実だろう...」という思いから触ってきませんでした。ですが、周りの評判を聞いて興味が再燃。ついにやってみようと決意しました。

  • Rust を使って何かしたかった
    プロダクトとして Rust を使う機会はないですが、WebAssembly の文脈でプライベートに触った経験はあって AI コードエディタでなにかやるの題材としました。

目的としたこと

  • AI に補完してもらって「はい、おしまい」ではなく、ある程度ラリーしたかった
    AI にコードを書いてもらうだけなら、コピペで済んでしまいます。ただ、今回の目的はあくまで「やり取りを通じて学ぶ」こと。実装の意図や根拠を確認しながら進めたいし、コードの修正依頼やテストもしてみたい。後、AI コードエディタって便利なんだなを体感したかったのもあります。

  • なので、自作の場合はそこそこ時間がかかる題材を選びたかった
    小さなコード片を生成してもらうだけだと物足りないかなと思い、ある程度の規模がある “チャットシステム” を作ることに決めました。

やったこと

  • 「Rust x WebSocket サーバー/リアルタイムメッセージングシステム」(要はチャット)を作ってもらった
    tokio や tokio-tungstenite を組み合わせ、並行処理&非同期処理をバリバリ活用したチャットサーバーを Rust で作成。データの受け渡しやエラー処理・ロギングなどを織り交ぜました。

技術スタック

進め方

  1. Cursor Ask mode で gpt-4o と題材について壁打ち
    まずは雑談しながら、Rust や Chat システムの要件・仕様を整理しました。

  2. 続いて成果物として Project Rules を出力してもらう
    「開発方針」や「命名規則」「ディレクトリ構成」などルールをプロジェクトの頭に明文化。

  3. Project Rules をもとに、タスクを出力してもらう
    実際にどういうステップを踏むか、タスクリストを整理してゴールを明確化。

  4. Agent mode でタスクを元に実装を進んでもらう
    AI が生成してくれるコードをベースにしつつ、必要に応じて私が手を加えたり調整したり。

  5. タスク毎に動作確認したりテスト書いたり
    実際に動くかどうか確かめながら、小さなステップで積み上げ。

  6. 細かい部分で機能追加してみたり
    「ここはこうしたら便利かも」と思いついたところを追加でオーダー。

  7. リファクタリング指示と合わせて Project Rules も足してみたり
    バラバラ感が出てきたらリファクタリングを依頼し、Project Rules に書き足す形で管理。

補足 1. Cursor の Agent mode と Ask mode について

Ask mode は ChatGPT みたいな形でチャットしたら答えが返ってくるやつです。
「例えば TDD したいんだけど」みたいな問い合わせができたり、

Cursor 用のプロンプト設定ファイルの Project Rules を出力してくれたりします。
ブラウザ ←→ エディタといったりきたりせず最低限のものはエディタ内で完結しちゃうのでとてもいいです。

補足 2. Cursor の Project Rules について

Cursor には User Rules と Project Rules から事前にプロンプトを仕込む仕組みがあります。( ref. https://docs.cursor.com/context/rules-for-ai

User Rules は Cursor 全体の処理で活用して、Project Rules は開いてるプロジェクトのみで有効案 Rule です。

↑↑の画像でデスデス言ってるのもこの Rules ですね。
なんか敬語だと寂しいなと思って設定しましたがうるさいです。

この Project Rules に「今後の設計方針」や「タスク管理について」「モジュール設計」「ディレクトリ構成」「技術スタック」などなど記載しておくと返答やエージェントの解像度がグッと上がってくれます。

これが結構奥が深そうで、Project Rules 作る専用の人もいるみたいです。

うまくいったこと

  • タスク管理も含めて一連の開発体験がよかった
    仕様策定やタスクの洗い出しも AI に手伝ってもらえると、すごく効率良く進みました。

  • 慣れない技術も下地を作ってくれるところは楽
    特に Rust の細かいところを補足してくれるのは助かりました。自分で全部調べると途方もなく時間がかかる部分も、サッと組み上げてもらえてスムーズ。

  • AI とある程度ラリーしたいという目的は果たせた
    「このコードの意図は?」「ここをもう少しスマートに書けない?」など、疑問点をぶつけると丁寧に答えてくれて、“相談相手” として機能してくれました。

うまくいかなかったこと

  • (本件とは直接関係ないが)この取り組みの前に選択した題材「ゲームエンジン」はあまりに難しかった
    Rust にはまだ安定したゲームエンジンライブラリが少なく、破壊的変更も多い。ベクトル計算や細かい描画周りを全部 AI と相談しながらやろうとすると、なかなか大変でした。今後のモデルのアップデートでカバーされる部分もありそうなので、今後に期待しつつ今は別の題材を選ぶかなと思いました。

  • 稀によく Project Rules に書いてあることも無視される
    同じ Project Rules が複数バージョンあるとき、AI 側から見て矛盾を含んでいると判断されると「あえて無視」されることもありました。要調整。

  • 一度のコード変更量が多い
    細かい箇所を何箇所もまとめて修正してくれるので、どこがどう変わったのか追いにくいことがありました。タスクや指示の粒度はもう少し細かく刻んだほうが良さそう。

  • プロダクトレベルに落とし込むのは時間がかかる
    「そもそも自分で明確に何を作りたいか把握していないと、AI もうまく動けない」と痛感。やはりゴールのイメージは大切ですね。

作れたものを軽く紹介

Web クライアント

簡易のID/Passwordログインした後にグローバル or 作成したルーム向けにメッセージを送れます。
見た目とか簡単な機能とかも軽くやりとりして作ったりしてます。

WebSocket サーバー

状態が分かりやすいよう一旦過剰にログを出力してます。
この辺も「こういったログ足して〜」で足してくれたりするのが便利でした。

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/ember`
2025-04-09T01:14:31.629483Z  INFO ember::ws::server: WebSocketサーバーがアドレス 127.0.0.1:8080で起動しました
2025-04-09T01:14:47.973660Z  INFO ember::ws::server: 新しいクライアント接続: 127.0.0.1:63706
2025-04-09T01:14:47.975031Z  INFO ember::handlers::ws_handler: WebSocket connection established: 127.0.0.1:63706
2025-04-09T01:14:47.975139Z  INFO ember::models::app_state: クライアントを追加しました: 127.0.0.1:63706 (user_id: user_63706)
2025-04-09T01:14:47.975170Z  INFO ember::models::app_state: 現在の接続数: 1
2025-04-09T01:14:47.982738Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Room","payload":{"command":"List","room_id":"global","sender_id":"client"}}
2025-04-09T01:14:47.982955Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からルームメッセージを受信: command=List, room_id=global, sender=client
2025-04-09T01:14:47.983010Z  INFO ember::handlers::message_handler: ルーム一覧取得: 
2025-04-09T01:15:03.406541Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Auth","payload":{"command":"Login","username":"hoge","password":"password","token":null}}
2025-04-09T01:15:03.406665Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706から認証メッセージを受信: command=Login, username=hoge
2025-04-09T01:15:03.406774Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706 の認証に成功: ユーザー=hoge
2025-04-09T01:15:03.408504Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Room","payload":{"command":"List","room_id":"global","sender_id":"hoge"}}
2025-04-09T01:15:03.408535Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からルームメッセージを受信: command=List, room_id=global, sender=hoge
2025-04-09T01:15:03.408554Z  INFO ember::handlers::message_handler: ルーム一覧取得: 
2025-04-09T01:15:14.521383Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Chat","payload":{"sender_id":"hoge","content":"こんちは","timestamp":"","room_id":null}}
2025-04-09T01:15:14.521692Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からチャットメッセージを受信: sender=hoge, content=こんちは, room=global
2025-04-09T01:15:14.521767Z  INFO ember::handlers::ws_handler: グローバルチャットメッセージを処理: 送信者=hoge
2025-04-09T01:15:14.521785Z  INFO ember::models::app_state: ブロードキャストするメッセージ内容: {"type":"Chat","payload":{"sender_id":"hoge","content":"こんちは","timestamp":"","room_id":null}}
2025-04-09T01:15:14.521814Z  INFO ember::models::app_state: メッセージをブロードキャストしました: 成功=0, 失敗=0, 送信元=127.0.0.1:63706
2025-04-09T01:15:27.509346Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Room","payload":{"command":"Create","room_id":"適当ルーム","sender_id":"hoge"}}
2025-04-09T01:15:27.509461Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からルームメッセージを受信: command=Create, room_id=適当ルーム, sender=hoge
2025-04-09T01:15:27.509473Z  INFO ember::handlers::message_handler: process_message: ルーム作成リクエスト '適当ルーム' を受信(実際の作成は別処理)
2025-04-09T01:15:27.509505Z  INFO ember::handlers::ws_handler: ルーム 適当ルーム を作成しました
2025-04-09T01:15:27.509515Z  INFO ember::models::app_state: [ルーム作成後] 現在のルーム一覧: 適当ルーム
2025-04-09T01:15:27.509521Z  INFO ember::models::app_state: ブロードキャストするメッセージ内容: {"type":"System","payload":{"content":"ルーム '適当ルーム' が作成されました","importance":"Info","room_id":null}}
2025-04-09T01:15:27.509537Z  INFO ember::models::app_state: メッセージをブロードキャストしました: 成功=1, 失敗=0, 送信元=127.0.0.1:63706
2025-04-09T01:15:28.002916Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Room","payload":{"command":"Join","room_id":"適当ルーム","sender_id":"hoge"}}
2025-04-09T01:15:28.003116Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からルームメッセージを受信: command=Join, room_id=適当ルーム, sender=hoge
2025-04-09T01:15:28.003266Z  INFO ember::models::app_state: クライアント 127.0.0.1:63706 (user_id: hoge) がルーム 適当ルーム に参加しました
2025-04-09T01:15:28.003301Z  INFO ember::models::app_state: クライアント 127.0.0.1:63706 の参加ルーム一覧: 適当ルーム
2025-04-09T01:15:28.003322Z  INFO ember::models::app_state: ルーム 適当ルーム 宛メッセージ内容: {"type":"System","payload":{"content":"ユーザー 'hoge' がルーム '適当ルーム' に参加しました","importance":"Info","room_id":"適当ルーム"}}
2025-04-09T01:15:28.003390Z  INFO ember::models::app_state: ルーム 適当ルーム 内にメッセージを送信しました: 成功=1, 失敗=0, 送信元=127.0.0.1:63706
2025-04-09T01:15:28.412753Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Room","payload":{"command":"List","room_id":"global","sender_id":"hoge"}}
2025-04-09T01:15:28.412853Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からルームメッセージを受信: command=List, room_id=global, sender=hoge
2025-04-09T01:15:28.412874Z  INFO ember::handlers::message_handler: ルーム一覧取得: 適当ルーム
2025-04-09T01:15:39.582484Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63706からテキストメッセージを受信: {"type":"Chat","payload":{"sender_id":"hoge","content":"適当ルームより","timestamp":"","room_id":"適当ルーム"}}
2025-04-09T01:15:39.582596Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63706からチャットメッセージを受信: sender=hoge, content=適当ルームより, room=適当ルーム
2025-04-09T01:15:39.582646Z  INFO ember::handlers::message_handler: チャットメッセージのルーム情報 - ルーム 適当ルーム は存在します, 送信者は所属しています
2025-04-09T01:15:39.582714Z  INFO ember::handlers::ws_handler: 送信元クライアント 127.0.0.1:63706 のルーム 適当ルーム 所属状況: 所属中
2025-04-09T01:15:39.582735Z  INFO ember::models::app_state: ルーム 適当ルーム 宛メッセージ内容: {"type":"Chat","payload":{"sender_id":"hoge","content":"適当ルームより","timestamp":"","room_id":"適当ルーム"}}
2025-04-09T01:15:39.585072Z  INFO ember::models::app_state: ルーム 適当ルーム 内にメッセージを送信しました: 成功=0, 失敗=0, 送信元=127.0.0.1:63706
2025-04-09T01:15:44.129503Z  INFO ember::ws::server: 新しいクライアント接続: 127.0.0.1:63723
2025-04-09T01:15:44.130502Z  INFO ember::handlers::ws_handler: WebSocket connection established: 127.0.0.1:63723
2025-04-09T01:15:44.130605Z  INFO ember::models::app_state: クライアントを追加しました: 127.0.0.1:63723 (user_id: user_63723)
2025-04-09T01:15:44.130652Z  INFO ember::models::app_state: 現在の接続数: 2
2025-04-09T01:15:44.139390Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723からテキストメッセージを受信: {"type":"Room","payload":{"command":"List","room_id":"global","sender_id":"client"}}
2025-04-09T01:15:44.139455Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63723からルームメッセージを受信: command=List, room_id=global, sender=client
2025-04-09T01:15:44.139468Z  INFO ember::handlers::message_handler: ルーム一覧取得: 適当ルーム
2025-04-09T01:15:56.118516Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723からテキストメッセージを受信: {"type":"Auth","payload":{"command":"Login","username":"foo","password":"password","token":null}}
2025-04-09T01:15:56.118650Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63723から認証メッセージを受信: command=Login, username=foo
2025-04-09T01:15:56.118776Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723 の認証に成功: ユーザー=foo
2025-04-09T01:15:56.122615Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723からテキストメッセージを受信: {"type":"Room","payload":{"command":"List","room_id":"global","sender_id":"foo"}}
2025-04-09T01:15:56.122691Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63723からルームメッセージを受信: command=List, room_id=global, sender=foo
2025-04-09T01:15:56.122766Z  INFO ember::handlers::message_handler: ルーム一覧取得: 適当ルーム
2025-04-09T01:16:03.766490Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723からテキストメッセージを受信: {"type":"Chat","payload":{"sender_id":"foo","content":"見えてる?","timestamp":"","room_id":null}}
2025-04-09T01:16:03.766587Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63723からチャットメッセージを受信: sender=foo, content=見えてる?, room=global
2025-04-09T01:16:03.766653Z  INFO ember::handlers::ws_handler: グローバルチャットメッセージを処理: 送信者=foo
2025-04-09T01:16:03.766671Z  INFO ember::models::app_state: ブロードキャストするメッセージ内容: {"type":"Chat","payload":{"sender_id":"foo","content":"見えてる?","timestamp":"","room_id":null}}
2025-04-09T01:16:03.768197Z  INFO ember::models::app_state: メッセージをブロードキャストしました: 成功=1, 失敗=0, 送信元=127.0.0.1:63723
2025-04-09T01:16:06.190496Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723からテキストメッセージを受信: {"type":"Room","payload":{"command":"Join","room_id":"適当ルーム","sender_id":"foo"}}
2025-04-09T01:16:06.190727Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63723からルームメッセージを受信: command=Join, room_id=適当ルーム, sender=foo
2025-04-09T01:16:06.190873Z  INFO ember::models::app_state: クライアント 127.0.0.1:63723 (user_id: foo) がルーム 適当ルーム に参加しました
2025-04-09T01:16:06.190908Z  INFO ember::models::app_state: クライアント 127.0.0.1:63723 の参加ルーム一覧: 適当ルーム
2025-04-09T01:16:06.190928Z  INFO ember::models::app_state: ルーム 適当ルーム 宛メッセージ内容: {"type":"System","payload":{"content":"ユーザー 'foo' がルーム '適当ルーム' に参加しました","importance":"Info","room_id":"適当ルーム"}}
2025-04-09T01:16:06.191030Z  INFO ember::models::app_state: ルーム 適当ルーム 内にメッセージを送信しました: 成功=2, 失敗=0, 送信元=127.0.0.1:63723
2025-04-09T01:16:13.470318Z  INFO ember::handlers::ws_handler: クライアント 127.0.0.1:63723からテキストメッセージを受信: {"type":"Chat","payload":{"sender_id":"foo","content":"ルームに送るよ","timestamp":"","room_id":"適当ルーム"}}
2025-04-09T01:16:13.470366Z  INFO ember::handlers::message_handler: クライアント 127.0.0.1:63723からチャットメッセージを受信: sender=foo, content=ルームに送るよ, room=適当ルーム
2025-04-09T01:16:13.470429Z  INFO ember::handlers::message_handler: チャットメッセージのルーム情報 - ルーム 適当ルーム は存在します, 送信者は所属しています
2025-04-09T01:16:13.470461Z  INFO ember::handlers::ws_handler: 送信元クライアント 127.0.0.1:63723 のルーム 適当ルーム 所属状況: 所属中
2025-04-09T01:16:13.470468Z  INFO ember::models::app_state: ルーム 適当ルーム 宛メッセージ内容: {"type":"Chat","payload":{"sender_id":"foo","content":"ルームに送るよ","timestamp":"","room_id":"適当ルーム"}}
2025-04-09T01:16:13.470485Z  INFO ember::models::app_state: ルーム 適当ルーム 内にメッセージを送信しました: 成功=1, 失敗=0, 送信元=127.0.0.1:63723

今回のコード量自体は 7,000 程度で収まってるので結構小規模に収まってます。
とはいえ自分でやると結構時間かかったろうなぁ ... という取り組みではあったので AI コードエディタの力を体験するには十分でした。

❯ git log --numstat --pretty="%H" --no-merges | awk 'NF==3 {plus+=$1; minus+=$2} END {printf("%d (+%d, -%d)\n", plus+minus, plus, minus)}'  

7476 (+6252, -1224)

結論

  • AI コードエディタを通した開発は楽しい
    あたかもパーツを作ってもらい、自分で組み合わせていくプラモデルのような感覚があります。ちょっとしたアイデアをサッと形にしてもらえるのは気持ち良い。思ったり簡単にできちゃうのでびびる。

  • AI コードエディタでコーディングするにしてもゴールの解像度は上げておく必要がある
    漠然とした指示だと「一応動くもの」は出てくるけど、何をどう実現したいのかが曖昧なまま進んでしまいがち。最終的に「まぁ、動いたかな……」で終わっちゃうので、しっかり考えた上でリクエストすると良さそう。

  • 慣れない技術に触れるハードルが大幅に下がる
    自学自習が遊び感覚で捗るのはめちゃくちゃ嬉しいポイント。これまでは挫折しがちだった新しい技術にも、気軽に手を伸ばしやすくなったと感じました。

以上、AI コードエディタを導入して、Rust × WebSocket サーバーでリアルタイムチャットを作ってみた開発体験についてざっくりまとめました。最初は食わず嫌いしていましたが、いざやってみると想像以上に“共創”感があって面白いです。もし「コード補完以上のやりとり」をしてみたい方がいれば、ぜひ試してみてはいかがでしょうか?僕自身ももっと大きな題材探してさらに活かせるシーンを探していきたいと思います。

Discussion