Tech Racho エンジニアの「?」を「!」に。
  • IT Tips

AIパフォーマンスの最適化を学ぶ(2)「SOWを作って」は超便利な指示

本記事は、CC BY-SA 4.0ライセンスで公開します。


本記事の文面は、明示している部分を除き、AIでは生成していません。
本シリーズ記事では簡単のため、特に断らない限り、各種AIサービスやLLM(大規模言語モデル)といった個別の要素を捨象して、一般的な語である「AI」と呼ぶことにしています。

本シリーズ記事で扱うAIは、特に断らない限り、以下の分類で言う「生成AI」、その中でも会話(自然言語)による指示で動くAIに限定しています。また、AIの用途も基本的に業務用を想定しています。

  • 生成AI: 新しいコンテンツを生成するAI(チャットAI、コーディング支援AI、ドキュメント作成補助AI)。画像・音声・動画を生成するAIも広義にはここに含まれる。
  • 認識AI: 入力された画像や音声などのデータを分析して認識するAI。識別AIとも呼ばれる。

こんにちは、hachi8833です。

今回も、AIに効果的な指示を出して、期待に沿う結果を増やすためのノウハウを順次紹介していきます。どのノウハウも、AIサービスの種類やLLMの種類や課金の種類に依存しません。もっと言えば、日本語でも英語でもJSでもPythonでも変わらない内容を扱います。

🔗 「違う、そうじゃない」

コーディングAIとのインタラクティブな会話で、大規模なタスクをいきなりやらせると、たいていとんでもないことになるのは皆さんも重々自分の身体でご承知だと思います。

もちろん会話では作業範囲を絞って少しずつ作業させ、「いや、そこはそうじゃなくてこう」と行ったり来たりするなら、リスクを小さくできますし、それも大事なことです。ただ、日本語入力がしづらいコマンドラインターミナルでは、長時間ああでもないこうでもないとやっていると、だんだん嫌になってくるでしょう。

このままでは「違う、そうじゃない」の嵐です。そこで、今回紹介するコツの出番です。

🔗 大事なコツ: 「SOWを作って」と指示する

SOWを作って」は、実にシンプルな方法です。

しかしこれは、特にコーディングAIでプログラミングや修正、リファクタリングを行うとき、あるいはインフラ関連の作業を行う際の、小規模な作業において著しく有用です。さらに、コーディング以外の用途でも広範囲に使えます。

「SOWを作って」は、AIコーディングの対話で使っても、CLAUDE.mdなどのデフォルトプロンプトに書いておくのも有用です。

🔗 SOWって何よ?

いわゆる「作業明細書」と呼ばれるものです。SOWはStatement of Workの略だったりScope of Workの略だったりしますが、それはこの際重要ではありません。

大事なのは、AIに「SOWを作って」と指示するだけで、何を作って欲しいのかを一発で理解してもらえることです。具体的にああ書けこう書けなどと細々指示する必要もありません。作業明細書などと書かなくても、日本語で「SOWを作って」でちゃんと通じます。なお、基本的にmarkdown形式で作るようです。

本記事において、SOWは「Gitの履歴に残すほどでもない、小規模作業用の一時的な指示書」という位置づけで使っていきます。後述しますが、本記事では基本的にSOWという用語を使います。

そういうわけで、私はSOWをtmp/ディレクトリに置くことがほとんどですし、AIに生成してもらったSOWは、作業実行に不要な部分を検証時にどしどし削っています(AIが最初に生成するSOWは一般性が高く、かなりこってりと長いので)。

このSOWメソッドは、私の友人が編み出したものを整理・発展させたものです。
友人も私も、AIコーディングは(たまたま)100%英語ベースでやっています(日本語だとターミナルでつっかえてつらいので)。本シリーズの内容は何語であっても関係ないことしか書かないので、特に問題は感じていません。

🔗 具体例

私が自作したRailsアプリでの例です。

app/models/highlight_match.rb をリファクタリングするための
SOWを、diff付きでtmp/ディレクトリの下に生成してください」

▶生成したSOW(クリックで展開)

HighlightMatch クラスリファクタリングSOW

概要

app/models/highlight_match.rbのリファクタリングを行い、可読性と保守性を向上させる。

現在の問題点

1. 責務の混在

  • 文字列処理とビューレンダリングが混在
  • Form Object的な要素が含まれている(開発者のコメントにもある通り)

