非営利組織のためのAI活用ラボ - 第5回:エンジニアがいなくても、AIと一緒に前へ進む – 非営利組織における“並走者”としての生成AI

非営利組織のためのAI活用ラボ - 第5回:エンジニアがいなくても、AIと一緒に前へ進む – 非営利組織における“並走者”としての生成AI

非営利組織では「エンジニアがいないから…」という壁に直面することが少なくありません。第5回「非営利組織のためのAI活用ラボ」では、生成AIを“IT人材の代替”ではなく“並走者”として活用する現実的な方法を探ります。実際にClaude Codeを使って「Googleフォーム→スプレッドシート連携」といった現場の小さなIT課題に挑戦し、どこまで頼れるのか、どこで注意が必要かを検証。エンジニア不在の組織が、AIを味方につけてIT自立を促すヒントになれば幸いです。
2025.10.28

1. 導入 – 「エンジニアに相談したいけど、いない」現場の現実

多くのNPO・非営利組織では、専任のIT人材がいないことがままあります。そうすると、

  • Googleフォームを手直ししたい
  • Googleスプレッドシートの集計が大変
  • WordPressでエラーが出ている・・・けど治せない

といった日常業務で遭遇するちょっとしたITニーズに対応するのも一苦労です。
とはいえ、外部リソースを活用しようとすれば思いのほか高額の請求がされますし、当然、専任のIT人材を雇う余裕はない。

こんな時、これまでは人力x運用でなんとかしようとしてきたのが実情かと思います。
しかし、AIを上手に活用することでこうしたニーズは解決できる可能性があります。

2. 実験 – AIに「小さなIT相談」をしてみる

まず大事なのは、AIは適当な要求を投げ込めばなんでも作ってくれる魔法の箱ではありません
"Garbage in, garbage out" (GIGO)という言葉が表す通り、「ゴミを入力すればゴミが出てくる」のがAIです。

AIに期待通りの出力を生成させるためには、詳細な指示とコンテキスト(文脈)、目的の入力が必要です。
そして、こうした入力を手短に作成するために有効なのがAIに任せるスコープ(作業範囲)を思いっきり限定することです。

その観点からも、AIに「小さなIT相談」をしてみて勘所を掴むのは有効です。

例えば、以下のような入力をしてみてください。

サービス利用者からの感想を受け付けるGoogleフォームがあります。このGoogleフォームに送られた回答をスプレッドシートに自動転記し、さらにその通知をSlackの特定チャンネルに送るにはどうしたら良いですか?スプレッドシートは集計、調査、分析用途に使います。Slack通知は関係者に速報を伝えることが目的です。Slackの通知先チャンネルは増減する可能性があります。これらの設定は非技術者が簡単に設定で変更できるようにしたいです。必要な情報が足りなければ聞いてください。

Claude Codeに実際に入力してみた結果、次の回答を得ました。

Googleフォームの回答をスプレッドシートに自動転記し
Googleフォームの回答をスプレッドシートに自動転記し、Slack通知を送る仕組みを構築する方法をご説明します。まず、いくつか確認させてください:

## 確認事項

1. **Googleフォームの状態**
   - すでにフォームは作成済みですか?
   - 質問項目の種類(選択式、記述式など)を教えていただけますか?

2. **スプレッドシートの要件**
   - 特定のフォーマットや列構成の希望はありますか?
   - 集計・分析で特に重要視する項目はありますか?

3. **Slack通知の詳細**
   - 通知に含めたい情報は何ですか?(回答者名、回答内容の要約、など)
   - 通知はリアルタイムで必要ですか?それとも一定間隔でまとめて送信でも構いませんか?

4. **権限関連**
   - SlackワークスペースにIncoming Webhooksやボットを追加する権限はありますか?
   - Google Workspaceの管理者権限はお持ちですか?

## 推奨する実装方法

これらの要件を満たす最適な方法として、**Google Apps Script (GAS)** を使った実装をお勧めします。理由は:

- 完全無料で利用可能
- Googleサービスとの連携が簡単
- 設定をスプレッドシートで管理することで、非技術者でも変更可能
- カスタマイズが柔軟

基本的な構成は以下のようになります:

[Googleフォーム] 
    ↓ (自動連携)
[回答記録用スプレッドシート]
    ↓ (GASトリガー)
[Google Apps Script]
    ↓ (Webhook/API)
[Slack通知]

設定用のシートを別途用意し、そこでSlack通知先チャンネルのWebhook URLを管理することで、非技術者でも簡単に通知先を変更できるようにします。

詳細な実装方法をご提案するため、上記の確認事項について教えていただけますでしょうか?

少しだけ進めてみましょう。
確認事項に回答します。

> 1. Googleフォームの状態

フォームは作成済です。
項目は次の通りです。

