LT大会で「LangGraphでのHuman-in-the-Loopの実装」というタイトルで話しました #StudyCo

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

StudyCoのLT大会で「LangGraphでのHuman-in-the-Loopの実装」というタイトルで話しました。

studyco.connpass.com

発表資料はこちらです。

speakerdeck.com

ソースコードはこちらです。

github.com

この記事には、発表内容に含めなかった感想などを書こうと思います。

LangGraphのフレームワークらしさ

LangGraphの機能を活用したHuman-in-the-Loopを実装してみると、LangGraphが処理の全体的な流れを担い、その流れの部品を実装することになります。

そのことから、LangGraphのフレームワークらしさを改めて実感しました。

個人的にはLangChain(とくにLCEL)もフレームワークだと理解することが実は重要で、フレームワークのつもりでキャッチアップすると上手に使いやすい、という感覚がある気がしています。

より本格的なアプリではどうか

今回のLTで紹介したのは、Streamlitのアプリケーションでの簡単なデモ程度の実装でした。

実際のアプリケーションでHuman-in-the-Loopのように処理の流れの根幹に関わる機能を実装するときは、フレームワークの機能を使う/使わないという判断も重要だと思います。

より本格的に、例えばSlackアプリに組み込んだりすると、LangGraphでのHuman-in-the-Loopの実装がどこまで便利で実用的なのかよりよく理解できそうです。近々挑戦したいです。

Notebookのコードをアプリケーションに

今回話題にしたLangGraphでのHuman-in-the-Loopの実装については、公式ドキュメントに解説とNotebookでのサンプルコードが記載されています。

しかし、Notebookのサンプルコードでは、実際にアプリケーションに組み込むにはどうするんだ?と試行錯誤することになりがちです。

LangChain/LangGraphの使い方を広めるうえでも、簡単ではありますがアプリケーション化したコードを紹介できてよかったです。

おわりに

今回のLT大会自体でも、自分の知らない話をたくさん聞くことができ、とても勉強になりました。

また時々LT大会を開催する予定なので、「自分も話したい!」という方は是非お声がけください!

「Difyソースコードリーディング#2 ―Difyの開発環境を起動してみる」を開催しました #もくもくDify

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

もくもくDifyで「Difyソースコードリーディング#2 ―Difyの開発環境を起動してみる」というイベントを開催しました。

dify-mokumoku.connpass.com

アーカイブ動画はこちらです。

youtube.com

Difyのソースコードは以下です。

github.com

今回のポイント

Difyの開発環境の起動方法

Difyの開発環境の起動方法自体は、CONTRIBUTING_JA.md の「インストール」の箇所に書かれている通りの手順でした。

手順自体は少し長めでしたが、まったくエラーに遭遇せず起動できました。

開発環境の起動と、docker/docker-compose.yamlでの起動の違い

補足になりますが、CONTRIBUTING_JA.md に書かれているのは、Dify自体の開発のためのセットアップ手順です。

docker/docker-compose.yaml にもDifyをDocker Composeで起動する手順が書かれていますが、こちらの場合は開発モードではなく、コードを変更しても反映されません。

UIの文言の修正

せっかく開発環境を起動できたので、UIの文言を少し変更してみました。

web/i18n ディレクトリのファイルを編集すると、しっかりUIに反映されました。

設定の変更

昨日のDifyもくもく会で話題になった、チャンクサイズの最大値の変更も挑戦してみました。

チャンクサイズを1000より大きくすると Custom segment length should be between 50 and 1000 と表示されるので Custom segment length should be between で検索してコードの該当箇所を探しました。

最終的にはINDEXING_MAX_SEGMENTATION_TOKENS_LENGTHという環境変数を変更することで、チャンクサイズの最大値を変更できることが確認できました。

コードの該当箇所:https://github.com/langgenius/dify/blob/3e7597f2bd0ff686e7a1ea1972f0421ed0568fbb/api/.env.example#L258

dify-docs

Difyのアプリケーション以外に、dify-docsも少し見てみました。

