kintoneとChatGPTで見積書の異常値チェック機能を構築しよう

著者名: 村濱 一樹 (External link) (kintoneエバンジェリスト)

目次

はじめに

kintoneの見積管理アプリにChatGPTを組み込んで、ビジネスコンテキストを考慮した賢い異常値チェック機能を作る方法を紹介します。

従来の異常値検出は「値引き率30%以上なら警告」といった機械的なルールが中心でした。
しかし実際のビジネスでは、顧客との関係性や時期、業界の慣習などを総合的に判断する必要があります。

この機能では、ChatGPTがそうした複雑な要因を理解して、以下のように柔軟な判断をします。

  • 顧客関係を考慮: 10年来の優良顧客なら大きめの値引きも戦略的に妥当と判断
  • 時期を考慮: 決算期の在庫処分なら大きめの値引きも理解できると判断
  • 業界知識を活用: IT業界では保守費用で回収するパターンもあると理解
caution
警告

ChatGPT含むAIによる判定は、必ずしも正しいとは限らないので、最終的な判断は人間が行ってください。
ここでのサンプルはあくまで意思決定をサポートするためのものとお考えください。

完成イメージ

今回のカスタマイズを適用することで、kintoneの見積管理アプリにAI異常値チェック機能を追加できます。

見積内容を入力後、「異常値チェック実行」ボタンをクリックすると、ChatGPTがビジネスコンテキストを考慮して分析し、4段階のリスク評価と具体的な改善提案を表示します。

次のような判定結果が表示されます。

  • 🚨 高リスク: 新規顧客への50%値引きなど、財務リスクが高い場合
  • ⚠️ 要確認: 決算期の45%値引きなど、条件付きで妥当な場合
  • 💡 低リスク: 軽微な懸念がある場合
  • ✅ 正常: 問題のない適性な見積の場合

注意事項

  • 今回の例はあくまでサンプルです。 AIの特徴として、必ずしも正しい答えを導き出せるとは限りません。
  • OpenAI APIの利用には料金が発生します。 詳細は、以下のOpenAI公式サイトを参照してください。
    公式サイト (External link)
  • 見積データがOpenAIのサーバーに送信されるため、機密性の高いデータの場合はデータマスキングを検討してください。

アプリの準備

見積管理アプリを次のように作成します。

フィールドの種類 フィールドコード 備考
文字列(1行) 顧客名 見積対象の顧客名です。
ドロップダウン 業界 IT/製造業/小売/サービス/建設から選択します。
数値 従業員数 顧客企業の規模を表します。
ドロップダウン 顧客ランク 新規/既存/優良から選択します。
文字列(1行) 商品・サービス名 見積対象の商品・サービス名です。
数値 単価 商品・サービスの単価(円)です。
数値 数量 見積数量です。
計算 見積金額 単価×数量で自動計算されます。
数値 値引率 値引き率(%)です。
計算 最終金額 値引き後の最終金額で自動計算されます。
文字列(複数行) 備考 追加情報や特記事項を記載します。
数値 過去取引回数 過去の取引実績回数です。
スペース ai_check_space AI異常値チェック結果を表示する領域です。
要素IDに「ai_check_space」を設定してください。

サンプルコード

以下のJavaScriptとCSSファイルを設定画面より指定してください。
指定方法はヘルプの JavaScriptやCSSでアプリをカスタマイズする (External link) を参考してください。

  • JavaScript: kintone-estimate-ai-check.js
  • CSS: kintone-estimate-ai-check.css(見た目のスタイルはCSSに分離しています)

kintone-estimate-ai-check.js

ChatGPTとの連携に必要なAPIキーの取得方法については後述します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
/*
 * kintone 見積書AI異常値検出機能
 * Copyright (c) 2025 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */
