2026年10月8日に、OpenAI Python APIリファレンス、pandasのgroupby、OWASP RAG Security Cheat Sheetを再読しました。数値集計は入力単位・欠損・重複を決めたコードで再現し、RAGは取得できたことを権限や根拠の正しさと同一視せず、アクセス制御・入力検査・取得結果の検証・監査ログを設けます。架空のROIや教材コードを、実運用の成果・安全性へ読み替えません。
AIを組み込む前に、「計算で決まる部分」「文章の候補を作る部分」「人が責任を持って決める部分」を分けると、試す対象が絞れます。この記事では文書作成・売上分析・顧客対応の三つを、実在企業の導入成果ではなく架空データによる教材として扱います。削減率やROIは仮定の計算であり、効果を保証する値ではありません。
30秒で要点
- 売上合計や増減率は検査したコードで計算し、AIへ数値を推測させない。文章は確認前の候補として扱う。
- 顧客対応では、権限のある資料だけを検索対象にし、根拠不足なら回答を保留する。検索できることと送信・返金の権限は別。
- 初期費用100万円、月の純削減額35万円という架空条件なら単純回収期間は約2.86か月。年間ROI320%とは別の量で、実績ではない。
自動化の境界を先に決める
表が収まらない場合は、横にスクロールできます
| 対象 | 自動化する候補 | 人が引き受ける判断 | 保留する条件 |
|---|---|---|---|
| 月次報告 | 数値集計、定型の説明候補 | 変化の原因、公開範囲 | 欠損、異常値、締め前のデータ |
| 売上分析 | 同じ定義の月を集計し比較 | 施策の寄与、次の施策 | 期間や税区分の不一致、基準月ゼロ |
| 顧客対応 | 許可済み規程に基づく回答候補 | 契約、補償、送信 | 権限なし、期限切れ資料、根拠なし |
パターンが明確で入力を検査できる仕事から始めます。金額や契約の決定を伴う操作は、文章の生成ができても別の認可と承認が必要です。
教材1:数値から月次レポートの土台を作る
旧稿の月次報告の役割を、計算と文章生成の境界が分かる形にしました。Python標準ライブラリだけで実行できます。ここで返すのは確定した数値と確認事項であり、「売上増加は施策Aの効果」といった原因の文章は生成しません。
from decimal import Decimal, InvalidOperation
def monthly_report(rows):
if not rows:
raise ValueError("空の報告対象")
sales = []
for row in rows:
try:
value = Decimal(str(row["sales"]))
except (KeyError, InvalidOperation):
raise ValueError("売上の値が不正")
if not value.is_finite() or value < 0:
raise ValueError("売上は有限の0以上")
sales.append(value)
total = sum(sales, Decimal(0))
return {"count": len(sales), "total_yen": str(total),
"mean_yen": str(total / len(sales)),
"review": "対象期間・返品・原因は担当者が照合"}
report = monthly_report([{"sales": "100"}, {"sales": "200"}])
assert report["total_yen"] == "300"
assert report["mean_yen"] == "150"
for bad in ([], [{"sales": "NaN"}], [{"sales": "-1"}], [{}]):
try:
monthly_report(bad)
except ValueError:
pass
else:
raise AssertionError("不正な入力を受理")
print(report)
AIへ文章候補を依頼する場合は、この結果と利用可能な資料だけを渡し、数値の改変や原因の創作を禁止します。ただし、指示だけでは遵守を保証できません。候補に含まれる金額・件数を元の計算結果と再照合し、元資料へない説明は担当者に戻します。
OpenAIのPythonライブラリはResponses APIを中心に案内しています。実接続へ進む場合は使用版を固定し、サーバー側で環境変数から認証情報と利用可能なモデル名を渡し、期限・エラー・課金上限を扱います。本教材にはAPIキー、固定の古いモデル名、実送信を入れていません。ローカル計算が動くことと、実モデルが正しい説明を返すことは別の検証です。
教材2:行順でなく、比較期間を指定する
先頭行と末尾行を引くだけでは、未整列データや複数行の月次売上を誤って比較します。架空の売上行を月へ集約し、比較する二つの月を明示します。返金を負値で管理する業務にはこの入力規約をそのまま使わず、純売上の定義を先に変えてください。
from collections import defaultdict
from decimal import Decimal, InvalidOperation
import re
def compare_months(rows, previous, current):
totals = defaultdict(Decimal)
for row in rows:
month = row.get("month", "")
if not re.fullmatch(r"\d{4}-(0[1-9]|1[0-2])", month):
raise ValueError("月はYYYY-MM")
try:
amount = Decimal(str(row["sales"]))
except (KeyError, InvalidOperation):
raise ValueError("売上が不正")
if not amount.is_finite() or amount < 0:
raise ValueError("売上は有限の0以上")
totals[month] += amount
if previous >= current or previous not in totals or current not in totals:
raise ValueError("比較月がない、または順序が不正")
if totals[previous] == 0:
return {"status": "基準月ゼロのため増減率は未定義"}
rate = (totals[current] - totals[previous]) / totals[previous] * 100
return {"status": "比較可能", "previous": str(totals[previous]),
"current": str(totals[current]), "growth_percent": str(rate)}
rows = [{"month": "2026-02", "sales": "120"},
{"month": "2026-01", "sales": "40"},
{"month": "2026-01", "sales": "60"}]
assert compare_months(rows, "2026-01", "2026-02")["growth_percent"] == "20.0"
assert compare_months(list(reversed(rows)), "2026-01", "2026-02")["growth_percent"] == "20.0"
assert "未定義" in compare_months([
{"month": "2026-01", "sales": "0"},
{"month": "2026-02", "sales": "1"}], "2026-01", "2026-02")["status"]
try:
compare_months([], "2026-01", "2026-02")
except ValueError:
pass
else:
raise AssertionError("空の比較を受理")
print(compare_months(rows, "2026-01", "2026-02"))
同じ考え方をpandasで実装する場合も、groupbyの並び順や欠損キーの扱い、sumのmin_countを確認します。ここではpandasを動かしたという検証にはせず、掲載した標準ライブラリのコードを対象にします。
売上が100円から120円になった教材上の増加率は20%です。しかし、その原因はこの三行から分かりません。価格、数量、返品、季節、対象顧客などを調べる仕事は、増加率の算出とは分離します。
教材3:顧客対応で資料の権限を先に絞る
検索拡張生成(RAG)は、資料を検索して回答の材料へ加える方法です。検索の類似度は閲覧権限でも正答確率でもありません。OWASPのRAGガイドは、検索時の権限確認と、取得した文章を命令として扱わない境界を求めています。
次の例はネットワークもモデルも使わず、認可済みIDと有効な資料の積集合だけから候補を作ります。ID一覧はサーバーが認証済み利用者の現権限から決める前提です。利用者の入力した一覧を信頼する認証機構ではなく、検索や攻撃対策を完成させた実装でもありません。
def support_candidate(query, documents, allowed_ids):
if not isinstance(query, str) or not 1 <= len(query) <= 200:
raise ValueError("質問の長さが不正")
ids = [d["id"] for d in documents]
if len(ids) != len(set(ids)):
raise ValueError("資料IDが重複")
authorized = [d for d in documents
if d["id"] in allowed_ids and d.get("active") is True]
matched = [d for d in authorized if query in d["topics"]]
if not matched:
return {"status": "保留", "source_ids": [], "draft": None}
return {"status": "担当者確認待ち",
"source_ids": [d["id"] for d in matched],
"draft": " / ".join(d["text"] for d in matched)}
docs = [{"id": "D1", "active": True, "topics": ["返品"],
"text": "受付後、担当者が条件を確認します。"},
{"id": "D2", "active": True, "topics": ["返品"],
"text": "別部門専用の情報"},
{"id": "D3", "active": False, "topics": ["返品"],
"text": "期限切れ情報"}]
r = support_candidate("返品", docs, {"D1", "D3"})
assert r["source_ids"] == ["D1"]
assert r["status"] == "担当者確認待ち"
assert support_candidate("返金", docs, {"D1"})["status"] == "保留"
assert support_candidate("返品", docs, set())["draft"] is None
print({"status": r["status"], "source_ids": r["source_ids"]})
本番でモデルへ渡す場合も、根拠の本文と指示を分離し、資料内の命令で権限が増えないようにします。回答候補にない資料IDや、根拠にない金額・約束が入れば保留します。ログには必要なID・認可結果・処理結果を残し、顧客の質問や本文を既定で丸ごと記録しません。根拠がなければモデルの記憶で補って自動送信する、という代替経路は設けません。
教材4:ROIと回収期間を別に計算する
以下は税・割引率・導入期間を無視した単純モデルです。月の「削減額」から運用費を引いた純額が一定で、12か月発生すると仮定します。空いた時間がそのまま現金支出の減少になるとは限りません。再配置できた時間と、実際に減った支払額は別々に測ります。
from fractions import Fraction
def investment(initial_yen, monthly_saving_yen, monthly_running_yen):
if initial_yen <= 0 or min(monthly_saving_yen, monthly_running_yen) < 0:
raise ValueError("費用の前提が不正")
net = monthly_saving_yen - monthly_running_yen
roi = Fraction(net * 12 - initial_yen, initial_yen) * 100
payback = None if net <= 0 else Fraction(initial_yen, net)
return roi, payback
roi, months = investment(1_000_000, 400_000, 50_000)
assert roi == 320 and months == Fraction(20, 7)
assert investment(1_500_000, 700_000, 0)[0] == 460
assert investment(2_000_000, 900_000, 0)[0] == 440
assert investment(1_000_000, 50_000, 50_000) == (-100, None)
assert investment(1_000_000, 40_000, 50_000)[1] is None
print({"annual_roi_percent": float(roi), "simple_payback_months": float(months)})
表が収まらない場合は、横にスクロールできます
| 架空条件 | 計算 | 解釈 |
|---|---|---|
| 初期100万円、月40万円削減、月5万円運用 | 純削減35万円/月 | 人件費等が実際に減るという仮定 |
| 年間の純削減420万円 | (420−100)÷100×100=320% | この定義での初年度ROI |
| 初期100万円の回収 | 100÷35=約2.86か月 | 一定効果が初月から続く単純回収期間 |
| 初期150万円、月純額70万円 | (840−150)÷150×100=460% | 旧例を架空条件として再計算 |
| 初期200万円、月純額90万円 | (1080−200)÷200×100=440% | 同様に実績ではない |
旧稿の「約1年で回収」は最初の仮定とは整合しませんでした。年間ROIと回収期間を混同しないよう訂正しています。純削減額が0以下なら、この単純モデルでは有限の回収期間を出しません。
作業時間を測るときの分母
表が収まらない場合は、横にスクロールできます
| 架空の変化 | 明示した分母 | 削減率 |
|---|---|---|
| 8時間から2時間 | 同じ作業の総人時8時間 | 75% |
| 2営業日から4時間 | 1日8時間、計16人時 | 75%。90%ではない |
| 2週間から1営業日 | 週5営業日、同じ稼働条件 | 90%。暦日や待ち時間と混ぜない |
| 5営業日から2時間 | 1日8時間、計40人時 | 95%。品質一定は別に検証 |
測定前に対象期間、入力条件、確認・修正工数、失敗時の手戻りを定義します。試行では同程度の作業を複数集め、速さだけでなく誤り、保留率、担当者負担も残します。初期費用、API費、監視・保守、教育、復旧を費用から落とさないことが重要です。
小さく試して続行を判断する
公開可能な架空データで上の四つのコードを動かし、空入力・ゼロ・権限なしが成功結果に紛れないことを確かめます。次に実際の運用に近い匿名化済みの試験を、利用許可と担当者の確認手順を決めて行います。検査に通らない場合は自動化範囲を増やさず、入力の定義と担当者へ戻す経路を直します。
AI・LLMの全体像、業務への統合パターン、プロセス最適化では、モデル選択と業務設計を分けて検討できます。ツール一覧は課題整理の補助であり、ここで示した架空ROIを保証するものではありません。