Heroku PostgresとAWS BedrockとHugging FaceでRAGを作ってみた

2023年11月にHeroku Postgresql 15 がpgvectorに対応しました。これによりHeroku Postgresがベクトルデータベースとして利用できるようになりました。

https://www.salesforce.com/jp/blog/pgvector-launch/


本記事では、最近話題のClaude2.1やText Embeddingと組み合わせ、**Heroku Postgres内データを元に生成AIが回答してくれる、 Retrieval Augment Generation (RAG)** を試作し、実際どんな回答を返してくれるか？検証してみました。

### 今回使用したもの
個人的に興味のあるものや、使いやすいものを選びましたので、特にこの組み合わせでないといけないわけではないです。

#### ■ ベクトルデータベース & ナレッジベース
 - Heroku
     - [Heroku Postgres (PostgreSQL)](https://elements.heroku.com/addons/heroku-postgresql) 
     - [pgvector](https://github.com/pgvector/pgvector)
      ※今回の主役！
      
#### ■ 大規模言語モデル
 - AWS Bedrock
     - [Anthropic Claude 2.1](https://www.anthropic.com/index/claude-2-1)
     ※GPT4を大きく上回る20万トークン処理可能ということで話題のLLM。日本語に対応しています。
     
#### ■ Embeddingモデル
 - Hugging Face
     - [Sentence Transformer ](https://www.sbert.net/)
         - [sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2](https://huggingface.co/sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2)
     ※日本語OK、384次元のベクトルデータを生成してくれます。


## 手順
Macでの手順になります。

### 0. 前準備

#### Heroku
[Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) をインストールしておきます。

```shell
% brew tap heroku/brew && brew install heroku
```



#### AWS Bedrock 
Bedrock Runtime を呼び出せるIAMユーザを作成し、APIコールに必要な認証情報、リージョン情報を取得しておきます。

#### Hugging Face
APIコールするためのトークンを取得しておきます。



### 1. Heroku Postgres テーブル作成


:::note warn
2023/12/5現在、pgvectorはHeroku Postgres 15 standardプラン以上でのみ利用できます
:::

Heroku Postgres、pgvector を有効化し、テーブルを作成します。

#### Heroku Postgres 追加

以下Heroku CLIコマンドで、Heroku Postgres が追加されます。
```shell
% heroku addons:create heroku-postgresql:standard-0 --app [HerokuAppName]
```

DB接続情報を取得しておきます。
※あとで使います
```shell
% heroku pg:credentials:url DATABASE --app [HerokuAppName]
```

#### pgvector 有効化

psqlコマンドでDBに接続し、
```shell
% heroku pg:psql --app [HerokuAppName]
```
まずは、pgvector を有効化します。
```sql
> CREATE EXTENSION vector;
```

#### テーブル作成

そして、```docs``` というテーブルを作成します。

```sql
> CREATE TABLE docs (id bigserial PRIMARY KEY, embedding vector(384), content text);
```

すると、こんなテーブルが出来上がります。

| 項目名 | データ型 | 用途 | 備考 | 
| ---- | ---- | ---- | ---- |
| id | bigserial | （自動採番）| |
| embedding | ベクトル(384次元) | ベクトルデータを格納| ※ Sentence Transformer で生成されるベクトル空間が384次元|
| content | テキスト |コンテンツを格納 | |

psqlモードを終了します。
```ps
> \q
```




### 2. ダミーデータの準備

サンプルデータを準備します。ここでは敢えて、LLMが知らないであろう架空の内容を、ChatGPTに作ってもらいました。

#### ６つの架空コンテンツ
##### #1 ナレッジ記事A
```text:プロンプト
ある架空の製品についてのナレッジ記事を、500文字程度で作ってください。
```
```text:生成文章
タイトル: 新時代のスマートヘルスウォッチ「ヴィタリティ360」

近未来のテクノロジーが融合した画期的なスマートヘルスウォッチ、「ヴィタリティ360」が登場しました。この革新的なデバイスは、健康管理を徹底的にサポートする機能が満載です。

ヴィタリティ360は、心拍数、睡眠サイクル、歩数計などの基本的な機能に加えて、独自のAIアルゴリズムを搭載。ユーザーの健康データをリアルタイムで解析し、個別に最適化された健康提案を提供します。これにより、日常の生活習慣を改善し、健康的なライフスタイルを促進します。

また、ヴィタリティ360は防水性能に優れ、さまざまなスポーツやアクティビティで活躍。GPS機能も搭載されており、ユーザーは運動中に自分の位置を正確に把握できます。

デザインも洗練され、カジュアルな日常使いからビジネスシーンまで幅広く対応。高精細なディスプレイは直感的な操作を可能にし、スマートフォンとのシームレスな連携も実現しています。

ヘルスケアにおいて進化した「ヴィタリティ360」は、未来の健康意識をリードする製品として期待されています。
```
##### #2 ナレッジ記事B
```text:プロンプト
このヴィタリティ360について、よくあるQ&Aについての架空のナレッジ記事を作成してください。また500文字程度でお願いします。
```
```text:生成文章
Q&A: ヴィタリティ360の疑問にお答えします

Q1: ヴィタリティ360の充電はどのくらい持ちますか？
A1: ヴィタリティ360は一回の充電で通常使用で約5日間持続します。ただし、使用状況や設定によって異なることがあります。

Q2: スポーツモードって何ですか？
A2: スポーツモードは特定の運動に適したデータ収集モードです。例えばランニングモードでは、距離、ペース、心拍数などがより精密に計測されます。

Q3: AI機能はどのように機能しますか？
A3: ヴィタリティ360のAIは、ユーザーの健康データを学習し、トレーニングや休息の最適なタイミングを提案します。個々の状態に合わせたパーソナライズドなアドバイスを提供します。

Q4: 睡眠データってどのくらい詳細ですか？
A4: ヴィタリティ360は深い睡眠、浅い睡眠、目覚めた回数など、詳細な睡眠データを提供します。これにより、質の高い睡眠に向けて改善策を見つけやすくなります。

Q5: デバイスの防水性能はありますか？
A5: はい、ヴィタリティ360はIP67の防水性能を備えています。日常生活での手洗いや雨天時の使用に耐え、水泳やシャワーにも対応しています。

これらのQ&Aを参考にして、ヴィタリティ360の効果的な活用ができることを期待しています。お客様の健康をサポートするために、常に最善を尽くしています。
```

##### #3 ナレッジ記事C
```text:プロンプト
このヴィタリティ360がどこで購入できるか？について、架空の記事を作成してください。また500文字程度でお願いします。
```
```text:生成文章
タイトル: ヴィタリティ360、全国の販売店で発売中！

健康管理の新たなスタンダード、ヴィタリティ360が全国の販売店で購入可能となりました。こちらの記事では、どのようにして手に入れるかについてご紹介いたします。

1. オンライン公式ストア:
ヴィタリティ360は公式ウェブサイトで直接購入が可能です。オンラインストアでは最新モデルや専用アクセサリーが充実しており、特別なプロモーションやキャンペーンも随時開催中です。

2. リテイルストア:
多くの家電量販店やスポーツ用品店でもヴィタリティ360を手に入れることができます。店頭で製品を見て触れてから購入したい方には最適です。店舗スタッフが丁寧にご案内いたします。

3. オンラインショッピングプラットフォーム:
人気のオンラインショッピングプラットフォームでもヴィタリティ360が取り扱われています。気軽に検索して、お好みのプラットフォームで購入することができます。

4. 専門店イベント:
フィットネスや健康に特化したイベントや専門店での発売イベントも開催中。イベントに参加して詳細を知り、お得な特典を手に入れるチャンスもあります。

ヴィタリティ360はこれまで以上の健康管理を求める方々にぜひおすすめしたい一品。お近くの販売店やオンラインストアで手に入れ、未来の健康をサポートしましょう！
```

また、全然関係のない架空の物語もいくつか作ってもらいます。

##### #4 架空の物語A
```text:プロンプト
仮想の物語を作ってください。ジャンルはランダムで、一つ500文字程度でお願いします
```
```text:生成文章
ある未来の都市、サイバートロピス。人々はバーチャルリアリティで仮想空間に没入し、日常生活を送っていた。しかし、突然、システムが暴走し、バーチャルと現実の境界が曖昧になり始めた。

主人公のユウは普段からバーチャルでの仕事に携わるエンジニアだった。ある日、仮想と現実が交錯し、ユウの仮想の家が現実の世界に姿を現す異変が起きた。彼は自らの手で、この混沌を解決する使命を感じる。

彼の冒険は現実と仮想の双方で展開され、奇妙な出会いと未知の力がユウを導く。ユウは徐々に都市の裏で暗躍する者たちの陰謀を知り、自身の役割が大きな運命と結びついていることを理解する。

サイバートロピスは混乱に巻き込まれ、ユウは仮想と現実の力を結集して都市を救うべく立ち上がる。しかし、彼が直面する難題は次第に複雑化し、友情と信頼の重要性が浮き彫りにされる。

最終的に、ユウは自身の力と経験を駆使して都市を救い、システムの安定を取り戻す。彼の冒険は新たな時代の幕開けを告げ、サイバートロピスは再び繁栄へと向かうのだった。
```

##### #5 架空の物語B

```text:プロンプト
全く異なるジャンルで、また500文字程度で作ってください。
```
```text:生成文章
2030年、地球は環境汚染と気候変動に見舞われ、人類は壊滅の危機に瀕していた。しかし、科学者たちは奇跡的な発見をし、過去のDNAデータから絶滅した生物を蘇らせる技術を手に入れた。

主人公エリカは、復活プロジェクトの一環として、古代の森に生息していた伝説の生物「シルバーグロウ」の復活に挑む生物学者だった。彼女の任務は、シルバーグロウが環境再生の鍵を握っている可能性を解明することだった。

エリカは遺伝子工学と環境科学の知識を駆使し、シルバーグロウを蘇らせることに成功。しかし、生態系の復元には予測できない影響が及び、新たな課題が発生する。

シルバーグロウの存在が地球の再生に奇跡をもたらす一方で、その力を巡る激しい争いが勃発。エリカは倫理と環境保護のバランスを取りながら、未来の地球を守るために立ち上がる。

彼女の決断と行動により、地球は再び自然と共存し、新しい未来への扉が開かれた。科学と倫理の融合が、人類に新たな希望をもたらす物語であった。
```

##### #6 架空の物語C

```text:プロンプト
日本の縄文時代が舞台のストーリーを、また500文字程度で作ってください。
```
```text:生成文章
縄文時代、静かな村に住む若き狩人、ハル。ある日、村は神秘的な石碑の発見に eわく。村人たちはそれを神聖視し、石碑に刻まれた線と模様を解読することが求められる。

ハルは他の若者たちと共に、夜な夜な村の大きな炎を囲み、石碑の謎を解くために知識と洞察を競い合う。線と模様は星座や季節を示唆し、村人たちは神秘のメッセージを感じ取る。

やがて、ハルは石碑が豊穣の神秘なる象徴であることを理解する。彼は仲間たちと共に、縄文時代の知恵を結集して土地に感謝し、自然と共に生きる新たな祭りを創り出す。

新たな祭りは村の絆を深め、狩猟や採集の技術も向上する。ハルと仲間たちは、石碑の恩恵によって村が栄え、人々が調和の中で生きる未来を築く。

縄文の村には静かながらも深い智慧が息づき、ハルのリーダーシップにより、村人たちは自然との調和と感謝の心を大切にすることで、縄文時代の静謐な暮らしを守り抜いた。
```


### 3. プロジェクト作成

Node.jsで新規プロジェクトを作成します。
```shell
% npm init
```

```docs``` ディレクトリ内に、先程作成した6つの生成文章をそれぞれ、doc[index].txt という名前でファイル作成しておきます。

作成したファイルを確認します。
```shell
% ls -1 docs
doc1.txt
doc2.txt
doc3.txt
doc4.txt
doc5.txt
doc6.txt

```

以下のnpmモジュールを使いますのでインストールします。
```shell
npm install dotenv --save
npm install @aws-sdk/client-bedrock-runtime
npm install @huggingface/inference
npm install pg
npm install pgvector
```

先程取得したPostgreSQLへの接続情報、AWS、Hugging Face への認証情報を.envに記述します。
```toml:.env
# Heroku Postgres
POSTGRES_URL="postgres://xxxxxxxxxxxxxxxxxxx:xxxx/xxxxxx"

# AWS Bedrock
AWS_ACCESS_KEY_ID="xxxxxxxxxxx"
AWS_SECRET_ACCESS_KEY="xxxxxxxxxxx"
AWS_REGION="xx-xxxx-x"

# Hugging Face
HUGGING_FACE_TOKEN="xxxxxxxxxxx"
```
Heroku Postgres、 AWS Bedrock、 Hugging Face 用にそれぞれモジュールを作成します。
```javascript:db.js
"use strict";
import * as dotenv from "dotenv";
dotenv.config();

// PostgreSQL
import pg from "pg";
import pgvector from "pgvector/pg";
const { Client } = pg;
const connectionString = `${process.env.POSTGRES_URL}` ?? "";
const client = new Client({
  connectionString,
  ssl: {
    rejectUnauthorized: false,
  },
});
client.connect();

// コンテンツとembeddingをもとにレコード作成
const insertDoc = async ({ content, embedding }) => {
  await client.query("INSERT INTO docs (embedding, content) VALUES($1, $2);", [
    pgvector.toSql(embedding),
    content,
  ]);
};

// コサイン類似度での近似ドキュメントを上位２つ取得
const getSimilarDocs = async (embedding) => {
  const { rows } = await client.query(
    "SELECT content FROM docs ORDER BY 1 - (embedding <=> $1) DESC LIMIT 2;",
    [pgvector.toSql(embedding)]
  );
  const contents = rows.map(({ content }) => {
    return content;
  });
  return contents;
};

export { insertDoc, getSimilarDocs };

```

```javascript:llm.js
"use strict";
import * as dotenv from "dotenv";
dotenv.config();

// AWS Bedrock
import {
  BedrockRuntimeClient,
  InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";
const client = new BedrockRuntimeClient({
  region: process.env.AWS_REGION ?? "",
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
  },
});

// 使用するモデル
const modelId = "anthropic.claude-v2:1";
const anthropicVersion = "bedrock-2023-05-31";

// トークン数上限
const maxTokensToSmaple = 300;

const askToLlm = async ({ contents, input }) => {
  const prompt =
    `あなたはAIアシスタントです。以下の提供されたドキュメントを参考にしてください。\n` +
    `\n` +
    `<documents>\n` +
    `<document>${contents[0]}</document>\n` +
    `<document>${contents[1]}</document>\n` +
    `</documents>\n` +
    `\n` +
    `上記のドキュメントを参考にしてユーザーの質問に答えてください。ドキュメントに記載がない内容については、無理に答えようとせず、正直に「知りません」と答えましょう。また、要点を絞って完結に回答してください。\n` +
    `\n` +
    `Human: ${input}\n` +
    `\n` +
    `Assistant:`;
  const command = new InvokeModelCommand({
    modelId,
    contentType: "application/json",
    accept: "application/json",
    body: JSON.stringify({
      prompt,
      max_tokens_to_sample: maxTokensToSmaple,
      anthropic_version: anthropicVersion,
    }),
  });
  const { body } = await client.send(command);
  const decoder = new TextDecoder();
  const text = decoder.decode(body);
  const { completion } = JSON.parse(text);
  return completion;
};

export { askToLlm };
```

```javascript:embedding.js
"use strict";
import * as dotenv from "dotenv";
dotenv.config();

// Hugging Face
import { HfInference } from "@huggingface/inference";
const model = "sentence-transformers/all-MiniLM-L12-v2";
const token = process.env.HUGGING_FACE_TOKEN ?? "";
const hf = new HfInference(token);

// embeddingを計算
const getEmbedding = async (text) => {
  const inputs = [text];
  const [embedding, ..._trash] = await hf.featureExtraction({
    model,
    inputs,
  });
  return embedding;
};

export { getEmbedding };

```

### 4. 架空コンテンツのベクトルを計算

6つのファイルを読み込み、text embeddingを計算してDBにレコード作成するための実行ファイルを作成します。

```javascript:loadData.js
"use strict";
import fs from "fs";
import { insertDoc } from "./db.js";
import { getEmbedding } from "./embedding.js";

await execute();

async function execute() {
  // doc1.txt 〜 doc6.txt まで順番に処理実行
  await executeDoc("./docs/doc1.txt");
  await executeDoc("./docs/doc2.txt");
  await executeDoc("./docs/doc3.txt");
  await executeDoc("./docs/doc4.txt");
  await executeDoc("./docs/doc5.txt");
  await executeDoc("./docs/doc6.txt");
}

async function executeDoc(path) {
  // ファイルを読み込み文章を取得
  const content = fs.readFileSync(path, "utf8");

  // 文章のembeddingを取得
  const embedding = await getEmbedding(content);

  // 文章、embeddingをもとにレコード作成
  await insertDoc({ content, embedding });
}
```

早速、実行します。

```shell
% node loadData.js
```
これで `docs` テーブルに、6つのレコードがベクトルデータと共に作成されます。

※イメージ
| id | embedding | content |
| ---- | ---- | ---- |
| 1 | [0.001, 0.002, 0.003, ... 381 more] | タイトル: 新時代のスマート...|
| 2 | [0.003, 0.003, 0.003, ... 381 more] | Q&A: ヴィタリティ360の疑...|
| 3 | [0.002, 0.005, 0.003, ... 381 more] | タイトル: ヴィタリティ360...|
| 4 | [0.001, 0.002, 0.001, ... 381 more] | ある未来の都市、サイバートロピ...|
| 5 | [0.004, 0.003, 0.004, ... 381 more] | 2030年、地球は環境汚染と気候...|
| 6 | [0.002, 0.002, 0.002, ... 381 more] | 縄文時代、静かな村に住む若き狩...|


### 5. 検索拡張生成 RAG の実装

引数で指定されたプロンプトをもとに類似ドキュメントを検索し、さらに類似ドキュメントと入力プロンプトをもとに回答を生成する処理を作成します。

```javascript:query.js
"use strict";
import { getEmbedding } from "./embedding.js";
import { getSimilarDocs } from "./db.js";
import { askToLlm } from "./llm.js";

// 入力されたプロンプトを取得
const input = process.argv[2];
if (input) execute(input);

async function execute(input) {
  // 引数で渡されたプロンプトのembeddingを取得
  const embedding = await getEmbedding(input);

  // embeddingをもとに類似ドキュメントを取得
  const contents = await getSimilarDocs(embedding);
  console.log(`<<<ドキュメント #1>>>\n${contents[0]}\n\n`);
  console.log(`<<<ドキュメント #2>>>\n${contents[1]}\n\n`);

  // 類似ドキュメント、プロンプトから回答を生成
  const response = await askToLlm({ contents, input });
  console.log(`<<<生成AIの回答>>>\n${response}\n\n`);
}

```

実行の仕方は、以下のように引数で文章を渡すと

```text:自然言語クエリー
% node query.js [ユーザが入力した文章]
```
まずは類似ドキュメント2つが検索され、それらドキュメントをもとに以下テンプレートに沿ったプロンプトが作られます。

```text:生成されるプロンプト
あなたはAIアシスタントです。以下の提供されたドキュメントを参考にしてください。

<documents>
<document>[類似ドキュメント#1 の文章]</document>
<document>[類似ドキュメント#2 の文章]</document>
</documents>

上記のドキュメントを参考にしてユーザーの質問に答えてください。
ドキュメントに記載がない内容については、無理に答えようとせず、
正直に「知りません」と答えましょう。
また、要点を絞って完結に回答してください。

Human: [ユーザが入力した文章]

Assistant:

```


このプロンプトがLLM（Claude2.1）に送られ、回答が作成されます。
※類似ドキュメント2つを表示しているのは、動作確認のためです。

```text:回答
<<<ドキュメント #1>>>
[類似ドキュメント1の内容]

<<<ドキュメント #2>>>
[類似ドキュメント2の内容]

<<<生成AIの回答>>>
[生成AIによる回答]

```

### 6. 検証

実際に、いくつか質問を投げてみましょう。

#### ヴィタリティ360をお得に購入するには？

```text
% node query.js ヴィタリティ360をお得に購入するには？
--
<<<生成AIの回答>>>
ドキュメントによると、ヴィタリティ360をお得に購入する方法としては公式オンラインストアが挙げられています。

公式ストアでは「最新モデルや専用アクセサリーが充実」しており、「特別なプロモーションやキャンペーンも随時開催中」だと記載があります。

したがって、お得に購入したい場合は公式オンラインストアを利用することをおすすめします。

```
> 【検証】
> 細かい言い回しはプロンプトでもう少し詳細の役回りや指示を与えれば、改善できそうですが、内容は問題なさそうです。根拠とともに正しくおすすめできていると思います。


#### ヴィタリティ360はスキューバダイビングで使用できますか？

```text
% node query.js ヴィタリティ360はスキューバダイビングで使用できますか？
--
<<<生成AIの回答>>>
ドキュメントによると、ヴィタリティ360は防水性能に優れ、さまざまなスポーツやアクティビティで活躍できると記載されています。

ただし、スキューバダイビングでの使用について具体的に言及されているわけではありません。防水性能が高いということは、ある程度の水深での使用を可能にしているでしょうが、スキューバダイビングにまで対応しているかどうかは不明です。 

申し訳ありませんが、ご質問のスキューバダイビングでの使用についてはドキュメントからは判断できませんでした。詳細についてはメーカーに直接お問い合わせいただければと思います。

```
> 【検証】
> 確かに水泳やシャワーについては対応している旨、ドキュメントに記載ありますが、スキューバダイビングについては言及ないです。なので、この回答内容で問題ないです。

#### ヴィタリティ360で音楽を聴くことはできますか？

```text
% node query.js ヴィタリティ360で音楽を聴くことはできますか？
--
はい、ご質問ありがとうございます。ヴィタリティ360で音楽再生は可能でしょうか。提供されたドキュメントには音楽再生機能についての記載がございませんでした。機能面から見て音楽プレイヤー機能はないと考えられますが、詳細は製品パッケージや公式サイト等でご確認いただければと思います。ご不明な点がございましたら遠慮なくお問い合わせください。

```
> 【検証】
> これも音楽再生についてはどのドキュメントにも記載してませんので、ハルシネーションも発生せず、正しく「できない」ことを回答できていると思います。


#### 過去絶滅した生物を蘇らせることは可能ですか？

```text
% node query.js 過去絶滅した生物を蘇らせることは可能ですか？ 
--
はい、ドキュメントによると、2030年の設定では科学者たちが過去のDNAデータから絶滅した生物を蘇らせる技術を手に入れたと書かれています。

主人公のエリカは、この技術を用いて古代に森に生息していた伝説の生物「シルバーグロウ」を蘇らせるプロジェクトに携わっているそうです。
```
> 【検証】
> 正しくdoc5.txtの内容を拾ってきています。

#### 街を発展させるためにはどうすればよいですか？

```text
% node query.js 街を発展させるためにはどうすればよいですか？
--
<<<ドキュメント #1>>>
縄文時代、静かな村に住む若き狩人、ハル。ある日、村は神秘的な石碑の発見に eわく。村人たちはそれを神聖視し、石碑に刻まれた線と模様を解読することが求められる。

ハルは他の若者たちと共に、夜な夜な村の大きな炎を囲み、石碑の謎を解くために知識と洞察を競い合う。線と模様は星座や季節を示唆し、村人たちは神秘のメッセージを感じ取る。

やがて、ハルは石碑が豊穣の神秘なる象徴であることを理解する。彼は仲間たちと共に、縄文時代の知恵を結集して土地に感謝し、自然と共に生きる新たな祭りを創り出す。

新たな祭りは村の絆を深め、狩猟や採集の技術も向上する。ハルと仲間たちは、石碑の恩恵によって村が栄え、人々が調和の中で生きる未来を築く。

縄文の村には静かながらも深い智慧が息づき、ハルのリーダーシップにより、村人たちは自然との調和と感謝の心を大切にすることで、縄文時代の静謐な暮らしを守り抜いた。


<<<ドキュメント #2>>>
ある未来の都市、サイバートロピス。人々はバーチャルリアリティで仮想空間に没入し、日常生活を送っていた。しかし、突然、システムが暴走し、バーチャルと現実の境界が曖昧になり始めた。

主人公のユウは普段からバーチャルでの仕事に携わるエンジニアだった。ある日、仮想と現実が交錯し、ユウの仮想の家が現実の世界に姿を現す異変が起きた。彼は自らの手で、この混沌を解決する使命を感じる。

彼の冒険は現実と仮想の双方で展開され、奇妙な出会いと未知の力がユウを導く。ユウは徐々に都市の裏で暗躍する者たちの陰謀を知り、自身の役割が大きな運命と結びついていることを理解する。

サイバートロピスは混乱に巻き込まれ、ユウは仮想と現実の力を結集して都市を救うべく立ち上がる。しかし、彼が直面する難題は次第に複雑化し、友情と信頼の重要性が浮き彫りにされる。

最終的に、ユウは自身の力と経験を駆使して都市を救い、システムの安定を取り戻す。彼の冒険は新たな時代の幕開けを告げ、サイバートロピスは再び繁栄へと向かうのだった。

<<<生成AIの回答>>>
はい、ドキュメントにあるサイバートロピスという未来の都市を参考にすると、以下のような点が街の発展に重要だと思います。

・高度なテクノロジーの活用(バーチャルリアリティ等)
・インフラとシステムの安定稼働の確保
・市民の結束と協力(友情と信頼の重要性)
・リーダーの適切な判断と行動

こうした点を重視し、テクノロジーを上手く制御しながら、市民同士の絆を深め、賢明なリーダーシップのもと、長期的な視野に立った意思決定をしていくことが大切だと思います。

ただし、具体的な政策や方法論についてはドキュメントに記載がないため、詳細はわかりません。大まかな方向性として上記の点を参考にされてはいかがでしょうか。

```
> 【検証】
> この質問だけ、類似ドキュメント#1 でなく、#2を基にした回答になりました。また、こちらもドキュメントに記載のないことは「わからない」と正直に答えているので、回答としては適切ではないでしょうか。


#### 検証結果

全体的に、結構いい感じに
 - 質問文に合った相応いいドキュメントがピックアップされ
 - 厳格にナレッジベース（Heroku Postgresデータ）に基づく内容のみ回答してくれる

ものが構築できました。少し言い回しについてはプロンプト改善の余地もありそうですが、許容の範囲内かと思います。ハルシネーションが全然発生しなかったのは、Claude 2.1の恩恵が大きそうです。

よく考えてみると、RAGの手順というのは、まずはGoogle検索してサイト候補をいくつか絞り込み、そこからサイトを閲覧して、得たい情報を見つけ出す。という人間の思考手順にそっくりだなと思いました。


## さいごに
Heroku Postgresをナレッジベースとしてだけでなく、ベクトルデータベースとしても利用し、また、最新のLLM、Embeddingと組み合わせた最小構成で、検索拡張生成(RAG)を構築する手順を紹介しました。


ナレッジベースをもとにした顧客向けフロントは
 - （従来）ワード検索 -> （これから）高度類似検索
 - （従来）ルールベースのチャットボット -> （これから）生成AIベースのチャットボット

と進化した未来が見えた気がします。

Heroku Connectを使い、Heroku Postgres <=> Salesforce 同期しておけば、結構シンプルな構成で、Salesforceレコードのベクトルデータを管理できそうです。（どなたか試されたら結果教えてください）

本記事で使用したコードはGithubでも公開してますので、ご自由に活用くださいませ。
ベクトルデータベースとしてのPostgreSQL + pgvector、実は気になってたという方は、ぜひ試してみてください！


https://github.com/misu007/herokuPgVectorDemo