8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

N高グループ・N中等部Advent Calendar 2024

Day 1

とりあえずAIを使ったサイトを作りたい時に見る記事

Last updated at Posted at 2024-11-30

はじめに

こんにちは
abc.osaka とかで色々書いたりしてるS高生(3期生)です

普段ウェブサイトを作っていて皆さんもこう思ったことがあるはずです

最近AIが流行ってるのか..。 なんか入れてみたいな。

こうなったとき、どうすればいいのかをコードを交えて書こうと思います

はじめに

この記事ではやること

  • LLMの得意な事やユーケースの説明
  • AIとの会話とその準備
  • 実際にアプリの作成

必要かもしれない予備知

  • 基礎的なHTML
  • 基礎的なJavascript

ぶっちゃけ分からなくてもコピペでも動きます

この記事のnoteとdiff
  • ノートは緑は参考程度、黄色は見といた方がいいかも、赤は絶対見といた方がいい、と言う感じです
  • コードのハイライト ( diffの緑 )は見といた方がいいという感じで使ってます

軽くAIについて分析

何事も既にあるものを知るところからがスタートです

AIは何が得意で何が苦手なのか

ここではある用途にフィットするように作られているAIは考えませんが
一般的には大まかに分けると次のようになります

得意なこと 苦手なこと
文章の要約 数学
与えられた文章から回答 推論
日常的な会話 学術的な会話

つまり、AIは考えることが苦手なのです。 数学の問題について話しても頓珍漢なことを言いますが、作者の気持ちを答えるのには長けているのです

数学や論理的な推論ができるAI

最近は一部ですが、数学の問題を解けたり、学術的な会話をしたり与えられた文章から推論することもできます

代表的なものに、数学の幾何学を得意とするGoogleのAlpha Geometoryなどがあります。

汎用性の高いものだとOpen AIのo1 Preview高い性能を出しています

その他コーディングに特化したStar Coderがあります。 Github Copilotも其の一種です。

どんなところで使われているのか

AIが使われている場所はどんどん増えていますが、それに追いつこうと、世間が言うほど焦る必要はありません。 ざっと並べますから、さらっと目を通すくらいでいいでしょう。 これからAIを使ったアプリケーションを作ろうとしているのであればじっくり見てみてください。

すること 詳細 実用例
カスタマーサポート 質問に答える Chat Plus, ZenDesk, Deca
要約 文章の要点を抜き出す NoteBookLM, YOMEL
文章生成 Catchy, 3秒敬語
カテゴリ化 コメントの分析など Youtube Comment Topic
アイデア出し Miro Ai, ひらめきAIメーカー
ウェブ開発 APIやサイト作成 Hanabi, v0
検索 検索してその結果を要約 Arc Search, ChatGPT search
総合 アイデア出し、要約、 その他 Notion Ai, Figma Ai

後はこれらを参考に実際に手を動かして作ってみましょう!

AIの可能性

この表を見るとAIを使ったサービスが数えきれないくらいあるので無限のことができそうですが実際にはやっていることは概ね同じことです。
ちなみに、僕の作ったAi32だってやっていることは要約と文章生成だけです。

つまりAIは、特に一般人に至ってはアイデア勝負です。
ぜひあなたも新しいサービスを作ってみてください!
AI占い師、なんてどうですか?

とりあえず使ってみる

とりあえず入れてみるとは言ったものの、何を使うか迷うと思います。
世はまさにLLM戦国時代、ChatGPT? Gemini? Claude?数々のLLMのAPIが乱立しています
この記事を読んでいる人はシンプルなものを求めていると思うので、今回はGroqを使おうと思います

なぜGroq?

凝ったことをしたいなら既にある程度手法が確立しているAzure OpenAI Serviceか、少なくともOpen AI系列を使うことになりますが、今回はあくまでも5分でできることを目標に書いてるのでGroqを使います

LLMを初めて使うのであったり、簡単なことをするには良い選択だと思います

API KEYの発行方法

1. とりあえずログインする

https://console.groq.com/login

上のリンクに飛べばログインページにいけます

2. api keyを発行する

