AIチャットボットを搭載したウェブエンジニアのポートフォリオサイトを作った話（Next.js, Dify, OpenAI）

:::note warn
複数のgifが含まれておりモバイルの場合はデータ消費がありえます。ご了承ください。
:::

:::note info
本内容には主観が含まれています。
誤った情報や理解、誤記などがありましたら、ご指摘いただけると幸いです。
:::

## 作った背景

普段、自分のスキルとキャリアを整理する意味でポートフォリオサイトを運用しています。
前回のポートフォリオサイトを作って数年が経て、スキルセットと興味分野もいろいろ変わってきたので、今回リニューアルをしようと思っていました。

そこで、どうせやるのであれば、AIを取り入れたポートフォリオサイトを作りたいと思いました。
単なる静的な情報提供を超えて、AIが代わりに答えることで動的で面白い体験を提供したいと考えたからですね。まだ回答の正確性や充実性を向上させる課題はありますが、誰かにインスピレーションや楽しい体験を与えられるウェブサイトへと成長させたいと考えています。

この記事では、その制作においてやったことを以下の観点でざっくり紹介しようと思います。

- サイトとAIチャットボット機能の紹介
- 利用した技術スタックの話
- システム全体構成の話
- アプリケーション構成の話
- AIチャットボット構成の話(Dify)

AIの活用したアプリケーションや自分のポートフォリオ整理に興味がある方に、この情報がお役に立つことができれば嬉しいです。また、この記事では、ざっくりした内容の紹介のみで終わりますが、AI(Dify活用、画像生成との統合)、アプリケーション（FSDアーキテクチャとUT実装、APIモックなど）と、ポートフォリオの整理の際に意識したことにおいても、今後、具体例を記事にしたいと思ってます。

## サイト紹介
紹介するサイトは以下となります。

https://genie-oh.vercel.app/ja

