はじめに
背景
バイトの家庭教師で、高校3年生の生徒に大学受験を指導しています。
その生徒は英語の素養がある一方、長文の読解速度・精度に課題を抱えていました。
一般に大学受験における英語は出題傾向として、西日本は精読重視、東日本は速読重視といわれていましたが、現代は共通テストとかいうので全国民が 速読 の能力が必要になりました。
速読 の能力を底上げするにはとにかく 演習量 を通じて「問題で聞かれている箇所だけ目を通す」訓練を内面化することかと思います。
やったこと
演習量が欲しいので、今驚きのLLMを使って無限に英語読解問題を生成するサイト「EnglishExamForever」をサーバーからフロントまで一人で開発しました。
生徒思いの王、生徒思いキングですね。
動作動画はこんな感じです。
長文をつっこむと、パラグラフごとのTF問題(正誤判定問題)が生成されるってわけですね。
嬉しさとして
- 10題程度しかない1600円の問題集を積まなくともよい
- 質より量重視!
- 自分な好きな(あるいは苦手な)ジャンルの文章で読解問題ができる
- 生徒はHCI分野に興味があるというので、HCI系の文章を引っ張ってきて、読解させるついでに分野を紹介しています。(
そこらへんの高校の進路指導教員は大学入学以後について何も情報提供しないので)
- 生徒はHCI分野に興味があるというので、HCI系の文章を引っ張ってきて、読解させるついでに分野を紹介しています。(
- 文章の内容により難易度を調整しやすい
- めっちゃ難しくしたいならデカルトの「方法序説」がオススメです。
ちなみに生成はGoogleのGemini
です。
アプリ仕様
本質部分
ユーザー関係
不特定多数に生成されるとGemini
料金で破産するので、会員制にしています。
- ログイン機能
ページ遷移です。どこでもよくみるインターフェースですね。(AWS Cognito
のデフォルトですからね) - トークン制
維持費があり、貧困学生ごときが無料配布できるはずもないので、月額100円頂いています。
同人文化の 頒布 の体をとっています。
- Mosh自動化
トークン販売をMosh.jpにて行っておりますが、MoshからのメールをGAS
で自動検知してトークン発行し、購入ユーザーにメールを送っています。
業務効率化の王、業務効率化キングですね。
その他
効能
想定運用
想定している使い方としては、速読訓練の内面化を目指してほしく、
- 問題文を読み、必要な情報を念頭に入れ、
- その情報を検索するように出題文を走査する
という試験用の読解プロセスを体得してほしいのですね。(参考: 東大入試のノリで解いてデータベーススペシャリストを攻略した話)
なので、気持ちとしては、ストップウォッチのタイムが早ければ早いほど偉い(満点前提で) という気持ちでやってほしいなという。
質
問題の質ですが、割と本文そのままの内容が聞かれます。ここはプロンプトエンジニアリングの範疇かな?
例えば先ほどの画像の、
本文1行目が「Good sence is, of all things among men, the most equally distributed」とあり、
問題1問目が「Good sense is not equally distributed among men」
そのままですね。
一方で、Gemini
がちょっとした機転を利かせてくれるときもあります。
こちらは著作物なのでみだりに転載しませんが(デカルトのものは著作権フリー)、
本文「A, B, and then C」というセンテンス構成に対して
問題文が「B has proceeded A」
のように、andが等位接続詞として働いているのでAとBは並列、そしてthenがあるから時間的な前後関係がある、という文章理解を問うような問題を出してきました。
技術構成
フロントエンド
Vue.js
により静的サイトを作り、静的ならだいたい無料のCloudflare
でインターネッツに放出しています。
必要な情報はサーバーからAPI通信で動的に取得しているわけですね。
npm
アンチなので、SFC
構造を保ったままCDN
として記述しているので、デプロイはビルドせずにドラッグ&ドロップで済みます。
参考: CDN版Vue3で単一ファイルコンポーネントを作る
コンポーネントコードの例
<template>
<div class="ScoreDisplay">
<div>
<p>{{ scoreEarned }} / {{ scoreMax }}</p>
<p>{{ scorePercentage }}%</p>
</div>
</div>
</template>
<script>
export default {
name: "ScoreDisplay",
props: {
scoreMax: {
type: Number,
required: true,
},
scoreEarned: {
type: Number,
required: true,
},
},
setup() {
//set up i18n
const {t} = VueI18n.useI18n()
return {t}
},
computed: {
scorePercentage() {
let double = (this.scoreEarned / this.scoreMax) * 100
return double.toFixed(2)
},
},
}
</script>
<style>
.ScoreDisplay {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid #000;
border-radius: 0.5em;
padding: 0.5em;
margin: 0.1em;
background-color: rgb(254, 176, 202);
}
.ScoreDisplay p {
font-family: "Roboto", sans-serif;
font-weight: 300;
font-style: normal;
font-size: large;
margin: 0;
}
</style>
また、ログイン状態のセッション関係はAWS Cognito
に依拠していますが、これが結構メンドウだったのでそのうち記事書きます。
バックエンド
フロントエンドから直接ウチのGemini
を呼ばれては、APIキーがバレたり悪者に大量リクエストされたりで苦しいので、自分のサーバーでコントロールする必要があります。
貧困の王、貧困キングなので、AWS Lambda
のサーバーレスでいきます。
ちなみに、本アプリの構築をしながらこの記事を書きました:
【Go, AWS】低予算APIサーバー構築を最初から最後まで【Golang, Lambda, DynamoDB, API Gateway, Cognito】
まさにこの構成です。
ここで、Lambda
を使って、GCP
のGemini
を叩いているわけですね。
ちなみにGolang
でGemini
叩く日本語ソースなかった記憶があるので、ここに書いておきます
package LlmFetcher
import (
"context"
"fmt"
"os"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
func getApiKey() (string, error) {
apikey := os.Getenv("GEMINI_KEY")
if apikey == "" {
return "", fmt.Errorf("API key is empty")
}
return apikey, nil
}
//make a Google AI client
func initializeClient() (*genai.Client, error) {
ctx := context.Background()
apikey, err := getApiKey()
if err != nil {
return nil, fmt.Errorf("error getting API key: %v", err)
}
client, err := genai.NewClient(ctx, option.WithAPIKey(apikey))
if err != nil {
return nil, fmt.Errorf("error creating client: %v", err)
}
return client, nil
}
//Fetch response from the LLM
func FetchLlm(prompt string) (string, error) {
context := context.Background()
client, err := initializeClient()
if err != nil {
return "", err
}
//fetch
model := client.GenerativeModel("gemini-pro")
resp, err := model.GenerateContent(context, genai.Text(prompt))
if err != nil {
return "", fmt.Errorf("error generating content: %v prompt(%v)", err, prompt)
}
client.Close()
//get result
result := ""
for _, part := range resp.Candidates[0].Content.Parts{
//convert to string
_text, ok := part.(genai.Text)
if !ok {
continue
}
str := string(_text)
result += str
}
return result, nil
}
Geminiはマルチモーダルモデルで、レスポンスはテキスト+画像の配列として設計されているので、最後に一つのstring
にするという操作が必要ですね。メンドウ。
最後に
もっと細かい技術詳細は小分けな記事にして放流していきます。
この記事に「いいね」をくだされば、泣いて喜びます!!!!!!!!