🗒

【ChatGPT+Flutter】謝罪文とお礼文の生成機能を実装したのでやったことなど

2023/03/27に公開

弊社のアプリ「敬語翻訳」にChatGPTでお礼文と謝罪文を生成する機能を付けました。

ChatGPTで文章を生成しているスクリーンショット

敬語翻訳には文章を敬語の種類(尊敬語、謙譲語、丁寧語など)ごとに色分けする機能があるのですが、
ChatGPTから文字が出力されるたびに色分け処理をすることで、待ち時間も視覚的に楽しめるようになっていると思います。

ちなみに生成後は文字をタップすることで言い回しを変更できます。

文字を押して「ご不便をおかけし」を変更しているスクリーンショット

大まかに以下のような処理を行っています。

  1. 作りたい文章の要点を利用者に記入していただき、サーバーに送信
  2. サーバー側で ChatGPT API に投げる
  3. ChatGPT API から送られてくる文章をストリーミングでアプリ側に送る
  4. (以降アプリ側)mecabで形態素解析
  5. 誤った文章表現を置換
  6. アプリ内蔵の敬語辞書で色分け

サーバー側もDartで

弊社ではコード量が少ない場合に限り、試験的にサーバーサイドDartを採用しています。
今回サーバー側でやることは、ユーザー認証した後に ChatGPT API から送られてきた文章をそのままアプリに流すだけなので、Dartを使用しました。

特にDartはNode.jsと同じくC10K問題(サーバーの同時接続数が1万を超えた辺りで動作がおかしくなる、LinuxとUnix系OSの問題)が回避できる仕様になっているので、
ChatGPTから出力された文章をストリーミングする関係上、同時接続数が増えやすい今回のような場合にうってつけです。

ChatGPT API のストリーミング処理に対応したパッケージは4つしか見つからなかったので、いいね数などからdart_openaiパッケージを採用しました。
アプリとサーバー間の通信はgRPCの「Server streaming RPC」を使用しています。

以下は実コードを簡易化したものです。

chat_gpt_streamer.proto
syntax = "proto3";

package exapmle;

service ChatGptStreamer {
  rpc SayTextStream (TextStreamRequest) returns (stream TextStreamReply) {}
}


message TextStreamRequest {
  string order_text_from_user = 1;
}

message TextStreamReply {
  enum Error {
    UNDEFINED = 0;
    FAILED = 1;
  }
  oneof text_or_error {
    string output_text = 1;
    Error error = 2;
  }
}

oneofで分岐し、エラーと ChatGPT API から送られてきた文章のどちらか一方だけ返すようにしています。
上記のコードを基にDartファイルを生成するやり方はこちらの記事が分かりやすくておすすめです。

chat_gpt_text_stream.dart
class ChatGptStreamer extends ChatGptStreamerServiceBase {
  
  Stream<TextStreamReply> sayTextStream(
      ServiceCall call, TextStreamRequest request) async* {
    try {
      OpenAI.apiKey = /* ChatGPTのAPIキー */;

      Stream<OpenAIStreamChatCompletionModel>? stream;

      getStream() => stream ??= OpenAI.instance.chat.createStream(
            model: "gpt-3.5-turbo",
            messages: [
              OpenAIChatCompletionChoiceMessageModel(
                role: OpenAIChatMessageRole.user,
                content: request.orderTextFromUser,
              )
            ],
          );

      await for (final event in getStream()) {
        for (final choice in event.choices) {
          if ((choice.delta.content ?? "").isNotEmpty) {
            yield TextStreamReply(outputText: choice.delta.content!);
          }
        }
      }
    } on TextStreamReply_Error catch (errorType) {
      yield TextStreamReply(error: errorType);
    } catch (errorType) {
      yield TextStreamReply(error: TextStreamReply_Error.FAILED);
    }
  }
}
main.dart
void main() async {
  final server = Server([ChatGptStreamer()]);
  await server.serve(port: /* ポート番号 */); // SSL接続する場合は別途引数を渡す必要あり
}

getStream = () => stream ??=の部分は最初の一回だけcreateStreamを呼んで、二回目以降はstreamの値を返す処理です。

普通にstream = OpenAI.instance.chat.createStreamと書くとawait forまでの間にChatGPTから送られてきた文章を流せないので、こういった処理になりました。

基本的にTextStreamReply_Errorしかエラーは流れてこないはずですが、念の為その他のエラーも処理できるようにしています。

