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?

利用者の財布を壊さないAIプラグイン設計 ― トークン代は誰に着地するか

0
Posted at

AIチャットボットのプラグインを公開して、いちばん多かった離脱がどこだったか。機能の説明でも設定画面でもなく、「APIキーを取得して課金を設定してください」という一文の前でした。インストールはされる。けれど、知らない会社にカードを登録して、いくらかかるか分からない蛇口を自分側に開ける、その手前で人が消える。インストール数と、実際にチャットが動いた数の差を見て、谷の深さに気づきました。

トークン代という見えない蛇口は、作者ではなく利用者の側に開きます。この記事は、その前提で、利用者の財布を壊さないために設計でできることをまとめたものです。AIを使う側の財布(自分が払う)と、載せる側の財布(利用者が払う)に分けて、後者の設計を実装寄りに書きます。

価格やプランは動きが速いところです。下記は確認した時点のものとして読み、最新は各社の公式の料金ページで確認してください。

  • 使う側: Claude のサブスク(実機・公式で現行価格を確認)、Codex CLI
  • 載せる側のルート: OpenRouter、各社の API
  • 対象は自作の WordPress プラグイン(AIチャットボット)

以下のコードは説明用の最小例です。単価表・通貨表示・プロバイダ分岐・nonce 検証は、実装側で埋める前提で読んでください。

コストは、どこかの財布に着地する

少し前まで、AIツールは月額固定が当たり前でした。その前提が崩れ始めています。GitHub Copilot は2026年6月1日に従量課金へ移り、リクエスト数ではなく入力・出力・キャッシュのトークン量でクレジットを消費する形になりました。背景は、エージェント化で計算コストが膨らみ、月額固定では抱えきれなくなったこと。

ここから見える一般則はシンプルです。AIの計算には実費がかかり、それは必ずどこかの財布に着地する。月額固定は提供者がいったん引き受けて平らに見せていただけで、従量課金は蛇口を利用者の手元に戻した。個人開発でも同じで、自分が払うか、利用者が払うか、宙には浮かせられません。

使う側と、載せる側

自分が払う側は、手綱を自分で握れます。定額と従量を用途で分ける、自走を区切る、毎回抱える前置きを軽くする。使い方しだいで増減するので、まだ御しやすい。

難しいのは載せる側でした。AI機能を製品に載せると、払うのは利用者で、設計するのは自分。自分の財布なら「高いから使わない」というブレーキが自然に効きますが、利用者の財布に自分のブレーキは効きません。冒頭の離脱は、その象徴でした。自作のRapls AI Chatbotを公開して、初めてこの非対称が見えました。

WordPress のプラグインは「入れれば無料で動く」感覚が染みついています。そこへ「使うたびに外部AIへ従量課金」を出すと、その前提と正面衝突します。利用者が身構えるのは値段の高さより、想定外の種類の出費だから、という面が大きい。最初に出費の性質を正直に伝え、まず無料で試せる入口を用意して、驚かせないことが効きます。

誰の財布に寄せるか

設計の最上流に分かれ道があります。利用者にキーを持ち込んでもらう(作者は払わないが、最初の手続きが壁)か、作者がまとめて払って定額提供する(体験はなめらかだが、トークン代と暴走リスクを作者が背負う)か。

後者は、定額で受け取る額は決まっているのに出ていくトークン代に上限がなく、ヘビーユーザーほど赤字を膨らませる構造になりかねません。個人で背負うには重い。なので自分は、利用者がキーを持ち込む形を基本にして、その最初のひと手間をできるだけ軽くする方向で設計しました。以下はその中身です。

設計: 利用者の財布を守る

まず、リクエスト1回をさばく通しの流れを骨格に置きます。どのガードを呼ぶ前に置き、どれを呼んだ後に置くか、で効き方が決まります。