スクリーンショット 2024-11-28 2.37.31.png

https://console.groq.com/keys

上の緑で囲われているボタンCreate Keyを押して適当に名前をつければKeyが貰えます

リミットについて

Groqの無料枠は多いです。 ( そもそも従量課金のプランがComing Soonでまだありません )。ですので、個人で普通に常識の範囲で使う分にはあまり困ることはないでしょう。

リクエスト数 トークン数
1分 30 req 7,000 - 15,000 token
1日 7,000 - 14,400 req 500,000 token
token数について

tokenは端的に言えば文章の量です、文章が増えればtoken数が増えますし、逆もしかりです。
英語の場合は単語数とほぼ同一ですが、日本語の場合は少し複雑ですが1文字1tokenとすればよいでしょう。

  • 普通に会話するだけなら1回100token程度 ( 設定付きなら300token )
  • しっかりと作り込んで何らかのタスクをこなさせたいなら2,000-4,000token程度
  • 普通の記事程度の分量を書かせたいなら数万tokenは必要でしょう

ただし、1回の会話が100程度と言っても、それはやり取りの中で最初の1回だけです。 理由はLLMにこれまでの会話をすべて渡す必要があるからです。 途切れ途切れでいいなら毎回100tokenでいいですが、多くの人は明日遊ぶ予定を考えてるときに、急に仕事の話をされるのは嫌でしょう。その場合は25回のやり取りで12000token程です。

Hello, world!

さてとりあえずhello, world!を実行しましょう

grpq/groq.js
/**
 * @param {string} content
 * @returns string
 */
const getGroqChatCompletion = async (body) => {
    const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
        method: "POST",
        headers: {
            /**
             * API_TOKENに発行したapi keyを入れる
             * ```
             * const API_KEY = "gsk_XXXXXXXXXXXXXXXXXXXXXXXXX"
             * ```
             */
            Authorization: `Bearer ${API_KEY}`,
        },
        ContentType: "application/json",
        body: JSON.stringify({
            /**
             * モデル一覧
             * @see https://console.groq.com/docs/models
             */
            model: "gemma2-9b-it",
            max_tokens: 4096,

            ...body,
        })
    })
    return res
}

/**
 * @param {Response | Promise<Response>} res
 * @returns string
 */
const getGroqChatCompletionTextWrapper = async (res) => {
    const json = await (await res).json()
    /**
     * 一番最初の回答を取り出して返す
     */
    return json.choices[0].message.content
}
console
console.log(
    await getGroqChatCompletionTextWrapper(
        getGroqChatCompletion({
            messages: [
                { role: "user", content: "こんにちは!、初めまして!" }
            ]
        })
    )
)

非常にシンプルですね?

リアルタイムで返してもらう さて、ここで普段ChatGPTなどを使ってる人は疑問に思ったかもしれません。 ChatGPTなどは返答がリアルタイム、言い方を変えると徐々に回答ができていきます。ところがこれは一度に結果が帰ってきています。 理由は、一度に帰ってきたほうが処理処理がしやすいからですが、もちろん徐々に返してもらうこともできます
stream.js
/**
  * chunk format:
  * data: {"id":"XXXXXXXXXXXXXX","object":"chat.completion.chunk","created":1700000000,"model":"gpt-3.5-turbo-0613","choices":[{"index":0,"delta":{"content":"\n\n"},"finish_reason":null}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
  * 
  * 上記の様に帰ってくるので、それをテキストにして返す。良い方法が見つからなかったので適当に
  */
/**
  * 
  * @param {string} chunk 
  * @returns 
  */
const parseChunk = (chunk) => 
    chunk
        .split("data: ")
        .map((text) => text.replaceAll("\n", ""))
        .filter((text) => text.startsWith("{\"id\":"))
        .filter((text) => text.endsWith("}"))
        .map((text) => JSON.parse(text))
        .map((json) => json.choices[0].delta.content)
        .join("")

/**
 * 
 * @param {Response | Promise<Response>} res 
 * @param {(parsedText: string)=> void} event 
 * @returns {Promise<string>}
 */