(() => {
  'use strict';

  // 設定
  const CONFIG = {
    OPENAI_API_KEY: 'your-api-key-here', // OpenAI APIキーを設定してください

    // フィールドコード
    FIELDS: {
      CUSTOMER_NAME: '顧客名',
      INDUSTRY: '業界',
      EMPLOYEE_COUNT: '従業員数',
      CUSTOMER_RANK: '顧客ランク',
      PRODUCT_NAME: '商品・サービス名',
      UNIT_PRICE: '単価',
      QUANTITY: '数量',
      ESTIMATED_AMOUNT: '見積金額',
      DISCOUNT_RATE: '値引率',
      FINAL_AMOUNT: '最終金額',
      REMARKS: '備考',
      PAST_ORDERS: '過去取引回数',
      AI_CHECK_SPACE: 'ai_check_space'
    }
  };

  // 現在の四半期を取得
  const getCurrentQuarter = () => {
    const month = new Date().getMonth() + 1;
    const quarter = Math.ceil(month / 3);
    return `第${quarter}四半期`;
  };

  // 数値をカンマ区切りでフォーマット
  const formatNumber = (num) => {
    return new Intl.NumberFormat('ja-JP').format(num);
  };

  // ChatGPT APIを使用した異常値検出
  const performAnomalyDetection = async (record) => {
    try {
      // レコードから必要な情報を抽出
      const data = {
        customerName: record[CONFIG.FIELDS.CUSTOMER_NAME]?.value || '',
        industry: record[CONFIG.FIELDS.INDUSTRY]?.value || '',
        employeeCount: record[CONFIG.FIELDS.EMPLOYEE_COUNT]?.value || 0,
        customerRank: record[CONFIG.FIELDS.CUSTOMER_RANK]?.value || '新規',
        productName: record[CONFIG.FIELDS.PRODUCT_NAME]?.value || '',
        unitPrice: parseFloat(record[CONFIG.FIELDS.UNIT_PRICE]?.value || 0),
        quantity: parseFloat(record[CONFIG.FIELDS.QUANTITY]?.value || 0),
        estimatedAmount: parseFloat(record[CONFIG.FIELDS.ESTIMATED_AMOUNT]?.value || 0),
        discountRate: parseFloat(record[CONFIG.FIELDS.DISCOUNT_RATE]?.value || 0),
        finalAmount: parseFloat(record[CONFIG.FIELDS.FINAL_AMOUNT]?.value || 0),
        remarks: record[CONFIG.FIELDS.REMARKS]?.value || '',
        pastOrders: parseInt(record[CONFIG.FIELDS.PAST_ORDERS]?.value || 0, 10)
      };

      // ChatGPTに送信するプロンプトを作成
      const prompt = `見積書の異常値検出を行ってください。実務の観点で、おかしい箇所は指摘し、妥当な範囲は「ok」としてください。

【評価方針】
- 指摘には簡潔な理由を添えること

【基本情報】
- 顧客名: ${data.customerName}
- 業界: ${data.industry}
- 従業員数: ${data.employeeCount}- 顧客ランク: ${data.customerRank}
- 過去取引回数: ${data.pastOrders}
【見積内容】
- 商品・サービス: ${data.productName}
- 単価: ${formatNumber(data.unitPrice)}- 数量: ${data.quantity}
- 見積金額: ${formatNumber(data.estimatedAmount)}- 値引率: ${data.discountRate}%
- 最終金額: ${formatNumber(data.finalAmount)}
【追加情報】
- 時期: ${getCurrentQuarter()}
- 備考: ${data.remarks}

【出力形式(JSONのみ、マークダウン不可)】
{
  "overallRisk": "high|medium|low|normal",
  "checks": [
    {"item": "単価|値引率|数量|総合評価", "status": "error|warning|ok", "message": "理由", "suggestion": "改善提案(あれば)"}
  ],
  "summary": "総合的な判断とアドバイス"
}`;

      // ChatGPT APIに送信
      const response = await fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${CONFIG.OPENAI_API_KEY}`
        },
        body: JSON.stringify({
          model: 'gpt-4o-mini',
          messages: [
            {
              role: 'system',
              content: 'あなたは見積書の異常値検出の専門家です。重要な点:' +
                '1) 一般的な商慣習の範囲内であれば「ok」と判定する、' +
                '2) 複合的にみて、注意するべきときは「warning」、ミスなどの可能性が高い場合は「error」、' +
                '3) 健全な見積書に対しては「normal」のoverallRiskを返す、' +
                '4) 正常な項目には「status": "ok"」を設定する。' +
                '5) 見積書の内容に対して、業界相場や顧客属性、時期を考慮して判断してください。' +
                '過度に厳しい判定は避け、実用的な観点で分析してください。'
            },
            {
              role: 'user',
              content: prompt
            }
          ],
          temperature: 0.3,
          max_tokens: 1000
        })
      });

      if (!response.ok) {
        throw new Error(`API Error: ${response.status}`);
      }

      const responseData = await response.json();
      const content = responseData.choices[0].message.content;

      // JSON解析(マークダウン対応)
      let result;
      try {
        // マークダウンコードブロックを削除してJSONを抽出
        const jsonContent = content
          .replace(/^```(json)?\s*/, '') // 先頭のコードブロック削除
          .replace(/\s*```$/, '') // 末尾のコードブロック削除
          .slice(content.indexOf('{'), content.lastIndexOf('}') + 1); // JSON部分のみ抽出

        result = JSON.parse(jsonContent);
      } catch (e) {
        throw new Error('AI応答の解析に失敗しました');
      }

      return result;

    } catch (error) {
      console.error('AI異常値検出エラー:', error);
      throw error;
    }
  };

  // チェックボタンを表示
  const showCheckButton = () => {
    const spaceElement = kintone.app.record.getSpaceElement(CONFIG.FIELDS.AI_CHECK_SPACE);
    if (!spaceElement) {
      console.warn('スペースフィールドが見つかりません');
      return;
    }

    spaceElement.innerHTML = `
      <div class="ai-check-container">
        <div class="ai-check-header">
          <span class="ai-check-icon">🤖</span>
          <span class="ai-check-title">AI異常値検出</span>
        </div>
        <div class="ai-check-description">
          ChatGPTが見積内容の妥当性を分析します
          <small>業界相場・顧客属性・時期を考慮した総合判断</small>
        </div>
        <button id="perform-ai-check" class="ai-check-button">
          <span style="margin-right: 8px;">✨</span>
          異常値チェック実行
        </button>
      </div>`;

    const btn = document.getElementById('perform-ai-check');
    if (btn) btn.addEventListener('click', executeAnomalyCheck);
  };

  // ローディング表示
  const showLoading = () => {
    const spaceElement = kintone.app.record.getSpaceElement(CONFIG.FIELDS.AI_CHECK_SPACE);
    if (!spaceElement) return;

    spaceElement.innerHTML = `
      <div class="ai-loading-container">
        <div class="ai-loading-spinner"></div>
        <div class="ai-loading-text">
          <strong>ChatGPTで分析中...</strong>
          <small>業界相場・顧客属性・ビジネスコンテキストを考慮しています</small>
        </div>
      </div>`;
  };

  // 結果表示
  const showResults = (result) => {
    const spaceElement = kintone.app.record.getSpaceElement(CONFIG.FIELDS.AI_CHECK_SPACE);
    if (!spaceElement) return;

    // リスクレベルに応じたアイコン
    const riskStyles = {
      high: {icon: '🚨', label: '高リスク'},
      medium: {icon: '⚠️', label: '要確認'},
      low: {icon: '💡', label: '低リスク'},
      normal: {icon: '✅', label: '正常'}
    };

    const statusStyles = {
      error: {icon: '❌'},
      warning: {icon: '⚠️'},
      ok: {icon: '✅'}
    };

    const riskStyle = riskStyles[result.overallRisk] || riskStyles.normal;
    const riskClassMap = {high: 'risk-high', medium: 'risk-medium', low: 'risk-low', normal: 'risk-normal'};
    const riskClass = riskClassMap[result.overallRisk] || riskClassMap.normal;

    const checksHtml = result.checks.map(check => {
      const statusStyle = statusStyles[check.status] || statusStyles.ok;
      const statusClass = `status-${check.status || 'ok'}`;
      return `
        <div class="ai-check-item ${statusClass}">
          <div class="ai-check-item-header">
            <span class="ai-check-item-icon">${statusStyle.icon}</span>
            <strong class="ai-check-item-title">${check.item}</strong>
          </div>
          <div class="ai-check-item-message">${check.message}</div>
          ${check.suggestion ? `<div class="ai-check-item-suggestion">💡 ${check.suggestion}</div>` : ''}
        </div>`;
    }).join('');

    const html = `
      <div class="ai-result-container ${riskClass}">
        <div class="ai-result-header">
          <div class="ai-result-status">
            <span class="ai-result-icon">${riskStyle.icon}</span>
            <span class="ai-result-label">総合評価: ${riskStyle.label}</span>
          </div>
          <button id="recheck-button" class="ai-result-reload-button">再チェック</button>
        </div>
        <div class="ai-result-content">
          ${checksHtml}
          <div class="ai-summary-container">
            <div class="ai-summary-header"><span>📊</span>総合判断</div>
            <div class="ai-summary-content">${result.summary}</div>
          </div>
        </div>
      </div>`;

    spaceElement.innerHTML = html;
    const re = document.getElementById('recheck-button');
    if (re) re.addEventListener('click', showCheckButton);
  };

  // 異常値チェック実行
  const executeAnomalyCheck = async () => {
    try {
      const record = kintone.app.record.get().record;

      // 必須項目チェック
      if (!record[CONFIG.FIELDS.UNIT_PRICE]?.value || !record[CONFIG.FIELDS.QUANTITY]?.value) {
        alert('単価と数量を入力してください');
        return;
      }

      showLoading();
      const result = await performAnomalyDetection(record);
      showResults(result);

    } catch (error) {
      console.error('異常値チェックエラー:', error);
      alert('エラーが発生しました: ' + error.message);
    }
  };

  // イベント登録
  kintone.events.on([
    'app.record.create.show',
    'app.record.edit.show',
    'app.record.detail.show'
  ], (event) => {
    console.log('💰 見積書AI異常値検出機能が有効です');
    showCheckButton();
    return event;
  });

})();

kintone-estimate-ai-check.css

スタイルシートにより、AIチェック結果を見やすくデザインします。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
/*
 * kintone 見積書AI異常値検出機能 - スタイルシート
 * Copyright (c) 2025 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

/* ==========================================
   AI異常値検出ボタンエリア
   ========================================== */

.ai-check-container {
  padding: 20px;
  background: #f8f9fa;
  border: 1px solid #dee2e6;
  border-radius: 8px;
  text-align: center;
}

.ai-check-header {
  margin-bottom: 15px;
}

.ai-check-icon {
  font-size: 24px;
  margin-right: 10px;
}

.ai-check-title {
  font-size: 16px;
  color: #495057;
  font-weight: bold;
}

.ai-check-description {
  margin-bottom: 15px;
  color: #6c757d;
  font-size: 14px;
}

.ai-check-description small {
  display: block;
  margin-top: 4px;
}

.ai-check-button {
  background: #28a745;
  color: white;
  border: none;
  padding: 12px 24px;
  border-radius: 6px;
  cursor: pointer;
  font-size: 15px;
  font-weight: bold;
  transition: all 0.2s;
}

.ai-check-button:hover {
  background: #218838;
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* ==========================================
   ローディング表示
   ========================================== */

.ai-loading-container {
  text-align: center;
  padding: 30px;
  background: #f8f9fa;
  border: 1px solid #dee2e6;
  border-radius: 8px;
}

.ai-loading-spinner {
  display: inline-block;
  width: 40px;
  height: 40px;
  border: 4px solid #f0f0f0;
  border-top-color: #28a745;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

.ai-loading-text {
  margin-top: 15px;
  color: #666;
  font-size: 14px;
}

.ai-loading-text strong {
  display: block;
  margin-bottom: 4px;
}

@keyframes spin {
  to { 
    transform: rotate(360deg); 
  }
}

/* ==========================================
   結果表示エリア
   ========================================== */

.ai-result-container {
  background: white;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.ai-result-header {
  padding: 15px 20px;
  border-bottom: 1px solid rgba(0,0,0,0.1);
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.ai-result-status {
  display: flex;
  align-items: center;
  gap: 10px;
}

.ai-result-icon {
  font-size: 24px;
}

.ai-result-label {
  font-size: 16px;
  font-weight: bold;
}

.ai-result-reload-button {
  background: #6c757d;
  color: white;
  border: none;
  padding: 6px 12px;
  border-radius: 4px;
  cursor: pointer;
  font-size: 12px;
  transition: background 0.2s;
}

.ai-result-reload-button:hover {
  background: #545b62;
}

.ai-result-content {
  padding: 20px;
}

/* ==========================================
   チェック項目
   ========================================== */

.ai-check-item {
  margin-bottom: 15px;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 4px;
  border-left: 4px solid;
}

.ai-check-item-header {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.ai-check-item-icon {
  font-size: 16px;
}

.ai-check-item-title {
  font-weight: bold;
  color: #333;
}

.ai-check-item-message {
  color: #666;
  font-size: 14px;
  margin-left: 24px;
  line-height: 1.4;
}

.ai-check-item-suggestion {
  margin-top: 8px;
  margin-left: 24px;
  padding: 8px 12px;
  background: #e7f3ff;
  border-radius: 4px;
  font-size: 13px;
  color: #004085;
  line-height: 1.4;
}

/* ==========================================
   総合判断エリア
   ========================================== */

.ai-summary-container {
  margin-top: 20px;
  padding: 15px;
  background: #f8f9fa;
  border-radius: 6px;
  border: 1px solid #dee2e6;
}

.ai-summary-header {
  font-weight: bold;
  color: #333;
  margin-bottom: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}

.ai-summary-content {
  color: #666;
  font-size: 14px;
  line-height: 1.6;
}

/* ==========================================
   リスクレベル別カラー
   ========================================== */

/* 高リスク */
.risk-high {
  border-color: #dc3545;
}

.risk-high .ai-result-header {
  background: rgba(220, 53, 69, 0.1);
  border-bottom-color: rgba(220, 53, 69, 0.25);
}

.risk-high .ai-result-label {
  color: #dc3545;
}

.ai-check-item.status-error {
  border-left-color: #dc3545;
}

/* 要確認 */
.risk-medium {
  border-color: #ffc107;
}

.risk-medium .ai-result-header {
  background: rgba(255, 193, 7, 0.1);
  border-bottom-color: rgba(255, 193, 7, 0.25);
}

.risk-medium .ai-result-label {
  color: #ffc107;
}

.ai-check-item.status-warning {
  border-left-color: #ffc107;
}

/* 低リスク */
.risk-low {
  border-color: #17a2b8;
}

.risk-low .ai-result-header {
  background: rgba(23, 162, 184, 0.1);
  border-bottom-color: rgba(23, 162, 184, 0.25);
}

.risk-low .ai-result-label {
  color: #17a2b8;
}

/* 正常 */
.risk-normal {
  border-color: #28a745;
}

.risk-normal .ai-result-header {
  background: rgba(40, 167, 69, 0.1);
  border-bottom-color: rgba(40, 167, 69, 0.25);
}

.risk-normal .ai-result-label {
  color: #28a745;
}

.ai-check-item.status-ok {
  border-left-color: #28a745;
}

/* ==========================================
   レスポンシブ対応
   ========================================== */

@media (max-width: 768px) {
  .ai-check-container {
    padding: 15px;
  }
  
  .ai-result-header {
    flex-direction: column;
    gap: 10px;
    align-items: flex-start;
  }
  
  .ai-result-content {
    padding: 15px;
  }
  
  .ai-check-item {
    padding: 12px;
  }
  
  .ai-check-item-header {
    flex-wrap: wrap;
  }
}

サンプルコードの解説

以下、コードを具体的に解説します。

ChatGPTを利用するための準備

カスタマイズでChatGPTを利用するために、まずはChatGPTのAPIキーを取得する必要があります。
OpenAI (External link) にアクセスしてアカウントを作成し、そちらでAPIキーを生成できます。
この記事では取得方法の詳細の説明を割愛します。

ChatGPT APIのエンドポイントURLと上記で取得したAPIキーを次のように定義します。

1
2
3
4
const CONFIG = {
  OPENAI_API_KEY: 'your-api-key-here', // ここにAPIキーを設定
  // ...
};

APIの利用には所定の料金が発生しますのでご注意ください。
詳細は、以下のOpenAI公式サイトを参照してください。
公式サイト (External link)

caution
警告

APIキーは秘密情報なので、絶対に公開しないでください。
APIキーの情報が漏洩するとAPIを自由に実行できてしまうため、kintoneのJavaScriptの開発においても慎重に扱ってください。

異常値チェック機能ロジック

この異常値チェック機能処理では、ChatGPTにビジネスコンテキストを含む詳細な情報を送信し、構造化された判定結果を受け取ります。

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// ChatGPTに送信するプロンプトを作成
const prompt = `見積書の異常値検出を行ってください。実務の観点で、おかしい箇所は指摘し、妥当な範囲は「ok」としてください。

【評価方針】
- 指摘には簡潔な理由を添えること

【基本情報】
- 顧客名: ${data.customerName}
- 業界: ${data.industry}
- 従業員数: ${data.employeeCount}- 顧客ランク: ${data.customerRank}
- 過去取引回数: ${data.pastOrders}
【見積内容】
- 商品・サービス: ${data.productName}
- 単価: ${formatNumber(data.unitPrice)}- 数量: ${data.quantity}
- 見積金額: ${formatNumber(data.estimatedAmount)}- 値引率: ${data.discountRate}%
- 最終金額: ${formatNumber(data.finalAmount)}
【追加情報】
- 時期: ${getCurrentQuarter()}
- 備考: ${data.remarks}

【出力形式(JSONのみ、マークダウン不可)】
{
  "overallRisk": "high|medium|low|normal",
  "checks": [
    {"item": "単価|値引率|数量|総合評価", "status": "error|warning|ok", "message": "理由", "suggestion": "改善提案(あれば)"}
  ],
  "summary": "総合的な判断とアドバイス"
}`;

重要なポイントは、単純な数値チェックではなく、ビジネスコンテキストを含めることです。

  • 顧客属性: 新規/既存/優良の区別、過去取引回数
  • 業界特性: IT業界と製造業では常識が相違
  • 時期情報: 決算期などの季節要因
  • 特記事項: 備考欄の追加情報

必要に応じてプロンプトを修正することで、判断を厳しくしたり、やさしくしたりできます。

JSON解析とエラーハンドリング

ChatGPTの応答は必ずしも純粋なJSONではなく、マークダウンのコードブロックで囲まれている場合があります。
そのため、以下のようにコードブロックにも対応した解析処理を実装しています。

135
136
137
138
139
140
141
142
143
144
145
146
147
// JSON解析(マークダウン対応)
let result;
try {
  // マークダウンコードブロックを削除してJSONを抽出
  const jsonContent = content
    .replace(/^```(json)?\s*/, '') // 先頭のコードブロック削除
    .replace(/\s*```$/, '') // 末尾のコードブロック削除
    .slice(content.indexOf('{'), content.lastIndexOf('}') + 1); // JSON部分のみ抽出

  result = JSON.parse(jsonContent);
} catch (e) {
  throw new Error('AI応答の解析に失敗しました');
}

この処理により、ChatGPTがコードブロックなどを含んだ応答でも、テキストに埋め込まれたJSONでも適切に解析できます。

リスクに応じたアイコン

検出結果はリスクレベルに応じたアイコンで表示され、直感的に判断できるようになっています。

205
206
207
208
209
210
211
// リスクレベルに応じたアイコン
const riskStyles = {
  high: {icon: '🚨', label: '高リスク'},
  medium: {icon: '⚠️', label: '要確認'},
  low: {icon: '💡', label: '低リスク'},
  normal: {icon: '✅', label: '正常'}
};

各チェック項目には具体的な改善提案も含まれるため、何が問題でどうすればよさそうかがわかります。

実用的な活用例

ケース1: 新規顧客への高額すぎる値引き

入力内容

  • 顧客: 新規顧客(従業員数200人)
  • 商品: ITシステム導入
  • 値引率: 45%

AI判定結果
高リスクと判断されます。

ケース2: 優良顧客への高額値引き

入力内容

  • 顧客: 優良顧客(取引回数15回)
  • 商品: 保守契約更新
  • 値引率: 20%
  • 備考: 複数年契約

AI判定結果
低リスクと判断されます。

おわりに

本記事では、kintoneとChatGPTを融合させた見積書の異常値チェック機能の手法について紹介しました。
kintoneとChatGPTを駆使し従来の単純なルールベースでは実現できない、ビジネスコンテキストを考慮した総合的な判断を可能にします。
見積管理だけでなく、契約書レビューや提案書の作成支援など、多岐にわたるシーンで利用できうると思います。
この記事がAIを使った業務効率化の一例としてご参考になると幸いです。

information

このTipsは、2025年08月版kintoneで動作を確認しています。