LLM経由のデータ取り込みを決定論化する — キャッシュが静かに嘘をつくのを止める
AIにAPI結果をJSONへ転記させる構成は、欠落と幻を同時に作り件数の自己申告では検出できない。サービスアカウント+ドメイン全体の委任で依存ゼロの決定論スクリプトに置き換える手順と、読み取り側で「データ無し」と「該当無し」を区別するガードまで。
約8.3万トークンの節約 (API料金換算で約120円分)。 要件定義・技術調査・試行錯誤ぶんのトークンがまるごと不要になります。※ 出品者申告とレビューに基づく推定値。モデル・タスク内容により変動します。
この巻物について
「LLM経由のデータ取り込みを決定論化する — キャッシュが静かに嘘をつくのを止める」は、業務自動化カテゴリのAI指示書(MDファイル)です。AIにAPI結果をJSONへ転記させる構成は、欠落と幻を同時に作り件数の自己申告では検出できない。サービスアカウント+ドメイン全体の委任で依存ゼロの決定論スクリプトに置き換える手順と、読み取り側で「データ無し」と「該当無し」を区別するガードまで。この巻物をAIに読み込ませると、ゼロから設計・調査する場合に比べて 約8.3万トークン(API料金換算で約120円)・87%のトークンを節約できます。
- カテゴリ
- 業務自動化
- 対応AI
- claude-code、cursor、codex-cli
- ライセンス
- 商用利用可 (再販不可)
- 価格
- 無料
- ゼロから開発時
- 約9.5万トークン
- この巻物使用時
- 約1.2万トークン
- 節約量
- 約8.3万トークン (約120円)
- 更新日
- 2026-08-27
使い方 (AIに渡す3つの方法)
いちばん簡単なのはワンライナー。Claude Code のターミナルに貼るだけです。
claude "https://makimono-md.vercel.app/api/v1/files/llm-5/raw を読み込んで、この指示書どおりに実装して"
中身
LLM 経由のデータ取り込みを決定論化する — キャッシュが静かに嘘をつくのを止める
AI エージェントに「API を叩いて結果を JSON ファイルに保存させる」構成は、静かに壊れる。 この指示書は、実際に事故った構成とその直し方(決定論スクリプトへの置換+読み取り側のガード)をまとめたもの。 外部 API のデータをローカルにキャッシュして使うシステム全般(カレンダー・在庫・顧客・料金表など)に効く。
1. 何が起きるのか(実例)
ある社内アプリが「担当者のカレンダーから空き時間を出す」機能を持っていた。 取り込みは次の構成だった:
定期フック → headless の AI CLI を起動
→ AI が Calendar MCP で予定を取得
→ AI が読んだ内容を JSON ファイルに手書き保存
→ アプリはその JSON を読んで空き時間を計算
API の実データと保存済み JSON を1件ずつ突き合わせたところ、1回の更新で両方向の誤りが同時に出ていた。
| 種別 | 実際に起きたこと | 表面化した症状 |
|---|---|---|
| 欠落 | 実在する予定2件が JSON に書かれなかった | その時間が「空き」として顧客に提示された |
| 幻 | 別の日時へ移動済みの予定が、移動前の位置に残っていた | 空いている時間が「予定あり」で潰された |
ログには refreshed 57 events と出ていた。しかしこれは書いた側の自己申告である。
欠落と幻が1件ずつなら総数は一致してしまうため、件数の自己報告では原理的に検出できない。
さらに悪いことに、この構成は「空き時間が1枠ずれる」という形で表面化する。 落ちるでもエラーになるでもないので、人間も気づかない。気づいたのは、 提示した候補時間に既に別の予定が入っていると担当者が指摘したときだった。
2. 原則
LLM に任せるのは「判断」だけ。値の移送・整形・保存は必ず決定論的なコードでやる。
LLM は要約・分類・判断には強いが、数十件の構造化データを一字一句転記する用途には向かない。 「件数を報告させる」「フォーマットを厳密に指定する」といった対策はどれも自己申告の域を出ない。
「認証経路が MCP しかないから AI 経由にした」が典型的な入口。先に決定論的な認証経路を探すこと。
3. 直し方 — サービスアカウント + ドメイン全体の委任で直接叩く
Google Workspace を使っている組織なら、サービスアカウント (SA) + Domain-Wide Delegation (DWD) で ユーザー個人の OAuth 同意なしに、そのユーザーとして API を読める。依存パッケージもゼロで書ける。
3-1. 既存の SA を使い回せるか先に確認する
新規に作る前に、組織内に既に DWD 設定済みの SA があるか探す。あればスコープ追加だけで済む。
# SA 鍵 JSON を探す(client_email と private_key を持つファイル)
# 秘密の値そのものは絶対に出力しないこと。パスとキー名の有無だけ確認する
3-2. JWT を自己署名してアクセストークンを取る(依存ゼロ)
Node 標準の crypto だけで書ける。ライブラリは不要。
import { createSign } from 'node:crypto';
const TOKEN_URL = 'https://oauth2.googleapis.com/token';
const SCOPE = 'https://www.googleapis.com/auth/calendar.readonly'; // 必要な読み取りスコープだけ
const b64url = (v) => Buffer.from(v).toString('base64url');
async function getAccessToken(keyPath, impersonatedUser) {
const key = JSON.parse(fs.readFileSync(keyPath, 'utf8'));
const now = Math.floor(Date.now() / 1000);
const header = b64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));
const claims = b64url(JSON.stringify({
iss: key.client_email,
sub: impersonatedUser, // ← ここが DWD。このユーザーとして読む
scope: SCOPE,
aud: TOKEN_URL,
iat: now,
exp: now + 3600,
}));
const signer = createSign('RSA-SHA256');
signer.update(`${header}.${claims}`);
const assertion = `${header}.${claims}.${signer.sign(key.private_key, 'base64url')}`;
const res = await fetch(TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion,
}),
});
const result = await res.json();
if (!res.ok || typeof result.access_token !== 'string') {
// ★ このエラーは必ず「スコープ未追加の可能性」を明示する。最初に必ず踏む
if (result.error === 'unauthorized_client' || res.status === 401 || res.status === 403) {
throw new Error(`${result.error}: 管理コンソールの「ドメイン全体の委任」に ${SCOPE} が未追加の可能性`);
}
throw new Error(`token request failed (${result.error ?? res.status})`);
}
return result.access_token;
}
秘密の扱い: private_key / access_token を標準出力・ログ・例外メッセージに絶対に含めない。
API のレスポンス body をそのままエラーに echo しないこと(トークンや個人情報が混じる)。
3-3. 人間にしか出来ない準備(2つだけ・API/CLI では不可)
この2つは Google の管理画面でしか変更できない。自動化を試みても権限で弾かれる
(実測: SA 自身には serviceusage.services.enable が無く AUTH_PERMISSION_DENIED)。
だからこそ、手順を非エンジニアでも踏めるフル手順で書いて渡す。
- 管理コンソール → セキュリティ → API の制御 → ドメイン全体の委任
対象 SA のクライアント ID の行を「編集」→ OAuth スコープ欄に、
既存の文字列を消さずにカンマ区切りで読み取りスコープを追記 → 承認
- ⚠️ 既存スコープを消すと他の自動化が全部止まる。「追記」を太字で伝えること
- Cloud Console → API とサービス → 対象の API → 「有効にする」
3-4. 取り込みスクリプトに必ず入れる4点
// ① nextPageToken を最後まで走査する(取りこぼしの最大要因)
let pageToken;
do {
const params = new URLSearchParams({ /* ... */ maxResults: '2500' });
if (pageToken) params.set('pageToken', pageToken);
const data = await fetchJson(url, params);
items.push(...data.items);
pageToken = data.nextPageToken;
} while (pageToken);
// ② tmp → rename の atomic 書き込み(途中失敗で壊れた JSON を残さない)
// ③ 書いた後に読み直して自己検証する
// ④ 検証失敗なら既存データを残して非ゼロ終了(壊れたキャッシュを本番に置かない)
function writeAtomicallyAndVerify(cache, expectedCount) {
const tmp = path.join(DIR, `.cache.json.${process.pid}.tmp`);
let backupMade = false;
try {
fs.writeFileSync(tmp, JSON.stringify(cache, null, 2) + '\n', { encoding: 'utf8', flag: 'wx' });
validate(fs.readFileSync(tmp, 'utf8'), expectedCount);
if (fs.existsSync(FILE)) { fs.copyFileSync(FILE, BACKUP); backupMade = true; }
fs.renameSync(tmp, FILE);
validate(fs.readFileSync(FILE, 'utf8'), expectedCount); // rename 後にもう一度
} catch (e) {
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
if (backupMade) fs.copyFileSync(BACKUP, FILE); // 既存に戻す
throw e;
}
}
検証で「開始日が対象期間内」を要求してはいけない。 期間をまたぐ長期データ (出張・休暇のような複数日にわたる予定)は開始日が過去になるため、 厳格に見ると正常な取得が毎回ロールバックされ、キャッシュが永久に更新されなくなる。 必ず「期間が重なるか」で判定する:
if (startDate > coverageTo || endDate < coverageFrom) throw new Error('does not overlap');
4. 読み取り側 — 「データが無い」と「該当が無い」を必ず区別する
これは取り込みと同じくらい重要。同じ事故のもう半分がここにあった。
キャッシュは7日分しか持っていないのに、UI は「14日」を選べた。 その結果、8日目以降が「予定ゼロ = 終日フリー」として提示されていた。 実際には予定が入っている日が「9:00〜18:00 まるごと空き」と表示される。
対策は3つセットで入れる:
- キャッシュ自身にカバー範囲を書く(
coverageFrom/coverageTo)。 「更新時刻 + 保持日数」から推測させない。推測は境界を1日ずらす(実際に踏んだ: 「今日から N 日先まで」は起点日を含めて N+1 日分であり、+N日で計算すると最終日が未取得扱いになる) - 範囲外の日は結果から外す。ただし「該当なし」ではなく
未取得という別ステータスにする - 必ず画面に警告を出す。黙って除外するのは不可
⚠️ 9/4〜9/9 はデータ未取得のため候補から除外しました(空きではありません)。更新してください。
黙って除外すると、利用者は「その期間は何も無い」と受け取る。それが事故の再発条件そのもの。
5. 移行時のチェックリスト
- 旧経路(AI にファイルを書かせる箇所)を消す。残すと古い経路が動き続ける
- 旧経路が AI CLI の場所を探すコードを持っていたら、それも消えるか確認
(実例: 「CLI が見つからなければ
return」が関数ではなくスクリプト最上位にあり、 後続の別処理まで丸ごと止まるバグが同居していた。置換で自然に解消した) - 新旧の日別件数を API 実データと突き合わせる。総数だけ見ない
- 認証未設定の状態で1回走らせ、失敗しても既存データが無傷であることを確認する
- 取得件数・期間・更新時刻を UI に表示する(
102件 / 8/27〜9/10 / 3分前)。 鮮度が見えないキャッシュは、古くなったことに誰も気づかない
6. 副産物
更新1回ごとの AI CLI 起動が消えるため、トークン消費もゼロになる。 「決定論にする」は正確さの話だが、コストにも効く。定期実行なら効果は毎日積み上がる。
7. まとめ
| やってはいけない | 代わりにやる |
|---|---|
| AI に API 結果を JSON へ転記させる | 決定論スクリプトで HTTP → parse → write |
| 件数を自己申告させて検証とする | 独立した取得元と日別件数を突き合わせる |
| 「データ無し」を「該当無し」として扱う | カバー範囲を明示し、範囲外は別ステータス + 警告 |
| 更新時刻から保持範囲を推測する | キャッシュ自身に範囲を書き込む |
| 検証で開始日の内包を要求する | 期間の重なりで判定する |
| 失敗時に書きかけを残す | tmp → rename + 失敗時は既存を復元 |
よくある質問
+「LLM経由のデータ取り込みを決定論化する — キャッシュが静かに嘘をつくのを止める」とは何ですか?
AIにAPI結果をJSONへ転記させる構成は、欠落と幻を同時に作り件数の自己申告では検出できない。サービスアカウント+ドメイン全体の委任で依存ゼロの決定論スクリプトに置き換える手順と、読み取り側で「データ無し」と「該当無し」を区別するガードまで。
+どれくらいトークン(費用)を節約できますか?
ゼロから開発すると約9.5万トークンかかりますが、この巻物を使えば約1.2万トークンで済みます。差し引き約8.3万トークン(API料金換算で約120円)・87%の節約です。
+どうやって使いますか?
無料です。MDファイルを Claude Code などのAIに読み込ませるだけ。ワンライナーをターミナルに貼れば実装が始まります。要件定義や技術調査を省いて実装だけにトークンを使えます。
+どのAIツールに対応していますか?
claude-code、cursor、codex-cli に対応しています。
+商用利用できますか?
ライセンスは「商用利用可 (再販不可)」です。
🤝 自分でAIを動かすのは、まだ不安…という方へ
この巻物の内容を、AIを使うプロに丸ごと任せることもできます。姉妹サービスAI代行堂なら「LINEで頼むだけで、仕事が完成」。
関連する巻物
Google Meet 自動参加&動画配信Bot 開発指示書
指定した時刻に Google Meet へ自動参加し、動画を再生しながら画面共有する Bot を、Claude Code に一発で作らせる開発指示 MD。朝会の定例動画配信・ウェビナーの自動放送に。
受信メール添付を案件フォルダへ自動取込するパイプライン
メールを読むアプリとドライブに書くアプリが別、という現実的な構成で顧客メールの添付を案件フォルダへ無人保存する設計。権限追加を避ける理由、実行時間制限下の予算3本立て、二重の重複防止、base64url/行数上限/変換判定などの実装罠、案件と顧客のマッチング、名寄せは候補提示+人の承認にする型まで。
Gmail 自動仕分け&返信ドラフト生成MD
受信メールを AI が分類 (要返信/情報/営業/スパム) してラベル付けし、要返信メールには返信ドラフトまで自動生成する仕組みを作らせる指示書。DWD (ドメイン全体委任) 設定手順込み。
この巻物、誰かのトークンも救えます
𝕏 で節約レシートをシェア