Node.jsでGPT-4oをAPI経由で使う

あきれるくらい簡単です

Freshmeat再起動計画の続きです。

いくつかの情報源をクロールして、新着ソフトウェアの情報を収集できるようになっています。

先日から毎日クローラを動かして、ぼちぼち説明文を翻訳してみたりしています。

説明がちゃんと書いてあるものならそれでいいんですが、ロクな説明がないものがあって苦労していました。

そこでChatGPTを駆使して作ったりもしていたのですが、プロンプトが安定して来たこととぽちぽち人力でやるのがダルくなったので、APIを使ってやることを考えました。

背景

Freshmeat再起動計画

例によってFreshmeat再起動計画なのですが、今のところ

  • SourceForge
  • FreshCode
  • GitHub

あたりをクロールして情報を収集しています。もっと手を拡げたいところですが、この3サイトだけでも情報が重複していたり件数が凄いことになっているので、その辺も考えながらやらなければなりません。希望としては、

  • HuggingFace
  • 代替アプリケーションの紹介サイト
  • 新着のハードウェアの情報

とかも集めたいと思っているところです。ただ、ここら辺を含めるためには、情報の整理について考えなければなりません。

情報の整理も大事ですが、その前に手をつけるべきこととして

ソフトウェアの紹介や説明の整理

をするべきだと思っています。これは、

  • 説明文の標準化
  • 検索用タグの標準化

等が必要です。

従来、このようなことはクラウドソーシング的に「みんなで協力して」というのが通常でしたが、ぽっと出のサイトで協力者が十分集まるとは思えませんし、集まれば集まったで品質の平準化が難しくなるということもあって、「とりあえず自分でやろう」ということにしていました。そこで、一番エントリ数の多いSourceForgeを軸にして、そこにある説明を翻訳することから始めました。機械的な説明なので、機械翻訳でもまぁ読めないことはありません。

とは言え、元の文章の質も微妙なものが結構ありますし、統一した方針で書かれたというわけでもないので、どうしたものかと思ってました。

AI(LLM)を使う

そこで、あまりに説明の少ないものからChatGPT-4oを駆使して説明文を書くことにしました。

ChatGPT-4oの良いのは、RAGが出来るようになっているため、サイトのURLを教えるだけでやってくれるということです。そしてうまいことに、それを再帰的にやってくれます。つまり、SourceForgeの説明のように「公式サイトはこちら」「GitHubはこちら」と書いてあれば、必要ならそっちもアクセスしてくれます(そうするように指示します)。また、指定したサイトがアクセス出来なければ、勝手に適当にインターネットを探してくれます。まぁここで変なサイトにハマったりしないように、「こことここもあるからね」と教えてやると良い感じになることがわかりました。

実は当初はGoogle AI StudioでGemini 1.5 Flashを使っていました。これはかなりいい感じに動いてくれるのですが、時々プロンプトを無視します。「プロンプトの効き目が切れたのかな?」と思って再度入れてもやっぱり無視することがあります(直ることもある)。この辺の動作がどうも信用できないというか制御しきれないので、主にはChatGPTを使うことにしていました。

まぁどちらのLLMでも、調子のいい時にはちゃんと機能してくれますし、自動的にサイトへのアクセスをしてくれますし、だいたい同じプロンプトで動いてくれます。

だるくなる

プロンプトを調整しながら処理を依頼するのは結構楽しいものです。ちまちまと調整しながら、いい感じの説明を安定して出してくれるようになりました。こうなると、もう右から左です。「説明」も英語からの翻訳とかではなくてオリジナルで執筆してくれますから、文章の品質も上々です。作業はほぼコピペになりました。

しかしそうなると人間とは欲なもので、「右から左」という機械的な作業になるとダルくなってしまいます。「刺身タンポポ」になってしまうと、「ここも機械化できないか?」と考えてしまうのは、コの業界の人の性(さが)ですね。

ChatGPTのAPIを使う

話を振ってみる

プロンプトの調整をして、いい感じの答えを常に出してくれるということを確認したところで、

さて、そろそろ君の能力もわかって来たし、どんな指示を与えればいいかわかって来たし、手作業するのも飽きて来た。これをプログラムでやって行きたいと思うんだ。

ということを言ってみました。

そうすると適当なおべんちゃらと共に勝手にコードを書き始めます。いや、まだ仕様とか言ってないし。

仕様については今までのやりとりがあるので、それに従って書いてくれたりするのかなと思ったら、別にそうではないですね。色々的外れなことを言って来るのですが、ここはいつものChatGPTだなぁというところです。そこで、

