SuperwhisperとVSCodeのCopilot Agentを使って、音声から素早くブログを書き上げる

3行まとめ

  • アウトプットの速度を上げたいが、記事を書くのは時間がかかる
  • SuperwhisperとVSCodeのCopilot Agentを組み合せて、音声からブログを書き上げるワークフローを組んだ
  • 実際に使っているpromptを含め、真似しやすいように詳しく紹介

背景: アウトプット速度を上げたい & LLMの急速な進化

相対的には私はアウトプットをしている自覚はありますが、それでも書きたいエントリが書き切れていません。エントリを書くのはそれなりに時間が必要なため、なんとかその時間を短縮したいなぁという思いがありました。

一方、私が語るまでもなく、最近のLLMは急速に進化を遂げています。LLMの進化に乗っかり、アウトプットの速度向上をやれないか試してみようと思いました。

利用している技術

Superwhisper: 技術用語も認識する書き起しアプリ

アウトプットの速度を上げようと思ったとき、私が重要だと考えることは「頭の中にあるアイディアを頭の外に追い出す」ということです。つまり、頭の中にあるアイディアをいかにデジタルで扱える形にするか、ということです。

アイディアを頭から追い出すには話したり、タイピングするなどがありますが、速度だけで言えば私の場合は話すのが一番早いです。書き起こしは以前はフィラー(言いよどみや間投詞)や専門用語(カタカナや英語)の認識がボトルネックでなかなか実用には遠いものが多かったですが、Superwhisperはそれを打破してくれるものでした。

SuperwhisperはMacやiPhoneなどで動作する書き起こしアプリです。アプリにはローカルにモデルファイルを置いてローカルで書き起こしもできますし、有料版ではクラウドの最新モデルも利用できます。

Superwhisperは月額プランがあり、$8.49から利用できます。安くはないですが、自分で書き起こす手間を考えると十分価値があると感じています。また、買い切り(Lifetimeプラン)もありますが、最近のLLMの進化を考えるとずっとSuperwhisperがいいかは分かりません。そのため、最初は月額プランで始めるのがよいかなと思います。

また「数分話した後の音声認識が全く使いものにならなかった...」というのは音声認識アプリあるあるだと思います(あって欲しくはないですが...)。Superwhisperは元の音声ファイル、認識されたテキスト、どのセグメントで何を話したか、LLM変換後のテキストなどをJSONとして出力してくれます。万が一認識結果が気に入らなくても、音声を他エンジンに再入力したり、JSONを使って自分で編集したりできるので、エンジニアとしても使いやすいツールです。

万が一認識結果がイマイチでも安心。元の音声ファイルや中間の認識結果をjsonで保持してくれる

Superwhisper自体は完璧ではないものの「後段の校正ステップで直せばいい」「トータルで執筆時間が減らせればよい」という気持ちで、どんどん頭の中にあるアイディアをテキストの落としていくことがとにかく重要です。

VSCode Copilot Agent: 自然言語で校正のワークフローを組み込む

頭の中にあるアイディアをテキストに落とせたとして、そのままではブログのエントリにはできません。例えば、以下のような修正が必要でしょう。

  • 音声の認識誤りの修正
  • 話言葉を書き言葉に修正
  • 話言葉は話題を行ったり来たりすることがあるので、同じような話題はまとめる

こういった作業をChatGPTにさせようと試みましたが、一度に全てを処理しようとすると難しかったです。また、修正が入るたびにpromptのコピペなどが煩雑で、ワークフロー化の必要性を感じました。ここ最近、同じようなことを試されている方がいらっしゃったので、そのアプローチを真似することにしました。

校正に使うエディタはClineやCursorなどAgentが使えればどれでも真似できると思いますが、今回はVSCodeのCopilot Agentで実装しています。理由はいくつかありますが

  • 先日、Reusable prompt filesが利用できるようになった
    • instructionのファイルの分割に対応できるので、今回のように細かいステップに分けて実装するケースに対応できるようになった
  • 細かいツールは次々と出てくるが、MicrosoftやGitHubがバックボーンにいるVSCodeにかけておくのが将来的にもよさそうと判断した

などが選択した理由です。

実用例: どれくらい早くアウトプットできるようになるか

先日書いたエントリは実はこのエントリで紹介するワークフローで書いたものでした。

鴨川を散歩しながら15~20分話した内容を元にSuperwhisperで書き起こしをさせ、後述のワークフローで校正しました。もちろん、完全に自動ではなく、図を入れたりコードを差し込むなどの手動の作業は必要です。とはいえ、これまでの3分の1から半分程度まで作業量は減少したという実感があります。完全ではないものの、叩き台の下書きがあるというのはエントリを書くまでのハードルをかなり下げてくれます。時間的な意味でも、心理的な意味でもアウトプットをより早く出せるようになりそうだという実感があります。

実際のワークフロー

実際のディレクトリツリーは以下のようになっています。

ディレクトリツリー(クリックで開きます)

.
├── README.md
├── .github
│   ├── copilot-instructions.md
│   ├── prompts
│   │   ├── 001_general_writing_characteristics.prompt.md
│   │   ├── 002_writing_characteristics.prompt.md
│   │   ├── 011_transcript_segmentation.prompt.md
│   │   ├── 012_clarify_unclear_words.prompt.md
│   │   ├── 013_colloquial_to_written.prompt.md
│   │   ├── 014_markdown_structuring.prompt.md
│   │   ├── 015_writing_style_application.prompt.md
│   │   ├── 016_reorganized.prompt.md
│   │   ├── 021_review_by_data_engineer.prompt.md
│   │   ├── 022_proofreading.prompt.md
│   │   ├── 031_integrated_review_proofread.prompt.md
│   │   └── 032_title_generation.prompt.md
├── entries
│   └── 20250427_transcript_refiner
│       ├── 000_raw.md
│       ├── 011_transcript_segmentation.md
│       ├── 012_clarify_unclear_words.md
│       ├── 012_replacements.yaml
│       ├── 013_colloquial_to_written.md
│       ├── 014_markdown_structuring.md
│       ├── 015_style_applied.md
│       ├── 016_reorganized.md
│       ├── 031_integrated_review_proofread.md
│       ├── 032_title_generation.md
│       └── reviews
│           ├── proofreading_001.md
│           ├── reviewed_by_data_engineer_001.md
│           ├── reviewed_by_data_engineer_002.md
│           └── reviewed_by_data_engineer_003.md
├── general_writing_characteristics.md
├── inputs
│   ├── 1998.md
│   └── 2645.md
├── pyproject.toml
├── replacements.yaml
├── scripts
│   ├── fetch_esa_post
│   │   ├── README.md
│   │   ├── main.py
│   │   ├── pyproject.toml
│   │   └── uv.lock
│   └── stepctl
│       ├── main.py
│       ├── pyproject.toml
│       └── uv.lock
└── tmp
    └── all_inputs.md

ディレクトリ構成の特徴としては、以下の通りです。

  • 本体は.github/prompts/*.mdファイル
    • ここで各ステップで行なう手順を定義したり、ステップの目的や入出力が何かを説明する
  • 次に重要なのが.github/copilot-instructions.md
    • 各ステップで用いる用語の定義、ステップ横断で気にして欲しい内容を記述する
  • 実際の作業ディレクトリはentries/*/*.md
    • 各ステップの中間生成物などが置かれる

実際の指示は以下のようにします。「entries/20250427_transcript_refinerに対して、ステップ13を実行して」という指示でもよいですが、Reusable promptでどのpromptを指定するかを明示するのがよいです(Add Context...のボタンから明示できます)。

そうすると、以下のようにステップ毎に校正を進めてくれます。必要なpromptsを見た上で、入出力のファイルに気を付けながら作業を進めてくれていることが分かりますね。かわいい!

ステップは10以上に分割しており、実際の各ステップの内容は以下の通りです。

ステップ1: 横断的な文体分析(クリックで開きます)

---
id: 001_general_writing_characteristics
title: 横断的な文体分析

depends_on: []

