コンテンツにスキップ

Rule Agent Tuning — 更新の仕組み

作成日: 2026-07-07

このドキュメントは tuning が どのファイルを、いつ、どういう仕組みで更新するか を説明する。特に「run 中は codebase READ-ONLY」「apply で初めて params.yaml が上書きされる」という 2 段階運用の背景と仕組みを明らかにする。

関連ドキュメント:

対応実装:

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/&lt;agent&gt;/main.py<br/>agent.py, deck.csv, strategy.yaml"]:::read
    end

    subgraph TEMP["2. 一時ファイル (/tmp、trial ごと生成→削除)"]
        direction TB
        OVERRIDE["/tmp/pca_tune_XXX/&lt;agent_id&gt;/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/&lt;agent&gt;/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

重要な原則:

  1. run 中に codebase が変わることはない。全て /tmp 内で完結、trial終了時に自動削除される
  2. apply を実行して初めて codebase の params.yaml が上書きされる
  3. main.py / strategy.yaml / deck.csv は どのタイミングでも触られない
  4. --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 を書いては削除
  • --output dir に 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 しない理由:

  1. 人間の判断を挟みたい: best.yaml がノイズで偶然 best になった可能性があり、apply 前に人間が top_k.yaml と history.csv を目視で確認する
  2. git commit タイミングを制御したい: apply → commit を明示的に紐づけることで、「どの tuning ラン由来の params か」が commit message で追跡可能
  3. 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 保護の仕組みを体系化。