2. メソッドの複雑さ

  • proofreadメソッドが複数の責務を持つ
  • 正規表現マッチング、HTML生成、エラー収集、カウンタ更新が同一メソッドに混在

3. 重複コード

  • proofreadproofread_with_idで類似のロジック

リファクタリング提案

1. 責務の分離

  • TextProcessor: 文字列処理とパターンマッチング
  • ErrorCollector: エラー情報の収集と整理
  • TooltipBuilder: ツールチップHTML生成(既存のTooltipモジュール活用)

2. メソッドの分割

  • パターンマッチングロジックを独立したメソッドに
  • HTML生成とエラー収集を分離

3. 設定の外部化

  • DELIMITERSなどの定数を設定クラスに移動

実装計画

Step 1: Service Objects作成

# app/services/text_processor.rb
class TextProcessor
  def initialize(text, patterns)
    @text = ERB::Util.html_escape(text)
    @patterns = patterns
  end

  def process
    # パターンマッチング処理
  end
end

# app/services/error_collector.rb
class ErrorCollector
  def initialize
    @errors = {}
  end

  def add_error(display_id, match, comment, color)
    # エラー情報の収集
  end

  def to_hash
    @errors
  end
end

Step 2: HighlightMatch のリファクタリング

class HighlightMatch
  include Tooltip

  def initialize(to_check)
    @text_processor = TextProcessor.new(to_check, Pattern.new.fetch_regex)
    @error_collector = ErrorCollector.new
  end

  def proofread
    result = @text_processor.process_with_errors(@error_collector)
    update_pattern_counters(result[:hit_ids])

    [result[:highlighted_text], @error_collector.to_hash]
  end

  def proofread_with_id
    @text_processor.process_with_id
  end

  private

  def update_pattern_counters(hit_ids)
    Pattern.update_counters([hit_ids], hit_count: 1, touch: true)
  end
end

変更差分

変更前

# frozen_string_literal: true

# ennoのコアとなるクラス
# Form Object的なビュー要素が混じっているのが少し残念だが、切り出してもあまりよくならなそうなので当面これでいく
class HighlightMatch
  include Tooltip
  DELIMITERS = "࿗-࿘"
  ErrorList = Data.define(:match, :comment, :color)

  def initialize(to_check)
    @highlighted_string = ERB::Util.html_escape(to_check)
    @patterns = Pattern.new.fetch_regex
  end

  # 校正: カウンタも更新する
  def proofread
    detected_errors = {}
    hit_ids = Set.new
    display_id = 0

    @patterns.each do |line|
      next unless @highlighted_string.match?(line[:regex])

      @highlighted_string.gsub!(line[:regex]) do
        hit_ids << line[:id]
        line[:id] += 1
        line[:match] = Regexp.last_match(0)

        line[:display_id] = (display_id += 1)
        detected_errors[line[:display_id]] = ErrorList.new(
          match: line[:match],
          comment: "【#{line[:category]}】#{line[:comment]}",
          color: line[:color]
        )
        build_tooltip(line)
      end
    end

    Pattern.update_counters([ hit_ids ], hit_count: 1, touch: true)

    @highlighted_string.tr!(DELIMITERS, "")
    return @highlighted_string, detected_errors
  end

  # メンテナンス画面用: 結果にidを付与する
  def proofread_with_id
    @patterns.each do |line|
      next unless @highlighted_string.match?(line[:regex])

      @highlighted_string.gsub!(line[:regex]) do
        line[:match] = Regexp.last_match(0)
        build_tooltip_with_id(line)
      end
    end

    @highlighted_string.tr!(DELIMITERS, "")
    @highlighted_string
  end
end

変更後

# frozen_string_literal: true

# ennoのコアとなるクラス - リファクタリング済み
class HighlightMatch
  include Tooltip

  def initialize(to_check)
    @text_processor = TextProcessor.new(to_check, Pattern.new.fetch_regex)
    @error_collector = ErrorCollector.new
  end

  def proofread
    result = @text_processor.process_with_errors(@error_collector)
    update_pattern_counters(result[:hit_ids])

    [result[:highlighted_text], @error_collector.to_hash]
  end

  def proofread_with_id
    @text_processor.process_with_id
  end

  private

  def update_pattern_counters(hit_ids)
    Pattern.update_counters([hit_ids], hit_count: 1, touch: true)
  end
