LangChainでPythonのDIライブラリ「Injector」を使う例の紹介 #LangChain

ジェネラティブエージェンツの大嶋です。

クラスの外部から依存を注入するDI(Dependency Injection)のPythonパッケージとしてInjectorがあります。

github.com

この記事では、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の導入を検討してもいいかもしれません。

なお、この記事で使用したInjectorの使用例は以下のGitHubリポジトリで公開しています。

github.com