🚀

現場PMでも“サクっと”作れる!生成AI×バイブコーディングで業務改善ツールを自作する方法

に公開

SREホールディングス株式会社でプロジェクトマネージャ(PM)をしている鍋谷です。

「ちょっとした業務改善、誰かに頼むほどでもないけど、毎日ちょっと面倒」
そんな作業、あなたのチームにもありませんか?

本記事では、生成AIとバイブコーディングを活用して、現場のPMや非エンジニアでも“サクっと”作れる業務改善ツールの実例を紹介します。

対象読者

・現場のプロジェクトマネージャ(PM)
・非エンジニアで業務改善に関心がある方
・生成AIやAPIを使った業務効率化に興味がある方
・「自分でもツールを作ってみたい」と思っている方
・SlackやNotionなどのSaaSを日常的に使っている方

本記事の概要

  • 生成AIとAPIの組み合わせで、外部委託するほどでもない小規模案件ROIが読みにくい試行のIT化コストが激減。
  • 結果として、現場のプロジェクトマネージャ(PM)や非エンジニアが、自分の課題を自分でササっと道具化できる=ITの民主化(=専門知識がなくても業務改善ツールを自作できる環境)が進む。
  • 具体例として、Slackの1日分の投稿を収集し、要約/課題の抽出/ToDo/アラートなどを朝会・夕会用に集約するミニツールを紹介。

IT化のハードルが下がった理由

1) 仕様を完全に固めなくても「方向性」を言語化すれば動く

従来は要件定義→設計→実装→レビュー…という直線工程が必須でした。生成AI時代は、こんなアウトプットが欲しいを言語で書くと、雛形(プロンプト/コード/データ整形)が一気に出てきます。そこから小さく回して学習すればよく、先に投資を張らなくても検証できます。

2) 「小さなIT化」が外注コストを超えやすくなった

かつては「人に頼むほどでもない細々とした作業」は非効率のまま手作業で実施したり、あきらめて放置されがちでした。いまはPMがバイブコーディングで1〜2時間あれば作れる簡易スクリプトでも、例えば現場のボトルネックを毎日見える化でき、十分な投資対効果が出せます。

3) 既存SaaSとAPIが“部品化”

SlackやGoogle Workspace、Notion、Jira/Backlog、Microsoft 365などをつなぐだけで実務レベルの自動化になります。バイブコーディングはこの“繋ぐ力”を民主化しました。


バイブコーディングで”ササっと”作る方法

バイブコーディングとは

バイブコーディング(Vibe Coding)とは、仕様を完全に詰める前に「こういう体験・結果が欲しい」という雰囲気(vibe)を自然言語で伝え、生成AIにまず動く叩き台を作らせ、手元で高速に試行錯誤する開発スタイルです。

バイブコーディングの手順

  1. バイブコーディングの基本を理解する
    コードを自ら書く必要がほぼない。ChatGPTやGeminiなどのAIツールを利用。成功のカギは「明確な要件定義」と「対話力」
  2. 必要な準備
    AIツールの選定: ChatGPTやGeminiなど。できれば有料のほうがよい。
    開発環境: ご自身のパソコンでOK。メモ帳やテキストエディタで作成可能。
  3. 実行手順
    ステップ1:作りたいものを明確化
    「誰のために」「何を解決するのか」を文章で整理。
    ステップ2:AIに役割を与える
    最初に「あなたはプロのITエンジニアです。この仕様でアプリを作ってください」と伝える。
    ステップ3:要件を自然言語で指示
    例「UIがシンプルなToDoアプリを作って。機能は追加・削除・完了チェックのみ」
    ステップ4:コード生成と実行
    AIが生成したコードをコピーし、エディタでプログラムファイルを作成する。プログラムを動かす方法は「初心者でもわかるように動作させるための手順を教えて」とAIに聞けば教えてくれる。動作させて不具合があればAIに「エラー内容」を伝えて修正依頼。
    ステップ5:反復改善
    「このデザインをもっとモダンに」「ダークモードを追加」など、会話で改善。
  4. 作成上のポイント
    ・小さく始める:最初は簡単なつくりに
    ・こだわる要件は丁寧に:AIは指示があいまいだと意図しないコードを生成
    ・AIは意外と間違える:バグや制約があるので根気よく試行錯誤する

