
こんにちは。EMの本田です。
AIで開発していますか? 世間では「AIで開発生産性が〇〇倍!」という景気の良い話が飛び交っていますが、現場のエンジニアとしては「いきなりそこを目指すと、逆に管理コストで疲弊しそうだな」と感じることも多いのではないでしょうか。
私のチームでもAI活用を進めていますが、「生産性の定量化」までは今のところ落とし込んでいません。
まずは、「人間がビジネスのコア(価値・設計)に集中するために、それ以外をAIとツールに任せる」というスタンスで、地に足のついた状態を目指しました。
今回は、RustとClaude Codeを中心に、私のチームで実践しているAI活用方法について紹介します。
AIに夢を見すぎない:役割分担の再定義
まず前提として、「すべてのコンテキストをAIに伝えることはできない」ということを受け入れるところから始めました。
仕様判断やアーキテクチャの核心部分をAIに丸投げするのは、現時点では時期尚早だと考えています。まず最初にAIに期待すべきは「自律的な思考」よりも「ルールの徹底」や「定型作業の遂行」です。
- 人間がやること: ビジネスロジックの設計、仕様の決定、最終的な品質責任。
- AIがやること: ボイラープレートの記述、Git操作(Issue/PR作成等)、一次レビュー。
- ツールがやること: フォーマット整形、静的解析、セキュリティチェック(CI/CD)。
失敗談:コンテキストの詰め込みすぎ問題
当初は「機能実装からGitのコミット、プッシュまで全部一つの流れでAIにやってもらおう」と考えたことがありました。しかし、これはうまくいかないことがよくありました。
実装の詳細、プロジェクトのルール、Gitの操作手順...これらすべてを一つのコンテキスト(チャットセッション)で処理しようとすると、コンテキストウィンドウが不足したり、AIの注意力が散漫になって出力品質が安定しなかったのです。
そこで私は「作業単位でコンテキストを区切ることができる」方針で環境設計をしました。
- 設計は設計のセッション。
- 実装は実装のセッション。
- コミットやPR作成は別のセッション(あるいはカスタムコマンド)。
結果として、各工程での成果物が明確になり、人間がチェック(レビュー)を入れるタイミングも作りやすくなりました。「認知負荷を作業単位にとどめておける」というのは、人間にとっても管理しやすいメリットがありました。
Claude Code × Git:定型作業の半自動化
現在、Issue作成、ブランチ作成、コミット、PR作成といったGit操作は、基本的にAI(Claude Code)を経由して行っています。
ただし、毎回プロンプトを手打ちするのではなく、プロジェクトのルールを反映した カスタムコマンドを用意しています。
create-issue, create-branch, create-commit, create-pr といったコマンドを用意し、必要なパラメータ(例: 機能名、変更内容)を渡すだけで、AIがルールに従って適切なIssueやPRを作成してくれます。
さらに、以下のようなルールを事前にドキュメント化し、AIが参照するようにカスタムコマンドの中で指示しています。
- Issue/PRテンプレート: フォーマットを統一。
- 命名規則: ブランチ名やコミットメッセージの一貫性をAIに強制させる。
人間は方向性を指示するだけで、AIがルールに従って定型作業をこなします。
AIを戦力化する「自動フィードバック」
ここで重要になるのが「AIが出力したコードを誰がチェックするか」という問題です。
AIは疲れを知らず、爆速でコードを生成しますが、その品質は完璧ではありません。インデントがズレたり、存在しない関数を呼んだり、プロジェクトの禁止事項を無視したりします。 これらをすべて人間がレビューして指摘していたら、AIを使う前よりも時間がかかってしまいます。
私たちが目指したのは、「人間が見る前に、システムがAIにダメ出しをする環境」です。
- 構文・フォーマット: 汚いコードは自動で整形されるか、エラーになる。
- 整合性: 動かないコード(型エラー等)は弾かれる。
- 設計: 不適切な依存関係は禁止される。
- 品質: 複雑すぎるコードは禁止される。
- セキュリティ: 脆弱性のあるライブラリは禁止される。怪しいソースから取ってきているライブラリは禁止される。
- ライセンス: 許可されていないライセンスの使用は禁止される。
この「ガードレール」が整備されていると、AIはエラーメッセージを読み取って自律的に修正を行うことができます。 「AIが書き、システムが指摘し、AIが直す」。このループを人間抜きで回せる環境こそが、AI開発において最も生産性が高く、ストレスのない状態だと言えます。
その実現になぜ「Rust」が最適なのか
この「システムによる厳格なフィードバック環境」を構築する上で、Rustは私たちのチームにとって非常にコストパフォーマンスが良い選択肢でした。もちろん、TypeScriptでも厳格な型チェックやESLint設定で近いことは実現できますが、Rustはそれが「デフォルト」である点が大きな違いです。
「コンパイルエラー」という最強のフィードバック
PythonやJavaScriptのような動的型付け言語では、AIが「なんとなく動く嘘のコード」を書いても、実行するまで気づかないことがあります。 しかしRustなら、型システムや所有権のルールに違反していれば即座にコンパイルエラーになります。
AIにとって、コンパイラからの明確なエラーメッセージは「最高の修正指示書」です。 人間が「ここ間違ってるよ」と教える代わりに、「コンパイルを通せ」と指示するだけで、AIは試行錯誤して正しいコードに到達できます。
公式ツールによる「迷いのない」環境構築
Rustは、ビルド(cargo build)、テスト(cargo test)、フォーマット(cargo fmt)、ドキュメント(cargo doc)、Linter(cargo clippy)といった開発に必要なツールがすべて公式で標準化されています。
JS/TS等でありがちな「Linterは何を使う?」「設定ファイルはどう書く?」といった環境構築の試行錯誤が一切ありません。
rustup でインストールさえすれば、人間もAIもすぐに開発に入れます。
AIに対して「標準のフォーマッターをかけて」と指示すれば、文脈を説明しなくても100%正解の挙動をしてくれるのは、プロンプトの簡略化にも繋がります。
明示的な構文:AIが「読みやすく書きやすい」
Rustは型、ライフタイム、可変性(mut)、エラーハンドリング(Result)などが構文上で明示されます。
暗黙の挙動が少ないため、AIはコードの意図を正確に読み取りやすく、生成時も「何を書くべきか」が明確です。
例えば「この関数は失敗する可能性がある」という情報が戻り値の型(Result<T, E>)に表れているため、AIはエラーハンドリングを忘れずに書いてくれます。
コンパイルの厳密性
AIが「なんとなく動く嘘のコード」を書いても、Rustの厳格な型システムと所有権ルールが即座にコンパイルエラーとして弾いてくれます。 「コンパイルが通るように直して」と指示するだけで、ある程度の品質が保証される点は、動的型付け言語にはない安心感です。
アーキテクチャによる「物理的」なガードレール
Rustの強力な型システムに加え、アーキテクチャ構成そのものもAI運用のためのガードレールとして活用しています。具体的には、クリーンアーキテクチャを実践し、各レイヤーをRustのワークスペース機能を使った別クレート(モノレポ構成)に分割しています。
依存関係の「物理的な」強制
単一のディレクトリ内でフォルダ分けするだけでは、AI(や人間)がついうっかり上位レイヤーから下位レイヤー(例:ドメイン層からインフラ層)をimportしてしまうミスを防げません。
しかし、レイヤーごとにクレート(ライブラリ)を分けてしまえば、Cargo.toml に依存関係を記述しない限りコードを利用できません。
. ├── Cargo.toml (workspace) ├── crates │ ├── kernel # ドメイン層。依存なし │ ├── app # アプリケーション層。kernelにのみ依存 │ ├── xxx-adapter # インフラ層。app,kernelに依存。 │ └── driver # 最も外側の層。すべての層に依存。
このように構成することで、「ドメイン層がDB操作(インフラ層)に依存する」といったアーキテクチャ違反は、論理的以前に物理的に(コンパイルレベルで)不可能になります。 AIが間違った依存関係のコードを書いても、ビルドが通らないため、AIは自律的に「あ、この構成は許されないんだ」と気づき、正しいインターフェース経由の実装に修正します。
AIとの共通言語としての「定石」
クリーンアーキテクチャは世界中で広く知られたパターンであり、AI(LLM)の学習データにも大量に含まれています。
独自のオレオレ・アーキテクチャだと、AIに毎回コンテキスト説明が必要になりますが、クリーンアーキテクチャであれば、比較的雑な指示でも、AIは意図をある程度正確に汲み取ってくれます。
「AIにとって設計しやすい(予測しやすい)構造」を採用することは、結果としてプロンプトエンジニアリングのコストを下げることにも繋がります。
PostToolUseフックによるフォーマット強制
Claude Codeの設定で .claude/settings.json に PostToolUse フックを仕込んでおくと、AIがファイルを編集した直後に自動コマンドを実行できます。
Rustの場合、ここでフォーマッターを走らせるのが効果的です。以下は自動フォーマットの設定例です。
"hooks": { "PostToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "file_path=$(jq -r '.tool_input.file_path'); if [ -n \"$file_path\" ] && [ -f \"$file_path\" ] && echo \"$file_path\" | grep -q '\\.rs$'; then cargo fmt -- \"$file_path\" 2>&1 || true; fi" } ] } ] }
これにより、AIが書いたコードは常にプロジェクトのスタイルガイドに沿った状態に保たれます。
CI/CDによる品質ゲート:人間が楽をするための厳格化
「AIにとって開発しやすい環境 = 人間にとっても開発しやすい環境」です。 人間がやる必要性の薄いチェックは、CI/CDで徹底的に自動化しています。
品質ゲート
以下のコマンド群が通らない限り、マージはできません。
cargo check(コンパイル)cargo fmt --check(フォーマット確認)cargo clippy -- -D warnings(Linterによる静的解析)- 単純なLintだけでなく、コードの複雑さを制限するルールを厳しめに設定しています。
clippy::cognitive_complexity: 認知的複雑度が一定以上のコードを禁止。clippy::too_many_lines: 1つの関数が長すぎる場合に警告。
- AIは文句も言わずに数百行の巨大な関数や、深いネスト構造を生成することができます。そのため、「人間が理解できる粒度(行数・複雑さ)に分割すること」をLintでAIに強制させています。これに引っかかると、AIは自らリファクタリングを行うようになりました。
- 単純なLintだけでなく、コードの複雑さを制限するルールを厳しめに設定しています。
cargo test/cargo test --release(テスト)cargo doc(ドキュメント生成確認)cargo build --release(リリースビルド確認)
# clippy.toml の設定例 cognitive-complexity-threshold = 7 # 複雑度が7を超えたら警告 too-many-lines-threshold = 50 # 50行を超えたら警告
以下は実際に使用しているGitHub Actionsの設定例です。
# .github/workflows/ci.yml name: CI on: pull_request: branches: - main - production paths: # Rustのソースコードとビルド設定ファイルが変更された場合のみCIを実行 - 'crates/**' - 'Cargo.toml' - 'Cargo.lock' - 'rust-toolchain.toml' - 'clippy.toml' - '.github/workflows/ci.yml' workflow_dispatch: env: RUST_BACKTRACE: 1 jobs: # Fast compilation check check: name: Check runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Setup Rust cache uses: swatinem/rust-cache@v2 with: shared-key: "ci-check" cache-on-failure: true - name: Run cargo check run: cargo check --workspace --all-targets --verbose # Code formatting check fmt: name: Format runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Check formatting run: cargo fmt --all --check # Linting with clippy clippy: name: Clippy runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Setup Rust cache uses: swatinem/rust-cache@v2 with: shared-key: "ci-clippy" cache-on-failure: true - name: Run clippy run: cargo clippy --workspace --all-targets -- -D warnings # Unit and integration tests (debug mode) test: name: Test (Debug) runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Setup Rust cache uses: swatinem/rust-cache@v2 with: shared-key: "ci-test" cache-on-failure: true - name: Run tests run: cargo test --workspace --verbose # Unit and integration tests (release mode) test-release: name: Test (Release) runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Setup Rust cache uses: swatinem/rust-cache@v2 with: shared-key: "ci-test-release" cache-on-failure: true - name: Run tests (release) run: cargo test --workspace --release --verbose # Documentation generation doc: name: Documentation runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Setup Rust cache uses: swatinem/rust-cache@v2 with: shared-key: "ci-doc" cache-on-failure: true - name: Generate documentation run: cargo doc --workspace --no-deps --document-private-items --verbose env: RUSTDOCFLAGS: "-D warnings" # Release build verification build-release: name: Build (Release) runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Setup Rust cache uses: swatinem/rust-cache@v2 with: shared-key: "ci-build-release" cache-on-failure: true - name: Build release run: cargo build --workspace --release --verbose # Summary job - all checks must pass # このジョブの目的: # 1. GitHub Branch Protectionで必須チェックとして設定する場合、 # 個別のジョブ全てを設定する代わりに、このジョブ1つだけを設定すればよい # 2. PRのステータスチェックで「全てのCIが成功したか」を一目で確認できる # 3. ジョブの追加/削除時にBranch Protectionの設定を変更する必要がない ci-success: name: CI Success runs-on: ubuntu-latest needs: - check - fmt - clippy - test - test-release - doc - build-release if: always() steps: - name: Check all jobs succeeded run: | if [ "${{ contains(needs.*.result, 'failure') }}" == "true" ]; then echo "One or more CI jobs failed" exit 1 elif [ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]; then echo "One or more CI jobs were cancelled" exit 1 else echo "All CI jobs succeeded" fi
セキュリティチェック (cargo-deny)
依存関係(クレート)の管理も自動化しています。cargo-deny を使用し、4つの観点でスキャンを行います。
| チェック | 内容 |
|---|---|
| advisories | RustSec Advisory DBを使った脆弱性検出 |
| bans | 禁止クレートの検出、複数バージョン警告 |
| licenses | 許可ライセンスのみ使用か確認(MIT, Apache-2.0, BSD等) |
| sources | crates.io以外からの怪しい依存関係を禁止 |
以下はGitHub Actionsでの設定例です。週次での定期実行も設定しています。
# .github/workflows/security.yml name: Security on: pull_request: branches: - main - production paths: # 依存関係やセキュリティ設定が変更された場合のみセキュリティチェックを実行 - 'crates/**' - 'Cargo.toml' - 'Cargo.lock' - 'deny.toml' - '.github/workflows/security.yml' schedule: # Run weekly on Sunday at 00:00 UTC (09:00 JST) - cron: '0 0 * * 0' workflow_dispatch: jobs: # Comprehensive dependency and security checks using cargo-deny deny: name: Cargo Deny runs-on: ubuntu-latest strategy: fail-fast: false matrix: check: - advisories - bans licenses sources steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Install cargo-deny run: cargo install cargo-deny --locked - name: Run cargo deny (${{ matrix.check }}) run: cargo deny check ${{ matrix.check }} # Summary job # このジョブの目的: # 1. Matrix jobの結果を集約して単一のステータスチェックにする # 2. GitHub Branch Protectionで必須チェックとして設定する場合に便利 # 3. PRのステータスチェックで「全てのセキュリティチェックが成功したか」を一目で確認できる security-success: name: Security Success runs-on: ubuntu-latest needs: - deny if: always() steps: - name: Check all security jobs succeeded run: | if [ "${{ contains(needs.*.result, 'failure') }}" == "true" ]; then echo "One or more security checks failed" exit 1 elif [ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]; then echo "One or more security checks were cancelled" exit 1 else echo "All security checks passed" fi
レビュープロセスの変革:Lint指摘からの卒業
これらのお膳立てをした結果、コードレビューの風景が大きく変わりました。
Before:
- 「この変数名は命名規則に従っていません」
- 「不要なimportが残っています」
- 「このコードは使われてなさそうなので不要でしょうか?」
- 「この関数、ドキュメントコメントがないので書いてほしいです」
- 「こことあそこで一貫性が取れていないように見えます。どちらかに統一していただけないでしょうか。」
以前はこうした細かいLintやコーディングスタイルの指摘に、人間の貴重なリソースを使っているシーンがありました。
After:
- 一次レビューはAIが実施: AIエージェントが自動でコードを巡回し、指摘を行う。
- 対応案の提示: 指摘に対して「どう修正するか」の案もAIが出し、人間はそれを承認・選択するだけ。
- CIによるブロック: スタイル違反やLintはそもそもCIで落ちる。ルールを厳しくしても人間への負荷がかからない。
これによって、人間のところにレビューが回ってくる頃には、ほとんどが純粋な「設計の議論」や「ビジネスロジックの妥当性」だけに集中できる状態になっています。
「これ、AIが直してくれるからいいや」と割り切れる部分が増えたことで、心理的な負担も大きく減りました。
まとめ
- ルールを決める: AIに守らせるための厳格なルール(型、Lint、テンプレート)を人間が決める。
- 作業を区切る: コンテキストを混ぜず、Issue作成、実装、PR作成を作業単位でAIに依頼する。
- コアに集中する: 浮いた時間で、エンジニアは本来やるべき「価値の創造」に向き合う。
Rustは単に「安全で速い言語」であるだけでなく、「AIの手綱を握り続けるための最適なパートナー」でもありました。 コンパイラが常に横でAIのコードを監査してくれる安心感があるからこそ、私たちはコアに集中できる時間が増えた気がします。
AI活用は今のところ銀の弾丸ではありません。どこから手をつけていくか迷っている方は、以下の順で試してみてはいかがでしょうか。
- まずはコミットメッセージの作成だけAIに任せてみる
- Issue/PRテンプレートを整備し、AIが参照できるようにする
- Linterの設定を厳しくして、AIが自動修正するループを作る
最初から「全自動化」を考えるのがしんどい場合は、まずは人間が指示を出しAIが手を動かす「半自動化」をしやすい環境を整えることから始めてみることをおすすめします。
ところで、スパイダープラスでは仲間を募集しています。 少しでも興味が出てきたなという方はお気軽にご連絡ください。