ジェネラティブエージェンツの大嶋です。
クラスの外部から依存を注入するDI(Dependency Injection)のPythonパッケージとしてInjectorがあります。
この記事では、LangChainでDIライブラリのInjectorを使う例を紹介します。
※LangChainを使うのはあくまで一例です。Injector自体は幅広く活用できます。
関数でChainを作成する実装
InjectorやDIの話に入る前に、なぜそのようなものがあると嬉しいのかから書いていきます。
LangChainでシンプルなRAGのChainを作成して実行する関数は、次のように実装できます。
def invoke_retrieval_chain(model: BaseChatModel, retriever: RetrieverLike): prompt = ChatPromptTemplate.from_messages( [ ("system", "Answer the question.\n\nContext: {context}"), ("human", "{question}"), ], ) chain = ( {"context": retriever, "question": RunnablePassthrough()} | prompt | model | StrOutputParser() ) return chain.invoke(question)
この関数では、引数でmodelやretrieverを変更できるようになっています。
このように、「ある処理で使う依存関係を状況に応じて切り替えられるようにしたい」ということは少なくありません。
クラスを使ったDependency Injection
依存関係を状況に応じて切り替えられるようにする定番の方法の1つは、クラスを使うことです。
以下のコードでは、RetrievalChainクラスが、依存関係にあるmodelとretrieverをコンストラクタで受け取るようになっています。
class RetrievalChain: def __init__(self, model: BaseChatModel, retriever: RetrieverLike): self.model = model self.retriever = retriever def invoke(self, question: str) -> str: prompt = ChatPromptTemplate.from_messages( [ ("system", "Answer the question.\n\nContext: {context}"), ("human", "{question}"), ], ) chain = ( {"context": self.retriever, "question": RunnablePassthrough()} | prompt | self.model | StrOutputParser() ) return chain.invoke(question)
このコードは以下のように呼び出すことになります。
def main() -> None: model = ChatOpenAI(model="gpt-4o-mini") retriever = TavilySearchAPIRetriever(k=3) chain = RetrievalChain(model=model, retriever=retriever) output = chain.invoke("東京の明日の天気は?") print(output)
chain = RetrievalChain(model=model, retriever=retriever)
の箇所で依存関係を注入し、そのあとで output = chain.invoke("東京の明日の天気は?")
のように呼び出すようになっています。
このように、あるクラスが必要とする依存関係を外部から注入できるようにする実装パターンは、依存性の注入(DI:Dependency injection)と呼ばれます。
上記のコードぐらいであれば、手作業で依存を注入してもそれほど大変ではないかもしれません。
しかし、本格的なアプリケーションになってくると、依存関係の受け渡しの手間が大きくなる場合があります。
そのような状況では、DI用のライブラリを使うと便利です。
Injectorの導入
ここから、PythonのDIライブラリ「Injector」を導入したコードの例を紹介します。
まず、依存関係を注入したいクラスのコンストラクタに @inject
というデコレータをつけます。
from injector import inject class RetrievalChain: @inject def __init__(self, model: BaseChatModel, retriever: RetrieverLike): self.model = model self.retriever = retriever
続いて、コードのどこかで、依存関係として注入するインスタンスを定義します。
from injector import Module, provider class ProdModule(Module): @provider def model(self) -> BaseChatModel: return ChatOpenAI(model="gpt-4o-mini") @provider def retriever(self) -> RetrieverLike: return TavilySearchAPIRetriever(k=3)
Injectorで依存を解決したうえでRetrievalChainを使うコードは次のようになります。
from injector import Injector def main() -> None: injector = Injector([ProdModule()]) chain = injector.get(RetrievalChain) output = chain.invoke("東京の明日の天気は?") print(output)
まず、injector = Injector([ProdModule()])
の箇所で、依存関係として何を使うのかを設定しています。[ProdModule()]
のようにリストを渡すことができるので、依存関係は複数のクラスに分けて定義することもできます。
続いて、chain = injector.get(RetrievalChain)
の箇所で、依存関係を注入したうえで RetrievalChain のインスタンスを作成しています。
※Injectorで依存関係の定義する方法は、Module
を継承したクラスを使う以外に、binder
を引数とする関数を実装する方法もあります。詳細は InjectorのREADME を参照してください。
依存関係の切り替え
依存関係を切り替えたいシーンの定番としては、自動テストが挙げられます。
次のようなコードで、テスト時に使いたい依存関係を定義して使うことができます。
class TestModule(Module): @provider def model(self) -> BaseChatModel: responses: list[BaseMessage] = [AIMessage("fake output")] return FakeMessagesListChatModel(responses=responses) @provider def retriever(self) -> RetrieverLike: return RunnableLambda(lambda _: [Document("fake document")]) def test_retrieval_chain() -> None: injector = Injector([TestModule()]) chain = injector.get(RetrievalChain) output = chain.invoke("東京の明日の天気は?") assert output == "fake output"
まとめ
この記事では、PythonのDIライブラリ「Injector」を使う例を紹介しました。
実際には、ここで紹介した程度の簡単なコードでは、Injectorまで使わずコンストラクタで依存関係を注入できれば十分なことが多いと思います。
もしもアプリケーションが大きくなってきて、依存関係の解決が大変だと思ったときは、Injectorの導入を検討してもいいかもしれません。