function rapls_chat_handle( $user_id, $message ) {
    // 1. 呼ぶ前の歯止め(回数・間隔)
    $gate = rapls_chat_check_limits( $user_id );
    if ( is_wp_error( $gate ) ) {
        return $gate; // 「上限に達しました」を利用者へ
    }
    // 2. 用件の重さでモデルを選ぶ(利用者指定があれば優先)
    $model = rapls_chat_pick_model( $user_id, $message );
    // 3. 出力上限を決めてから呼ぶ
    $res = rapls_chat_call_api( $model, $message, array( 'max_tokens' => 512 ) );
    // 4. 呼んだ後に使用量を記録(可視化・上限判定に使う)
    rapls_chat_record_usage( $user_id, $model, $res['usage'] ?? array() );
    return $res;
}

無料で試せる入口を作る

いちばん効きました。カード登録の前に、一度チャットが動くのを見てもらう。OpenRouter の無料で試せる枠を使って、キー発行とカード登録の段差を最初は飛ばせるようにしています。動くと分かってから本格利用のキーを考えてもらえばいい。

無料枠には回数・速度の制限があり、仕様も提供側の都合で変わります。無料導線は「一度動かして試す」入口と割り切り、続けるなら自分のキーへ、という道筋を最初から見せておく。無料枠に寄りかかった設計は、その枠が変わった日に製品ごと止まります。

上限を置く(暴走を設計で止める)

日次の天井で総量に蓋をし、最短間隔で連打やループを止めます。とくに後者が大事で、人が見ていない時間に呼び出しが無限に続く事故は、間隔ガード一本でだいぶ防げます。

function rapls_chat_check_limits( $user_id, $daily = 100, $min_interval = 2 ) {
    $today = 'rapls_chat_count_' . $user_id . '_' . gmdate( 'Ymd' );
    $last  = 'rapls_chat_last_'  . $user_id;

    if ( get_transient( $last ) ) {
        return new WP_Error( 'too_fast', 'リクエストが速すぎます。少し待ってください。' );
    }
    set_transient( $last, 1, $min_interval );

    $count = (int) get_transient( $today );
    if ( $count >= $daily ) {
        return new WP_Error( 'daily_limit', '本日の利用上限に達しました。' );
    }
    set_transient( $today, $count + 1, DAY_IN_SECONDS );
    return true;
}

悪意だけでなく、ただの設定ミスやエラーループでも暴走は起きます。利用者を信じるかどうかではなく、事故は善意でも起きるので、設計で塞ぎます。

モデルの段使い(安く受けて、必要なときだけ上げる)

簡単な用件に最上位モデルは過剰です。利用者の明示指定を最優先し、なければ用件の重さで振り分け、答えが弱いときだけ一度だけ上位へ上げ直す。

function rapls_chat_pick_model( $user_id, $message ) {
    $chosen = get_user_meta( $user_id, 'rapls_chat_model', true );
    if ( $chosen ) {
        return $chosen; // 財布の主導権は利用者に
    }
    $is_simple = mb_strlen( $message ) < 40
        && ! preg_match( '/なぜ|理由|比較|詳しく|どうやって/u', $message );
    return $is_simple ? 'cheap-model' : 'strong-model';
}

「安く受けて、弱ければ一度だけ上げる」を実装にすると、こうなります。はっきりした上げ要求は最初から上位へ、それ以外は安いモデルで受けて、弱い答えのときだけ上げ直します。

function rapls_chat_answer( $user_id, $message, $context ) {
    if ( preg_match( '/もっと詳しく|詳細に|長めに|きちんと/u', $message ) ) {
        return rapls_chat_call_api( 'strong-model', $message, $context );
    }
    $res = rapls_chat_call_api( 'cheap-model', $message, $context );
    if ( rapls_chat_looks_weak( $res['text'] ?? '' ) ) {
        return rapls_chat_call_api( 'strong-model', $message, $context ); // 一度だけ
    }
    return $res;
}

function rapls_chat_looks_weak( $text ) {
    if ( mb_strlen( $text ) < 20 ) {
        return true; // 短すぎる
    }
    return (bool) preg_match( '/わかりません|お答えでき|不明です/u', $text );
}