end

期待される効果

  1. 可読性の向上: 責務が明確に分離される
  2. 保守性の向上: 各クラスが単一の責務を持つ
  3. テスタビリティの向上: 各コンポーネントが独立してテスト可能
  4. 再利用性の向上: Service Objectsは他の場所でも利用可能

注意点

  • 既存のAPIとの互換性を保つ
  • パフォーマンスの劣化がないことを確認
  • 既存のテストが継続して動作することを確認

実装タイムライン

  1. Service Objects作成 (1日)
  2. HighlightMatch リファクタリング (1日)
  3. テスト実装・修正 (1日)
  4. パフォーマンステスト (0.5日)
  5. レビュー・修正 (0.5日)

総所要時間: 4日

Claude Codeで生成

生成されたSOWには、「この作業を行った結果、どうなるか」を説明するセクションが必ずあります。セクション名は多少変わるかもしれませんが、そのセクションが、そのまま「AIコーディング作業の結果のソースコード」の置き場所として使われるわけです。そしてAIへの指示や資料が適切であれば、そのセクションに実行後のコードをAIが書き込んでくれます。

コーディングAI(ここではClaude Code)に生成させたSOWはだいぶボリュームがありますが、いらないセクションはどしどし捨て、作業に必要なものだけ残していけばよいでしょう。SOWは、あくまで小規模な作業を過不足なく行うためのものなので。

お気付きかもしれませんが、生成したSOWの「実装タイムライン」セクションに「2. HighlightMatch リファクタリング (1日)」とか「3. テスト実装・修正 (1日)」のように、人間が作業する前提の見積まで出ています😆。AIコーディングならもちろん削っても構わない項目でしょうね。

このような要領で、SOWに書き足すなり削るなり書換えるなり自由にやってからAIに再チェックしてもらうことを繰り返し、SOWができあがったらその通りに実行してもらえばよいのです(当たり前ですね)。言い換えれば、SOWを一種のDry-run(=予行演習)の結果ファイルとして、Dry-run的に動作の事前検証に使うということです。

項目の追加や書換えもAIにお願いしちゃいましょう。

もちろん、SOWを作って実行完了してgit push でデプロイするところまで一気にやらせることも可能といえば可能ですが、私はさまざまな理由から、SOWの作業範囲を欲張らないようにしています。

🔗 適用範囲

SOWは、いわゆるAIコーディングで、作業内容がほぼ定まっているような個別の小規模な作業を進めるうえで、ほとんどの場合有効です。

逆に、作業範囲が定まってない、仕様が大きく変動中、といった場合は、SOWよりも先に作業範囲や仕様を固める方を優先する方がよいでしょう。

また、アプリケーション全体の要件定義や、それに伴う各種設計ドキュメント、実装に関するドキュメントは、SOWに引用するには大変有用ですが、このような大規模なドキュメントや概要などはSOWとは違うものなので、別物として考えるのがよいでしょう。

SOWは業務向けの概念ではありますが、かなり一般性が高いので、別にAIコーディングに限らずとも、たとえば庭の草むしりの段取りや、旅行で明日はどことどこを回ろうかといった小規模な計画を立てるときにだって「SOW作って」が使えますね。

🔗 「SOWを作って」を使わないのは損

もちろん皆さんの多くも、さんざん痛い目にあって大出血した末に、多かれ少なかれ既にこれと同じようなことはやっているでしょう。

「SOWを作って」が素晴らしいのは、次の4つの点です。

🔗 1: AIへの指示が短く済み、しかも的確に理解される

SOWは、「サギョウメイサイショ」などと律義に入力するよりずっと楽です。しかもどんな章立てにするかなどを細々と指示する必要もありません。
たったこれだけの指示で、必要なものが全部入ったSOWを作ってもらえるのは、それだけでありがたいことです。これ以上簡単に言ったり書いたりするのは無理でしょう。

SOWは中国語でも「工作说明书」とか「项目工作说明书」だそうです。

もちろん、「XXについてのSOW作って」ぐらいの文脈は与える必要がありますが、AIが「SOWとはどういうものか」という常識を押さえてくれているおかげで、経験では、通常のプロンプトよりずっと緩い指示でもうまくやってくれます。どっちみちSOWは削ったり参考資料を貼ったりするのですから。