- 氏名
- 生年月日
- メールアドレス
- サービスの満足度(高・中・低の3択)
- メッセージ(記述式)

> 2. スプレッドシートの要件

フォームの各項目が各列にマッピングされて欲しいです。
さらに、先頭にNo列を追加して、回答を一意に特定できるようにしてください。
分析で重要視する項目は生年月日とサービスの満足度です

> 3. Slack通知の詳細

通知には回答内容をそのまま含めてください。
一定間隔でまとめて送信で問題ありませんが、間隔は設定で調整したいです。

> 4. 権限関連

Incoming Webhooksやボットを追加する権限はあります。
Google Workspaceの管理者権限はありませんが、管理者に作業を依頼可能です。

すると次の回答が返ってきました。

ありがとうございます。要件を理解しました。Google Apps Script (GAS) を...
ありがとうございます。要件を理解しました。Google Apps Script (GAS) を使って実装する具体的な手順をご説明します。

## 実装の全体像

1. **Googleフォームとスプレッドシートの連携設定**
2. **Slack Incoming Webhookの設定**
3. **Google Apps Scriptの実装**
4. **設定管理シートの作成**
5. **定期実行の設定**

## 手順1: フォームとスプレッドシートの連携

1. Googleフォームを開く
2. 「回答」タブをクリック
3. スプレッドシートアイコンをクリックし、新規または既存のスプレッドシートに連携

この時点で、以下の列が自動的に作成されます:
- タイムスタンプ
- 氏名
- 生年月日
- メールアドレス
- サービスの満足度
- メッセージ

## 手順2: スプレッドシートの準備

連携されたスプレッドシートで以下の作業を行います:

1. **「回答」シートの調整**
   - A列に「No」列を挿入
   - B列以降に既存の回答データが入る形に

2. **「設定」シートを新規作成**し、以下の項目を設定:

   A列          B列
   -----------------------------------
   Webhook URL  [SlackのWebhook URLを入力]
   通知間隔(分)  30
   最終通知行    1

## 手順3: Slack Webhook URLの取得