上げ直しが走ると、その用件は安いモデルと上位モデルの2回ぶん呼ぶことになり、1回ぶんの費用が倍になりかねません。上げ直しは一度きりに限り、この2回ぶんも回数の天井の中で数える。上げる条件は辛めにしておくのが落としどころでした。

入力も出力も、送る量を減らす

固定の人格設定(システムプロンプト)は毎回同じなので、対応していればキャッシュに乗せ、変わる質問だけ毎回送る。出力トークンは入力より単価が高いことが多いので、応答の上限を置きつつ「簡潔に答える」と方向づける。短く的確なほうが、財布にも体験にも良かったです。プロバイダごとの送り方の違い(エンドポイント、認証、キャッシュ指示の形)は rapls_chat_call_api の内側に閉じ込めて、上流がプロバイダを気にせず書けるようにしています。

function rapls_chat_call_api( $model, $message, $options = array() ) {
    $provider = rapls_chat_provider_of( $model ); // 'openrouter' / 'anthropic' など
    $system   = rapls_chat_system_prompt();       // 固定の人格設定(毎回同じ)

    $body = array(
        'model'      => $model,
        'max_tokens' => $options['max_tokens'] ?? 512,
        'messages'   => array(
            array( 'role' => 'system', 'content' => $system ),
            array( 'role' => 'user',   'content' => $message ),
        ),
    );

    // キャッシュの渡し方はプロバイダ依存。対応していれば固定部分に付ける
    if ( rapls_chat_supports_cache( $provider ) ) {
        $body['messages'][0]['cache_control'] = array( 'type' => 'ephemeral' );
    }

    $res = wp_remote_post( rapls_chat_endpoint( $provider ), array(
        'headers' => rapls_chat_auth_headers( $provider ), // キーはここで載せる
        'body'    => wp_json_encode( $body ),
        'timeout' => 30,
    ) );
    return rapls_chat_parse_response( $provider, $res );
}

cache_control の形もエンドポイントも認証も、プロバイダで違います。上はある一社の書き方に寄せた例なので、実際は分岐が要りますし仕様も変わります。OpenRouter のように複数プロバイダをまとめて叩ける口を使うと、この分岐自体を薄くできて、無料導線とも相性がよかったです。

透明性(見えない蛇口を、見える蛇口に)

記録した使用量を単価表と掛けて概算に直し、利用者に見せます。まず、トークン数を単価表と掛ける見積もり。

const RAPLS_CHAT_PRICE = array(
    'cheap-model'  => array( 'in' => 0.0, 'out' => 0.0 ), // 公開時に単価表で埋める
    'strong-model' => array( 'in' => 0.0, 'out' => 0.0 ),
);

function rapls_chat_estimate_cost( $model, $usage ) {
    $p   = RAPLS_CHAT_PRICE[ $model ] ?? array( 'in' => 0, 'out' => 0 );
    $in  = ( $usage['input_tokens']  ?? 0 ) / 1000000 * $p['in'];
    $out = ( $usage['output_tokens'] ?? 0 ) / 1000000 * $p['out'];
    return $in + $out; // 1リクエストの概算
}

これを呼び出しのたびに月次へ足し込みます。

function rapls_chat_record_usage( $user_id, $model, $usage ) {
    $cost = rapls_chat_estimate_cost( $model, $usage ); // 単価表 × トークン数
    $key  = 'rapls_chat_usage_' . gmdate( 'Ym' );

    $stats = get_user_meta( $user_id, $key, true );
    if ( ! is_array( $stats ) ) {
        $stats = array( 'calls' => 0, 'in' => 0, 'out' => 0, 'cost' => 0.0 );
    }
    $stats['calls'] += 1;
    $stats['in']    += $usage['input_tokens']  ?? 0;
    $stats['out']   += $usage['output_tokens'] ?? 0;
    $stats['cost']  += $cost;
    update_user_meta( $user_id, $key, $stats );
}

足し込んだ月次を読んで、ひと言にまとめる関数も用意します。

