はじめに
社会人1年目のプログラミング初心者が、チャットボットアプリを開発してみました。今回の記事では、以前書いた記事のアプリケーション開発(コードの部分)に関する深堀りをしていきたいと思います。
【Amazon Bedrock × Streamlit】社内ガイドラインを利用したチャットボットアプリ開発
この記事を読むことで、少しでもStreamlitやLangchainを用いたチャットボットアプリ開発の理解を深めていただければ幸いです。
開発環境
- Windows 11
- Python 3.12.3
- streamlit 1.39.0
- boto3 1.35.41
- langchain 0.3.3
- langchain-aws 0.2.2
- langchain-chroma 0.1.4
- langchain-community 0.3.2
- langchain-core 0.3.11
- langchain-openai 0.2.2
本記事の流れ
- 1 章:アプリ概要
- 2 章:作成したコードの概要説明
- 3 章:今回使用したStreamlitのメソッドまとめ
- 4 章:作成したコードの詳細説明
- 5 章:振り返り
- 参考資料
1 章:アプリ概要
チャットボットによる社内文書検索と質問応答
社内文書(ガイドラインやルールなど)をもとにしたチャットボットを開発しました。
主な機能
質問応答:ユーザーが入力した質問に対して回答を提供します。
参考資料一覧提示:チャットボットが読み込んでいる社内文書から質問応答に使用した関連情報を参考資料一覧として提示します。
チャット履歴リセット機能:Clear Conversationsボタンを押すことで、チャット履歴をリセットします。
チャットボット画面
① メイン画面:
② 利用規約画面(記事公開のため省略ver):
③ リンク先画面(記事公開のため省略ver):
2 章:作成したコードの概要説明
私が作成したコードは以下の流れで構成されています。
主な流れ
-
Webインターフェースの構築
Streamlitでサイドバーとタイトルを作成し、チャット履歴リセット機能を追加しました。 -
LLMの設定
ChatBedrockクラスを使用し、特定のモデルIDを設定しました。 -
Amazon Kendraによる情報検索設定
KendraインデックスIDを設定し、検索結果の信頼度と件数を定義しました。 -
S3バケットからのデータ読み込み
S3バケットからCSVを読み込んでデータフレームに変換しました。 -
質問への回答生成機能
質問を文脈化し、リトリーバと回答チェーンを統合して回答を生成しました。 -
チャット履歴の管理
セッション内でチャット履歴を管理し、RAGチェーンを統合しました。 -
アプリ内の質問入力と回答管理
質問を受け付け、回答を生成して履歴に保存・表示しました。 -
利用規約と関連リンクの表示
利用規約や関連リンクページを表示するための関数を実装しました。 -
ページ表示の制御
サイドバーの選択に基づいて表示するコンテンツを決定しました。
3 章:今回使用したStreamlitのメソッドまとめ
コードの詳細説明を行う前に、今回実装したコード内で使用したStreamlitのメソッドをまとめておきます。
- st.container:複数のStreamlit要素をコンテナ内にグループ化し、表示するために使用
- st.sidebar:サイドバー内にタイトルを表示
- st.sidebar.title:サイドバー内にタイトルを表示
- st.sidebar.radio:サイドバー内にラジオボタンを表示し、複数の選択肢から一つ選択
- st.title:ページ全体に大きなタイトルを表示
- st.session_state:ユーザーのセッション状態(変数の値など)を保持・管理
- st.write:テキスト、データフレーム、画像などのほぼすべてのデータ型を表示
- st.chat_message:チャット形式でメッセージを表示させ、ユーザーとアシスタントのメッセージを区分して表示
- st.chat_input:ユーザーがチャット形式でメッセージを入力するためのテキスト入力欄を表示
- st.markdown:マークダウン形式のテキストを表示
以上がそれぞれの機能の簡潔な説明です。
4 章:作成したコードの詳細説明
実際に構築したアプリのコードについて、重要な箇所に絞って段階的に説明します。
(一部箇所は省略しています。ご了承ください。)
① 必要なライブラリのインポート
# インポート:LangChainのモジュール
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.chat_history import BaseChatMessageHistory, InMemoryChatMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_aws import ChatBedrock
from langchain_aws import AmazonKendraRetriever
from langchain_core.messages import AIMessage
from langchain_core.messages import HumanMessage
# インポート:Streamlitとその他のユーティリティ
import streamlit as st
from io import StringIO
import boto3
import pandas as pd
import random, string
import logging
from logging.handlers import SysLogHandler
上記は今回のコード実装にあたり、使用したライブラリです。以下はそれぞれの簡単な説明です。
langchain:大規模言語モデル(LLM)を活用した各種チェーンを作成するためのライブラリ
- create_history_aware_retriever:チャット履歴を考慮して、適切な文脈に基づいた情報を検索するための関数
- create_retrieval_chain:質問に対して情報を検索し、その結果を用いて回答を生成するためのチェーンを作成する関数
- create_stuff_documents_chain:検索されたドキュメントを組み合わせて1つの回答を生成するためのチェーンを作成する関数
- BaseChatMessageHistory:チャット履歴を保持する抽象クラス
- InMemoryChatMessageHistory:メモリ内でチャット履歴を管理する具体的なクラス
- ChatPromptTemplate:チャットボットの応答生成に使用するテンプレートを作成するためのクラス
- MessagesPlaceholder:テンプレート内でメッセージのプレースホルダーとして使用するクラス
- RunnableWithMessageHistory:チャット履歴を取り込みながらチェーンを実行するクラス
- ChatBedrock:AWSの大規模言語モデルを使用するためのクラス
- AmazonKendraRetriever:Amazon Kendraを使用してドキュメントを検索するためのクラス
- AIMessage:AIからユーザーへのメッセージを表現するクラス
- HumanMessage:ユーザーからAIへのメッセージを表現するクラス
streamlit:ウェブアプリを簡単に構築するためのフレームワーク
StringIO:文字列をファイルのように扱うためのモジュール
boto3:PythonからAWSサービスにアクセスするためのライブラリ
pandas:データ操作や分析のためのライブラリ
random, string:ランダム文字列の生成に使用されるモジュール
logging:主にログメッセージの生成に使用されるモジュール
datetime:日付と時刻を操作するためのモジュール
② Streamlitを使用したユーザーインターフェースの作成
# 画面作成 (streamlit)
# サイドバーにナビゲーションを作成
with st.container():
st.sidebar.title("ナビゲーション")
selection = st.sidebar.radio("選択してください", ["社内ガイドラインチャットボット", "利用規約", "リンク先"])
if st.sidebar.button("Clear Conversations", type="primary"):
del st.session_state['ChatMessageHistory']
del st.session_state['ChatMessageHistoryForView']
# アプリのタイトル
with st.container():
st.title("社内ガイドラインチャットボット")
Steamlitを用いてメイン画面を作成している箇所になります。
具体的には、サイドバーにナビゲーションメニュー、タイトルを実装し、"Clear Conversations"ボタンが押されたら、チャット履歴をリセットする機能も実装しました。
こんなにも短いコードで画面を実装することができ非常に便利ですね!
③ 大規模言語モデル (LLM) の設定
# LLMの定義
llm = ChatBedrock(
model_id="anthropic.claude-3-haiku-20240307-v1:0",
model_kwargs={"max_tokens": 1000}
)
LLMの設定をしました。ChatBedrockクラスを作成し、特定のモデルIDを指定しています。
今回は、Anthropic社の「Claude-3-haiku」バージョン「20240307-v1:0」を使用しました。
必要に応じてこれを他のモデルに変更してください。
また、"max_tokens"を変えることで、応答として返す最大文字数を設定できます。
④ 検索サービス(Amazon Kendra)の設定
# Kendraの定義
#パラメーターチューニング
top_k=5
min_score_confidence=0
# Retrieverの定義
kendra_index_id = "各自のKendra Index IDに書き換えてください"
attribute_filter = {"EqualsTo": {"Key": "_language_code","Value": {"StringValue": "ja"}}}
retriever = AmazonKendraRetriever(index_id=kendra_index_id,
attribute_filter=attribute_filter,
top_k=top_k,
min_score_confidence = min_score_confidence)
Amazon Kendraを用いた情報検索の設定をしました。
kendra_index_idは各自のものに書き換えてください。
パラメータの詳細説明:
index_id:KendraインデックスIDを渡します。
top_k:検索結果として返される上位k件の項目数を指定します。ここでは5件の結果を返す設定をしています。
min_score_confidence:検索結果のスコアの信頼度に対する最低値を設定します。0は制限を設けないことを意味します。0~1の範囲でスコアの設定が可能です。
⑤ S3からCSVファイルを読み込む
# S3にアクセス
# バケット名
bucket_name = "各自名前を付けてください"
# CSVファイル名
obj_name = "リンク対応表.csv"
# S3インスタンス
s3 = boto3.client("s3")
# CSVファイル参照
# オブジェクト取得
csv_file = s3.get_object(Bucket=bucket_name, Key=obj_name)
csv_file_body = csv_file["Body"].read().decode("utf-8")
# データフレームとして出力
df = pd.read_csv(StringIO(csv_file_body))
指定したS3バケットから保存されているCSVファイルを読み込み、読み込んだCSVの内容をデータフレームに変換して保持する機能を実装しました。
⑥ 質問を文脈化し、応答チェーンを生成(★重要)
# 質問を文脈化する
# 文脈化システムプロンプトの定義
contextualize_q_system_prompt = ""
# 文脈化用のChatPromptテンプレートの作成
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_q_system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
# 履歴を考慮したレトリーバーの作成
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
# 質問への回答
# システムプロンプトの定義
system_prompt = (
"You are an assistant for question-answering tasks. "
"Use the following pieces of retrieved context to answer "
"the question. If you don't know the answer, say that you "
"don't know. Use three sentences maximum and keep the "
"answer concise."
"\n\n"
"{context}"
)
# 質問応答用のChatPromptテンプレートの作成
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
# 質問応答チェーンの作成
question_answer_chain = create_stuff_documents_chain(llm, qa_prompt)
# RAGチェーンの作成
rag_chain = create_retrieval_chain(history_aware_retriever, question_answer_chain)
ユーザーの質問に対して文脈を考慮した適切な回答を生成するように実装を行いました。
詳細:
- 情報検索機能(レトリーバー)の作成:
過去のチャット履歴を参考にして質問を文脈化するプロンプトテンプレートを作成および履歴を考慮した情報検索機能(レトリーバー)を作成しました。 - 質問応答チェーンの作成:
ステムプロンプトを設定して、質問応答用のテンプレートを作成し、それを基に質問応答チェーンを作成しました。 - RAG(Retrieval-Augmented Generation)チェーンの作成:
情報検索機能と質問応答チェーンを統合して、RAG(Retrieval-Augmented Generation)チェーンを完成させました。
これにより、過去のチャット履歴や関連する情報を基にしたより正確で一貫性のある応答を実現しています。
※RAG(Retrieval-Augmented Generation):RAGは「検索拡張生成」と訳されるAI技術です。簡潔に述べると、AIがデータベースから情報を検索し、その情報を参照しながら回答を生成する仕組みのことです。Retrieval(情報検索)とAugmented Generation(生成の拡張)で分解して考えるとわかりやすいです!
Retrieval(情報検索):
S3に格納してある資料から関連性の高い情報(資料)を検索します。
Augmented Generation(生成の拡張):
検索で取得した情報を基に、AIはユーザーの質問に適切な回答を生成します。これにより、単なる生成モデルが持つ限界を超え、より具体的で正確な回答が可能になります。
⑦ チャット履歴の状態管理
# チャット履歴の状態管理
# 空のchat履歴
if 'ChatMessageHistory' not in st.session_state:
st.session_state['ChatMessageHistory'] = {}
if 'ChatMessageHistoryForView' not in st.session_state:
st.session_state['ChatMessageHistoryForView'] = {}
st.session_state["chat_history_session_id"] = randomstr(16) + "_" + dt_now_str
st.session_state['ChatMessageHistoryForView'][st.session_state["chat_history_session_id"]] = {}
st.session_state['ChatMessageHistoryForView'][st.session_state["chat_history_session_id"]]["messages"] = []
# セッション履歴の取得関数
def get_session_history(session_id: str) -> BaseChatMessageHistory:
if session_id not in st.session_state['ChatMessageHistory']:
st.session_state['ChatMessageHistory'][session_id] = InMemoryChatMessageHistory()
return st.session_state['ChatMessageHistory'][session_id]
# チャットRAGチェーンの設定
conversational_rag_chain = RunnableWithMessageHistory(
rag_chain,
get_session_history,
input_messages_key="input",
history_messages_key="chat_history",
output_messages_key="answer",
)
セッション内でチャット履歴を管理し、RAGチェーンを用いたチャットシステムを構築しました。
詳細:
- 空のチャット履歴の初期化
- セッション履歴の取得関数の定義:
session_idを受け取り、そのセッションIDに対応するチャット履歴を取得し、チャット履歴が存在しない場合は新しく作成する設定を行いました。 - チャットRAGチェーンの設定:
RunnableWithMessageHistoryクラスを利用して、RAGチェーンとチャット履歴管理を統合しました。
⑧ チャット機能の実装(★重要)
# 質問入力関数の定義
def question_input():
question = st.chat_input("質問を入力してください")
if question:
query = question
st.session_state['ChatMessageHistoryForView'][st.session_state["chat_history_session_id"]]["messages"].append(HumanMessage(query))
with st.chat_message("user"):
st.write(query)
# 質問に対する回答を生成
result = conversational_rag_chain.invoke(
{"input": query, "chat_history": st.session_state['ChatMessageHistory']},
config={"configurable": {"session_id": st.session_state["chat_history_session_id"]}}
)
documents = result["context"]
# 参考資料名を格納するリスト
titles = []
box = []
# ドキュメントのタイトルをリストに追加(重複を避ける)
for i in range(len(documents)):
title = documents[i].metadata["title"]
if title not in titles:
titles.append(title)
# 各タイトルに対して一致するリンクを検索し、出力
for title in titles:
# 一致するリンクを検索
matched_links = df[df['リンクファイル名'].isin([title])]['リンク']
# 一致するリンクを出力
for link in matched_links:
if link.startswith("\\"):
link = "\\" + link
Link = title + ":" + st_write_sep + link
# 文字列中の_をエスケープする処理
Link = Link.replace("_", "\\_")
box.append(Link)
links = st_write_sep.join(["参考資料一覧:"] + box)
# 最新のAIメッセージを取得して更新
st.session_state['ChatMessageHistory'][st.session_state["chat_history_session_id"]].messages.pop()
latest_ai_message_with_links_str = st_write_sep.join([result["answer"], links])
ai_message_with_links = AIMessage(latest_ai_message_with_links_str)
st.session_state['ChatMessageHistory'][st.session_state["chat_history_session_id"]].messages.append(ai_message_with_links)
st.session_state['ChatMessageHistoryForView'][st.session_state["chat_history_session_id"]]["messages"].append(ai_message_with_links)
# 古いメッセージを削除して最新のメッセージのみ保持
st.session_state['ChatMessageHistory'][st.session_state["chat_history_session_id"]].messages = st.session_state['ChatMessageHistory'][st.session_state["chat_history_session_id"]].messages[-max_message_times*2:]
# 回答を表示
with st.chat_message("assistant"):
st.write(latest_ai_message_with_links_str)
# チャット履歴をファイルに保存
with open(f"./chatbot_history/{st.session_state['chat_history_session_id']}.txt", mode="a", encoding="UTF-8") as f:
f.write("---質問---------------------------------------\n")
f.write(query + "\n")
f.write("---回答---------------------------------------\n")
f.write(latest_ai_message_with_links_str + "\n")
ユーザーによる質問入力から回答生成および生成された回答と関連するリンクを表示、チャット履歴の保存までのプロセスを扱う関数 question_input() を定義しました。
詳細:
- ユーザーからの質問受け付け:
st.chat_input を使用してユーザーからの質問を受け付け、質問が入力されたら現在のチャットセッションの履歴に追加し、ユーザーの質問として表示しました。 - 質問に対する回答生成:
conversational_rag_chain.invoke を呼び出して、文脈情報を基に質問に対する回答を生成しました。 - 関連ドキュメントとリンク収集:
検索結果の文書からタイトルを収集し、それに関連するリンクを検索しました。 - チャット履歴へ保存し、ユーザーに表示:
回答とリンクを結合して、AIからのメッセージとしてチャット履歴に追加し、表示しました。チャット履歴をテキストファイルに保存して、後で参照できるように設定しておきました。
⑨ 利用規約ページとリンクページ
# 利用規約画面の内容
def terms_and_conditions():
st.title("利用規約")
st.write("""
1. 個人情報の入力は禁止とします。
2. 悪用可能な出力を得る目的で生成指示を行うことは禁止とします。
""")
st.title("留意事項")
st.write("""
1. 出力結果の正確性は完全には保証されないことを前提にご利用ください。
""")
# リンク先画面の内容
def links_page():
st.title("関連リンク")
st.write("以下は関連するリンク先情報です。")
st.markdown("- [ガイドライン]()")
利用規約と関連リンクのページ内容を定義しました。
⑩ ページ表示の制御
# 選択に基づいてページを表示
if selection == "社内ガイドラインチャットボット":
st.session_state['page'] = "社内ガイドラインチャットボット"
# 繰り返し処理
for key, value in st.session_state['ChatMessageHistoryForView'].items():
messages = value["messages"]
for message in messages:
if type(message) is HumanMessage:
user_text = message.content
with st.chat_message("user"):
st.write(user_text)
elif type(message) is AIMessage:
assistant_text = message.content
with st.chat_message("assistant"):
st.write(assistant_text)
question_input()
elif selection == "利用規約":
st.session_state['page'] = "利用規約"
terms_and_conditions()
elif selection == "リンク先":
st.session_state['page'] = "リンク先"
links_page()
ユーザーの選択に応じて、表示するページを制御する機能を実装しました。
5 章:振り返り
今回のチャットボットアプリ開発を通じて、プログラミング初心者として多くのことを学びました。ここでは、その振り返りとして主なポイントをまとめてみました!
1.Streamlitの使いやすさ
最初に感じたことは、Streamlitの使いやすさでした。簡単なコードでUIを構築できるので、初心者でも気軽に開発を進めながら、画面を徐々に作り上げていくことができてとても楽しかったです。
具体的なエピソード:プロジェクトの初期段階で、チャットボットのメイン画面とユーザーからの質問入力欄をStreamlitで実装しました。リアルタイムでフィードバックがあるため、コードを修正しながら即座に結果を確認できる点が、開発のモチベーションを高く保つことに繋がりました!初めてユーザーインターフェースの設計に成功したときの達成感は非常に大きかったです!
2.LangChainの凄さ
LangChainを利用することで、自然言語処理の複雑な部分をシンプルに扱える点が非常に強力でした。特に、回答生成やRAGチェーンといった高度な機能を比較的簡単に実装できることが、スムーズな開発につながりました。
具体的なエピソード:LangChainを使って、回答生成やRAGチェーンの実装というチャットボットの中心部分を作り上げた時は、とても感動しました!とはいえ、LangChainのドキュメントは初心者には難しくて何度もつまずき、途中で嫌になることもありました。それでも先輩社員の助けに支えられてなんとか完成させることができました。この成功したときには嬉しさでいっぱいになり、近所のラーメン屋に行って爆食したのはここだけの話です(笑)。
3.エラーメッセージへの苦労
初心者として多くのエラーに直面し、それを解決していく過程で多くの学びを得ることができました。最初はエラーメッセージの読み方や解決に向けた調べ方も分からず、戸惑うことが多かったですが、エラーメッセージを理解し、それを修正する過程を通じてスキルが向上したことを実感しています。
具体的なエピソード:特に苦労したのが、question_input() 関数を定義した際です。この関数は、ユーザーが質問を入力して回答を生成し、その回答と関連するリンクを表示し、チャット履歴を保存する一連のプロセスを扱いました。この複雑な関数を実装する過程で、何度もエラーが続いて修正に多くの時間がかかりました。インターネットで調べるだけでなく、先輩社員の助言を受けながら、プログラミングの基礎を身に着け、エラー解決に向けた取り組み方に関して多くのことを学びました。
4.大規模言語モデル(LLM)の学び
ChatGPTは以前からよく使っていましたが、開発経験もない1年目の私には、「こんな難しいことが自分にできるのか?」と不安でした。しかし、実際にLLMを使ったチャットボットアプリを開発してみたところ、大変な部分もたくさんありましたが、想像以上に簡単にできて驚きました。テクノロジーの進歩は本当にすごいと感じました。
具体的なエピソード:最も大きな学びとなったのは、LLMを活用することで技術的な可能性が大きく広がることを実感した点です。特に回答生成機能において、関連する情報を検索し、その情報に基づいた回答を提供するRAGチェーンを実装しました。これによって、社内ドキュメントに基づいた回答をスムーズに生成できる様子を見て、その精度と便利さに感動しました。また、異なるモデルを比較し、どのモデルが最適なのかを検討するプロセスでも多くのことを学びました。モデルの種類や特性、精度の違い、さらにはどのプロンプトが最も効果的かについて深く理解することができました。この経験を通じて、LLMの利用を通じた技術の進歩とその可能性を強く感じました。
5.まとめ
今回の開発を通じて、プログラミングの基礎から始めて実際に動くアプリケーションを作り上げるというプロセスを経験し、多くの知識とスキルを得ることができました。今後もこれらの経験を活かして、さらなるアプリケーション開発に挑戦していきたいと思います。また、開発したチャットボットをさらに良いものにしていくために、利用者へのアンケートやログデータの分析、費用対効果の評価を行うことで、利用者が使いやすいアプリケーションを目指し改善を続けていきます。最後に、チャットボット開発を進めるにあたり、根気強く多くの助言と指導を行ってくださった先輩社員のHさんにここで感謝申し上げます。ありがとうございました。
参考資料
参考①:Amazon BedrockとStreamlitで簡単チャットアプリを作成する方法
参考②:Amazon BedrockとStreamlitでチャットボットを作成してみた
参考③:LangChainからKendraでRAGする(その1)-Amazon Bedrock APIで始めるLLM超入門⑤
参考④:LangChainからKendraでRAGする(その2)-Amazon Bedrock APIで始めるLLM超入門⑥
参考⑤:Streamlit
参考⑥:AmazonKendraRetriever
参考⑦:AmazonKendraRetriever "Could not load credentials" error in latest release #7571
参考⑧:RunnableWithMessageHistory to output source documents in RAG #20041
参考⑨:RAG(検索拡張生成)とは | 生成AIとの関係性や仕組み、活用事例を紹介
上記参考資料に加えて、streamlit, langchain, pythonの公式ドキュメントを参照しました。