HEROZ Tech Blog

日本将棋連盟公認「将棋ウォーズ」や、AIを活用したシステム企画・開発を行う、AI企業HEROZの公式テックブログです。

HEROZ ASKへのo1の導入

はじめに

昨年末の12/5に12 Days of OpenAIでついにo1が発表となりました。
また、Azure OpenAI Serviceでも、このo1が使用できるようになりましたので、HEROZ ASKへと組み込みました。
langchainを用いた組み込みで気になった点を書いていきます。

なお、このo1はHEROZ ASKで2025/1/24以降で使用可能です。

o1-preview時代からの共通項目

昨年9月にo1-previewがリリースされて以来、内部的にはo1-previewを組み込む試作を実施していました。
その時から注意点や気になった点は以下となります。

o1-previewやo1-miniはChatOpenAI()を用いてgpt-4oと同じように使おうとすると、以下が非対応のため修正が必要になります。

  • temperature: オプションの削除
  • ストリーミング(streaming): オプションの削除
  • SystemMessage: システムメッセージの除去

ストリーミングオプションを削除するとストリーミングを期待している時に何も出力されなくなるため、以下のようなラッピングクラスを用意しました。

コードを見る

class ChatOpenAI_o1(ChatOpenAI):
    def _generate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Optional[CallbackManagerForLLMRun] = None,
        **kwargs: Any,
    ) -> ChatResult:
        results = super()._generate(
            messages,
            stop,
            run_manager,
            *kwargs,
        )
        if run_manager is not None:
            # on_llm_new_tokenしないと結果が表示されない
            run_manager.on_llm_new_token("".join([g.text for g in results.generations]))
        return results

    async def _agenerate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
        **kwargs: Any,
    ) -> ChatResult:
        results = super()._generate(
            messages,
            stop,
            run_manager,
            *kwargs,
        )
        if run_manager is not None:
            # on_llm_new_tokenしないと結果が表示されない
            await run_manager.on_llm_new_token(
                "".join([g.text for g in results.generations])
            )
        return results

また、ストリーミング時はstream_usageオプションを使用すると使用トークン数を取得できますが、これの出力トークン(completion tokens)に思考用のReasoning tokenも含まれており、助かりました。

o1での変更点

o1はo1-previewと比べると以下の点で変更となっています。

  • SystemMessageへの対応: roleはdeveloperとする
  • 画像入力: 数式の入力がやりやすくなります
  • Markdown出力: プロンプトにFormatting reenabledと追加すると、Markdownで出力できるようになります

SystemMessageについてはlangchainにおいてはSystemMessagePromptTemplateの呼び出しで、オプションにadditional_kwargs = { "__openai_role__": "developer" }を渡すことで対応できます。
確かにこの方法でSystemMessageの指定はできるようになったのですが、試してみる限りではgpt-4oのSystemMessageと比べて遵守率が低いように感じました。

一方で、o1は未だにコスト計算用の単価が書かれているopenai_info.pyやtiktokenのmodel.pyが対応していないので、テーブルを動的に上書きしない限りはトークンやコストの計算時にエラーが発生します。
これらのファイルについては早期に追加されることを期待します。

組み込み結果

早速、難解な数式を画像で入力して解かせたところ、無事に正解しました。

o1の実行結果

おわりに

gpt-4oより賢いと言われているo1をHEROZ ASKへ導入することができました。
o1は確かに込み入った数式のような問題には抜群の能力を発揮するのですが、普通の質問や知識を問う内容だとgpt-4oと回答が変わらず、o1は時間とコストだけがかかるので、使い所が難しいように思いました。
一般ユーザーを含めて導入することにより、その辺りのユースケースをもっと発掘していきたいと思います。

MCP(Model Context Protocol)をLangChainのエージェント機能で動かしてみた

はじめに

先月、Anthropic社よりMCP(Model Context Protocol)というプロトコルが発表されました。 www.anthropic.com MCPAIアシスタント(RAGシステム)と情報源やツール群をつなげるための通信規約(プロトコル)ですが、現時点で何がどこまでできるかが未知数でしたので、調査を行い、LangChainのエージェント機能で動作させました。
とりあえずは、得られた知見についてシステム構成面の切り口で考察していきたいと思います。

MCPとは

MCPの各種情報は以下にあります。

modelcontextprotocol.io

概要

MCPプロトコル仕様のArchitectureにある下記の図のようにアプリケーションがMCPのクライアントとなり、ローカルやリモートに位置するMCPのサーバーと通信して、情報を取得したり、ツールを動作させたりします。
アプリケーションにはデスクトップアプリやSaaSサービスが想定されます。

MCPの概要図

そして、MCP自体は以下のようなプロトコルのフローやメッセージの中身が規定されています。

MCPプロトコルフロー

認証の不在

プロトコルの規定の中で気になったのは、現時点では認証に対応しておらず、今後議論していくように書かれていました。
認証が存在しない以上、インターネットを介しての通信は難しいかもしれません。

Auth

Authentication and authorization are not currently part of the core MCP specification, but we are considering ways to introduce them in future. Join us in GitHub Discussions to help shape the future of the protocol!

Clients and servers MAY negotiate their own custom authentication and authorization strategies.

2つのトランスポート

プロトコル仕様のTransportsによると、MCPには以下の2つのトランスポートがあるそうです。

後者のSSEタイプはhttpsを介した通信なのでMCPのサーバーはローカルとリモートを問わずに適用できます。
一方で、前者のstdioタイプはMCPのクライアントがMCPのサーバーとなるプロセスを起動して、パイプ機能による標準入出力(stdio)を介して通信するため、stdioタイプはローカル内でしか動作しないです。

SDKとリファレンスサーバー群

MCPの公式githubにはPython SDKTypeScript SDKが整備されており、これらを使用したリファレンスやサードパーティサーバー群も提供されています。

