Rule Agent Tuning — 更新の仕組み¶
作成日: 2026-07-07
このドキュメントは tuning が どのファイルを、いつ、どういう仕組みで更新するか を説明する。特に「run 中は codebase READ-ONLY」「apply で初めて params.yaml が上書きされる」という 2 段階運用の背景と仕組みを明らかにする。
関連ドキュメント:
- 使い方: rule-agent-tuning-optuna.md
- アルゴリズム詳細: rule-agent-tuning-algorithm.md
- 全体設計: rule-agent-tuning.md
- Config ローダー: rule-agent-config.md
- 採用理由 (ADR): ../decisions/0003-tuning-tpe-vs-grid.md
対応実装:
- src/pca/rule_agents/config.py (環境変数分岐)
- src/pca/rule_agents/tuning/evaluator.py (SelfplayEvaluator)
- src/pca/rule_agents/tuning/override_dir.py (一時 params 生成)
- src/pca/rule_agents/tuning/main.py (
run/apply)
1. 3 種類のファイル群¶
tuning が扱うファイルは 3 グループに分かれる。
flowchart TB
subgraph READ["1. Codebase (読むだけ)"]
direction TB
SPACE["configs/rule_agents/tuning/<agent>_space.yaml"]:::read
BASE["src/pca/rule_agents/agents/<agent>/params.yaml"]:::read
POOL["configs/rule_agents.yaml"]:::read
MAIN["src/pca/rule_agents/agents/<agent>/main.py<br/>agent.py, deck.csv, strategy.yaml"]:::read
end
subgraph TEMP["2. 一時ファイル (/tmp、trial ごと生成→削除)"]
direction TB
OVERRIDE["/tmp/pca_tune_XXX/<agent_id>/params.yaml<br/>= base + overrides"]:::temp
FILTERED["/tmp/pca_tune_XXX/rule_agents.yaml<br/>= pool の subset"]:::temp
SUMMARY["/tmp/pca_tune_XXX/agent-summary.csv<br/>agent-matchup-summary.csv"]:::temp
end
subgraph OUT["3. 出力 (--output dir、永続)"]
direction TB
HIST["history.csv"]:::out
TOPK["top_k.yaml"]:::out
CONF["confirmed.csv<br/>(--confirm-top-k 時)"]:::out
BEST["best.yaml"]:::out
MAN["manifest.json"]:::out
TRIALS["trials.jsonl (Optuna のみ)"]:::out
end
subgraph APPLY["4. apply で更新 (手動、初めて codebase 変更)"]
direction TB
UPDATE["src/pca/rule_agents/agents/<agent>/params.yaml<br/>← best.yaml で上書き"]:::apply
end
SPACE --> OVERRIDE
BASE --> OVERRIDE
POOL --> FILTERED
OVERRIDE -.-> SUMMARY
FILTERED -.-> SUMMARY
MAIN -.-> SUMMARY
SUMMARY --> HIST
SUMMARY --> TOPK
SUMMARY --> CONF
SUMMARY --> BEST
SUMMARY --> MAN
SUMMARY --> TRIALS
BEST -. "手動 apply" .-> UPDATE
classDef read fill:#e3f2fd,stroke:#1976d2,color:#000
classDef temp fill:#fff3e0,stroke:#f57c00,color:#000
classDef out fill:#e8f5e9,stroke:#388e3c,color:#000
classDef apply fill:#fce4ec,stroke:#c2185b,color:#000
重要な原則:
- run 中に codebase が変わることはない。全て
/tmp内で完結、trial終了時に自動削除される applyを実行して初めて codebase の params.yaml が上書きされる- main.py / strategy.yaml / deck.csv は どのタイミングでも触られない
--confirm-top-k有効時、best.yaml は探索 top-1 でなく確認ラン (top-K + baseline を高 games 数で再評価) の winner になる。探索の記録 (history.csv / top_k.yaml) と確定値 (best.yaml) は分離されており、両者のズレは confirmed.csv で確認できる。winner が baseline の場合は stdout に WARNING が出る (apply は no-op になるため)
2. 環境変数注入 mechanism¶
「baseline
params.yaml を触らずに、trial ごとに違う params で agent を動かす」のが tuning の技術的な核。環境変数
PCA_PARAMS_OVERRIDE_DIR と 2 つの Python モジュールの協調で実現する。
2.1 config.py の分岐ロジック¶
# src/pca/rule_agents/config.py
OVERRIDE_DIR_ENV = "PCA_PARAMS_OVERRIDE_DIR"
def _override_dir_for(agent_id: str) -> Path | None:
"""Return <override_root>/<agent_id> if env var is set and that dir exists."""
override_root = os.environ.get(OVERRIDE_DIR_ENV)
if not override_root:
return None
candidate = Path(override_root) / agent_id
return candidate if candidate.is_dir() else None
def load_params_alongside(main_py_file, name="params.yaml"):
agent_dir = Path(main_py_file).resolve().parent
override = _override_dir_for(agent_dir.name)
if override is not None and (override / name).exists():
return load_yaml(override / name) # ← override 版を優先
return load_params(agent_dir, name=name) # ← 通常はこちら
2.2 agent の main.py での呼び出し¶
# src/pca/rule_agents/agents/raging_bolt_ogerpon/main.py
from pca.rule_agents.config import load_params_alongside
PARAMS = load_params_alongside(__file__) # モジュール ロード時に 1 回
# 以降、閾値は PARAMS から取る
OPP_MAX_DAMAGE = {
**_OPP_MAX_DAMAGE_DEFAULTS,
**PARAMS.get("opp_max_damage", {}),
}
_SETUP_ACTIVE_PRIORITY = {
P_OGERPON: (
PARAMS.get("setup.active_priority.energy_engine", 100000),
"..."
),
...
}
2.3 環境変数の有無で PARAMS が別ものになる¶
| 起動条件 | PARAMS の中身 |
|---|---|
| 通常 selfplay (env なし) | src/pca/rule_agents/agents/raging_bolt_ogerpon/params.yaml (baseline) |
| tuning 内の subprocess (env あり) | /tmp/pca_tune_XXX/raging_bolt_ogerpon/params.yaml (baseline + overrides) |
| tuning subprocess で 他の agent (env あるが subdir なし) | 通常の baseline (_override_dir_for が None を返す) |
キーポイント: 同じ main.py が 環境変数だけで異なる挙動をする。これによって:
- tuning 対象 agent には override params を適用
- 同時に走る opponent 側の agent は baseline params のまま (env に subdir なし)
- codebase の params.yaml を書き換えずに実現
3. 1 trial の subprocess フロー¶
Optuna trial 1 個が起きる時の詳細フロー:
sequenceDiagram
autonumber
participant O as Optuna study
participant E as SelfplayEvaluator
participant OD as override_dir helper
participant SP as selfplay subprocess
participant C as config.py
participant AG as agent main.py
O->>E: objective(trial)<br/>overrides = {"supporter_tier.critical": 22000, ...}
E->>E: mkdtemp('/tmp/pca_tune_XXX/')
E->>OD: write_override_params_dir(base, overrides, workdir, agent_id)
OD->>OD: yaml.safe_load(base_params.yaml)
OD->>OD: deep-copy + _set_dotted で overrides 適用
OD->>OD: yaml.safe_dump(merged, workdir/agent_id/params.yaml)
E->>E: _materialise_rule_agent_config: pool を filter して<br/>workdir/rule_agents.yaml を書く
E->>SP: subprocess.run(['python', '-m', 'pca.training.selfplay', ...],<br/>env={'PCA_PARAMS_OVERRIDE_DIR': workdir, ...})
SP->>AG: PortedRuleAgent が main.py を temp モジュールとして exec
AG->>C: load_params_alongside(__file__)
C->>C: os.environ.get('PCA_PARAMS_OVERRIDE_DIR')<br/>= '/tmp/pca_tune_XXX'
C->>C: <override_dir>/raging_bolt_ogerpon/params.yaml 存在確認 → OK
C->>AG: return Config(override version)
Note over AG: PARAMS.get(...) は override 値を返す
SP->>SP: N games 実行
SP->>E: workdir/agent-summary.csv<br/>workdir/agent-matchup-summary.csv 出力
E->>E: _parse_agent_summary で CSV → EvaluationResult
E->>E: finally: shutil.rmtree(workdir)
Note over E: 一時ファイルは完全削除される
E->>O: return EvaluationResult
O->>O: aggregator.objective(result) を Study に記録<br/>TPE 内部モデル更新
各ステップの補足:
- step 4 (deep-copy): base params を破壊せず、コピーに overrides を適用。base params.yaml は絶対に書き換わらない
- step 8 (env): subprocess.run に env dict を明示的に渡すことで、子プロセスの環境変数のみに影響。親プロセスや他 subprocess は影響を受けない
- step 9-13 (env → PARAMS): main.py の 1 行
PARAMS = load_params_alongside(__file__)が全てを繋ぐ - step 16 (rmtree): 例外があっても finally で確実に削除される (
SelfplayEvaluator.evaluateの try/finally)
4. run と apply の違い¶
pca.rule_agents.tuning は 2 つのサブコマンドを持つ:
4.1 run: codebase READ-ONLY¶
uv run python -m pca.rule_agents.tuning run \
--agent raging_bolt_ogerpon \
--stage optuna \
--space ... \
--output data/rule_tuning/raging_bolt/2026-07-07/
やること:
--space/agents/<id>/params.yaml/configs/rule_agents.yamlを 読む/tmpに override / filtered config / summary CSV を書いては削除--outputdir に 4〜5 個のファイルを書き出す
codebase 上の files (src/, configs/) は一切書き換わらない。git status は run 前後で不変。
4.2 apply: params.yaml を上書き¶
uv run python -m pca.rule_agents.tuning apply \
--agent raging_bolt_ogerpon \
--from data/rule_tuning/raging_bolt/2026-07-07/best.yaml \
--dry-run # まず確認
--dry-run で内容確認:
[apply] would copy data/rule_tuning/raging_bolt/2026-07-07/best.yaml
-> src/pca/rule_agents/agents/raging_bolt_ogerpon/params.yaml
問題なければ --dry-run を外して実行:
[apply] wrote src/pca/rule_agents/agents/raging_bolt_ogerpon/params.yaml
これで src/pca/rule_agents/agents/raging_bolt_ogerpon/params.yaml
が best.yaml の中身で完全上書きされる。git diff で確認、commit すれば履歴に残る。
4.3 実装¶
# src/pca/rule_agents/tuning/__main__.py
def _apply_best(args: argparse.Namespace) -> int:
agent_dir = args.agent_dir or _default_agent_dir(args.agent)
target = agent_dir / "params.yaml"
if not args.src.exists():
print(f"source not found: {args.src}", file=sys.stderr)
return 2
if args.dry_run:
print(f"[apply] would copy {args.src} -> {target}")
return 0
target.write_text(args.src.read_text(encoding="utf-8"), encoding="utf-8")
print(f"[apply] wrote {target}")
return 0
shutil.copy2 ではなく read_text/write_text を使っているのは:
- 権限・タイムスタンプを引き継がず、"新しく書いた" ファイルとして扱う
- encoding を明示 (UTF-8 統一)
4.4 なぜ 2 段階か¶
自動 apply しない理由:
- 人間の判断を挟みたい: best.yaml がノイズで偶然 best になった可能性があり、apply 前に人間が top_k.yaml と history.csv を目視で確認する
- git commit タイミングを制御したい: apply → commit を明示的に紐づけることで、「どの tuning ラン由来の params か」が commit message で追跡可能
- regression 検証を挟める: apply 前に best.yaml で 500 games の regression ラン (別 output dir) を回して、baseline より確実に改善しているか検証できる
5. ファイル別 read/write テーブル¶
| ファイル | run で読む | run で書く | run で削除 | apply で書く | 備考 |
|---|---|---|---|---|---|
configs/rule_agents/tuning/<agent>_space.yaml |
✅ | ❌ | ❌ | ❌ | 探索空間 |
src/pca/rule_agents/agents/<agent>/params.yaml |
✅ | ❌ | ❌ | ✅ 上書き | baseline (run では触らない) |
src/pca/rule_agents/agents/<agent>/main.py |
✅ | ❌ | ❌ | ❌ | agent コード、常に不変 |
src/pca/rule_agents/agents/<agent>/strategy.yaml/md |
❌ | ❌ | ❌ | ❌ | tuning は無関係 |
src/pca/rule_agents/agents/<agent>/deck.csv |
✅ | ❌ | ❌ | ❌ | agent 起動時に読む |
configs/rule_agents.yaml (master) |
✅ | ❌ | ❌ | ❌ | pool の filter 元 |
| 凍結 agent の main.py | ✅ | ❌ | ❌ | ❌ | opponent として起動、test_frozen_hash が守る |
| 凍結 agent の params.yaml | ✅ | ❌ | ❌ | ❌ | 存在すれば baseline として読むだけ |
/tmp/pca_tune_XXX/ |
✅ | ✅ | ✅ | ❌ | trial ごと生成→削除 |
<output>/history.csv |
❌ | ✅ | ❌ | ❌ | 全 trial 記録 |
<output>/top_k.yaml |
❌ | ✅ | ❌ | ❌ | 上位 K の overrides |
<output>/confirmed.csv |
❌ | ✅ | ❌ | ❌ | 確認ランの記録 (--confirm-top-k 時) |
<output>/best.yaml |
❌ | ✅ | ❌ | ✅ (読む) | apply の入力 (確認ラン時は confirm winner) |
<output>/manifest.json |
❌ | ✅ | ❌ | ❌ | 再現用メタ |
<output>/trials.jsonl |
❌ | ✅ (追記) | ❌ | ❌ | trial ごと 1 行追加 (Optuna) |
セマンティクス:
- run: codebase READ-ONLY、出力 dir に集約
- apply: 明示的な指示に応じて 1 ファイルだけ更新
- どちらも凍結 agent の main.py には触らない
6. 凍結 agent 保護の仕組み¶
docs/research/2026-07-05-rule-agents-redesign.md の凍結ポリシーに従い、 notebook 由来 agent の
main.py は tuning でも改変されない。仕組み:
6.1 SelfplayEvaluator は tuning 対象しか override しない¶
# src/pca/rule_agents/tuning/evaluator.py
def evaluate(self, overrides, opponents, ...):
...
write_override_params_dir(
base_params_path=self.base_params_path,
overrides=overrides,
dest_dir=workdir,
agent_id=self.agent_id, # ← ★ tuning target のみ
)
...
workdir/<agent_id>/params.yaml のみ作成される。opponent 側の agent が
load_params_alongside(__file__) を呼んでも、_override_dir_for(opponent_id) は None
を返し、baseline の params.yaml が採用される。
6.2 test_frozen_hash が hash lock¶
tests/rule_agents/test_frozen_hash.py
が全 notebook 由来 agent の main.py の SHA-256 を lock。CI で 1 バイトでも変わったら赤くなる。
6.3 apply は tuning target のみ¶
target = agent_dir / "params.yaml" # tuning target の params.yaml のみ
target.write_text(args.src.read_text(...))
opponent agent や凍結 agent の params.yaml は apply でも書き換わらない。
6.4 tuning 対象は「凍結でない agent」だけを想定¶
凍結 agent を --agent に渡した場合の挙動:
runは動く (params.yaml があるなら)。ただし挙動改善は期待できない (main.py 側で PARAMS を参照していないため)applyは params.yaml を上書きするが、main.py の hash lock に影響しない
推奨: 凍結 agent は tuning 対象にしない。派生 agent を新規作成してそちらを tuning する。
7. ロールバック手順¶
apply 後に問題が発覚したときの戻し方。
7.1 git commit 前ならば¶
git checkout -- src/pca/rule_agents/agents/ < agent > /params.yaml
または --dry-run を必ず挟む運用にしておけば、apply 前に気づく確率が高い。
7.2 commit 後ならば¶
git log src/pca/rule_agents/agents/ < agent > /params.yaml # 過去 commit を確認
git checkout src/pca/rule_agents/agents/ < previous_commit > -- < agent > /params.yaml
git commit -m "revert: rollback <agent> params to <previous_commit>"
7.3 過去の best.yaml を再 apply¶
data/rule_tuning/
は git 管理外だが (.gitignore)、明示的に残しておけば過去 best を再 apply できる:
uv run python -m pca.rule_agents.tuning apply \
--agent \
data/rule_tuning/ < agent > --from < agent > / < yyyy-mm-dd > /best.yaml
変更履歴¶
- 2026-07-07: 初版。3 種類のファイル群、環境変数注入、run vs apply、凍結 agent 保護の仕組みを体系化。