Claude Batch APIで論文を一次スクリーニングする — 採択基準の固定と一致率の測り方
数百本の論文のタイトルと抄録を、固定した採択・除外基準でBatch APIにかける手順。include / exclude / unsureの3択で返させ、人手との取りこぼし率を測るところまで書きます。
システマティックレビューの一次スクリーニングでは、検索で集めた数百本の論文について、タイトルと抄録だけを見て「全文を読むか、ここで落とすか」を決めます。この作業をMessage Batches APIに回すときの要点は、採択・除外基準を先に文書として固定し、機械には「落とす」判断を慎重にさせ、人手との食い違いを取りこぼしの側から測ることです。
検索式や読書計画を立てる段階はClaudeで文献レビューの計画を立てるで扱っています。ここでは、検索を終えた後の「数百件を同じ基準で機械的に判定する」部分に絞ります。バッチ機能そのものの仕様はClaude Batch APIの使い方にあります。
機械に任せる判定は「落とす」より「残す」に寄せる
一次スクリーニングで怖いのは、採るべき論文を誤って落とすことです。残してしまった無関係な論文は、後段の全文確認で落とせます。落とした論文は、誰も見直さなければそのまま消えます。
この非対称性を、出力の選択肢に反映します。二択の include / exclude ではなく、unsure を加えた三択にします。
| 判定 | 意味 | その後 |
|---|---|---|
include | 意味基準をすべて満たすと抄録から読める | その後全文確認へ進む |
unsure | 意味抄録だけでは判断できない、または基準に境界例がある | その後全文確認へ進む |
exclude | 意味除外基準のどれかに、抄録の記述ではっきり当たる | その後除外。ただし人手サンプルで取りこぼしを検査する |
unsure を設けると、抄録が空欄の論文や、研究デザインが書かれていない論文を、無理に二択へ押し込まずに済みます。exclude は「除外基準のどれに、抄録のどの語句で当たったか」を示せるときだけ、と基準に書いておきます。
採択・除外基準をプロトコルとして固定する
バッチは、送信した後に変更できません。基準を直したくなったら、バッチを取り消して新しく作ることになります。基準はリクエストを組む前に、版付きのファイルにします。
書くのは、対象(P)・介入や曝露(I)・比較(C)・アウトカム(O)・研究デザイン・言語・出版期間といった、レビューのプロトコルで決めた項目そのままです。例えば次のような形になります。
# スクリーニング基準 v1(2026-10-13 版)
タイトルと抄録のみで判定する。全文は見ない。
## 採択基準(すべて満たすものを include)
- A1 対象: 成人(18歳以上)の2型糖尿病患者を含む
- A2 介入: 遠隔モニタリングを用いた血糖管理
- A3 デザイン: ランダム化比較試験
## 除外基準(1つでも明確に当たれば exclude)
- E1 対象が小児のみ
- E2 動物実験・in vitro
- E3 レビュー・症例報告・学会抄録のみ
- E4 言語が英語・日本語以外
## 判断ルール
- 抄録が空、または判定に必要な情報がない: unsure
- 基準に当たるか迷う: unsure(exclude にしない)
- exclude のときは reason_code に該当する除外基準の番号を1つ入れるポイントは3つあります。
- 採択基準は「すべて満たす」、除外基準は「1つでも当たる」と、判定の向きを文章で決める
- 除外基準に番号を振る。後で理由別に集計でき、レビュー報告の除外理由の記載にもつなげられる
- 「迷ったらunsure」を判断ルールに入れる。モデルの迷いを
excludeに流さないための一行です
この基準は人間の査読者にも同じものを渡します。機械と人が別々の基準を見ていると、一致率を測っても何の差か分からなくなります。
出力スキーマで判定を三択に縛る
output_config.format にJSON Schemaを渡すと、出力の形を縛れます。Batch APIは構造化出力と併用でき、バッチの50%割引もそのまま適用されます。
SCHEMA = {
"type": "object",
"properties": {
"decision": {
"type": "string",
"enum": ["include", "exclude", "unsure"],
},
"reason_code": {
"type": "string",
"enum": ["none", "E1", "E2", "E3", "E4", "no_abstract"],
},
"evidence": {"type": "string"},
},
"required": ["decision", "reason_code", "evidence"],
"additionalProperties": False,
}evidence には、判定の根拠になった抄録中の語句を短く抜き出させます。人手が不一致を調べるとき、Claudeがどこを見て決めたかを追えます。
スキーマの書き方には、ドキュメントに載っている制約があります。
スキーマ設計で先に知っておくこと
数値と文字列長の制約は使えない
minimum/maximumやminLength/maxLengthは未対応で、使うと400エラーです。requiredとadditionalProperties: falseは必須の書き方です。enumは文字列・数値・真偽値・nullに限る
複雑な型をenumに入れられません。除外理由は番号の文字列にし、意味の説明は基準ファイル側に置きます。
初回だけ遅れ、スキーマを変えるとキャッシュが切れる
同じスキーマを初めて使うときは文法のコンパイルで余計な待ちが出ます。コンパイル結果は最後の使用から24時間キャッシュされ、スキーマの構造を変えると無効になります。
enum の大文字小文字がずれて返る場合の扱いは、自由記述の分類をバッチで回す手順に書いています。取り込み時に小文字へ揃えて照合すれば足ります。
1論文1リクエストでバッチを組む
論文1本が1リクエストです。custom_id は、検索データベースの出力に付いたレコードIDに接頭辞を付けて使います。結果の順序は投入順と限らないため、突き合わせはこのIDで行います。
import json
import anthropic
client = anthropic.Anthropic()
criteria = open("screening_criteria_v1.md", encoding="utf-8").read()
records = json.load(open("records.json", encoding="utf-8"))
# 例: [{"id": "0001", "title": "...", "abstract": "..."}, ...]
def build(rec):
abstract = rec["abstract"] or "(抄録なし)"
return {
"custom_id": f"P-{rec['id']}",
"params": {
"model": "claude-sonnet-5-5",
"max_tokens": 1024,
"system": criteria,
"messages": [{
"role": "user",
"content": (
f"<title>{rec['title']}</title>\n"
f"<abstract>{abstract}</abstract>"
),
}],
"output_config": {
"format": {"type": "json_schema", "schema": SCHEMA}
},
},
}
batch = client.messages.batches.create(
requests=[build(r) for r in records]
)
print(batch.id)基準を system に置き、論文は最後に置いています。全リクエストで共通の部分が前に来る並びです。プロンプトキャッシュを効かせる場合の考え方はバッチ処理のキャッシュヒット率にあります。バッチは並列かつ順不同で処理されるので、キャッシュヒットは確実には保証されません。
1バッチの上限は10万リクエストか256 MBで、先に届いたほうが効きます。数百本ならどちらにも届きません。投入前に、1本だけ同期のMessages APIで同じ形のリクエストを試すと、スキーマ違反などの検証エラーを先に拾えます。
費用は数百本ならほぼ誤差になる
抄録の長さと基準の分量で入力トークンは変わります。仮に800本、1本あたり入力1,100トークン(基準700 + タイトルと抄録400)、出力120トークンとすると、入力は合計88万トークン、出力は9.6万トークンです。
| モデル | バッチ入力 / 出力(MTokあたり) | 800本のバッチ概算 |
|---|---|---|
| Claude Haiku 5.5(100,000トークンまでのプロンプト) | バッチ入力 / 出力(MTokあたり)$0.05 / $0.25 | 800本のバッチ概算約$0.07 |
| Claude Sonnet 5.5 | バッチ入力 / 出力(MTokあたり)$1 / $5 | 800本のバッチ概算約$1.36 |
| Claude Opus 5.5 | バッチ入力 / 出力(MTokあたり)$2 / $10 | 800本のバッチ概算約$2.72 |
どのモデルでも数ドルに収まるため、この規模ではモデルの選択を料金で決める理由は薄いと言えます。むしろ見るべきは、人手との食い違いです。安いモデルで回して取りこぼしが多ければ、全文確認に回る論文が減る代わりにレビューの網が荒くなります。同じ基準・同じサンプルでモデルを替えて取りこぼし率を比べ、バッチ料金の差が数ドルであることを踏まえて選びます。
結果の取り込みとunsureの扱い
結果は succeeded / errored / canceled / expired のいずれかで返ります。後ろの3つは課金されません。取り込みの要点は、succeeded でも判定が得られたとは限らない点です。拒否や出力の打ち切りが成功扱いで混ざります。拒否の挙動はBatch APIの拒否は成功扱いになる落とし穴にまとめています。
import json
DECISIONS = {"include", "exclude", "unsure"}
done, redo = {}, []
for item in client.messages.batches.results(batch.id):
cid = item.custom_id
if item.result.type != "succeeded":
redo.append(cid) # errored / canceled / expired
continue
msg = item.result.message
if msg.stop_reason != "end_turn":
redo.append(cid) # refusal / max_tokens
continue
text = next(b.text for b in msg.content if b.type == "text")
data = json.loads(text)
decision = data["decision"].lower()
if decision not in DECISIONS:
redo.append(cid)
continue
done[cid] = {**data, "decision": decision, "criteria": "v1"}redo の論文は、機械が判定できなかった扱いにして、再投入するか人手に回します。判定できなかった論文を黙って exclude と数えないことが大切です。取りこぼしの源になります。
unsure と include は、合わせて全文確認の候補です。unsure の割合が大きすぎるときは、基準の記述が曖昧で、モデルが判断を避けている可能性があります。基準の境界例を足して v2 を作り、同じサンプルで比べ直します。
結果が取得できるのは、バッチ作成から29日間です。取り込みを終えたら、done をファイルに書き出しておきます。
人手との一致率は「取りこぼし」から見る
機械の判定を信頼してよいかは、人手のサンプルと比べて確かめます。ここでは全体の一致率より先に、次の指標を見ます。
一致率を測る手順
- 1
パイロットで基準を直す
全体から数十本を抜き、人が先に基準に沿ってラベルを付けます。機械の判定と見比べ、不一致の原因が基準の書き方にあるものを直して
v2にします。本番のバッチはその後に回します。 - 2
本番のサンプルを抜く
機械が
excludeにした論文から、ランダムにサンプルを抜きます。includeとunsureは全文確認に進むため人の目が入りますが、excludeは誰も見直さないので、ここが検査の主戦場です。 - 3
人手が付けたラベルと突き合わせる
サンプルの論文を、機械の判定を伏せた状態で人が判定します。可能なら2人が別々に付け、割れた論文は協議で決めて正解ラベルにします。
突き合わせの指標は2つです。
- 取りこぼし: 人が
includeまたはunsureにすべきと判断したのに、機械がexcludeにした論文の数。サンプルの中でこの数が0に近いことが、機械のexcludeを採用する条件です - 全体の一致: 3択のラベルがどれだけ揃ったか。Cohenのκ(偶然の一致を除いた一致度)と混同行列で見ます
from sklearn.metrics import cohen_kappa_score, confusion_matrix
labels = ["include", "unsure", "exclude"]
human = [...] # 人手(協議後)のラベル
model = [...] # 同じ論文に対するClaudeのラベル
print(cohen_kappa_score(human, model))
print(confusion_matrix(human, model, labels=labels))
missed = sum(
1 for h, m in zip(human, model)
if m == "exclude" and h != "exclude"
)
print("取りこぼし:", missed)サンプルが小さいと、取りこぼしが0件でも「全体で0件」とは言えません。サンプルの件数は、許せる取りこぼしの水準とレビューの規模で決める数字で、一律の目安はここでは置きません。取りこぼしが出たら、その論文の抄録と evidence を読み、基準の書き方で防げた型かを調べます。防げる型なら基準を直して v2 で回し直し、防げない型なら exclude の採用を見送る範囲を決めます。
人手同士のκが低いなら、機械の問題の前に基準の問題です。2人が割れる論文は、どちらの基準でも unsure にすると決めておけば、運用が安定します。
論文データを外部に送る前に確かめること
バッチの入力と出力は、バッチ作成から最長29日間保存されます。バッチはWorkspace内に隔離され、同じWorkspaceのAPIリクエストか、権限のあるユーザーだけが参照できます。処理済みのバッチは DELETE /v1/messages/batches/{batch_id} で削除でき、処理中のものは先に取り消します。
タイトルと抄録は公開された書誌情報ですが、未公開の原稿や、共同研究先から預かった論文を混ぜる場合は、先方との取り決めで送ってよいかが決まります。データベースから書き出した抄録の再利用条件も、利用契約によって異なります。送信前に所属機関のルールと契約を確かめます。
よくあるつまずき
落とし穴
基準を途中で変えて、バッチの結果が混ざる
v1 で回した結果の一部と、v2 で回し直した結果を同じ表に混ぜると、どの基準で除外したのかを後から言えなくなります。結果には基準の版を残し、版を変えたら全件を回し直します。レビュー報告には、最終的に使った版を書きます。
抄録のない論文がexcludeにされる
抄録が空の論文に、タイトルだけで判定させると、情報不足のまま exclude が出ることがあります。入力に「(抄録なし)」と明示し、判断ルールで unsure と決めておけば防げます。取り込み後に、抄録が空だった論文が exclude になっていないかも検査します。
結果の順番が元データと違う
順序は保証されません。突き合わせは必ず custom_id で行います。
同期では通ったのにバッチで落ちる
stream: true や max_tokens: 0 はバッチで使えず、検証エラーになります。1件の失敗が他のリクエストの処理を止めることはありません。
29日を過ぎて結果が読めない
29日は、完了時点でなくバッチ作成時点から数えます。
まとめ
論文の一次スクリーニングをBatch APIに回すと、数百本の判定が数ドルと短い待ち時間で済みます。結果を使える形にするのは、バッチの外側の作業です。基準を版付きのプロトコルにして、判定を include / unsure / exclude の三択に縛り、機械が exclude にした論文の中から取りこぼしを人手で数える。この3点が揃っていれば、一次スクリーニングを機械に任せた根拠を、レビューの手法欄に書けます。文献を探す側の手順はConsensusコネクタでの検索式づくりで扱っています。