AIエージェント開発におけるモデル選定、軽量モデルか高性能モデルか

# 目次
1. [はじめに](#1-はじめに)
    - [1.1 参考](#11-参考)
2. [Q&A生成機能](#2-qa生成機能)
    - [2.1 補足: json_repairについて](#21-補足-json_repairについて)
3. [フレームワーク選定機能](#3-フレームワーク選定機能)
4. [タスク生成機能](#4-タスク生成機能)
    - [4.1 タスク詳細を除くタスク情報の生成機能](#41-タスク詳細を除くタスク情報の生成機能)
    - [4.2 詳細フィールドを加えたタスク情報の生成機能](#42-詳細フィールドを加えたタスク情報の生成機能)
5. [おわりに](#5-おわりに)


# 1. はじめに
その辺の学生です。
AIエージェントを作成する際に、モデルの性能の高さと処理時間のトレードオフに悩まされまくった結果、どういう基準でモデルを選定するべきか、どのような場合に軽量なモデルで問題を解決できるかを1ミリくらい理解したのでそれを備忘録としてまとめてみる記事となります。

結論から言いますと、どうしようもない0から1を作成するようなタスクは高精度のモデルや推論モデルを使用推奨、1から10や100にするときはそれほど高度な推論モデルは必要ないという考えに至りました。1が作れてしまえばプロンプト過多などの状況を除いて、大抵の場合軽量モデルでなんとかなります。
その結論に行き着いた理由について、実際に開発したAIエージェントの事例に基づいて解説していきます。
あくまで今回の事例に限る話にはなりますが、そこそこ参考にできるかも？

## 1.1 参考
制作したAIエージェントについて本題に入る前に触れておきます。
### ハッカソン支援エージェント
https://hackson-support-agent.vercel.app/

↑リンク
### 使用技術
Next.js, FastAPI, langchain(本日の主題), etc...

GitHubは[こちら](https://github.com/vyuma/hackson_support_agent)。

現在開発中ですので出力などが不安定な可能性がありますがご容赦ください。
あと、基本的に**Gemini API**を前提としています。API料金が安いのでお財布に優しい！魅力は無料枠だけにとどまらないよ！！！

初心者のプログラミングのさわりはやったけど何かしらものを作るとなると何からすれば良いかわからない！という人に向けたアプリ制作補助ツールです。
特徴としてはアイデアからQ&Aを作成し作りたいものの解像度を上げ、そこから仕様書を作りタスクを分割し...とやることを明確化しアプリ制作に取り掛かる一助となるものを目指しています。

また、AIのレスポンス処理の時間がアプリ使用時の負担にしたくないので処理時間に気をつけました。

:::note info
今回このアプリを触っていることを前提とした記事ですので一度使ってみる、もしくは使いながら記事を読むことを強く推奨します。
:::

# 2. Q&A生成機能
まず、アプリで最初に呼び出されるQ&A生成API。
こちらは典型的な前者、プロンプトが少ない上に作りたいアイデアをもとに深掘るための質問のリストを0から制作しなければなりません。つまり高度な推論モデルの使用が求められます。
この場合、トークンの使用量が異常に高くなる心配もないので、特に迷うことなく推論モデルの採用ができますね。
実際、本アプリでのQ&A生成機能では推論モデルを採用しています。

:::note warn
注意
推論モデルにあまりに長いjson objectを返させると想定していないjson形式で返され、jsonがパース出来ないものを出力する可能性が高くなります。
プロンプトでエラーの低減を図るか、json_repairで対処しましょう。
:::

## 2.1 補足: json_repairについて
以下のようなBNFルールに基づいて、jsonを解析します。
```text
<json>      ::= <primitive> | <container>  
<primitive> ::= <number> | <string> | <boolean>  
<container> ::= <object> | <array>  
<object>    ::= '{' [ <member> *(', ' <member>) ] '}'  
<array>     ::= '[' [ <json> *(', ' <json>) ] ']'  
<member>    ::= <string> ': ' <json>  

```

この定義に基づかないjsonを適切に補完する(括弧の自動追加、引用符の整合化など)というライブラリです。使い方については[ライブラリのGitHub](https://github.com/mangiucugna/json_repair)を参考にしてください。
今回のようなLLMの出力を安定させたい時に便利です。


# 3. フレームワーク選定機能
次に、フレームワーク選定をAIモデルにさせるAPIではどのようにしたか。
フレームワーク選定ではフロントエンド、バックエンド二つにおいて数多くのフレームワーク、ライブラリが存在します。ではベストプラクティスを仕様書などのインプットから探すので推論モデルでしょうか。

ここでは、判断させるためのインプットとして仕様書の内容などを投げるためトークンが増大します。そのため、処理時間低減のためにモデルは軽量なものにするするべきと考えました。
軽量のモデルでも無限の選択肢から選ばせるという実装でも動きはしますが、自由度を持たせすぎると不具合の原因となり返答が不安定になりました。つまりどうすれば良いか。

選択肢を最初から提示することで解決です。ライブラリ/フレームワークの選定は、初心者が使うものとなればある程度絞れてきます。ならば先に選択肢を絞り、その中で優先度をつけさせれば良いではないか。

 以下は実際に使われているコードです。ちゃんとコード見たい方は[こちら](https://github.com/vyuma/hackson_support_agent/blob/develop/back/services/framework_service.py)。
```python:レスポンススキーマ
        response_schemas = [
            ResponseSchema(
                name="frontend",
                description="配列形式のフロントエンドフレームワークの提案。各項目は {name: string, priority: number, reason: string} の形式。",
                type="array(objects)"
            ),
            ResponseSchema(
                name="backend",
                description="配列形式のバックエンドフレームワークの提案。各項目は {name: string, priority: number, reason: string} の形式。",
                type="array(objects)"
            )
        ]
```
```:プロンプト内容
あなたはプロダクト開発のエキスパートです。
以下の仕様書の内容に基づいて、固定のフロントエンド候補（React, Vue, Next, Astro）とバックエンド候補（Nest, Flask, FastAPI, Rails, Gin）について、各候補の優先順位とその理由を評価してください。
各候補に対して、プロジェクトにおける適合性を考慮し、優先順位（数字が小さいほど高い）を付け、理由を記述してください。
回答は以下のフォーマットに従って、JSON 形式で出力してください。
ここで日本語で出力してください。
{format_instructions}
仕様書:
{specification}
```
このように出力をパターン化してしまうことで出力の安定化を図ることで、軽量モデルでも安定した出力を可能にしました。
なるだけ安定した出力が保証されるかつ軽量で済ませる工夫がAIエージェント制作の基本な気がする。

# 4. タスク生成機能
まず前提として、タスクが持つ情報は、
```python:タスク情報
{
  "tasks": [
    {
      "task_name": "string", #タスクの名前
      "priority": "string", #タスクの優先度(Must,Should,Could)
      "content": "string", #タスクの主な内容
      "detail": "string" #タスクの詳細なハンズオン情報
    }
  ]
}
```
の4つのフィールドからなります。
最終的にはこのタスク情報を約30ほど生成する必要があります。
これは0から作るタイプに該当するため生成時に多少トークンがかかろうと推論モデルを使いたい。が、それをする上で考慮するべき問題が複数あります。
主な問題点は以下の通り。
- **Gemini APIのRequest/minの制限**
    - 推論モデルなど高性能なものは10リクエストRPM
- **Next.jsAPIのタイムアウト制限(300秒)**
    - 一度に多くのプロンプトを送りすぎると、応答時間が300秒を超えてしまう。
    - バッチ処理をした時にも同様の問題が起こりうる。
- **レスポンスの不安定さ**
    - プロンプトが大きくなりすぎた時の弊害その2。返答が安定しない上、詳細タスクの情報の出力が不十分になる。
- **高性能なモデルを使用した時のトークンの制限**
    - token/minの制限がよくあり、これも考慮しないといけない。
- **高性能なモデルを使用した時のマルチスレッドの制限**
    - そもそもマルチスレッドにするとトークンなど他の制限に引っかかることになる。
- **軽量なモデルの性能の低さ**
    - タスク生成自体を担わせるには性能が低すぎる。

つまり処理を短く高精度なモデルを使えば解決です......と、そんな都合良くはいかないので工夫してなんとか処理していきます。

## 4.1 タスク詳細を除くタスク情報の生成機能

タスク情報自体を生成するタイミングではどうしても高性能なモデルを使いたい。使わなければ話にならない。その時問題となるのはトークン量の問題。
トークンがかさむのは圧倒的にタスク詳細情報であることは自明だと思います。ならば困難は分割するものです。詳細以外を高性能モデルで出力させましょう。

タスク情報のうちdetailのフィールドが抜けているリストを作成してもらいます。トークンの削減の恩恵か、処理時間としては2分程度で、出力は安定します。

## 4.2 詳細フィールドを加えたタスク情報の生成機能

こちら、最初は高性能のモデルを使用し、トークン量は妥協してタスク全体をインプットとしてしまって、全てを出力させたのちjson_repairを使用することで実装していました。
成功率は6割程度で、3,4分の処理時間を必要とします。タスク内容の文脈が入っている分、タスク情報を0から出すよりも精度は上がりますが、効果的な対処法ではありません。
これは改善の余地あり。

## 改善案とその結果

以下が改善のために試行した方法になります。

- 一つ目、タスクを5つごとに分割して、それぞれをインプットとすることでトークン量を下げることで対処する。いわゆるバッチ処理です。
    - これは、逆に処理時間が増大してAPI処理が300秒を超過します。返答としてトークンをより使える状態になってしまい、結局処理時間が少なくならなかったことが原因と考えられます。
    - また、マルチスレッドで処理することも考えましたが、問題点で明記した通り複数を並列して処理させるとトークン制限に引っかかるため不可能です。
- 二つ目、フロントエンド側でバッチ処理をし、それぞれをバックエンドのAPIに送ることで300秒の制限を実質無効化する方法。
    - これは本質的な解決でないので却下です。300秒でも長いのにそれ以上ユーザーを待たせるわけにはいきません。
    - また、バッチ処理の問題点として、タスク間の文脈が読み取れなくなってしまう問題も潜在しています。

ここまで、モデルの変更だけは頑なに許さない解決方法を模索していました。
モデルを妥協すると、トークン制限やリクエスト制限が大幅緩和。これによりマルチスレッドでもなんでもござれ状態。ただ回答の質が下がる。

そのように考えていたのですが、バッチ処理かつマルチスレッドで処理を回してもトークンにはまだまだ余裕があるため、基本のプロンプトに情報を追加できます。つまりタスク間の文脈の欠落の問題を解消でき、回答の質は下がるどころか上がるのです。
厳密なjson形式の返答精度は低いですが、そこはjson_repairで軌道修正できる範囲です。
さらに、処理自体が軽い上にマルチスレッドで回せるので全体の処理時間も大幅に低下。
結果的に、処理時間3分かつ回答が安定しない状態から**処理時間30秒かつ回答が安定する状態**まで改善されました。

まとめると、以下の表の通りです。


| 改善案               | メリット                              | デメリット                            | 結論              |
| -------------------- | ------------------------------------- | ------------------------------------- | ----------------- |
| 情報全投げ(初期案) | 想定通りの挙動をたまにしてくれる | 応答が安定しない | NG |
| 1. タスクを分割投入  | トークン量が減少 | 総処理時間が API タイムアウトを超過 | NG |
| 2. フロントでバッチ処理 |  Next.js側で300秒制限を回避 | UX が悪化 | NG |
| 3. マルチスレッド併用 |  文脈を保持しつつ高速化が可能 → トークンに余裕が生まれる |  JSON の厳密性がやや低下 → `json_repair` が必須 | 採用 |

こうして、現行バージョンのAIエージェントの完成となります。

# 5. おわりに
最終的にここから学んだのは、AIエージェントにおいて、**モデルの性能が全てではないこと**。AIにとってのタスクの難易度やどれだけの回答性能を求めるかを吟味した上で、それは工夫次第で軽量モデルで代替可能かを検証してみましょう。Ultra Cが見つかって幸せになれるかもしれませんという話でした。おしまい。