個人開発の EDINET 分析ツールでは、取得した有価証券報告書の PDF を Gemini に渡して「怪しさ判定」をさせている。
単なる要約ではなく、3段階のスコア(normal / caution / danger) を返させる設計にしたので、その仕組みをまとめる。
なぜスコアが必要か
毎日数十〜数百件の書類が提出される。全部読むのは無理なので、AI に「これは要注意」かどうかを仕分けさせたい。
スコアが danger の書類だけ Discord 通知を飛ばす、といった使い方ができる。
プロンプト設計
プロンプトの末尾に必ずスコアを出力させるよう指示する:
分析の最後に必ず以下の形式でスコアを出力してください:
SCORE:normal # 特に問題なし
SCORE:caution # 気になる点あり・要確認
SCORE:danger # 重大なリスクの可能性
Gemini はマークダウン形式で分析テキストを返した後、最終行に SCORE:danger のような文字列を出力する。
PDF を渡す方法
@google/generative-ai SDK では PDF を base64 で渡せる:
const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' })
const result = await model.generateContent([
{
inlineData: {
mimeType: 'application/pdf',
data: pdfBuffer.toString('base64'),
},
},
{ text: prompt },
])
最大 50MB まで渡せるが、大きすぎるとトークン消費が跳ね上がるので注意。
スコアのパース
正規表現で SCORE: 以降を抽出:
function parseScore(text: string): 'normal' | 'caution' | 'danger' {
const match = text.match(/SCORE:(normal|caution|danger)/i)
return (match?.[1] ?? 'normal') as 'normal' | 'caution' | 'danger'
}
この1行で取れる。テキスト全体で最初にマッチした SCORE:xxx を使うので、プロンプトの中に SCORE: を書いてしまうと誤マッチする。プロンプト側では SCORE:normal という形式を「例として」書かずに指示だけにするか、出力セクションを明示的に区切るとよい。
DB への保存
analysis_results テーブルの score カラムに CHECK 制約を入れている:
score VARCHAR(10) CHECK (score IN ('normal', 'caution', 'danger'))
これで不正な値が入らない。プロンプトの出力がブレてもDB側で弾ける。
まとめ
- プロンプト末尾に
SCORE:xxxを出力させる設計はシンプルで使いやすい - PDF は base64 で
inlineDataとして渡す - スコアのパースは正規表現1行
- DB の
CHECK制約でバリデーションを二重にかける