10月7日、Stack Overflowが「Part 1: Make your AI agents boring: the determinism layer」と題した記事を公開した。この記事では、LLMエージェントを本番環境で安全に動かすために「決定論レイヤー」を設計し、モデルを単一ノードに封じ込める実装手法について詳しく紹介されている。
「AIエージェントを退屈にせよ」という設計思想
AIエージェントのデモは派手だ。ツールを呼び出し、ループし、自律的に「なんとかする」。だが本番環境はそれを嫌う。
モデルの挙動がその日の推論パスに依存する限り、テストできず、監査できず、重要な処理には触れさせられない。金融、医療、インフラといった規制のある領域では「だいたい動く」は許容されない。
この記事はLLMシステムを本番投入するための6段階の成熟度モデルのLevel 1、すなわち「決定論レイヤー」を扱う。同モデルは以下の6段階で構成される:Level 1「決定論レイヤー」(本記事)、Level 2「評価(eval)レイヤー」、Level 3「信頼度ルーティング」、Level 4「フィードバックループ」、Level 5「監査・説明可能性」、Level 6「自律的な継続改善」。evalや信頼度ルーティングを実装する前に、まずこの土台を固める必要がある。
核心となるアイデアは一つ:モデルを単一ノードに封じ込め、それ以外をすべて通常のテスト可能なコードにする。
最重要ルール:エージェントは純粋関数である
記事が最初に提示するのは、エージェントの役割定義だ。
エージェントは「提案」を返す純粋関数であり、世界を変える権限を持たない。
@dataclass(frozen=True)
class Proposal:
decision_id: str
capability: str
action: dict # 提案された変更 — まだ適用されていない
confidence: float
routing: Literal["auto", "hitl_recommended", "hitl_required", "reject"]
reasoning: list[str]
evidence: list[dict]
class Agent(Protocol):
def propose(self, ctx: "Context") -> Proposal: ... # 副作用なし
class Substrate(Protocol):
def apply(self, proposal: Proposal, approval: "Approval") -> "Effect": ... # 唯一の変更者
propose()が純粋関数であることで、テストは単純になる:
def test_propose_is_pure():
agent = ClassifyAgent(gateway=FakeGateway(scripted))
assert agent.propose(ctx) == agent.propose(ctx) # 同じコンテキスト → 同じ提案
この境界が三つの性質をもたらす:
- テスト可能:
propose()は純粋関数なので、世界をモックせずにロジックをテストできる - 安全:バグやジェイルブレイクされたエージェントは悪い「提案」を生むだけで、悪い「アクション」は起こせない。被害はガードレールか人間の拒否で止まる
- 合成可能:エージェントが別のエージェントを呼ばない。処理はsubstrate(案件テーブル+スケジューラ等)を通して流れるので、隠れた副作用の連鎖が生まれない
これにより「AIが説明できない何かをした」が「AIが何かを提案し、誰がそれを承認したかが明確に残る」に変わる。
自由なReActループを固定グラフに置き換える
ReAct(Reasoning + Acting)とは、モデルが「思考→行動→観察」を繰り返しながら次のステップを自ら決定するエージェント設計パターンだ。プロトタイプや探索用途では強力だが、本番環境では「次に何が起きるか事前に分からない」という性質が致命的になる。実行パスがモデルの推論によって毎回変わるため、テストが書けず、レイテンシの上限も定まらず、監査証跡を事後に再構築しなければならない。規制当局や社内コンプライアンス部門が求める「なぜその決定をしたか」の説明が、構造上困難になるのだ。
代わりに、各ケイパビリティを固定ノード列としてモデル化する。
entry → load context → reason (LLM) → output guardrail → verify →
judge (sampled) → compose confidence → route → prepare proposal → record → exit
| 自由形式のReActループ | 固定グラフ(この手法) | |
|---|---|---|
| 制御フロー | モデルが次のステップを決定 | 事前に既知 |
| テスト性 | 困難(パスが変動) | 各ノードを独立でテスト可能 |
| レイテンシ/コスト | 上限なし | 有界・予測可能 |
| 監査 | トレースから再構築が必要 | 毎回均一な行 |
グラフは12ノードで構成され、大半は全ケイパビリティで共有される。新しいケイパビリティを追加するには約4ノードを実装するだけでよい。
1 entry decision_id生成、テナントとIDのバインド [共有]
2 pre_check 入力検証、参照解決、早期リジェクト [ケイパビリティ固有]
3 context_load この決定に必要なコンテキストのみ取得 [共有]
4 llm_decision 推論ステップ — 構造化入力・構造化出力 [ケイパビリティ固有]
5 output_guardrail モデル出力のPII/ポリシースクラブ [共有]
6 verification 決定論的な正確性チェック [ケイパビリティ固有]
7 judge 高リスク時のサンプリング済み二番手レビュー [共有]
8 confidence_compose シグナルからのスコア合成 [共有]
9 routing auto / hitl / reject の振り分け [共有]
10 prepare_proposal 最終提案の成形 [ケイパビリティ固有]
11 memory_write エージェント自身の監査メモリへの書き込み [共有]
12 exit 不変のレジャーエントリを追記 [共有]
モデルの出力を構造化する:自由記述を排除する
LLMの脆弱性の多くは一つの選択から来る。モデルに自由記述を返させ、それをパースしようとすることだ。
「了解です!おそらくApproveだと思いますが、Escalateの可能性も……」——こうなると、文字列からApproveを抽出するNLU問題を抱えることになり、明日のモデルの言い回し変化でregexが壊れる。
解決策は単純だ:モデルに検証済み構造体を出力させ、それ以外は失敗としてリトライする。
from pydantic import BaseModel
from typing import Literal
class Decision(BaseModel):
decision: Literal["approve", "escalate", "reject"] # enumそのものがガードレール
confidence: float
reasons: list[str]
def decide(base_prompt, model, max_retries=2) -> Decision:
prompt = base_prompt
for _ in range(max_retries + 1):
raw = model.generate(prompt, schema=Decision.model_json_schema())
try:
return Decision.model_validate_json(raw)
except ValidationError as e:
prompt = f"{base_prompt}\n\nYour previous output was invalid: {e}. Return JSON only."
raise NonConformingOutput() # 失敗時はクローズ — 推測を下流に渡さない
ただし、構造化は形式を制約するものであり、正確性を保証しない点は明記されている。{"decision":"approve","confidence":0.99}が完全に間違っている可能性はある。構造化出力はパース失敗モードを除去するが、判断失敗モードは除去しない。その上にevalや検証レイヤーが必要になる。
ルーティングと不変の監査台帳
信頼度スコアは4つのパスに変換される:
def route(confidence: float, verified: bool, T: float = 0.85) -> str:
if not verified: return "reject"
if confidence >= T: return "auto"
if confidence >= T - 0.2: return "hitl_recommended"
return "hitl_required"
閾値Tはケイパビリティごとの設定値であり、定数ではない。記事は「最初は保守的に——すべてを人間へ——設定し、データが安全性を証明した範囲でのみ下げよ」と推奨する。
すべての決定は不変の一行として監査台帳に記録される:
CREATE TABLE decision_ledger (
decision_id TEXT PRIMARY KEY,
inputs_hash TEXT NOT NULL, -- 生の機密データではなくハッシュ
outcome TEXT, -- 後から新しい行として追記、UPDATEしない
supersedes TEXT REFERENCES decision_ledger(decision_id),
prev_hash TEXT -- 改ざん証跡のためのハッシュチェーン
);
-- UPDATE/DELETEの権限を剥奪。修正は上書きではなく新行で行う。
UPDATEできる台帳は監査証跡ではない。
この設計の要点は、モデルの「賢さ」を一ノードに閉じ込め、残りをすべて普通のソフトウェアとして扱うことだ。予測不能性を排除しつつ、判断が必要な箇所にのみモデルの能力を使う。
詳細はPart 1: Make your AI agents boring: the determinism layerを参照していただきたい。




