OpenAI のモデルと推論レベルを用途ごとに評価したら、設定がかなりバラけた

個人で使っている Google Apps Script の中に、OpenAI API を使っている処理がいくつかあります。
最近まとめてモデルや prompt を見直してみたところ、思っていた以上に用途ごとで結果が違いました。
特に面白かったのが、「難しそうな処理ほど reasoning を上げればよい」という単純な話ではなかったことです(・ω・)
この記事で話すこと
先に結論を書くと、最終的な構成はこうなりました。
| 用途 | model | reasoning |
|---|---|---|
| ヨドバシの商品分類 | gpt-5.6-luna | low |
| 会話ログの検索用要約 | gpt-5.6-luna | none |
| 会話ログ検索前のクエリ生成 | gpt-5-mini | 指定なし |
| 会話ログ検索の Embedding | text-embedding-3-small | - |
| ジャーナリングの総合分析 | gpt-5.6-luna | none |
| ジャーナリングのポジティブ抽出 | gpt-5.6-luna | low |
同じ gpt-5.6-luna でも none と low に分かり、gpt-5-mini のままにした処理もあります。
今回は、実際に使っているケースを fixture にして比較し、必要な品質を満たす設定を用途ごとに選びました。
約1年前、この仕組みについて登壇していた
2025年6月に「AI 駆動個人開発ナイト【HackForX】」で、「夫婦の雑談が “検索できる知識” になる 〜Slack × Embedding の暮らし方〜」という LT をしました。
ということで、いつもながら家でやったことを話します(*・ω・)ノ
— bvlion🍣🦪 (@ninjinx) June 18, 2025
お手柔らかに🙏#Stract_HackForX https://t.co/c8n2Z0oHSg
普段の夫婦の会話は基本的に Slack に書いていて、古い投稿も GAS 経由で Spreadsheet へ保存しています。
ただ、ログが残っていても「前に温泉の話をした気がするけど、どこだったっけ?」くらいしか覚えていないと、普通の文字列検索ではなかなか見つかりません(^^;
そこで Embedding を使った意味検索を作りました。
ところが、会話をそのまま Embedding すると、「温泉」を探したいのに「お風呂」の話が高い類似度で出てきます。
当時の登壇資料にも「ここで上手く行かないのが人生 (・∀・)」と書いていましたが、本当にその通りでした。
そこで保存時には LLM で検索用に要約してから Embedding し、検索時にも質問を LLM でカテゴリとキーワードへ変換してから Embedding するようにしました。
登壇資料の最後には「時代に合わせて LLM のモデルを変えたり、もう少し安定した出力になるよう prompt も修正したい」と書いていました。
約1年後、今回まさにそれをやることになりました(*・ω・)ノ
Slack 検索を改めて評価する
検索対象を作る要約では、指示語を解決できるか、固有名詞を残せるか、別の話題を混ぜないか、存在しない行動を勝手に追加しないか、といったケースを用意しました。
gpt-5-mini や gpt-5.6-luna の none / low を比較した結果、gpt-5.6-luna + none で必要な品質を満たしました。low にしても明確な改善がなかったため、ここでは none を採用しています。
会話の文脈を読んで「そこ」が何を指すかまで判断するので、見た目にはそれなりに複雑な処理です。
それでも reasoning を上げる必要はありませんでした。
検索前の LLM は残した
一方、検索時に質問を gpt-5-mini でカテゴリとキーワードへ変換する処理については、「そもそも LLM が必要なのか?」も確認しました。
| 方式 | top1 | top3 |
|---|---|---|
| 質問文を直接 Embedding | 3/6 | 6/6 |
gpt-5-mini で検索前処理 | 17/18 | 18/18 |
後者は6問を3回ずつ評価した結果です。
この検索では1位を「いちばん近そうな発言」として表示するため、top1 の差はかなり大きいです。
そのため検索前 LLM は残しましたが、仕事自体は限定的なので gpt-5-mini のままとしました。
同じ Slack 検索の中だけでも、「要約は Luna + none、検索前処理は gpt-5-mini、検索本体は text-embedding-3-small」と役割ごとに分かれています。
1つの機能だからといって、全部同じモデルにする必要はなさそうです (・∀・)
ヨドバシの商品分類は Luna + low
次は、ヨドバシ.com の注文メールから商品を取り出し、Zaim の category / genre を選ぶ処理です。
元々は gpt-5-nano + minimal でしたが、代表ケースを見るとなかなか面白い誤分類をしていました。
| 商品 | nano + minimal | Luna + low |
|---|---|---|
| ボックスティッシュ | 食費 > 食料品 | 日用雑貨 > 消耗品 |
| USB Type-C ケーブル | 食費 > カフェ | 住まい > その他 |
| フライパン | 食費 > 食料品 | 日用雑貨 > その他 |
| ゴールドポイント利用 | その他 > カードの引落 | その他 > ポイント利用 |
USB Type-C ケーブルがカフェなのは、どういう判断をしたのか少し気になります(;・∀・)
prompt だけ改善して nano のまま使う案も試しましたが、十分には改善しませんでした。gpt-5.6-luna + low にすると代表ケース全体でかなり自然になったため、こちらを採用しました。
USB Type-C ケーブルだけは gpt-5.6-terra + low も試し、Luna の「住まい > その他」に対して Terra は「住まい > 家電」になりました。
Terra の方が自然ですが、この 1 ケースのために大幅に高いモデルへ上げるほどではないと判断しています。
この処理では、文章を深く考えることよりも似た候補の中から変なものを選ばないことが重要そうです。
ジャーナリングは同じ Luna でも分かれた
ジャーナリング用途では、Slack のログから「総合分析」と「ポジティブな出来事の抽出」をしています。
一見すると、good / bad / score / emotion / summary / advice / tags まで生成する総合分析の方が複雑そうです。
ところが結果はこうなりました。
| 処理 | model | reasoning |
|---|---|---|
| 総合分析 | gpt-5.6-luna | none |
| ポジティブ抽出 | gpt-5.6-luna | low |
逆っぽいですよね(・ω・)
総合分析では、good / bad の判断などにある程度の解釈の幅を許容しています。
ただし、ログにない感情・目的・意図・因果関係を事実として作ることは禁止しています。
この条件なら Luna + none で必要品質を満たしました。
一方、ポジティブ抽出では「単に何かをやった」という中立な出来事をポジティブ扱いしないことが重要です。Luna + none では、理由や感情が書かれていないケースまでポジティブとして拾うことがありました。low にすると期待通り空配列になったため、こちらは Luna + low を採用しました。
処理の複雑さより、どこに判断の境界線があるかの方が reasoning に影響しているように見えます。
ここは今回かなり意外でしたΣ(・ω・ノ)ノ
高いモデルへ統一しなくてよかった
評価前は「全部 Luna あたりに揃えてしまえば楽かな」と少し考えていました。
ですが結果を見ると、統一しない方がよさそうです。
検索前処理は gpt-5-mini で十分。
検索用要約と総合分析は Luna + none で十分。
商品分類とポジティブ抽出では Luna + low が必要でした。
さらに上位モデルで少し改善しても、その差がコストに見合わなければ採用しませんでした。
今回の基準は、代表 fixture で必要品質を満たす中で、できるだけ軽い設定を使うです。
個人開発なので、最高性能より「実際に使って困らない」を優先しています(^^)
eval も使い捨てにしないことにした
実は以前も同じような評価をしていましたが、終わった後に eval.js を削除していました。
次にモデルを見直そうとすると「前回どんなケースで評価したんだっけ?」となり、GitHub の過去 PR から掘り起こすことになります(´・ω・`)
そこで今回は、ヨドバシ分類、Slack 検索、ジャーナリングそれぞれに eval runner を恒久的に残しました。
基本的には Apps Script から 1 回実行すれば、現在の本番設定と candidate を比較できます。
本番の model / reasoning / prompt / schema は eval 側へコピーせず、本番コードを直接参照します。
次に新しいモデルが出たときは candidate だけ差し替え、同じ fixture でもう一度評価できます。
これで次回は過去の PR を発掘するところから始めなくて済みそうです (^w^)
自動採点しすぎない
もう1つ意識したのが、何でも PASS / FAIL にしないことです。
検索の top1 / top3、空配列であるべきケース、JSON Schema の検証など、機械的に判定できるものは自動化しました。
一方で「この summary が一番自然」「この emotion が唯一の正解」といったものは固定せず、REVIEW_REQUIRED として人間が確認します。
生成 AI の評価をするために、人間側で無理やり唯一の正解を作ってしまうのも違う気がします。
モデルではなく「失敗の仕方」を見る
今回、一番しっくりきたのはこれでした。
タスクが難しいかどうかではなく、そのタスクで何に失敗すると困るかを見る。
- ヨドバシ分類なら、候補を取り違えないこと。
- 検索用要約なら、文脈を解決しつつ存在しない事実を作らないこと。
- 検索前処理なら、欲しい情報を top1 へ持ってくること。
- 総合分析なら、解釈の幅を残しつつ事実を捏造しないこと。
- ポジティブ抽出なら、中立な出来事を勝手にポジティブへ寄せないこと。
ユーザーから見ればどれも「AI を使った機能」ですが、AI にやらせている仕事は別物です。
だから最適な model や reasoning も違って当然なのかもしれません。
1年前の登壇資料に書いた「時代に合わせて LLM のモデルを変えたい」という展望も、ようやく少し回収できました (*・ω・)ノ
全部を最新・最高設定へ揃えるより、用途ごとに小さな eval を持って、必要品質を満たしたところで止める。
個人開発で生成 AI を使い続けるなら、このくらいの運用が私には合っていそうです(๑•̀ㅂ•́)و✧