ChatGPTのAPIで結果をリアルタイム表示する

# やりたいこと

OpenAIからChatGPT相当のAPIである「gpt-3.5-turbo」が公開されたので色々遊べるようになったと思う。

ただAPI使うと、なんか公式WEB版よりレスポンスが遅いなと感じたことはないだろうか。

実際のところ、公式WEB版は結果がリアルタイムに表示されているので体感速く感じているだけだったりする。

じゃあAPIは完全な文章が生成されるまで指を咥えて待っていなければいけないのかというとそんなことはなくて、APIにストリーミングモードがあるのでそれを使うと以下のようにリアルタイム表示することができる。

![a.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/107001/a69e67e1-d1d3-4d08-736a-cee104632d73.gif)

今回はそれをTypeScriptで実装してみる。

# インストール

OpenAIのライブラリだけでも実装は可能だが、なるべく簡単に実装したいので[LangChain](https://github.com/hwchase17/langchainjs)のJS版を使用する。実は本家LangChainと作者が同じ。

```
npm install langchain openai
```

# サンプルコード

```ts
import { CallbackManager } from 'langchain/callbacks';
import { ChatOpenAI } from 'langchain/chat_models';
import { HumanChatMessage, SystemChatMessage } from 'langchain/schema';

export const run = async () => {
  const chat = new ChatOpenAI({
    streaming: true,
    openAIApiKey: process.env.OPENAI_APIKEY,
    callbackManager: CallbackManager.fromHandlers({
      async handleLLMNewToken(token: string) {
        console.log({ token });
      },
    }),
  });

  const response = await chat.call([
    new HumanChatMessage('日本国憲法の一部を紹介してください'),
  ]);
  console.log(response);
};
run();
```

これを実行すると以下のように1文字ずつ表示されていく。すげー

```js
{ token: '' }
{ token: '1' }
{ token: '.' }
{ token: ' 第' }
{ token: '9' }
{ token: '条' }
{ token: ':' }
{ token: ' 国' }
{ token: '民' }
{ token: 'の' }
{ token: '権' }
{ token: '利' }
{ token: '及' }
{ token: 'び' }
{ token: '義' }
{ token: '務' }
{ token: 'は' }
{ token: '、' }
{ token: '社' }
{ token: '会' }
{ token: '秩' }
{ token: '序' }
```

# Reactで動くようにする

じゃあReactのコンポーネントと組みわせられるのかって話。結論可能。

例えば以下のようなバックエンドAPIを用意する。ポイントはContent-Typeを`application/octet-stream`にしてTransfer-Encodingを`chunked`にする必要がある。

```ts:src/pages/api/chat.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { CallbackManager } from 'langchain/callbacks';
import { ChatOpenAI } from 'langchain/chat_models';
import { SystemChatMessage, HumanChatMessage } from 'langchain/schema';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    res.writeHead(200, {
      'Content-Type': 'application/octet-stream',
      'Transfer-Encoding': 'chunked'
    });

    const chat = new ChatOpenAI({
      openAIApiKey: process.env.OPENAI_APIKEY,
      modelName: 'gpt-3.5-turbo',
      streaming: true,
      temperature: 0.7,
      callbackManager: CallbackManager.fromHandlers({
        async handleLLMNewToken(token: string) {
          res.write(`${token}`);
        },
      }),
    });
    await chat.call([
      new SystemChatMessage('弁護士として行動してください。'),
      new HumanChatMessage('窃盗と犯罪はどちらが罪が重たいですか？')
    ]);
  } catch (error) {
    console.error(error);
    res.status(500).send('Internal Server Error');
  }
  res.end();
}
```

で、コンポーネント側では以下のような実装

```ts
const handleSubmit = async () => {
  setOutput('');

  const response = await fetch('/api/chat/', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      system_message: 'xxxx',
      human_message: 'xxxx',
    }),
  });
  const stream = response.body;
  const reader = stream?.getReader();
  const decoder = new TextDecoder('utf-8');
  try {
    while (true) {
      const { done, value }:any = await reader?.read();
      if (done) {
        break;
      }
      const decodedValue = decoder.decode(value, { stream: true });
      setOutput(prevOutput => prevOutput + decodedValue);
    }
  } catch (error) {
    console.error(error);
  } finally {
    reader?.releaseLock();
  }
};
```

大まかな処理は以下

- POSTリクエストを'/api/chat/'に送信し、レスポンスを取得する
- レスポンスからストリームを取得し、リーダーを作成する
- UTF-8でデコーダーを作成する。
- ストリームからデータを読み込む
- ストリームの終わりに達したら、ループを抜ける
- デコードされた値を取得し、出力に追加する
- 最後にリーダーのロックを解除する

# サンプルアプリ

箇条書きから文章を生成するアプリを作ったのでぜひ試してほしい。（宣伝

https://article-generator.turai.work/

ソースコードは以下

https://github.com/thr3a/gpt-article-generator/tree/tedasuke


よきChatGPTライフを

# 参考リンク
- https://hwchase17.github.io/langchainjs/docs/modules/chat_models/examples/streaming/
    - 公式ドキュメント
- https://github.com/sachio222/langchain-nextjs-streaming