AIエージェントの力でコーディングの泥沼から脱出した話
コーディングスキルゼロと情熱だけでチャットボット構築に飛び込んだ結果、 v0.1がCSVデータベースと原始的なチャンキングの惨事だったことに気づきました。 AIエージェントが救い出してくれるまでは。
アップデート(2026年): このチャットボットはSydneyに進化しました!何度もイテレーションを重ねた結果、Sydneyは現在/ask/で活動し、ブログコンテンツと製品に焦点を当てています。
昨年の11月初旬に、DIYチャットボットのバージョン0.1をリリースしました。その時、「v0.1はコーディング初心者としての大きな一歩だったが、大きな制限があった」と書きました。結果的に言えば、それは控えめな表現でした。プロセス全体と自分の成果物がどれほど不格好だったか、当時は理解していませんでした。実際のところ、私の最初の試みは、真剣ではあったものの、情熱はあっても知識が限られた状態で寄せ集めたプロトタイプに過ぎませんでした。
この記事は単なるフォローアップではありません。そこから展開した旅への深掘りです。試行錯誤と貴重な教訓に満ちた旅です。この冒険の細部を包み隠さず公開するのは、透明性のためだけでなく、私の経験が同じような道を歩んでいる誰かの共感を呼んだり、役に立てばという思いからです。(補足として、私はコーディング経験ゼロの中年の広告業界人です。)
前述の通り、チャットボットv0.1をリリースするために、主にこの短期コース「Building Systems with the ChatGPT API」と、OpenAIの2つのクックブック:Question answering using embeddings-based searchとHow to count tokens with tiktokenの手順に従いました。
チャットボットv0.1がひどかった理由はこちらです:
- チャンキング:長いブログ記事をトークン長に基づいて小さなチャンクに分割しただけでした。つまり、最も原始的なデータの静的文字チャンク分割です :D これがなぜひどいアイデアなのかを理解したい方は、Greg Kamradtによるテキスト分割の5つのレベルについてをお読みください。
- エンベディング:エンベディングに苦労しました。OpenAIのエンベディングモデルを使いましたが、APIリクエスト制限に何度もぶつかり、エンベディング処理が途中で失敗しました。その後、リクエストをバッチ処理し、バッチ間にタイムアウトを追加して制限を回避することを学びました。最終的に、生成されたエンベディングを簡易な.csvファイルに保存して、間に合わせの「データベース」としました。
- データベース:CSVがデータベースとして最適でないことは分かっていましたが、より良い代替手段を使うスキルがありませんでした。
- メタデータ:公開日や記事のURLなどのメタデータを含めることが、チャットボットがユーザーの質問に正確に答えるために重要だということに、最初は気づいていませんでした。関連するメタデータを組み込むために、エンベディングと保存を繰り返す必要がありました。
- リトリーバー:異なるリトリーバーの種類やアルゴリズムを知りませんでした。OpenAIの関連性検索を使って、ハードコードされた数の結果を取得するだけでした。
- メモリ:会話をするためには、チャットボットがユーザーの以前の発言を記憶できる必要があります。そしてここが、gpt-3.5の限られたコンテキストウィンドウ長(当時)では、チャンクサイズ、取得したい結果の数の間に明確なトレードオフがありました。
- 例えば、チャンクサイズが800トークンでリトリーバーが上位8件の結果を返す場合、6,400トークン、つまり旧モデルの制限の50%以上を占めます。
- 上記は1つの質問だけなので、マルチターンの会話ではメモリがいかに早く埋まるか想像できるでしょう。
- この問題を解決する方法の一つは、チャンクサイズを小さくし、リトリーバーが返す結果を少なくすることですが、基本的なリトリーバー(上記)では、モデルが質問に答えるための包括的な情報を持てないことを意味します。
- IDEさえ使っていませんでした。すべてのコードはMacのTextEditで編集していました :D(私が初心者だったと言いましたっけ? :P)
- まだまだ続けられますが、お分かりいただけたかと思います。
私の「死の谷」
v0.1の制限を超えて改善しようと意気込み、スキルアップに必要なピースを提供してくれることを期待して、いくつかのオンラインコースに挑戦しました。しかし、進歩は行き止まりにしかつながりませんでした。
ベクトルデータベース(Vector Databases: from Embeddings to Applications with Weaviate)、評価型RAG手法(Building and Evaluating Advanced RAG Applications with Llama-Index and Truera)、高度な検索テクニック(Advanced Retrieval for AI with Chroma)の演習に悪戦苦闘しました。どれだけ努力しても、理論を自分のブログデータを使った実践に結びつけることができませんでした。
コースの設計が悪かったのか?いいえ。不足していたのは私自身の基礎知識でした。それでも、失敗に次ぐ失敗は非常にフラストレーションが溜まりましたし、言うまでもなく意気消沈しました。先に進む方法がわからない、比喩的な谷に自分がいることに気づきました。
時間をかけた小さな勝利
それでも、繰り返しの失敗からは価値のある発見がありました:
-
TextEditからVS Codeへの移行
-
GitHub Copilotエクステンションの活用
-
開発環境としてのJupyter Notebookの価値を理解
最後に受けたコースでLangChainに言及していました。チャットボット構築のための人気の新しいフレームワークです。実は数ヶ月前にLangChainのチュートリアル(「LangChain: Chat with Your Data」と「Functions, Tools and Agents with LangChain」)を試みましたが、あまりうまくいきませんでした。しかし今、苦労して得た知識を身につけた状態で、そのドキュメントを改めて読んでみると、非常に啓発的でした。概念がつながり、そのモジュラーアーキテクチャが直感的に理解できるようになりました。
LangChainの堅牢な機能を自分のパッションプロジェクトに適用できるイメージが湧きました。ついに、前進する道が見えたのです!一歩ずつ、データ取り込み、エンベディング、ストレージ、検索のパイプラインに慣れていきました。
実装できる部分が増えるごとに自信が育ちました。V2が形を取り始めました...
基盤の再構築
Langchainをガイドに、チャットボットをゼロから再構築し始めました:
WordPressエクスポートをJSONに取り込む
取り込みパラメータの試行錯誤の後、LangChainのJSONLoaderがエクスポートした記事を適切にパースできるようになりました。これで、検証済みの入力が下流のパイプラインに供給できます。(このステップのコードは、このパブリックGithub リポジトリの「DataIngestionAndIndexing.ipynb」ファイルにあります。)
自動テキスト分割
素朴なトークン長チャンキングは、LangChainのSentenceTransformersに置き換えられ、高度なNLPを使ってセマンティックユニットで分割するようになりました。文が途中で切断されることはもうありません!設定により、メモリ制約のあるモデルに適切なサイズのチャンクを維持しています。
from langchain.text_splitter import SentenceTransformersTokenTextSplitter
# Define the token splitter with specific configurations
token_splitter = SentenceTransformersTokenTextSplitter(
chunk_overlap=0, # Overlap between chunks
tokens_per_chunk=256 # Number of tokens per chunk
)
# Split the documents into chunks based on tokens
all_splits = token_splitter.split_documents(documents)
print(f"Total document splits: \{len(all_splits)\}")
エンベディングとインデックスの生成
過去のOpenAI APIリミットとの格闘は、LangChainのOpenAIエンベディングラッパーを使うことで解消されました。たった2行のコードに凝縮され、エンベディングが分割テキストから重要な特徴をきれいに抽出しました。
ベクトルストアには、WeaviateやChromaよりもFAISS(Facebook AI Similarity Search)を選びました。業界で実績のあるFAISSは、私のニーズに対して機能と複雑さのちょうど良いバランスでした。CPU版がブログのチャンクを高速にインデックス化し、コンパクトで検索可能なデータベースを出力しました。バッチ処理やOpenAIへのAPIリクエスト制限を心配する必要がなくなりました。
Langchainは複数のベクトルストアをサポートしているので、こちらで確認できます。
# Initialize embeddings and FAISS vector store
embeddings = OpenAIEmbeddings()
db = FAISS.from_documents(all_splits, embeddings)
# Save the vector store locally
db.save_local("path/to/save/faiss_index") # Placeholder for save path/ index name
たった2行のコードです!それだけです。
気づいたこととして、コンテンツが少量でない場合(私は400以上のブログ記事があります)、エンベディング直後にリトリーバーを使おうとすべきではありません。インデックスが完成/安定するまでに少し時間が必要だからです。
リトリーバーの評価
このリトリーバーのセットアップはたった1行のコード :D で、FAISSをベクトルストアとして使用しています。
retriever = db.as_retriever(search_type="mmr")
インデックスとリトリーバーが整ったことで、インテリジェントなチャットボットに供給するデータパイプラインの準備ができました!
会話エージェントの設計
Langchainのエージェントフレームワークを使ってこのチャットボットを構築することにしました。この時点ではやりすぎでしょうか?はい、やりすぎです。しかし、時間をかけてこのチャットボットを進化させ、より多くの「ツール」つまり機能を持たせたいと考えています。Langchainを使えば、エージェントのセットアップとツールの提供がとても簡単です。
embeddings = OpenAIEmbeddings()
db = FAISS.load_local("path/to/your/faiss_index_file", embeddings) # Replace the path with your actual FAISS index file path
retriever = db.as_retriever(search_type="mmr")
tool = create_retriever_tool(
retriever,
"search_your_blog", # Replace "search_your_blog" with a descriptive name for your tool
"Your tool description here" # Provide a brief description of what your tool does
)
tools = [tool]
prompt_template = ChatPromptTemplate.from_messages([
# Customize the prompt template according to your chatbot's persona and requirements
])
llm = ChatOpenAI(model_name="gpt-3.5-turbo-1106", temperature=0)
llm_with_tools = llm.bind_tools(tools)
agent = (
\{
"input": lambda x: x["input"],
"agent_scratchpad": lambda x: format_to_openai_tool_messages(x["intermediate_steps"]),
"chat_history": lambda x: x["chat_history"],
\}
| prompt_template
| llm_with_tools
| OpenAIToolsAgentOutputParser()
)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
エージェントの最終Pythonコード全体
最後に、チャットボットv2を試したい方はこちらからどうぞ。
チャットボットがChandlerについて何も知らないのは変ですよね?
P.S: チャットボットが私について何も知らないと教えてくれた方々、ありがとうございます。その通りです!「About」ページのエクスポートを忘れて、「公開済み記事」だけをエクスポートしていたからです。これで2回目の忘れなので、評価質問のリストに自分に関する基本的な質問を含めることにします。教訓です!
建設的なフィードバックを共有してくださった皆さん、ありがとうございます。ぜひ引き続きお寄せください。はい、チャットボットの起動がとても遅いのは分かっています。それも取り組んでいます。 :| (初心者だと言いましたっけ? :P)
簡単なアップデート
チャットボットが私について何も知らない問題は修正されました。以下が私がやったことと学んだことです:
- WordPressから「About me」ページを.XMLにエクスポート
- 上記と同様にテキスト分割を行い、FAISSを使用してエンベディングを生成。ベクトルストアを別の名前でローカルに保存してリトリーバーをテスト
# save the vector store to local machine
db.save_local("faiss_index_about")
# set up the retriever to test the new vector store about Chandler
from langchain.retrievers.multi_query import MultiQueryRetriever
llm = ChatOpenAI(temperature=0)
retriever_from_llm = MultiQueryRetriever.from_llm(
retriever=db.as_retriever(), llm=llm
)
# test retriever
question = "Who is Chandler Nguyen?"
results = retriever_from_llm.get_relevant_documents(query=question, top_k=8)
for doc in results:
print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
- 結果として、2つのFAISSベクトルストアを統合するプロセスは驚くほど簡単でした。ドキュメントをご覧ください。
# Try to merge two FAISS vector stores into 1
# load the vector store from local machine
from langchain_community.vectorstores import FAISS
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
db_1 = FAISS.load_local("faiss_index_about", embeddings)
db_2 = FAISS.load_local("faiss_index", embeddings)
db_2.merge_from(db_1)
# save the vector store to local machine
db_2.save_local("faiss_index_v2")
# test the new vector store to confirm correct retrieved documents
retriever = db_2.as_retriever(search_type="mmr")
results = retriever.get_relevant_documents("Who is Chandler Nguyen?")
for doc in results:
print(f"Content: {doc.page_content}, Metadata: {doc.metadata}")
- その後は、新しいベクトルストアを使って、上記とほぼ同じプロセスです
2月14日アップデート チャットボットv2.10公開
このチャットボットのデプロイから2週間後、ユーザーエクスペリエンスの向上、スピードの改善、スケーラビリティとシンプルさを備えたバージョン2.10を公開しました。詳しくはこちらをお読みください。
3月25日アップデート:フロントエンドのアップグレードからDockerの苦闘とブレークスルーへ
詳しくはこちらをお読みください。





