ルールエージェントの判断履歴¶
作成日: 2026-07-05
Rule-based agent が どの option を、なぜ、どのスコアで選んだか を JSONL に出す仕組み。「壊れている agent の原因特定」「立ち回りが strategy.md 通りか確認」「tuning 効果の可視化」に使う。
対応実装:
src/pca/rule_agents/trace.pysrc/pca/rule_agents/base.pyのTraceEntry/RuleDecision.tracesrc/pca/rule_agents/policy.pyの書き出しフックsrc/pca/training/selfplay/cli_args.pyの CLI 引数src/pca/training/selfplay/cli.pyの writer install/close
関連ドキュメント:
- rule-agent-tuning.md — Tuning 全体設計
- rule-agent-tuning-algorithm.md — TPE アルゴリズム詳細
- rule-agent-tuning-flow.md — Tuning のファイル更新フロー
- rule-agent-config.md — Config loader
目的¶
現在の rule agent は RuleDecision.scores: list[float] を返すだけで、 なぜそのスコアになったか
を後から追跡できない。壊れている agent (例: mega_venusaur_ex_grass_sustain
勝率 0.045) の原因が「回復ループで END_TURN に至らない」なのか「攻撃可能ターンで sustain を優先している」なのかを、trace なしでは特定できない。
trace はこの説明性を提供する。各 decision で:
- どの option を選んだか
- スコアの内訳と reason 文字列
- どの layer (universal / archetype / deck) が寄与したか
- turn goal / turn budget
データフロー¶
flowchart LR
CLI["CLI: --rule-explain N<br/>--rule-explain-output PATH"]:::cli
INS["_install_rule_trace_writer(args)"]:::wire
SW["set_writer(TraceWriter)"]:::wire
AGT["Agent.act(obs)"]:::agent
DEC["RuleDecision (with trace)"]:::val
POL["rule_pool_policy_factory.policy()"]:::policy
GET["get_writer()"]:::wire
W["TraceWriter.record(...)"]:::writer
JSONL["output.jsonl<br/>(one line per decision)"]:::out
CLI --> INS --> SW
AGT --> DEC --> POL --> GET --> W --> JSONL
classDef cli fill:#e3f2fd,stroke:#1976d2,color:#000
classDef wire fill:#fff3e0,stroke:#f57c00,color:#000
classDef agent fill:#f3e5f5,stroke:#7b1fa2,color:#000
classDef val fill:#f1f8e9,stroke:#689f38,color:#000
classDef policy fill:#fff9c4,stroke:#f9a825,color:#000
classDef writer fill:#fce4ec,stroke:#c2185b,color:#000
classDef out fill:#e8f5e9,stroke:#388e3c,color:#000
設計ポイント: writer は プロセス singleton
(trace.py の _active_writer)。selfplay のスタック全体に writer 引数を配線しない。default は
None で、CLI が明示的にセットした時だけ record される。
公開API¶
TraceEntry¶
@dataclass(frozen=True)
class TraceEntry:
option_index: int
score: float
reason: str
layer: str = "deck" # "universal" | "archetype:<name>" | "deck"
tags: tuple[str, ...] = ()
RuleDecision¶
@dataclass(frozen=True)
class RuleDecision:
selected: list[int]
scores: list[float]
target_policy: list[float] | None = None
meta: dict[str, Any] = ...
trace: tuple[TraceEntry, ...] = () # NEW
turn_goal: str | None = None # NEW
turn_budget: dict[str, int] | None = None # NEW
BaseRuleAgent.trace(obs, scores) フックが RuleDecision.trace
を埋める。default は空 tuple なので凍結 agent は影響を受けない。
TraceWriter¶
writer = TraceWriter(output_path, max_games=None) # max_games=None → 無制限
writer.record(
game_id="...",
step_index=0,
agent_id="raging_bolt_ogerpon",
select_context=SelectContext.PLAY_ITEM_OR_SUPPORTER,
decision=rule_decision, # RuleDecision
options=obs.select.option, # 任意 (option の meta サマリを追加保存)
)
writer.close()
- スレッドセーフ (内部
Lock) close()後の record は no-opmax_gamesを超える game_id は 静かにスキップ (無制限に肥大化しない)
プロセス singleton¶
from pca.rule_agents.trace import set_writer, get_writer
set_writer(TraceWriter(...)) # 有効化
get_writer() # 呼び出し側で参照
set_writer(None) # 無効化
コマンドラインからの実行¶
uv run python -m pca.training.selfplay \
--rule-agent-deck-pool \
--rule-agent-config configs/rule_agents.yaml \
--games 20 \
--rule-explain 20 \
--rule-explain-output data/rule_trace/2026-07-05.jsonl
要点:
--rule-explain N— 最大 N 個の distinct game_id を記録--rule-explain-output PATH— 出力先 JSONL (親ディレクトリは自動作成)- N=0 (default) の時は writer をセットしないので 性能影響ゼロ
JSONL 形式¶
一行あたり 1 decision:
{
"game_id": "g001",
"step_index": 12,
"agent_id": "raging_bolt_ogerpon",
"select_context": 3,
"selected": [1],
"scores": [1000.0, 22000.0, 8000.0],
"trace": [
{ "option_index": 0, "score": 1000.0, "reason": "fallback", "layer": "universal", "tags": [] },
{
"option_index": 1,
"score": 22000.0,
"reason": "Akamatsu: no Lightning in hand",
"layer": "deck",
"tags": ["supporter"]
},
{
"option_index": 2,
"score": 8000.0,
"reason": "Boss setup pressure",
"layer": "deck",
"tags": ["supporter"]
}
],
"turn_goal": "SETUP",
"turn_budget": { "supporter_left": 1, "attach_left": 1 },
"option_summaries": [
{ "type": 5, "cardId": 1198, "area": 0, "index": 3 },
{ "type": 5, "cardId": 1182, "area": 0, "index": 5 }
]
}
使い方の典型例¶
1. 壊れている agent の原因特定¶
uv run python -m pca.training.selfplay \
--rule-explain 20 \
--rule-explain-output data/rule_trace/mega_venusaur.jsonl \
... (他の args)
# 「END_TURN が selected になった step」を抽出
jq 'select(.trace[]?.reason | contains("end turn"))' \
data/rule_trace/mega_venusaur.jsonl | head -50
2. tuning 効果の検証¶
チューニング前後で trace を録り、選ばれる option の分布 が変わったか確認。数値だけでは見えない挙動変化を追える。
3. golden fixture の種¶
trace 中の代表 decision をピックし、 tests/rule_agents/golden/<agent>/<scenario>.json に obs
snapshot を保存 → expected.yaml で期待挙動を宣言 (Phase 3)。
設計判断¶
なぜ singleton なのか¶
writer 引数を関数チェーン全体に配線するのは侵襲的。selfplay の中に policy factory があり、その中に act() があり、深いネスト。singleton なら:
- 既存 API を変えない (
.policy(obs) -> PolicyDecisionのシグネチャそのまま) - CLI 側で set/close する 2 行だけで済む
- テストは
set_writer(None)で無効化して他テストと干渉しない
なぜ max_games による cap を用意するか¶
長時間の selfplay で trace を全部書くと JSONL が数百 MB になる。実際に見たいのは最初の数ゲームなので、cap で肥大化を防ぐ。cap 超過の game は静かにスキップ (record はエラーにならない) 挙動にしてある。
なぜ trace は tuple なのか¶
RuleDecisionはfrozen=Trueの dataclass。mutable list を持つと等価性 (hashable ではないが eq) の意味が曖昧になる- tuple の方が生成コストがわずかに軽い
- 呼び出し側で
tuple(entries)するだけ
凍結 agent との後方互換¶
BaseRuleAgent.trace(obs, scores) は default で ()
を返す。凍結 agent の main.py はこのフックを override しないので trace は常に空。writer がセットされていても "空エントリの record" しか書かれない (trace: []
で 1 行、ただし selected と scores は書かれる)。
テスト方針¶
tests/rule_agents/test_trace.py (8 tests):
| テストクラス | 何を確認 |
|---|---|
TraceWriterTest |
JSONL 出力 / max_games cap / close 冪等性 / option summaries |
SingletonTest |
set/get の一貫性、None default |
既知の制約¶
- game_id 抽出の best-effort: CABT の obs 内
gameId/game_id/matchIdを順に見る。どれも無いときNoneになり、max_games cap の計算が「不明ゲームは cap 未達なら記録」というルールで動く - 並列 workers 対応が未検証: プロセス singleton なので
--workers > 1だと各 worker プロセスで 別々の writer が独立に動く。同じ output path を指すと file が競合する。当面は--workers 1前提 - step_index の意味: CABT の
current.stepを使うが、CABT 版の step の意味と食い違う可能性あり (要確認) - trace の中身は agent 依存: 現時点で凍結 agent は空 trace を返す。意味のある trace は redesign 対象 agent (raging_bolt 以降) で埋める
変更履歴¶
- 2026-07-05: 初版。TraceEntry / RuleDecision 拡張、TraceWriter、CLI wire。