実例:Slackの投稿をNotionに集約し、要約・課題・ToDoを抽出する

ゴール:
指定チャンネルの1日分の投稿を収集し、要約/課題/ToDo/アラートを整理してNotionに保存。
NotionAIでレポート形式に要約させ、朝会・夕会に活用します。

プロンプト例

あなたはプロのITエンジニアです。Slackで複数のチャンネルから最近のメッセージをまとめて取得し、それをNotionのページに整理して保存する仕組みを作ってください。
- Slackの複数のチャンネルのメッセージを1日に1回収集する
- Slackのメッセージとスレッドの返信も含めたい
- ユーザー名はIDではなく表示名でわかるようにしたい
- まとめたメッセージをNotionの1ページに集約したい
- 集約されたメッセージはNotionAIにレポート形式で要約させたい。
- できれば前回取得した時刻以降の新しいメッセージだけを対象にしたい
- 設定情報(チャンネル一覧やユーザ名)は簡単にメンテナンスできるようにしたい
- 実装は自動化しやすい方法でお願いします

作成したコード

生成AIで作成したコードをテストして、エラーをAIに報告、修正内容を反映してまたテストというのを繰り返しました。最終的に完成したコードが以下となりました。

このコードはPythonで書かれています

import os
import json
import time
import datetime
import requests

CONFIG_FILE = "slack_channel.json"
TIMESTAMP_FILE = "last_fetch_timestamp.txt"
TEMP_JSON_FILE = "latest_simplified_slack_messages.json"
JST = datetime.timezone(datetime.timedelta(hours=9))
MAX_TEXT_LENGTH = 2000

# 設定読み込み
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
    config = json.load(f)

slack_token = config["slack_token"]
notion_token = config["notion_token"]
database_id = config["database_id"]
channel_map = config["channel_map"]
channel_ids = list(channel_map.keys())

headers = {"Authorization": f"Bearer {slack_token}"}
notion_headers = {
    "Authorization": f"Bearer {notion_token}",
    "Content-Type": "application/json",
    "Notion-Version": "2022-06-28"
}

def get_user_map():
    response = requests.get("https://slack.com/api/users.list", headers=headers)
    user_map = {}
    if response.status_code == 200 and response.json().get("ok"):
        for user in response.json()["members"]:
            user_id = user["id"]
            display_name = user["profile"].get("display_name") or user["profile"].get("real_name") or user_id
            user_map[user_id] = display_name
    return user_map

def replace_user_mentions(text, user_map):
    if not isinstance(text, str):
        return text
    for user_id, name in user_map.items():
        text = text.replace(f"<@{user_id}>", f"@{name}")
    return text

def fetch_thread_replies(channel_id, thread_ts):
    replies = []
    params = {
        "channel": channel_id,
        "ts": thread_ts,
        "limit": 100
    }
    response = requests.get("https://slack.com/api/conversations.replies", headers=headers, params=params)
    if response.status_code == 429:
        retry_after = int(response.headers.get("Retry-After", 1))
        print(f"⏳ スレッド取得でレートリミット。{retry_after}秒待機...")
        time.sleep(retry_after)
        return fetch_thread_replies(channel_id, thread_ts)
    data = response.json()
    if data.get("ok"):
        replies = data.get("messages", [])[1:]  # 最初の1件は親メッセージなので除外
    else:
        print(f"❌ スレッド取得エラー: {data.get('error')}")
    return replies