リファレンスサーバー群

SDKは前述した2つのトランスポートの両方に対応しているのですが、リファレンスサーバー群のほとんどはstdioタイプで作られているため、リモートで動作するものはないです。

LangChainへの実装

Python SDKを使用してLangChainへ実装したlangchain-mcpというライブラリもありますが、stdioタイプを前提としているため、ローカル内でのみ動作します(=リモートでは動作しません)。
langchain-mcpToolコンポーネントMCPToolもしくはToolkitコンポーネントMCPToolkitという形態で提供されるため、LangChainのエージェント機能で使う形になります。

このlangchain-mcpasync with構文でトランスポートのクライアント(下記の例ではstdio_client)を起動した状態で実行する必要があるため、チェインの構成と実行を近接して行う必要があります。

async with stdio_client(server_params) as (read, write): 
    async with ClientSession(read, write) as session: 
        toolkit = MCPToolkit(session=session) 
        await toolkit.initialize() 
        (toolkitを使用してrunする)

HEROZ ASKはチェインの構成と実行のタイミングが離れているので、langchain-mcpが適用しづらく、組み込みについては一旦は断念することにしました。

システム構成

MCPの動作についてシステム構成面で図を交えて考察していきます。

Claude Desktopでのシステム構成

Anthropic社はMCPの発表とともに応用例としてデスクトップ版Claude(Claude Desktop)からのデスクトップ検索を提示しています。
これが「デスクトップ検索が便利だ」や「デスクトップを覗かれるのは危険だ」みたいな感想とともに強調されてしまい、「MCP=デスクトップ検索」や「MCP=Claude Desktop」みたいな勘違いが発生しています。
MCPはあくまで通信プロトコルであって、Claude Desktopやデスクトップ検索は直接的には関係なく、単なる応用例の一つに過ぎません。

Claude Desktopでのシステム構成を図にすると、以下になります。
Claude DesktopはMCPクライアントとして位置し、必要なMCPサーバーを自ら起動して、stdioタイプのトランスポートで接続します。 そして、起動したMCPサーバーのプロセスからデスクトップのファイルにアクセスしています。

Claude Desktopでのシステム構成

起動するMCPサーバーの種類やアクセス先は設定ファイルのjsonにてClaude Desktopに渡しますが、任意のプログラムが起動できたり、ローカルのリソースに自由にアクセスできるという点で、安全とは言えない代物です。

SaaSシステムでの目指したいシステム構成

HEROZ ASKのようなSaaS型のAIアシスタントサービスにMCPを組み込む場合には、以下のような2つのパターンのシステム構成があると思います。

SaaSシステムで目指したいシステム構成

a.はSaaSサービスがMCPクライアントとなるパターンで、ユーザーからの質問に応じて、AIアシスタントがリモートサーバーにある情報を取得したり、ツールで何か実行したりします。
b.はSaaSサービスがMCPサーバーとなるパターンで、SaaSサービスが持っているリソースに対して、ユーザのAIアシスタントアプリや、別のSaaSサービスのAIアシスタントからのアクセスを受け付けます。 こちらはSaaSサービスが持つリソースが重要となるので、どちらかと言うとコンテンツプロバイダー的な位置付けになります。

HEROZ ASKは独自のリソースを持つわけではないので、a.のパターンを狙うことになります。
また、HEROZ ASKのようなエンタープライズ向けのサービスの場合には、リモートサーバーはお客様の情報やツールが入ったサーバーになることが想定されます。

現状のシステム構成

前述のようにAIアシスタントSaaSサービスはa.のようなMCPクライアントとして、リモートサーバーへのアクセスを望む場合が多いですが、現状はlangchain-mcpがstdioタイプのトランスポートにしか対応していないため、下記のようなシステム構成にならざるをえません。

現状のシステム構成

すなわち、ユーザから質問を受け取ると、MCPサーバーとなるプロセスを起動するとともに、SaaSサービスが動作しているサーバーのリソースにアクセスします。
これは、セキュリティ的にも問題が多いので、HTTP with SSEの対応が待たれます。

SaaSシステムでの問題点

仮にlangchain-mcpがHTTP with SSEタイプのトランスポートに対応したとしても、お客様のサーバーに情報を取りに行ったり、ツールを実行しに行ったりする場合には以下のような問題があります。

  • MCPに認証機能が実装されていないので、非公開情報のアクセスができない
  • お客様のサーバーがイントラネット内に存在する場合には、アクセスするための術がない

これらを図示すると以下となります。

SaaSシステムでの問題点

LangChainのエージェント機能で動かしてみた

langchain-mcpのREADMEにはエージェント機能を使わない例が載っていますが、せっかくなのでエージェント機能を使って動作させてみました。
/docというディレクトリと/doc/test.txtというファイルを作成してから、「/docには何のファイルがありますか?」という質問でディレクトリ内のファイル一覧を取得させています。

コードを見る

!pip install langchain langchain-openai langchain-mcp langchain-community==0.3.12 langchain-core==0.3.25
!mkdir -p /doc
!touch /doc/test.txt

import os
import asyncio
from langchain_openai import ChatOpenAI
from langchain import hub
from langchain.agents import AgentExecutor, create_structured_chat_agent
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp import MCPToolkit

os.environ["OPENAI_API_KEY"] = "(OpenAIのAPIキー)"

server_params = StdioServerParameters(
    command="npx",
    args=["-y", "@modelcontextprotocol/server-filesystem", "/doc"],
)

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents.structured_chat.prompt import FORMAT_INSTRUCTIONS, PREFIX, SUFFIX
HUMAN_MESSAGE_TEMPLATE = "{input}\n\n{agent_scratchpad}"

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "\n\n".join([PREFIX, "{tools}", FORMAT_INSTRUCTIONS, SUFFIX])),
        MessagesPlaceholder("chat_history", optional=True),
        ("human", HUMAN_MESSAGE_TEMPLATE),
    ]
)