const getGroqChatCompletionStreamWrapper = async (res, event) => {
    const reader = (await res).body.getReader();
    const decoder = new TextDecoder("utf-8");

    /**
     * @type {string}
     */
    let content;

    while (true) {
        const { done, value } = await reader.read();
        const chunk = decoder.decode(value, { stream: true });

        const parsed = parseChunk(chunk);
        event(parsed)

        content += parsed;
        if (done) break;
    }
    return content;
}
console
getGroqChatCompletionStreamWrapper(
    getGroqChatCompletion({
        stream: true,
        messages: [
            { role: "user", content: "こんにちは!、初めまして!"}
        ]
    }),
    (res) => {
        console.log(res)
    }
);
会話の履歴と会話に参加する人物

勘のいい人ならお気づきかと思いますが、AIにプロンプトを渡すとき、以下のようなmessagesという配列で渡します。

messages
[
    {role: "system", content: "あなたはアシスタントです。"},
    {role: "user", content: "こんにちは"}
]

みたいな感じです。これが所謂会話の履歴です。

会話の履歴

更にgetGroqChatCompletion函数の返す値をよく見てみると、以下の様に帰ってくる事がわかります

response.choices[0]
{role: "assistant", content: "はじめまして。なにかお手伝いできることはありますか?"}

つまり、これを次の会話でAI渡してあげると、AIが文脈を把握し、より適切な会話ができるようになります。

ところでroleとはなんでしょうか?

会話に参加する人物

roleは直訳すると役割です。それはそのままの意味で、特に説明することは無いのですが、面白い事にuserassistant以外にも様々な役割があります。

役割 機能
user ユーザー
assustant AI
system AIに指示を出したりするときに使う
tool AIを補助する

ざっとこんな感じです。
キャラ設定をつけるときにsystemを使ってAIに設定通り動くよう命令するっと言った使い方です。

toolに関してはTool Useと呼ばれる機能で使うのですが、残念ながらここに書くには余白が少なすぎるので、詳しくは次のセクション、天気を教えて!を御覧ください

画像を与えてみる

上の例だとテキストだけですが、画像をLLMに与える事もできます

console
getGroqChatCompletionStreamWrapper(
    getGroqChatCompletion({
        stream: true,
        messages: [{
            role: "user",
            content: [
                { type: "text", text: "この画像みてどう思う?" },
                { type: "image_url", image_url: { url: "https://upload.wikimedia.org/wikipedia/en/a/a9/Example.jpg" } }
            ]
        }],
        model: "llama-3.2-90b-vision-preview"
    }),
    (event)=> {
        console.log(event)
    }
)

使い方は簡単ですが、少し制約があります

  1. modelllama-3.2-90b-vision-preview、またはllama-3.2-11b-vision-previewのみ
  2. { role: "system" }は使えない
  3. 使える画像は1枚のみ
  4. 画像は最初に入れなければならない

少し面倒です、特に{ role: "system" }が使えないというのは命令するときに少し厄介ですが、まぁ問題はありません。

コード: /app/chat-vision/page.tsx
デモ: https://qsla.i32.jp/chat-vision

画像をユーザーに入力させる場合 LLMは全く関係ないですが、JSで画像をURLに変換する方法を少し

LLMは画像をurlでしか受け付けないですが、そのurlは別にbase64でいいのです。なので別にサーバーを立てて、そこに画像を保存するとかする必要はありません

/**
 * @param {File} file 
 */
const convertFileToBase64Url = (file) => {
    const reader = new FileReader();
    reader.readAsDataURL(file);
    return new Promise((resolve, reject) => {
        reader.onload = function () {
            const url = reader.result;
            resolve(url)
        };
    })
}

別にreactじゃなくてもいいですが、reactの場合はこんな感じ

<Input type="file" onChange={async (e) => {
    if (e.target.files && e.target.files.length > 0) {
        const file = e.target.files[0];
        const url = await convertFileToBase64Url(file)
    }
}}/>

プロンプトエンジニアリングってなーに?

少し話が脱線しますが、これも重要なので。
みなさんプロンプトエンジニアリングについてご存知でしょうか

端的に言うとAIにやってほしいこと指示するための文章です

