僕が A.I. に特に惹かれるのは、それが「人間であるとはどういうことか」というこちらの考えを、少なくとも認知と創造性のレベルで、かなり近い距離からつついてくる技術だからだ。そんなことを言える技術はそう多くない。そして、この A.I. の瞬間が持つ重みは、まだ僕らの肩に完全には乗りきっていないと思う。コンテンツ制作のコストが劇的に下がるなかで、生きて動いている歴史の中にいるとはどういうことか、という話だ。実際、そういう瞬間を生きた人類の大半は、印刷機の先に宗教改革とその後のあれこれが来ることも、インターネットの先に大統領選挙が来ることも予見できなかったはずだ。そんなわけで、いくつかの Proof of Concept (PoC) をあちこちで作ったあと、僕はいま、従来のソフトウェアの書き方と、データ駆動の開発が進んでいく形とのあいだに、根本的な違いがあると感じる場所に着地している。従来型のプログラミングと機械学習の違いは、あの格言に近い。
「人に魚を一匹与えれば、その人は一日食べられる。魚の釣り方を教えれば、その人は一生食べていける。」
ML/LLM 駆動開発は、ソフトウェア開発のパラダイムシフトである
そもそも僕らの多くは、ソフトウェア開発の現場で実際に何が起きているのかさえ大して分かっていない。そこへデータ駆動開発が、「疑似科学」っぽい試行錯誤と確率的な考え方を大量に投げ込んでくる。結果として、関わるプロセス、ワークフロー、ツールの多くは、経験豊富なエンジニアにとってすらまるで新大陸になる。この記事は、この二つの実践の歴史的な流れと違いをかなりうまく整理していた(ただ悲しいことに、僕が気づいたことの一つは、技術の速度と広がりは地域によってあまりにも違うので、僕らはもう全員タイムマシンの中で暮らしている、ということでもある。SFTP とか思い浮かぶ…)。
問題の核心は結局こうだ。機械に学習をどう教えるのか。集めるデータから、次元削減、モデル選択、ハイパーパラメータ調整などに至るまで、提供プロセスのあらゆる段階で実験を続けながら、エンドユーザーにはどう安定したサービスを出すのか。大規模言語モデルは、自然言語、知識、推論の強力な道具一式を持ち込んでくれるが、もちろん癖もある。だから、こうした課題の総和として、これらのシステムをスケールさせて提供するには、レジストリを基盤にし、インターフェースで駆動するアプローチが必要だと気づいた。もちろん ChatGPT の助けを借りて、である。
これは少し科学のプロセスに似ている。繰り返し出てくるタスクやワークフローがあり、結果を常に監視する必要があり、理想を言えば、提供のトリガーまで自動化したい。そこで、これまで作ってきたいくつかの実験、そこに出てきた反復パターン、そしてこのシステムをどう作ろうとしているのかを、大まかに書いておく。
週末プロトタイプ
週末で何が作れるのか。やってみると、かなりいろいろ作れる。一つ作ると次が出てくる。新しいアプリや体験の宇宙がまるごと広がっている。ただし最終的な制約は開発者の時間だ。だから今の僕の関心は、どの開発者でも効率を 100 倍、できれば 1000 倍にできる(といいな)ような構成要素を作ることにかなり寄っている。
Bertrand - プロンプトから公開まで
動機/ユースケース:最小限の労力と保守で、毎月 100 ドル払ってくれる人を 1000 人集めるには何を作ればいいのか、と考えていた。それなら、プロンプトから公開までをつなぐ仕組み、あるいは完全自動のソーシャルメディア代理店のようなものが答えになりそうで、技術的にもいけそうに見えた。そこで、受講中のコース用のフラッシュカードを生成してみた。上で触れた機能をいじりながら、返ってくる内容がより関連性の高いものになるのか、かなり限定されたシナリオで LLM の生成能力をどこまで引き伸ばせるのかを見たかった。
仮説:LLM に新しい知識を取り込み、記憶を持たせれば、より良い検索拡張生成(RAG)ができ、体験もパーソナライズできる。
Bertrand - ChatGPT の、プロンプトから公開までを扱うプラグイン
結果と学び:これが GraphQL とベクトルデータベース、具体的には Weaviate との最初の接触だった。そして、以前のプレゼンテーションで延々と話していた埋め込みについて、かなり理解が深まった。必要なのは、個別のプロセスをつなぎ合わせて、新しいトレンドの学習、コンテンツの生成、スケジュール設定、公開までを端から端まで自動化する方法だ、と思った。そして当然、そういうものはあった。Langchain と Deep Lake を見つけ、次の PoC でさらに触り始めた。
Arthur - 検索して取り出す
動機/ユースケース:ChatGPT で作っているうちに、そして少ない資源で最大のリターンを取りたいという全般的な好みもあって、LLM 搭載のペアプログラマーが、ドキュメント、文書、Web 検索などから自力で新しいことを学び続けてくれたらかなりいいなと思うようになった。さらに、指示すれば自律的にコードを書き、コンパイルし、遭遇したエラーをもとに自分で直してくれたらなお良い。検索して読むより、探している答えを含む返答がそのまま返ってくるほうが速い。アクセントや返答待ちで人間同士のやり取りが難しく、やたら時間を食うことにもかなり苛立っていた。だったら、既存のコードベースを A.I. に学習させて、それに質問すればよくないか。A.I. が既存プロジェクトの構造とコードベースを理解していれば、プロンプトから機能実装へ進むのは、それほど遠い跳躍ではなさそうに見える。
仮説:LLM がさまざまな知識源を意味のある形で扱うための戦略を作れば、何に対しても問い合わせ、生成できる。
Arthur - 何でも検索して取り出す
結果と学び:生成されるコンテンツの品質は、それが本当に重要なら少し問題になる。なので、より大きなポイントは、LLM 搭載アプリが俊敏性と反復の水準をまったく別のところへ引き上げることだ。途中のあらゆる段階を反復し、最適化できる。もっとコンテンツを食わせれば、もっと良いコンテンツを生成するかもしれない。コンテンツの意味をよりうまく理解させれば、検索と生成が改善するかもしれない。.json をオブジェクト単位で分割する方法を作った。筋は良さそうだったが、トークン上限にぶつかったので、取り出すチャンク数を制限する必要があった。取り込み対象になりうるデータソースは、文書、コード、音声、動画、画像など何であれ、それをより小さく、あるいはより意味のある単位へ分ける方法がたくさんある。使える埋め込みモデルも違うし、ベクトルストア(このときは Deep Lake を使った)や検索戦略も違う。だから本番には、次のようなシステムが本当に必要になる。
-
安定していること - ユーザーが一貫した体験を得られるように。
-
拡張しやすいこと - データ取り込みの新しいソースや新しい埋め込みモデルなどを簡単に追加できるように。
それから ZOMG、仮想環境とコンテナ化は本当に大事だ。コードを一行書く前のセットアップ工程の 4 分の 3 くらいを占めている気がする…
The Sound of Stories
動機/ユースケース:僕が関心を持っているのは、こうした技術をリアルタイムで進む実際のパフォーマンスに投入し、A.I. で人間の能力を拡張することだ。つまり、これまで不可能だった新しいことを可能にすること。語り手本人であれ、大切な人であれ、その声で、多言語の物語を観客が聞けるインタラクティブなストーリーテリング体験。その最終形につながりそうな部品をいろいろ試してきたので、全部を組み合わせて人前に出すのは自然な次の一歩に思えた。
仮説:A.I. によって新しい体験や能力を解放し、積み重ねられる。たとえば、テキスト生成と他のサービスをつないで、生成された物語を読み聞かせてもらう、といった形だ。
The Sound of Stories
結果と学び:LLM ベースのアプリの UI を試作するには、Streamlit は正直いまいちだ。Chainlit のほうが良さそう。SeamlessM4T にはややがっかりしたが、僕がよく知っている言語については、テキストからテキスト、テキストから音声、音声クローンはもう解けた問題に見える。むしろ本当に厄介なのは、レイテンシや対応言語の広がりのほうかもしれない。3 つの PoC すべてに共通しているのは、データソースを取り込み、何らかの方法で分割し(特定の文字数で切る、オブジェクト単位で切る、など)、埋め込みを生成するというパターンだ。最初の二つでは、その埋め込みを検索用にベクトルデータベースへ保存した。今回のものでは、かなりの部分が「プロンプトエンジニアリング」だった。
余談:この PoC のあと、正直なところ、プロンプトエンジニアリングが本物の職業になるとは思わなくなった。仮になったとしても、タイプライター係の現代版みたいな仕事になるのではないか。このプロンプトを反復するプロセス全体は、自動化したほうがずっと向いている類のものに感じる。実際、スケールしたときの最大級の課題はこうなるはずだ。自分の調整がどれだけ効いているのかを、どう監視し、評価するのか。人間をループに入れるのか。MLOps で可能な限り端から端まで自動化するのか。
この PoC では、異なるサービス間でデータを受け渡しし、マルチモダリティをもう少し触ってみたかった。たとえば、生成されたテキストを SeamlessM4T に渡して音声を作らせるように、LLM が生成した出力を別のサービスが消費できる。もちろん、何らかの形でエンドユーザーに表示することもできる。言い換えると、この種の RAG では、消費または提示のレイヤーもカスタマイズ可能なのだ。
したがって、開発環境と本番環境で安定し、拡張しやすいシステムであることに加えて、さらに必要なのは次の性質だ。
-
柔軟であること - ワークフローを簡単に組み替えたり、構成要素を差し替えたりできるように。
-
スケールできること - リアルタイムで一定水準のレイテンシと品質を保証できるように。要するに、本当の規模で動かしたときの性能が気になっている。
Part I:拡張可能な取り込み、チャンク分割、埋め込みシステムを設計する
では、このシステムは実際にはどんな形になるのか。僕はいまもリファクタリングしながら、工程を一つずつ踏み、新しい用語やプロセスを作りながら学んでいる。ひとまず、大枠だけ下に整理しておく。
レジストリを用意する。
このレジストリは、ファイル拡張子と、それぞれに対応するプロセッサの対応関係を保持する。各プロセッサは、自分が担当するファイル形式をどうチャンク分割し、どう解析するかを知っているべきだ。
FILE_TYPE_PROCESSORS = {
".py": PythonFileProcessor,
".md": MarkdownFileProcessor,
# Add other file types and their processors here...
}基底クラスを用意する。
from abc import ABC, abstractmethod
class DataSource(ABC):
"""
Base class for any data source. This provides a generic interface for data ingestion.
"""
@abstractmethod
def ingest(self):
"""
Ingest data and return a consistent format for further processing.
"""
pass
@abstractmethod
def get_metadata(self):
"""
Extract metadata from the data source. This metadata can vary depending on the source type.
"""
pass
class GitRepoSource(DataSource):
def ingest(self):
# Logic to clone/pull git repos and filter relevant files.
pass
def get_metadata(self):
# Extract metadata specific to git repositories.
pass
class PDFSource(DataSource):
def ingest(self):
# Logic to ingest PDFs.
pass
def get_metadata(self):必要に応じて、ファイル処理、チャンク分割、埋め込み、そしてパイプライン上のその他の工程についても同じことを繰り返す。
CHUNKING_STRATEGIES = {
".py": PythonCodeChunker,
".md": MarkdownChunker,
# ... other chunkers
}つまり、たとえば次のような、設定可能で柔軟な処理ロジックが必要になる。
def process_files(files, embedding_strategy_name):
embedding_strategy = EMBEDDING_STRATEGIES[embedding_strategy_name]()
for file_path in files:
file_extension = os.path.splitext(file_path)[1]
file_processor = FILE_TYPE_PROCESSORS.get(file_extension)
if not file_processor:
print(f"No processor found for {file_extension}. Skipping...")
continue
chunks = file_processor().process(file_path)
for chunk in chunks:
embedded_text = embedding_strategy.embed(chunk["content"])
# Store embedded_text and metadata...このようなレジストリと抽象クラスによるアプローチには、次の利点がある。
-
スケーラビリティ: 辞書(
FILE_TYPE_PROCESSORSとEMBEDDING_STRATEGIES)を使い、ファイル形式や埋め込み戦略を、それぞれ対応するプロセッサへ簡単に対応付ける。新しいファイル形式や埋め込み手法への対応は辞書を拡張するだけで済むため、この設計は本質的にスケールしやすい。 -
堅牢性:関心事をより明確に分離し、障害をより効果的に隔離できる。あるファイルプロセッサへの変更が、他のプロセッサに影響するべきではない。
-
拡張性:拡張しやすいように設計している。先ほど触れたように、新しいファイル形式や埋め込み戦略に対応するには、新しいクラスを作って該当するレジストリに追加すればよい。既存コードへの変更は最小限で済む。
-
柔軟性:柔軟性が高い。構成要素は辞書に登録されているため、差し替えや追加が素直にできる。たとえば “.py” ファイル用のプロセッサを変更するなら、
FILE_TYPE_PROCESSORS辞書のエントリを更新するだけでよい。
上の仕組みは、素早い開発や共同作業にはかなり有利だ。ただし思い出してほしい。僕らが欲しいのは、本番で安定して一貫したシステムだ。そこで、インターフェース駆動というもう一つの話が出てくる。それについては次の記事で触れる。
初出は PubPub の erniesg.pubpub.org/pub/zc0zx741 です。