はじめに
こんにちは
abc.osaka とかで色々書いたりしてるS高生(3期生)です
普段ウェブサイトを作っていて皆さんもこう思ったことがあるはずです
最近AIが流行ってるのか..。 なんか入れてみたいな。
こうなったとき、どうすればいいのかをコードを交えて書こうと思います
軽く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を発行する
上の緑で囲われているボタン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!を実行しましょう
/**
* @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.log(
await getGroqChatCompletionTextWrapper(
getGroqChatCompletion({
messages: [
{ role: "user", content: "こんにちは!、初めまして!" }
]
})
)
)
非常にシンプルですね?
リアルタイムで返してもらう
さて、ここで普段ChatGPTなどを使ってる人は疑問に思ったかもしれません。 ChatGPTなどは返答がリアルタイム、言い方を変えると徐々に回答ができていきます。ところがこれは一度に結果が帰ってきています。 理由は、一度に帰ってきたほうが処理処理がしやすいからですが、もちろん徐々に返してもらうこともできます/**
* 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;
}
getGroqChatCompletionStreamWrapper(
getGroqChatCompletion({
stream: true,
messages: [
{ role: "user", content: "こんにちは!、初めまして!"}
]
}),
(res) => {
console.log(res)
}
);
会話の履歴と会話に参加する人物
勘のいい人ならお気づきかと思いますが、AIにプロンプトを渡すとき、以下のようなmessages
という配列で渡します。
[
{role: "system", content: "あなたはアシスタントです。"},
{role: "user", content: "こんにちは"}
]
みたいな感じです。これが所謂会話の履歴です。
会話の履歴
更にgetGroqChatCompletion
函数の返す値をよく見てみると、以下の様に帰ってくる事がわかります
{role: "assistant", content: "はじめまして。なにかお手伝いできることはありますか?"}
つまり、これを次の会話でAI渡してあげると、AIが文脈を把握し、より適切な会話ができるようになります。
ところでrole
とはなんでしょうか?
会話に参加する人物
role
は直訳すると役割
です。それはそのままの意味で、特に説明することは無いのですが、面白い事にuser
とassistant
以外にも様々な役割があります。
役割 | 機能 |
---|---|
user | ユーザー |
assustant | AI |
system | AIに指示を出したりするときに使う |
tool | AIを補助する |
ざっとこんな感じです。
キャラ設定をつけるときにsystem
を使ってAIに設定通り動くよう命令するっと言った使い方です。
tool
に関してはTool Use
と呼ばれる機能で使うのですが、残念ながらここに書くには余白が少なすぎるので、詳しくは次のセクション、天気を教えて!を御覧ください
画像を与えてみる
上の例だとテキストだけですが、画像をLLMに与える事もできます
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)
}
)
使い方は簡単ですが、少し制約があります
-
model
はllama-3.2-90b-vision-preview
、またはllama-3.2-11b-vision-preview
のみ -
{ role: "system" }
は使えない - 使える画像は1枚のみ
- 画像は最初に入れなければならない
少し面倒です、特に{ 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
コンソールの使い方
必要ないかもしれませんが、一応説明しておきます
ログインした状態でコンソールを開いている前提です
使い方
0. chatとstudio
緑の枠で囲まれているところの上にChat
とStudio
を切り替えるところがあります
chat
のところは普通にチャットするだけなので、特に説明するところもなく、うん。って感じなのでStudio
を選びます
1. モデルを変える
モデルは黄色のところから選べます。
一応モデルについて簡単に説明しておくと、モデルは一言で言うと誰に頼むかです。お金が多くかかる代わりに性能の高いものを使うか、あまり能力は必要としない単純作業だから、一番安いものを使う、とか選べます。
おすすめはgemma2-9b-it
です
性能的にはllama3-groq-70b-8192-tool-use-preview
が一番高いと思います
2. プロンプトを入力する
入力したいプロンプトを画像でいう緑のふちのところに書きます
3. 必要に応じてパラメーターを調整する
パラメーターを調整することはあまりないですが、よく使う項目はStream
とJSON Mode
です。
それ以外はほぼ使わなくて良いと思います。
パラメーター | 効果 |
---|---|
Temperature | どの程度ランダムな結果を返すか。0にすると同じプロンプトには同じことしか返ってこなくなる |
Max Tokens | AIが返せる最大の文章量、日本語の場合は大体1文字1トークンなので1024にすると、最大1024文字で返す |
Stream | 文章を細々と返すかどうか |
JSON Mode | 結果をテキストでは無くJSONで返すかどうか |
4. 実行する
画像の青緑色で囲まれたボタンを押します
5. コードとして書き出す
画像の紫色で囲まれたボタンを押すと、良い感じにコードを表示してくれます。
Curl
, Python
, Javascript
, JSON
に対応してます。
プロンプトを試してみる
色々と説明しても良いのですが、百聞は一見に如かず、まずは見てみましょう
Persona:
Description: あなたは与えられた単語を基に短い小論文を書きます。
Instructions:
- 400字程度で書いてください
- それに関連するいくつかのアイデアを出してください
Output_format: 400字程度の文章で、最後にいくつかの関連する分野や、創造的なアイデアを提示する
Examples:
- Word: 大規模言語モデル
- Solution:
LLM(大規模言語モデル)は、膨大なデータを学習し、自然言語の理解や生成を行う人工知能の一形態です。例えば文章の生成、翻訳、要約など、多岐にわたるタスクをこなすことができます。
しかし、LLMにはいくつかの課題も存在します。まず、膨大な計算資源とデータを必要とするため、環境への影響が懸念されています。さらにハルシネーションなど、誤った情報を生成する可能性があります。
それでも、LLMは今後も進化を続け、さまざまな分野で新たな価値を生み出すことが期待されます。
RAG (検索拡張生成): 事前に用意した資料を基にLLMに回答させてハルシネーションを低下させる技術
プロンプトエンジニアリング: LLMへの命令方法を変えて、よりLLMのパフォーマンスを向上させる技術
レビューの要約: ユーザーが投稿した大量のレビューをジャンル毎に要約してユーザーの声を把握しやすくします
アシスタント: 天気を教えたり、TODO Listにやることを追加したり、ユーザーの興味がある分野のニュースを要約して教えたり、ユーザーの生活をサポートします
Task:
- Word: {INPUT}
こんな感じです。
YAMLを使っている理由
最近発表された論文Does Prompt Formatting Have Any Impact on LLM Performance?によると
普通のテキストやマークダウンで渡すよりもYAML
で渡したほうが、高いスコアが出ているためです。JSON
を採用しなかったのはモデルによってばらつきが大きいので、中間を取った形です。
もちろん言語によっても変わるので一概には言えませんが。
僕はプロンプトエンジニアリングの分野は詳しくないので、そこら辺は自分で調べてみることをおすすめします
( じゃあ何でこの章を作ったんだって話だけど、重要だと思ったんだもん )
作ってみる
さて、閑話休題いよいよ実際に作ってみようと思います。 使用する言語やフレームワークは好きなものを使用していただければよいのですが、この記事では簡単のためNext.jsを使用します。
此処から先に出てくるコードは、大体の流れがつかめるように書いているので、なんの説明もなく突然コンポーネントやフック、関数ができてます。 触ってみたい場合はデプロイされているリンクから、コード全体を見たい場合はgithubのリンクに飛んでください
なぜかって言うと、流石に長くなりすぎるから
チャットしたい
まずは王道のチャットボットから作ってみたいと思います
とは言っても、さっきのを繋ぎ合わせるだけです
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
です
試しにやってみましょう
/**
* @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.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はただチャットすることしかできませんが、この機能があれば、タイマーを設定したり、ニュースを関連付けて話したりなど何らかの機能を提供することができるようになります。
終わりに
いかがでしたか?
もしこの記事が役に立ったなら、あー役立つ記事があったなーとでも思っていてください。
ぜひ、みなさんも何か作ってみてください。 すでに同じものがあったとしても、同じものを作り始めても、同じものはできませんから。
ではまた、何時かのアドベントカレンダーでお会いしましょう