決済プロバイダの REST リファレンスが PDF でしか配られていないことに気づいたのは、個人開発で運用しているアプリの課金まわりを作り直していた夜でした。
エンドポイントの一覧、リクエストボディの型、エラーコードの対応表。どれも PDF の表の中にあって、私自身、それを一行ずつチャット欄に書き写していました。写し間違えれば、エージェントは存在しないフィールド名でクライアントを生成します。
Antigravity 2.1.4 で PDF をそのまま添付できるようになってから、この書き写しの作業がなくなりました。ただ、何でも放り込めば賢く読んでくれるわけではありません。数日使ってみて、効く渡し方とそうでない渡し方がはっきり分かれたので、その線引きを残しておきます。
どんな PDF なら添付が効くのか
結論から書きますと、添付が効くのは「テキストレイヤーを持つ PDF」だけです。
PDF には大きく二種類あります。文字が文字データとして埋め込まれているものと、紙をスキャンしただけで中身が画像になっているものです。前者はエージェントが文字を直接読めます。後者はエージェントから見れば一枚の絵で、表の罫線も数値も読み取れません。
公式の API ドキュメントを書き出した PDF、OpenAPI を印刷した PDF、デザインツールから出した仕様書。このあたりはほぼテキストレイヤーを持っています。一方、古い社内資料を紙からスキャンしたもの、スクリーンショットを貼り付けただけの手順書は画像 PDF であることが多く、添付しても期待した精度が出ません。
添付する前に、その PDF がどちらなのかを 1 コマンドで確かめられます。
# テキストレイヤーの有無を判定する
# poppler-utils に含まれる pdftotext を使う
pdftotext spec.pdf - | head -c 400
# 何も出力されない、または記号の羅列しか出ない場合は画像 PDF
# → そのまま添付しても読めないので OCR が必要
数百文字でも素直なテキストが返ってくれば、そのまま添付して問題ありません。空っぽなら、後述の OCR をかけるか、元データ(HTML 版やテキスト版)を探した方が早いです。
添付の基本操作
操作自体は単純です。チャット入力欄に PDF をドラッグするか、添付アイコンからファイルを選びます。複数枚をまとめて添付することもできます。
添付したあとは、エージェントに「この PDF のどこを見てほしいのか」を必ず添えます。ここを省くと、エージェントは PDF 全体を眺めようとして、関係ないページの記述に引きずられた答えを返してきます。
添付の payment-api.pdf を参照してください。
14〜18 ページの「Create Charge」エンドポイントだけを対象に、
リクエストボディの型を TypeScript の interface で起こしてください。
PDF に書かれていないフィールドは推測で足さないでください。
「推測で足さないでください」の一文は、私の場合ほぼ毎回入れています。仕様書の型は省略するとそのまま欠陥になりますので、エージェントの善意の補完を止めておく方が安全だと考えています。
仕様駆動で実装する手順
実際に決済クライアントを起こしたときの流れを、再現できる形で並べます。
PDF をテキスト判定してから添付する(前述の pdftotext)
対象ページとタスクを限定してエージェントに渡す
まず「型だけ」を生成させ、PDF と目視で突き合わせる
型が固まってから、その型を使う関数本体を生成させる
エラーコード表を別の指示で渡し、例外マッピングを作る
型を先に固めるのが肝心です。いきなり「クライアントを丸ごと作って」と頼むと、型・通信・エラー処理がまとめて出てきて、どこが PDF 由来でどこが推測なのかが分からなくなります。
生成された型は、たとえばこういう形に落ち着きます。
// PDF 14〜18 ページの Create Charge 定義から起こした型
// 任意/必須の区別は PDF の "Required" 列に従っている
export interface CreateChargeRequest {
amount : number ; // 最小通貨単位(円ならそのまま整数)
currency : "jpy" | "usd" ; // PDF の Supported currencies 表より
customerId : string ;
description ?: string ; // PDF 上 Optional
metadata ?: Record < string , string >;
}
export interface CreateChargeResponse {
id : string ;
status : "succeeded" | "pending" | "failed" ;
createdAt : string ; // ISO 8601、PDF の Notes 欄に明記あり
}
ここまで来たら、型を握ったまま関数本体を頼みます。型が確定しているので、エージェントは通信処理に集中でき、出力のぶれが小さくなります。
export async function createCharge (
req : CreateChargeRequest ,
apiKey : string ,
) : Promise < CreateChargeResponse > {
const res = await fetch ( "https://api.example-pay.test/v1/charges" , {
method: "POST" ,
headers: {
Authorization: `Bearer ${ apiKey }` ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify (req),
});
if ( ! res.ok) {
// エラーコード表は次の手順で別途マッピングする
throw new Error ( `charge failed: ${ res . status }` );
}
return ( await res. json ()) as CreateChargeResponse ;
}
スキャン画像 PDF という落とし穴
最初につまずいたのがこれでした。読めると思って画像 PDF を添付し、返ってきた型が PDF にない名前ばかりだったのです。エージェントは画像から文字を起こせないので、見えないものを文脈から想像していたわけです。
対処は二択です。元のテキストデータを探すか、OCR でテキストレイヤーを足すか。後者は ocrmypdf が手軽でした。
# 画像 PDF にテキストレイヤーを付与する
# 日本語が含まれるなら言語パックを指定する
ocrmypdf -l jpn+eng scanned-spec.pdf searchable-spec.pdf
# 付与後にもう一度テキストが取れるか確認
pdftotext searchable-spec.pdf - | head -c 400
OCR を通した PDF は、表組みの認識が完璧ではありません。数値の桁や記号がずれることがあるので、OCR 後の仕様書を渡したときは、型の数値範囲や enum を特に念入りに目視するようにしています。罠を避けるというより、罠があると分かった上で確認の密度を上げる、という運用です。
大きな PDF をそのまま渡さない
100 ページを超える仕様書を丸ごと添付したことがありますが、これは失敗でした。応答は遅くなり、コンテキストの大半が無関係なページで埋まり、肝心の数ページへの注意が薄まります。トークンの消費も馬鹿になりません。
私は今、必要なセクションだけを切り出してから添付しています。分割は手作業より、ページ範囲を指定するスクリプトに任せた方が確実です。
# 仕様書を章ごとに切り出す簡易スクリプト
# pip install pypdf
from pypdf import PdfReader, PdfWriter
def extract_pages (src: str , start: int , end: int , dst: str ) -> None :
"""start〜end ページ(1 始まり・両端含む)を dst に書き出す。"""
reader = PdfReader(src)
writer = PdfWriter()
for i in range (start - 1 , end):
writer.add_page(reader.pages[i])
with open (dst, "wb" ) as f:
writer.write(f)
# 例: Create Charge の章だけを切り出す
extract_pages( "payment-api.pdf" , 14 , 18 , "charge-section.pdf" )
体感では、必要な 5 ページだけを渡したときの応答は、全体を渡したときと比べて目に見えて速く、生成された型の正確さも上がりました。コンテキストに無関係な情報を入れないことが、そのまま精度につながる印象です。
出力を検証する仕組みを入れる
生成物を信じすぎないために、私は検証を二段構えにしています。
一つ目は、エージェントに根拠ページを言わせることです。「各フィールドの型について、PDF の何ページの記述に基づくかを併記してください」と頼むと、突き合わせの手間が一気に減ります。ページ番号が書けないフィールドは、PDF にない=推測の可能性が高い、という判断材料になります。仕様書 PDF を扱うときは、この根拠ページの併記を常に推奨します。
二つ目は、機械的なチェックです。生成された型が、自分の手元の最小テストを通るかを確認します。
// 生成された型が想定どおりかを最小限で確認する
import { CreateChargeRequest } from "./payment-client" ;
const sample : CreateChargeRequest = {
amount: 1000 ,
currency: "jpy" ,
customerId: "cus_test_001" ,
// description と metadata は任意なので省略できるはず
};
// description を必須にしてしまっていれば、ここで型エラーになる
console. log ( "型チェック OK:" , sample.amount);
このサンプルがコンパイルを通るかどうかだけで、「任意項目を必須にしていないか」という頻出のずれを捕まえられます。仕様書から起こした型は、こうした小さなガードを一枚かませておくと安心です。
運用での落としどころ
数日使った今の私の結論は、PDF 添付は「テキストレイヤーのある仕様書を、セクション単位で、根拠ページを言わせながら渡す」ときに最も力を発揮する、というものです。
逆に、画像 PDF を丸ごと、何ページを見てほしいかも言わずに渡すと、書き写しの手間が消えた代わりに、見えない誤りを後から探す手間が増えます。便利な機能ほど、渡し方の作法で結果が変わると感じています。
同じように仕様書 PDF とにらめっこしている個人開発者の方の、ひとつの叩き台になれば嬉しいです。お読みいただきありがとうございました。