2026-10-20確認。RAGの回答に引用が付いていても、古い規約や相互に矛盾する資料を根拠にしていれば、そのまま回答候補を採用できません。この記事は、回答を提示するか、人へ戻すか、回答を控えるかを決めるエンジニア・テックリード向けの設計例です。
結論:引用の存在と、回答を出せる条件を分ける
低リスクの定型的な事実照会なら、根拠と主張の対応、有効期間、取得できた資料同士の矛盾を確認するゲートを追加する価値があります。ただし、以下の検証はAI支援(Codex)で準備・実行した、架空の構造化データによるローカル検証です。自然言語の意味判定や実LLMの品質を証明するものではありません。
| 条件 | この設計での判断 | 次の処理 |
|---|---|---|
| 必要な主張が有効な根拠と一致し、取得範囲で矛盾なし |
candidate:回答候補 |
質問との対応・権限・業務リスクを別途確認 |
| 期限外の根拠、現行資料の矛盾、日付やIDの不備 |
review:人間確認 |
内容の断定を保留し、資料管理者へ確認 |
| 根拠なし、未知の引用ID、支持されない主張、主張なし |
abstain:回答を控える |
「確認できない範囲」を返し、追加検索や質問へ |
candidate は自動送信の許可ではありません。 承認・送信権限は別の境界に置きます。高リスクな判断や、適用条件を構造化できない文書では、人による確認を維持します。
前提:正しい引用と、正しい回答は同じではない
MicrosoftのRAG評価資料は、根拠との整合性であるgroundednessと、事実としてのcorrectnessを別の指標として扱っています。元資料が誤っていたり解釈を誤ったりすれば、根拠があるだけでは正しさを保証できません。Microsoft Learn:RAGのエンドツーエンド評価
Googleのgrounding検証も、回答全体のsupport scoreに加え、主張とそれを支持する引用の対応を返す仕様です。scoreは与えた事実にどの程度支持されるかの指標です。Google Cloud:Check grounding with RAG
ここからは独自の設計案です。スコアの高低だけに頼らず、日付・引用ID・矛盾の条件を先に分けます。資料の信頼性、閲覧権限、適用対象の正規化は上流の責任とします。LLMに日付や根拠本文を自由に作らせず、文書管理側のメタデータを使う想定です。
選択肢:ゲートの追加にも前提がある
| 選択肢 | 適する条件 | 採用を見送る条件・負担 |
|---|---|---|
| 現行FAQ・人手回答を維持 | 問いが少ない、誤回答の影響が大きい | 件数増加時の待ち時間は別途測定が必要 |
| 引用IDの存在だけを検査 | 引用の配線ミスを見つける補助チェック | 内容・鮮度・矛盾の採用判定には不足 |
| 既製のgrounding評価を併用 | 自然言語の主張と根拠を照合したい | 自分の評価セットで誤通過・過剰保留を調べる必要 |
| 構造化された事実への決定的ゲート | 対象・項目・値・有効期間を正規化できる | 構造化できない例外や自由記述を無理に通さない |
今回のコードは最後の選択肢を小さく検証します。既製サービスのAPI呼び出し・比較試験はしていません。
検証:同じ13ケースで引用チェックとゲートを比較する
架空の単一製品・単一プランについて「試用日数は何日か」を考えます。文書の事実と回答の主張は人が正規化したものとして固定し、LLMによる抽出は省いています。trial_days=14 は実在サービスの仕様ではありません。
- 実行環境:Python 3.14.3、標準ライブラリのみ。
- 判定基準日:
2026-10-20。有効開始日・終了日を両端含みで比較。 - 入力:根拠は
(ID, 項目, 整数値, 有効開始日, 有効終了日)、主張は(項目, 整数値, 引用ID)。 - 比較方式:引用IDが取得結果に存在するだけの方式と、期限・矛盾・主張を照合する方式。
- 検証対象:正常、空入力、未知ID、期限切れ、将来施行、矛盾、値の誤り、無関係な引用、余分な主張、メタデータ不備。
次のコード全体を gate_demo.py に保存し、python3 gate_demo.py で実行できます。ネットワーク・外部書込みは行いません。
from datetime import date
from collections import Counter
import platform
AS_OF = date.fromisoformat('2026-10-20')
# fact: (id, key, integer_value, valid_from, valid_until)
# claim: (key, integer_value, cited_id)
# 同一製品・同一プランに正規化済みの固定fixtureだけを対象とする。
CURRENT = ('d1', 'trial_days', 14, '2026-10-01', '2026-10-31')
OLD = ('d0', 'trial_days', 14, '2026-09-01', '2026-09-30')
FUTURE = ('d2', 'trial_days', 30, '2026-11-01', '2026-11-30')
OTHER = ('d3', 'support_hours', 8, '2026-10-01', '2026-10-31')
CONFLICT = ('d4', 'trial_days', 30, '2026-10-01', '2026-10-31')
BAD_DATE = ('d5', 'trial_days', 14, 'invalid', '2026-10-31')
CLAIMS = [('trial_days', 14, 'd1')]
def gate(claims, facts):
if not claims:
return 'abstain', 'no_claims'
if not facts:
return 'abstain', 'no_evidence'
by_id = {f[0]: f for f in facts}
if len(by_id) != len(facts):
return 'review', 'invalid_metadata'
try:
periods = {f[0]: (date.fromisoformat(f[3]),
date.fromisoformat(f[4])) for f in facts}
except ValueError:
return 'review', 'invalid_metadata'
if any(start > end for start, end in periods.values()):
return 'review', 'invalid_metadata'
if any(cid not in by_id for _, _, cid in claims):
return 'abstain', 'unknown_citation'
if any(not (periods[cid][0] <= AS_OF <= periods[cid][1])
for _, _, cid in claims):
return 'review', 'noncurrent_citation'
current = [f for f in facts if periods[f[0]][0] <= AS_OF <= periods[f[0]][1]]
# 引用された1件だけでなく、取得済みの同じ項目の根拠を比較する。
if any(len({f[2] for f in current if f[1] == key}) > 1
for key, _, _ in claims):
return 'review', 'conflicting_sources'
if any((by_id[cid][1], by_id[cid][2]) != (key, value)
for key, value, cid in claims):
return 'abstain', 'unsupported_claim'
return 'candidate', 'supported'
def citation_only(claims, facts):
ids = {f[0] for f in facts}
return bool(claims) and all(cid in ids for _, _, cid in claims)
CASES = [
('valid', CLAIMS, [CURRENT], ('candidate', 'supported')),
('no_evidence', CLAIMS, [], ('abstain', 'no_evidence')),
('unknown_id', [('trial_days', 14, 'missing')], [CURRENT],
('abstain', 'unknown_citation')),
('expired', [('trial_days', 14, 'd0')], [OLD],
('review', 'noncurrent_citation')),
('future', [('trial_days', 30, 'd2')], [FUTURE],
('review', 'noncurrent_citation')),
('conflict', CLAIMS, [CURRENT, CONFLICT], ('review', 'conflicting_sources')),
('wrong_value', [('trial_days', 30, 'd1')], [CURRENT],
('abstain', 'unsupported_claim')),
('unrelated', [('trial_days', 14, 'd3')], [OTHER],
('abstain', 'unsupported_claim')),
('expired_plus_other', [('trial_days', 14, 'd0')], [OLD, OTHER],
('review', 'noncurrent_citation')),
('invalid_date', [('trial_days', 14, 'd5')], [BAD_DATE],
('review', 'invalid_metadata')),
('extra_claim', CLAIMS + [('support_hours', 24, 'd1')], [CURRENT],
('abstain', 'unsupported_claim')),
('empty_claims', [], [CURRENT], ('abstain', 'no_claims')),
('duplicate_id', CLAIMS, [CURRENT, CURRENT], ('review', 'invalid_metadata')),
]
counts = Counter()
passed_baseline = 0
print('Python', platform.python_version())
for name, claims, facts, expected in CASES:
actual = gate(claims, facts)
assert actual == expected, (name, expected, actual)
baseline = citation_only(claims, facts)
passed_baseline += baseline
counts[actual[0]] += 1
print(name, int(baseline), *actual)
print('citation_only_pass', passed_baseline, '/', len(CASES))
print('gate', dict(counts))
print('assertions_passed', len(CASES))
実行結果
| ケース | 引用IDだけなら通過 | ゲートの結果 / 理由 |
|---|---|---|
| 正常な現行根拠 | 1 | candidate / supported |
| 根拠なし | 0 | abstain / no_evidence |
| 未知ID | 0 | abstain / unknown_citation |
| 期限切れ | 1 | review / noncurrent_citation |
| 将来施行 | 1 | review / noncurrent_citation |
| 現行資料同士の矛盾 | 1 | review / conflicting_sources |
| 引用先と値が違う | 1 | abstain / unsupported_claim |
| 引用先の項目が違う | 1 | abstain / unsupported_claim |
| 古い引用+無関係な現行資料 | 1 | review / noncurrent_citation |
| 日付不正 | 1 | review / invalid_metadata |
| 根拠にない主張を追加 | 1 | abstain / unsupported_claim |
| 主張が空 | 0 | abstain / no_claims |
| 根拠ID重複 | 1 | review / invalid_metadata |
13ケースの期待結果とのassert照合は13件成功しました。引用方式は10/13件が通過、ゲートは回答候補1件・回答を控える6件・人間確認6件です。このfixtureで、引用方式が通す10件のうち9件を保留できました。
これは境界条件のテスト結果です。ケースは失敗パターンを説明するために作っており、実際の質問分布を代表しません。回答正解率、改善率、業務の削減率として使えません。
なぜこの判定順にしたか
先にメタデータを検査し、次に引用IDと引用した根拠の有効期間を確認します。その後、取得済みの同じ項目の現行根拠を比較し、最後に主張との対応を照合します。複数の問題がある場合は先に検出した理由を返します。
「古い引用+無関係な現行資料」を用意したのは、検索結果に新しい資料が1件あるだけで、古い引用の回答を通さないためです。また矛盾確認は引用された1件だけに限定していません。conflict では、回答候補が引用していない別資料の異なる値も確認します。
単に文書の更新日から30日たったら古い、とは判定していません。長く有効な文書もあるため、ここでは明示的な適用期間を使いました。適用日や失効日が不明な実文書をどう扱うかは、資料管理者と別に決める必要があります。
このコードで分からないことと、本番への接続
構造化された値の一致は、自然言語の含意判定ではありません。 この関数に自由記述や外部JSONを直接渡す想定ではなく、型・形式が固定されたfixture専用です。実入力にはスキーマ検証・サイズ制限・不正データの退避が必要です。
さらに、この検証には質問文がありません。根拠と一致した主張でも、質問への答えになっているか、必要な条件や例外がすべて揃うかは判定できません。未取得の文書との矛盾、権限、プロンプトインジェクション、モデルの採点品質、自然言語への再生成も未検証です。
本番へ広げる場合は、次の責任を分ける設計が考えられます。以下は未実装の運用案です。
| 担当 | 確認・保存するもの | 保留後の対応 |
|---|---|---|
| 資料管理 | 正本、版、有効期間、適用対象、権限 | 矛盾や期限不明を整理し再取込 |
| 検索・評価の実装担当 | 検索条件、主張と根拠の対応、評価版、判定理由 | 根拠不足は回数制限付き追加検索、解消しなければ保留 |
| 業務レビュー担当 | 根拠と必要な例外条件、候補回答 | 修正して再評価。高リスク回答は承認を維持 |
| 運用担当 | 問い合わせ相関ID、資料版、状態、レビュー滞留 | 取得・評価の失敗を品質合格と扱わず停止 |
ログには根拠全文や機密入力を無条件に保存せず、アクセス制限した参照と判定理由を残します。再取込や人の修正後は根拠の版を更新して再評価し、古い判定を流用しません。送信済みか不明な場合は結果を照合してから再試行する必要があります。今回のコードには送信・再試行・状態永続化はありません。
費用は生成APIだけでなく、検索、評価呼び出し、追加検索、人の確認、資料保守の各件数と単価・工数を分けて見積もります。今回は有料APIを呼ばず、製品価格、処理時間、レビュー工数、ROIは測っていません。保留を増やすと人の負担が増えるため、回答率だけで採否を決めない設計が必要です。
採用・見送り・追加検証をどう決めるか
この段階で推奨するのは、既存の人手確認を残したままゲートを並行評価することです。誤通過と過剰保留を理由別に確認し、構造化できる範囲から適用します。資料の版や適用対象を揃えられないなら、先にFAQ・文書管理を整えます。
次に、利用許可を確認した代表質問と、根拠なし・矛盾・期限不明を含む評価セットで、次を測ります。
- 人が保留すべきと判定した回答を通した件数 / 保留すべき総数。
- 人が回答可能と判定した回答を保留した件数 / 回答可能な総数。
- 回答候補のうち、質問の必要条件を満たす件数と、レビューに要した時間。
合否基準は誤回答の影響と確認体制から事前に定めます。今回の13件が通ったことを、本番の自動回答を開始する条件にはしません。
参考資料
以下の公開一次資料を2026-10-20に確認しました。資料にある評価の考え方と、この記事の独自ゲート・架空検証は別のものです。
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表。AI × Web × 業務自動化の開発・検証に取り組んでいます。
技術選定・品質・運用の判断材料を、検証範囲と制約を示して発信しています。
AI導入や業務自動化の技術選定・設計整理については、対応できる範囲を確認のうえご相談を承ります。