人に頼む時とやることはほとんど同じでですが、人に頼む時と違って、AIに頼むときは明らかに結果が変わってきます。 ただ単に命令するのでは無く、どんな感じにすれば良いのかサンプルを渡すなどの工夫ですが、まずはあまり深く考えずに、早速やってみましょう

効率の良いプロンプトの試し方 プロンプトを試すときに、普通にいちいちコードを変更していては手間がかかります そこで簡単に試せる環境が欲しいのですが、Groqの場合は専用のコンソールがあります。

https://console.groq.com/playground

Open AIにも同様のコンソールがあります
https://platform.openai.com/playground

コンソールの使い方

必要ないかもしれませんが、一応説明しておきます

ログインした状態でコンソールを開いている前提です

スクリーンショット 2024-11-11 23.17.42.png

使い方

0. chatとstudio

緑の枠で囲まれているところの上にChatStudioを切り替えるところがあります
chatのところは普通にチャットするだけなので、特に説明するところもなく、うん。って感じなのでStudioを選びます

1. モデルを変える

モデルは黄色のところから選べます。

一応モデルについて簡単に説明しておくと、モデルは一言で言うと誰に頼むかです。お金が多くかかる代わりに性能の高いものを使うか、あまり能力は必要としない単純作業だから、一番安いものを使う、とか選べます。

おすすめはgemma2-9b-itです
性能的にはllama3-groq-70b-8192-tool-use-previewが一番高いと思います

2. プロンプトを入力する

入力したいプロンプトを画像でいう緑のふちのところに書きます

3. 必要に応じてパラメーターを調整する

パラメーターを調整することはあまりないですが、よく使う項目はStreamJSON Modeです。
それ以外はほぼ使わなくて良いと思います。

パラメーター 効果
Temperature どの程度ランダムな結果を返すか。0にすると同じプロンプトには同じことしか返ってこなくなる
Max Tokens AIが返せる最大の文章量、日本語の場合は大体1文字1トークンなので1024にすると、最大1024文字で返す
Stream 文章を細々と返すかどうか
JSON Mode 結果をテキストでは無くJSONで返すかどうか

4. 実行する

画像の青緑色で囲まれたボタンを押します

5. コードとして書き出す

画像の紫色で囲まれたボタンを押すと、良い感じにコードを表示してくれます。
Curl, Python, Javascript, JSONに対応してます。

プロンプトを試してみる

色々と説明しても良いのですが、百聞は一見に如かず、まずは見てみましょう

system
Persona:
    Description: あなたは与えられた単語を基に短い小論文を書きます。
Instructions:
    - 400字程度で書いてください
    - それに関連するいくつかのアイデアを出してください
Output_format: 400字程度の文章で、最後にいくつかの関連する分野や、創造的なアイデアを提示する
    
Examples:
    - Word: 大規模言語モデル
    - Solution:
        LLM(大規模言語モデル)は、膨大なデータを学習し、自然言語の理解や生成を行う人工知能の一形態です。例えば文章の生成、翻訳、要約など、多岐にわたるタスクをこなすことができます。
        しかし、LLMにはいくつかの課題も存在します。まず、膨大な計算資源とデータを必要とするため、環境への影響が懸念されています。さらにハルシネーションなど、誤った情報を生成する可能性があります。
        それでも、LLMは今後も進化を続け、さまざまな分野で新たな価値を生み出すことが期待されます。

        RAG (検索拡張生成): 事前に用意した資料を基にLLMに回答させてハルシネーションを低下させる技術
        プロンプトエンジニアリング: LLMへの命令方法を変えて、よりLLMのパフォーマンスを向上させる技術

        レビューの要約: ユーザーが投稿した大量のレビューをジャンル毎に要約してユーザーの声を把握しやすくします
        アシスタント: 天気を教えたり、TODO Listにやることを追加したり、ユーザーの興味がある分野のニュースを要約して教えたり、ユーザーの生活をサポートします
user
Task:
    - Word: {INPUT}

こんな感じです。

YAMLを使っている理由

最近発表された論文Does Prompt Formatting Have Any Impact on LLM Performance?によると