さらに、企業や業界によって異なりがちな「作業指示書」なのか「業務依頼書」なのかといったローカル表記ルールに惑わされることもなく、SOWという言葉だけで、何を作って欲しいかがAIに一発で伝わるのもありがたい点です。

私はSOWが通じることをClaude Codeでしか確認していませんが、英語圏で完全に普及している概念である以上、おそらくどのAIサービス、どのLLMであろうと「SOWなにそれおいしいの?」となることはないと思います。

そのうちSOW的なものを自動で追加するお便利機能がコーディングAIに入ってくるのかもしれませんが、変に型にはめるよりも自分で「SOWを作って」する方がずっと柔軟そうです。

🔗 2: 概念が明確に言語化される

これまでもAIコーディングで似たようなことは行われているに決まっていますが、こういう作業指示という概念は、おそらくここまで明確に言語化されていなかったでしょう。
方法論としても定まらず、作業指示ファイルを作ったり作らなかったり、Issue作ったり作らなかったり。そもそも、このドキュメントを何と呼ぶかもふわっとしたままだったでしょう。

これをSOWという語にまで落とし込むことで、今後は社内の会話でも「SOWちゃんと作った?」などと誤解なく明快に伝わるようになります。
概念がばらつかなくなることは、後々助けとなるでしょう。

🔗 3: SOWの概念は「軽い」

従来のIT業界やSI業界だと、「要件定義書」だの「内部仕様書」だのといった書類や言葉が毎日飛び交っています。
本記事で述べているSOWは、そういうものより概念も作業範囲もはるかに小さい、今目の前のタスクを的確に終わらせるための一時的ドキュメントとして使い分けるのが便利だと思います。

「SOWの概念をお前が決めるのか?」などとツッコまれそうな気もしますが、AIコーディングなどの文脈で使うなら、要件定義などより規模のずっと小さい、戦略ではなく戦術的な、小規模作業のための、気軽に作って気軽に捨てて構わない一時ドキュメントを指す言葉として使っていきたいと思っています。

もっと言うなら、SOWは自分がAIに作らせて自分がAIに使わせ、終わったら捨てても構わない究極のドッグフーディングなので、開発プロジェクトで定められたIssueの書式だのドキュメントのフォーマットだのといった小うるさいことを一切気にする必要がないのも嬉しい点です。逆に、それを気にする必要があるならSOWにするには大きすぎる作業なのではないでしょうか。

なお、私はほとんど内情を知りませんが、たとえばコンサル業界などでは既にSOWという言葉がさんざん飛び交っていそうですね。
そういう業界でもなければ、SOWという言葉を、「気軽にAIに生成してもらって確認してから実行するための書類」という位置づけで問題なく使えると思います。

🔗 4: 作業のギャンブル性をほぼゼロにできる

コーディングAIの作業で、デフォルトプロンプトとチャットでの指示をごりごりに武装して巨大な変更を一発で決めようとするのは、AIが本質的に非決定的であることを考えれば、ギャンブルだと思います。さもなければせいぜい曲芸でしょう。それも、数年後にはウケなくなる曲芸。

呼び方はSOWであろうと何であろうと、作業を事前にドキュメント化して検証したうえで、そのとおりにやってもらう形を取っていれば、ドキュメント作成の段階で作業結果を明確に予測できます。SOWベースで作業することで、英語で言うところの「predictable」、つまり予測可能性を手に入れられるわけです。

頼んでもいない余計なことをAIが「やっときました」となって、青ざめたり吐き気が止まらなくなったり田舎に帰りたくなったりせずに済むでしょう。

少なくともSOWベースでやっていて、書いていないことを勝手に始められたことはなかったと思います。
そんなに心配なら、SOWの冒頭に「ここに書いていないことは絶対無断で行わず、必要ならSOW作成の段階で質問すること」などと書いておけばよいのです。

むしろ、「SOWを実行して」と指示した直後に、「あ、gitブランチ新しいの作っとけばよかった」といった、こちら側の手抜かりの方がありがちでした。😅

AIをしばいて言うことを聞かせようとムキになっているうちにデフォルトのプロンプトが数千行に膨れ上がっていくくらいなら、それよりもまず「SOWを作って」を試してみましょう。何度も書いていますが、要らない章やセクションはどんどん削ればいいのですから。

合言葉は「SOWを作って」。

🔗 SOWを作らせるときのコツ

  • SOWを作らせるときはdiff表示も添えさせよう

