この記事は、無料診断フォームに入った回答をそのまま読むだけで終わらせず、AI導入前にどの業務から整理するべきかをスコア化するための実装メモです。
AI APIは呼びません。Googleフォーム、スプレッドシート、GASだけで、問い合わせ内容を業務棚卸しログへ変換します。
作るもの
Googleフォームの回答シートから、次の3枚を作ります。
- form_responses: Googleフォームの回答
- workflow_inventory: 業務棚卸しログ
- scoring_rules: 優先度スコアのルール
完成形は、AIツール選定表ではありません。
「問い合わせ対応」「FAQ」「CRM」「承認フロー」「データ整理」のどこから着手するべきかを、フォーム回答から判断しやすくするための台帳です。
なぜフォーム回答を棚卸しログに変換するのか
無料診断フォームでは、次のような回答が集まりがちです。
- AIを入れたい業務
- 困っていること
- 使っているツール
- 月間件数
- 手作業の時間
- 個人情報や契約情報の有無
- 誰が最終確認しているか
この回答を文章のまま読むだけだと、毎回判断が属人化します。
たとえば、同じ「問い合わせ対応をAI化したい」という相談でも、状態は大きく違います。
| 状態 | 先にやること |
|---|---|
| FAQがない | FAQ候補を作る |
| 問い合わせログがない | 受付ログを作る |
| 個人情報が多い | AIへ送らない情報を決める |
| 返金や契約が多い | 人間確認ルールを作る |
| 件数が少ない | 自動化より運用整理を優先する |
そのため、フォーム回答を受けた時点で、業務領域、件数、リスク、手戻り、AI向きかどうかを同じ形式に揃えます。
フォーム項目
Googleフォームには、最初は次の項目を置きます。
| 項目 | 例 | 目的 |
|---|---|---|
| company_name | 株式会社サンプル | 相談元 |
| contact_name | 山田 | 連絡担当 |
| workflow_area | 問い合わせ対応 | 業務領域 |
| current_tools | Googleフォーム, スプレッドシート | 現在のツール |
| monthly_volume | 120 | 月間件数 |
| manual_minutes_per_case | 8 | 1件あたり手作業分 |
| pain_points | 返信が遅れる、FAQがない | 困りごと |
| has_personal_data | yes | 個人情報の有無 |
| has_contract_or_money | yes | 契約、料金、返金の有無 |
| has_human_approval | no | 人間確認ルールの有無 |
| desired_ai_use | 返信下書き | AIでやりたいこと |
| notes | 自由記述 | 補足 |
ポイントは、AIでやりたいことだけでなく、現状のログ、確認者、リスクを聞くことです。
AIに何をさせるかは、業務の入口が見えてから決めた方が安定します。
workflow_inventory シート
GASで作る棚卸しログには、次の列を置きます。
| 列 | 項目 | 意味 |
|---|---|---|
| A | logged_at | 変換日時 |
| B | diagnosis_id | 診断ID |
| C | company_name | 会社名 |
| D | workflow_area | 業務領域 |
| E | monthly_volume | 月間件数 |
| F | manual_hours_monthly | 月間手作業時間 |
| G | risk_level | low / medium / high |
| H | readiness_level | low / medium / high |
| I | priority_score | 優先度スコア |
| J | recommended_first_step | 最初にやること |
| K | ai_use_candidate | AI用途候補 |
| L | blocker_notes | 先に解くべき課題 |
| M | source_row | 元フォーム回答行 |
priority_score は「すぐAI化してよい点数」ではありません。
優先度が高いほど、先に業務整理、ログ整備、FAQ化、人間確認ルール化の価値が大きいという意味で扱います。
scoring_rules シート
スコアの重みはコードに直書きせず、シートで管理します。
| rule_id | condition_key | condition_value | score | recommended_first_step | blocker_note |
|---|---|---|---|---|---|
| volume_high | monthly_volume_min | 100 | 20 | 受付ログと分類ルールを作る | 件数が多いため判断ログが必要 |
| manual_time_high | manual_hours_min | 20 | 20 | 手作業の内訳を分ける | どの作業が重いか分解する |
| personal_data | has_personal_data | yes | 25 | AIへ送らない情報を決める | 個人情報の扱いを先に固定 |
| contract_money | has_contract_or_money | yes | 25 | 人間確認ルールを作る | 契約、料金、返金は自動送信しない |
| no_approval | has_human_approval | no | 15 | 承認フローを作る | 最終確認者が未定 |
| faq_intent | desired_ai_use_contains | FAQ | 10 | FAQ候補ログを作る | よくある質問を先に集める |
| reply_draft_intent | desired_ai_use_contains | 返信下書き | 10 | 返信下書きの承認キューを作る | 送信前確認が必要 |
この表は、最初から完璧にする必要はありません。
診断を重ねながら、よく出る相談に合わせて重みと推奨ステップを見直します。
GASコード
次のコードは、フォーム回答から業務棚卸しログを作る最小サンプルです。
const SHEETS = {
responses: 'form_responses',
inventory: 'workflow_inventory',
rules: 'scoring_rules',
};
const INVENTORY_HEADERS = [
'logged_at',
'diagnosis_id',
'company_name',
'workflow_area',
'monthly_volume',
'manual_hours_monthly',
'risk_level',
'readiness_level',
'priority_score',
'recommended_first_step',
'ai_use_candidate',
'blocker_notes',
'source_row',
];
function buildWorkflowInventoryFromResponses() {
const ss = SpreadsheetApp.getActiveSpreadsheet();
const responseSheet = ss.getSheetByName(SHEETS.responses);
const inventorySheet = getOrCreateSheet_(ss, SHEETS.inventory, INVENTORY_HEADERS);
const rules = loadScoringRules_(ss);
const responses = readObjectsWithRow_(responseSheet);
const existingSourceRows = loadExistingSourceRows_(inventorySheet);
const newRows = [];
responses.forEach(({ rowNumber, data }) => {
if (existingSourceRows.has(rowNumber)) return;
const result = scoreResponse_(data, rules);
newRows.push([
new Date(),
buildDiagnosisId_(rowNumber),
data.company_name || '',
data.workflow_area || '',
toNumber_(data.monthly_volume),
calcManualHoursMonthly_(data),
result.riskLevel,
result.readinessLevel,
result.priorityScore,
result.firstSteps.join(' / '),
data.desired_ai_use || '',
result.blockerNotes.join(' / '),
rowNumber,
]);
});
if (newRows.length > 0) {
inventorySheet
.getRange(inventorySheet.getLastRow() + 1, 1, newRows.length, INVENTORY_HEADERS.length)
.setValues(newRows);
}
}
function scoreResponse_(data, rules) {
const matched = rules.filter((rule) => matchesRule_(data, rule));
const priorityScore = matched.reduce((sum, rule) => sum + toNumber_(rule.score), 0);
return {
priorityScore,
riskLevel: resolveRiskLevel_(data, priorityScore),
readinessLevel: resolveReadinessLevel_(data),
firstSteps: unique_(matched.map((rule) => rule.recommended_first_step).filter(Boolean)),
blockerNotes: unique_(matched.map((rule) => rule.blocker_note).filter(Boolean)),
};
}
function matchesRule_(data, rule) {
const key = String(rule.condition_key || '').trim();
const value = String(rule.condition_value || '').trim();
if (key === 'monthly_volume_min') {
return toNumber_(data.monthly_volume) >= toNumber_(value);
}
if (key === 'manual_hours_min') {
return calcManualHoursMonthly_(data) >= toNumber_(value);
}
if (key === 'has_personal_data') {
return normalizeYesNo_(data.has_personal_data) === normalizeYesNo_(value);
}
if (key === 'has_contract_or_money') {
return normalizeYesNo_(data.has_contract_or_money) === normalizeYesNo_(value);
}
if (key === 'has_human_approval') {
return normalizeYesNo_(data.has_human_approval) === normalizeYesNo_(value);
}
if (key === 'desired_ai_use_contains') {
return String(data.desired_ai_use || '').includes(value);
}
return false;
}
function resolveRiskLevel_(data, priorityScore) {
if (
normalizeYesNo_(data.has_personal_data) === 'yes' ||
normalizeYesNo_(data.has_contract_or_money) === 'yes'
) {
return 'high';
}
if (priorityScore >= 40) return 'medium';
return 'low';
}
function resolveReadinessLevel_(data) {
const hasTools = String(data.current_tools || '').trim().length > 0;
const hasApproval = normalizeYesNo_(data.has_human_approval) === 'yes';
const hasArea = String(data.workflow_area || '').trim().length > 0;
if (hasTools && hasApproval && hasArea) return 'high';
if (hasTools && hasArea) return 'medium';
return 'low';
}
function calcManualHoursMonthly_(data) {
const volume = toNumber_(data.monthly_volume);
const minutes = toNumber_(data.manual_minutes_per_case);
return Math.round((volume * minutes / 60) * 10) / 10;
}
function loadScoringRules_(ss) {
const sheet = ss.getSheetByName(SHEETS.rules);
if (!sheet) return [];
return readObjectsWithRow_(sheet).map(({ data }) => data);
}
function loadExistingSourceRows_(sheet) {
if (sheet.getLastRow() < 2) return new Set();
const sourceRowColumn = INVENTORY_HEADERS.indexOf('source_row') + 1;
const values = sheet.getRange(2, sourceRowColumn, sheet.getLastRow() - 1, 1).getValues();
return new Set(values.map(([value]) => Number(value)).filter(Boolean));
}
function readObjectsWithRow_(sheet) {
const values = sheet.getDataRange().getValues();
if (values.length < 2) return [];
const headers = values[0].map((header) => String(header).trim());
return values.slice(1).map((row, index) => {
const data = {};
headers.forEach((header, columnIndex) => {
data[header] = row[columnIndex];
});
return {
rowNumber: index + 2,
data,
};
});
}
function getOrCreateSheet_(ss, name, headers) {
const sheet = ss.getSheetByName(name) || ss.insertSheet(name);
if (sheet.getLastRow() === 0) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
}
return sheet;
}
function buildDiagnosisId_(rowNumber) {
const date = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyyMMdd');
return `DIA-${date}-${String(rowNumber).padStart(4, '0')}`;
}
function normalizeYesNo_(value) {
const text = String(value || '').trim().toLowerCase();
if (['yes', 'true', 'あり', '有', 'はい'].includes(text)) return 'yes';
if (['no', 'false', 'なし', '無', 'いいえ'].includes(text)) return 'no';
return text;
}
function toNumber_(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function unique_(items) {
return [...new Set(items)];
}
フォーム送信時に動かす
フォーム回答が追加された時に棚卸しログを更新したい場合は、Apps Scriptのトリガーで次の設定にします。
| 項目 | 設定 |
|---|---|
| 実行する関数 | buildWorkflowInventoryFromResponses |
| イベントのソース | スプレッドシートから |
| イベントの種類 | フォーム送信時 |
既存回答をまとめて変換したい場合は、手動で同じ関数を実行します。
source_row を残しているので、同じフォーム回答を重複登録しにくくなります。
スコアの見方
priority_score は、次のように読みます。
| score | 読み方 | 次の行動 |
|---|---|---|
| 0から20 | 低優先 | まず相談内容を読む |
| 21から45 | 中優先 | ログ、FAQ、確認者を整える |
| 46以上 | 高優先 | AI化より先に運用ルールを作る |
注意したいのは、点数が高いほどAIで自動化しやすい、という意味ではないことです。
点数が高い相談は、改善余地が大きい一方で、個人情報、契約、承認不足などのリスクも含みやすいです。
よくある最初の推奨ステップ
フォーム回答から出す recommended_first_step は、最初は次の程度で十分です。
| 推奨ステップ | 使う場面 |
|---|---|
| 受付ログと分類ルールを作る | 件数が多いがログが散らばっている |
| FAQ候補ログを作る | 同じ質問が繰り返されている |
| AIへ送らない情報を決める | 個人情報や契約情報が含まれる |
| 人間確認ルールを作る | 返信、料金、返金、契約に触れる |
| 承認キューを作る | AI下書きを外へ出す前に確認が必要 |
| CRM項目を揃える | 顧客メモが後から探せない |
この一覧は営業資料ではなく、診断後の作業順を決めるための運用メモです。
AI APIを呼ぶ前に確認すること
この段階では、AI APIを呼ばなくても十分に価値があります。
先に確認するのは次の項目です。
- フォーム回答に個人情報や認証情報が入っていないか
- AIに渡してよい項目と伏せる項目が分かれているか
- 契約、料金、返金、専門判断に見える相談を人間確認に戻せるか
- 診断結果を誰が読み、次の提案へ進めるか
- 判断理由がログに残るか
ここが決まっていないままAI要約や返信下書きを入れると、便利さより確認コストが増えることがあります。
まとめ
無料診断フォームは、単なる問い合わせ窓口ではなく、AI導入前の業務棚卸しログにできます。
フォーム回答をスコア化すると、どの業務から整えるべきか、どこに人間確認が必要か、AIを使う前に何を固定すべきかが見えやすくなります。
Miraigentでは、AI導入の相談でも、最初にツール名ではなく業務フロー、ログ、確認ルールを整理します。フォーム回答を棚卸しログへ変換することは、その入口になります。