github.com

こちらはGitBookが使われているようで、マークダウン形式のドキュメントを更新すれば反映されるようです。

translate.py という自動翻訳のスクリプトが置かれていて、このスクリプトの内部ではDifyのアプリケーションを呼び出しているようでした。

dify-sandbox

最後に、dify-sandboxも少しみてみました。

github.com

Go言語で実装されていて、システムコールを制限したりした環境でPythonやNode.jsを実行できるようでした。

詳細までは分からなかったので、またそのうちじっくり読みたいです。

次回のご案内

Difyソースコードリーディングの第3回として、次回はAPIのリクエストからレスポンスまでどんなふうに実装されてるのか読み解いたり、どんなAPIがあるのか見てみる予定です。 ご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

また、水曜日にもDifyのもくもく会が開催されます。 こちらではDifyの使い方の解説として、テンプレートをみていく予定となっています。 こちらもご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

LangChain Meetup Tokyo #2で「LangChainの現在とv0.3にむけて」というタイトルで話しました #LangChainJP

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

LangChain Meetup Tokyo #2で「LangChainの現在とv0.3にむけて」というタイトルで話しました。

langchain.connpass.com

(西見さんが撮影した写真)

発表資料はこちらです。

speakerdeck.com

当日の様子はこちら。

togetter.com

「LangChainの現在とv0.3にむけて」の補足的な話

LangChainは以前に比べて変更も落ち着き、使い方も程よくまとまってきて、扱いやすくなっていると感じます。

LangSmithやLangGraphといったエコシステムを活用できる強みも大きいです。

先日、langchain-coreを0.1.42から0.2.36(その時点の最新)まで一気にバージョンアップする機会がありましたが、その際ERROR・WARNINGには一つも遭遇しませんでした。

  • 最新の使い方は公式ドキュメントでキャッチアップして、deprecatedな機能を避ける
  • 無理やりカスタマイズして解決しようとしない(無理のない方法で対応する)
  • コードの構成上、LangChainに依存する範囲を小さくする・コードの多くの箇所が依存するLangChainの機能を限定する

といった点を意識すると、より扱いやすくなるように思います。

おまけの話

昨日は想定より進行が早かったため、追加でLangGraphでのHuman-in-the-Loopの実装例を紹介しました。

ソースコードは以下のGitHubリポジトリで公開しています。

github.com

こちらの詳細は今週木曜日のLT大会で話す予定です。

studyco.connpass.com

おわりに

他の方の発表も楽しく、今回のLangChain Meetupはとても良い会だったと思います。

Discordも盛り上がっていて、次回も楽しみです。

LangChain Meetupでも他の場でも、今後もLangChainについてアップデートや使い方のコツなどを発信していきます!

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

「Difyソースコードリーディング#1 ―Difyのシステム構成をざっくり把握」を開催しました #もくもくDify

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

もくもくDifyで「Difyソースコードリーディング#1 ―Difyのシステム構成をざっくり把握」というイベントを開催しました。

dify-mokumoku.connpass.com

アーカイブ動画はこちらです。

youtube.com

Difyのソースコードは以下です。

github.com

ポイントのまとめ

今回ソースコードリーディングしていて、ポイントだと思った点をまとめます。

docker/docker-compose.yaml

まず、今回は第一回としてDifyのシステム構成(フロントエンド・バックエンド・データベースなどの構成)を把握したかったので、そのあたりが把握できそうなファイルから読んでみました。

Difyのソースツリーを見るとdockerというディレクトリがあり、その中にあったdocker-compose.yamlを読むと、基本的なシステム構成を把握できるようになっていました。

docker/docker-compose.png

同じディレクトリにはとくに中心的な構成要素のつながりを整理した図もあり、参考になりました。

CONTRIBUTING_JA.md