1. Slackワークスペースにログイン
2. [Slack App Directory](https://slack.com/apps/A0F7XDUAZ-incoming-webhooks) にアクセス
3. 「Add to Slack」をクリック
4. 通知を送りたいチャンネルを選択
5. 生成されたWebhook URLをコピー

## 手順4: Google Apps Scriptの実装

スプレッドシートで「拡張機能」→「Apps Script」を開き、以下のコードを実装します:

// メイン処理
function sendSlackNotifications() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const responseSheet = ss.getSheetByName('フォームの回答 1'); // シート名を確認して変更
  const settingsSheet = ss.getSheetByName('設定');

  // 設定を読み込み
  const webhookUrl = settingsSheet.getRange('B1').getValue();
  const lastNotifiedRow = settingsSheet.getRange('B3').getValue();

  // 回答データを取得
  const dataRange = responseSheet.getDataRange();
  const data = dataRange.getValues();

  // 新しい回答がない場合は終了
  if (data.length <= lastNotifiedRow) {
    return;
  }

  // 新しい回答を処理
  let messages = [];
  for (let i = lastNotifiedRow; i < data.length; i++) {
    const row = data[i];
    const no = i; // No列の値(0始まりなので、実際の行番号-1)

    // 各列のデータを取得(インデックスは実際の列配置に合わせて調整)
    const timestamp = row[0];
    const name = row[1];
    const birthdate = row[2];
    const email = row[3];
    const satisfaction = row[4];
    const message = row[5];

    // Slack用のメッセージを作成
    const slackMessage = {
      text: `新しい回答が届きました(No: ${no})`,
      blocks: [
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*【回答No: ${no}】*\n*回答日時:* ${formatDate(timestamp)}`
          }
        },
        {
          type: "section",
          fields: [
            {
              type: "mrkdwn",
              text: `*氏名:*\n${name}`
            },
            {
              type: "mrkdwn",
              text: `*生年月日:*\n${formatDate(birthdate)}`
            },
            {
              type: "mrkdwn",
              text: `*メールアドレス:*\n${email}`
            },
            {
              type: "mrkdwn",
              text: `*満足度:*\n${satisfaction}`
            }
          ]
        },
        {
          type: "section",
          text: {
            type: "mrkdwn",
            text: `*メッセージ:*\n${message || '(なし)'}`
          }
        },
        {
          type: "divider"
        }
      ]
    };

    messages.push(slackMessage);
  }

  // Slackに送信
  messages.forEach(msg => {
    sendToSlack(webhookUrl, msg);
    Utilities.sleep(1000); // 1秒待機(レート制限対策)
  });

  // 最終通知行を更新
  settingsSheet.getRange('B3').setValue(data.length);
}

// Slackに送信する関数
function sendToSlack(webhookUrl, payload) {
  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload)
  };

  try {
    UrlFetchApp.fetch(webhookUrl, options);
  } catch (e) {
    console.error('Slack送信エラー:', e);
  }
}

// 日付フォーマット関数
function formatDate(date) {
  if (!date) return '';
  if (typeof date === 'string') return date;

  const d = new Date(date);
  return Utilities.formatDate(d, 'JST', 'yyyy/MM/dd HH:mm:ss');
}

// No列を自動採番する関数(オプション)
function addRowNumber() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('フォームの回答 1');
  const lastRow = sheet.getLastRow();

  for (let i = 2; i <= lastRow; i++) {
    if (!sheet.getRange(i, 1).getValue()) {
      sheet.getRange(i, 1).setValue(i - 1);
    }
  }
}

// トリガー設定用の関数
function setupTriggers() {
  // 既存のトリガーを削除
  const triggers = ScriptApp.getProjectTriggers();
  triggers.forEach(trigger => ScriptApp.deleteTrigger(trigger));

  // 設定シートから間隔を取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const settingsSheet = ss.getSheetByName('設定');
  const intervalMinutes = settingsSheet.getRange('B2').getValue();

  // 時間ベースのトリガーを設定
  ScriptApp.newTrigger('sendSlackNotifications')
    .timeBased()
    .everyMinutes(intervalMinutes)
    .create();

  // フォーム送信時のNo列追加トリガー
  ScriptApp.newTrigger('addRowNumber')
    .forSpreadsheet(ss)
    .onFormSubmit()
    .create();
}

## 手順5: 初期設定とテスト

1. **設定シートにWebhook URLを入力**
2. **スクリプトエディタで「setupTriggers」関数を実行**
   - 初回実行時は権限の承認が必要
3. **テスト送信**
   - 「sendSlackNotifications」関数を手動実行してテスト

## 非技術者向けの設定変更方法

### Slack通知先を追加/変更する場合:
1. 新しいチャンネルのWebhook URLを取得
2. 設定シートのA列に「Webhook URL 2」などと追記
3. B列に新しいURLを入力
4. スクリプトを少し修正(複数送信対応)

### 通知間隔を変更する場合:
1. 設定シートのB2セルの値を変更(例: 60 = 1時間ごと)
2. Apps Scriptで「setupTriggers」関数を再実行

### 注意事項:
- Webhook URLは外部に漏れないよう注意
- スプレッドシートの共有設定に気をつける
- 大量の回答がある場合はSlackのレート制限に注意

この設定で問題ないか、追加で必要な機能があれば教えてください。

この回答の内容が理解できるようであれば、そのまま実行してみましょう。もし、わからない部分があれば、それもまたAIに詳しく解説を頼んでみましょう。

大切なのは、自分が理解できるレベルにまで噛み砕いてやり取りすることを面倒がらずに行うことです。

3. 考察 – AIを「代替」ではなく「伴走者」として捉える

先の例でもわかるように、AIはIT人材の「代替」にはなりづらいです。
ただし、隣を一緒に走ってくれる「伴走者」として考え、適切な入力を与えること、また、AIとのやり取りから自身も学ぶ姿勢を忘れなければ、十分に問題解決の役に立ってくれます。

おそらく、AIを活用しても最終的にはIT人材が必要になる部分はあると思いますし、その方が効率的といった場合もあるはずです。しかし、その場合でも、先述の例のように一度AIとやり取りをして、要件を言語化しておけば、IT人材に発注するときもスムーズに、かつ工数を抑えての発注が可能になります。

ドキュメントの形でIT人材への指示書が欲しければ、追加でプロンプトにこう入力すれば良いでしょう。

これまでにやり取りしたシステムを外注したいと思います。仕様、設計をITエンジニア向けの指示書として生成してください

4. まとめ – AIは“エンジニアに代わる存在”ではなく、“エンジニアのいない組織を支える存在”

生成AIを活用することで、NPO・非営利組織でも自分たちの言葉で技術を扱うことができるようになります。

これは、これまで専任のIT人材がいるケースや外注をするケースにおいても問題になっていた箇所です。
発注側・依頼側のNPO・非営利組織と受注側のIT人材との間で言葉が違い、コンテキストが違い、重視する価値が違うため、要望とはズレたものが出来上がったり、要望を実現するために過剰に高いコストがかかることがままありました。

生成AIはうまく使うことで、NPO・非営利組織とIT人材との間の橋渡し、トランスレーターとして活躍できます。
もちろん、生成AIとのやり取りを通して、自組織のエンジニアリングの知見を深めていくという使い方もできるでしょう。

システム化を安易に検討する前に、一度AIと会話して、自分たちの行いたいことや実現可能性を検証し、その結果をもって必要ならばIT人材と向き合うというスタイルを試してみてください。

この記事をシェアする

FacebookHatena blogX

関連記事