async def main(message):
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            toolkit = MCPToolkit(session=session)
            await toolkit.initialize()
            
            model = ChatOpenAI(model_name="gpt-4o")
            tools = toolkit.get_tools()
            
            agent = create_structured_chat_agent(model, tools, prompt)
            agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
            
            await agent_executor.ainvoke({"input": message})

asyncio.run(main("/docには何のファイルがありますか?"))

無事にtest.txtがあるという回答を得ることができましたが、langchain-communitylangchain-coreをバージョン固定にしないとうまく動きませんでした。

実行結果

おわりに

新しく登場したMCPは外部の情報やツールを活用できるので、期待の技術と思っていたのですが、以下の理由によりまだまだ時期尚早のように思いました。

  • プロトコルの仕様に認証機能が搭載されていないため、インターネット経由の接続が難しい
  • リファレンスサーバーやlangchain-mcpのトランスポートがstdioタイプなので、ローカル内でしか実行できない(リモートサーバーへの接続ができない)
  • Python SDKasync with構文の中でしか実行できないので、セッションの確立(チェインの作成)と実際の動作(チェインの実行)を分離することができない

とは言え、これらの技術課題が解決されれば、大規模言語モデル(LLM)を用いたエンタープライズサーチの実現も夢が広がります。こうしたOSSへのコントリビュートについても一緒にやってくれる仲間を募集しています。

社内向けHEROZ ASKにSLMのPhi-3.5を入れてみた

はじめに

昨今、小規模言語モデル(SLM, Small Language Model)の話が生成AI界隈で賑わせています。
SLMはgpt-4oのような大規模言語モデル(LLM, Large Language Model)と比較して小型軽量である故に以下のような特徴があるとされています。

  1. エッジデバイスやオンプレサーバーで動作させることができる。動作させてもコストが大きくならない。
    • セキュリティーやプライバシーの問題で海外のサーバーへプロンプトを送ることに対しての抵抗は根強くあると思います。
  2. 応答までのレイテンシーが短い
    • 特に音声会話のような場合にはミリ秒(ms)を争うので、レイテンシーが低いに越したことはないです。
  3. ドメイン特化のためのファインチューニングを実施しやすい。
    • 従来の中規模や大規模のモデルと比べてはるかに少ないGPU枚数でファインチューニングができるので、敷居が下がることを期待できます。

弊社でもSLMの動向は追っていましたが、以前の実験のように小規模のモデル(この時は10B前後)だと大した精度が出ないと考えていましたので、スルーしていました。
ところが、最近のSLMは以前のgpt-3.5-turboに匹敵する精度が出るという話や、精度が多少劣っても使い所があるかもしれないという話を聞き、遅ればせながら社内向けの環境に導入して、いろいろ検証しようと思いました。

検証に使用したSLMは今年の8月にリリースされたMicrosoft社のPhi-3.5の中で最も軽量なPhi-3.5 mini instructとしました。Phi-3.5 mini instructは3.8Bとかなり軽量です。
これをAzure Machine Learning サーバーレスAPIで動作させました。Phi-3.5 mini instructは1000トークンあたり$0.00013で推論できます。

HEROZ ASKに組み込むにあたってはlangchainで動作するようにしなければならないのですが、Azure Machine Learning サーバーレスAPIへの接続に関する情報がほとんどありませんでしたので、こちらに書こうと思います。

モデルのデプロイ

langchainからの呼び出しにはAzureMLChatOnlineEndpointを使用するのですが、こちらはAzure Machine Learning Studioでデプロイしたモデルにしか対応していませんでした。
先日のIgnite 2024でデビューしたAzure AI Foundryでデプロイしたモデルにはつながらず、少しハマりました。

以下がMachine Learning Studioでのモデルのデプロイ方法です。

1. ワークスペースの作成

Azure Machine Learning Studioにログインし、ワークスペースを作成する。

ワークスペースの作成

2. サーバーレスエンドポイントの作成

作成したワークスペースに入り、エンドポイントページのサーバーレスエンドポイントタブからサーバーレスエンドポイントを作成する。

サーバーレスエンドポイントの作成

3. エンドポイントの詳細

作成したエンドポイントの詳細ページを開くと、エンドポイントのURLとAPIキーを取得できます。この時に「APIルート」のパネルがあることがポイントです。

エンドポイントの詳細

Azure AI Foundryで作成したエンドポイントとの比較

Azure AI Foundryで作成したエンドポイントの詳細には「APIルート」が存在しません。

AI Foundryで作成したエンドポイント

Azure AI Foundryのエンドポイント一覧では、Machine Learning Studioで作成したOKの方のエンドポイントは「サーバーレス」となっていて、AI Foundryで作成したNGの方のエンドポイントは「Azure AI Services」となっています。

エンドポイントの一覧

langchainからの呼び出しコード

langchainからは以下のコードにて呼び出すことができます。

!pip install langchain langchain_community

from langchain_core.prompts import ChatPromptTemplate
from langchain_community.chat_models.azureml_endpoint import AzureMLChatOnlineEndpoint, AzureMLEndpointApiType, CustomOpenAIChatContentFormatter

AZURE_PHI3_ENDPOINT="(エンドポイント)"
AZURE_PHI3_API_KEY="(APIキー)"

human = "あなたは何者ですか?"
prompt = ChatPromptTemplate.from_messages([("human", human)])

chat = AzureMLChatOnlineEndpoint(
    endpoint_url=AZURE_PHI3_ENDPOINT,
    endpoint_api_type=AzureMLEndpointApiType.serverless,
    endpoint_api_key=AZURE_PHI3_API_KEY,
    content_formatter=CustomOpenAIChatContentFormatter(),
)