その後、コントリビュータ向けのCONTRIBUTING_JA.mdを開いて読んでみると、ソースコードの構成についての説明もあり、ソースコードリーディングの起点として役立つ内容がたくさん記載されていました。 CONTRIBUTING_JA.mdには、以下のような内容が記載されていました。

  • モデルプロバイダーやツールプロバイダーを追加するときに読むべきガイドへのリンク
  • バックエンドがFlask・SQLAlchemy・Celeryで作られていること
  • フロントエンドがNext.js・Tailwind CSSで作られていること
  • バックエンドとフロントエンドのおおまかなディレクトリ構成

Difyのソースコードリーディングに挑戦する方は、まずはこのCONTRIBUTING_JA.md(またはCONTRIBUTING.mdなど)を読んでみるのがおすすめです。

api/core/model_runtime/model_providers/openai/llm/gpt-4o-2024-08-06.yaml

OpenAIのLLMとのインテグレーションについては、モデルごとの機能やコンテキストサイズなどがYAMLファイルに記載されており、モデルの追加時に素早く対応できるようになっていました。

api

その後、apiディレクトリ(バックエンド)のソースコードをざっと見て、雰囲気をつかんでいきました。

api/models

データモデルに対応するコードも簡単に見て、今回のソースコードリーディングは終了となりました。

次回のご案内

Difyソースコードリーディングの第2回として、次回はDifyの開発環境を起動していろいろさわってみるつもりです。 ご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

また、水曜日にもDifyのもくもく会が開催されます。 こちらではDifyの使い方の解説として、テンプレートをみていく予定となっています。 こちらもご興味ある方はぜひ気軽にご参加ください。

dify-mokumoku.connpass.com

勉強会「【LangChainゆる勉強会#11】LangGraphでのHuman-in-the-loopの実装」を開催しました #StudyCo

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

運営している勉強会コミュニティStudyCoで「【LangChainゆる勉強会#11】LangGraphでのHuman-in-the-loopの実装」というイベントを開催しました。

studyco.connpass.com

アーカイブ動画はこちらです。

youtube.com

勉強会の内容

この勉強会では、LangGraphでのHuman-in-the-loopの例として、エージェントがツールを使う際に人間が承認する挙動をライブで実装していきました。 LangGraphの 公式ドキュメント のコードをベースとして、Streamlitのアプリとして実装を進めました。

配信した1時間弱ではある程度動いたあたりまででしたが、その後追加で時間をとって以下のGIFのような動作まで完成させました。

コードはGitHubで公開しています。

github.com

実装の詳細については、9/5(木)の LT大会 で簡単に話す予定です。

LangGraphでのHuman-in-the-loopの実装自体よりも、Streamlitでの状態管理で混乱・苦戦しました...

ほぼ事前準備なしでコーディングするだけの配信でしたが、参加者の方には想像していたよりも楽しんでいただけたようです。 またこのような会も企画しようと思います!

LangChainゆる勉強会シリーズ

「LangChainゆる勉強会」と題して、LangChainをその場のノリで解説するゆるい勉強会をしています。

3月から2〜3週間に1度のペースで開催していて、今回は第11回でした。

次回の開催はまだ未定ですが、もしも「こんな話が聞きたい」というテーマがあれば、ぜひお声がけください!

LLMアプリケーションのテストに関する決定版ガイド「The Definitive Guide to Testing LLM Applications」の紹介 #LangChain

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

先日LangChainから、LLMアプリケーションのテストに関する決定版ガイド「The Definitive Guide to Testing LLM Applications」が公開されました。

LangChain公式によるXでのアナウンスはこちらです。

ガイドのPDFは以下のページからダウンロードできます。

www.langchain.com

この記事では、こちらのガイドの概要を紹介します。

LLMアプリケーションにおけるテストが重要な理由

当ガイドでは、LLMアプリケーションにおけるテストが重要な理由を3つ挙げています。