function rapls_chat_usage_summary( $user_id ) {
    $s = get_user_meta( $user_id, 'rapls_chat_usage_' . gmdate( 'Ym' ), true );
    if ( ! is_array( $s ) ) {
        return '今月の利用はまだありません。';
    }
    // 通貨表示は単価表しだい。概算なので「目安」と添える
    return sprintf( '今月: %d回 / 概算 約%s', $s['calls'], rapls_chat_format_cost( $s['cost'] ) );
}

足し込んだ結果は、利用者のプロフィール画面が見せ場所として手軽でした。モデル選択(pick_model が尊重する rapls_chat_model)と今月の目安を、同じ画面に並べます。

add_action( 'show_user_profile', 'rapls_chat_profile_fields' );
add_action( 'edit_user_profile', 'rapls_chat_profile_fields' );

function rapls_chat_profile_fields( $user ) {
    $models  = array( 'cheap-model' => '安め', 'strong-model' => '高品質' );
    $current = get_user_meta( $user->ID, 'rapls_chat_model', true );
    echo '<h2>AIチャットの設定</h2><table class="form-table">';
    echo '<tr><th>使うモデル</th><td><select name="rapls_chat_model">';
    foreach ( $models as $key => $label ) {
        printf(
            '<option value="%s"%s>%s</option>',
            esc_attr( $key ), selected( $current, $key, false ), esc_html( $label )
        );
    }
    echo '</select></td></tr>';
    echo '<tr><th>今月の利用目安</th><td>'
        . esc_html( rapls_chat_usage_summary( $user->ID ) ) . '</td></tr></table>';
}

// 保存(権限チェックとサニタイズ。本来は nonce 検証も入れる)
add_action( 'personal_options_update',  'rapls_chat_save_profile' );
add_action( 'edit_user_profile_update', 'rapls_chat_save_profile' );

function rapls_chat_save_profile( $user_id ) {
    if ( ! current_user_can( 'edit_user', $user_id ) ) {
        return;
    }
    $model = isset( $_POST['rapls_chat_model'] )
        ? sanitize_text_field( wp_unslash( $_POST['rapls_chat_model'] ) ) : '';
    if ( in_array( $model, array( 'cheap-model', 'strong-model' ), true ) ) {
        update_user_meta( $user_id, 'rapls_chat_model', $model );
    }
}

これで利用者は、自分でモデルを選べて、今月どれくらい使ったかの目安も同じ画面で見られます。概算は本物の請求と一致しないので「目安です」と添える。それでも、回数とおおよその額が見えるだけで、不安はだいぶ減りました。金額そのものより、分からないことが不安の正体だったので。

やりがちな取り違え

  • 良いものを作れば払う、と思い込む。実際の壁は、払うこと自体より「いくらか分からない」こと。
  • 最上位モデルを既定にする。利用者から見れば、いちばん高い蛇口を黙って全開にしている。
  • 上限を置かない。自分の財布なら感覚で止まるが、利用者の財布に自分の感覚は効かない。
  • 無料の入口を出し惜しみする。入口で止まられたら、取り分も何もない。試してもらえない損のほうが大きい。
  • 長く返すほど親切だと思う。長い返答は高くつき、読むのも面倒で、チャットとしてもくどい。

どれも、自分が払う側の目線のまま、載せる側を設計していたから起きた取り違えでした。

次の自分に渡すメモ

トークン代は必ずどこかの財布に着地する。自分が払うなら手綱を握れるが、載せる側は、払うのは利用者で設計するのは自分、というねじれを引き受けることになる。まず誰の財布に寄せるかを決め、キー持ち込みなら入口の摩擦を、作者が抱えるなら上限と料金設計を、それぞれ死守する。そのうえで、無料の入口・選べるモデル・段使い・上限・透明性。全部、自分が痛まない蛇口の先に誰かの財布がある、と忘れないための仕掛けです。

可視化の作り込みは、まだ宿題のままです。蛇口の先に、いつも誰かの財布がある。それだけは忘れずにいたいと思っています。

参考

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?