chain = prompt | chat
chain.invoke({})
→ AIMessage(content='私はMicrosoftのAIアシスタントです。...

Azure AI Foundryで作成したエンドポイントもlangchain_azure_aiに含まれるAzureAIChatCompletionsModelを使えば呼び出せるような記事はありました。ただし、現時点(2024/12/12)ではlangchain_azure_aiはままだ非公開のようです。

learn.microsoft.com

実行結果

HEROZ ASKの社内環境に組み込み、無事に動作しました。

Phi-3.5の動作

以下、実行して気になったことです。

モデルの精度と推論速度

モデルの精度については、プロンプトの些細な違いによっては壊れた回答になったり、Few-shotの例に引きづられすぎたりといった小中規模のオープンソースのモデルにありがちな間違いが発生することもありますが、プロンプトが合っていれば正しい回答を得られることは確認できました。

また、Azure Machine Learning サーバーレスAPIによる推論速度はモデルサイズの割には速くはなかったです。
やはりサーバーレスタイプですと、起動のオーバーヘッドがそれなりにあるのかもしれません。

ストリーミング

AzureMLChatOnlineEndpoint_(a)generate()関数から呼ばれる時にはストリーミングにならないことが分かりました。
そこで、以下のようなラッピング関数を作成して、_(a)stream()関数に分岐するようにしました。

コードを見る

from typing import Any, List, Optional

from langchain_core.callbacks import (
    AsyncCallbackManagerForLLMRun,
    CallbackManagerForLLMRun,
)
from langchain_core.language_models.chat_models import (
    agenerate_from_stream,
    generate_from_stream,
)
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatResult

class AzureMLChatOnlineEndpointStreaming(AzureMLChatOnlineEndpoint):
    streaming: bool = False

    def _generate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Optional[CallbackManagerForLLMRun] = None,
        stream: Optional[bool] = None,
        **kwargs: Any,
    ) -> ChatResult:
        should_stream = stream if stream is not None else self.streaming
        if should_stream:
            stream_iter = self._stream(
                messages, stop=stop, run_manager=run_manager, **kwargs
            )
            return generate_from_stream(stream_iter)
        return super()._generate(messages, stop, run_manager, *kwargs)

    async def _agenerate(
        self,
        messages: List[BaseMessage],
        stop: Optional[List[str]] = None,
        run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
        **kwargs: Any,
    ) -> ChatResult:
        should_stream = self.streaming
        if should_stream:
            stream_iter = self._astream(
                messages, stop=stop, run_manager=run_manager, **kwargs
            )
            return await agenerate_from_stream(stream_iter)
        return await super()._agenerate(messages, stop, run_manager, *kwargs)

使用トークン数の取得

AzureMLChatOnlineEndpointならびにAzure Machine Learning サーバーレスAPI上のPhi-3.5モデルは"stream_usage"に対応していないようですので、使用トークン数が取得できませんでした。
今回は社内向けなので使用トークン数は気にしなくてもよいですが、本番サービス向けには使用トークン数を取得する方法を検討する必要がありそうです。
(デバッグしている限りではストリーミングの途中では使用トークン数を配信しているようですが、AzureMLChatOnlineEndpointが拾っていないようです。また、回答完了時に使用トークン数を返すこともなかったです)

おわりに

今回は話題のSLMについて、Phi-3.5をHEROZ ASKの社内環境で動作させることに成功しました。
まだ多くは触っていないですが、社内で精度限界の検証ならびにユースケースの発掘を引き続き進めていきたいと思います。 何か分かりましたら、別途記事にしようと思います。

NoSQL(mongoDB) 導入ガイド

はじめに

NoSQL の利用を促進したいという意識のもとこの記事を作成しました。 今迄、数多くの案件に関わってきましたが、DB といえば大体 RDB でした。NoSQL を扱っている案件もあるのですが数少ないです。 NoSQL にしておけばこんなに苦労することはなかったんだろうなというケースも多々あったので、とりあえず使い慣れた RDB を使うといった風潮に異を唱えたく思いました。 ロールの多様化によって DB に触れる機会があまりないエンジニアも増えてきた昨今、利用促進には情報発信したり説得材料を作るのが肝要だと思います。ビギナーでも手軽に試せるクイックスタート、どういった局面で利用すべきかといったガイドライン、現実に発生し得る要求に対処できるようなケーススタディなどを書いたブログ記事を作成することで、その一助とできればと願います。

環境構築

GitHub リポジトリに本記事で扱う環境やソースコードを全て格納しています。

Docker(docker-compose)を利用できる環境であれば、コマンド一つで環境を構築できます。

docker-compose up -d

コマンドを実行すると以下の 3 つのコンテナが起動します。

  • backend
    • リポジトリに配置しているファイルが/usr/src/app ディレクトリにマウントされます
    • Python(Jupyter)を実行できる環境です。
    • VSCode などでリモートログインして利用してください。
  • mongo
    • mongo DB 本体です。
    • ユーザーやパスワードなどは docker-compose.yml に書いてあります。
  • mongo-express
    • mongo DB をブラウザの GUI でメンテナンスできるツールです。
    • ブラウザでこのリンクを開いてください。

導入

pymongoでmongo DBを操作します。接続と動作確認を行うJupyterスクリプトを以下にGistで貼り付けます。

hello mongo

ケーススタディ: ユーザー情報を管理する

階層構造データの基本的なCRUD操作をしてみます。

mongo case study manage user info

おわりに

今回は導入部分の説明のみとしました。今後、より実践的な内容にも踏み込んでいきたいと思っています。

GraphRAGを試してみた

はじめに

GraphRAGはLLMによってナレッジ(知識)グラフを生成することで、複雑な情報のドキュメントから質疑応答を行う際の精度を向上させることができる手法ならびにソフトウェアです。
GraphRAGはマイクロソフトが開発しましたが、先日オープンソース化が発表されましたので、早速試してみました。
GraphRAGはAzureで動作させる方法(GraphRAG Accelerator)もありますが、今回はローカル(手元のMac)で動作させました。