普通のテキストやマークダウンで渡すよりもYAMLで渡したほうが、高いスコアが出ているためです。JSONを採用しなかったのはモデルによってばらつきが大きいので、中間を取った形です。
もちろん言語によっても変わるので一概には言えませんが。

僕はプロンプトエンジニアリングの分野は詳しくないので、そこら辺は自分で調べてみることをおすすめします
( じゃあ何でこの章を作ったんだって話だけど、重要だと思ったんだもん )

作ってみる

さて、閑話休題いよいよ実際に作ってみようと思います。 使用する言語やフレームワークは好きなものを使用していただければよいのですが、この記事では簡単のためNext.jsを使用します。

此処から先に出てくるコードは、大体の流れがつかめるように書いているので、なんの説明もなく突然コンポーネントやフック、関数ができてます。 触ってみたい場合はデプロイされているリンクから、コード全体を見たい場合はgithubのリンクに飛んでください

なぜかって言うと、流石に長くなりすぎるから

チャットしたい

まずは王道のチャットボットから作ってみたいと思います

とは言っても、さっきのを繋ぎ合わせるだけです

chat/page.tsx
export default function() {
    
    const [response, setResponse] = useState("どうしたの? ごしゅじんさまぁ")
    const { push } = useChatMessages([
        { role: "system", content: "あなたは...、何でしょうね。少なくとも哲学は好きなのかも" }
    ])

    const send = (input: string)=> {

        const history = push([
            /**
             * 前回の回答を送信する。初回は事前に用意した回答
             */
            { role: "assistant", content: response },
            { role: "user", content: input }
        ])

        setResponse("")

        getGroqChatCompletionStreamWrapper(
            getGroqChatCompletion({
                stream: true,
                messages: history,
            }),
            (value) => setResponse((r) => r.concat(value))
        )

    }
    return <ChatContainer>
        <Alert className="mb-2">
            <Info className="h-4 w-4" />
            <AlertTitle className="text-lg">せつめい</AlertTitle>
            <AlertDescription>
                これ以上ないほどシンプルなLLMを使ったアプリ
            </AlertDescription>
        </Alert>
        <Chat response={response} onSend={send} />
    </ChatContainer>
}

簡単ですね?

天気予報を教えて!

さて、ここまで会話するだけでしたが。少しつまらないので天気を教えてもらおうと思います。 とは言ったものの、今までの延長線で天気を教えてもらおうとすると、プロンプトに天気の情報を渡す必要が出てきます。しかし、いちいち世界中の天気を渡すことは不可能に近いですし、会話の中で必要になるかすらわかりません。なのでもし会話の中で天気の情報が必要になったら、AIからシステムに尋ねて貰えば良いのです。

そのための機能がTool Useです

試しにやってみましょう

groq/tool.js
/**
 * @typedef Tool
 * @property {string} type ほぼ`"function"`固定
 * @property {object} function
 * @property {function} function.function 実行したい関数
 * @property {string} function.name 関数の名前
 * @property {string} function.description 関数の説明
 * @property {object} function.parameters 関数の引数
 * @property {string} function.parameters.type `object`とか、他は知らないけどあるかも
 * @property {object} function.parameters.properties
 * @property {string[]} function.parameters.required 絶対に必要なやつ
 */

import { getGroqChatCompletion } from "./groq"

/**
 * @param {Array<Tool>} tools
 * @param {*} messages
 */
