0
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の出力整形を再依頼しないJSON検収Python

0
Posted at

実は、LLMの出力を整えるためにもう一度LLMを呼ぶのは、社内業務だと割に合わないことが多いです。

商談メモから「案件名・金額・確度」を拾わせ、スプレッドシートへ流す。ここで欲しいのは気の利いた言い換えではなく、3列が揃ったJSONです。前置きの「承知しました」やMarkdownのコードフェンスが混じっただけで、次の処理が止まります。

今朝、Claudeの出力に残る余計なトークンを別のLLMで片付けるツールを見かけました。文章を読みやすくする用途ならありです。ただ、列に流すデータまで再生成に任せると、金額や確度が書き換わった時に追えません。僕なら、整形はPython、内容の判断だけをLLMに任せます。

Q. プロンプトで「JSONだけ返して」と書けば足りない?

足りません。モデル側の設定や会話の流れで、```json の囲みや一言の説明は普通に混じります。出力をそのまま json.loads() に渡すと、たとえば次のエラーになります。

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

手元で同じ形のサンプルを流すと、前置きが付いただけでCSV化の処理は空振りします。モデルの答え自体は合っているのに、受け側が形式を決めていない。ここ、地味に効きます。

必要なのは、少しだけ受け取りを頑丈にすることです。ただし何でも救済すると壊れたデータまで通すので、列名と値は最後に検収します。

JSONを切り出して検収するPython

下のコードは標準ライブラリだけで動きます。標準入力にLLMの生の回答を渡し、JSON配列を見つけたら案件の3項目を確認します。余計な列、金額の文字列、A/B/C以外の確度はエラーにします。

import json
import sys

REQUIRED_KEYS = {"案件名", "金額", "確度"}
CONFIDENCES = {"A", "B", "C"}

def extract_json(text: str):
    text = text.strip()

    fence_start = text.find("```")
    if fence_start != -1:
        first_newline = text.find("\n", fence_start)
        closing_fence = text.find("```", first_newline + 1)
        if first_newline == -1 or closing_fence == -1:
            raise ValueError("コードフェンスが閉じていません")
        if text[closing_fence + 3 :].strip():
            raise ValueError("コードフェンスの末尾に説明文が残っています")
        text = text[first_newline + 1 : closing_fence].strip()

    decoder = json.JSONDecoder()
    found_json = False

    for start, char in enumerate(text):
        if char not in "[{":
            continue
        try:
            value, end = decoder.raw_decode(text[start:])
        except json.JSONDecodeError:
            continue

        found_json = True
        if not text[start + end :].strip():
            return value

    if found_json:
        raise ValueError("JSONの末尾に説明文が残っています")
    raise ValueError("JSONを見つけられません")

def validate(items) -> None:
    if not isinstance(items, list):
        raise ValueError("JSONの先頭は配列にしてください")

    for index, item in enumerate(items):
        if not isinstance(item, dict):
            raise ValueError(f"item[{index}] がオブジェクトではありません")
        if set(item) != REQUIRED_KEYS:
            raise ValueError(f"item[{index}] のキーが違います: {sorted(item)}")
        if not isinstance(item["案件名"], str) or not item["案件名"].strip():
            raise ValueError(f"item[{index}] の案件名が空です")
        if isinstance(item["金額"], bool) or not isinstance(item["金額"], int):
            raise ValueError(f"item[{index}] の金額は整数にしてください")
        if item["金額"] < 0:
            raise ValueError(f"item[{index}] の金額が負です")
        if item["確度"] not in CONFIDENCES:
            raise ValueError(f"item[{index}] の確度はA/B/Cにしてください")

try:
    result = extract_json(sys.stdin.read())
    validate(result)
except ValueError as error:
    raise SystemExit(f"エラー: {error}")

print(json.dumps(result, ensure_ascii=False, indent=2))

図にすると、LLMは候補を出す担当で、採用の可否はPython側です。ここを分けると、シートに入った数字がどこで壊れたかを見つけやすくなります。

Q. 実際に何が通り、何を止める?

次のように、回答の前に短い文章やコードフェンスがあっても、配列の中身が正しければ通ります。

python3 check_llm_json.py <<'EOF'
抽出結果です。
```json
[
  {"案件名": "展示会ブース", "金額": 120000, "確度": "A"},
  {"案件名": "提案資料作成", "金額": 48000, "確度": "B"}
]
```
EOF

出力はこうなります。

[
  {
    "案件名": "展示会ブース",
    "金額": 120000,
    "確度": "A"
  },
  {
    "案件名": "提案資料作成",
    "金額": 48000,
    "確度": "B"
  }
]

一方、"金額": "120,000円" は通しません。見た目は親切でも、集計用のセルには文字列です。ここで落として、プロンプトを直すか人が確認した方が早いです。末尾に「以上です。」が付いたケースも止めます。途中まで正しいJSONを勝手に採用すると、次回の障害が見えなくなるからです。エラー文と元の回答を一緒に残すと、直す場所もすぐ分かります。

check_llm_json.py に保存したら、通ったJSONだけをGASやCSV出力の入力に渡します。案件名や確度の候補を変えたくなった時は、REQUIRED_KEYSCONFIDENCES を先に直せばよいです。受け側のルールがコードに残ります。

Q. これでプロンプトは雑でいい?

そこは逆です。プロンプトにも「配列だけを返す」「金額は整数、円記号とカンマなし」「確度はA/B/C」と書きます。Pythonの検収は、プロンプトを省略するためのものではありません。外れ値を早く発見するための最後の関門です。

入力元が商談メモなら、案件名の重複や金額の妥当性まではこのコードで決めません。そこは元のメモと見比べる仕事です。形式チェックと内容確認を一緒にすると、どちらも曖昧になります。

で、現場でどう使うか

LLMに再整形を頼む前に、まず受け取り側の条件を固定します。案件抽出のように次工程が決まっている仕事では、きれいな文章より壊れないJSONの方が価値があります。

この小さな検収を挟むだけで、スプレッドシートの集計が「たまに変な行を含む」状態から抜けられます。LLMには候補を出させる。数字と列の責任は、手元のコードで持つ。営業や企画の現場では、この分け方がいちばん再現しやすいです。

0
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
0
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?