GraphRAGが使用しているナレッジグラフは、ドキュメント内の複雑な情報の検索に向いていると言われています。
HEROZ ASKのドキュメント検索でも使用しているエンべディング(embedding)を用いた検索は、文意に基づく曖昧検索には向いているのですが、複雑な情報の検索ではカバーしきれないがあって気になりました。

インストール

インストールは公式のGet Startedや解説してくださっている動画(英語)の通りに実施すると、わりと簡単にできました。
python:3のdockerイメージ上で実施して気になったのは以下です。

  • Rustのインストールは個別で実施する必要がある
  • ragtest/.env: GRAPHRAG_API_KEYにOpenAIのAPIキーを書く(名前が紛らわしい)
  • ragtest/settings.yaml: GRAPHRAG_LLM_MODEL=gpt-4-turbo-previewをgpt-4oに変更する

実行結果

とりあえずは桃太郎の文章を読み込ませて、Get Startedの質問(What are the top themes in this story?)を和訳した「この物語の主なテーマは何ですか?」を問い合わせてみました。

# python -m graphrag.query --root ./ragtest --method global "この物語の主なテーマは何ですか?"

  :

SUCCESS: Global Search Response: ### 物語の主なテーマ

物語「桃太郎」にはいくつかの重要なテーマが含まれています。それぞれのテーマは物語の異なる側面を強調し、全体として豊かな物語を形成しています。

#### 勇気とリーダーシップ

桃太郎の鬼ヶ島征伐の使命は、勇気とリーダーシップのテーマを中心に展開されています。桃太郎の卓越した強さ、戦略的な手腕、そして仲間を鼓舞し導く能力は、物語の中で重要な役割を果たしています [Data: Reports (2, 4, 5)]。

#### 団結とチームワーク

桃太郎と彼の忠実な家来である犬、猿、キジの協力は、団結とチームワークのテーマを強調しています。彼らの結束したスキルと献身は、鬼たちがもたらす困難を乗り越えるために不可欠です [Data: Reports (2, 4, 5)]。

#### 家族の絆と育成

桃太郎と彼を育てた老夫婦との関係は、家族の絆と育成のテーマを象徴しています。老夫婦の支援と愛情は、桃太郎が旅に出るための準備において重要な役割を果たしています [Data: Reports (1, 2)]。

#### 神の介入と運命

老夫婦が桃太郎を神の贈り物と信じることから、物語には神の介入と運命のテーマが含まれています。これにより、物語に霊的な次元が加わり、運命と神の祝福のテーマが強調されます [Data: Reports (1)]。

#### 文化的意義

「桃太郎」の物語は日本の民間伝承に深く根ざしており、強さ、リーダーシップ、団結の力といったテーマを祝うものです。また、きびだんごなどの文化的要素も含まれています [Data: Reports (2)]。

### 結論

「桃太郎」の物語は、勇気、リーダーシップ、団結、家族の絆、神の介入、そして文化的意義といった多くのテーマを通じて、豊かで多層的な物語を提供しています。これらのテーマは、物語のキャラクターや出来事を通じて巧みに織り交ぜられ、読者に深い印象を与えます。

やや冗長ではありますが、かなり的を得た回答になっています。
そして、日本語でも大丈夫そうでした。

気づいたこと

思考言語

インデックス作成時の画面出力を見る限りでは、nodeのdescriptionが英語になっていたりするので、英語で思考している可能性があります。
付属のプロンプトは指示も例示も全て英語なので、プロンプトを改良すると日本語にできるかもしれません。

🚀 create_final_nodes
    level      title      type                                        description  ... graph_embedding                 top_level_node_id  x y
0       0    "おじいさん"  "PERSON"  おじいさん (Grandfather) is an elderly character wh...  ...            None  b45241d70f0e43fca764df95b2b81f77  0  0
1       0    "おばあさん"  "PERSON"  おばあさん is an elderly woman who plays a signific...  ...            None  4119fd06010c494caa07f439b333f4c5  0  0
2       0        "川"     "GEO"  "川 (river) is a geographical feature where おばあ...  ...            None  d3835bf3dda84ead99deadbeac5d0d7d  0  0
3       0        "桃"   "EVENT"  "\u6843" (peach) is a significant element in t...  ...            None  077d2820ae1845bcbb1803379a3d1eae  0  0

既存システムへの組み込み

GraphRAGは以下のような点で、既存のシステムに組み込むのは現段階では難しそうです。

  • Azureでの実行を前提としている部分が多い
  • データの保存形式がメモリ、ファイル(.parquet)、Azure blobの3択であり、既存のデータベースに載せる場合には自分で改造する必要がある
  • 検索もlangchainが対応していないので、自力でchainを定義する必要がある

おわりに

今回は出たばかりのGraphRAGを試してみましたが、思ったりよりも苦労せずに動作して良かったです。
まだ単純な例文でしか試していないですが、もう少し込み入った文章も入力してみて、従来のエンべディングの場合と比較してみようと思います。

また、GraphRAGはプロンプトの指示文や、例示文を書き換えることで、ドメイン適用も可能そうですので、そちらも試してみたいと思います。

HEROZ ASK を支えるインフラ技術(第2回)

はじめに

こんにちは、HEROZ ASK の開発チームです。

herozask.ai

今回のポストでは、このプロダクトの開発で活用しているインフラ技術を紹介したいと思います。

前回の記事

heroz-tech.hatenablog.jp

『BRIDGE』掲載記事

HEROZ ASK について取り上げていただきました。

Microsoft Azure』の情報収集

皆さんは、どのようにAzureを勉強していますか?今回は情報のキャッチアップ方法について紹介したいと思います。

