ルールブックをWorkflowに食わせて、決定論的なlintを生成する——LLM校正の確率性を2レイヤーで畳む
あるドメインのルールブックをdynamic workflowに食わせて構造化し、それをregex lint + LLM verifierの2レイヤーに落とす。LLM一発校正で200万トークン・毎回違う指摘という確率的な状態から、56倍トークン削減・10倍高速・候補集合は完全決定論まで持っていった途中経過。ground truth pytestで正当性を客観保証するメカニズムも含めて、設計の判断と失敗を時系列で記録する。
ルールブックがある領域は多い。法令、業界規範、社内手順、コーディング規約、用字用語。これを Claude Code の dynamic workflow に食わせて構造化し、そのまま実行可能な lint に落とす——という設計を1セッションで通した。
最初に LLM だけで校正する素朴な実装をして、決定論性に欠けることに気付き、regex lint + LLM verifier の2レイヤーに組み直した。トークンは約56倍削減、時間は10倍速くなり、検出候補集合は完全に決定論的になった。各検出の正当性は、ルールブック自身の例文を ground truth とした pytest で客観的に保証されている。
題材としたドメインや書名は伏せる(手元の蔵書を私的な学習用途で扱った話で、組織所属の場合は出版元・所属の許可が要る話だ)。同型のパイプラインは法務・医療・金融・製造・出版・公務、どこのドメインのルールブックでも当てはまる。ショーケースとして抽象化したまま記録する。
技術評論社『Claude Codeで学ぶ Agent Skills入門』が出る同じ週の作業だ。あの本が「スキルを使う・作る」の地図なら、この記事はスキルを LLM に作らせて、その正当性を客観検証する応用編の途中経過にあたる。
1. ルールブックを構造化する——著作権と社内文書の取り扱い
最初の工程は、書籍 PDF 約480ページから個別ルールを構造化された YAML に落とすこと。目次から各ルールの開始ページを取り、page スライスをエージェントに渡して、項目タイトル・規則の要約・OK 例・NG 例・例外・関連 ID を構造化スキーマで返してもらう。
ここで Workflow を使う判断が要った。1ルール=1エージェントで並列に走らせる。160ルール並列、cap で queue されるが10〜15分で完了する。書き起こしのプロンプトは「rule 番号 N の解説をこのページ範囲から読み取って、決まったスキーマで返せ」だけ。
技術的な注記としてもう1つ。出版物や社内規程をエージェントに食わせるのは、個人の学習用途と組織の業務利用で扱いがまったく違う。前者は私的複製の範囲に収まる場合があるが、後者は権利者の許可がほぼ必要になる。本稿はあくまで前者の話で、Workflow の組み方そのものを記録するために抽象化している。自分の手元の本ならやってよいことが、組織の本では一気にやってはいけない——この境界は最初に確認すべきだ。
ルールブックを構造化した結果として、約160ルール+付録6本+約914パターンの YAML が手元に残った。これがこの後のすべての出発点になる。
2. ショーケースとして社内フローを1つ通す
ルールブックがあるだけでは何にもならない。「このルールブックに照らして、入力文書を校正する」社内フローを1本通すのが次の工程だ。題材は適当な業務文書を1本選んで、docx → 平文 → 文単位 ID 付与 → チャンク分割 → 並列校正 → 集約 → 提示、を通す。
エージェント設計の起点として明確にしたのは、「実行までさせない、人間承認を残す」こと。校正は提示で止める。自動修正はしない。pre-commit や CI に乗せても検出のみ・置換しない。これは agent システムの導入初期の規範として、被害ゼロから始めるための線引きだ。失敗しても誰も困らない位置に置くから、安心して実証を積める。
最初の実装は、素朴に LLM だけで校正した。チャンク × レンズ並列で、6カテゴリのレンズエージェントが本書の該当ルール群を Read して、入力文書から違反を抽出する——という設計。動くは動いた。が、これに大きな問題が3つあった。
3. スキルとWorkflowの責務分離——並列はWorkflowの内側
エージェント設計の責務をどう切るか、最初の論点はここだった。CLI ラッパは何を持ち、スキルは何を持ち、Workflow は何を持つか。
整理した結果はこうなる。
- CLI ラッパ (Bash): docx → text 変換、
claude -p呼び出し、出力整形、/tmp掃除 - スキル (
SKILL.md): 内部手順(前処理 → Workflow 起動 → 集約 → 提示)の指示書 - Workflow (.js): 並列実行エンジン(N チャンク × M レンズ並列、集約、dedup、sort)
- lint パッケージ (Python): 後で出てくる regex 適用レイヤー
- rules/*.yaml: 構造化ルール DB(凍結)
ここで一度、「Workflow をアトミックにして、スキル側で並列をオーケストレーションする」案を考えた。Workflow を「1チャンク1校正」みたいな最小単位にして、スキルがそれを N 回呼ぶ形だ。これは却下した。理由は2つ。
ひとつ、Workflow の存在意義はそもそも「複数エージェントを deterministic に並列実行する」ことにある。並列度の制御を外に出すと、Workflow を使う意味が薄まる。
ふたつ、スキル ≒ Claude のメインループは1本しかない。複数 Workflow を順次起動して待ち合わせると、本質的に直列になる。スキルは I/O アダプタ、Workflow は並列実行エンジン、と割り切るのが筋がいい。
dynamic workflow の中で、チャンクとレンズの直積を1つの parallel に展開し、その内側で集約・重複除去・ソートまでやって、最終的な findings 配列だけスキルに返す。スキルはそれを表に整形して人間に見せる。これで責務はきれいに分かれた。
4. LLM一発校正は決定論的じゃない
責務分離が決まって、6レンズ並列の素朴な実装で1セッション走らせた。検出7件。「悪くない」と思った。
別セッションで同じ文書を同じ Workflow に通したら、検出が違った。前回は出た指摘が出ず、出なかった指摘が出る。両方とも妥当な指摘だ。LLM の sampling が確率的なので、見落としの場所が run ごとに変わる。同じ文書に対して結果が再現しないツールは、実運用に出せない。
数値で言うと、この素朴な実装は1文書あたり約200万トークン、約2分、検出件数は run ごとに4〜7件で揺れる状態だった。校正観点の網羅性は出ているが、決定論性が皆無なので「同じ入力 → 同じ出力」が成立しない。pre-commit や CI に組み込むこと自体が成り立たない。
ここで2つの方向性があった。
ひとつ、multiple-runs で union を取る。3回走らせて全部マージすれば取りこぼしは減る。が、これは決定論性そのものを解決していない。再現性は依然としてない。
ふたつ、LLM を呼ぶ前に regex で候補集合を確定させる。決定論的な lint で候補を絞り、LLM は「絞られた候補が文脈で本当に違反か」を yes/no で判定するだけにする。後者を選んだ。
5. regex pre-filter + LLM verifier——2レイヤーで畳む
Layer 1 と Layer 2 に分ける。
入力テキスト
↓
Layer 1: Python regex lint(決定論、トークンゼロ)
├ 確定指摘(規範違反が文脈なしで決まるもの)
└ verify 候補(regex で当たるが、文脈で OK/NG が分岐するもの)
↓
Layer 2: LLM verifier(候補のみを1回のバッチ判定)
├ violate(確定)
└ ok(誤検出だった)
↓
集約 → 提示(人間承認)
Layer 1 は完全に決定論的だ。同じテキストに同じ regex を当てるだけなので、同じ入力なら必ず同じ候補集合が出る。トークンは消費しない。
Layer 2 は LLM を使うが、役割が「open-ended な検出」から「狭い yes/no 判定」に変わっている。候補は決まっていて、文脈で許容範囲か違反かを1bitで返すだけ。temperature 影響が大きく減って、同じ候補に対する判定はほぼ安定する。実装上は、候補リストと元文書を Workflow スクリプトに埋め込んで1エージェントでバッチ verify した。
数値はこうなった。
| 観点 | 素朴な LLM 校正 | 2レイヤー(lint + verify) |
|---|---|---|
| トークン | 約200万 | 約36K |
| 時間 | 約2分 | 約11秒(lint 0.2秒 + verify 11秒) |
| 検出候補 | run ごとに揺れる | 完全に決定論 |
| 同じ文書での再現性 | なし | あり(verify の判断のみ薄く揺れる) |
約56倍トークン削減、10倍高速、決定論性確保。同じ品質を、確率性なしで出せる状態になった。
6. ルールはregexで書けるのか——rubricを明文化する
ここで前のめりにやらず、立ち止まる必要があった。「914パターンの regex は誰が正しいと保証したのか」という問題だ。
最初に Workflow に「各ルールに detect_patterns を生成してくれ」と頼んだとき、各エージェントが個別に「これは regex で書ける/書けない」を判断した。判定基準は明文化されていない。同じルールを別 run で評価すれば、また違う分類になりうる。生成過程が確率的なので、914パターン自体が信頼できる前提に立っていない。
これに対して判定基準(rubric)を明文化した。各ルールに対して以下の9要件に boolean で yes/no を埋める。
A. lint_only(regex のみで判定可)
A-1: 違反パターンを具体文字列で記述できる
A-2: 例外を exceptions_regex で完全列挙可能
A-3: 構文的に判定が確定する
→ A-1 ∧ A-2 ∧ A-3 ならば A
B. lint_with_llm_verify(regex で候補抽出、文脈で最終判定)
B-1: 目的語・主語・前後文脈で OK/NG が分岐
B-2: 慣用例外が多くホワイトリスト化困難
B-3: 同一 regex が文脈で OK/NG 反転
→ A でなく、B-* のいずれかなら B
C. llm_required(regex 不適、LLM のみ)
D. not_implementable(機械化困難)
エージェントには「直感で分類しろ」ではなく「rubric の9要件を順に check して、適用順序に従って分類しろ。各 yes/no の根拠を justification に書け」と指示する。
これで個別判断のばらつきは大きく減る。結果は A=30 / B=128 / C=2 / D=0 になった。各分類に「A-1 は yes(理由)、A-2 は yes(理由)、A-3 は yes(理由)、よって A」のような justification が必ず付いてくる。
この rubric 自体も人間が決めた規約だから、誰かが他の rubric を立てれば違う分類になりうる。「LLM の直感」から「人間が明文化した規約への LLM 適用」に評価レイヤーを1つ上げた、という性質の改善だ。完全な決定論にはなっていないが、判定の根拠を辿れる状態にはなった。
7. 正当性は誰が保証するか——ground truthを本書から引く
rubric で分類しても、まだ問題がある。A 判定の各ルールが、本当に決定論的に違反を捕まえているかは別の話だ。regex が examples_ng を1件も検出していなかったり、examples_ok を誤検出したりしているかもしれない。
ここで救いになったのは、ルールブック自身が規範ごとに「OK 例」「NG 例」を提示していることだった。本書がそうしている以上、それを ground truth として pytest fixture にすればよい。
@pytest.mark.parametrize("rule_id,ng_example", A_NG_CASES)
def test_A_ng_detected(rule_id, ng_example):
"""A 判定 rule の NG 例 → lint で検出される(取りこぼしテスト)"""
findings = lint_text(ng_example, [rule])
assert findings, f"{rule_id}: NG 例 {ng_example!r} が検出されない"
@pytest.mark.parametrize("rule_id,ok_example", A_OK_CASES)
def test_A_ok_not_detected(rule_id, ok_example):
"""A 判定 rule の OK 例 → lint で誤検出されない(過剰検出テスト)"""
findings = lint_text(ok_example, [rule])
assert not findings, f"{rule_id}: OK 例 {ok_example!r} が誤検出"全 A ルールに対して examples_ng と examples_ok を当てる。最初は2208ケース中6件 fail。1件ずつ regex を補強して、最終的に2208 pass / 0 failまで持っていった。
このとき副次的に大きい発見が1つあった。失敗した1件で、Workflow が生成した exceptions_regex に「真の検出ターゲットを例外として除外してしまうロジックミス」が混入していた。具体的には「ターゲット語 + 直後がひらがな」を例外指定していて、ターゲット語の典型用法をことごとくスキップしていた。
これは人間が yaml を眺めても見つけにくいバグだった。examples テストで初めて気付ける性質のもの。Workflow に何かを作らせたら、その何かが本当に意図通り動いているかを、ルールブック自身の例文で機械的に検証する仕組みが要る——という教訓だ。
正当性の保証メカニズムが、初めて自己完結したループとして動くようになった。
1. ルール追加・修正
2. examples_ng / examples_ok をルールブックから記入
3. detect_patterns を作る(LLM or 人間)
4. pytest 実行
├ 失敗 → regex 補強 → 3 へ戻る
└ 全 pass → 客観的に正当性 OK
5. git commit(yaml と patterns が同期)
8. 残課題と「実証はまだまだこれから」
ここまでで2レイヤーは動き、A判定30ルールは ground truth テストに完全合格した。が、実証はまったく足りていない。
ひとつ、B判定128ルールの verify 経路は本走未検証。lint 単独で false positive が出すぎていないか、verify で正しく弾けるかは、もっと多様な文書で当てて確認する必要がある。
ふたつ、pattern 単位での A 昇格。ルール全体は文脈判定要だが、特定の NG 例文字列パターンは決定論的——というケースが結構ある。今は rule 単位でしか分類していないので、pattern 単位の rubric 適用に拡張する余地がある。
みっつ、自動修正はまだ早い。仕組み上は auto_apply=true のパターンを置換適用できるが、当面はオフ固定にしている。十分なドメイン文書で「提案を出して人間が採用率を判断する」期間を経てから、置換モードを開放する。
そして、同型タスクへの展開。法務文書のドメイン規範、医療記録の記載ガイドライン、コーディング規約、社内マニュアル——どれも構造は同じだ。「ルールブック → 構造化 → rubric 分類 → 2レイヤー lint → ground truth テスト」のパイプラインは、対象ドメインを差し替えても通る。同じテンプレートで複数ドメインに展開していくのが、本稿のアーキテクチャの実証になる。
終わりに
LLM の確率性そのものを否定する必要はない。確率的な検出は強い。が、「確率的に検出する」と「確率的に判定する」は分けられる。決定論的に候補を出して、確率的に文脈判定する。それだけで運用上の決定論性は取り戻せて、トークンは桁で削れる。
Agent Skills を使う・作るのが入門だとしたら、スキルを LLM に作らせて、その正当性を客観検証するのが次の段階だと思う。スキルを生成するメタスキル、Workflow を生成するメタ Workflow、rubric を生成するメタ rubric——どこかで決定論的な保証メカニズムを差し込まないと、生成物の信頼が積み上がらない。本稿で組んだ ground truth pytest は、その最小単位だ。
実証はこれからだ。自動実行をさせない位置から始めて、人間承認のループを残しながら、徐々に置換モードを開放していく。失敗しても被害ゼロの場所から始める、という規範が、エージェント基盤を実運用に乗せる初手として一番強いと信じている。