はじめまして。高坂と申します。AIを触り始めて3か月ほどの法学徒です。いわゆるバイブコーダーとして興味のあるものを作っています。今回は、自作のRAGチャットボット(HN-RAG)を題材に、記事を書いてみます。qiitaの利用は初めてで記事を書くのも不慣れなので、拙い部分も多いかと思いますが、全くの初心者がRAGチャットボットを作成するまでの過程をまとめているので、良ければ最後までご覧ください。
まず、リポジトリは以下のものです。
→https://github.com/shg195/hn-rag
背景
まずは簡単に自己紹介がてら背景をお話ししたいと思います。AIをコーディングなどにも使い始めたのは、今年の4月頃です。AI(ここでいうAIはほぼLLMですが)自体はチャットベースで2023年終わり頃から継続的に利用していました。その時は無知なりに、いわゆるclaude.md, skills, memory機能と思想を同じくするプロンプトを自分で再発明したりしていました。4月以降にAIを触っていくうちに、自分の作っていたものが確立された技術だと知った時には驚きました。そんな感じでチャットベースで利用をしていましたが、春休み頃からもっとAIを使ってみたくなって、開発のようなことを始めました。
本題ですが、今回の主題であるHN-RAGを説明するにあたり、前提となるDIfyとn8nで作成したhacker news(以下、HN)digest botを紹介します。これは、Difyとn8nを利用して、ポイント数とコメント数が閾値以上にあるHNの記事を毎朝自動取得して、NotionDBに保存しつつメールで記事とコメント欄の要約を配信、NotionDBで興味のある記事にチェックを入れると5分毎に周回しているn8nがそれを自動取得し詳細記事が作成されるというものです。ワークフローとしては、①HN自動取得→閾値判定→閾値以上の記事とコメント欄を取得→qwen2.5:7b-instructに渡して要約→NotionDBに保存しつつメール配信、②5分毎にn8nがNotionDBを観察→記事にチェックが入っていればその記事とコメント欄URLを取得→claude APIを叩きclaude haiku4.5に渡す→詳細記事をNotionに生成、というものです。
今回のHN-RAGは、この詳細記事を対象に、RAGを構築しています。背景および前提はこんな感じでしょうか。以降は本題、HN-RAGについてのお話です。
補足ですが、筆者はRAGについての知識が一切ない状態からこのRAGを作成しました。ただ、構築の過程で都度AIに技術や仕組みを質問し、現在では少なくともこの記事に書いてある内容であれば自分で説明できる程度の知識状態にはなっています。RAG構築のセッション最後には、学んだ内容や質問を踏まえたRAG教科書を作成しています。どのような知識を土台としてこの記事書かれているかもわかると思いますので、よければそちらもご覧ください。noteで公開する予定です。noteはXのプロフィールからご覧ください。
技術スタックと選定理由は以下のようなものです。
埋め込みモデル:cl-nagoya/ruri-v3-30m
理由:日本語特化かつローカルで無料。
ベクトルストア:ChromaDB
理由:AI選定。座標の保管と近傍検索と永続化に便利そうだと判断。
LLM:Claude Haiku 4.5
理由:ローカルで済ませたかったが、ハルシネーションを減らしたり出典準拠の回答をさせるにはある程度の性能をもったモデルが良いと判断。その中でもAPI費を抑えるためにこのモデルを選択。
対話UI:Streamlit
理由:最小工数でチャット画面化できるため。
Notion取得:notion-client
理由:AI選定。再帰でページを潜る処理が素直とのこと。情報源になるNotionページが親ページの下に月ごとのページがあり、その中に記事を入れています。
主要パラメータについては、チャンク長500、オーバーラップ100、最小チャンク50、検索はtop-5、クエリ書き換えに渡す履歴は直近3往復、取り込み実績は約60記事、355チャンクです。
設計判断についてお話します。
①埋め込みモデルは用途の言語で選択する。
最初はAIに提案された英語中心のモデル(all-MiniLM-L6-v2)を使いました。その結果、日本語の検索がまるで当たらず、無関係な記事が上位に来て「分かりません」を連発しました。日本語特化の Ruri に差し替えただけで、検索精度が劇的に改善しました。モデルのサイズやアルゴリズムの前に、扱う言語に合った埋め込みモデルを選ぶことが重要だと実感しました。RAGの精度の話になると検索アルゴリズムやリランキングに目が行きがちですが、土台の埋め込みが言語に合っていないと、結局全てやり直しになるので、埋め込みモデルの選定は慎重に行っていきたいです。
②正規化とコサイン
埋め込みモデルと切り替えた直後、類似度スコアが-431のように、暴れていました。これはベクトルの長さがスコアに影響しているためでした。この原因特定については、AIより先に自分で特定できたところなので記憶に残っています。埋め込みを正規化し、ChromaDB側もコサイン距離に設定したところ、スコアが暴れなくなりました。スコアがきれいに表示されるようになったことで、後述する出典の扱いも素直になりました。
③チャンク分割を固定長にした理由
理想は、チャンクは見出し単位で切った方が良いとは思います。情報源としている詳細記事はマークダウンでかかれていますし、意味のまとまりでチャンクを分ける方がきれいです。ではなぜ固定長分割なのかというと、筆者の場合は、以下の理由があります。
- 見出しの名前、数が揺れている。前述のHN digest botを作る以前の話になるのですが、自動化する前はカスタムGPTを利用して半自動化していました。自動化に当たり、見出しの名前や数を調整をしました。また、カスタムGPTの利用にあたっても記事の形式を都度改善していたため、記事の中でも見出しの名前や数が揺れています。
- 自動生成された記事がマークダウンとして保存されない。これはHN digest botの修正案件なのですが、自動化で作成した詳細記事は見出しブロックを持っておらず、##や**が文字としてべた書きされた巨大なブロックに全文が入ってしまっています。
- チャンク分割の手法を色々と試したかった。言い訳的にはなりますが、先の2つの理由から、一旦は固定長分割をためして、HN digest botの修正が完了次第、見出し分割に変更するとしました。自分用のもので、かつ学習的な側面も多分にあるので、まずは固定長、その後意味分割を試すことができるようにしました。(HN digest botの修正はすぐかもしれませんが、新しく学ぶことが楽しすぎて修正より前に進みたいという考えが大きすぎたというのもあります。)
以上のような理由から、構造関係なしに分割できる固定長+オーバーラップにしました。端数は最小チャンク長(ここでは50字未満に設定)を足切りしますが、オーバーラップ100字があるので、情報の取りこぼしはほぼないと思います。
④追い質問とクエリ書き換え
チャットボットについてですが、API経由では「もっと詳しく」などと言ってもコンテキストを有していないため、チャットを往復するのであれば工夫が必要です。そこで、今回は履歴を踏まえて独立した質問に書き換えてから検索するという手法を採用しました。調べた限り、LangChainのhistory-aware retrieverと同じ思想だと思います。この方式では、LLMの呼び出しが2回になります。①書き換え(指示+履歴+最新質問、履歴には回答も含める)→ ②回答生成(指示+資料+書き換え後の質問)。チャットボットの出典に書き換え後の質問も載せるようにしているので、質問の解釈が適切かどうかも確認できます。
⑤出典の明示
回答には出典を示してほしいわけですが、LLMの自由記述では信頼できないので、出典は検索結果のメタデータから機械的に取り出して表示するようにしています。記事タイトルと類似度をメタデータから引くだけのため、回答本文の信頼性とは別レイヤーで出典の正しさを担保しています。
コスト感
コストについてですが、埋め込み、検索、Notion取得は全て無料、回答生成のみAPI費がかかります。とはいえ、個人で動かし続ける分には負担にはならない程度です。
まとめ
ここまで読んでいただき、ありがとうございます。RAGの構築にはチュートリアルなどもあるそうですが、それはこの記事を書いている最中に知りました。この記事を読まれる方からすれば、筆者の書いている内容などは当たり前のものばかりかもしれません。しかし、筆者としては数日でHN digest botで自動化→RAGの学習・構築をできたのは非常に楽しい経験でした(タイトルに3日とありますが、内訳は自動化に1日、RAGとチャットボット構築を同日で1日、本記事執筆で1日の計3日です。現実時間で正確にいえば、それぞれの間には試用期間として数日ずつ空いているので自動化開始→本記事投稿までは1週間程度です)。チャンク分割の手法やクエリの分解など、RAGについてもまだまだ試したい手法があるので、記事を書いている間にも何をしようかと楽しみで仕方がない状態です。今後は、エージェント化し、その後MCPで外部クライアントから叩けるようにもしていきたいと考えています。とはいえ、それらについても言葉しか知らない状態なので、学びつつ作っていきます。私の以上の知識はここ3か月、特に5月中盤~後半にかけてのものなので、誤っている箇所、理解が不正確であったり浅い箇所も多いと思います。そういった箇所はコメントなどで教えて頂けると幸いです。Xやnoteもありますので、良ければそちらもご覧ください。
補足:記事本文に書き忘れていました。私が普段利用しているモデル(記事内でAIと表記しているもの)は、基本claude opus系統です。RAG構築段階ではclaude opus4.7です。RAG構築ではブラウザで利用していましたが、コーディングメインの時はcursorでclaude codeを利用することも多いです。