気が早いな。
実は一連のシステムはNode.jsで書いているので、これもNode.jsでやりたいと思ってる。アクセスのライブラリはaxiosを使ってる。この環境で目的のプログラムを作って欲しい。なお、他の処理に組み込むので、functionになっていればいいよ。ESModuleとして書いてみて

別にPythonで書いてもいいんですが他の部分はNode.jsですし、APIを叩くだけなので何もPythonを持ち出すこともないだろうということ、またデータベースを使う都合(Sequelizeを使っている)とかもあって、Javascriptで作る方が都合が良いわけです。

そういった要求をして、

  • Node.js(Javascript)でaxiosを使う
  • 情報源のURLを与えてそこからの情報を元に要約する
  • 結果はHTML断片とする

という仕様で作ることにしました。

ChatGPTが教えてくれる

仕様がはっきりしているので、いよいよ書いてもらいます。まずはこんなのを書いてくれました。

import axios from 'axios';
import { Configuration, OpenAIApi } from 'openai';

// OpenAIの設定
const configuration = new Configuration({
  apiKey: 'YOUR_OPENAI_API_KEY',
});
const openai = new OpenAIApi(configuration);

/**
 * OpenAI APIにURLを渡し、返答を受け取る関数
 * @param {string} url - スクレイピングするURL
 * @returns {string} - 生成されたHTML説明文
 */
export async function getSoftwareDescription(url) {
  try {
    // URLに関する情報を取得
    const response = await axios.get(url);
    const pageContent = response.data;

    // OpenAI APIにリクエストを送信
    const aiResponse = await openai.createCompletion({
      model: "text-davinci-003",
      prompt: `以下のページ内容に基づいて、ソフトウェアの説明文をHTML形式で生成してください。\n\nページ内容:\n${pageContent}\n\nHTML形式の説明文:`,
      max_tokens: 1000,
    });

    const descriptionHtml = aiResponse.data.choices[0].text.trim();
    return descriptionHtml;
  } catch (error) {
    console.error('Error fetching the description:', error);
    return `<p>説明の取得に失敗しました。</p>`;
  }
}

// テスト実行
(async () => {
  const url = 'https://setup-php.com/';
  const descriptionHtml = await getSoftwareDescription(url);
  console.log(descriptionHtml);
})();

なんかそれっぽいものなので、試してみます。

APIキーは以前他のことで必要になったのでいくつか作っているのですが、チャット画面からAPIキーを作る動線がなくなっていた(前はあった気がしたんだが)ので、ググろうとしたんですが「目の前に開いているそれは何だ?」と思って聞いてみますと、

OpenAI APIキーの取得手順

  1. OpenAIアカウントの作成

    まず、OpenAIのウェブサイトにアクセスし、アカウントを作成します。既にアカウントをお持ちの場合は、ログインしてください。

    • OpenAI公式サイト
  2. APIキーの生成

    アカウントにログイン後、APIキーを生成します。

    1. ダッシュボードにアクセスします。
    2. 左側のメニューから「API Keys」セクションを選択します。
    3. 「Create API Key」ボタンをクリックして、新しいAPIキーを生成します。
  3. APIキーの管理

    生成されたAPIキーを安全な場所に保存してください。APIキーは一度しか表示されないため、コピーしておきましょう。

とか言って来るので、そのとおりにします。とは言え、ここでちょっと違和感を覚えます。

違和感をスルーしてモデルに何を指定すれば良いか聞いてみます。GPT-3.5とGPT-4とでは随分知識もノリも違いますし、プロンプトは4oを使って育てていたものですし、4oはRAGもあるし... ということもあって、出来れば4oか同等のものが使いたいところです。そうすると、

OpenAIのAPIで使用するモデルは、主に以下のようなオプションがあります。一般的に使用されるモデルとその特徴を以下に示します:

  1. text-davinci-003
    • 最も高性能なモデルで、幅広いタスクに対応できます。
    • クリエイティブな文章生成、複雑な質問への回答、文章の要約など、様々な用途に適しています。
  2. text-curie-001
    • 高い性能を持ちながら、text-davinci-003よりもコストが低いモデル。
    • 文章生成や質問応答タスクに優れています。
  3. text-babbage-001
    • 比較的低コストなモデルで、基礎的なタスクに適しています。
    • シンプルな質問応答やテキスト分類タスクに向いています。
  4. text-ada-001
    • 最もコストが低いモデル。
    • 簡単なタスクや大規模なデータ処理に適しています。

一般的な選択

