はじめに
GMOコネクトの平島です。
複数シナリオ×2パターンで複数シートのExcel手順書、Claude Codeに丸投げしてみたら最初は全然うまくいきませんでした😇 プロンプトの書き方を変えたら動いた、という話です。
具体的には、Markdownで書かれた調査手順を「開発者向け(プレースホルダあり)」と「検証者向け(具体値あり)」の2パターンのExcel手順書に自動変換するスクリプトをClaude Codeに生成させました。テンプレートには結合セル・列幅・罫線・連番数式がびっしり設定されており、書式を維持したまま生成するには、それなりの指示の工夫が必要でした。
コードはClaude Codeが書いてくれます。人間が考えるべきは指示の設計だけです。
先にまとめ
- 事前調査をプロンプトに埋め込む:まずClaude Codeにテンプレートを調査させ、判明した構造を指示プロンプトに書き込む
-
フォルダを
references/(読み取り専用)とoutput/(書き込み先)に分ける:「元ファイルを上書きしない」は明文化しないとやってしまう -
使うAPIを名指しする:
copy_worksheet()と明示しないとcreate_sheet()が選ばれ書式が崩れる - Acceptance criteriaを機械的に検証できる条件で書く:「シート数N枚」「連番数式が存在する」のように書くと、Claude Codeが自発的に検証スクリプトまで生成する
-
指示プロンプト自体をファイルとして
output/に保存する:再実行・チーム共有・Git管理が可能になる
失敗パターン:「なんとなく指示」するとどうなるか
最初に試みたのは、こういう指示でした。
このMarkdownファイルを読んで、テンプレートのExcelに内容を転記するPythonスクリプトを書いて
結果として生成されたコードには、以下の問題が含まれていました。
ハマり1: テンプレートを直接上書きした
load_workbook(template_path) → save(template_path) という破壊的な実装になっていました。
ハマり2: create_sheet() で書式が飛んだ
テンプレートの書式が引き継がれず、セルスタイルがすべてデフォルトに。
ハマり3: 出力先が未指定
カレントディレクトリに謎のファイルが生成されました。
ハマり4: 2パターンの作り分けが考慮されない
プレースホルダ版と具体値版の区別がまったくありませんでした。
これらは「普通のスクリプト」としては正しい実装ですが、「書式凝りまくりのExcelテンプレートを維持しつつ複数派生を生成する」という要件が指示に含まれていなかったため、LLMには伝わりませんでした。
足りなかったのはコードの知識ではなく、指示の精度です。
事前調査フェーズ:Claude Codeにテンプレートの構造を調査させる
本番の指示プロンプトを書く前に、まずClaude Codeにテンプレートの構造を調査させます。「このテンプレートのシート構成・列幅・結合セル・数式を調べてほしい」と依頼すると、Claude Codeが以下のようなスクリプトを生成・実行してくれます。
# Claude Codeが生成・実行した調査スクリプト
import openpyxl
wb = openpyxl.load_workbook("references/templates/template_procedure.xlsx")
ws = wb.active
print("シート名:", ws.title)
print("結合セル:", sorted(str(m) for m in ws.merged_cells.ranges))
print("列幅:", {k: round(v.width, 1) for k, v in ws.column_dimensions.items() if v.width})
for coord in ['B6', 'B11', 'C11', 'D11', 'G11', 'B13', 'G13']:
c = ws[coord]
print(f"{coord}: value={c.value}, font={c.font.name}/{c.font.size}, bold={c.font.bold}")
この調査で判明した事実をプロンプトに直接埋め込みます。
### テンプレートの構造(調査済み)
- シートは1枚のみ。シート名: `対応手順_<サービス名>_手順`
- `B6:D9`(結合セル)に `■前提条件` ブロック
- 11行目がヘッダ:`B=No / C=確認 / D=作業概要 / E=担当 / F=目的 /
G=詳細手順(実行コマンド等) / H=参照画面 / I=備考`
- 13行目以降がデータ行。`B列`は `=ROW()-12` の連番数式
- 列幅:A列(狭め)/ B列(狭め)/ D列(中)/ G列(広め)/ I列(中)
こうすることで、LLMが「自己流に構造を推測する」のを防ぎます。LLMは指示に書かれていない部分を補完しようとするため、重要な制約ほど明文化が必要です。
また、2種類のテンプレートの差分も調査して明文化しました。
### 2テンプレートの差分(調査済み)
| 観点 | 開発者向け | 検証者向け |
|----------|--------------------|-----------------------|
| G列詳細手順 | [取得するリージョン] 等のプレースホルダを残す | プレースホルダを具体値に全置換 |
| I列備考 | 「※環境名は手順書作成時に記載」の注記あり | 同注記なし |
フォルダ設計:「読み取り専用」と「書き込み先」を分離する
プロジェクトのフォルダ構成は、指示プロンプトを書く前に決定しておきます。
project/
├── references/ # 読み取り専用(元ネタ・テンプレート)
│ ├── playbook.md ← 手順の元ネタ(Markdown)
│ └── templates/
│ ├── template_procedure.xlsx ← 開発者向けテンプレート
│ └── template_procedure_ver.xlsx ← 検証者向けテンプレート
└── output/ # 書き込み先(生成物)
├── procedure.xlsx ← 生成された開発者向け手順書
└── procedure_ver.xlsx ← 生成された検証者向け手順書
このフォルダ設計の重要なポイントは references/ と output/ の明確な分離です。
LLMは「元ファイルを修正するか、コピーして修正するか」を指示で明示しないと、どちらかわかりません。指示プロンプトのConstraintsに次のように書きました。
Constraints:
1. 元ファイルを直接修正しない。3つの入力ファイルはいずれも読み取り専用として扱い、
上書き保存しない。各テンプレートはまず output/ 配下にコピーを作成し、
コピーに対してのみ編集する。
この一文があることで、生成されたコードは必ず shutil.copyfile(template, outpath) でコピーしてから編集するという実装になりました。
プロンプト設計:Goal/Constraints/Acceptance criteriaの3層構造
実際に使用したエージェント指示プロンプトの構造を解説します。
Goalセクション
Goal:
`references/playbook.md` の全シナリオの調査手順を、既存テンプレートの体裁に
沿ってExcelの手順書に落とし込む。成果物は次の2ファイルを output/ フォルダに作成する。
1. 開発者向け手順書 … 開発者向けテンプレートをコピーしたxlsx
2. 検証者向け手順書 … 検証者向けテンプレートをコピーしたxlsx
各ファイルは、テンプレートの既存シートを残したまま、1シナリオ=1シートで
シートを追加した状態にする。
Goalには「何を作るか」と「成果物の形」を書きます。特に重要なのが「テンプレートの既存シートを残したまま追加する」という一文です。これがないとLLMは既存シートを上書きします。
Constraintsセクション
ConstraintsはLLMが「よかれと思ってやってしまう」行動を事前に封じるために書きます。
Constraints:
2. 書式の維持:新規シートはテンプレートの既存シートを複製(openpyxlの
copy_worksheet等)して作成し、ヘッダ・結合セル・列幅・セル書式・罫線を引き継ぐ。
テンプレートと同じ列レイアウト(B〜I列、11行目ヘッダ、13行目以降データ)を崩さない。
3. B列の連番数式 `=ROW()-12` はテンプレートの形式を踏襲し、追加行にも設定する。
4. 開発者向けと検証者向けの作り分け:
- 開発者向け:プレースホルダをそのまま残す。I列備考に
「※環境名・IPは手順書作成時に置換」の注記を入れる。
- 検証者向け:プレースホルダを仮値(別途定義)に全置換し、
コピー&ペーストで即実行できる状態にする。
I列備考の手順書作成時注記は入れない。
5. プレイブックに記載のない値を創作しない。判断に迷う箇所は
I備考にその旨を明記する。
注目してほしいのは 「openpyxlのcopy_worksheetを使う」と明示した点です。
openpyxlには新規シートを追加するcreate_sheet()もありますが、これだとテンプレートの書式が引き継がれません。copy_worksheet()を明示しなければ、LLMはcreate_sheet()を選ぶことがあります。使ってほしいAPIを名指しで指定することで、実装方針のブレがなくなります。
Acceptance criteriaセクション
Acceptance criteriaは機械的に確認できる形で書くのがポイントです。「正しい出力になっている」ではなく「シート数が10枚である」のように、検証スクリプトでアサートできる条件を書きます。
Acceptance criteria:
1. `output/` に開発者向け・検証者向けの2つのxlsxが生成されている。
元の3入力ファイルのタイムスタンプ・内容が変更されていない。
2. 各xlsxは、テンプレート由来の既存シート1枚+新規追加シートN枚の計(N+1)シートを持つ。
新規シート名は `対応手順_<サービス名>_<シナリオ名>` 形式。
3. 各シナリオシートに、全ステップ行が漏れなく存在する。
4. ヘッダ行(11行目)・列幅・結合セル・B列連番数式がテンプレートと同一の体裁で
維持されている。
5. 開発者向けはプレースホルダが残存し、検証者向けは具体値に置換されている、
という差分が全シナリオで一貫している。
6. 生成したxlsxがopenpyxlで正常に開けること(破損していないこと)を確認済みである。
Acceptance criteriaにこれだけ書いておくと、Claude Codeは自発的に検証スクリプトを生成します。実際に生成されたのが以下のような検証コードです。
# Claude Codeが自動生成した検証コード(_verify.py)
for path, ver in [(OUT_DEV, False), (OUT_VER, True)]:
wb = openpyxl.load_workbook(path)
assert len(wb.sheetnames) == N + 1, wb.sheetnames # シート数チェック
assert wb.sheetnames[0] == BASE_SHEET # 元シートが先頭
for sc in scenarios:
ws = wb[sc['sheet']]
assert ws['B11'].value == 'No' and ws['I11'].value == '備考' # ヘッダ確認
for i in range(len(sc['rows'])):
assert ws['B' + str(13 + i)].value == '=ROW()-12' # 連番数式確認
for col in 'DFGI':
assert ws[col + str(13 + i)].value # セル非空確認
「何をもって完了とするか」が明確であれば、LLMは完了条件を満たすための実装を自分で設計します。
指示プロンプトをファイルとして残す
もう一つ重要な実践として、エージェント指示プロンプト自体を成果物として保存しました。
output/
├── 【手順書Excel化】_エージェント指示プロンプト.md ← これも成果物
├── procedure.xlsx
└── procedure_ver.xlsx
プロンプトをファイルとして残すことには、次のメリットがあります。
- 再実行できる:Markdownのプレイブックが更新されたとき、同じ指示プロンプトを別エージェントに渡せば即座に再生成できる
- チーム共有できる:「どう指示したら動いたか」がGitで管理される
- 改善できる:出力が期待と違ったとき、どのConstraintが不足していたかをプロンプトに追記していける
プロンプトファイルをGitに置いておくと、次回は渡すだけで終わります。
結果:Claude Codeが生成したコードの構成
Acceptance criteriaを含む指示プロンプトを渡した結果、Claude Codeは以下の5ファイル構成を自然に生成しました。
_engine.py # Excel生成の中核ロジック(copy_worksheetによるシート複製、行高計算等)
_run.py # エントリポイント(TEMPLATE_DEV/VERとOUT_DEV/VERを定義して呼び出す)
_scen_part1.py # シナリオ前半のデータ定義
_scen_part2.py # シナリオ中盤のデータ定義
_scen_part3.py # シナリオ後半のデータ定義
_verify.py # 検証スクリプト(Acceptance criteriaをコードに落とした版)
コードを分割した理由は、シナリオデータが大きくなるためです。Claude Codeはファイルを分割するかどうかを自分で判断し、_scen_part1/2/3.pyという構成を選びました。これは明示的に指示していません。Acceptance criteriaに「全シナリオを網羅する」と書いたことで、スコープが大きいと判断して自発的に分割したと考えられます。
実装の核心部分がどういうものかだけ示すと、以下のようなイメージです。
# _engine.py の核心(Claude Codeが生成)
def build_workbook(template, outpath, scenarios, ver):
shutil.copyfile(template, outpath) # 元ファイルを保護してコピー
wb = openpyxl.load_workbook(outpath)
base = wb[BASE_SHEET]
for sc in scenarios:
ws = wb.copy_worksheet(base) # 書式をまるごと引き継ぐ
ws.title = sc['sheet']
# 行13のセルスタイルを全データ行に複製する
src_style = {col: copy.copy(ws[col + '13']._style) for col in 'BCDEFGHI'}
for idx, row in enumerate(sc['rows']):
r = 13 + idx
for col in 'BCDEFGHI':
ws[col + str(r)]._style = copy.copy(src_style[col])
ws['B' + str(r)] = '=ROW()-12' # 連番数式
ws['G' + str(r)] = row['g_ver'] if ver else row['g_dev'] # 2パターン切り替え
wb.save(outpath)
ver=True/False の一つのフラグで2種類のファイルを生成できているのは、Constraintsで「2つの差分は〇〇だけ」と明確に定義したからです。
まとめ
- 事前調査をプロンプトに埋め込む — LLMは指示にない部分を補完しようとします。まずClaude Codeにテンプレートを調査させ、判明した構造を指示プロンプトに書き込む
-
references/とoutput/を分け、制約として明文化する — 「元ファイルを上書きしない」は当然に思えても、書かないとやってしまいます -
使うAPIを名指しする —
copy_worksheet()と書けば書式ごと複製、書かなければcreate_sheet()で素のシートが生まれます -
Acceptance criteriaを機械的に検証できる条件で書く — 「正しい」ではなく「シート数N枚」「
=ROW()-12が存在する」のように書くと、Claude Codeが検証スクリプトまで生成してくれます -
指示プロンプト自体を
output/に保存する — 再実行・チーム共有・Git管理が可能になります
最後の目視確認だけ人間がやれば十分です。
最後に、GMOコネクトではサービス開発支援や技術支援をはじめ、
幅広い支援を行っておりますので、何かありましたらお気軽にお問合せください。