指示が「SOWを作って」で済むのはありがたいのですが、どうやらClaude Codeが認識しているSOWには、指示しないとファイル変更のdiff表示は入ってこないようです。英語圏の世間一般におけるSOWにはdiffなんか入ってこないでしょうから、これは十分想像がつきます。

しかし少なくともAIコーディングでは、SOWにファイル変更のdiff表示がある方がずっと便利です。

そういうわけで、「SOWをdiff表示付きで作って」と指示するか、CLAUDE.mdなどのデフォルトプロンプトでdiff表示も添えるよう指示しておくとよいでしょう。

  • 作業に必要な参照資料はSOWに転記すること

SOWというドキュメントの性質上、SOWさえあれば、AIであろうと、事情を知らない初参加の開発者であろうと、まったく同じように処理できることが前提となります。

「参照すべき資料はこことここ」などのようにURLで示して済ませたい気持ちもわかりますが、そうした資料は極力SOWに転記しましょう。それにより、AIであっても人間であっても、他の資料を探しに行かなくて済むようになり、パフォーマンスも精度も上がるはずです。

SOWの参考資料セクションにURLを貼って「ここに引用して」と指示するのも可能でしょうが、このぐらいは自分でやる方が再確認もできてよいかもしれません。

こうすることで、調べものやコード生成といったほとんどの作業はSOWの段階で完了するので、実際のSOW実行はひたすら反映していくだけとなり、著しく速くなるはずです。

別に関係ありませんが、モーツァルトが作曲するときは、「できた!」と一瞬で頭の中で全楽章が完成し、後は食事しながらとか談笑しながら譜面にダンプしていたという伝説をふと思い出しました。私は半分も信じていませんが。

いわゆる「Single-Source-of-Truth」という概念に近づけるつもりで、外部の情報になるべく依存しない、完結したドキュメントを目指してSOWを作っていくのがポイントです(もちろんライブラリファイルやヘッダファイルなど、ディレクトリを指定するしかない場合もあります)。

  • 作業に必要ない資料までSOWに転記しないこと

「SOWに全部書いとけばいいんだな、よっしゃ」とばかり、大量の資料をどっさり貼り付けて「これでよろしく」とやりたい気持ちはわかります。

しかしこういう欲張りさんなことをすると、SOW作成段階とSOW実行段階のAIのパフォーマンスを確実に落とします。

人間だって、巨大なドキュメントの全容を把握して、あるセクションを読んだときに他のどんな部分と関連しているか、うんと離れたセクションや別ドキュメント同士の記述が矛盾してないか、といったことを一滴残らず把握するのは大変な作業です。

そしてそれは、AIにとっても同じです。

ずぼらな「資料全部盛り」SOWを作っていては、AIにとってのノイズを増やすばかりです。むしろそのせいで、資料内部の矛盾や情報不足からAIが勘違いする可能性もあります(しました🫠)。
そうでなくても、資料やコードが増えれば増えるほど、コーディングAIの探索作業がみるみる増えていくのは、コーディングAIの動きを観察していればわかるはずです。

面倒がらずに、資料は必要にして十分なものだけをSOWに転記しましょう。大きなドキュメントなら、作業に必要なセクションだけを抽出してから、SOWに転記しましょう。
作業範囲を欲張らずに絞っていれば、自然とできるでしょう。

  • SOWの使い回しは避けよう

SOWは使い捨てであり、生ものだと思うぐらいがよいでしょう。一年後に状況が変われば、ケチケチ取っておいた同じSOWが同じ結果をもたらすと思う方が無理があるでしょう。必要が生じたら、その都度作るぐらいの気持ちで。食あたりにご注意。

🔗 おまけ

検証済みのSOWを実行する前に、たまたまMacbookが不調で再起動しなければならなくなったことがあったのですが、当然ながら問題なくSOWを実行してもらえました。
つまり、SOWは会話が変わったぐらいなら問題なく実行できるというメリットもあるということです😋。もちろんあまり間は空けたくありませんが。

🔗 参考: Claude Sonnet 4にも聞いてみた

とおっしゃっております。

私はどっちの発音でも構いません。皆さんもドヤ顔で「こっちが正しい」とかマウントを取らないように。

次回もお楽しみに!

関連記事

AIパフォーマンスの最適化を学ぶ(1)適切なレスポンスが重要な理由


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。