簡易的なAIによる文章補完エディタを作ってみた
はじめに
コーディング時にはお馴染みとなったAIによる補完体験ですが、コーディング以外でブログ記事やドキュメントにおいても欲しいなと思う人は少なくないと思います。
補完機能がどのように実現できるか個人的に興味があったので、簡易的なものですが作ってみました。(使いものになっているかはさておき😌)
今回は、簡単にエディタの紹介をさせていただきたいと思います。
リポジトリは公開しています。ぜひ、補完文字生成のプロンプトを変更したり、パラメータを調整してしてお試しください。(「自分はこうした」みたいなフィードバック大歓迎です🎉)
デモ
次のキャプチャは、microCMSの拡張フィールドにCloudflare Pagesでホストしたエディタを設定して、利用してみた様子です。
一応、それらしい内容を補完している様子がお分かりいただけるかと思います。ただし、うまく補完文章を生成してくれないこともあります。
ゴーストテキスト(斜体の薄色の文字) で表示し、タブキーを押すと補完内容を受け入れる処理 としています。
また、ゴーストテキストが表示された後に、引き続き文字を入力すると、ゴーストテキストを削除(受け入れなかったと解釈)します。
補完以外の機能
補完のON/OFF切り替え
補完は不要、のんびり気ままに書きたい時はOFFにしましょう。
補完文字のモード切り替え
お試しで、「一般的な文章向け」と「技術的な文章向け」を用意しています。
補完文字の生成用プロンプトの内容が若干異なります。また、パラメータも多少変えています。
今回は同じモデルを用いていますが、ライティングする内容に応じて、モデルを変えてみるとよりよい補完が得られるかもしれません。
microCMSの拡張フィールドで利用 & 連携ができているかのステータス
こちらはおまけです。
microCMSの拡張フィールドに設定すると、管理画面で表示された際にフィールドの初期値と連携のための識別子が送られてきます。
その識別子が受け取れたか否かで判定しています。
連携中 | 未連携 |
---|---|
![]() |
![]() |
アプリケーションの構成
非常にシンプル🍰で、Cloudflare Workers & Pagesで完結します。
- Cloudflare Workers AI
- 入力文を補完する文章を生成
-
Hono
- フロントエンド実装(主にエディタのUI、入力制御)
- バックエンド実装(主に補完文字をレスポンスするWebAPIのエンドポイント)
補完文字生成から、エディタへの差し込み・確定まで
おおまかに流れを説明します。
- フロントエンドで入力イベントを監視
- Debounce制御して、一定時間入力がなければ補完リクエストを実行
- レスポンス内容をテキストカーソル(caret)位置に差し込み
- タブキー押下で確定 / ESCキー押下、その他操作が行われたら取り消し
一見シンプルですが、エディタでは入力が断続的に行われるため、補完リクエストの実行タイミングやキャンセルなど考慮すべき点が多いです。
特に、無駄な補完リクエストを抑えることとトークン数の浪費は気にしておきたいところです。
次に、それらについて気をつけた点に触れたいと思います。
実装時に気をつけた点
無駄な補完リクエストを抑える
たとえば、文章の書き出しすぐ(文字数が少ない状態)で補完が必要な場面は少ないと思います。あとは、末尾が句点といった終止符の場合も挙げられます。
以上を踏まえて、次の条件に当てはまる場合は補完リクエストを実行しないようにしています。
- スペースといった空白文字でのみ構成されている
- 5文字未満
- 末尾が句点といった終止符である
補完リクエストをトリガーするタイミング
入力イベントといっても、IMEの変換中か、変換が伴わない文字入力か、ペーストあるいはバックスペースによる文字削除などさまざまです。
すべてのイベントで補完リクエストをトリガーするのは過度と考え、次のイベントに限定しました。
-
テキストを入力した時(IMEの変換を伴わないもの)
-
InputEvent
のinuptType
がinsertText
でハンドリング
-
-
IME入力の変換が確定した時
-
oncompositionend
イベントでハンドリング
-
バックスペースで削除された時やペーストされた時も考えましたが、不要と感じたので今回は除きました。
以下に、エディタのコード中から関連する箇所を簡略化したものを載せておきます。複雑なことをしているので、コメントのみで載せておきます。コードはリポジトリをご覧ください。
const Editor = () => {
const triggerCompletionByInputEvent = () => {
/*
* テキストカーソル位置より前の文字列を切り出すなどの文字列処理
*
* Debounce制御
* - setTimoutでタイマーをセット(時間が経てば、補完リクエストの実行)
* - もし、タイマーの時間より前に入力があれば、clearTimeoutでタイマーをリセット
* - 補完リクエストがすでに走っている場合も考慮して、AbortControllerでabort()実行
*/
};
const handleInput = (e: InputEvent) => {
if (e.inputType === "insertText") {
// 補完リクエストのトリガー
triggerCompletionByInputEvent();
}
};
const handleCompositionEnd = (_e: CompositionEvent) => {
// 補完リクエストのトリガー
triggerCompletionByInputEvent();
};
/*
* タブキー, ESCキー押下やテキストカーソルが外れた場合もありますが
* ここでは割愛します
* (onKeyDownとonBlurのイベントハンドラ中で制御)
*/
return (
<div
onInput={handleInput}
onCompositionEnd={handleCompositionEnd}
role="textbox"
contentEditable="plaintext-only"
aria-multiline="true"
/>
);
};
モデルへの入力トークン数を減らす
補完文字を生成する際に、記述された文章のすべてが必要ではありません。
次の文章を例に考えてみます。
テスト駆動開発(TDD)は、ソフトウェア開発においてテストを先に書く手法です。
まず失敗するテストケースを作成し、その後に最小限の実装を行い、テストを通過させます。
最後にリファクタリングを行いながら品質を保つことで、(※ここで入力が終わっている)
補完したいのは最後の入力途中の文章です。この場合に、モデルに入力する文章は最後の一文だけでよいと考えられます。
最後にリファクタリングを行いながら品質を保つことで、
また、エディタの入力内容だけでなく、モデルへの指示文も同様にトークンとしてカウントされるので、気にしておく必要があります。
Few-shotの例文も含めると、それなりのボリュームになってしまいます。
次のような例文もプロンプトに含めていたのですが、期待するような結果が得られなかったので、いっそのこと無くてよいかなと思ったので消してしまいました。
(もっとよいプロンプト設計があったのかもしれない😔)
# 入力と出力の例
## 補完が必要な例
入力: 今日は朝から雨が降っていて、気分も
出力: 落ち込みがちです。
入力: この映画はとても面白くて、ストーリーも
出力: 面白かったです。
## 補完が不要な例
入力: 今回の記事は、先週友人と川に行ったことを書こうと思います。
出力:
おわりに
以上、簡単ですが紹介させていただきました。
思いつきでAIによる文章補完エディタを作り始めたのですが、入力イベントの制御と補完リクエストのタイミング、さらにトークンの消費を考慮して実装しないといけないなど予想以上に大変でした。
補完文字の生成もなかなかに思うようにしてくれないので、プロンプトの調整は手こずるばかりです。ライティングする内容の分野を限定的にすることで、よりマッチする補完文字が得られる可能性もあると思います。今回は「一般的な文章向け」「技術的な文章向け」と抽象的なものだったので、カスタマイズしたい方はその点を工夫してみるとよいかもしれません。
リポジトリは公開していますので、興味がある方はクローンしてお試しください。
リポジトリで CHANGEME
という文字で検索をかけると、利用するモデルやパラメータ、プロンプトが探せます。
今回はCloudflare Workers AIを用いましたが、補完文字を得る部分の処理をOpenAI APIなどに置き換えることもできますので、ぜひカスタマイズしてみてください。
さいごに、今回作ったエディタは単体でも機能しますが、microCMSの拡張フィールドでも利用できますのでぜひお試しくださいまし。
Discussion