const getGroqChatCompletionWithTools = async (tools, messages) => {
    /**
     * ツール(関数)の実行結果や、LLMが要求したツールを保存するところ。
     * 最後にその会話履歴をLLMに渡すと結果が生成される
     */
    const resultMessage = messages.concat()

    /**
     * LLMにツールを渡して、何を使うのか聞く。
     */
    const toolUseRes = await getGroqChatCompletion({
        messages,
        tools: tools
            .concat()
            /**
             * .map(tool) => {
             *     delete tool.function.function
             *     return tool
             * })
             * と同じ ( 参照元も一緒に消されたので仕方なく下みたいな感じにした )
             */
            .map((tool)=> ({
                type: tool.type,
                function: {
                    name: tool.function.name,
                    description: tool.function.description,
                    parameters: tool.function.parameters
                }
            })),
        tool_choice: "auto",
    })
    const toolUseResJson = await toolUseRes.json()

    /**
     * LLMから帰ってきた使うツール一覧をとりあえず会話履歴にいれる
     */
    const toolUseResJsonMessage = toolUseResJson.choices[0].message
    resultMessage.push(toolUseResJsonMessage)

    /**
     * 使うツールのリスト
     */
    const toolCalls = toolUseResJsonMessage.tool_calls || []

    for(const tool of toolCalls) {
        const toolName = tool.function.name // 使う関数の名前
        const toolArgs = JSON.parse(tool.function.arguments) // 関数の引数

        const useTool = tools.find((tool) => tool.function.name === toolName) // 名前から関数を取り出す
        const toolFunction = useTool.function.function

        /**
         * 使うツールを管理するためにGroqから渡されたid
         */
        const toolId = tool.id

        /**
         * ツールを実行する
         */
        const toolResult = await toolFunction(toolArgs)

        /**
         * 会話履歴に実行結果を入れる
         */
        resultMessage.push({
            tool_call_id: toolId,
            role: "tool",
            name: toolName,
            content: JSON.stringify(toolResult)
        })
    }

    /**
     * 必要なツールの一覧と、そのツールの実行結果が含まれている会話履歴を返す
     */
    return resultMessage
}

export {
    getGroqChatCompletionWithTools
}
console
console.log(
    await getGroqChatCompletionTextWrapper(
        getGroqChatCompletion({
            stream: true,
            messages: await getGroqChatCompletionWithTools(
                [
                    {
                        type: "function",
                        function: {
                            name: "get_weather_forecast",
                            description: "都市IDから天気予報を取得する",
                            parameters: {
                                type: "object",
                                properties: {
                                    locationId: {
                                        type: "string",
                                        description: "都市のID, 大阪府: 270000, 東京都: 130010, その他: 230010",
                                    },
                                    dateLabel: {
                                        type: "string",
                                        description: "today or tomorrow"
                                    }
                                },
                                required: ["locationId", "dateLabel"],
                            },
                            function: async (args: { locationId: string, dateLabel: string }) => {
                                const forecast = await getForecastSimple(args.locationId, args.dateLabel as any)
                                return JSON.stringify(forecast)
                            }
                        },
                    }
                ],
                [
                    { role: "system", content:"あなたは天気を伝えることしかできません。あなたは天気のことを話さずにはいられません。"},
                    { role: "user", content: "大阪の天気教えて" }
                ]
            ),
        }),
    )
)

コード: /app/chat-with-tool/page.tsx
デモ: /chat-with-tool

天気予報を取得する関数

今回は天気予報を取得するのに

export const getForecast = async (locationId: string) => {
    const res = await fetch(`https://weather.tsukumijima.net/api/forecast/city/${locationId}`)
    const json = await res.json()
    return json
}

export const getForecastSimple = async (locationId: string, date: "today" | "tomorrow") => {
    const json = await getForecast(locationId)
    const index = date == "today" ? 0 : 1
    return {
        text: json.description.text,
        forecast: {
            detail: json.forecasts[index].detail,
            temperature: json.forecasts[index].temperature,
            telop: json.forecasts[index].telop,
        },
    }
}

えぇ、なんか、思ってたよりも仰々しくなりましたが、やってることはシンプルです ( 多分 )
図にすると

とてもシンプルですね!
ですがこの機能はとても強力です、普通ならLLMはただチャットすることしかできませんが、この機能があれば、タイマーを設定したり、ニュースを関連付けて話したりなど何らかの機能を提供することができるようになります。

終わりに

いかがでしたか?
もしこの記事が役に立ったなら、あー役立つ記事があったなーとでも思っていてください。

ぜひ、みなさんも何か作ってみてください。 すでに同じものがあったとしても、同じものを作り始めても、同じものはできませんから。

ではまた、何時かのアドベントカレンダーでお会いしましょう

8
1
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
8
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?