- [Genie-GPT (Home)](https://genie-oh.vercel.app) : AIチャットボットを通して、私に関する質問をすることができるページです
- [Portfolio](https://genie-oh.vercel.app/ja/portfolio) : 私の関する紹介ページです
- [Blog](https://genie-oh.vercel.app/ja/blog) : 書いた記事情報をまとめたページです
- [MyService](https://genie-oh.vercel.app/ja/my-service) : 今後作って行く個人プロジェクトやサービスを紹介するページです

## AIチャットボット機能紹介
### 概要
ここでは、AIチャットボットでできることにフォーカスをおいて話します。

### 初期画面とチャット開始
AIチャットボットの初期画面では、チャットUIと、初めて利用する方に役立つサンプル質問が表示されます。サンプル質問をクリックすると、簡単に質問を送信できます。

![初期画面とチャット開始](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/78903d53-a318-3709-aa1c-8b1b1087c557.gif)


### チャットメッセージ送信・回答・フィードバック・おすすめ質問
直接チャットメッセージを送ることもできます。
回答では、回答本文とともに、以下の項目が表示されます。

- アバター：回答時の感情によって表情が違う画像が表示されます
    - ※アバターは筆者の趣向と願望が多く反映されています
- フィードバック：回答に対して「like」「dislike」をすることができます
- おすすめ質問：AIが次のおすすめ質問を提案してくれます

以下の動画は「チャットメッセージの送信」の例です。少しひどいことを言うと悲しい表情と回答が返ってきます。（皆さんは、この子をあまりいじめないようお願いいたします🙇‍♂️）
![チャットメッセージの送信](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/dfb682b6-8f80-62d9-c990-93989f20bdf2.gif)

以下の動画は「likeのフィードバック」の例です。フィードバックはサーバ側(Dify)にデータとして保存されます。
![likeのフィードバック](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/22fa4478-95e1-e904-a3fc-21b815d9443a.gif)

以下の動画は「おすすめ質問での質問ショットカット」の例です。次の質問に迷うときは、AIからの提案を参考して引き続き質問を誘導できるように図ってます。
![おすすめ質問での質問ショットカット](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/b70d58ed-af1a-7515-4603-5f48ee3ac07f.gif)

### 多国語機能
本サイトは「日本語」「韓国語」「英語」の言語で提供しており、AIチャットボットも同様です。言語に切り替えによってAIチャットボットの基本言語も変わります。以下は「AIチャットボットの韓国語、英語での利用」の例です。

![AIチャットボットの韓国語、英語での利用](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/c09f954c-5530-e53a-f523-bc5676dbbc9c.gif)


### メンテナンス・流量制限
AIチャットボットは「自前サーバのDify(Self-Hosted)」と「OpenAI(gpt-4o-mini, text-embedding-3-large)」を利用しています。
そのため、自前サーバのダウンや、過度なOpenAIの課金を防ぐための仕組みが用意されています。

以下の画像は「メンテナンス時の画面」の例です。自前サーバにアクセスできない時は、以下の画面を表示します。
![メンテナンス時の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/c3b78998-3800-d28a-73fb-50aabfbbb1da.png)

以下の画像は「重量制限時の画面」の例です。「IP別」と「全体利用量」の制限が設けられており、24時間ごとを目安にリセットされます。
![重量制限時の画面](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/b5179a99-a95c-de9c-926c-51f225a8f3c2.png)

## 利用した技術スタックの話

システムの開発においては、以下の技術スタックのもとに構築しました。

#### フロントエンド
- **ベース**: TypeScript、[Next.js (App Router)](https://nextjs.org/)、[React](https://ja.react.dev/)を利用
- **チャットUI**: [chatscope Chat UI Kit React](https://github.com/chatscope/chat-ui-kit-react)を利用
- **チャット機能の状態管理**: [Redux Toolkit](https://redux-toolkit.js.org/)を利用
- **UIフレームワーク**: [shadcn/ui](https://ui.shadcn.com/) と [TailwindCSS](https://tailwindcss.com/)を利用
- **ライト・ダークモード**: [shadcn/ui](https://ui.shadcn.com/)に加え、[next-themes](https://github.com/pacocoursey/next-themes)を利用
- **多国語対応**: [i18next](https://www.i18next.com/)を利用。[Next.js App Router での i18n 対応例](https://zenn.dev/k0kishima/articles/956ba3f3dc9629)という記事を参考にさせていただきました

#### バックエンド
- **ベース**: [Next.js (App Router - API Route)](https://nextjs.org/)を利用
- **リクエストバリデーション**: [zod](https://zod.dev/)を利用
- **外部APIのMock**: [msw](https://mswjs.io/)を利用。[Next.js（App Router）でMSWを使用する](https://zenn.dev/kattsuuya/articles/2795cc3fa80412)という記事を参考にさせていただきました

#### テスト
- **Unitテスト**: [Vitest](https://vitest.dev/) と [react-testing-library](https://testing-library.com/docs/react-testing-library/intro/)を利用
- **UIコンポーネントの動作テスト & ドキュメント生成**: [Storybook](https://storybook.js.org/)を利用。

#### **AIチャットボット**
- **AIエンジン**: [Dify(Self-Hosted)](https://dify.ai/)、[OpenAI (gpt-4o-mini, text-embedding-3-large)](https://platform.openai.com/docs/overview)を利用
- **感情表現用画像生成**: [Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)を利用。Difyと連携し、リアルタイムで画像生成可能なチャットボット構成も対応しています（今回はコスト削減のため、事前生成画像を利用）

#### **Difyホスト環境構築**
- **環境**: 自前のWindows PC (WSL2 Ubuntu) を使用
- **リバースプロキシ**: [Nginx](https://www.nginx.com/)を利用
- **動的DNS (DDNS)**: [No-IPのDDNSサービス](https://www.noip.com/remote-access)を活用

#### **流量制限**
- **Nginxの流量制限**：同時接続を制限
- **API利用回数制限(24h)**: アプリケーション側メモリキャッシュの[flat-cache](https://www.npmjs.com/package/flat-cache)を利用して、全体利用とIP別利用の条件で制限

## システム全体構成の話

システムの全体構成はざっくりですが、以下となります。
![システムの全体構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/bd86e47c-cfac-9405-5864-0889ebb36a12.png)

特徴としては、以下となります。
- Next.jsアプリケーションのAIチャットAPIから、どの言語のDifyのチャットボットAPIを呼び出すかを判別する仕組みになってます。（厳密にはAPIKEYで分けられてます）
- アプリケーションレベルの流量制限を設けています。Memory Cacheを利用して全体、IP別リクエスト数をカウントしており、24時間を目安にキャッシュがリセットされるように仕組んでます
- Difyは自前PCでホストしています。自前PCのネットは動的IPなため、DDNSによるDOMAINと動的IPを自動同期してくれるサービスを使って、IPが変わってもDOMAINNAMEからの接続ができるように仕組んでます
- Home RouterとFirewall、Reverse Proxyを設けたことで、アクセスできるポートとURIを厳しく制限しています。また、Next.jsで踏み台APIを用意することで、自前サーバPCのアクセスアドレスを隠匿しています

## アプリケーション構成の話

### アプリケーション全体構造
アプリケーションは大きくは以下のような構成になっています。
- フロントエンドアプリケーション階層(`/app`,`/fsd`)
- バックエンドアプリケーション階層（`/backend`）
- インフラ階層（依存性ライブラリモジュール等）

具体的な図は以下になります。
![アプリケーション全体構造](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/d0651119-d06e-be31-0e7f-bde3eb7dedd2.png)


### フロントエンドアプリケーションの構造
フロントエンドのアプリケーションにおいては、大きく「チャット」「ポートフォリオ」「ブログ」「サービス一覧」の4つの機能ドメインを想定し、FSD(Feature-Sliced Design)の適用を試しました。具体的な図は以下になります。
![フロントエンドアプリケーションの構造](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/02929134-3f9f-f5f5-20a0-13445d1dfd4a.png)

Next.jsにおいてのFSD適用は[FSD - Usage with NextJS](https://feature-sliced.design/docs/guides/tech/with-nextjs)を参考に、少し構造変更を行っています。

FSDに対する詳細は[FSD(Feature-Sliced Design)](https://feature-sliced.design/)のガイドをご参考ください。こちらはボリュームが少しある話で、まだ筆者も熟達には程遠いですが、機会があれば、実際にどういう考えで設計をしたのかを記事にしてみたいと思います。

### バックエンドアプリケーションの構造
バックエンドにおいては、こちら、DifyのAPIを呼び出しにおいて、バリデーションとデータ整形の要件です。そのため、従来のThree-Layered-Architecutreの思想をもとに「Controller」「Service」「Gateway」のシンプルな3つの階層の構造にしました。リクエスト・レスポンス時のエラーハンドリング、外部通信時のfetchにおいては、interfaceに該当するTypeを置くことで依存性逆転をし、テストコード作成がしやすいように図ってます。
![バックエンドアプリケーションの構造](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/e0980f1c-8f74-c5c6-cda3-aa0f831e4501.png)

また、バックエンドの場合、Difyを呼び出す機能の外部依存において、[msw](https://mswjs.io/)というHook方式のMockServerを利用して模擬APIを定義し、ローカル開発では利用しています。UTのMockやStubだけではカバーしきれなかったり維持保守が上昇する部分の緩和を図ってます。ここに関しては[Next.js（App Router）でMSWを使用する](https://zenn.dev/kattsuuya/articles/2795cc3fa80412)の記事をとっても参考にさせていただきました。


### テストコードの話 (vitest, storybookのcoverage)

アプリケーション構造の工夫によって、テストコードが書きやすくなり、以下のポイントを抑えてUnitTestの範囲としています。

- フロントエンド（FSD）
    - `src/fsd`の配下（FSDのpagesから、sharedまでの階層）
    - ※今後は、`app`階層までカバーしたいと思ってます
- バックエンド（3 Layered）
    - 「Controller」「Service」「Gateway」の配下
    - ※今回はinfrastructure layerのコードも一応テストコード範囲としてます

UTのcoverage確認には、`@vitest/coverage-v8`仕組みを利用しています。
[https://vitest.dev/guide/coverage](https://vitest.dev/guide/coverage)

<details><summary>本記事のVitestのCoverage確認例</summary>

※Directory出力のみ引用しています。

```sh
$ vitest run --coverage

File                                                              | % Stmts | % Branch | % Funcs | % Lines |
------------------------------------------------------------------|---------|----------|---------|---------|
All files                                                         |   96.35 |    89.96 |   87.95 |   96.35 |
 backend/controller/chat                                          |     100 |      100 |     100 |     100 |
 backend/error                                                    |     100 |      100 |     100 |     100 |
 backend/gateway/chat                                             |     100 |      100 |     100 |     100 |
 backend/lib/chat-limit-check                                     |     100 |       90 |     100 |     100 |
 backend/lib/fetch                                                |     100 |      100 |     100 |     100 |
 backend/service/chat                                             |     100 |      100 |     100 |     100 |
 backend/types                                                    |       0 |        0 |       0 |       0 |
 fsd/features/blog-list/ui                                        |     100 |      100 |     100 |     100 |
 fsd/features/chat/api/gateway                                    |     100 |      100 |     100 |     100 |
 fsd/features/chat/lib                                            |      99 |       75 |     100 |      99 |
 fsd/features/chat/models                                         |     100 |      100 |      75 |     100 |
 fsd/features/chat/models/chat-slice-model                        |     100 |      100 |     100 |     100 |
 fsd/features/chat/models/chat-slice-model/reducers               |   96.46 |     97.5 |   85.71 |   96.46 |
 fsd/features/chat/types                                          |       0 |        0 |       0 |       0 |
 fsd/features/chat/ui                                             |   75.92 |    61.53 |   33.33 |   75.92 |
 fsd/features/chat/ui/message-component/agent-answer-message      |   84.52 |    81.25 |      80 |   84.52 |
 fsd/features/chat/ui/message-component/agent-maintenance-message |   97.74 |      100 |      80 |   97.74 |
 fsd/features/chat/ui/message-component/agent-welcome-message     |   95.78 |      100 |      80 |   95.78 |
 fsd/features/my-service-list/ui                                  |     100 |      100 |     100 |     100 |
 fsd/pages/blog/ui                                                |     100 |      100 |     100 |     100 |
 fsd/pages/home/ui                                                |     100 |      100 |     100 |     100 |
 fsd/pages/my-service/ui                                          |     100 |      100 |     100 |     100 |
 fsd/pages/portfolio/lib                                          |   82.64 |    64.61 |     100 |   82.64 |
 fsd/pages/portfolio/ui                                           |     100 |      100 |     100 |     100 |
 fsd/shared/hooks-use-effect-once/hooks                           |     100 |      100 |     100 |     100 |
 fsd/shared/lib-http-fetcher/lib                                  |     100 |      100 |      75 |     100 |
 fsd/shared/lib-redux-shared-kit/hooks                            |     100 |      100 |     100 |     100 |
 fsd/shared/lib-redux-shared-kit/lib                              |     100 |      100 |     100 |     100 |
 fsd/shared/ui-animation/ui                                       |   68.57 |      100 |     100 |   68.57 |
 fsd/shared/ui-article-item/ui                                    |     100 |     93.1 |     100 |     100 |
 fsd/shared/ui-client-only/hooks                                  |       0 |        0 |       0 |       0 |
 fsd/shared/ui-client-only/ui                                     |       0 |        0 |       0 |       0 |
 fsd/shared/ui-dialog/ui                                          |     100 |      100 |     100 |     100 |
 fsd/shared/ui-external-navigation-guard/ui                       |    92.1 |      100 |      50 |    92.1 |
 fsd/shared/ui-image-view/ui                                      |     100 |       50 |     100 |     100 |
 fsd/shared/ui-logo/ui                                            |     100 |      100 |     100 |     100 |
 fsd/shared/ui-social-link/ui                                     |     100 |      100 |     100 |     100 |
 fsd/shared/ui-sticky-component/hooks                             |       0 |        0 |       0 |       0 |
 fsd/shared/ui-sticky-component/ui                                |       0 |        0 |       0 |       0 |
------------------------------------------------------------------|---------|----------|---------|---------|
```
</details>

また、FSDの`ui`セグメントに該当する部分においては、Storybookを作成しコンポーネント単位動作確認・ドキュメント化を図ってます。

<details><summary>※本記事のStorybookの画面例(gif動画)</summary>

![本記事のStorybookの画面例(gif動画)](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/9751f230-d74b-ef7a-2ad4-29731c33c415.gif)

</details>

Storybookもcoverage確認が可能です。確認には`@storybook/addon-coverage`を利用しています。
[https://storybook.js.org/docs/writing-tests/test-coverage](https://storybook.js.org/docs/writing-tests/test-coverage)

<details><summary>※本記事のStorybookのcoverage確認例</summary>

※Directory出力のみ引用しています。

```sh
$ npm run test-storybook -- --coverage

File                                                          | % Stmts | % Branch | % Funcs | % Lines |
--------------------------------------------------------------|---------|----------|---------|---------|
All files                                                     |   83.12 |    41.66 |   71.85 |   81.02 |
 features/blog-list/ui                                        |   85.71 |      100 |      50 |   83.33 |
 features/chat/ui                                             |   48.64 |    13.63 |    12.5 |   47.22 |
 features/chat/ui/message-component                           |     100 |      100 |     100 |     100 |
 features/chat/ui/message-component/agent-answer-message      |   72.22 |     37.5 |      40 |   71.87 |
 features/chat/ui/message-component/agent-maintenance-message |   93.33 |      100 |   85.71 |    92.3 |
 features/chat/ui/message-component/agent-welcome-message     |   86.84 |        0 |   88.88 |   85.29 |
 features/my-service-list/ui                                  |   85.71 |      100 |      50 |   83.33 |
 pages/blog/ui                                                |     100 |      100 |     100 |     100 |
 pages/home/ui                                                |     100 |      100 |     100 |     100 |
 pages/my-service/ui                                          |     100 |      100 |     100 |     100 |
 pages/portfolio/ui                                           |     100 |      100 |     100 |     100 |
 pages/portfolio/ui/components                                |   93.22 |       50 |   86.36 |   92.45 |
 pages/portfolio/ui/parts                                     |   75.55 |      100 |      75 |   73.68 |
 pages/portfolio/ui/parts/parts-development-skills            |   95.65 |      100 |    87.5 |      95 |
 pages/portfolio/ui/parts/parts-personality                   |   88.88 |      100 |      80 |   86.95 |
 shared/ui-animation/ui                                       |   47.36 |        0 |      40 |   41.17 |
 shared/ui-article-item/ui                                    |     100 |      100 |     100 |     100 |
 shared/ui-client-only/ui                                     |     100 |       50 |     100 |     100 |
 shared/ui-dialog/ui                                          |   93.75 |      100 |   66.66 |   92.85 |
 shared/ui-external-navigation-guard/ui                       |   27.27 |      100 |       0 |      20 |
 shared/ui-image-view/ui                                      |     100 |        0 |       0 |     100 |
 shared/ui-logo/ui                                            |     100 |      100 |     100 |     100 |
 shared/ui-social-link/ui                                     |     100 |      100 |     100 |     100 |
 shared/ui-sticky-component/ui                                |     100 |       50 |     100 |     100 |
 widgets/container/ui/header                                  |     100 |      100 |     100 |     100 |
 widgets/container/ui/main                                    |     100 |      100 |     100 |     100 |
 widgets/container/ui/section                                 |     100 |       50 |     100 |     100 |
 widgets/footer/ui                                            |     100 |      100 |     100 |     100 |
 widgets/footer/ui/notice                                     |     100 |      100 |     100 |     100 |
 widgets/nav/ui                                               |     100 |      100 |     100 |     100 |
 widgets/nav/ui/lang                                          |   77.77 |       50 |      50 |      75 |
 widgets/nav/ui/menu                                          |      95 |       50 |    90.9 |   94.44 |
 widgets/nav/ui/theme                                         |   85.71 |      100 |   66.66 |   83.33 |
--------------------------------------------------------------|---------|----------|---------|---------|
```
</details>

## AIチャットボット構成の話(Dify)

### AIチャットボット処理の全体の流れ
処理の流れとしては「ナレッジベース作成」「ナレッジ検索」「LLM推論->回答・感情表現単語・おすすめ質問生成」「全体回答文脈をパーシング」「UIに表示」でシンプルですが、「ナレッジベース作成」や、「LLM推論->回答」段階のプロンプトには工夫を加えています。

詳細は以下の図となります。
![AIチャットボット処理の全体の流れ](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/c0d3968c-9e4e-8968-68f1-5dca7f9f927a.png)


### Dify側のWorkflowの構成概要

上記の概念を反映して、Dify上でWorkflowを組んでいます。
Workflowのイメージは、以下のようにシンプルです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/71f30605-60f3-1cd5-fa24-26ffc8403ea7.png)


言語ごとに「RAGのためのナレッジベース」と「LLMのプロンプトおよび回答言語」の設定を変更し、3つのChatbotが構成されています。それぞれ、言語ごとに分離されたAPIとして呼び出される仕組みになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/34404ba6-9459-4817-99fc-95390f1cc6d2.png)

### ナレッジベース作成
ナレッジベース作成においては、以下のようなファイルをインプットしています

<details><summary>※例文 (text形式)</summary>

質問回答.txt　
※必ず正確に答えてほしいものに対しては事前に質問と回答を用意してます。

```text
# このウェブサイトの開発にはどういう技術が利用されましたか？
回答:
このウェブサイトは TypeScript、Next.js、React、Redux、TailwindCSS を基盤に構築されています。
UI実装: shadcn/ui
チャット機能: chatscope Chat UI Kit React
ダークモードテーマ: shadcn/ui & next-themes
多言語対応: i18next
AIチャットボット: DifyとOpenAIを利用したRAGおよび生成型AIを使用しています。

...省略
```
</details>

<details><summary>※details 例文 (json形式)</summary>
`開発スキル.txt,パーソナリティ.txt,ブログ記事.txt,強み.txt,キャリア-職歴.txt,キャリア-目標.txt`

※一般的な質問全般において参考にするデータとなります。
※ウェブサイトの多国語データ（JSON）を活用しています。

```text
{
  "imgUrl": "/images/blog/cover/stocksmeal.webp",
  "categories": ["AI", "crewAI", "llama", "個人開発"],
  "title": "[AI] AIを活用して「米株の分析レポート提供サービス」を個人で作ってみた (crewAI & Ollama & OpenAI)",
  "text": "テーマは「米株のAI分析と運用レコメンド生成」により作られたコンテンツをWebサービスとして提供することです。前提としては、「最低限の予算で、週16時間を目安に、1ヶ月以内にリリース・運用する」が目標で、開発もスクラッチに行ったので、コンテンツの調整が不足している部分や不安定な機能もありますが、今後改善・機能追加などを試しながら学習と経験を得ようかなと思ってます。",
  "date": "2024.07",
  "contentUrl": "https://qiita.com/genie-oh/items/09878d5adb4e5e23d761",
  "onlySupportLanguages": ["ja"]
},

...省略
```
</details>

ナレッジベース作成時のパラメータについては、「chunkサイズ、overlapサイズ、検索方法(semantic or keyword or hibrid)、TOP K」を意識して設定しています。筆者もまだ素人ですが、詳細は以下をご参照ください。

<details><summary>※ナレッジベース作成時のパラメータ参考</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/93b1e2e3-acf6-5034-5d60-ffe3400e5a9c.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/f628d69b-62de-281c-0042-5c065b71b836.png)

- Chunk settings -> Custom
    - chunk size: 1000
        - 全体データに対して、どのサイズで分割するかのオプションです。検索Hit時のコンテキスト単位にもなります。（例えば、小さすぎると類似度で検索Hitしても、途中で情報が切れたままコンテキストとしてLLMに渡されることで、RAGの精度に影響する場合もある認識です。）
    - Chunk overlap:128
        - chunkを分割する際に、chunkの間で重複させるデータのサイズです。大きいほどデータやtokenの浪費はあるかもですが、検索時に連続する複数の関連chunkがヒットする確実が上がる認識です
- Embedding Model
    - openaiの`text-embedding-3-large`を利用しています
- Retrieval Setting -> Hibrid Search (Vector & Full-Text) -> Weighted Score
    - SEMANTIC:0.7、KEYWORD:0.3
        - embedding及びvector変換からの類似度検索と、KEYWORD一致検索のバランスを調整します
    - Top K:7
        - 検索されたchunkデータの上位選択数を意味します。高いほどデータは多くなりますが、より多くの関連情報がLLMが参考できるようになります
</details>

### 感情表現の変数定義

感情表現を実現するためにやったことは以下の感じです。

- あらかじめ、感情表現として使う単語を定義
- AIの回答時に、システム・ユーザープロンプトにて「回答時に回答に一番近い感情表現の単語を選定し一緒に回答する」を指示

定義した単語は以下です。

```
angry, happy, sad, surprised, anxious, excited, disappointed, smiling, serious, thoughtful, worried, confused, calm, shy, confident, bored, cheerful, skeptical, irritated, defiant, contemptuous, joyful, tired, intrigued, amazed, embarrassed, relaxed, passionate, depressed, blank, determined, pained, discouraged, elated, slight_smile, frowning, holding_back_laughter, dissatisfied, hiding_surprise
```

### SystemPrompt

システムプロンプトでは、大まかに以下の指示を出しています。

- a.推論時の性格指定
- b.使える感情表現の単語を指定
- c.推論時のルールの指定

a,bにおいての指示例は以下となります。

```
あなたは感情豊かなウェブエンジニアです。
MBTIはINFJ-Tで、内向的で恥ずかしがり屋な性格を持っています。
性格を積極的に反映し、以下の感情を自由に活用してください。
---
{※感情表現の単語リスト}
---
...省略
```

### UserPrompt

ユーザープロンプトにおいては、大まかに以下の指示を出しています。

- a.回答時のルール指定
- b.回答フォーマット指定

bにおいての指示例は以下となります。
該当フォーマットの回答をLLMに誘導させ、ウェブアプリケーション側で文脈をパーシングすることで、「回答」「表情付きアバター表示」「おすすめ質問」を実現しています。

:::message
※Difyにはおすすめ質問生成APIがありますが、回答と同時に生成したほうがtokenを節約できるかなと思い、この方式にしました。しかし、おすすめ質問機能があんまり使われないようだったら、逆にon-demand形式のDifyのおすすめ質問生成APIを使うのが良いかもしれません。
:::

```
...省略

「感情」「おすすめ質問」を追加で回答されるために、以下を指定しています。
------
回答の最後に以下の情報を含めてください。
@@--additional_data--@@
#impression
※1
#suggestion
※2

※1 : 感情を英語で1語で表現してください。感情を示す英単語は必ず以下の中から選んでください。[{感情表現の単語リスト}]
※2 : 回答に関連する次の質問を2つ作成してください。
事前知識ではなく、コンテキスト情報を考慮して質問を生成してください。
コンテキスト情報に基づく具体的な質問のみ作成してください。
コンテキストにない質問や回答は除外してください。質問は改行で区切ってください。
```

### Difyの会話データの分析・運用について

Difyの管理画面では、モニタリングとログ画面が提供されています。

<details><summary>※モニタリング画面の例</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/13da28c2-3615-7919-1531-599c6dee8172.png)
</details>

<details><summary>※ログ画面の例</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/67180d46-f207-097a-616c-83cfca0a6d01.png)
</details>

しかし、会話セッション全体のデータを分析したり、フィードバックの統計を出して分析し、改善などに活用するには、一部データのみが表示される管理画面では難しい印象があります。

対策として、Dify構築時にpgAdmin4を含めてdocker-composeを構成しています。簡単にDBデータ参照が可能になり、Difyを使う際の会話・フィードバックデータなどの参照の活用に使うのがいいと思います。構成方法は[PostgreSQL と PgAdmin4 を Docker Compose で立ち上げる](https://zenn.dev/onozaty/articles/postgresql-pgadmin-container)を参考すると良いともいます。

<details><summary>※pgAdmin4での会話履歴データ確認</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/998661d7-2c19-414c-2565-75a00f8e584a.png)
</details>

<details><summary>※pgAdmin4でのフィードバックデータ確認</summary>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/6d746891-abdf-db26-154c-44c93cb63a04.png)

</details>

または、本格的に集計・分析の運用したい場合は[metabase](https://www.metabase.com/)のようなBIツールと連動するのも良いと思います。

### 「ボーナス」感情表現のアバター画像のリアルタイム生成（Dify & Stable Diffusion 連携)

今回は、運用とコスト関係で、LLM推論をOpenAIのAPIと、感情表現アバターは事前にStable Diffusionで生成した画像を表示する形で実現しています。

しかし、当初の構成はDify(Self-Hosted)に加え[Ollama](https://ollama.com/)と[Stable Diffusion WebUI](https://github.com/AUTOMATIC1111/stable-diffusion-webui)を使った、完全なSelf-Hosted AIシステム構築によるサービスを構想していました。
その際の構成図と試したLocal LLMモデルを紹介します。こちらも機会があったら、詳細な構築・活用と実際動作検証例を、別記事にしたいと思ってます。また、Local LLMの構築において書いた記事[Local LLM AIを簡単に構築し開発への活用を試す](https://qiita.com/genie-oh/items/2bf46a150f15aadb755a)もあるので、ご興味ある方はご参考ください。

以下は、もし構成した場合のイメージ図となります。
![Dify&Stable Diffusion連携のイメージ図](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/18562137-19c1-69a7-2be8-545df4fcb55c.png)


以下は、Stable Diffusionを連携してDifyを動かく動画です。RAGとLLMをOpenAIにしていますが、ここはOllamaを連携すると、完全なLocal LLMでの感情を持つアバター生成付きチャットボットシステムの仕上がりです。
![7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436887/9faef0dc-ded5-986a-acbf-cfe65653dfe8.gif)


余談として、こちら実際に構築完了まで行きましたが、1回の質問・回答においても、高いマシンパワーが利用されるため、多数が同時にリクエストする環境では実運用が現実性がないと思い諦めました。

:::message
今後、頑張って予算を確報して、Mac mini M4Pro (64GB)を買ったら、実運用に挑戦したいと願っています。NVIDIA GPUよりパフォーマンスは落ちると思いますが、電気料金と安全性を考えるとMac miniのほうが良いかなとおもい、検証したいとずっと思ってます。
:::

## 最後の話
作った背景と思いは、最初にお話できたと思います。
今回の制作中に、楽しかったこと、学びになったこと、やりきれず今後改善したいことなどを振り返りながら、「今回の学び」と「今後の課題」の話を最後として、締めたいと思います。

### 今回の学びのまとめ
今回のプライベートプロジェクトを進めながら学んだことは以下の点です。
特に心象にのこったり、やって楽しかったりしたことは★をつけています。

#### Dify（AI）の学び
- ★Dify APIと、API連携を前提としたチャットボットシステム構築
- ★Difyでの結果処理のカスタマイズ（Transform）、画像生成連携（Stable Diffusion）
- LLMのプロンプトの考え方
- DifyでのRAGの考え方（embeddingに親和的なデータ作成、embeddingパラメータ調整）

#### アプリケーション(フロント・バックエンド)の学び
- ★FSDアーキテクチャ
- ★`chatscope/chat-ui-kit-react`を利用したリッチなChat機能の実装
- ★`redux-toolkit`を利用した状態管理の深堀りとNext.jsへの適用
- `i18next`を利用した、サーバコンポーネント・クライアントコンポーネントにおいての多国語対応
- `shadcn/ui`,`next-themes`を利用したライト・ダークテーマの実装
- `msw`を利用した外部HTTPリクエストに対してのHookベースMocking
    
#### アプリケーション（テストコード観点）の学び
- ★フロント・バックエンド両方においての、テストしやすいアプリケーション構造の考え方
- ★test coverageの考え方の深堀り

### 今後の課題の備忘録
まだ抽象的なところもありますが、課題と改善したい方向性は以下となります。

#### AI構築

| **課題**                          | **具体的課題内容例**                                                                                                          | **改善したい方向性**                                                                                                                                         |
|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ナレッジベースの更新が非効率       | - 3つの言語ごとにインプットが必要<br>- JSONの場合、delimiterの関係上修正が必要                                                | - ウェブサイトのJSONやDBデータを自動で知識基盤に反映させる仕組みを構築                                                                                         |
| 回答の精度に不安定さがある         | - 同じ質問でも回答がブレる場合がある<br>- 事前回答や知識基盤データの連関性が薄いほど不安定                                     | - 知識基盤データのリファインメント<br>- 事前質問・回答の補強<br>- Agentを活用した回答の正確度判定など、AIが回答精度を評価・改善する仕組みを検討                  |
| 感情表現のパターンが単調           | - 数十個の感情表現が用意されているが、実際に使われるのは数個のみ                                                              | - 感情のグルーピングを行い、感情表現単語の頻度を考慮したランダム性を追加                                                                                       |
| 知識不足                          | - LLMのプロンプト調整、LLMパラメータ調整、embeddingパラメータ調整などが不足                                                   | - 該当分野をじっくり学習し、高度なAI機能を目指す                                                                                                               |

#### バックエンド

| **課題**                     | **具体的課題内容例**                                                                                 | **改善したい方向性**                                                                                                     |
|------------------------------|------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| 流量制限の仕組みの改善        | - アプリケーションごとのCacheでの制御になり、サーバスケールアウトや同時性安全保障が満たされていない    | - サーバをスケールアウトしても共通の流量制限と同時性に安全な仕組みを検討                                                                 |
| CleanArchitecture、DDDの適用検討 | - 機能のスケールアウト時にリアーキテクチャが求められる可能性がある                                   | - 今後の機能拡張の際、構造のリファインメントとともにCleanArchitectureやDDDを適用                                           |


#### フロントエンド

| **課題**                         | **具体的課題内容例**                                                                                                                                          | **改善したい方向性**                                                                                                                                                        |
|----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| FSDアーキテクチャのより深い実践    | - entities階層を含めたボリュームのあるFSDアーキテクチャをまだ十分に実践できていない<br>- UI部分の分け方が未熟                                                    | - FSDの公式ドキュメントの深掘りや、他者のGithubコードを多面的に分析                                                                                                         |
| i18n多国語処理の改善               | - i18n機能に直接依存している<br>- 言語ファイル同士のデータ整合性が取りづらく、一部言語にデータ漏れがある                                                      | - i18nのデータ取得をFSDのModelに当たるセグメントで制御し、UIへの依存を緩和<br>- データ整合性確認ツールやTypeScript→JSON変換の導入を検討                                  |
| infrastructure layerの複雑性と直接依存 | - i18n, redux, theme, shadcn/uiなどの依存ライブラリが/src/配下に配置されている<br>- 直接依存度が高い部分が残っている                                            | - infrastructure layerを作成し、依存性注入パターンを活用して依存関係を改善                                                                                                 |

#### システム構築

| **課題**                     | **具体的課題内容例**                                                                                         | **改善したい方向性**                                                                                                                                      |
|------------------------------|------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 自前PCのサービスダウン問題    | - 自前PCがWindows Updateでリブートされたり故障すると、AIチャットボットが使えなくなる                                                            | - AIシステムをCloud化する方向を検討<br>- Windowsの再起動時に安定的な自動起動の仕組みを導入<br>- 予算が許せばMac mini M4Proでの運用を試す                  |