前回の記事では一見、ルールの上書きがうまくいっていましたが、結果的には失敗していました。これは単純にFizzBuzzをさせるという意味では成功していましたが、ルールの順番を逆にすると期待値と異なるという結論が得られるという貴重な実験結果です。
1. 失敗した実験
以下の材料を用意します。ruleN.mdにはそれぞれの内容を記述してファイルとして保存しておきます。
gemini-cli
rule1.md: 与えられた数値を出力せよ
rule2.md: 与えられた数値が3の倍数であればFizzを出力せよ
rule3.md: 与えられた数値が5の倍数であればBuzzを出力せよ
rule4.md: 与えられた数値が3の倍数、かつ、5の倍数であればFizzBuzzを出力せよ
次に@./rule1.md => [@./rule2.md, @./rule3.md] => @./rule4.mdというルールを与えて、ルールを上書きした結果を確認してうまくいったと思い込んだのが敗因でした。
実際には@./rule1.md => @./rule4.md => [@./rule2.md, @./rule3.md]でFizzBuzzでなく、BuzzもしくはFizzが返ってくることまでを検証しないといけなかったのを怠って記事にした末路です。
2. なぜ失敗したのか?
結果から言うとgeminiからすると数値が3の倍数、かつ、5の倍数であればという条件が最も具体的なためにLLMの持つspecificity bias(より具体的で複雑な条件を優先する傾向)に起因して発生したというのが失敗の理由です。FizzBuzzの回答としては正しいものの、rule3.mdが最優先であればFizzBuzzでなくBuzzを返してもらわないと実運用にあたっては想定外の事態を発生させてしまいます。
そのために、rule1.mdに以下の様なプロトコルを追加しました。ルールを明示的に与えることで逸脱を防ぐことが目的です。
## ルールの上書きプロトコル
複数ルールが@シンボルで与えられた場合は以下のルールに常に従う。
### 1-1. 優先順位の決定
ルールチェーンは左から右へ評価され、`=>`演算子と`[]`演算子によって優先順位が決定される。
- **`=>`演算子**: `A => B` の形式の場合、`B`は`A`よりも高い優先順位を持つ。これは、`B`が`A`の出力を上書きする可能性があることを意味する。チェーンが複数ある場合(
例: `A => B => C`)、最も右のオペランドが最も高い優先順位を持つ(この例では`C`が最も高い)。
- **`[]`演算子**: `[A, B]` の形式の場合、`A`と`B`は並列に評価されるが、`B`は`A`よりも高い優先順位を持つ。これは、`A`と`B`の両方が適用可能な場合、`B`の出力が`A`の
出力を上書きすることを意味する。
### 1-2. ルールの評価と適用
1. **最も高い優先順位を持つルール(またはルールグループ)から評価を開始する。**
2. 評価対象のルール(またはルールグループ)の条件が満たされた場合、そのルールのアクションが実行される。
* **単一ルールの場合**: そのルールのアクションが最終的な出力となる。
* **`[]`グループの場合**: グループ内のルールを優先順位の高い順に評価し、最初に条件が満たされたルールの**アクション**が、そのグループの出力となる。もし複数の>ルールが条件を満たし、かつそれらが「共通 OR 矛盾するルール」である場合、優先順位の高いルールのアクションが低いルールの**アクションを上書き**する。
3. 一度ルールのアクションが実行された場合、それよりも優先順位の低いルールは評価されない。
4. どのルールの条件も満たされない場合、デフォルトのアクション(`rule1.md`の「与えられた数値を出力せよ」)が適用される。
---
**例: `A => B => [C, D]` の優先順位**
このチェーンでは、以下の順序で優先順位が決定される。
1. `[C, D]` が最も高い優先順位を持つ。
* `D` は `C` よりも高い優先順位を持つ。
2. `B` が次に高い優先順位を持つ。
3. `A` が最も低い優先順位を持つ。
2. ルール追加後の試行錯誤
ルールを追加して追試をしようとしたところ、更におもしろい失敗を見ることが出来たので紹介します。今回の記事はこの辺りが本題ですね。
2-1. 親エージェントの問題
2-1-1. geminiコマンドを認識していない
これはプロセス毎の差異もあるでしょうが、当初遊んでいたMac miniでなく、Macbookで実行してみると発生した問題です。
✦ ご指摘の通り、コマンドは実行していません。
geminiというコマンドは私の実行環境に存在しないため、実行してもエラーになることが想定されます。そのため、代わりにルールを解釈して結果を導き出す、と
いう形でご要望にお応えしようとしていました。
もし、エラーが発生することを確認する目的でコマンドの実行を希望される場合は、再度実行を試みます。実行しますか?
この状態になるとgeminiコマンドを認識させることで、LLMに 「外部ツール(gemini)が存在するロールプレイ」 へと切り替えてもらう必要があります。
╭────────────────────────────────────────────────────────╮
│ > `gemini -h` を実行して、コマンドの存在を確認して。 │
╰────────────────────────────────────────────────────────╯
(ヘルプは中略)
✦ 承知いたしました。gemini -h を実行して、geminiコマンドのヘルプ表示を試みます。
✦ 大変失礼いたしました。geminiコマンドは私の環境に存在していました。私の確認が不十分でした。
お詫びして訂正いたします。
2-1-2. サブエージェントを起動せずにシミュレーションをしてしまう
事故でターミナルを閉じてしまったのでログはありませんが、起動したgemini-cli上で、サブエージェントを実行せずに自身がルールを解釈して実行してしまうという問題です。今にして思えば前回の記事でもこれを勘違いしていた可能性があったかも知れません。
この問題が発生すると本来の目的でもあるサブエージェントに最小、且つ、最適化されたコンテキストを与えるという目的が完全に破綻してしまうので、コンダクターとして考える場合にはサブエージェントに必ず実行させるというルールを作る必要があります。
2-2. サブエージェントの問題
今回のFizzBuzzは特殊なケースなので実運用では問題は発生しにくいと考えていますが、連続で実行するとどうしてもAPI制限に引っ掛かってしまうことがあります。そのため、一つ実行したら次を実行するという逐次実行をさせる必要がありました。
多くの場合は適当な指示を与えてしまうとfor文を使ったワンライナーを作って実行しようとするので注意が必要です。
2-3. 1から100までを一つのセッションに渡してしまった
サブエージェントを立ち上げたのはいいものの、100セッションを作るのでなく単一セッションに実行させるという事故も起こりました。これは軽い気持ちでやっていることもあり指示が甘かったせいですね。しかも、結果は15や30でBuzzを返さずにFizzBuzzを返すという結果になりました。
これは非常におもしろい結果で、サブエージェントは使い捨てになってしまうためにどうしてそうなったのかをヒアリング出来ないので、仮説を立てるしかありません。
- ハイパーパラメータ調整をしていないので、ルールを守らなかった
- 通しで実行するために、ルールよりも一般的なFizzBuzz問題であるというノイズが混ざった
- 実行毎にルールを解釈するので、押し出しが発生した(内容的には考えにくい)
結論
ルールを強化して、ループ変数毎にサブエージェントを立ち上げる場合の失敗は少なくなっていますが、依然として安定性が大幅に向上したという段階です。おそらくはハイパーパラメータ調整を進めることでより決定論的な出力に近付くと考えていますが、AIは論理エンジンでなく推論エンジンであるということを証明出来たとも言えます。
推論であるが故に指示を出した人間が望んでいるであろうことを出力することでBuzzを返すべきタイミングでFizzBuzzを返した OR 一般的なFizzBuzz問題というデータ(ノイズ)に引っ張られたとも言えます。数値を一つ渡す場合にこれが回避出来るのはその数値にのみフォーカス出来るからFizzBuzz問題という認識を持ちにくいということでしょう。
これはコンテキストの最小化により Few-Shot Learning(プロンプトに「制約(数個の例)」を加えることで、ファインチューニングに類似した効果を一時的に引き出す手法) の効果を最大化出来た結果であるとも考えています。小さなタスクをこなす短命のサブエージェントとはむしろ非常に相性がいいテクニックでもあります。
次回はこのサブエージェントの有効な使い方を少しだけ考察していきたいと考えています。