Deterministic Shell, Probabilistic Core|決定論的な殻・確率的な核¶
一言で(TL;DR)¶
状態遷移・入出力検証・権限チェック・予算管理など決定論的に記述できるすべての制御を通常のコードで実装し、LLM には「どの選択肢を選ぶか」「自由形式の入力をどう解釈するか」といった曖昧さの解消だけを委ねます。エージェントシステム設計の最も基礎的な分離原則です。
解決する問題¶
LLM は確率的であり同じ入力でも出力が変わります(F3)。状態遷移や権限判定まで LLM に任せると、(1) 禁止操作をすり抜ける、(2) 予算を超過する、(3) 不正な状態遷移が起きる、(4) テストで再現できない、といった障害が確率的に発生し、原因特定も困難になります。さらに監査対象となるシステム(F16)では、判断の根拠がプロンプトの中に埋もれていると、ポリシー違反の検出も事後検証も困難です。
決定論的に解ける部分をコードに押し出すことで、確率的な層の表面積を最小化し、テスト容易性・監査性・安全性を確保します。
選定条件(When to use / When NOT)¶
- 使う条件
- システムに明示的なビジネスルール・権限・予算制約があります。
[failure_cost]が中〜高で、不正な状態遷移や権限違反が実害を生みます。[accountability]が高く、「なぜその操作が許可されたか」をコードレベルで説明する必要があります。- ほとんどのエージェントシステムに適用すべきデフォルト原則です。迷ったら適用してください。
- 使わない条件(=代替に倒す)
[task_variability]が極めて高く、事前に経路を列挙できない探索的タスク(研究補助、オープンエンドなコード生成など) → B3 予算付き自律ループ に倒し、予算と安全弁で律速します。- プロトタイプ段階で制約が未確定 → まず自律ループで動かし、制約が明確になった時点で殻に切り出します。
駆動変数とチューニング(程度)¶
| 目盛り | 効かなすぎ ⇔ 効きすぎ | 決め方 [駆動変数] |
目安(出発点) |
|---|---|---|---|
| 殻の範囲(どこまでコードで制御するか) | LLMが禁止操作を実行 ⇔ 柔軟性喪失・開発速度低下 | [failure_cost] が高い経路ほど殻を厚く、低い経路は核に委ねてよい |
副作用のある操作・金銭移動・権限変更は必ず殻 |
| 検証の厳格さ | 不正出力が後段に流れる ⇔ 正常出力も棄却しリトライ過多 | [failure_cost] × [accountability] の積で決める |
構造化出力(E4)でスキーマ検証+ビジネスルール検証 |
| 状態遷移の粒度 | 大きすぎて部分再開不能 ⇔ 細かすぎてオーバーヘッド | [task_variability] が低いほど細かく刻める |
副作用境界ごとに1状態 |
| ポリシーの外部化度 | ハードコードで変更にデプロイ必要 ⇔ ポリシーエンジン運用コスト | [accountability] が高く頻繁に変わるならポリシーエンジン(E2)へ |
規制要件があれば外部化、なければコード内で十分 |
相反における立ち位置(相反)¶
- F-4 ワークフロー vs エージェント → ワークフロー寄り。制御フローをコードで固定する本パターンはワークフロー側の原則です。ただし核の内部では LLM が自由に推論して構いません。
[task_variability]が高まり経路列挙が困難になったら B3 へ移行する判断点が訪れます。 - F-11 LLM推論 vs コード委譲 → コード側。決定論的に解ける処理(検証・計算・ルール適用)はコードで書きます。LLM に委ねるのは自然言語解釈・分類・要約など非決定論的な判断のみです。
構造¶
flowchart TD
Input[入力] --> V1[殻: 入力検証・認証]
V1 -->|不正| Reject[拒否]
V1 -->|正常| Router[殻: 状態遷移ルーター]
Router --> LLM[核: LLM推論]
LLM --> V2[殻: 出力スキーマ検証]
V2 -->|不正| Retry[リトライ / フォールバック]
V2 -->|正常| Policy[殻: ポリシー・権限チェック]
Policy -->|違反| Block[ブロック / 人間承認]
Policy -->|通過| Budget[殻: 予算チェック]
Budget -->|超過| Stop[停止]
Budget -->|残あり| Effect[殻: 副作用実行]
Effect --> Next[殻: 次状態へ遷移]
Next --> Router
殻(灰色の箇所)はすべて通常のコードであり、ユニットテストで100%カバーできます。核は LLM 呼び出し1回分だけです。
実装メモ¶
殻の最小構造(擬似コード):
def agent_step(state: State, user_input: str) -> State:
# 殻: 入力検証
validated = validate_input(user_input, state.schema)
# 殻: 権限チェック
assert_permission(state.user, state.current_action)
# 核: LLMに曖昧な判断だけ委ねる
llm_output = llm.generate(
prompt=build_prompt(state, validated),
response_format=ActionSchema, # 構造化出力で型を強制
)
# 殻: 出力検証
action = parse_and_validate(llm_output, ActionSchema)
# 殻: ポリシー適用
check_policy(action, state.policies)
# 殻: 予算確認
state.budget.deduct(action.estimated_cost)
# 殻: 副作用実行(冪等キー付き)
result = execute_with_idempotency(action)
# 殻: 状態遷移
return state.transition(action.next_state, result)
落とし穴:
- 検証をプロンプトだけに頼らないでください。「JSONで返して」と指示しても破る場合があります。構造化出力(E4)でスキーマを強制し、さらにビジネスルールをコードで検証します。
- 殻と核の境界を明確なインターフェースにしてください。核への入力と核からの出力を型定義し、殻のテストを核のモックで書けるようにします。
- 殻の中で LLM を呼ばないでください。殻にLLM呼び出しが混入すると分離が崩れます。判断が必要な箇所は核に切り出します。
- 状態遷移図を先に書いてください。殻の設計は状態機械設計そのものであり、コードより先に遷移図を確定させると実装がぶれません。
効かせる力学(forces)¶
- F3(確率的):確率的な層を核に閉じ込め、殻は決定論的にテスト・検証可能にします。核の出力は必ず殻の検証を通るため、確率的な逸脱がシステム全体に波及しません。
- F16(監査対象):権限チェック・ポリシー適用・予算管理がコードで明示されているため、監査時に「なぜこの操作が許可されたか」をコードパスとログから説明できます。
関連・代替¶
- B2 ワークフロー骨格:本パターンの具体化です。殻をDAGとして定義し、各ノードに核を配置します。
- B3 予算付き自律ループ:
[task_variability]が高く経路列挙が不能なときの代替です。予算と安全弁で制御します。 - B4 計画-実行-検証:計画フェーズと検証フェーズが殻、実行フェーズが核という三相構成です。
- E2 Policy-as-Code:殻のポリシー部分を外部エンジンに切り出し、デプロイなしでルール変更を可能にします。
- E4 構造化出力:核の出力を型で縛り、殻がスキーマ検証で捕まえます。本パターンの実装に不可欠です。
- C1 ツールゲートウェイ:殻がツール呼び出しの許可・拒否を制御します。
コーディングエージェント向け指示(machine-actionable)¶
このパターンを人間に提案するなら、同時に以下を提案/確認してください:
- [ ] 殻の範囲を
[failure_cost]から導き、どの操作を殻に含めるかを一覧で示したか - [ ] 核の出力に E4 構造化出力 を併置し、スキーマ検証を設計したか
- [ ]
[accountability]が高いなら E2 Policy-as-Code でポリシー外部化を提案したか - [ ] 副作用のある操作に C1 ツールゲートウェイ を併置したか
- [ ]
[task_variability]が高い部分は B3 との使い分けを説明したか - [ ] 目盛り(上表)の値を
[駆動変数]から導き、理由を添えて提示したか - [ ] 殻と核の境界を型定義(入力/出力スキーマ)として具体化したか