🤑

なんとかハッカソンで賞をもぎ取った話

に公開

こんにちは

先日,「PR TIMES HACKATHON 2025 WINTER」ぶりにハッカソンに参加したので記事にしました.今回はサポーターズさん主催の「技育CAMPハッカソン Vol.3」に参加し,結果は努力賞ではありますが賞を取ることができました.ぜひ読み進めていただけると嬉しいです.

技育CAMPハッカソンとは

サポーターズさんが主催する,毎年何十回も実施されているハッカソンです.完走率が高く,事前開発の1週間(任意) + 本番の2日間で開発を進める大会となります.僕が参加した時は80人くらい(20チームほど?)の規模でした.特にお題などはなく,どんな形態で作っても良いというかなり自由度の高いハッカソンでした.初ハッカソンの人が多く,敷居も低いのでぜひ参加してみてください〜

事前開発期間(5/16~5/23)

作るプロダクト決め

実は,開発期間前からある程度作りたいもの内容がメンバーそれぞれあったのでそれらをブラッシュアップするところから開始しました.
おっと,今回のメンバーを紹介します.

  • 僕:M1,27卒.最近の悩みは9月のインターン参加がなかなか決まらないこと.
  • きむ:京都に佇む強強エンジニア(M2).バックエンドとフロントエンドどっちもレベル高く実装できてしまう人.
  • しず:文系エンジニア(B3),フロントエンドのデザインセンスが最近ぶち上がってる&成長スピードが凄まじい人.

そう,みて貰えばわかるんですけどあのPR TIMESハッカソンのチームメンバーで参加しました.というのも,このハッカソン,前回うまくいかなかったリベンジの意味もあって参加したんです.
僕らは,プロダクトを「なぜ作るか?」「どう作るか?」にかなり力を入れるチームでして,以下の流れで行いました.

  1. テーマ(今回ならAIとプログラミング)から何を作るか?
  2. 話し合って一番わくわくするもの&使ってもらえそうか?ライバルはいないか
  3. どう実現するか?(WebApp?拡張機能?機械?)
  4. 技術選定&仕様決め
  5. 開発

開発までの工程が多い分,コミュニケーションにリソースを割くのですが開発の二度手間やトラブルを極限まで下げることができたので結果的に良い手法でした.1から3までをメンバーでアイデアを持ち寄って発表したり,他プロダクトの事例を見ながらブラッシュアップしていきました.
その結果,

  • 作るもの:技術面接対策ツール with AI(RepoInterviewer)
  • 実現手段:VSCode Extension(拡張機能)
    となりました.
    WebAppでの実装方法もありましたが,ユーザーが使う時の手間であったり,コードを振り返りながら使用することができるといった点で拡張機能での実装となりました.

技術選定&設計

作るものが決まったので

  • フロントエンド(拡張機能でユーザーに表示する部分):しず
  • バックエンド(送信するデータ,AIとの連携):僕
  • どっちも:きむ

といった分担で行いました.
今回開発するツールは

  • 会話形式
  • 採点&FBがもらえる
  • キャラクター&難易度が選べる

これらがマストであったため,しずがキャラクターのアイコンやデザインを製作する中,僕ときむはシーケンス図の作成,それに必要なAPIの仕様,技術選定を行いました.

技術選定

項目 技術
バックエンド FastAPI(Python),Ollama
フロントエンド VSCode Extension(WebView + TS), React, TailwindCSS,MaterialUI
データ管理 Redis
モデル連携 Ollama(Elyza),Gemini flash-2.0
ツール GitHub, GitHubActions, Redoc, Docker

結果として,こんな構成になりました.
フロントエンドは,しずがReactとTailwindの扱いに慣れていた部分や勉強していたということもあり選定しました.バックエンドはかなり苦戦し,GoとPythonで迷っておりましたが,以下の理由でPythonになりました.

  • 同時に面接セッションが開始した際の処理はGoの方が容易かつ得意,しかし当初実装したOllamaのライブラリに脆弱性が見つかる.
  • Ollamaライブラリがある,ナレッジが多い
  • 開発スピードが速い

正直言語の仕様とアーキテクチャ設計のしやすさ,エラー管理はGoの方がやりやすいと感じましたがこれらの理由で,Pythonになりました.
また,FastAPIを選んだ理由としては

  • 非同期・並行処理が可能
  • Pydanticで型安全が確保できる(APIとしてフロントにデータを返す型を保証)
  • APIサーバーとしての実装に最適

これらの理由となります.
また,コーディング面接の履歴や内容をLLMに適宜渡すため,それらの情報を保存しなくてはなりません.しかし,DBに保存するほど永続的な内容でないため,インメモリデータベースのRedisをキャッシュデータ管理ツールとして採用しました.

設計

設計として

  • シーケンス図の作成
  • 必要なAPIの設計書作成
  • 拡張性を意識したフォルダ構成