高品質な文章生成や複雑なタスクには、text-davinci-003を使用するのが一般的です。

とか教えてくれます。モデルの名前と「ChatGPTいくつ」との対応がよくわからないのと、昔mayumiが使った時に「davinciってのを使った」という話を思い出して、「とりあえずtext-davinci-003というのを使う」ということにします。

試してみる

とりあえず最低限の修正をして走らせてみます。

そうすると、いきなり

import { Configuration, OpenAIApi } from 'openai';

で怒られます。

エラーを記録してなかったのでそのままは書けませんが、要するにConfigurationとかOpenAIApiなんてものはopenaiからexportされてないということです。

その旨を伝えると、

了解しました。OpenAIの最新のAPIを使って、URLから情報を取得し、それを用いてHTML形式の説明文を生成する関数を作成します。Configurationが見つからない場合は、古いバージョンのOpenAIライブラリを使用している可能性があります。最新のライブラリを使用するようにします。

とか言って

import axios from 'axios';
import OpenAI from 'openai';

// OpenAIの設定
const openai = new OpenAI({
  apiKey: 'YOUR_OPENAI_API_KEY'
});

/**
 * OpenAI APIにURLを渡し、返答を受け取る関数
 * @param {string} url - スクレイピングするURL
 * @returns {string} - 生成されたHTML説明文
 */
export async function getSoftwareDescription(url) {
  try {
    // URLに関する情報を取得
    const response = await axios.get(url);
    const pageContent = response.data;

    // OpenAI APIにリクエストを送信
    const aiResponse = await openai.chat.completions.create({
      model: "gpt-3.5-turbo",
      messages: [
        { role: "system", content: "以下のページ内容に基づいて、ソフトウェアの説明文をHTML形式で生成してください。" },
        { role: "user", content: `ページ内容:\n${pageContent}\n\nHTML形式の説明文:` }
      ],
      max_tokens: 1000,
    });

    const descriptionHtml = aiResponse.choices[0].message.content.trim();
    return descriptionHtml;
  } catch (error) {
    console.error('Error fetching the description:', error);
    return `<p>説明の取得に失敗しました。</p>`;
  }
}

// テスト実行
(async () => {
  const url = 'https://setup-php.com/';
  const descriptionHtml = await getSoftwareDescription(url);
  console.log(descriptionHtml);
})();

というコードを教えてくれます。

長くなって面倒臭いんで結論を先に書くと、こっちの方が正しいです。こっちの方が現行バージョンのnpmと一致しています。つまり、

Configurationが見つからない場合は、古いバージョンのOpenAIライブラリを使用している可能性があります。最新のライブラリを使用するようにします。

は大嘘です。

これを元にコードを書くと、何となく動くようになります。

さらなる問題

これで動くようになったので、SourceForgeのエントリを食わせて処理させてみました。

そうすると、「トークン数が大きくて処理できねー」という意味のエラーが出ます。

ちょっと前のChatGPTをヘビーに使っていた人はすぐわかると思いますが、あまり大きな入力を与えるとトークン数の上限に当たってしまうのですね。このことをChatGPTに聞くと、

リクエストが大きすぎる場合に対応するために、ページ内容を適切にトリミングするか、より効率的にプロンプトを構築する方法を取ります。以下に、gpt-3.5-turboを使用してスクレイピングした内容を適切に処理するためのスクリプトを示します。

ということで、取得したページを千切って処理することを勧められます。コード片が示されたので、それを参考にいじってみます。

import axios from 'axios';
import { Configuration, OpenAIApi } from 'openai';

// OpenAIの設定
const configuration = new Configuration({
  apiKey: 'YOUR_OPENAI_API_KEY',
});
const openai = new OpenAIApi(configuration);

/**
 * OpenAI APIにURLを渡し、返答を受け取る関数
 * @param {string} url - スクレイピングするURL
 * @returns {string} - 生成されたHTML説明文
 */
export async function getSoftwareDescription(url) {
  try {
    // URLに関する情報を取得
    const response = await axios.get(url);
    const pageContent = response.data;

    // ページ内容を分割
    const chunkSize = 4000; // 分割サイズ
    const chunks = [];
    for (let i = 0; i < pageContent.length; i += chunkSize) {
      chunks.push(pageContent.slice(i, i + chunkSize));
    }

    let descriptionHtml = '';
    for (const chunk of chunks) {
      // OpenAI APIにリクエストを送信
      const aiResponse = await openai.createChatCompletion({
        model: "gpt-3.5-turbo",
        messages: [
          { role: "system", content: "以下のページ内容に基づいて、ソフトウェアの説明文をHTML形式で生成してください。" },
          { role: "user", content: `ページ内容:\n${chunk}\n\nHTML形式の説明文:` }
        ],
        max_tokens: 1000,
      });

      descriptionHtml += aiResponse.data.choices[0].message.content.trim();
    }

    return descriptionHtml;
  } catch (error) {
    console.error('Error fetching the description:', error);
    return `<p>説明の取得に失敗しました。</p>`;
  }
}