アプリ側で受け取り

実際はRiverpodのAsyncNotifierなどを使うことが多そうですが、今回の例では簡潔に書くために単純なクラスにしています。

chat_gpt_streamer.dart
class ChatGptStreamer {
  Stream<String> sayTextStream(String orderTextFromUser) async* {
    final channel = ClientChannel(
      /* サーバーのアドレス */,
      port: /* ポート番号 */,
      options: const ChannelOptions(
          credentials: ChannelCredentials.insecure()), // SSL接続する場合は削除
    );

    final client = ChatGptStreamerClient(channel);
    final request = TextStreamRequest()..orderTextFromUser = orderTextFromUser;

    final stream = client.sayTextStream(request);
    await for (final reply in stream) {
      if (reply.hasError()) yield throw reply.error;
      yield reply.outputText;
    }
    await channel.shutdown();
  }
}

後はこの関数を呼んでlistenするだけで動作します。

gRPCのストリーム処理の注意点としては、中断する際はClientChannelshutdown関数を呼ぶのはもちろん、listenしたStreamSubscriptioncancel()が必要です。
そうしないとアプリ側では中断されていても、サーバー側では出力され続けてしまいます。

出力結果の修正

ChatGPTは英語の文章を日本語に翻訳している可能性が高く、「尊敬する」から始まって「敬具」で終わる文章がそこそこの頻度で出力されます。
(おそらく「Dear」と「Sincerely」を訳しているのだと思います)

他にも「謝罪させていただきます」や「より満足いただける」のような変な文章も出るので、
形態素解析ライブラリーのmecabを用いて文章を品詞ごとに分けた後に、変換表を基に置換していきます。

Flutter用のmecabパッケージは、弊社では自社開発したものを使用しておりますが、調べたらpub.devにもあるようです(動作は未確認です)

mecabは動作が非常に早く、今回のように文章が送られてくるたびに形態素解析しても十分な余裕があります。

より	副詞,一般,*,*,*,*,より,ヨリ,ヨリ
満足	名詞,サ変接続,*,*,*,*,満足,マンゾク,マンゾク
いただける	動詞,自立,*,*,一段,基本形,いただける,イタダケル,イタダケル

こんな感じに品詞ごとに分類されます。

この場合、謙譲語の「満足いただける」の前には「ご」を付ける必要があるので、「ご満足いただける」にする必要があります。

また、「よりご満足いただける」は、「以前よりも満足していただける」という意味になります。
謝罪する相手が以前から満足している可能性は低いでしょうから…、怒られる確率を減らすために「より」を削除します。

ちなみに「○○より××の方が良い」などの比較の「より」は助詞なので、形態素解析しているおかげで誤って削除してしまうことがありません。

処理自体はChatGPTの出力結果を品詞ごとに分けた配列に対して、一致したら置換するだけなのでコードは割愛します。

敬語の種類(尊敬語、謙譲語、丁寧語など)ごとに色分けするのも同じような仕組みです。

ChatGPTに置換処理は効果的かも

去年社内で「自然言語AIってどんどん凄くなるだろうから、それに対してプログラムで簡単な修正処理していくだけでも効果ありそう」と話していたのですが、やはり実用性が高そうだなと感じました。

今のところChatGPTにしろGPT-4にしろ、AIの学習元の大半はネット上の文章でしょうから、自然な日本語の文章を書くには十分なものの、正しい敬語の文章を書くのは難しいと考えています。
正しい敬語のお礼文・謝罪文を書くのって、もはや専門技術に近いんですよね…。

ChatGPTなどのAIに対し、期待通りの出力結果を出すための指示文を考える技術(「AIプロンプト」や、単に「プロンプト」と言っている人が多いです)が注目されつつありますが、それと並行して今回のような手法も重要になるのでは、と感じる結果でした。

まだまだ今回の敬語の文章生成機能は置換処理が足りていないので、バージョンアップを重ねて拡充してまいります🙇

備考

本記事は以下の環境で検証しました

Flutter 3.7.8 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 90c64ed42b (5 days ago) • 2023-03-21 11:27:08 -0500
Engine • revision 9aa7816315
Tools • Dart 2.19.5 • DevTools 2.20.1

pub.dev (http://pub.dev/) packages:
dart_openai: 1.9.1
grpc: 3.1.0
protobuf: 2.1.0
GitHubで編集を提案
合同会社zoome(ズーム)

Discussion