2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLM が書いた返信を人の承認なしで送る前に「事実台帳に無い主張」を列挙させて止める(fail-closed の関門)

2
Last updated at Posted at 2026-10-24

SNS で質問している人への返信の下書きを LLM に作らせ、人が1本ずつ承認してから送る運用をしていました。ところが、承認待ちの間に候補が古くなって、見送りの半分近くが「間に合わなかった」ものでした。

そこで承認をやめて自動で送ることにしました。ただ、人の目の代わりになる検査が要ります。いちばん怖いのは、それらしいけれど確かめていない事実を言い切ることです。この記事は、LLM に「台帳に無い主張」を列挙させて、1つでもあれば送らない関門の作り方です。

問題:既存の検査は「知っている誤り」しか止められない

検査はすでに3つありました。

  • 文面の検査:長さ・宣伝の語・URL・決まり文句など
  • 言い切りの検査:過去に実際に出した誤り(例:条件付きのものを「確実」と言う)を正規表現で止める
  • 型の検査:相手の質問に具体的に答えているか

それでも、承認制の間に人が止めていた下書きには、どれにも当たらないものがありました。

下書きの主張(要旨) 人が止めた理由
大手の解約金と転出手数料の金額を言い切る 事実台帳に書いていない(確かめていない)
別の会社では、ある通話の無料枠が有料のオプションになる 他社の仕様で、台帳に無い
ある手続きに対応しているので、番号の発行なしで進められる 手続きの対応状況が台帳に無い

どれも正しいかもしれません。ただ、確かめていないことを言い切っていたのが問題でした。正規表現で書けるのは、一度踏んだ誤りだけです。

手順1:事実台帳を「出典付きの Markdown」で持つ

料金・条件・手続きなど、返信で言ってよい事実を1つのファイルにまとめます。行ごとに出典と確認日を書きます。

| 項目 | 内容 | 出典 |
|---|---|---|
| 12/1 からの料金 | 40GB まで 3,135円 / 60GB まで 4,125円 / 120GB まで 5,115円 | 公式ページ(2026-10-09 確認) |

手順2:LLM に「台帳に無い主張」を列挙させる

判定は「正しいか」ではなく、**「台帳から言えるか」**にします。モデルの知識で正誤を判断させると、確かめていない知識で通してしまうからです。

FACT_RUBRIC = """あなたは事実確認の係です。下の「事実台帳」だけを根拠に、「返信文」の中の事実の言い切り
(料金・容量・手数料・期間・条件・手続き・サービスの仕様・他社の料金など、確かめられる主張)を1つずつ調べてください。
- 台帳に書いてある、または台帳から明らかに言える → supported
- 台帳に無い・台帳と食い違う → unsupported
- 「公式の地図で確かめる」のような一般的な手順の勧めや、相手の発言の言い換え・感想は主張に数えない
出力は JSON 1行だけ: {"unsupported": ["台帳に無い主張", ...]}(無ければ空配列)

=== 事実台帳 ===
"""

def facts_check(text: str) -> list[str] | None:
    """台帳に無い主張の一覧。判定できなければ None(呼び出し側は送らない)。"""
    prompt = FACT_RUBRIC + FACTS.read_text(encoding="utf-8") + "\n\n=== 返信文 ===\n" + text
    try:
        p = subprocess.run("claude -p --disallowedTools Edit,Write,Bash", input=prompt,
                           capture_output=True, text=True, encoding="utf-8", timeout=600, shell=True)
        m = re.search(r"\{.*\}", p.stdout or "", re.S)
        return list(json.loads(m.group(0)).get("unsupported", [])) if m else None
    except Exception:
        return None

ここでは Claude Code の CLI(claude -p)を、編集系のツールを禁止して呼んでいます。台帳はおよそ27KBで、そのままプロンプトに入れています。

手順3:関門は fail-closed でつなぐ

LLM の判定は、上限や停止で返ってこないことがあります。返ってこなかったときに「問題なし」と読むと、関門が無いのと同じです。

def gates(text: str) -> str | None:
    """落ちた理由(通れば None)。どれか1つでも落ちたら送らない。"""
    if re.search(r"https?://", text):
        return "URL が入っている"
    why = reject_reason(text)                 # 文面の検査
    if why:
        return f"reject_reason: {why}"
    bad = check_texts({"t": text})            # 過去の誤りの正規表現
    if bad:
        return f"claim_facts_gate: {bad[0]}"
    un = facts_check(text)                    # 台帳との照合
    if un is None:
        return "事実の照合ができない(LLM 停止・上限の可能性)"   # ← 判定不能は「送らない」
    if un:
        return f"事実台帳に無い主張: {' / '.join(un)}"
    return None

安い検査から順に並べ、LLM は最後に1回だけ呼びます。

確かめ方:通すべき例と止めるべき例を両方流す

関門は、止めすぎてもエラーにならず、送る数が減るだけで終わります。弾きすぎに気づけないので、通すべき例も必ず入れます。承認制の時代に人が「送った」もの・「止めた」ものが、そのまま正解の例になりました。

tests = {
    "通すべき(人が承認して送った)": "安さで決めるなら、先に自宅と職場が公式のエリアマップで…",
    "通すべき(台帳の数字だけ)":   "11/1時点で契約中なら、12/1から40GBの上限が自動で入って…",
    "止めるべき(台帳に無い)":     "大手なら今は解約金も転出手数料も…なので…",  # 金額の言い切り
    "止めるべき(他社の仕様)":     "…の通話無料は…だと別料金のオプションになる点だけ注意かな…",
    "止めるべき(手続きの対応)":   "…に対応してるので、…すれば番号の発行なしで進められますね…",
}
for k, t in tests.items():
    print(k, "→", gates(t) or "通過")

結果は5件とも期待どおりでした。止めた3件には、台帳に無い主張が具体的に列挙されました(例:解約金と転出手数料の金額の言い切りが2つ)。

通すべき(人が承認して送った) → 通過
通すべき(台帳の数字だけ)   → 通過
止めるべき(台帳に無い)     → 事実台帳に無い主張: (解約金の金額の言い切り) / (転出手数料の金額の言い切り) / …
止めるべき(他社の仕様)     → 事実台帳に無い主張: …では5分以内の通話無料が別料金のオプションになる(台帳に記載なし)
止めるべき(手続きの対応)   → 事実台帳に無い主張: …に対応している / …

まとめ

  • 「正しいか」ではなく「台帳から言えるか」で判定させる。台帳に無ければ、正しくても止める
  • LLM の判定が返らないときは送らない(fail-closed)
  • 安い検査を先に、LLM は最後に1回
  • 承認制で人が「送った・止めた」記録は、そのまま評価セットになる。通すべき例も必ず入れる
2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?