Open10

【超雑メモ】普通のWebエンジニアのためのChatGPT系API入門

meijinmeijin

目的

いわゆる普通のWebアプリケーション開発をずっとやってきたWebエンジニアが、OpenAIのAPIを使って自社サービスに生成系の機能を組み込むために、知っておいたほうがいいことをまとめておく。

書くこと

  • APIコールするだけでできること
  • 単にベーシックなchat.completion API叩いて終わる以上のことができるようになるには
  • 要件定義する人がLLMに詳しくなくても、開発者がある程度どういう機能を使えばいいのか話せるようになるというのが目的

たぶん書かないこと

  • プロンプト自体のTips
  • Fine-Tuning的なことをどうやるか
  • ちゃんとした書き方はしない。10分でちょっと手札が増えるみたいなのを目標にする
meijinmeijin

【Lv.1】構造化データをOutputする

  • テキストのOutputなら、APIを叩くだけなので誰でもできる
  • 構造化データのOutputを求められるところがLv. 1
  • ピンと来ないかもしれないが、要件を聞いたときに出力先が配列やオブジェクトなど単なるStringじゃないのでは?って思ったらそれが構造化データの使い時
  • Structured Outputが使える以上、「JSONで吐いて!」なんてプロンプトで指定するのは基本的には時代遅れと思ったほうがいい

Structured Outputを使おう

https://platform.openai.com/docs/guides/structured-outputs

const CalendarEvent = z.object({
  name: z.string(),
  date: z.string(),
  participants: z.array(z.string()),
});