First, LLMs are non-deterministic, generating a distribution of possible outcomes from a single input. This can lead to inconsistent or hallucinated outputs. Additionally, LLMs ingest arbitrary text, forcing developers to grapple with a broad domain of possible user inputs. And, finally, LLMs output natural language, often requiring new metrics to assess style or accuracy.

  1. LLMは非決定論的であり、出力には一貫性がなかったりハルシネーションが発生したりする
  2. LLMは入力として任意のテキストを受け取るため、ユーザーの入力を幅広く想定する必要がある
  3. LLMの出力は自然言語のため、文体や正確性を評価する新たな指標が必要になることが多い

LLMアプリケーションのテストの3つのステージ

LLMアプリケーションのライフサイクルの中で、テストは以下の3つのステージに適用できると整理されています。

  • Design
  • Pre-Production
  • Post-Production

この3つについて、順に解説されています。

Design

まずはアプリケーションの設計上の工夫として、エラーが発生した際にLLMにフィードバックして出力を修正させること (self-correction) が挙げられています。

LLMアプリケーションの処理がうまくいかなかった際にLLMにフィードバックする処理は、RAGの工夫として使われることもあります。 Self-RAGCorrective RAG (CRAG) を参考にした例は、LangChain公式ブログでも紹介されています。

blog.langchain.dev

また、ガイドに記載がある例ではありませんが、マインクラフトを自動プレイするAIエージェントの「Voyager」では、生成されたコードがエラーにならないかチェックする機構が取り入れられています。

Pre-Production

続いて、Pre-Production、つまり本番環境にデプロイする前のテストについてまとめられています。 事前に決められたテストスイートにもとづくバッチでの評価は「オフライン評価」とも呼ばれます。

Pre-Productionのテストの目的は、以下の3つだと整理されています。

  • アプリケーションのパフォーマンスを測定する
  • 継続的な改善を可能にする
  • リグレッションを検知する

データセットの構築には、以下の3つの方法があると整理されています。

  • 手動で例を収集する
  • アプリケーションのログを活用する
  • 合成データを使用する

LLMアプリケーションのパフォーマンスを数値化する方法は、以下の3つがあると整理されています。

このように、ガイドの中ではLLMアプリケーションのテストにどんな要素があるのか、分かりやすく整理されています。

Advanced

Pre-productionのテストの発展的な工夫として、以下の4つが挙げられています。

  • LLMによる評価を、人間が修正したものをFew-shotとして使うことで、LLMによる評価を自己改善 (self-improving) できる
  • LLMアプリケーションの出力を単独で評価するのが難しい場合、2 つの出力のどちらが望ましいか評価するほうが簡単な可能性がある (ペアワイズ評価)
  • エージェントのテストは「最終的な回答のテスト」「個々のステップのテスト」「エージェントの軌跡のテスト」の3つに分けられる
  • LLMアプリケーションのCIの工夫として「キャッシュの使用」「CI用のデータセットの分離」「人間による補助」が推奨される

Post-Production

最後に、Post-Production、つまり本番環境にデプロイした後のテストについてまとめられています。 データセットとして期待する出力を持たず、ユーザーの入力に対してリアルタイムで実施する評価は「オンライン評価」とも呼ばれます。

Post-Productionのテストとしては、以下の2つが挙げられています。

  • ユーザーによるフィードバック
  • LLM-as-Judge評価器によるフィードバック

LLMアプリケーションを本番環境にデプロイして、トレーシングとオンライン評価器をセットアップすることで、エラーをもとにアプリケーションを改善したり、テストデータを追加したりできると書かれています。

おわりに

以上、LangChainの「The Definitive Guide to Testing LLM Applications」の概要を紹介しました。 こちらの記事で紹介したのはあくまで概要であり、ガイドの中ではよりしっかりとした説明や、LangSmithで具体的に構築するための手順へのリンクなども掲載されています。 再掲になりますが、ガイドのPDFは以下のページからダウンロードできます。

www.langchain.com

このガイドで紹介されている内容の多くは、LangSmithの機能として提供されています。 LLMアプリケーションの評価を学ぶうえで、LangSmithの公式ドキュメントを読んで実際に構築してみるのは、個人的にもとてもおすすめです。

docs.smith.langchain.com