Microsoft Build』

先日、米国 Microsoft 主催の年次開発者会議「Microsoft Build」(2024年5⽉21⽇‐23⽇ 米国時間)が開催されました。

build.microsoft.com

Microsoft Build Book of News」に、発表された主要なニュース項目がまとまっています。

news.microsoft.com

ただ、発表内容が多いため、キャッチアップが大変ですね

Microsoft Build Japan』

日本の開発者向けに「Microsoft Build Japan」(2024年6月27日‐28日)が開催されます。 1ヵ月程度遅れますが、まとまった日本語情報を提供していただけるのは助かります。

Microsoft Ignite

冬には、「Microsoft Ignite」(2024年11⽉18⽇‐22⽇ 米国時間)が開催されます。

ignite.microsoft.com

2023年は、「MICROSOFT IGNITE BOOK OF NEWS」に、発表された主要なニュース項目がまとまっています。

news.microsoft.com

Microsoft Ignite Japan』

2023年と同様に、日本の開発者向けに「Microsoft Ignite Japan」も開催されることでしょう。

Microsoft (有志)』の記事

Zennでは、Microsoft Azureをはじめとする最新技術情報が提供されています。 米国 Microsoft 主催の大規模なイベント直後は、Microsoft社員の方々により、一斉更新が始まります。

zenn.dev

Azure OpenAI Service の GPT-4o 対応をするのに、以下のブログ記事が参考になりました。

zenn.dev zenn.dev zenn.dev

Azure AI Searchについては、花ヶ﨑さんのQiitaが参考になります。

qiita.com qiita.com qiita.com

Azure OpenAI Serviceについては、蒲生さんのSpeaker Deckが参考になります。

『Azureの新機能』

AzureやAzure OpenAI Service の新機能をまとめたページがあります。 ただし、「Microsoft Build」などの大規模なイベントで発表された新機能の反映は多少遅れる印象があります。 また、日本語への翻訳が遅れる場合もあるため、英語ドキュメントのチェックも必要です。

azure.microsoft.com learn.microsoft.com

Microsoft Learn』

製品、キャリア パス別のトレーニングなど、公式ドキュメントがとても充実しています。

learn.microsoft.com

ただし、

  • あまりに膨大なため、何から手をつけていいかわからない。
  • 英語の自動翻訳のため、あまり頭に入ってこない。
  • どこまで勉強すれば、商用サービスを構築・運用できるのかわからない。

など、迷子になってしまうことはあるかもしれません。

ドキュメントと睨めっこしながら、実際に手を動かして、Azureのサービスを触ってみるということが大事だと思います。

『Azure コマンド ライン インターフェイス (CLI)』

Azure Portal から手を動かしたことは、なるべく効率化したいですよね。 Azure CLIチュートリアルやサンプルで自動化できないか試すことをお勧めします。

learn.microsoft.com

注意点としては、

  • Azure Portal から手作業でしか設定できない項目の方が多い
  • 新機能のアップデートで仕様が変わり、エラーになる場合がある
  • 削除したリソース次第で、論理削除(完全に削除されていない)となり、再作成するとエラーになる場合がある

などがあります。

コマンドラインで出来ること、手作業でしか設定できないことを把握した上で、どれくらい労力をかけるのが構築/運用フェーズでコスパがいいか、見極めが重要です。 Azure のリソースをコードで管理できるか試すのは、それからでも遅くないのではないかと思います。

learn.microsoft.com learn.microsoft.com

『日本マイクロソフト サポート情報』

他のクラウドでは一般的ですが、「Microsoft Learn」の記載にないことをやりたい場合、情報が不足していて困ることがあります。 そんな時に役立つのが、「Microsoft Azure テクニカルサポートチーム」のブログ記事です。

cssjpn.github.io

「Azure OpenAI Service」であれば、「Japan Azure Cognitive Services サポートチーム」のブログが参考になります。 自分が困っていることは、皆さん困っている印象があります。

jpaiblog.github.io

どうしてもわからなければ、あまり悩まずにAzure サポートにサポートケースを起票するのも手かもしれません。 ただし、漠然とした理解で質問しても、有効な回答は得られないかもしれません。質問の前に下調べして、十分に理解する努力は必要です。

azure.microsoft.com

最後に

今後も HEROZ は、クラウド技術と生成 AI を活用したサービス提供について、今後も技術的にさらに踏み込んだ内容を発信して行きたいと思います。本ブログをご愛顧いただければ幸いです。最後までお付き合いいただきありがとうございました。

HEROZ ASK プロトタイプでマルチモーダルRAGを動かしてみた

はじめに

HEROZ ASKのプロトタイプに今年(2024年)に来ると言われているマルチモーダル(Multi modal) RAGを組み込みましたので、その結果について書いていきたいと思います。 HEROZ ASKのプロトタイプは実験やデモ用に新機能や新モデルを使えるようにした試作品です。
なお、本記事はRAGやlangchainのことをある程度理解している人を対象としています。

マルチモーダルRAGの実現方式

マルチモーダルRAGの実現方式はlangchainのblogで解説されています。
この記事によると実現方式には以下の3種類があるとされています。

  • Option 1: Retrieve raw image
  • Option 2: Retrieve image summary
  • Option 3: Retrieve image summary but pass raw image to LLM fir synthesis

https://blog.langchain.dev/content/images/size/w1000/2023/10/image-22.png (図はlangchainのblogに掲載されているもの)

Option 1はOpenCLIPのようなテキストとイメージをシームレスに取り扱えるマルチモーダルエンベンディング(Multi modal embedding)を使用して、エンべディングと検索を行います。
Option 2とOption 3はイメージを一度GPT-4oGPT-4Vのようなマルチモーダル対応LLMでサマリーのテキストを取得して、それに対してエンべディングと検索を行います。 Option 2は検索後の回答生成でも検索結果としてサマリーのテキストをそのまま使用しますが、Option 3は検索結果として元のイメージをマルチモーダル対応LLMに送信します。