const completion = await openai.beta.chat.completions.parse({
  model: "gpt-4o-2024-08-06",
  messages: [
    { role: "system", content: "Extract the event information." },
    { role: "user", content: "Alice and Bob are going to a science fair on Friday." },
  ],
  response_format: zodResponseFormat(CalendarEvent, "event"),
});
curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o-2024-08-06",
    "messages": [
      {
        "role": "system",
        "content": "You are a helpful math tutor. Guide the user through the solution step by step."
      },
      {
        "role": "user",
        "content": "how can I solve 8x + 7 = -23"
      }
    ],
    "response_format": {
      "type": "json_schema",
      "json_schema": {
        "name": "math_reasoning",
        "schema": {
          "type": "object",
          "properties": {
            "steps": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "explanation": { "type": "string" },
                  "output": { "type": "string" }
                },

要点

  • Official SDKならZodなどLanguage Specificなスキーマライブラリを使える
  • そうじゃない環境ならCurlサンプルなどを参考に自前でスキーマ指定して投げる

たとえばPHP SDKは非公式なので以下のように対応する。

https://github.com/openai-php/client/issues/464

meijinmeijin

【Lv. 1】Promptを投げるときにroleを指定したり、あらかじめ会話させてみよう

単に大きなテキストの塊でプロンプトを一発投げる方法でもいいが、roleを使って、事前に何ラリーか会話がされた状態でプロンプトを投げるというTipsがある。
(few-shotなどと呼ばれるので、ちゃんと知りたければその筋で調べてください)

https://platform.openai.com/docs/guides/text-generation#conversations-and-context

meijinmeijin

【Lv. 1】画像に対して生成AIを使う

https://platform.openai.com/docs/guides/vision

  • 画像→テキストは情報量が減っているので、精度が高めやすいです(精度が高いと思われやすい)
  • 一方で、テキスト→画像は情報量が増えるので、精度が低いと思われやすい
  • 一般に、情報量が減る方向への利用用途は、精度が高いと感じられやすいので向いている傾向にある
meijinmeijin

【Lv. 2】リアルタイムで生成した結果を返す

ChatGPTのWebサイトみたいにリアルタイムで出力を1文字(ひとかたまり)ずつ出力したくなったら。

WebsocketやWebRTCを使うことが本質的な解決策だが、Vercel AI SDKを使える環境下なら使っちゃったほうが明らかに楽。

https://sdk.vercel.ai/docs/foundations/streaming

  • Next.jsのAPI Routeを使うことを前提とすることで、HTTP層での工夫を全部カプセル化できるので。

使えない環境下なら、要はWebSocket使うには?という問いになるので、各自のアプリケーションによって検討すること。事例がないなら、とにかく適当にWebSocket使ったAPI立ててデプロイして疎通するか実験すると良い(インフラレイヤの課題を見つけた方がいい)。

meijinmeijin

【Lv. 3】リアルタイムでSchemaが守られたオブジェクトを生成して返す

同じくVercel AI SDKを使うとある程度簡単だが、使えないなら同等の処理を自前で実装することになるので内部実装を追うと良さそう。また、Schemaが守られないことも多いし、生成に失敗しまくってじゃじゃ馬になることがあるので、生成時の緩いZodと、適用時の厳しめZodの2種類併用するのも有力。

https://sdk.vercel.ai/docs/ai-sdk-ui/object-generation

meijinmeijin

【Lv. 2】じゃじゃ馬になったときのプロンプトプチ改善Tips(WIP)

普通に世の中で言われていることかもしれないけど

  • 文字数指定はあまり通じないことが多い(文字数という概念がLLMに存在しないと考えてよい)
  • 一方で、配列のLength指定はある程度通じる印象
  • ◯◯するな系の注意を伝えるときは、具体的にダメな例を数個プロンプトに混ぜてあげたほうが通じる。要は人間が勝手に抽象化した指示を与えるとLLMに解釈余地を生んでしまうので、具体例を添付せよという話
  • オレオレ表現形式を使うな。たとえばSQLを書いてほしいなら、テーブル定義はDDL(CREATE TABLE文)で教えてあげること。そこでMarkdownなど、テーブルを表現するに際してOfficialではない表現を使ってはならない
  • 英語か日本語かは他の要素と比べると割とどうでもいい。なんなら混ざってもいい。下手に英語にして意味不明な文章になるくらいなら日本語で書いたほうがマシ
  • 生成結果が絶対的な正しさを要求されるものをそもそも取り扱わない。個人的には、《精度云々ではなく、何かが生成されていることが重要である》ケースが最も刺さる用途(そもそも精度が求められる領域に使ってはならない)。
  • 生成結果に対するリスクヘッジとして、生成結果をユーザーが《簡単に》Edit可能にすること、生成結果は原則保存することが大事
meijinmeijin

【Lv. 3】似たような性質のあるテキストを検索しないといけないケース

Embeddingを使う。

https://platform.openai.com/docs/guides/embeddings

テキストをベクトルで表現してベクトルの近さを検索できるようにするというもの。類似したテキストがほしいとか、レコメンド文章を考えたいとか、そういうケースで使われうるので、一応覚えておいたほうがよさそう。

ベクトルがピンと来ない人は多いかもしれないけど、たとえば「こんにちは」と「こんばんは」はどれくらい似ていますか?って言われたら、まずは「こんにちは」とはなにか?というのをたくさんの要素で表現しないといけないじゃないですか。具体的にいうと「日本語であり、名詞であり、挨拶であり、昼であり、丁寧であり、端的である」みたいな。こういう風に1つの値をたくさんの数値の集合で表現できるのって、N次元のグラフに座標を打つってことになると思うんだけど(X座標、Y座標をそのままZ、次、次...と広げるイメージ)、それがベクトルって呼ばれるやつです。

そしたら、「こんばんは」は昼である以外が一致しているので相当似ているよね。一方で「PHP」は、まったく似ていない(名詞だけ)ので、相当類似度が低い。そこまで来ると数学の問題になるので、コサイン類似度っていう、一般にベクトルの近さを計算できる手法が使えるので、プログラミングフレンドリーになって幸せ。

以下のページがかなりちゃんとした説明なので読むとよさそう

https://zenn.dev/umi_mori/books/llm-rag-langchain-python/viewer/rag-flow

meijinmeijin

【Lv. 1】(WIP)モデルとパラメータを吟味する

かけられる時間の要件や、求められる精度、費用面などによって適切なモデルを選ぶ。最初から何が適切かなんて分からないことの方が多いので、一通り試すのが大事。

無印とminiとturbo、そしてo1などのバージョン(って呼ぶのかな)の組合せを漏れなく考え、試しておく。
慣れてくると期待値が見えてくるので、そうなると毎回総当りしなくてもよくなる。

あとはtemperatureといったある程度精度と時間をコントロールできるパラメーターも含めて検証する。