MIRAIS 設計・実装記録 #5|頭脳部分 — マッチング・表彰・割り当てのアルゴリズムと外部連携
第 5 回 頭脳部分と、外部との接続
ヒアリングで伺った運用——「企業には序列がある」「学科賞の候補から企業賞へ格上げする」「テンプレをコピーしてデータを入れる」——を、そのままアルゴリズムと連携処理に翻訳した回です。
第 X 部 アルゴリズム詳解
32. LLM セマンティックマッチング
32-1. 全体の流れ
学生が提出物を保存 → タイトル+抄録+タグ を埋め込みベクトル化して保存
企業が関心テキスト保存 → 関心テキストを埋め込みベクトル化して保存
推薦時 → 企業ベクトルと各提出物ベクトルのコサイン類似度を計算し、上位を返す
32-2. 二重化戦略
学内 LLM サーバーが設定されていない → モック埋め込みを使う
設定されている → API を呼び、成功すれば正規化して使う
呼び出しが失敗した → モック埋め込みにフォールバック
設計の要点は、LLM が落ちていてもネットワークが繋がらなくても、推薦機能がエラーを返さないことです。 学内サーバーが応答しない場面でも、推薦は動き続けます。
これは NF-C2 の実装であると同時に、実演の場での保険でもありました。数分のデモ中に外部依存が落ちる可能性を、設計で消しておく。「動かないかもしれない」という不安を抱えたまま人前に立たなくて済むのは、想像以上に大きな効果がありました。
32-3. モック埋め込みの中身
代替経路とはいえ、中身は真面目に作っています。
① 小文字化・改行除去
② 英数字トークンと日本語トークンに分割
③ 日本語トークン(2 文字以上)は 2-gram も追加
例: "機械学習" → ["機械学習", "機械", "械学", "学習"]
④ 各トークンをハッシュして 256 次元のいずれかのバケットに加算
⑤ L2 正規化
- 256 次元のハッシュトリック。語彙表を持たないため未知語にも対応でき、メモリも固定です。
- 日本語の 2-gram を追加しているのは、形態素解析器を依存に加えずに日本語の部分一致を捉えるためです。「機械学習」と「学習支援」が
学習を共有してスコアが上がります。 - 決定論的。同じ入力に対して常に同じベクトルを返すため、実演の再現性が保証されます。
32-4. コサイン類似度
ベクトルは生成時点で L2 正規化されているため、内積がそのままコサイン類似度になります。
次元数が異なるベクトルが混在した場合(学内 LLM を後から設定した場合など)は、短い方に切り詰めて計算します。数学的には正しくありませんが、クラッシュしないことを優先した実務的な判断です。正しい運用は、設定を変更したら再インデックスを実行することです。
32-5. 遅延生成による自己修復
埋め込みが存在しない提出物は、検索時に自動生成して保存します。過年度データの取り込みなどで埋め込みが欠けていても、運用上「再インデックスを忘れた」が致命傷になりません。
33. 表彰選定アルゴリズム
ヒアリングで伺った「ゼミ側から学科賞リストを受け取り、企業賞へ格上げする」「企業には序列がある」という運用を、そのままアルゴリズムに翻訳したものです。
33-1. Phase 1:教員推薦フェーズ
① 各ゼミの「提出を完了した学生数」を集計
② 合計 30 名の推薦枠を、ゼミの学生数比率で Hamilton 法により配分
③ ゼミごとに、投票総得点の 1 位から枠の数だけ「学科賞」候補を作成
④ 教員・運営は画面から手動で調整可能
Hamilton 法(最大剰余方式)を使った理由
単純な四捨五入では合計が 30 になりません。切り捨てでは枠が余ります。Hamilton 法は「比例配分の小数部が大きい順に余りを 1 ずつ配る」ことで、合計を厳密に保ちながら比例性を最大限維持します。米国下院の議席配分にも使われた古典的な手法です。
① 比例配分を計算(小数)
② 切り捨てて整数部を確定
③ 配りきれなかった余りを、小数部が大きい順に 1 ずつ配布
④ ゼミの人数を超えないようクリップ
タイブレークを 3 段階(小数部 → 人数 → 定義順)にしているのは、実行のたびに結果が変わらない決定性を保証するためです。「昨日と今日で推薦枠が違う」というのは、運営の信頼を失う類のバグです。
33-2. Phase 2:企業選出フェーズ
① 前回選出済みの企業賞を、いったん学科賞に戻して推薦プールへ復帰させる
← これをしないと再実行のたびに推薦候補が減っていく
② 承認済み企業を序列の高い順に巡回
③ 各企業の投票を順位の昇順で辿る
④ 「推薦プールにあり、かつまだ他社に確保されていない」提出物を 1 件確保
⑤ 確保されたレコードをそのまま更新して「学科賞 → 企業賞」に格上げ
⑥ どの推薦とも合致しない企業はスキップし、企業名を記録して運営へ返す
この設計が体現しているルール
- 企業賞は必ず学科賞の推薦プールから選ばれる。教員が推薦していない学生が、企業の一存で受賞することはない。
- 序列の高い企業から先に選ぶ。運営が設定した序列がそのまま優先順位になる。
- 1 学生 1 賞。既に他社に確保された学生は選べない。
- 冪等。何度実行しても同じ結果になる(①の復帰処理があるため)。
①の復帰処理は、最初の実装では入れておらず、「実行するたびに推薦候補が減っていく」というバグとして発現しました。冪等性は後から足すものではなく、最初から設計に織り込むものだと痛感した箇所です。
33-3. 手動編集の尊重
統合実行では、推薦プールが空のときだけ Phase 1 を実行し、既に推薦があるときは上書きしません。教員が調整したプールを、運営が自動実行ボタンを押した拍子に消してしまうことがないようにしています。
自動化は、人の判断を上書きしない範囲で行う。 ロゴのコピペ運用を許容した判断(第 7-5 節 / 第 1 回)と同じ発想です。
33-4. 受賞者の差し替え
受賞を別の学生へ付け替える操作では、第 21-1 節 / 第 2 回の遅延評価制約が効きます。2 件を入れ替える際、中間状態で一時的に制約違反となりますが、評価がトランザクション終了時まで遅延されるため成功します。
34. ブース自動割り当て
「誰が 3 部制のどのタイミングで発表するか」の割り振り(P-A3)への解です。
34-1. ルール
① ゼミが設定されている提出物を対象とする
(未設定の学生はスキップし、名前を運営へ返す)
② ゼミごとに Hamilton 法の 3 等分で 3 部へ配分
③ 各部内では、同じゼミの学生が連続するようゼミ名順に並べ、
「部-通し番号」をブース番号として割り振る
④ 提出ステータスに依存しない(未提出の学生も配置する)
⑤ 冪等。実行するたびに再計算し、以前の割当を上書きする
34-2. 3 等分と、余りの寄せ方
10 人 → [4, 3, 3]
11 人 → [4, 4, 3]
9 人 → [3, 3, 3]
余りを早い部に寄せる理由は、1 部が最も来場者の多い時間帯であり、人数を厚くするほうが発表の機会損失が少ないためです。
34-3. 並び順
学籍番号を第一キーにすることで、同じゼミの学生が学籍番号順に並びます。名札作成や当日の点呼が学籍番号順で行われる運用に合わせました。
34-4. 失敗を隠さない
割り当て結果には「ゼミ未設定で割り当てられなかった学生の一覧」が含まれます。運営は「この学生のゼミを設定し忘れている」ことに即座に気づけます。
処理が成功したかどうかだけを返す設計にすると、こういう取りこぼしが当日まで発覚しません。部分的に失敗したことを、失敗として返す——地味ですが、運用で効く設計だと思っています。
35. 投票の集計
35-1. 2 種類の投票モード
【企業の順位投票】
同じ (イベント, 企業, 順位) の投票が既にあれば、対象を書き換える
=「1 位を A さんから B さんに変更」という操作になる
【一般の評価投票】
同じ (イベント, 提出物, 投票者) の投票が既にあればスコアを更新
=「1 人 1 提出につき 1 票」
企業の順位投票は、一意制約によってデータベースのレベルでも重複が防がれています。アプリケーションロジックとデータベース制約の二重防御です。ロジック側だけで守ると、別の経路から書き込まれたときに崩れます。
35-2. 同じデータを 2 つの観点で読む
集計では、総票数・総スコア・順位別の得票数を返します。順位別の内訳があることで、「1 位票を 3 社から得ている」といった質的な情報が表示できます。
表彰アルゴリズムでは、Phase 1 が総スコアを、Phase 2 が順位の昇順を使います。同じ投票データを 2 つの異なる観点で読む構成にしたことで、「たくさんの人に支持された研究」と「特定の企業が最も欲しいと思った研究」を、別々に評価できるようになりました。
第 XI 部 外部サービス連携
36. スライド自動生成
ヒアリングで伺った「表彰スライド」「ブース案内紙」への解です。
36-1. 基本アプローチ
① サービスアカウントでスライド API に接続
② テンプレートを複製
③ 複製先のプレースホルダを実データで置換
④ 生成されたスライドの URL を返す
「テンプレートを作っておいて、それをコピーし、データを入力する」という運営の方の言葉を、そのまま実装方針にしました。利用者が既に持っている作業モデルに合わせるほうが、新しい概念を覚えてもらうより確実です。
36-2. 表彰スライド
企業賞:1 スライド = 1 企業。テンプレートを企業数だけ複製し、それぞれにスコープを限定した置換を適用します。企業名・受賞者名・ゼミ名・研究タイトル・企業ロゴを差し込みます。
学科賞:氏名の一覧を複数列に自動分割します。テンプレートに列のプレースホルダを何個置くかで列数が決まり、氏名リストが均等分割されて流し込まれます。人数が年によって変わっても、テンプレートを差し替えるだけで対応できます。
技術的な落とし穴:スライドの並べ替え API は「既存の並び順で ID を渡すこと」を要求するため、単純な並べ替えではエラーになります。複製した ID の順序を逆順にすることで、視覚的な並び順を揃える方法を採りました。
36-3. ブース案内スライド
1 スライド = 1 物理ブース位置。同じ位置に 1〜3 部の発表者が入れ替わりで座る前提で、1 枚に 3 部分をまとめます。
ブース位置番号
1 部の発表 ID / 発表者 / ゼミ
2 部の発表 ID / 発表者 / ゼミ
3 部の発表 ID / 発表者 / ゼミ
これは「ポスター掲示の上部に貼る、誰がいつこのボードで発表するかを伝える案内紙」というヒアリング内容そのものです。紙 1 枚の構造をデータ構造に翻訳するという、地味だけれど本質的な作業でした。
37. スプレッドシート書き出し
「DB を正暦とし、運営が見やすい形でスプレッドシートへ書き出す」の実装です。
3 つのタブ(提出物・企業・表彰)を、日本語のヘッダー付きで書き出します。書き出し先が未指定なら新規作成し、実行者へ自動共有します。
37-1. なぜ一方向なのか
ヒアリングで「現在のスプレッドシートと同じ形にならなくてよい」と伺えたことで、DB を単一の正暦にできることが確定しました。双方向同期は競合解決が複雑になり、データ破損のリスクを生みます。一方向のエクスポートに限定したことで、
- 同期ロジックが単純で壊れない
- スプレッドシート側を誰かが手で編集しても DB は汚染されない
- スプレッドシートは「バックアップ兼、運営が見慣れたビュー」として機能する
という 3 つの利点が得られました。同期は難しいが、書き出しは簡単。この違いを利用者と合意できたことが、設計上の大きな収穫でした。
38. ポスター保管
38-1. 二重保存
【ローカル保存】必ず実行
→ 認証付きの即時プレビューに使う
→ 閉幕後は削除する前提
【外部ストレージ保存】設定時のみ
→ 運営・教員が普段の環境で直接扱えるようにする
→ 2 回目以降のアップロードは旧ファイルを削除してから登録し、
「1 名 = 1 ファイル」を維持する
外部ストレージ連携が失敗しても、ローカル保存は成功として扱います。学生の提出が、外部サービスの障害で止まらない設計です。学生からすれば「提出したのにエラーが出た」ほど不安なことはありません。
38-2. 実装上の注意点
サービスアカウントは自身の保存領域を持たないため、管理者が所有するフォルダを事前に用意し、そこへ書き込む権限を付与する必要があります。この前提を知らずに実装すると、容量エラーで失敗します。ドキュメントに残しておくべき類の知見です。
39. リマインド連携
未提出の学生を抽出し、チャットツールへ通知する機能です。
現イベントに紐付く提出物を持つ学生のうち、完了していない学生を抽出
→ 通知先が設定されていれば送信
→ 未設定なら「待機中」として履歴にのみ記録
→ 送信失敗なら「失敗」として記録
抽出時に内部結合を使っているのは、過年度の学生を混ぜないためです。卒業生に催促が飛ぶ事故を、クエリのレベルで防いでいます。
39-1. 現状と残課題
| 構想 | 現状 |
|---|---|
| 期限直前の自動実行 | 未実装。手動トリガーのみ |
| 個別 DM 送信 | 未実装。簡易通知のみ |
| 運営画面からの ON/OFF・文面編集 | 部分的 |
| 学生のチャット ID の収集フロー | 未実装 |
この機能だけが「部分実装」で残っています。学内チャットツールへの Bot 導入には申請手続きが必要で、それが並行して進む前提だったため、意図的に後ろに回した経緯があります。技術的な難所ではなく、組織的な段取りが律速になっている箇所です。