今回の組み込みではOption 1とOption 3に対応することにしました。

改造ポイント

ドキュメントの保存

ドキュメントの分解にはunstructuredを使用しました。

unstructuredのpartition_pdf()chunking_strategyの設定によってチャンキングも行えます。その場合には、CompositeElementというelementで内容を取得します。
また、extract_images_in_pdfをTrueにすると、image_output_dir_pathに切り出したイメージが出力されます。extract_element_typesにTableも入れますと、表も切り出されます。

切り出したイメージはbase64にして保存します。アイコンのような小さなイメージも切り出されますので、20kb以下のイメージは無視するようにしています。
Option3ならGPT-4oやGPT-4Vでサマリーを取得します。
Option1の場合には、embeddingに含まれるembed_image()を使用してベクトル化します。今回はOption1用のembeddingとしてOpenClipのマルチリンガルモデルであるCLIP-ViT-H-14-frozen-xlm-roberta-large-laion5B-s13B-b90kを使用しました。

コードを見る

!apt-get install libgl1-mesa-dev poppler-utils tesseract-ocr tesseract-ocr-jpn
!pip install cmake
!pip install unstructured[pdf]==0.11.8 langchain openai langchain-openai

from unstructured.partition.pdf import partition_pdf
from langchain.docstore.document import Document
from langchain_openai import ChatOpenAI
from langchain.schema.messages import HumanMessage
import tempfile
import base64
import os
from io import BytesIO

os.environ["OPENAI_API_KEY"] = "(OpenAIのキー)"

filename = "(ファイル名)"
option_mode = "option3"

def encode_image(image_path):
    with open(image_path, "rb") as image_file:
        return base64.b64encode(image_file.read()).decode('utf-8')

