前回(第1回)で企画とUI設計を固めたので、今回は実際にGroq APIを呼び出してAI生成度を診断し、文章を自動改善する処理を実装していきます。途中で「改善したはずなのにスコアが悪化する」という想定外の壁にぶつかった経緯も含めて記録します。

Groq APIのストリーミング処理を実装する

Groq APIはOpenAI互換のエンドポイントを持っており、stream: trueを指定するとSSE(Server-Sent Events)形式でレスポンスがチャンク単位で返ってきます。ReadableStreamのreaderで読み取り、data: から始まる行を1つずつJSONパースして文字列を連結していく処理が基本構造です。

async function callGroq(systemPrompt, userContent, onChunk) {
  const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': 'Bearer ' + apiKey
    },
    body: JSON.stringify({
      model: 'llama-3.3-70b-versatile',
      max_tokens: 2048,
      stream: true,
      messages: [
        { role: 'system', content: systemPrompt },
        { role: 'user', content: userContent }
      ]
    })
  });

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let full = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    const lines = decoder.decode(value).split('\n');
    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const chunk = line.slice(6).trim();
      if (chunk === '[DONE]') continue;
      const obj = JSON.parse(chunk);
      const delta = obj.choices?.[0]?.delta?.content;
      if (delta) { full += delta; onChunk?.(full); }
    }
  }
  return full;
}

このストリーミング処理を使うことで、文章改善の結果が翻訳サイトのようにリアルタイムで右側に流れていく演出を実現できました。

10項目スコアリング用プロンプトの設計

スコアリングでは、AIに自由記述させると後続処理が不安定になるため、JSON形式のみで返答させることを徹底しました。

function scorePrompt() {
  return '日本語文章のAI生成度を診断する専門家として、' +
    '以下10項目を0〜10点(10点が最も人間らしい)で評価してください。\n' +
    '【重要】返答はJSONのみ。コードブロック・説明文・前置き一切不要。\n\n' +
    'tone:語尾の単調さ / redundant:冗長表現 / concrete:具体性\n' +
    'rhythm:読点リズム / emotion:感情・温度感 / length:文長の均一さ\n' +
    'conjunc:接続詞の偏り / structure:構成の硬直性\n' +
    'keigo:敬語の均一さ / bullets:箇条書き依存\n\n' +
    '返答フォーマット:\n' +
    '{"tone":5,"redundant":5,"concrete":5,...,"comment":"コメント"}';
}

レスポンスから{...}の部分を正規表現で抜き出し、JSON.parse()で構造化データに変換します。この段階ではシンプルな実装でしたが、後ほど想定外の落とし穴に直面することになります(詳細は次回記事で解説)。

文章を自動リライトする「改善」プロンプト

診断だけでなく、ワンクリックでAI臭を取り除いた文章に書き換える機能も実装しました。最初に書いたプロンプトはシンプルなものでした。

const sys =
  '日本語文章のリライト専門家として、AIが生成したような文章を' +
  'より人間らしく書き換えます。以下の点を意識してください:\n' +
  '・定型句を避ける\n' +
  '・語尾に変化をつける\n' +
  '・読点の位置を自然にばらつかせる\n' +
  '・文の長さを意図的に変化させる\n' +
  '書き換えた文章のみを出力してください。';

改善したのにスコアが下がるという誤算

実際に動かしてテストしたところ、改善前のスコアが79点だったのに、改善後はなんと69点まで下がるケースが頻発しました。原因を分析すると、主に2つの要因がありました。

  1. HTMLタグの中の日本語まで一緒に書き換えてしまい、構造が崩れる
  2. Llama 3.3 70Bが「改善」しようとした結果、かえって文体が画一的になる

特に2つ目は根が深い問題です。Groq無料枠で使えるモデルは、日本語の文体の微妙なニュアンスの再現精度がClaudeやGPT-4と比べて見劣りします。「人間らしくしてください」という抽象的な指示だけでは、モデルが何をすればいいか迷い、結果的に逆効果な書き換えをしてしまうことが分かりました。

プロンプトを具体的なルールに作り変える

そこで、漠然とした指示から、モデルが迷わない具体的な数値ルールへとプロンプトを全面的に書き直しました。

var sys =
  '日本語文章のリライト専門家として、AIが生成したような文章を' +
  'より人間らしく書き換えます。\n\n【必ず守るルール】\n' +
  '(1) 語尾を変化させる。「だ」「だろう」「ではないか」も使う。\n' +
  '(2) 3〜5文に1回は10文字以下の短文を入れる。\n' +
  '(3) 「また」「さらに」「加えて」「なお」を連続して使わない。\n' +
  '(4) 読点は文ごとに位置を変える。\n' +
  '(5) 「〜することが重要です」等の定型句を削除する。\n' +
  '(6) 書き手の視点・感情・驚きを1〜2箇所加える。\n' +
  '(7) 内容・事実は変えない。\n' +
  '\n書き換えた文章のみを出力。説明・コメントは不要。';

この変更により、改善効果はある程度安定しましたが、それでも完全には解決しません。最終的には「改善後スコアが改善前を下回った場合は警告を表示し、そのまま使わず部分修正を促す」というUI側のフォールバック設計も追加しています。AIの出力を無条件に信頼しない設計が、実用上は重要だと痛感しました。

次回はさらに発生した「JSONパースエラー」と「スマホでのレイアウト崩れ」という2つの大きなバグについて、原因と対策を詳しく解説します。

FAQ

Groq APIのストリーミングレスポンスはどう処理しますか?

OpenAI互換のSSE(Server-Sent Events)形式で返ってくるため、ReadableStreamのreaderでchunkを読み取り、「data: 」で始まる行をJSONパースしてdelta.contentを連結していきます。

文章を改善すると逆にスコアが下がることがあるのはなぜですか?

Groqの無料枠で使えるLlama 3.3 70Bモデルは日本語の文体ニュアンスの再現精度がClaudeやGPT-4と比べて低く、書き換えが画一的になりやすいためです。プロンプトに具体的なルールを与えることである程度緩和できますが、根本解決にはモデル変更が必要です。

AI生成度の診断はどのようなプロンプトで実現していますか?

10項目それぞれの定義をプロンプトに明記し、JSON形式のみで返答するよう厳密に指示しています。コードブロックや説明文を含めないことを明示することで、後続のJSONパース処理を安定させています。