def fetch_channel_messages(channel_id, start_ts, end_ts):
    messages = []
    cursor = None
    total_fetched = 0
    while True:
        params = {
            "channel": channel_id,
            "oldest": str(start_ts),
            "latest": str(end_ts),
            "limit": 200,
        }
        if cursor:
            params["cursor"] = cursor
        response = requests.get("https://slack.com/api/conversations.history", headers=headers, params=params)
        if response.status_code == 429:
            retry_after = int(response.headers.get("Retry-After", 1))
            print(f"⏳ レートリミットに達しました。{retry_after}秒待機します...")
            time.sleep(retry_after)
            continue
        data = response.json()
        if not data.get("ok"):
            print(f"❌ APIエラー: channel={channel_id}, error={data.get('error')}")
            break
        batch = data.get("messages", [])
        if not batch:
            print(f"ℹ️ API応答は成功したがメッセージが空です: channel={channel_id}")
        else:
            print(f"✅ メッセージ取得成功: channel={channel_id}, 件数={len(batch)}")
        for msg in batch:
            messages.append(msg)
            if "thread_ts" in msg and msg["thread_ts"] == msg["ts"]:
                replies = fetch_thread_replies(channel_id, msg["ts"])
                messages.extend(replies)
        total_fetched += len(batch)
        cursor = data.get("response_metadata", {}).get("next_cursor")
        if not cursor:
            break
    print(f"📥 チャンネル {channel_id}:合計 {len(messages)} 件取得(返信含む)")
    return messages

def split_text_into_blocks(content):
    blocks = []
    while len(content) > MAX_TEXT_LENGTH:
        blocks.append(content[:MAX_TEXT_LENGTH])
        content = content[MAX_TEXT_LENGTH:]
    blocks.append(content)
    return blocks


def post_to_notion_append(simplified_data, start_ts, end_ts):
    MAX_TEXT_LENGTH = 2000

    def split_text_into_blocks(content):
        blocks = []
        while len(content) > MAX_TEXT_LENGTH:
            blocks.append(content[:MAX_TEXT_LENGTH])
            content = content[MAX_TEXT_LENGTH:]
        blocks.append(content)
        return blocks

    start_dt = datetime.datetime.fromtimestamp(start_ts, JST)
    end_dt = datetime.datetime.fromtimestamp(end_ts, JST)
    start_str = start_dt.isoformat()
    end_str = end_dt.isoformat()
    title = f"Slackメッセージまとめ({start_str}{end_str})"
    all_blocks = []

    for channel_id, messages in simplified_data.items():
        channel_name = channel_map.get(channel_id, channel_id)
        all_blocks.append({
            "object": "block",
            "type": "heading_2",
            "heading_2": {
                "rich_text": [{"type": "text", "text": {"content": f"Channel: {channel_name}"}}]
            }
        })
        for msg in messages:
            content = f"[{msg['timestamp']}] {msg['user']}: {msg['text']}"
            if msg["reply_to"]:
                content += f"\n↳ Reply to: {msg['reply_to']}"
            for chunk in split_text_into_blocks(content):
                all_blocks.append({
                    "object": "block",
                    "type": "paragraph",
                    "paragraph": {
                        "rich_text": [{"type": "text", "text": {"content": chunk}}]
                    }
                })

    first_blocks = all_blocks[:100]
    payload = {
        "parent": {"database_id": database_id},
        "properties": {
            "Name": {
                "title": [{"type": "text", "text": {"content": title}}]
            },
            "取得開始時刻": {
                "date": {"start": start_str}
            },
            "取得終了時刻": {
                "date": {"start": end_str}
            }
        },
        "children": first_blocks
    }

    response = requests.post("https://api.notion.com/v1/pages", headers=notion_headers, json=payload)
    if response.status_code != 200:
        print("❌ Notionページ作成エラー:", response.text)
        return
    page_id = response.json().get("id")
    print("✅ Notionページが作成されました:", page_id)

    for i in range(100, len(all_blocks), 100):
        chunk = all_blocks[i:i + 100]
        append_url = f"https://api.notion.com/v1/blocks/{page_id}/children"
        append_payload = {"children": chunk}
        append_response = requests.patch(append_url, headers=notion_headers, json=append_payload)
        if append_response.status_code == 200:
            print(f"➕ {len(chunk)} ブロックを追記しました")
        else:
            print("❌ 追記エラー:", append_response.text)