def summarize_image(image_base64):
    prompt = """あなたは画像の内容を説明する役割をもっています。
入力された画像の内容を詳細に説明してください。
基本的には日本語で回答してほしいですが、専門用語や固有名詞を用いて説明をする際には英語のままで構いません。
"""
    chat = ChatOpenAI(model="gpt-4-vision-preview", max_tokens=1024)
    human_message = [
        HumanMessage(content=[
            {"type": "text", "text": prompt},
            {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
        ])
    ]
    msg = chat.invoke(human_message)
    return msg.content

docs = []
with tempfile.TemporaryDirectory() as image_dir:
    elements = partition_pdf(
        filename,
        strategy="hi_res",
        languages=['jpn', 'eng'],
        extract_images_in_pdf=True,
        extract_element_types=["Image", "Table"],
        infer_table_structure=True,
        chunking_strategy="by_title",
        max_characters=800,    # チャンクサイズ
        new_after_n_chars=760,
        image_output_dir_path=image_dir,
    )
    for element in elements:
        if "unstructured.documents.elements.CompositeElement" in str(type(element)):
            element_text = str(element)
            # テキストをいろいろ加工
            metadata = {"source": filename, "type": "text"}
            docs.append(Document(page_content=element_text, metadata=metadata))
    for image_file in sorted(os.listdir(image_dir)):
        if image_file.endswith('.jpg'):
            image_path = os.path.join(image_dir, image_file)
            if os.path.getsize(image_path) <= 20 * 1024:
                # 20kb以下のファイルは無視
                continue
            image_base64 = encode_image(image_path)
            if option_mode == "option1":
                metadata = {
                    "source": filename,
                    "type": "image",
                }
                docs.append(Document(page_content=image_base64, metadata=metadata))
            elif option_mode == "option3":
                image_text = summarize_image(image_base64)
                metadata = {
                    "source": filename,
                    "type": "summary_image",
                    "original": image_base64,
                }
                docs.append(Document(page_content=image_text, metadata=metadata))

vectorstore = (何かしらのベクトルストア)
for doc in docs:
    if not "type" in doc.metadata \
        or doc.metadata["type"] in ["text", "summary_image"]:
        vectorstore.add_documents([doc])
    elif doc.metadata["type"] == "image":
        embeddings = vectorstore.embedding_function.embed_image(
            [BytesIO(base64.b64decode(doc.page_content))]
        )
        vectorstore.add_embeddings(
            texts=[doc.page_content],
            embeddings=embeddings,
            metadatas=[doc.metadata],
        )

retrieve結果の集約

langchainのConversationalRetrievalChainチェインを使用してRAGを行っている場合には、retrieverで取得した情報をCombineDocumentsChainチェインで一つのinputsにまとめてからLLMへの問い合わせを行っています。

この部分もマルチモーダル対応をする必要があるため、CombineDocumentsChainチェインの一つであるStuffDocumentsChainチェインに含まれる_get_inputs()を改造しました。
冒頭のテキスト部分はそのままで、途中にイメージ関係の処理を追加しています。 retrieverで取得したイメージについては配列にして、キー"image"でinputsに追加するようにしました。また、input_variablesにもキー"image"を追加しています。

コードを見る

class CustomStuffDocumentsChain(StuffDocumentsChain):
    def _get_inputs(self, docs: List[Document], **kwargs: Any) -> dict:
        """Construct inputs from kwargs and docs."""
        # Format each document according to the prompt
        doc_strings = [format_document(doc, self.document_prompt) for doc in docs if not "type" in doc.metadata or doc.metadata["type"] == "text"]
        # Join the documents together to put them in the prompt.
        inputs = {
            k: v
            for k, v in kwargs.items()
            if k in self.llm_chain.prompt.input_variables
        }
        inputs[self.document_variable_name] = self.document_separator.join(doc_strings)
        
        # images
        image_docs = list(filter(lambda doc: "type" in doc.metadata and doc.metadata["type"] in ["summary_image", "image"], docs))
        if (hasattr(self.llm_chain.llm, "model_name") \
            and self.llm_chain.llm.model_name in ["gpt-4-vision-preview", "gpt-4o"] \
            and len(image_docs) > 0:
            images = []
            for doc in image_docs:
                if doc.metadata["type"] == "summary_image":
                    metadata = {
                        "file_id": doc.metadata["file_id"],
                        "source": doc.metadata["source"],
                        "type": doc.metadata["type"],
                        "summary": doc.page_content,
                    }
                    images.append(Document(page_content=doc.metadata["original"], metadata=metadata))
                elif doc.metadata["type"] == "image":
                    images.append(doc)
            inputs["images"] = images
            if not "images" in self.llm_chain.prompt.input_variables:
                self.llm_chain.prompt.input_variables.append("images")
        return inputs

マルチモーダル形式への変更

langchainのプロンプトテンプレートはGPT-4oやGPT-4VといったマルチモーダルLLMの入力には対応していないようでしたので、その部分も改造するようにしました。以下はHumanMessagePromptTemplateに含まれるformat()を改造した時の例です。

テキストのみの場合には従来の処理をして終了します。
マルチモーダルの場合には配列にテキストとイメージを順番にdict形式で追加していきます。

コードを見る

class CustomHumanMessagePromptTemplate(HumanMessagePromptTemplate):
    """Human message prompt template. This is a message sent from the user."""

    model_name: str

    def format(self, **kwargs: Any) -> BaseMessage:
        """Format the prompt template."""
        text = self.prompt.format(**kwargs)
        if not self.model_name in [
            "gpt-4-vision-preview",
            "gpt-4o",
        ]:
            return HumanMessage(content=text, additional_kwargs=self.additional_kwargs)
        
        content = []
        content.append({"type": "text", "text": text})
        if "images" in kwargs:
            for image in kwargs["images"]:
                content.append({
                    "type": "image_url" ,
                    "image_url": {"url": "data:image/jpeg;base64," + image.page_content }
                })
        return HumanMessage(content=content, additional_kwargs=self.additional_kwargs)

組み込み結果

上記の改造をlangchain側に施し、UIを調整すると以下のように無事にマルチモーダルのRAGを実現することができました。
この例では資料として有名なTransformerの論文を読み込ませた上で、質問しています。
論文内のTransformerのアーキテクチャー図を元にした解説が返ってきました。
意外とイメージ中の文字を正しくスキャンできていたり、表の構造を正しく理解していて驚きました。 特にGPT-4oになってからは文字の認識精度は格段に上がっています。 そして、HEROZ ASKの機能である参照文の表示でもイメージを表示できるようにしています。

マルチモーダルRAGの結果

マルチモーダルRAGは成功しましたが、以下のような課題(苦労話)もありました。

トークン数の計算

langchainのConversationalRetrievalChainチェインではretrieverで取得した情報のトークン数が最大トークン数より多い場合には、スコアが低いチャンクを切り捨てる_reduce_tokens_below_limit()という関数があります。
イメージをマルチモーダル対応LLMに送る場合にはトークン数の計算が異なるため、この関数も改造する必要がありました。
特にOption1の場合には本文(page_content)にbase64エンコードしたイメージを格納しているため、そのままだとすぐに最大トークン数を超過してしまい、1枚もイメージが送られなかったことがありました。

Option1の精度

Option1でマルチモーダルRAGを試したところ、テキストとイメージが混在しているドキュメントではイメージの部分がヒットしないという現象がありました。
調査してみますと、テキストとイメージでretrieveのスコアが4倍ぐらい異なっていることが分かりました(イメージのスコアがテキストのスコアの1/4)。

OpenClipのようなマルチモーダルエンベンディング(Multi modal embedding)はテキストとイメージがシームレスに取り扱えるかと思っていましたが、現実的には同じスケールで評価されないようです。
Option3はイメージを一度サマリーにして検索するので情報の抜け落ちが気になりますが、テキストとイメージが混在している場合はOption3でしかうまく動作しませんでした。
テキストとイメージがよりシームレスに評価されるマルチモーダルエンベンディングが登場することが待ち望まれます。

図表のタイトル付与

マルチモーダルRAGも動作するHEROZ ASKのプロトタイプは第8回 AI・人工知能EXPO【春】で展示しており、パワーポイントで作成されたグラフや表入りの決算資料を検索するデモを準備していました。
ところが、unstructuredで切り出したグラフや表は、以下のようにグラフや表の単体になってしまい、前後の文脈である何に対するものかが分からないものになっていました。 このため、マルチモーダルRAGでうまく検索できない問題が発生しました。
例のグラフは弊社決算資料のものです。

売上高グラフ(改良前)

こちらについては、ヘッダーやタイトルも同時に切り出して、元のイメージに無理やりそれらを合成することで解消しました。
以下の改良後のグラフには「売上高」という文字列が含まれるようになったため、「2024年3Qの売上高は?」のような質問も正しく回答するようになりました。(正解はグラフ中の1,296百万円)

売上高グラフ(改良後)

今回は展示会のデモ用に限定的な条件で動作するようにしましたが、より汎用的に使えるようにすることは大きな課題だと思います。

おわりに

今回はマルチモーダルRAGをHEROZ ASKのプロトタイプに組み込み、無事に図表を含めて検索できることに成功しました。
そして、これを第8回 AI・人工知能EXPO【春】の弊社ブースで展示し、多くのお客様から驚きの反応を頂くことができました。

一方で、上記にも書いたように、

  • マルチモーダル対応LLMの認識精度
  • 前後の文脈を補完する方法

あたりはまだまだ課題であることが分かりました。
今後は、この辺りの課題をLLMの発展や技術改良により解決して、製品化できるようにしたいと思います。