を行いました.
まず,シーケンス図ではユーザーが使用するフローを予測しながら図を書いて,その間でサーバーとどのようなデータをやりとりするか,どう動くかを考えて実装しました.
例えば,今回作るツールの大まかな流れとしては

[ユーザーがVSCodeで特定のフォルダを開き,プラグイン起動]
        ↓
[LLMが質問5問(可変対応)を一気に送信]
        ↓
[ユーザーが回答]
        ↓
[LLMがそれを評価&FBを送る.スコアリング(20点max)]
        ↓
[再帰的に5ターン]
        ↓
[スコア + 総評 + キャラ口調コメント]

という流れでした.これをもっと深掘り,フロントとバックエンドの通信の流れをとにかく具体化しつつ

実際に作成したシーケンス図
実際に作成したシーケンス図

こんな感じで作成し,フロントエンドとバックエンド,メンバー間での認識の齟齬がとにかく生まれないことを徹底するために設計はかなり頑張りました.また,これらを元にAPIを設計してRedocにまとめ,メンバー間でいつでも確認できるようにするといった取り組みもしました.
https://js-ninjaaaa.github.io/RepoInterviewer/

最後に,このプロダクトはハッカソンの題材として終わるべきものではないとメンバーらで共通した認識であったため,今後の開発のしやすさと拡張性を持たせるといった面で,バックエンドは責務と関心をしっかり分離した以下の構成で開発を進めました.

backend/
├── app/
│   ├── main.py                   # 🎯 エントリポイント
│   ├── api/                      # 🌐 ルーティング
│   │   ├── endpoints/            # ┗ 各機能のエンドポイント定義
│   │   │   ├── interview.py
│   │   │   └── health.py
│   │   └── __init__.py           # Routerをまとめる
│   ├── services/                 # 🧠 ビジネスロジック (LLM, 評価など)
│   │   └── interview_service.py
│   ├── core/                     # ⚙️ 設定・共通リソース
│   │   ├── config.py
│   │   └── redis.py
│   ├── utils/                    # 🔧 ユーティリティ
│   │   └── zip_handler.py
│   ├── repositories/             # 🗃️ DBやRedisとの接続
│   │   └── interview_repo.py
│   └── models/                   # 📦 fastapi-codegen用モデル(自動生成専用)
│       └── interview_models.py
├── api.yaml                      # 🧾 OpenAPI スキーマ
├── requirements.txt
└── Dockerfile

一部変更はありましたが,概ねこの構成で開発しました.これにより,プロンプト開発とAPI開発を並行で進めることができ,後々かなり良い影響が出ました.
また,進捗管理も怠らず前回同様にMiroを用いて行いました.

実際に作成した進捗管理シート

開発(事前開発期間&本番開発期間)

あとはAPIの仕様書ができたので,それに沿って必要な機能をバックエンド・フロントエンドに別れて開発するだけです.ただ,ここでもいろんなトラブルがありました.

ローカルLLM あまりにも低速

当初の予定では,Ollamaを用いてローカルLLM(Llama-3-ELYZA-JP-8B-GGUF)を活用する予定でした.理由としては,

  • Modelfileを用いて細かいキャラ設定をできる
  • キャラが急に化けることがない(Modelfileベースでキャラを決めると,出力が良さげだった)
  • 無料(金がないんだよと)

これらの理由で,当初はOllamaと連携して開発を進めていましたが,ユーザーのプログラムを受け取り,LLMに関連した質問を複数生成させる段階でなんと数十秒もかかってしまうことが発覚しました.また,OllamaにてModelfileをキャラ単位で用意して,動作を切り替えようとも思いましたがそれもできないようなので,使う意味がなくなってしまいました.

きむ「だめだ,Ollamaが遅すぎる」
僕「これは....もう金を払うしかないか?」
きむ「...いや,Geminiなら制限あるけど無料で使えるぞ?」
僕「!」

ということで,Gemini対応の構造に変更したことで,体感数倍以上のスピードで質問生成を行うことができました.

はじめてのキャッシュ実装

このツールでは,各ユーザーが面接を行った際の情報を一定時間保存しておかないと,会話履歴などに基づいた面接ができません.そこで,インメモリDBを使ったキャッシュの実装を行いました.
当初の実装では,

"{interview_id}" =>
{
  "difficulty": "normal",
  "total_question": 3,
  "questions": [
    {
      "score": 0,
      "history": [
        {
          "role": "model",
          "content": "who are you"
        }
      ]
    },
    {
      "score": 0,
      "history": [
        {
          "role": "model",
          "content": ""
        }
      ]
    }
    // ... 3つ目の質問がここに入る
  ]
}

面接セッションごとの単位で実装する予定でした.
しかし,

  • 会話ごとに全体を更新する必要があるのでは?
  • 取り出す必要のないデータまだまるまる取り出すのはパフォーマンスが悪い
    といった懸念点が生まれた結果