def main():
    if os.path.exists(TIMESTAMP_FILE):
        with open(TIMESTAMP_FILE, "r") as f:
            start_ts = float(f.read().strip())
    else:
        start_ts = time.time() - 86400
    end_ts = time.time()

    start_ts = int(start_ts)
    end_ts = int(end_ts)

    print("🔍 取得範囲(JST):")
    print("開始:", datetime.datetime.fromtimestamp(start_ts, JST))
    print("終了:", datetime.datetime.fromtimestamp(end_ts, JST))

    user_map = get_user_map()
    all_messages = {}
    parent_lookup = {}

    for channel_id in channel_ids:
        messages = fetch_channel_messages(channel_id, start_ts, end_ts)
        time.sleep(1)
        if not messages:
            print(f"⚠️ チャンネル {channel_id} にメッセージがありません")
        all_messages[channel_id] = messages
        for msg in messages:
            ts = msg.get("ts")
            text = replace_user_mentions(msg.get("text", ""), user_map)
            if ts and text:
                parent_lookup[ts] = text

    simplified_data = {}
    for channel_id, messages in all_messages.items():
        simplified_data[channel_id] = []
        for msg in messages:
            ts = msg.get("ts", "")
            user = msg.get("user", msg.get("bot_id", ""))
            text = replace_user_mentions(msg.get("text", "").replace("\n", " ").strip(), user_map)
            thread_ts = msg.get("thread_ts", "")
            is_reply = thread_ts and thread_ts != ts
            reply_to_text = parent_lookup.get(thread_ts, "") if is_reply else ""
            try:
                readable_time = datetime.datetime.fromtimestamp(float(ts), JST).strftime("%Y-%m-%d %H:%M:%S")
            except:
                readable_time = ts
            simplified_data[channel_id].append({
                "timestamp": readable_time,
                "user": user_map.get(user, user),
                "text": text if text else "(No text)",
                "reply_to": reply_to_text
            })

    if os.path.exists(TEMP_JSON_FILE):
        os.remove(TEMP_JSON_FILE)
    with open(TEMP_JSON_FILE, "w", encoding="utf-8") as f:
        json.dump(simplified_data, f, ensure_ascii=False, indent=2)

    post_to_notion_append(simplified_data, start_ts, end_ts)

    with open(TIMESTAMP_FILE, "w") as f:
        f.write(str(end_ts))

if __name__ == "__main__":
    main()

できあがったコードの処理概要

  1. Slack APIで指定チャンネル群の当日投稿を取得(conversations.history)。
  2. 投稿を時系列で整形(スレッドは親にネスト)。
  3. Notion APIで日次のまとめページを作成。

NotionAIをコードから直接操作しようとしたが、現時点ではNotionAIのAPIが公開されていないため、「要約/課題/ToDo」の抽出はNotion AIに手動で依頼することとしました。

Notion AIへの依頼プロンプト

作成されたページ上でNotionAIに以下の依頼プロントでレポートさせます。

あなたはプロジェクトマネージャの片腕として、このページのメッセージを分析し、
以下の観点でプロジェクトマネージャ向けにレポートを作成してください:

・発生事象(何が起きたか、誰が関与しているか)
・課題・懸念点(技術的・業務的な問題、未解決の点)
・ToDo・アクション項目(誰からの依頼で誰が何をすべきか、期限があるか)
・システムアラート・障害(エラー通知、監視アラート、対応状況)
・不明点やフォローアップすべきこと
・直近の予定
・その他、共有すべき重要な情報

レポートは、簡潔かつ明確に、関係者がすぐに理解・対応できるようにまとめてください。
また、必要に応じてチャンネル名・日時・発言者を引用してください。

レポート例

Notion AIで生成されたレポートのサンプルの一部です。

レポートを活用

レポート結果をNotionのページに保存→朝会・夕会の準備やフォローアップに活用します。

まとめ:ITの民主化はもう始まっている

生成AIとAPIの進化により、「ちょっとした業務改善」や「試してみたいアイデア」が、専門知識なしでも形にできる時代が到来しました。
バイブコーディングは、そんな時代における新しい開発スタイルであり、現場のPMや非エンジニアが自分の手でIT化を進めるための武器です。

今回紹介したSlack要約ツールのように、小さく作ってすぐ使える仕組みは、日々の業務に確かな変化をもたらします。
そしてその積み重ねが、組織全体の情報活用力や意思決定のスピードを底上げしていくでしょう。

「自分にもできるかも」と思った方は、ぜひ一度バイブコーディングを試してみてください。

SRE Holdings 株式会社

Discussion