// テスト実行
(async () => {
  const url = 'https://setup-php.com/';
  const descriptionHtml = await getSoftwareDescription(url);
  console.log(descriptionHtml);
})();

言いたいことはわかりますね。

これを走らせてみると問題があります。それはchunk毎に処理するものだから、chunk数分だけの要約を返して来て、それが結合されたものが結果となってしまいます。違うそうじゃない!

この点を指摘すると、

各チャンクの内容が連続的に処理されるようにするため、まず各チャンクから重要な情報を抽出し、それを統合してからHTML形式の説明文を生成する方法を取ります。

このプロセスは以下のように行います:

  1. ページ内容を分割:大きなページ内容をチャンクに分割します。
  2. 各チャンクの要約を生成:各チャンクを要約し、重要な情報を抽出します。
  3. 要約を統合:すべての要約を統合して一つの説明文を作成します。
  4. 最終的な説明文を生成:統合された要約を基に、HTML形式の説明文を生成します。

とか教えてくれます。

コード片も示されるので、それを参考にしつつ修正するのですが、やっぱり返答が微妙です。chunk毎にメモを作ってそれを統合すると言っても、そもそもどのメモが本質情報になっているか、断片的なメモだけではわかりようもないからです。

嘘つきChatGPT

実は教えられたことにはもう一つ大きな「嘘」があります。と言うのは、現行のChatGPTのモデルはもっと色々ありますし、GPT-4oのモデルもあります。

Models

いわゆるLLMだけでも、いろんな種類があります。davinciとか昔の話です。

考察

あれこれうまく行かない例を並べましたが、前から薄々感付いていたことを「やっぱりな」と納得した次第です。

それは何かと言えば、

ChatGPTは最新のChatGPTのことは知らない

ということです。

凄く当たり前のことなんですが、ChatGPTの知識は「ある時点」のものを元にしています。たとえば、現行のgpt-4oに関して言えば、「2023年10月まで」と書かれています。それ以後のことは知りません。

ところが、「最新のChatGPT」はその後に出ています。つまり、ChatGPTの最新版についてChatGPTは知らないのです。

最初は「そんなことわかったことだろ」「対策してあるよね」と思っていたのですが、一連のセションを見る限りそれは単なる希望に過ぎず、実際には知らないようです。もしかしたら特別扱いとかされているのかと思っていたんですが、そうでもないようです。

ですから、最新のopenai npmのことも知りませんし、最新のモデルのことも知りません。そして、知らないことは平気で嘘をつくのがChatGPTです。

まとめ

ダメな例ばかり書いたので、最後にちゃんと結果が出たコード片を示しておきます。

        const aiResponse = await openai.chat.completions.create({
            model: "gpt-4o",
            messages: [
                { role: "system", content: `
--- プロンプトは秘伝のタレ ---
説明は全て日本語で書いて下さい。\n`},
                { role: "user", content: `URL:\n${urls.join(',')}\n\nJSON形式の説明文:` }
                ],
            max_tokens: 4000,
        });
        descriptionHtml += aiResponse.choices[0].message.content.trim();

ポイントはこれだけですよ。これだけ。

つまり、

  • モデルとしてgpt-4oを指定する
  • 入力はURLを並べるだけでいい

ということだけです。これだけで期待する結果が出るようになります。

ね、簡単でしょう?

最近のエントリー

404 WASP not found

第12回 「パーソナルサーバ」について考える

Jar Gardenに植物を植える

新しいお友達

Google翻訳、ChatGPT、Gemini...

Node.jsでGemini 1.5 FlashをAPI経由で使う

今日は「ぴろろんの日」

Node.jsでGPT-4oをAPI経由で使う

遠隔手話通訳実験

LLMと戯れてみる

新刊情報の収集

最近のできごと

Hieronymusのインボイス番号対応について

会計システム「Hieronymus」の現状

OrangePi5にZabbixをインストールする

オープンソースのノートアプリ「SiYuan」 - CasaOS AppStoreレビュー

お気に入りの色さがし1

創立記念日

SPDX License Listをデータ化した

Orange Pi5でC3TR-Adapterを試す