"{interview-id}-{question_id}" =>
{
 diffculty: "normal",
 total_question: 4,
 score: 0
 history: [
   {
    role: "model",
    content: "question",
   }
 ]
}
{
  "{interview_id}": {
    "difficulty": "normal",
    "total_question": 3,
    "results": [
      {
        "score": 0,
        "comment": ""
      }
      // ... 各質問ごとの結果が続く
    ]
  }
}

面接セッションのスコアとDB各質問の会話履歴の2種類のキャッシュに設定することで解決しました.

また,もちろん面接に使用する使用者のソースコードの情報もキャッシュしておく必要があります.これをRedisにて保管する方法もありましたが,ソースファイルのサイズによってはRedisで対応し切れない可能性があると考えたため,/tmpフォルダに解凍したものを一時的に保存することで解決しました.

トラウマの繋ぎこみ,今回は如何に?

そうです.前回ハッカソンの一番のトラウマ,それは「バックエンドとフロントエンドの繋ぎこみ」です.ここでかなりリソースを割いて,開発が進まないことがありました.しかし,今回はAPIドキュメントを用いてフロント,バックエンドそれぞれのデータ通信の共通認識を持たせたことで,思ってた数倍以上にすんなりと,終わってしまいました.

え?技術面接なんだから深掘りもするよね?

先ほど話した通り,今回作ったRepoInterviewerは以下の流れで動きます.

[ユーザーがVSCodeで特定のフォルダを開き,プラグイン起動]
        ↓
[LLMが質問5問(可変対応)を一気に送信]
        ↓
[ユーザーが回答]
        ↓
[LLMがそれを評価&FBを送る.スコアリング(20点max)]
        ↓
[再帰的に5ターン]
        ↓
[スコア + 総評 + キャラ口調コメント]

ただ,我々が対策したいのは技術面接,一問一答で終わるとしたら間違いなくその企業はお祈り確定に違いない.ユーザーにそんな気持ちで使ってもらうなど気が引ける.
という考えもあり,当初はこんな実装の予定でした.

難易度 (difficulty) キャラクター 面接タイプ 説明
easy ギャル 一問一答 丁寧で寄り添うスタイル
normal 頼れる先輩エンジニア 一問一答 優しくカジュアルに質問
hard 超論理的上司 深掘りあり 回答を掘り下げていく厳格スタイル
extreme 激詰冷徹エンジニア 深掘りあり 技術的思考や設計まで踏み込む

ただ,実装の時間も想定以上にかかってる工程があったので,初中級レベルの実装で終わるとたかを括っていた僕がいました.

しかし,ハッカソン最終日 11時

しず「けい,上級・激詰用のフロント作ったけどAPIまだ?」
僕「え?いやあれってやれたらやるもんだと思ってたけど」
きむ,しず「「え????まだ時間あるっしょ??」」

やはり持つべきものは優秀な仲間ですね💢(冗談です)

正直,ギリギリまで粘って実装することを避けていた自分がいたことに改めて気付かされました.ハッカソンでも職場でも個人開発でも,その粘り一つでプロダクトの完成度に一気に差が出ることだってあるはずです.なので僕がやるべきはコードフリーズの15時までに,上級・激詰に対応させたAPIとプロンプトの作成でした.とにかく急足でAPIの設計変更を少しして(当初は上級・激詰に対応した仕様ではなかったが,改変の余地を残した設計にしていた),ひたすらコードを書いて14:30にギリギリマージしました!

発表&スライド

スライド準備

コードフリーズが15:00で,16:00から発表が始まる予定でした.しかし,1時間でスライドがすぐ終わるわけもないので,メンバーでリレー式にスライド作成のタスクを回しながら,2分という短い間で魅力を伝える準備をしました.Marpからpptxに変換したのに編集できずに困ったり,Googleスライドにデモを上げられず手こずったり結構大変でした.

発表

僕一人で気合いでなんとか行いました.少し早口ではありましたが,全力でやり切ったのであとは天命を待つのみです.

結果

なんとか「努力賞」を取ることができました!最優秀賞と優秀賞になかった時はほぼ諦めていましたが,最後の最後で発表されてびっくりして,声が出なかった記憶を覚えてます.

理解が追いつかない様子

びっくりしすぎてもう言葉が出てないですね....

最後に

まずは,僕のわがままを聞いて,このハッカソンに参加してくれた2人に感謝します.本当にありがとう.2人がいなかったら正直こんなにすごいプロダクトは作れなかった,確実に.次にここまで読んでくれた方にも本当に感謝します.文章も拙いし,おそらくツッコミどころが多いとは思いますがコメントいただければ幸いです.(僕もかなり勉強になります!!)
そして,これで終わりにするのではなく,次は技育博を目指して進んでいくので見かけたらぜひ声をかけてもらえると嬉しいです!!!
ありがとうございました.

Discussion