references:
  - path: root:inputs/*.md # ルート直下の inputs ディレクトリを指定
    intent: 分析対象の全記事を読み込む

outputs:
  - path: root:general_writing_characteristics.md
    intent: 分析結果を書き出す
  - path: root:tmp/all_inputs.md
    intent: 分析のため全記事を結合した一時ファイル
---

## 目的
- リポジトリroot配下の`root:inputs/`ディレクトリにある様々な話題のブログ記事群を横断的に分析する
- 特定のトピックに限定せず、著者の一般的な文体特徴を抽出する
- 分析結果を今後の文章スタイル適用の一般的な参考資料として活用できるようにする

## 対象ディレクトリ
- リポジトリroot配下の`root:inputs/`ディレクトリ内の`*.md`ファイル全て

## 準備ステップ
- 分析対象ファイルの結合
  - すべての記事を1つのファイルに結合して全体像を把握しやすくします
  - 以下の複数行のスクリプトをターミナルで実行し、結合ファイル(`root:tmp/all_inputs.md`)を作成してください
  - bashではなくfishを使用してください。スクリプトの内容の書き換えは禁止されています

\`\`\`fish
mkdir -p tmp
rm -f tmp/all_inputs.md
echo "" > tmp/all_inputs.md
for file in inputs/*.md # この部分はターミナルコマンドなので変更しない
  echo "## ファイル: $file" >> tmp/all_inputs.md
  echo "" >> tmp/all_inputs.md
  cat "$file" >> tmp/all_inputs.md
  echo "" >> tmp/all_inputs.md
  echo "---" >> tmp/all_inputs.md
  echo "" >> tmp/all_inputs.md
end
\`\`\`

## 分析手順
1. 結合ファイル`root:tmp/all_inputs.md`を読み込み、文体傾向を把握する
2. 以下の観点から文体を分析する
   - 全体的な文体の特徴(専門性、語り口、一人称/三人称の使い方など)
   - 頻出する特徴的な表現パターン(独自の言い回し、二段括弧による補足説明など)
   - 文章構成の特徴(「3行まとめ」の使用、見出しの構成パターンなど)
   - 読者とのコミュニケーション手法(質問形式、対話的表現など)
3. 分析結果を以下の構造に従って記述する

\`\`\`markdown
# 文体的特徴の分析

## 全体的な特徴

1. 専門的かつ実践的な文体:
   - 具体例1
   - 具体例2
   - 具体例3

2. 一人称視点の使用:
   - 具体例1
   - 具体例2
   - 具体例3

(以下同様の形式で続ける)

## 固有の表現パターン

1. エンジニア独特の言い回し:
   - 具体例1
   - 具体例2

2. 二段括弧による補足説明:
   - 具体例1
   - 具体例2

(以下同様の形式で続ける)

## 文章構成の特徴

1. 3行まとめの活用:
   - 具体例1
   - 具体例2

2. 背景 → 課題 → 解決策 → まとめの流れ:
   - 具体例1
   - 具体例2

(以下同様の形式で続ける)

## 総括

(文体の特徴をまとめた結論)
\`\`\`

4. 分析は具体例を引用し、どの記事のどの部分に見られる特徴かを明示する
5. 特に顕著な特徴に焦点を当てつつ、網羅的な分析を心がける

## 記述上の注意点
- 強調するためにmarkdownの`**`を使用しない
- 括弧は全角ではなく半角を用いる(括弧の前後にスペースを入れない)
- バッククオートの前後にスペースを入れない
  - OK: このファイル`example.md`を
  - NG: このファイル `example.md` を
- markdownにインデントを深くする際はスペース2つを使用する
- 箇条書きの末尾に句読点は不要

## 成果物
- 分析結果をリポジトリルート配下の`root:general_writing_characteristics.md`に保存する

ステップ2: エントリーの文体分析(クリックで開きます)

---
id: 002_writing_characteristics
title: エントリーの文体分析

depends_on:
  - step:001_general_writing_characteristics

references:
  - path: root:general_writing_characteristics.md
    intent: 一般的な文体特徴を読み込む
  - path: entry:inputs/*.md
    intent: 分析対象のエントリー固有記事を読み込む

outputs:
  - path: entry:002_writing_characteristics.md
    intent: 分析結果を書き出す
---

## 目的
- 指定ディレクトリ配下のmarkdownファイルを走査し、以下を行なう
  1. 各ファイルのトピック / キーワード / 要点を抽出
  2. 著者固有の文体 / 口癖 / 構成上の特徴をリストアップ
- 抽出結果を後で再利用できるメタデータとして保存する

## 処理手順
- ルートディレクトリ直下の `root:general_writing_characteristics.md` のファイルを読み込み、今回の話題に限らない著者の特徴を読み取る
- 各markdownを読み込み、以下を抽出する
  - `title`: H1またはファイル名
  - `topics`: セクション見出し・タグ・本文頻出語から3~5個
  - `summary`: 120字以内で要点を箇条書き3行
- 全ファイルを横断し、文体の共通点を収集する
  - よく使う語尾や表現
    - 例「〜かな」「なるほど」
  - 構成パターン
    - 例: 見出し > 箇条書き > 解説
  - 好むフォーマット
    - 半角カッコ、強調なし、など
- 作業ディレクトリ配下の`entry:002_writing_characteristics.md`に以下の内容を出力(すでに存在する場合は上書き)
  - `root:general_writing_characteristics.md` には見られない固有の特徴を追加する
  - 語尾
  - 口癖
  - 構成

ステップ11: 話言葉の書き起しを分割するステップ(クリックで開きます)

---
id: 011_transcript_segmentation
title: 話言葉の書き起しを分割するステップ

depends_on: []

references:
  - path: entry:000_raw.md
    intent: 書き起こし元テキストを読み込む

outputs:
  - path: entry:011_transcript_segmentation.md
    intent: 分割済みテキストを書き出す
---

## 目的
口頭で話した内容をもとに書き起こしたテキストは、一文が非常に長くなりやすく、さらにパラグラフや文の区切りが付いていない場合があります。そのため、書き起こしテキストを意味のまとまりごとに適切に分割し、句読点を追加して一文を読みやすい長さに整形することで、後続ステップでの可読性と理解のしやすさを高めます。

## 手順
- `entry:000_raw.md`は書き起こし直後の生データであり、文や段落の区切りが存在しません
  - まずは文の切れ目を推定し、段落構造を組み立てます
- 一文が極端に長い場合、句読点を追加しつつ分割してください
  - 分割時に発言者の意図や文意が変わるような修正は行わないでください
- filepath のような余計なメタデータを出力することは禁止されています
- 段落は空行で区切り、改行だけで段落が途切れないように注意してください
- 分割した結果は`entry:011_transcript_segmentation.md`に保存してください

ステップ12: 分からない/意味が通じない単語を確認するステップ(クリックで開きます)

---
id: 012_clarify_unclear_words
title: 分からない/意味が通じない単語を確認するステップ

depends_on:
  - step:011_transcript_segmentation

references:
  - path: entry:011_transcript_segmentation.md
    intent: 分割済みテキストを読み込む
  - path: root:replacements.yaml
    intent: 恒常置換ルールを参照する
  - path: entry:012_replacements.yaml
    intent: セッション固有置換ペアを入力値として使用

outputs:
  - path: entry:012_clarify_unclear_words.md
    intent: 置換確定テキストを書き出す
  - path: entry:012_replacements.yaml
    intent: ユーザー確認後の置換ペアを保存
  - path: chat:yaml_snippet # チャット出力は特別なパスで表現
    intent: 新規ペア候補の YAML スニペットをチャット内で提示する
---

## 目的
不明確な単語や表現を特定し、適切な置換を行うことで、書き起し文の正確性と読みやすさを向上させる。

## yamlフォーマット

\`\`\`yaml
pairs:
  - wrong: "プロリク"
    correct: "プルリクエスト" # wrongと同じ内容である場合もあります。これはユーザーが「置き換えないで欲しい」という意図と表現したものになります
    context: "こういったプロリクでは" # ここについては存在しない場合もある
\`\`\`

## 作業手順
- yamlの読み込み(読込専用)
  - a: ルートの `root:replacements.yaml` をロード
  - b: `entry:012_replacements.yaml`が存在する場合はロード
  - aとbの`pairs`を結合し、同一wrongはbを優先
- 自動置換
  - 一つ前の手順で得たペアを用い、`entry:011_transcript_segmentation.md`の単語を完全一致でwrongからcorrectに置換する
  - 大小文字・全半角は正規化して比較すること
- 意味不明/不自然語の抽出
  - 置換後テキストから、読みにくい・文意が崩れる・固有名が誤っている語を検出しリスト化
    - 置換対象の粒度は単語程度を想定
      - 例: 「プロリク」→「プルリクエスト」など
    - フレーズや文全体を置換することは避ける
  - 各候補には前後1~2文(50~100字程度)の文脈を添付する
  - 候補数が多い場合には先頭から10件ずつ提示する
- ユーザー確認フェーズ
  - 一つ前の手順で作成したのリストをユーザーに提示する
  - ユーザーが作業をしやすいように、前述のyamlフォーマットでチャットに出力する (`chat:yaml_snippet`)
  - リストを元にユーザー自身が`entry:012_replacements.yaml`を更新する
  - ユーザーの明確な指示があるまでテキストは変更しない
    - ユーザーが「完了」などの返答をした場合、次に進む
- 反映と保存
  - `entry:012_replacements.yaml`の内容を読み込み、意味不明/不自然語を置換する
  - 完成テキストを`entry:012_clarify_unclear_words.md`に保存する
    - 完成テキストをチャット内に表示する必要はありません

ステップ13: 口語を書き言葉に書き換えるステップ(クリックで開きます)

---
id: 013_colloquial_to_written
title: 口語を書き言葉に書き換えるステップ

depends_on:
  - step:012_clarify_unclear_words

references:
  - path: entry:012_clarify_unclear_words.md
    intent: 置換確定テキストを読み込む

outputs:
  - path: entry:013_colloquial_to_written.md
    intent: 書き言葉に変換したテキストを書き出す
---

## 目的
書き起こしテキストに含まれる口語表現や話し言葉特有の表現を適切な書き言葉に変換し、より読みやすく洗練された文章にすることで、読者の理解を促進する。最終アウトプット先はブログエントリであるため、読者にとって読みやすい文章形式に整えることが重要である。

## 手順
- `entry:012_clarify_unclear_words.md`は口語の書き起こしなので、話言葉が多いです
- 書き言葉に書き換えて、`entry:013_colloquial_to_written.md`に保存してください。文意は変えないでください
- フィラーは消してください

例:

- 「みたいなことをやるようにしていました」 => 「ということをやるようにしました」
- 「なんで自分がやり始めたかっていうと」 => 「なぜ自分がやり始めたかといえば」
- 「というか」 => 「」

ステップ14: 構造化ステップ(クリックで開きます)

---
id: 014_markdown_structuring
title: 構造化ステップ

depends_on:
  - step:013_colloquial_to_written

references:
  - path: entry:013_colloquial_to_written.md
    intent: 書き言葉に変換したテキストを読み込む

outputs:
  - path: entry:014_markdown_structuring.md
    intent: Markdown で構造化されたテキストを書き出す
---

## 目的
書き言葉に変換されたテキストをMarkdown形式で適切に構造化する。見出しや箇条書きを活用して文書の階層構造と論理的な流れを明確にし、最終的なブログエントリとしての読みやすさを向上させる。出力内容は長文になるため、適切な構造化によって読者が情報を把握しやすくなり、疲れずに読み進められる文書構造を提供することが重要である。

## 手順
- `entry:013_colloquial_to_written.md`の内容をmarkdownを使って構造化して`entry:014_markdown_structuring.md`に出力してください
  - 意味のまとまりを`##`を使ってセクションに分割しましょう
  - 2つ以上の並列構造にある文は`-`を使って箇条書きにしましょう
    - 箇条書きにする場合は、必ず2つ以上の要素を含めてください
    - 要素が1つしかない場合(並列構造にない場合)は、箇条書きにせず通常の文として記述してください
  - 箇条書きの末尾には句読点は不要です
- 過剰な箇条書きの使用は避けてください
  - ダメな例: セクション内の全ての文が箇条書きになっている
  - 並列構造であることを明確にしたい場合など、効果的に箇条書きは利用しましょう
- 要約は一切行なわないでください
  - テキスト内の情報量は元の口語起こしと同等のままでお願いします
- 箇条書きの一つのitemの中で複数文がある場合はitemを追加してください
  - 文に意味的なまとまりがある場合、ネストした箇条書きにしてください
  - 箇条書きをする際は(1)のような順番を出力する必要はありません
- 段落内の文と文の間(句点の直後を含む)には改行を入れないでください
  - ただし、箇条書きブロックの開始前と終了後には、それぞれ1行ずつ改行を入れてください

ステップ15: 文体適用ステップ(クリックで開きます)

---
id: 015_writing_style_application
title: 文体適用ステップ

depends_on:
  - step:014_markdown_structuring
  - step:002_writing_characteristics
  - step:001_general_writing_characteristics

references:
  - path: entry:014_markdown_structuring.md
    intent: 構造化済みテキストを読み込む
  - path: entry:002_writing_characteristics.md
    intent: エントリー固有の文体特徴を読み込む
  - path: root:general_writing_characteristics.md
    intent: 一般的な文体特徴を読み込む

outputs:
  - path: entry:015_style_applied.md
    intent: 文体適用後のテキストを書き出す
---

## 目的
構造化されたテキストに著者固有の文体特徴を適用し、一貫性のある個性的な文章として仕上げる。エントリー固有の文体特徴と一般的な文体特徴を組み合わせ、著者の個性が反映された読みやすいブログエントリを作成する。

文体の特徴は、作者が過去に書いたエントリーを元にしたものである。エントリー固有の文体特徴は、関連するカテゴリーやトピックの文章を元にしたものである。また、入力となる文章はLLMを使って出力されたテキストであるため、著者自身の特徴を入れることで、著者が書いたような文章に仕上げることを目的とする。

## 手順
`entry:014_markdown_structuring.md`を元に、`entry:002_writing_characteristics.md`および`root:general_writing_characteristics.md`の語尾 / 口癖 / 構成 / フォーマットを適用し、著者の文体でリライトする。

- `entry:002_writing_characteristics.md`を読み取り、語尾例、口癖、構成パターン、フォーマットの好みを取得
- `root:general_writing_characteristics.md`も読み取り、横断的な文体特徴も考慮する
  - 文体特徴に矛盾がある場合は、`entry:002_writing_characteristics.md`の内容を優先する
- `entry:014_markdown_structuring.md` の各文を取得した特徴に沿ってリライト
  - 内容は要約しない
  - 専門用語や固有名は変更不可
    - 例: Google Cloudを「GCP」に変更しない
    - 例: BigQueryを「BQ」に変更しない(コマンド名としての`bq`は問題ない)
  - 同一のものを指す場合には同じ表現を一貫して使うこと
    - 例: 「Pull Request」を「PR」や「プルリクエスト」とマチマチに書いてはいけない
- リライト後の全文を `entry:015_style_applied.md` として保存

ステップ16: 構成再編ステップ(クリックで開きます)

---
id: 016_reorganized
title: 構成再編ステップ

depends_on:
  - step:015_writing_style_application

references:
  - path: entry:015_style_applied.md
    intent: 文体適用済みテキストを読み込む

outputs:
  - path: entry:016_reorganized.md
    intent: トピック再編成後のテキストを書き出す
---

## 目的
入力元となる文章は口頭で話された内容をもとにしており、話題の順番が前後している可能性がある。そのため、話題の流れを整理し、読みやすい文章としてまとめることが重要である。

段落や節の順序を最適化することで、最終的なブログエントリとしての論理的一貫性と読みやすさが向上する。あまりにも些細なトピックや、大筋の内容に関係のない内容については、全体の流れを損なわない範囲で削除を許容する。ただし、主要な内容や著者の意図を変えるほどの大幅な削除は避けること。

## 手順
`entry:015_style_applied.md`を対象に、散在する同一トピックをまとめ、話題の流れを整理して読みやすい章立てに再編成する。

- トピック抽出
  - 見出しと段落を走査し、主要キーワードからトピックを識別
  - 同一トピックを示す見出し語は統一する(`ランニングフォーム``フォーム改善`などは前者に統一など)
- セクション統合・並び替え
  - 同一トピックに属する段落を一つのセクションへ集約
  - 論理展開が自然になるようセクション順を以下の優先度で並べ替え(あくまで一例)
    - 全体概要 / 前提
    - 原因・背景
    - 対応策・手順
    - 結果・考察
    - まとめ / 今後の課題
- クロスリファレンス調整
  - セクション移動に伴い内部参照(「前述の〜」「後述する〜」など)の語を修正
  - 同一内容の重複説明が出現した場合、冗長にならないようどちらか一方に統合
- フォーマット維持
  - 見出しレベル(`##`以上)や箇条書きの階層は保持
  - コードブロック / 引用部は位置のみ移動可、内容は改変不可
- 用語 / 表記の一貫性確認
  - ステップ015で決定した用語統一ルールを尊重し、再配置後も揺れが無いか最終チェック
- 不要な内容の省略
  - どの話題にも属さない内容を省略する場合、チャット内に省略した部分を記載しておく
- 保存
  - 完成した文章を`entry:016_reorganized.md`として保存し終了

ステップ21: データエンジニアによるレビュー(クリックで開きます)

---
id: 021_review_by_data_engineer
title: データエンジニアによるレビュー

depends_on:
  - step:016_reorganized

references:
  - path: entry:016_reorganized.md
    intent: 再編成済みテキストを読み込む

outputs:
  - path: entry:reviews/reviewed_by_data_engineer_001.md
    intent: データエンジニアによるレビュー結果を書き出す (ファイルが存在する場合は連番をインクリメント)
---

## 想定読者
以下のようなSWEとしての素養を持ったデータエンジニアあるいはAnalytics Engineer(中級レベル)。

- 想定経験
  - IT業界 3〜5年
  - データ基盤または分析基盤を1プロジェクト以上リード
- コアスキル
  - BigQuery / Snowflake のテーブル設計とパフォーマンスチューニング
  - Dimensional Modeling
    - star schema / snowflake schema
    - fact table / dimension table 設計
  - dbt でのモデル分割・マクロ開発・CI 連携、semantic layer 構築
  - Airflow での DAG 設計と依存関係管理
  - Terraform で Google Cloud リソース定義 (ベストプラクティス深掘りは未経験)
  - Python でのデータ変換スクリプトと自動テスト (pytest + mypy)
- SWE 的素養
  - GitHub Flow に基づく PR ベースの開発
  - SOLID 原則やデザインパターンを小〜中規模コードに適用
  - GitHub Actions でリント・ユニットテスト・dbt build の自動化
- インフラ理解
  - Cloud Run / Cloud Functions へデプロイし、環境変数やサービスアカウント設定が行える
  - VPC や IAM の細かな最適化、ランタイムチューニングは詳しくない
  - 監視は Cloud Monitoring や Datadog のダッシュボード閲覧レベル
- できること
  - 事業ドメインを踏まえた dimensional modeling
    - slowly changing dimension (SCD)
    - snapshot strategy
  - dbt のリファクタリングとマクロ共通化
  - metrics layer / semantic layer を定義し、BI で再利用可能な指標を提供
  - ビジネス指標定義とデータ品質モニタリング設計
  - データドリブンな A/B テスト設計支援
  - Airflow → Cloud Run → BigQuery のパイプライン開発
- 苦手・サポートが必要
  - Kubernetes クラスタ運用や Helm チャート管理
  - 高スループットのストリーミング処理 (Kafka, Dataflow) の最適化
  - IAM 権限の最小化設計と運用ガバナンス
- アプリケーション / バックエンドエンジニアとの違い
  - API 設計よりもデータモデル整備と ETL/ELT、指標の信頼性確保を重視
  - SLA/SLO はデータリフレッシュ頻度と品質指標が中心 (レスポンスレイテンシは二次要件)
  - ビジネスロジックを dbt/SQL に寄せ、アプリ側は参照に徹する
- よく使うツール
  - BigQuery、Snowflake、dbt、Airflow、Terraform、Cloud Run
  - GitHub Actions、Cloud Monitoring、Datadog
  - Looker / Looker Studio、Tableau
  - Great Expectations
- 今後の成長課題
  - データ品質とガバナンスを組織プロセスに統合し、継続的改善を可能にする
  - メタデータ管理と可観測性を高め、障害検知から復旧までの時間を短縮する
  - ビジネス合意の SLA/SLO を定量的に計測し、改善サイクルを確立する
  - スケーラビリティとコスト効率の最適バランスを、プロダクトエンジニアや SRE と協働して設計する

## 出力項目
- ステップ016の出力内容である`entry:016_reorganized.md`を読んだ上で、後述するチェック観点と感想パートをそれぞれ述べてください
- 出力は`outputs`で指定されたファイルに出力してください
- チェック観点
  - 以下の観点をチェックしてください、問題がない箇所は出力する必要はありません
    - てにをはの誤用や欠落
    - 一文が長すぎて理解が難しい箇所がないか
    - 主語抜けや指示語の曖昧さがないか
    - 冗長な表現(二重表現)
      - 例: 頭痛が痛い
      - 例: 違和感を感じる
      - 例: SWEエンジニア
      - 例: SREエンジニア
    - 段落間や文間の論理の飛びがないか
    - 一般的には未定義の用語(例: 社内用語)を使用していないか
      - 一般的であっても、SWEの中でも曖昧性がある用語を使用していないか
  - 指摘だけでなく、どう出力するとよくなるか改善案も出力しましょう
- 感想パート
  - 率直な感想を5~10行程度で箇条書きで述べる
    - 抽象的な感想ではなく、「具体的にここが面白かった」、「明日から真似できそうだった」などの感想を述べる
    - ↑はあくまで例であり、必ずしもこの内容に沿う必要はない
  - 改善案
    - 具体的にどこが難しかったか、分かりにくかったかを述べる

ステップ22: 記者ハンドブックによる校正ステップ(クリックで開きます)

---
id: 022_proofreading
title: 記者ハンドブックによる校正ステップ

depends_on:
  - step:016_reorganized

references:
  - path: entry:016_reorganized.md
    intent: 再編成済みテキストを読み込む

outputs:
  - path: entry:reviews/proofreading_001.md
    intent: 校正結果を書き出す (ファイルが存在する場合は連番をインクリメント)
---

## 目的
- 日本語技術ブログ下書きを、読みやすさと一貫性を重視して校正する
- チェック対象は用字用語、漢字とひらがなの使い分け、ら抜き・い抜き語、二重表現、冗長表現、文構造
- ファクトチェックやタイトル生成は行わない

## 基本ポリシー
1. 常用漢字表に含まれる字を原則使用
2. 常用外漢字や難読字はひらがな表記
3. 同音異義語は文脈に合う漢字を選択
4. 文章全体で表記を統一(例: たとえば/例えば を混在させない)
5. 一文一義・40〜70字目安、読点で調整
6. コードブロック・引用・パス・URLは非改変(誤字のみ修正可)

## 校正チェックリスト
1. 用字用語統一
2. 漢字をひらく/閉じる基準
3. ら抜き・い抜き・二重表現
4. 主語述語対応、読点位置、冗長語削除
5. 専門用語初出時の注釈

## 出力フォーマット

\`\`\`md
## 指摘1
- `コンテキスト`: …しかしながら、本番環境で <校正前>見れる</校正前> よう…
- `校正前`: 見れる
- `校正後`: 見られる
- `理由`: 可能表現のら抜き言葉を是正

## 指摘2
- `コンテキスト`: この処理を <校正前>おこなう</校正前> 場合は…
- `校正前`: おこなう
- `校正後`: 行う
- `理由`: 用字用語統一

## 指摘3
- `コンテキスト`: …設定することが <校正前>できます</校正前>- `校正前`: 〜することができます
- `校正後`: 〜できます
- `理由`: 二重表現の冗長削除
\`\`\`

- 各指摘をナンバリング
- コンテキストは前後1〜2文、合計100字以内を目安
- `<校正前>`タグは説明用。実装時は任意の強調でも可
- 指摘がない場合は「指摘なし」とだけ出力
- 出力は`outputs`で指定されたファイルに出力してください

## 処理手順
1. 入力文を文単位で解析
2. チェックリストに沿って疑義箇所を抽出
3. 各箇所に校正案を適用し、前後コンテキストを取得
4. 指摘リストをフォーマット通り生成し、指定されたファイルへ出力
5. 分量が多い場合は指摘リストを分割して出力しても構わない

ステップ31: レビューと校正結果の統合(クリックで開きます)

---
id: 031_integrated_review_proofread
title: レビューと校正結果の統合
depends_on:
  - step:021_review_by_data_engineer
  - step:022_proofreading
references:
  - path: entry:016_reorganized.md
    intent: 修正のベースとなる再編成済みテキストを読み込む
  - path: entry:reviews/*.md
    intent: データエンジニアレビュー、記者ハンドブック校正、人間によるレビューなど、様々な種類のレビュー/校正結果(複数ファイルの場合あり)を読み込む
outputs:
  - path: entry:031_integrated_review_proofread.md
    intent: レビューと校正結果を反映した最終テキストを書き出す
---

## 目的
ステップ016で再編成されたテキスト(`entry:016_reorganized.md`)に対し、ステップ021のデータエンジニアレビュー結果とステップ022の校正結果を総合的に反映させ、最終的なテキストを作成します。

## 処理手順
1.  `entry:016_reorganized.md` ファイルの内容を読み込みます
2.  `entry:reviews/` ディレクトリ内に存在する `.md` ファイルをすべて読み込みます
3.  読み込んだレビュー/校正結果を確認します
    - レビュー結果(`reviewed_by_data_engineer_*.md` など): 内容の正確性、構成の妥当性、表現の分かりやすさなどに関する指摘や改善提案が含まれます。データエンジニアレビュー、記者ハンドブック校正、人間によるレビューなど、様々な種類のレビュー結果が含まれる可能性があります
    - 校正結果(`proofreading_*.md` など): 用字用語の統一、文法誤り、ら抜き言葉、冗長表現などの機械的な修正指摘が含まれます。記者ハンドブック校正など、様々な種類の校正結果が含まれる可能性があります
4.  `entry:016_reorganized.md` の内容に対し、以下の観点で修正を加えます
    - レビュー結果で提案された内容・構成・表現の改善を反映します
    - 校正結果で指摘された用字用語、文法、表現の誤りを修正します
    - レビュー指摘と校正指摘が競合する場合は、文脈を考慮し、より適切と思われる方を優先するか、両方の意図を満たすように調整します。例えば、校正指摘は機械的な修正であるため、レビュー指摘で意図的にその表現が使われている場合はレビュー指摘を優先するなど、判断が必要です
5.  全ての修正を反映した最終的なテキストを `entry:031_integrated_review_proofread.md` として出力します

ステップ32: 記事タイトル案の生成(クリックで開きます)

---
id: 032_title_generation
title: 記事タイトル案の生成
depends_on:
  - step:031_integrated_review_proofread
references:
  - path: entry:031_integrated_review_proofread.md
    intent: タイトル生成のための最終校正済みテキストを読み込む
outputs:
  - path: entry:032_title_generation.md
    intent: 生成したタイトル候補と選定理由を書き出す
---

## 目的
ステップ31で最終校正されたテキスト(`entry:031_integrated_review_proofread.md`)に対して、バラエティ豊かな10個のタイトル案を生成し、各タイトル案の選定理由を付記します。

## 処理手順
- `entry:031_integrated_review_proofread.md` ファイルの内容を読み込みます
- 記事の内容を十分に理解し、以下の観点を考慮して10個のタイトル案を生成します:
  - 記事の主要なテーマや主張
  - 対象となる読者層のニーズや関心
  - 記事が提供する価値や解決策
  - 記事の独自性や特徴
- 以下のようなバリエーションを含む10個のタイトル案を作成します:
  - 真面目で堅実なタイトル(専門家や業界関係者に向けたもの)
  - 特定の層に刺さるタイトル(特定のニーズや関心を持つ読者に訴求するもの)
  - SEO最適化タイトル(検索エンジンでの表示を意識したもの)
  - 好奇心を引くタイトル(クリック率を意識したもの)
  - 問題解決型タイトル(読者の課題解決を強調するもの)
  - ベネフィット訴求タイトル(記事から得られる利益を明示するもの)
  - 数字を含むタイトル(具体性を高めるもの)
  - 質問形式のタイトル(読者の興味を引くもの)
  - 比較・対比を用いたタイトル(選択肢や違いを示すもの)
  - 簡潔明瞭なタイトル(記事の本質を端的に表現するもの)
- 各タイトル案について、以下の内容を含む選定理由を記載します:
  - そのタイトルが訴求する読者層
  - タイトルが記事内容とどのように合致しているか
  - タイトルがもたらす効果や期待される反応
  - タイトルの強みや特徴
- 全てのタイトル案と選定理由を `entry:032_title_generation.md` として出力します
  - 出力形式は以下の構成に従います:

\`\`\`markdown
# 記事タイトル候補

## タイトル候補とその選定理由

### 1: [タイトル案1]
[選定理由の詳細説明]

### 2: [タイトル案2]
[選定理由の詳細説明]

...

### 10: [タイトル案10]
[選定理由の詳細説明]
\`\`\`

### 過去に採択されたタイトル例
以下は過去に採択されたタイトルの例です。これらを参考にして、同様の質と魅力を持つタイトル案を生成してください。なお、過去のタイトルを直接使用するのではなく、現在の記事内容に合わせた独自のタイトルを考案することが重要です。

- `ChatGPTをランニング用のパーソナルトレーナーとして使い倒す`
- `ジュニアエンジニアからシニアエンジニアになるまでに自分がやっていたことまとめ`
- `アナリスト出身の人にバッチ処理を書いてもらう際にレクチャー & サポートしたことメモ`
- `データエンジニアリングに関するOSSに自分がPull Requestを送る際に気をつけていること`
- `JSON Schemaで組織独自の制約をdbtのyamlに設定する`
- `知っておくと運用で役に立つbq loadのTips`
- `Data Contractに向けたProtocol Buffersの調査`
- `Looker Studioの魅力と便利な使い方を紹介します`
- `BigQueryを補完する技術: DuckDBとDataflowでのデータ処理入門`

### 補足事項
- タイトルはクリック率や読者の興味を引くだけでなく、記事の内容を正確に反映したものである必要があります
- 記事のテーマやトーン、対象読者層を考慮したタイトルを心がけてください
- 各タイトル案は互いに似通ったものではなく、異なる角度や表現方法を用いた多様性のあるものにしてください
- タイトルの長さは適切に調整し、必要に応じてサブタイトルの提案も含めることができます

.github/copilot-instructions.md(クリックで開きます)

# AIアシスタント向け作業指示書: transcript-refiner

## 目的
このドキュメントは、AIアシスタント(あなた)が、音声書き起こしツール(例: [Superwhisper](https://superwhisper.com/))によって生成されたテキストを、外部公開可能な品質のブログ記事へと段階的に変換や構成するための作業指示書です。

LLMを用いた高品質な記事生成は一度の処理では難しいため、このリポジトリでは10以上のステップに分割されたワークフローを採用しています。各ステップは特定の変換処理を担当し、前のステップの出力を受け取って次のステップへと繋ぎます。

この指示書には、ワークフロー全体を正確に実行するために必要な以下の情報が含まれています。

- ファイル命名規則、Front-matter、リンク記法: 各ステップやファイルを参照・操作するための統一ルール
- 全体ワークフローと依存関係: 各ステップの実行順序と繋がり
- ステップ内作業フロー: 各ステップで共通する基本的な作業手順
- 入出力ファイルの定義: 各ステップがどのファイルを読み込み、どのファイルを生成・更新するか

これらの定義とルールを正確に理解し、各ステップのプロンプト指示に従って、依存関係やファイルの入出力を考慮しながら作業を進めてください。

## 用語の定義やルール
### ルール: ファイル命名規則
- ファイル名は、この`.github/copilot-instructions.md`ファイルおよび`.github/prompts`ディレクトリ内の各ステップのドキュメントで指定された命名規則に厳密に従ってください
- 他のエントリーのファイル名を参考にしないでください

### 定義: リンク記法
本文および front‑matter 内のパス指定は以下のスキームを使用してください。

| scheme | 基準ディレクトリ | target 例 | 説明 |
| --- | --- | --- | --- |
| entry | `entries/${ENTRY_DIR}/` | `entry:014_markdown_structuring.md` | エントリーディレクトリ内のファイル |
| input | `entries/${ENTRY_DIR}/inputs/` | `input:000_raw.md` | エントリーの入力ファイル |
| step | `.github/prompts/` | `step:011_transcript_segmentation` | ステップファイル (拡張子なし) |
| root | リポジトリ直下 | `root:README.md` | リポジトリルートからのファイル |
| chat | (なし) | `chat:yaml_snippet` | ファイルではなくチャットへの出力を示す特別なパス |

`${ENTRY_DIR}` は、「作業ディレクトリの確認」セクションの手順に従って特定された、現在作業中のエントリーディレクトリを指します(例: `entries/20250417_junior_engineer_to_senior_engineer`)。`entry:``input:` スキームで参照されるファイルの絶対パスは、この特定されたエントリーディレクトリに基づいて解決されます。作業開始時にユーザーから指示されたディレクトリ、または確認プロセスを経て特定されたディレクトリが `${ENTRY_DIR}` の値となります。

**参照の展開例**

- `entry:011_transcript_segmentation.md`
  - 展開先: `entries/20250417_junior_engineer_to_senior_engineer/011_transcript_segmentation.md`
- `input:000_raw.md`
  - 展開先: `entries/20250417_junior_engineer_to_senior_engineer/inputs/000_raw.md`
- `step:015_writing_style_application`
  - 展開先: `.github/prompts/015_writing_style_application.prompt.md`
- `root:replacements.yaml`
  - 展開先: `replacements.yaml`

### 定義: front‑matterフォーマット
front‑matterとは、`.prompt.md`ファイル(主に`.github/prompts/`ディレクトリ内に置かれるプロンプトファイル)の先頭に記述されるYAML形式のメタ情報です。ステップごとの依存関係や参照ファイルなど、作業手順をメタデータとしてまとめておくために利用します。

#### 目的
- 各ステップのIDや依存関係(`depends_on`)などを明示し、全体ワークフローを管理しやすくする
- 入出力ファイル(`references`, `outputs`)を一覧できるようにし、作業漏れを防ぐ
- ステップの概要(タイトルなど)を簡潔に記載し、ステップ全体の意図を把握しやすくする

#### front‑matterの具体的な定義
以下の項目を基本とします。

- `id`: ステップID(例: `012_clarify_unclear_words`)
  - ファイル名沿ったidが基本です
- `title`: ステップの短いタイトル(例: `分からない/意味が通じない単語を確認するステップ`)
- `depends_on`: 前段となるステップ(拡張子は付けず、`step:ステップID`の形式)
- `references`: 入力や参照のためのファイルリスト
- `outputs`: 生成・上書きされるファイルリスト

#### 注意点(使用上のポイント)
- front‑matterは`.github/prompts/`配下の「プロンプトファイル」にのみ必要です
- 出力される成果物(例: `entry:012_clarify_unclear_words.md`)にはfront‑matterを含めず、本文のみを書き出します
- プロンプトファイルを更新する際は、必ず先頭のYAMLブロック(`---`で囲んだ部分)を維持し、指示書内の形式を守ってください
- `references``outputs`では、リンクスキーム(`entry:`, `root:`, `step:`, `chat:`など)と`intent`(そのファイルをどう使うか)を正しく記述してください

#### front‑matterのサンプルコード
以下は`.github/prompts/012_clarify_unclear_words.prompt.md`の冒頭例です。参考にしてください。

\`\`\`markdown
---
id: 012_clarify_unclear_words # ファイル名沿ったid
title: 分からない/意味が通じない単語を確認するステップ

depends_on:
  # 拡張子なし、step スキームのみ
  - step:011_transcript_segmentation

references:
  # 読み取り専用ファイル(拡張子あり)
  - path: entry:011_transcript_segmentation.md
    intent: 分割済みテキストを読み込む
  - path: root:replacements.yaml
    intent: 恒常置換ルールを参照する
  - path: entry:012_replacements.yaml
    intent: セッション固有置換ペアを入力値として使用

outputs:
  # 生成・上書きファイル(拡張子あり)
  - path: entry:012_clarify_unclear_words.md
    intent: 置換確定テキストを書き出す
  - path: entry:012_replacements.yaml
    intent: ユーザー確認後の置換ペアを保存
  # チャット出力(特別なパス)
  - path: chat:yaml_snippet
    intent: 新規ペア候補の YAML スニペットをチャット内で提示する
---
\`\`\`

## 作業ワークフローについて
### 作業ディレクトリの確認
- 作業ディレクトリは`entries`配下のいずれかのディレクトリです
- 作業開始時に明示的に指定されたディレクトリに沿って、ステップの内容を実行してください
  - 作業開始時にディレクトリが明示的に指定されなかった場合は、必ずユーザーに「どのディレクトリで作業すべきか」を確認してください
  - ディレクトリを推測したり、暗黙的な指示だと解釈したりせず、明確な指示がない場合は必ず確認してください
  - たとえ可能性が高いと思われるディレクトリが1つしかなくても、作業前に確認を取ってください
- `.github`ディレクトリや`scripts`ディレクトリに関する作業の場合は、作業ディレクトリの確認は不要です
  - これらのディレクトリは`entries`ディレクトリに依存しない共通の設定やユーティリティを含むため、具体的なエントリーの指定なしで作業を進めることができます

### 全体ワークフロー
transcript-refinerは以下のワークフローに従って、音声の書き起こしテキストを整形・構造化し、最終的にブログ記事に仕上げます。各ステップは前のステップの出力を入力として使用する依存関係を持っています。

\`\`\`mermaid
graph TD
    001_general_writing_characteristics["ステップ1: 横断的な文体分析"]
    002_writing_characteristics["ステップ2: エントリーの文体分析"]
    001_general_writing_characteristics --> 002_writing_characteristics
    011_transcript_segmentation["ステップ11: 話言葉の書き起しを分割するステップ"]
    012_clarify_unclear_words["ステップ12: 分からない/意味が通じない単語を確認するステップ"]
    011_transcript_segmentation --> 012_clarify_unclear_words
    013_colloquial_to_written["ステップ13: 口語を書き言葉に書き換えるステップ"]
    012_clarify_unclear_words --> 013_colloquial_to_written
    014_markdown_structuring["ステップ14: 構造化ステップ"]
    013_colloquial_to_written --> 014_markdown_structuring
    015_writing_style_application["ステップ15: 文体適用ステップ"]
    014_markdown_structuring --> 015_writing_style_application
    002_writing_characteristics --> 015_writing_style_application
    001_general_writing_characteristics --> 015_writing_style_application
    016_reorganized["ステップ16: 構成再編ステップ"]
    015_writing_style_application --> 016_reorganized
    021_review_by_data_engineer["ステップ21: データエンジニアによるレビュー"]
    016_reorganized --> 021_review_by_data_engineer
    022_proofreading["ステップ22: 記者ハンドブックによる校正ステップ"]
    016_reorganized --> 022_proofreading
    031_integrated_review_proofread["ステップ31: レビューと校正結果の統合"]
    021_review_by_data_engineer --> 031_integrated_review_proofread
    022_proofreading --> 031_integrated_review_proofread
    032_title_generation["ステップ32: 記事タイトル案の生成"]
    031_integrated_review_proofread --> 032_title_generation
\`\`\`

このワークフロー図に示される各ステップ(例: `001_general_writing_characteristics`)は、`.github/prompts`ディレクトリ内に格納されている、対応するIDを持つプロンプトファイル(例: `001_general_writing_characteristics.prompt.md`)の詳細な指示に基づいています。これらのプロンプトファイルは、`front-matter`内の`depends_on`フィールドなどで`step:ID`(例: `step:001_general_writing_characteristics`)という形式で参照されます。

### ステップ内作業フロー
各ステップの作業では、以下の流れに従って処理を進めてください。これにより、一貫性のある結果と次のステップへのスムーズな移行が可能になります。

\`\`\`mermaid
graph LR
    A[開始] --> B[1: 準備]
    B --> C[2: 実行]
    C --> D[3: 出力]
    D --> E[4: 次ステップ確認]
    E --> F[終了]

    subgraph 準備プロセス
        B --> B1[ディレクトリ確認]
        B1 --> B2[プロンプト理解]
        B2 --> B3[入力ファイル確認]
    end

    subgraph 実行プロセス
        C --> C1[指示に従った処理]
        C1 --> C2[変換・編集作業]
    end

    subgraph 出力プロセス
        D --> D1[ファイル生成]
        D1 --> D2[内容検証]
    end

    subgraph 次ステップ確認プロセス
        E --> E1[次ステップ特定]
        E1 --> E2[ユーザー案内]
    end
\`\`\`

### 1. 準備フェーズ
- 作業ディレクトリの確認
  - entriesディレクトリ配下か、それ以外か
- プロンプトファイルの目的・手順を理解
  - 各プロンプトのfront-matterと内容を読む
- 参照するファイル(`references`に記載されたパス)の内容確認
  - 指定されたファイルの内容を読む
  - 必要に応じてファイル内を検索する

### 2. 実行フェーズ
- プロンプト指示に従った処理の実行
  - 指示を解釈し、計画を立てて実行する
- 指定された変換・編集作業の実施
  - テキストの変換や編集を行う
- 不明点があればユーザーに質問
  - 不明点はユーザーに確認する

### 3. 出力フェーズ
- 指定された出力先(`outputs`に記載されたパス)にファイル生成
  - 指定されたパスにファイルを作成または上書きする
- 出力内容の検証
  - 生成・編集したファイルの内容を確認する
    - 例: Markdownの構文チェックなど
- 成果物の保存
  - ファイル生成・上書きによって行われる
  - 成果物(出力ファイル)には、プロンプトファイル(`.prompt.md`)と異なりfront-matterを含めないこと
    - 出力ファイルは本文のみで構成する(YAMLブロックやヘッダーは除外)

### 4. 次ステップ確認フェーズ
- 現在のステップIDの確認(現在のステップIDを把握する)
- 次のステップ(`depends_on`に現IDを含むプロンプトファイル)の特定
  - `.github/prompts` ディレクトリ内のすべての `.prompt.md` ファイルを調査
  - 各ファイルの冒頭にあるYAMLフロントマター部分(`---`で囲まれた部分)を解析
  - フロントマター内の `depends_on:` キーの下にあるリストを確認
  - リスト内の各項目(例: `- step:ID`)が、現在のステップID (`step:<現在のステップID>`) と完全に一致するかどうかをチェック
  - 一致する項目が見つかったファイルが、次のステップのプロンプトファイルとなる
- ユーザーへの次ステップ案内(特定した次のステップをユーザーに伝える)

## 記述上の注意
- 強調するためにmarkdownの`**`を使用することは避けてください
- 括弧は全角`()`ではなく半角`()`を用いてください
  - 括弧の前後にスペースを入れないでください
- バッククオートの前後にスペースを入れないでください
  - OK: このファイル`example.md`  - NG: このファイル `example.md`- markdownにインデントを深くする際はスペース2つを使用してください、タブは使用しないでください
- 箇条書きの末尾に句読点は不要です
- 箇条書きの前後には以下のように改行を入れるようにしましょう
  - セクションタイトル(`##`など)の直後の行には、空行を入れずに本文を記述してください

よい例:

\`\`\`markdown
## 目的: ChatGPTによるランニングデータ管理の自動化
そのため、そういった作業を簡単にできるようにしたいと考えました。また、それが可能になれば、以下のようなことをChatGPTに相談したいと考えた次第です。

- 最近の練習結果を見て、テンポ走をした際に、前回のテンポ走と比較して今回のテンポ走にはどのような特徴があったか
- 直近で2週間後にレースがある場合に、最近の練習状況を踏まえながら、この2週間でどのような練習メニューを組み立てるのが良いか

最近の練習状況などをコンテキストやプロンプトとして毎回コピーして渡すという方法もありますが、毎回コピーするのは手間がかかるため、それを効率的に自動化できるように、まず入力の自動化を図りました。
\`\`\`

ダメな例:

\`\`\`markdown
## 目的: ChatGPTによるランニングデータ管理の自動化

そのため、そういった作業を簡単にできるようにしたいと考えました。
また、それが可能になれば、以下のようなことをChatGPTに相談したいと考えた次第です。
- 最近の練習結果を見て、テンポ走をした際に、前回のテンポ走と比較して今回のテンポ走にはどのような特徴があったか
- 直近で2週間後にレースがある場合に、最近の練習状況を踏まえながら、この2週間でどのような練習メニューを組み立てるのが良いか

最近の練習状況などをコンテキストやプロンプトとして毎回コピーして渡すという方法もありますが、毎回コピーするのは手間がかかるため、それを効率的に自動化できるように、まず入力の自動化を図りました。
\`\`\`

工夫した点

Agentを使って執筆活動を効率化させる内容を書かれた方はいらっしゃるので、ここではエンジニアである私ならではの工夫について説明します。

過去に自分が執筆したテキストの資産を活用する

LLMに話した内容を校正させていくと、ステップを経る毎に自分のテキストでなくなっていく感覚がありました。ありきたりなテキストになり、自分の語り方などが平均回帰されていくようで、正直あまりいい気分はしませんでした。

一方、私は2006年から約20年に渡ってブログを書き続けているので、大量のテキスト資産があります(実際、このブログでは1867件も書いていた)。これを生かさない手はありません。最近はブログの下書きをesaで行なっているのですが、自分の代表作と言えるようなエントリの下書きをesaのAPIから抽出します。次にLLMを使って、これらのエントリから私の特徴的な文体を書き出させます。その文体を真似するようなステップを組み入れたところ、自分の語り口が残ったままになり、これまでの自分の文体を考慮したエントリに近いものができあがるようになりました。やはり、時代を経ても信じられるものはplain text...!

esa.ioから該当エントリのmarkdownをダウンロードするスクリプト(クリックで開きます)

#!/usr/bin/env python3
"""
esa.ioのポストをマークダウンファイルとして保存するスクリプト
"""

import argparse
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Any

import requests
from dotenv import load_dotenv

# 環境変数の読み込み
load_dotenv()

# ESA API設定
ESA_API_TOKEN = os.getenv("ESA_API_TOKEN")
ESA_TEAM_NAME = os.getenv("ESA_TEAM_NAME")

if not ESA_API_TOKEN or not ESA_TEAM_NAME:
    print("環境変数 ESA_API_TOKEN または ESA_TEAM_NAME が設定されていません")
    sys.exit(1)


def extract_post_id(url: str) -> str | None:
    """URLからポストIDを抽出する

    Args:
        url: esa.ioのポストURL

    Returns:
        抽出されたポストID、抽出できない場合はNone
    """
    pattern = r"https://[\w.-]+\.esa\.io/posts/(\d+)"
    match = re.match(pattern, url)
    if match:
        return match.group(1)
    return None


def fetch_post(post_id: str) -> dict[str, Any] | None:
    """esa.ioからポストを取得する

    Args:
        post_id: 取得するポストのID

    Returns:
        ポストデータを含む辞書、取得に失敗した場合はNone
    """
    url = f"https://api.esa.io/v1/teams/{ESA_TEAM_NAME}/posts/{post_id}"
    headers = {"Authorization": f"Bearer {ESA_API_TOKEN}"}

    response = requests.get(url, headers=headers)
    if response.status_code != 200:
        print(f"エラー: ステータスコード {response.status_code}")
        print(response.text)
        return None

    return response.json()


def create_markdown_file(post_data: dict[str, Any], output_dir: str | None = None) -> str:
    """マークダウンファイルを作成する

    Args:
        post_data: esa.ioから取得したポストデータ
        output_dir: マークダウンファイルの保存先ディレクトリ、Noneの場合は現在のディレクトリ

    Returns:
        作成したマークダウンファイルのパス
    """
    post_id = post_data["number"]
    title = post_data["name"]
    body = post_data["body_md"]
    url = post_data["url"]
    category = post_data["category"] or "未分類"
    tags = post_data["tags"]
    created_at = datetime.fromisoformat(post_data["created_at"].replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M:%S")
    updated_at = datetime.fromisoformat(post_data["updated_at"].replace("Z", "+00:00")).strftime("%Y-%m-%d %H:%M:%S")

    # YAMLフロントマター形式のメタデータを作成
    yaml_header = f"""---
title: "{title}"
url: {url}
category: {category}
tags: [{", ".join(tags)}]
created_at: {created_at}
updated_at: {updated_at}
---

"""

    # 保存先ディレクトリの設定
    filename = f"{post_id}.md"
    if output_dir:
        # 出力ディレクトリが指定されている場合
        output_path = Path(output_dir)
        # ディレクトリが存在しない場合は作成
        output_path.mkdir(parents=True, exist_ok=True)
        file_path = output_path / filename
    else:
        # 出力ディレクトリが指定されていない場合は現在のディレクトリに保存
        file_path = Path(filename)

    # マークダウンファイルを作成
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(yaml_header + body)

    return str(file_path)


def parse_args() -> argparse.Namespace:
    """コマンドライン引数を解析する

    Returns:
        解析されたコマンドライン引数
    """
    parser = argparse.ArgumentParser(description="esa.ioのポストをマークダウンファイルとして保存するスクリプト")
    parser.add_argument("url", help="esa.ioのポストURL")
    parser.add_argument("-o", "--output-dir", help="マークダウンファイルの保存先ディレクトリ", default=None)
    return parser.parse_args()


def main() -> None:
    """メイン関数"""
    # コマンドライン引数の解析
    args = parse_args()

    # URLからポストIDを抽出
    post_id = extract_post_id(args.url)

    if not post_id:
        print(f"無効なesa.io URL: {args.url}")
        sys.exit(1)

    print(f"ポストID {post_id} を取得中...")
    post_data = fetch_post(post_id)

    if post_data:
        file_path = create_markdown_file(post_data, args.output_dir)
        print(f"マークダウンファイルを保存しました: {file_path}")
    else:
        print("ポストの取得に失敗しました")
        sys.exit(1)


if __name__ == "__main__":
    main()

依存関係の抽出を自動で行なう

ワークフローを細かいステップに分割したところ、次に行なうべきステップが私もAgentも分からなくなる、という問題が起きました。そこで、特定のステップを行なう際に「前提となるステップ」「入力になるテキスト」「出力先のテキスト」をmarkdown内にFront Matterとしてメタデータを持たせることにしました。例えばこういったものです。

---
id: 016_reorganized
title: 構成再編ステップ

depends_on:
  - step:015_writing_style_application

references:
  - path: entry:015_style_applied.md
    intent: 文体適用済みテキストを読み込む

outputs:
  - path: entry:016_reorganized.md
    intent: トピック再編成後のテキストを書き出す
---

この情報を用いると、各ステップ間の依存関係を自動で洗い出すことができます。また、LLMも次に実行すべきステップを自動で判断できるようになりました。ステップ間の依存関係は、具体的には以下のようになります。

graph TD
    001_general_writing_characteristics["ステップ1: 横断的な文体分析"]
    002_writing_characteristics["ステップ2: エントリーの文体分析"]
    001_general_writing_characteristics --> 002_writing_characteristics
    011_transcript_segmentation["ステップ11: 話言葉の書き起しを分割するステップ"]
    012_clarify_unclear_words["ステップ12: 分からない/意味が通じない単語を確認するステップ"]
    011_transcript_segmentation --> 012_clarify_unclear_words
    013_colloquial_to_written["ステップ13: 口語を書き言葉に書き換えるステップ"]
    012_clarify_unclear_words --> 013_colloquial_to_written
    014_markdown_structuring["ステップ14: 構造化ステップ"]
    013_colloquial_to_written --> 014_markdown_structuring
    015_writing_style_application["ステップ15: 文体適用ステップ"]
    014_markdown_structuring --> 015_writing_style_application
    002_writing_characteristics --> 015_writing_style_application
    001_general_writing_characteristics --> 015_writing_style_application
    016_reorganized["ステップ16: 構成再編ステップ"]
    015_writing_style_application --> 016_reorganized
    021_review_by_data_engineer["ステップ21: データエンジニアによるレビュー"]
    016_reorganized --> 021_review_by_data_engineer
    022_proofreading["ステップ22: 記者ハンドブックによる校正ステップ"]
    016_reorganized --> 022_proofreading
    031_integrated_review_proofread["ステップ31: レビューと校正結果の統合"]
    021_review_by_data_engineer --> 031_integrated_review_proofread
    022_proofreading --> 031_integrated_review_proofread
    032_title_generation["ステップ32: 記事タイトル案の生成"]
    031_integrated_review_proofread --> 032_title_generation

各ステップの依存関係のvalidationや可視化を行なうスクリプト(クリックで開きます)

#!/usr/bin/env python3
import argparse
import re
import sys
from pathlib import Path
from typing import Any

import yaml

ROOT = Path(__file__).resolve().parents[2]
DEFAULT_MD_GLOB = ".github/prompts/[0-9][0-9][0-9]_*.prompt.md"
# raw string (r"...") を使用して正規表現を定義
FRONT_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.S)


class ValidationError(Exception):
    """Base class for all validation errors."""

    def __init__(self, message: str) -> None:
        self.message = message
        super().__init__(self.message)


class FrontMatterError(ValidationError):
    """Raised when front-matter is missing or invalid."""

    pass


class TitleMissingError(ValidationError):
    """Raised when title is missing in front-matter."""

    pass


class FilenameConsistencyError(ValidationError):
    """Raised when filename does not match the ID in front-matter."""

    pass


class DuplicateIdError(ValidationError):
    """Raised when multiple files have the same ID."""

    pass


class DependencyError(ValidationError):
    """Raised when dependencies are invalid or missing."""

    pass


class ReferenceError(ValidationError):
    """Raised when references are invalid or missing."""

    pass


class OutputError(ValidationError):
    """Raised when outputs are invalid or duplicate."""

    pass


class Step:
    path: Path
    id: str
    title: str
    depends: list[str]
    refs: list[dict[str, str]]
    outs: list[dict[str, str]]

    def __init__(self, path: Path, meta: dict[str, Any]) -> None:
        self.path = path
        self.id = meta["id"]
        self.title = meta["title"]
        self.depends = meta.get("depends_on", [])
        self.refs = meta.get("references", [])
        self.outs = meta.get("outputs", [])

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Step):
            return NotImplemented
        return self.id == other.id

    def __hash__(self) -> int:
        return hash(self.id)


def parse_front(path: Path) -> dict[str, Any]:
    m = FRONT_RE.match(path.read_text(encoding="utf-8"))
    if not m:
        raise FrontMatterError(f"{path}: front‑matter missing")
    return yaml.safe_load(m.group(1))


def normalize_io(item: dict[str, str], kind: str, step_id: str, path: Path) -> str:
    if not isinstance(item, dict) or "path" not in item or "intent" not in item:
        error_class = ReferenceError if kind == "references" else OutputError
        raise error_class(f"{step_id}: {kind} 要素は {{path,intent}} 必須 -> {path}")
    return item["path"]


def validate_required_fields(meta: dict[str, Any], path: Path) -> None:
    """必須フィールドのバリデーションを行う

    Args:
        meta: frontmatterのメタデータ
        path: 対象ファイルのパス
    """
    if "title" not in meta or not meta["title"]:
        raise TitleMissingError(f"{path}: front-matter に title が存在しないか空です")


def load_steps_from_dir(root_dir: Path = ROOT, md_glob: str = DEFAULT_MD_GLOB) -> list[Step]:
    """ディレクトリからMarkdownファイルを読み込み、Stepオブジェクトのリストを返す
    バリデーションは行わない

    Args:
        root_dir: ルートディレクトリ (デフォルトはROOT)
        md_glob: Markdownファイルのグロブパターン (デフォルトはDEFAULT_MD_GLOB)

    Returns:
        Stepオブジェクトのリスト
    """
    # ディレクトリからMarkdownファイルを読み込み、Stepオブジェクトを作成
    steps_dict: dict[str, Step] = {}

    # ファイルをソートして、処理順序を予測可能にする
    # これにより、テストがより安定するようになる
    for md in sorted(root_dir.glob(md_glob)):
        meta = parse_front(md)
        # 必須フィールドのバリデーション
        validate_required_fields(meta, md)

        step = Step(md, meta)
        steps_dict[step.id] = step

    # リストに変換して返す
    return list(steps_dict.values())


def validate_filename_consistency(steps: list[Step]) -> None:
    """ファイル名とIDの一致をチェック

    Args:
        steps: Stepオブジェクトのリスト
    """
    for step in steps:
        fname_id = step.path.name.replace(".prompt.md", "")
        if step.id != fname_id:
            raise FilenameConsistencyError(f"{step.path}: id ({step.id}) とファイル名 ({fname_id}) が一致しません")


def check_duplicate_ids(steps: list[Step]) -> None:
    """IDの重複をチェック

    Args:
        steps: Stepオブジェクトのリスト
    """
    id_to_paths = {}
    for step in steps:
        if step.id not in id_to_paths:
            id_to_paths[step.id] = [step.path]
        else:
            id_to_paths[step.id].append(step.path)
            paths = id_to_paths[step.id]
            if len(paths) > 1:
                raise DuplicateIdError(f"ID 重複: {step.id} ({paths[0]} と {paths[1]})")


def validate_steps_consistency(steps: list[Step]) -> None:
    """ステップの一貫性をチェックする
    1. ファイル名とIDの一致
    2. IDの重複

    Args:
        steps: Stepオブジェクトのリスト
    """
    validate_filename_consistency(steps)
    check_duplicate_ids(steps)


def load_steps(root_dir: Path = ROOT, md_glob: str = DEFAULT_MD_GLOB) -> list[Step]:
    """Markdownファイルからステップ定義を読み込み、一貫性チェックも行う

    Args:
        root_dir: ルートディレクトリ (デフォルトはROOT)
        md_glob: Markdownファイルのグロブパターン (デフォルトはDEFAULT_MD_GLOB)

    Returns:
        バリデーション済みのStepオブジェクトのリスト
    """
    steps = load_steps_from_dir(root_dir, md_glob)
    validate_steps_consistency(steps)
    return steps


def validate_steps(root_dir: Path = ROOT, md_glob: str = DEFAULT_MD_GLOB) -> None:
    """Validate dependencies, references, and outputs of steps.

    Args:
        root_dir: ルートディレクトリ (デフォルトはROOT)
        md_glob: Markdownファイルのグロブパターン (デフォルトはDEFAULT_MD_GLOB)
    """
    steps = load_steps(root_dir, md_glob)
    steps_dict = {s.id: s for s in steps}

    # depends_on validation
    for s in steps:
        for dep in s.depends:
            # depends_on は step:ID の形式のみ許可
            if not isinstance(dep, str) or not dep.startswith("step:") or "." in dep:
                raise DependencyError(f"{s.id}: depends_on は 'step:ID' の形式のみ許可 -> {dep}")
            dep_id = dep.split(":")[1]
            if dep_id not in steps_dict:  # 辞書で存在チェック
                raise DependencyError(f"{s.id}: depends_on {dep} が存在しません")

    # refs / outs validation
    produced = {}
    for s in steps:
        for kind, lst in [("references", s.refs), ("outputs", s.outs)]:
            for item in lst:
                p = normalize_io(item, kind, s.id, s.path)
                if kind == "outputs":
                    # 出力パスの重複チェックは維持
                    if p in produced and produced[p] != s.id:
                        raise OutputError(f"{p} が {produced[p]} と {s.id} で重複出力")
                    produced[p] = s.id

                if p.startswith("step:"):
                    step_id = p.split(":")[1]
                    # depends_on と同様に .prompt.md 拡張子なしでチェック
                    if step_id not in steps_dict:
                        error_class = ReferenceError if kind == "references" else OutputError
                        raise error_class(f"{s.id}: {kind} の step:{step_id} が存在しません")
                elif p.startswith("root:"):
                    # ワイルドカードが含まれていない場合、および特定のパスを除外して存在チェック
                    if (
                        "*" not in p
                        and p != "root:general_writing_characteristics.md"
                        and p != "root:tmp/all_inputs.md"
                    ):
                        resolved_path = root_dir / p.split(":", 1)[1]
                        if not resolved_path.exists():
                            error_class = ReferenceError if kind == "references" else OutputError
                            raise error_class(f"{s.id}: {kind} の {p} ({resolved_path}) が存在しません")
                # entry:, input:, chat:, ワイルドカード(*) を含むパスはチェックしない

    print("✅ validate passed")


def escape_mermaid_label(label: str) -> str:
    """Escape characters in a label for Mermaid node definition."""
    # ダブルクォートをHTMLエンティティに置換
    return label.replace('"', "#quot;")


# graphサブコマンド用のハンドラ関数
def graph_steps(root_dir: Path = ROOT, md_glob: str = DEFAULT_MD_GLOB) -> None:
    """Generate Mermaid graph of step dependencies.

    Args:
        root_dir: ルートディレクトリ (デフォルトはROOT)
        md_glob: Markdownファイルのグロブパターン (デフォルトはDEFAULT_MD_GLOB)
    """
    steps = load_steps(root_dir, md_glob)  # 引数を渡す

    print("graph TD")  # Mermaidグラフヘッダー

    # ノード定義と依存関係の出力
    for step in steps:
        # titleをエスケープ
        escaped_title = escape_mermaid_label(step.title)
        # step.id から数字部分を抽出 (例: "016_reorganized" -> "16")
        match = re.match(r"(\d+)_", step.id)
        step_number = int(match.group(1)) if match else step.id  # マッチしない場合は元のIDを使う
        node_label = f"ステップ{step_number}: {escaped_title}"
        print(f'    {step.id}["{node_label}"]')
        # 依存関係の出力
        for dep in step.depends:
            # dep は "step:ID" の形式
            dep_id = dep.split(":")[1]
            print(f"    {dep_id} --> {step.id}")


# argparseを使ったメイン処理
def main() -> None:
    parser = argparse.ArgumentParser(description="Step control tool")
    subparsers = parser.add_subparsers(dest="command", help="Available commands")

    # validateサブコマンド
    parser_validate = subparsers.add_parser("validate", help="Validate step definitions")
    parser_validate.add_argument("--root", type=Path, default=ROOT, help="Root directory to search from")
    parser_validate.add_argument("--glob", default=DEFAULT_MD_GLOB, help="Glob pattern for markdown files")
    parser_validate.set_defaults(func=lambda args: validate_steps(args.root, args.glob))

    # graphサブコマンド
    parser_graph = subparsers.add_parser("graph", help="Generate step dependency graph (Mermaid)")
    parser_graph.add_argument("--root", type=Path, default=ROOT, help="Root directory to search from")
    parser_graph.add_argument("--glob", default=DEFAULT_MD_GLOB, help="Glob pattern for markdown files")
    parser_graph.set_defaults(func=lambda args: graph_steps(args.root, args.glob))

    args = parser.parse_args()

    if hasattr(args, "func"):
        try:
            args.func(args)
        except ValidationError as e:
            print(f"Error: {e.message}", file=sys.stderr)
            sys.exit(1)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()

複数のAgentにレビューをさせる

ブログを書く際に気になるのは想定読者の感想や反応でしょう。今回のワークフローでは、ある程度経験があるデータエンジニアをペルソナにして、内容をレビューさせるようにしています。VSCodeのCopilot Agentでは様々なLLMを指定することができるので、私の場合はClaude 3.7 Sonnet / Gemini 2.5 Pro / GPT 4.1にそれぞれレビューさせています。レビューで共通したことを以外にそれぞれ微妙に観点が異なるのが面白いです。

様々なAgentにデータエンジニアになりきってレビューをしてもらう

以下は実際のAgentによるレビュー内容です。

具体的なレビュー内容

また、記者ハンドブックに近い内容もプロンプトに組み込み、改善案やその理由を説明させるようにしています。

タイトル案の自動生成

エントリのタイトルをどうするかは毎回悩みどころですが、LLMを使って多様なタイトル案を生成させるようにしました。最終的にタイトル案を採用するかはともかく、叩き台があると改善がしやすくなります。

ちなみに、本エントリに対するタイトル案とその理由は以下のように出力されました。

タイトル案とその理由

実装を通して得られた学び

自然言語でワークフローを組み立てることの難しさ

これまではエンジニアがコードを書いてワークフローを組み立てていたわけですが、今回のワークフローは自然言語を用いて組み立てました。自然言語は簡単に書けるように思える一方、プログラミング言語よりも曖昧性を持ちやすい性質があります。そのため、うまく書かないとAgentが思った通りに動いてくれません。

今回のワークフローの実装の中でも思った通りに動いてくれないことは多々あり、その際はAgentに

  • 013_colloquial_to_writtenの指示書にxxxするようにって指示あったけど、うまくできなかったのなぜか教えてくれる?
    • 実際にファイルを修正して欲しいわけじゃなくて、理由や背景を知りたいと思っています
    • Agentは指摘されると理由を開示せず防衛的になることがあるため、↑のように質問するようにしました
  • じゃあ、指示書がどうなっていると今回のミスが起きにくかったと思う?
  • よし、じゃあその形で指示書を実際に修正してみようか!

といったことを質問し、二人三脚でpromptsを改善しました。Agentを相手にしてますが、やってることは人間相手にやっていることとあんまり変わらないですね。今後Agentを使う人は、相手に対して分かりやすく曖昧性の少ない形やコミュニケーションをしながら仕様書を記述していくプロジェクトマネージャー的なスキルが今後求められていくようになるんだろうなと思いました。

エンジニアリングでワークフローを支える

ワークフローを組み立てる場合、プログラムであればテストを書きCI/CDでそれが通ることで、既存の挙動が壊れないことを確認していました。一方、今回のように自然言語でワークフローを組み立てる場合、曖昧性をなるべく少なくして記述するようにしたとしても、promptsを修正した際にそれが壊れる可能性はあります。そして、壊れたことに気付くのは中々難しいです。

今回のエントリの中で紹介した方法でワークフローのテストが完全にできているわけではないですが、壊れにくい / 壊れたことに気付ける設計になるように気を配りました。

  • 挙動が担保しやすいように細かいステップを積み上げる設計
  • Front Matterを使った人間にもAgentにも読みやすいメタデータの設計
  • 静的なファイルト動的に変わるファイルパスを扱うためのリンク記法の定義
  • Front Matterをプログラムで解析し、不整合があればCI/CDで失敗させる仕組み

prompts自体にはプログラムは登場しないですが、上記のような考え方や仕組みはまさにエンジニアリングで培われたものです。プログラム自体はSWEではなくAgentが今後書くことが増えていきそうですが、それを支えるガードレールを作る側面などでもまだまだエンジニアリングのスキルは必須だなと感じました。

まとめ

今回紹介したワークフローは、音声からブログ記事を効率的に作成するための総合的なアプローチです。Superwhisperによる高精度な書き起こしを基盤に、段階的なLLM処理によって文章の質を向上させ、作業効率を大幅に改善しています。

各ステップを明確に分けることで、プロセスの透明性と再現性を高め、必要に応じて個別のステップを調整することが可能です。特に執筆時間の短縮と品質の向上を両立させる点で、このワークフローは効果的であると感じています。

もし興味があれば、ぜひ試してみてください。