はじめに:今年も生成AIで遊んでみる話
kintone advent calendar 2025 の 17日目にエントリーしました、Spicaと申します。
今年もカレンダーの季節ですね。昨年同様、Qiitaへの寄稿がまたしても毎年のこの1回のみ。しかし開き直って生きていきます。
昨年の寄稿では、APIを使って入力内容を生成させるという、生成AIの活用方法としては単純なものをやってみました。
今年もやはり生成AI絡みであろうと。
そしてAIを使うからには、やはり非エンジニアの方でも使えるもの、というのがよかろうと。というわけで今回のテーマはタイトルの通りこちら。
AIにコーディングさせて js ファイル書くけどあくまでノーコードと言い張ってみる
kintone といえば「ノーコードで業務アプリ」が売りですが、最近は “AIがコードを書くなら、それってノーコードじゃない?” という、プログラマの職を奪いかねない(実際にはそんなことは全く無いですが)疑問が頭をよぎります。
生成AIを使えるかどうかは、昭和の時代にあった「ビデオの録画設定ができるかできないか」くらいの差があるとかないとか。道具は使いこなしてこそでありましょう。
というわけで、
- コードは書かない
- でも js ファイルは作る
- AI が全部書く
- それでも「ノーコードです!」と開き直る
これでいきます。
そして肝心の作る機能ですが、
ふりがな自動生成
というシンプルかつ安全な補助機能を作ってみることにします。
1:前提となる kintone アプリを作る
今回の実験では、あらかじめ作成済みのシンプルな kintone アプリを使います。
構成はとても単純で、「名前」と「そのフリガナ」を入力するだけのフォームです。
(※フィールド名とフィールドコードの違いを知っているひとは読み飛ばしてOKです)
こんなアプリを作ってみました。フィールドの構成は以下の通り。
| フィールド名 | 型 | フィールドコード | 役割 |
|---|---|---|---|
| 性 | 文字列(1行) | 性 | ファーストネームを手入力する |
| 名 | 文字列(1行) | 名 | セカンドネームを手入力する |
| 性(フリガナ) | 文字列(1行) | 性_カナ | ファーストネームのフリガナを自動入力する |
| 名(フリガナ) | 文字列(1行) | 名_カナ | セカンドネームのフリガナを自動入力する |
今回はこの構成でやりますというだけで、フィールド名、フィールドコードはてきとうに変更して大丈夫。
AIにコーディングさせるにあたって、指定をきっちり厳密にするとうまくやってくれるはず。
ここで重要なのはフィールド名ではなく フィールドコード のほうです。
では早速AIに働いてもらいましょー。
2:AIにコードを書いてもらう
ここからが今回の記事の本題です。AIに指示を出す前にちょっとこのテーマの意義めいたものを解説します。
自分でコードを書くのではなく、AIにコードを書いてもらう。
これは最近よく 「バイブコーディング(Vibe Coding)」 と呼ばれているやつですね。
AIがコードを書いてくれる、というと、
「勝手にいい感じにコードを書いてくれる魔法」
みたいな印象を持たれがちですが、実際にはそうではありません。
AIがコードを書くために必要なのは、人間側から渡す「指示」 です。
この指示文のことを、生成AIの世界では プロンプト と呼びます。
「プロンプト」 は頻出ワードなので初耳の方はこの際覚えてしまいましょう。
AIとのやり取りというと「毎回その場で思いつきで書くもの」というイメージがあるかもしれません。それでも最終的にはうまくいくかもしれないですが、問答形式では冗長になるし再現性も微妙です。
プロンプトというのはそういった「その場限りの指示」ではないのです。
- どういう環境で動かすのか
- 何を入力にして
- 何を出力したいのか
- どこをAIに任せるのか
- どこから人間が判断するのか
こういった条件を整理した 設計書 といえるものです。
なので、一度うまく書けたプロンプトは、
- 別のアプリでも使い回せる
- 修正や改善がしやすい
- 他の人に渡しても再現できる
という、立派な 「資産」 になるのです。
というわけで次は、AIに出す「指示」を整理していきます。
2-1:AI にやらせたいことを整理する
いきなりプロンプトを書き始める前に、
まずは 「今回 AI に何をやらせたいのか」 を人間側で整理します。
ここを曖昧にしたまま指示を出すと、AI はそれっぽいコードは書いてくれますが、動かない、もしくは意図した通りに動かない、ということが普通に起こります。
今回 AI にやってもらいたいことはシンプルに言うと次の通りです。
やりたいこと(機能要件)
- ユーザーが
- 「性」フィールド
- 「名」フィールド
に文字を入力する
- 入力された文字列を元に、フリガナの候補を取得する
- 同一アプリ内の 過去レコード を参照し、すでに登録されているフリガナがあればそれを使う
- 該当する過去レコードが存在しない場合は、JavaScript 内に定義した 静的な簡易辞書(日本の苗字・名前の頻出上位)を参照する
- その文字列を元に
- 「性(フリガナ)」
- 「名(フリガナ)」
にカタカナでセットする
- 自動生成は保存時ではなくボタンを押したときに実行 する
- フリガナは補助なので自動保存はしない
- 気に入らなければ手修正できる
実行環境の前提条件
- kintone の JavaScript カスタマイズとして動かす
- 最終的な成果物は 1つの js ファイル
- import / require / npm / CDN は使わない
- ブラウザ上で単体で動作する
ちょっとエンジニア寄りの条件を書いてますが、
普段の Web 開発の感覚で書かれたコードは、この条件を満たさないことが多いので、指定しておかないと
外部ファイルをjsでインポートする部分がある
みたいなコードを書いてしまうと思います。
ここは明示的に伝える必要があります。
そして、フリガナを「変換」するのではなく、すでにある正解を探してくる という方針です。
外部のAPIを使うとかはAPIトークンの発行の話が絡んでくるので、今回はしません。ただ、このあたりは応用が簡単な範囲だと思うのでアレンジでさくっといけそうです。(それこそやり方をAIに聞くのでぇす)
エラー時の挙動(デバッグしやすさ)
ここでいうデバッグとは プロンプト の修正です。
うまくいかないときに何が起きているかを詳しく出力するようにすることで、プロンプトの修正に反映しやすくなります。
あとは、今回は実験なので「うまく動かないときに何が起きているか」も見たいところです。
ということで、
- 例外は握りつぶさない
- console.error で
- どこで
- 何が起きたか
が分かるようにする
という点も、あらかじめ AI に伝えることにします。
ここまで整理できたら、
あとは この内容をそのまま文章にして AI に渡すだけ です。
次は、実際に AI に投げる プロンプト全文 を見ていきます。
2-2:実際に作ったプロンプト
以下が、今回の方針変更を反映して 実際にAIに投げたプロンプト全文 です。
あなたは JavaScript に詳しいエンジニアです。
kintone の JavaScript カスタマイズとして動作するコードを書いてください。
【前提】
- kintone のレコード詳細画面・編集画面で動作すること
- 最終的な成果物は「1つの JavaScript ファイル」とする
- import / require / npm / CDN / 外部ファイルの読み込みは使用しない
- ブラウザ上で単体で動作するコードにすること
【フィールド構成】
- 姓を入力するフィールド
- フィールド名:性
- フィールドコード:性
- 名を入力するフィールド
- フィールド名:名
- フィールドコード:名
- 姓のフリガナを入力するフィールド
- フィールド名:性(フリガナ)
- フィールドコード:性_カナ
- 名のフリガナを入力するフィールド
- フィールド名:名(フリガナ)
- フィールドコード:名_カナ
【やりたいこと(機能要件)】
- ユーザーが
- 「性」フィールド
- 「名」フィールド
に文字を入力する
- 入力された文字列を元に、フリガナの候補を取得する
- 同一アプリ内の過去レコードを参照し、
すでに登録されているフリガナがあればそれを使用する
- 該当する過去レコードが存在しない場合は、
JavaScript 内に定義した静的な簡易辞書
(日本の苗字・名前の頻出上位を想定)を参照する
- 取得したフリガナを
- 性 → 性_カナ
- 名 → 名_カナ
にカタカナでセットする
- 自動生成は保存時ではなく、
ユーザーが押すボタンをレコード作成および編集画面上に配置し、
そのボタンを押したときに実行する
- フリガナは補助用途とし、
自動保存は行わず、ユーザーが手修正できる状態にする
【実装方針】
- フリガナを「変換」する処理は行わない
- すでに存在する正解(過去レコード・静的辞書)を
参照してセットする方針とする
- 外部 API は使用しない
【過去レコード参照について】
- kintone REST API を使用して同一アプリ内のレコードを検索する
- 「性」または「名」が完全一致するレコードを対象とする
- フリガナフィールドに値が存在する場合のみ使用する
- 複数ヒットした場合は、最初に取得したものを使用してよい
【静的辞書について】
- JavaScript 内にオブジェクトとして定義する
- キー:漢字の姓名
- 値:対応するカタカナのフリガナ
内訳の補足条件:
- 対象は日本人の「姓」「名」の頻出上位
- 法務省の公開統計や一般的な日本人名ランキングを根拠とした
「現実的に多い名前」のみを採用すること
- 架空・創作・極端に珍しい名前は含めないこと
- 合計100件ちょうど出力すること
- 苗字:60件
- 名前:40件
- 男性名・女性名が極端に偏らないようにする
【エラー処理・デバッグ】
- 例外は握りつぶさず、try-catch を使用する
- エラー発生時は console.error を使って
- どの処理で
- どのようなエラーが発生したか
が分かるように出力すること
【その他】
- コード内には適度にコメントを入れ、
処理内容が追いやすいようにすること
このプロンプトを投げることで、漢字を含む名前に対して
- 外部APIなし
- kintone内部だけで完結するフリガナ補助機能
を生成AIに実装させることができます。
実際に書かせたものが以下のコード。
生成は ChatGPT 5.2 Auto で行いました。
生成されたJavscriptコード
(function () {
"use strict";
/**
* kintone フリガナ自動セット(過去レコード / 静的辞書参照)
* - 詳細画面・編集画面でボタンを表示
* - 詳細画面は編集できないため、押下で編集画面へ遷移(候補は sessionStorage で引き継ぎ)
* - 変換処理(漢字→かな変換)はしない。既存の正解(過去レコード / 辞書)のみ参照。
*/
// ===== 設定(フィールドコード)=====
var FIELD_SEI = "性";
var FIELD_MEI = "名";
var FIELD_SEI_KANA = "性_カナ";
var FIELD_MEI_KANA = "名_カナ";
// ===== 静的辞書(100件ちょうど:苗字60件 / 名前40件)=====
// キー:漢字(姓 or 名) / 値:カタカナ
var STATIC_DICT_SEI = {
"佐藤": "サトウ",
"鈴木": "スズキ",
"高橋": "タカハシ",
"田中": "タナカ",
"伊藤": "イトウ",
"渡辺": "ワタナベ",
"山本": "ヤマモト",
"中村": "ナカムラ",
"小林": "コバヤシ",
"加藤": "カトウ",
"吉田": "ヨシダ",
"山田": "ヤマダ",
"佐々木": "ササキ",
"山口": "ヤマグチ",
"斎藤": "サイトウ",
"松本": "マツモト",
"井上": "イノウエ",
"木村": "キムラ",
"林": "ハヤシ",
"清水": "シミズ",
"山崎": "ヤマザキ",
"森": "モリ",
"阿部": "アベ",
"池田": "イケダ",
"橋本": "ハシモト",
"山下": "ヤマシタ",
"石川": "イシカワ",
"中島": "ナカジマ",
"前田": "マエダ",
"小川": "オガワ",
"藤田": "フジタ",
"後藤": "ゴトウ",
"岡田": "オカダ",
"長谷川": "ハセガワ",
"村上": "ムラカミ",
"近藤": "コンドウ",
"石井": "イシイ",
"斉藤": "サイトウ",
"坂本": "サカモト",
"遠藤": "エンドウ",
"青木": "アオキ",
"藤井": "フジイ",
"西村": "ニシムラ",
"福田": "フクダ",
"太田": "オオタ",
"三浦": "ミウラ",
"藤原": "フジワラ",
"岡本": "オカモト",
"松田": "マツダ",
"中川": "ナカガワ",
"中野": "ナカノ",
"原田": "ハラダ",
"小野": "オノ",
"田村": "タムラ",
"竹内": "タケウチ",
"金子": "カネコ",
"和田": "ワダ",
"中山": "ナカヤマ",
"石田": "イシダ",
"上田": "ウエダ"
};
var STATIC_DICT_MEI = {
// 男性寄り
"太郎": "タロウ",
"翔": "ショウ",
"大輔": "ダイスケ",
"健太": "ケンタ",
"拓也": "タクヤ",
"亮": "リョウ",
"直樹": "ナオキ",
"悠斗": "ユウト",
"陽斗": "ハルト",
"蓮": "レン",
"颯太": "ソウタ",
"大翔": "ヒロト",
"優太": "ユウタ",
"隼人": "ハヤト",
"一樹": "カズキ",
"海斗": "カイト",
"光": "ヒカル",
"誠": "マコト",
"智也": "トモヤ",
"祐介": "ユウスケ",
// 女性寄り
"花子": "ハナコ",
"愛": "アイ",
"美咲": "ミサキ",
"結衣": "ユイ",
"葵": "アオイ",
"杏奈": "アンナ",
"優奈": "ユウナ",
"陽菜": "ヒナ",
"莉子": "リコ",
"美優": "ミユ",
"美穂": "ミホ",
"彩香": "アヤカ",
"真由": "マユ",
"沙織": "サオリ",
"麻衣": "マイ",
"由美": "ユミ",
"明日香": "アスカ",
"恵": "メグミ",
"真紀": "マキ",
"幸子": "サチコ"
};
// ===== 内部キー(詳細→編集の引き継ぎ用)=====
function storageKey(appId, recordId) {
return "kana_suggest:" + String(appId) + ":" + String(recordId);
}
// ===== ユーティリティ =====
function isBlank(v) {
return v === null || v === undefined || String(v).trim() === "";
}
// ひらがな→カタカナ(過去レコードの値がひらがなでも統一)
function toKatakana(str) {
try {
var s = String(str || "");
return s.replace(/[\u3041-\u3096]/g, function (ch) {
return String.fromCharCode(ch.charCodeAt(0) + 0x60);
});
} catch (e) {
console.error("[kana] toKatakana error:", e);
return String(str || "");
}
}
// クエリ用に " をエスケープ
function escapeForKintoneQuery(value) {
return String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
// ===== 過去レコード検索(完全一致 / フリガナが空でないもののみ / 最初の1件)=====
async function findKanaFromPastRecords(appId, kanjiFieldCode, kanaFieldCode, kanjiValue, currentRecordId) {
try {
if (isBlank(kanjiValue)) return "";
var conds = [];
conds.push(kanjiFieldCode + ' = "' + escapeForKintoneQuery(kanjiValue) + '"');
conds.push(kanaFieldCode + ' != ""');
// 自分自身を除外(編集時に自レコードがヒットするのを避ける)
if (!isBlank(currentRecordId)) {
conds.push("$id != " + String(currentRecordId));
}
var query = conds.join(" and ") + " order by $id asc limit 1";
var params = { app: appId, query: query, fields: [kanaFieldCode] };
var resp = await kintone.api(kintone.api.url("/k/v1/records", true), "GET", params);
if (!resp || !resp.records || resp.records.length === 0) return "";
var kana = resp.records[0][kanaFieldCode] && resp.records[0][kanaFieldCode].value;
if (isBlank(kana)) return "";
return toKatakana(kana);
} catch (e) {
console.error("[kana] findKanaFromPastRecords error:", {
appId: appId,
kanjiFieldCode: kanjiFieldCode,
kanaFieldCode: kanaFieldCode,
kanjiValue: kanjiValue,
currentRecordId: currentRecordId,
error: e
});
throw e;
}
}
// ===== 静的辞書参照 =====
function findKanaFromStaticDict(dict, kanjiValue) {
try {
if (isBlank(kanjiValue)) return "";
var key = String(kanjiValue).trim();
var kana = dict[key];
return isBlank(kana) ? "" : toKatakana(kana);
} catch (e) {
console.error("[kana] findKanaFromStaticDict error:", { kanjiValue: kanjiValue, error: e });
throw e;
}
}
// ===== 候補生成(過去レコード→辞書の順)=====
async function resolveKana(appId, kanjiFieldCode, kanaFieldCode, dict, kanjiValue, currentRecordId) {
try {
// 1) 過去レコード
var kanaPast = await findKanaFromPastRecords(appId, kanjiFieldCode, kanaFieldCode, kanjiValue, currentRecordId);
if (!isBlank(kanaPast)) return kanaPast;
// 2) 静的辞書
var kanaDict = findKanaFromStaticDict(dict, kanjiValue);
if (!isBlank(kanaDict)) return kanaDict;
return "";
} catch (e) {
console.error("[kana] resolveKana error:", {
kanjiFieldCode: kanjiFieldCode,
kanaFieldCode: kanaFieldCode,
kanjiValue: kanjiValue,
currentRecordId: currentRecordId,
error: e
});
throw e;
}
}
// ===== 編集画面に反映(既に入力済みなら上書きしない)=====
function applyKanaToRecordIfEmpty(record, kanaFieldCode, kanaValue) {
try {
if (isBlank(kanaValue)) return false;
if (!record || !record[kanaFieldCode]) return false;
var current = record[kanaFieldCode].value;
if (!isBlank(current)) return false; // 手修正を尊重して上書きしない
record[kanaFieldCode].value = toKatakana(kanaValue);
return true;
} catch (e) {
console.error("[kana] applyKanaToRecordIfEmpty error:", { kanaFieldCode: kanaFieldCode, kanaValue: kanaValue, error: e });
throw e;
}
}
// ===== ボタン設置(重複防止)=====
function addButtonIfNeeded(onClick) {
try {
var space = kintone.app.record.getHeaderMenuSpaceElement();
if (!space) return;
var existing = space.querySelector('button[data-kana-btn="1"]');
if (existing) return;
var btn = document.createElement("button");
btn.type = "button";
btn.textContent = "フリガナ候補をセット";
btn.dataset.kanaBtn = "1";
btn.style.marginLeft = "8px";
btn.style.padding = "6px 10px";
btn.style.cursor = "pointer";
btn.addEventListener("click", onClick);
space.appendChild(btn);
} catch (e) {
console.error("[kana] addButtonIfNeeded error:", e);
throw e;
}
}
// ===== 詳細画面:押下で候補生成→編集画面へ遷移(候補は sessionStorage に保存)=====
kintone.events.on("app.record.detail.show", function (event) {
try {
addButtonIfNeeded(async function () {
var appId = kintone.app.getId();
var record = event.record;
var recordId = kintone.app.record.getId();
try {
var sei = record[FIELD_SEI] && record[FIELD_SEI].value;
var mei = record[FIELD_MEI] && record[FIELD_MEI].value;
var seiKana = await resolveKana(appId, FIELD_SEI, FIELD_SEI_KANA, STATIC_DICT_SEI, sei, recordId);
var meiKana = await resolveKana(appId, FIELD_MEI, FIELD_MEI_KANA, STATIC_DICT_MEI, mei, recordId);
// sessionStorage に保存して編集画面で反映
var payload = { seiKana: seiKana, meiKana: meiKana, at: Date.now() };
sessionStorage.setItem(storageKey(appId, recordId), JSON.stringify(payload));
// 編集画面へ(同一タブ遷移)
// ※ kintone の URL 仕様により hash の mode=edit を利用
var base = location.href.replace(/#.*$/, "");
location.href = base + "#record=" + encodeURIComponent(String(recordId)) + "&mode=edit";
} catch (e) {
console.error("[kana] detail button click error:", e);
throw e;
}
});
} catch (e) {
console.error("[kana] app.record.detail.show handler error:", e);
}
return event;
});
// ===== 編集画面:ボタン押下で候補生成→その場でセット(未入力フィールドのみ)=====
kintone.events.on("app.record.edit.show", function (event) {
try {
var appId = kintone.app.getId();
var recordId = kintone.app.record.getId();
// 詳細画面からの引き継ぎがあれば反映
try {
var raw = sessionStorage.getItem(storageKey(appId, recordId));
if (raw) {
var data = JSON.parse(raw);
var changed1 = applyKanaToRecordIfEmpty(event.record, FIELD_SEI_KANA, data.seiKana);
var changed2 = applyKanaToRecordIfEmpty(event.record, FIELD_MEI_KANA, data.meiKana);
if (changed1 || changed2) {
kintone.app.record.set({ record: event.record });
}
sessionStorage.removeItem(storageKey(appId, recordId));
}
} catch (e) {
console.error("[kana] apply from sessionStorage error:", e);
// 引き継ぎ失敗は致命ではないので継続
}
addButtonIfNeeded(async function () {
try {
var rec = kintone.app.record.get().record;
var sei = rec[FIELD_SEI] && rec[FIELD_SEI].value;
var mei = rec[FIELD_MEI] && rec[FIELD_MEI].value;
var seiKana = await resolveKana(appId, FIELD_SEI, FIELD_SEI_KANA, STATIC_DICT_SEI, sei, recordId);
var meiKana = await resolveKana(appId, FIELD_MEI, FIELD_MEI_KANA, STATIC_DICT_MEI, mei, recordId);
var changed = false;
changed = applyKanaToRecordIfEmpty(rec, FIELD_SEI_KANA, seiKana) || changed;
changed = applyKanaToRecordIfEmpty(rec, FIELD_MEI_KANA, meiKana) || changed;
if (changed) {
kintone.app.record.set({ record: rec });
}
} catch (e) {
console.error("[kana] edit button click error:", e);
throw e;
}
});
} catch (e) {
console.error("[kana] app.record.edit.show handler error:", e);
}
return event;
});
})();
2-3 実際に動かしてみた
生成されたコードをコピーしてテキストファイルとして保存します。
拡張子は .js にしましょう。保存したらアプリにいれます。
このへんの詳しいやり方は 公式のヘルプ を確認ということで。
アプリでデータ追加の画面を開いてみると「フリガナ候補をセット」というボタンが設置されています。
「佐藤」は辞書にあるのでフリガナが取得できています。
「権兵衛」はないので空欄のまま。

「竈門」「炭治郎」も辞書にないので空欄のままですが、
手入力で「カマド」「タンジロウ」というデータを登録したことがあれば……

次からはフリガナ取得できました。

ちゃんと意図した通りに動いてるようです。グッド。
2-4:プロンプトを工夫したポイント
まず「実装制約」を最初に書いて、生成AIにやってほしくないことを明示しています。
これをしないと通常のweb開発のように外部ファイルをインポートしようとしたり、外部APIを使うことをおすすめされたりします。
ボタンが画面によって配置されたりされなかったりしました。
閲覧画面にだけボタンを設置したパターンもありました。
最初から、作成時と編集時に設置、と書くべきでしたね。このへんは基本でした。
静的辞書を作る部分が意外と融通が効きませんでした。
「◯件くらい」とか「上限を◯件で」とか曖昧さを残すと、数件程度の辞書で終わらせて、あとは自分で追加してね、とか言ってきます。サボるな。
こんなところですね。
かなりすんなり、成功させることができたと思います。
これをベースにいろんなプロンプトへ応用して遊んでみるのも面白いかもしれません。
3:まとめ
今回は、
「AIにコードを書かせているけど、これはノーコードと言い張れるのでは?」
という、ネタだけどあわよくば本気、な実験をしてみました。
コードのパーツとしてはバイブコーディングを活用するのはもうエンジニアの必須スキルみたいになってきてると思いますが、すべてAIで、ということで成功したのは恐ろしいというかなんというか。
ただ、結局なにをさせるのか決めるのは人間だし、動作内容の責任を取るのも人間。
という当たり前の結論でもありました。
何をさせるかよりも何をさせないか、という部分に踏み込んでいかないとうまくいかなさそうという感じです。
非エンジニアの方がやる場合は、コードレビューが自分ではできないわけで、危険な動作がないかどうかという判断は難しいですね。
そこは細心の注意が必要です。
が!
臆して何もしないというのももったいくらい強力なパートナーになり得ると思うので、小規模なカスタマイズで利用するのは全然アリだな、と個人的には思います。
さて最後にいつもの宣伝です
『機能拡張スタンダードAll-In』というプラグインを販売しておりますー。
レイアウトを作ってPDF出力したり、自動計算とかレコード検索の入力ボックス設置などいろいろできるプラグインとなっております。
2017年のリリース以来ありがたいことに多くの方にご利用いただいております。
機能追加、最近ちょっと滞っていますが……個人的に開発環境も刷新しましたのでまだまだ頑張っていきます。
お問い合わせなど不要でご試用いただけますので、ひとつお試しいただければと。
そんな感じでーす!
