0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Azure AI Search の Agentic RetrievalをPythonで試す

Posted at

Azure AI Search のAgentic Retrievalを試してみました。
基本的には以下の内容ですが、少しだけ変更しています。

Agentic Retrievalの説明はこちら参照。

プログラム

前提

Azure環境

以下の作業を実施

  1. Azure AI Search
    1. Azure AI Searchリソースを作成
    2. Azure Portalからメニュー 設定 -> キー で APIアクセス制御を「両方」に設定
    3. Azure Portalからメニュー 設定 -> ID で システム割り当て済みマネージドIDをONに設定
    4. 実行ユーザに以下のロールを割り当て
      - Search Service サービス貢献者
      - 検索インデックス データ共同作成者
      - 検索インデックス データ閲覧者
  2. Azure AI Foundry
    1. Azure AI Foundryリソースを作成
    2. Azure Portalからメニュー アクセス制御(IAM) で ロール割り当て「Cognitive Services OpenAI User」を前ステップで作成したシステム割り当て済みマネージドIDに追加
    3. AI Foundry Portal から モデルgpt-5-miniおよびtext-embedding-3-largeをデプロイ

Python環境

WSLでUbuntu 22.04 状でPython3.13.2を使っています。
使っているPythonパッケージをproject.tomlで表示。

project.toml
azure-search-documents = "11.7.0b1"
azure-identity = "^1.25.1"
openai = "^2.2.0"
aiohttp = "^3.13.0"
ipykernel = "^6.30.1"
requests = "^2.32.5"

プログラム実行前にaz loginしておきます。

1. インデックス定義

ベクトル項目を含む検索インデックス定義の作成。ここは通常のAI Searchと変わらないです。

from azure.search.documents.indexes.models import SearchIndex, SearchField, VectorSearch, VectorSearchProfile, HnswAlgorithmConfiguration, AzureOpenAIVectorizer, AzureOpenAIVectorizerParameters, SemanticSearch, SemanticConfiguration, SemanticPrioritizedFields, SemanticField
from azure.search.documents.indexes import SearchIndexClient
from azure.identity import get_bearer_token_provider

azure_openai_token_provider = get_bearer_token_provider(credential, "https://cognitiveservices.azure.com/.default")
index = SearchIndex(
    name=index_name,
    fields=[
        SearchField(name="id", type="Edm.String", key=True, filterable=True, sortable=True, facetable=True),
        SearchField(name="page_chunk", type="Edm.String", filterable=False, sortable=False, facetable=False),
        SearchField(name="page_embedding_text_3_large", type="Collection(Edm.Single)", stored=False, vector_search_dimensions=3072, vector_search_profile_name="hnsw_text_3_large"),
        SearchField(name="page_number", type="Edm.Int32", filterable=True, sortable=True, facetable=True)
    ],
    vector_search=VectorSearch(
        profiles=[VectorSearchProfile(name="hnsw_text_3_large", algorithm_configuration_name="alg", vectorizer_name="azure_openai_text_3_large")],
        algorithms=[HnswAlgorithmConfiguration(name="alg")],
        vectorizers=[
            AzureOpenAIVectorizer(
                vectorizer_name="azure_openai_text_3_large",
                parameters=AzureOpenAIVectorizerParameters(
                    resource_url=aoai_endpoint,
                    deployment_name=aoai_embedding_deployment,
                    model_name=aoai_embedding_model
                )
            )
        ]
    ),
    semantic_search=SemanticSearch(
        default_configuration_name="semantic_config",
        configurations=[
            SemanticConfiguration(
                name="semantic_config",
                prioritized_fields=SemanticPrioritizedFields(
                    content_fields=[
                        SemanticField(field_name="page_chunk")
                    ]
                )
            )
        ]
    )
)

index_client = SearchIndexClient(endpoint=search_endpoint, credential=credential)
index_client.create_or_update_index(index)
print(f"Index '{index_name}' created or updated successfully.")
ターミナル出力
Index 'earth-at-night' created or updated successfully.

2. インデックスにアップロード

GitHubから分散表現を含んだearth-at-nightのデータを取得し、インデックスにPush。
ここも通常のAI Searchと変わらないです。

import requests
from azure.search.documents import SearchIndexingBufferedSender

url = "https://raw.githubusercontent.com/Azure-Samples/azure-search-sample-data/refs/heads/main/nasa-e-book/earth-at-night-json/documents.json"
documents = requests.get(url).json()

with SearchIndexingBufferedSender(endpoint=search_endpoint, index_name=index_name, credential=credential) as client:
    client.upload_documents(documents=documents)

print(f"Documents uploaded to index '{index_name}' successfully.")
ターミナル出力
Documents uploaded to index 'earth-at-night' successfully.

3. ナレッジ ソースを作成

インデックスに対してナレッジソース作成。Agentic Retrievalを使うにはナレッジソースが必要で、ナレッジソースは複数のナレッジエージェントで使いまわしができます。

from azure.search.documents.indexes.models import SearchIndexKnowledgeSource, SearchIndexKnowledgeSourceParameters
from azure.search.documents.indexes import SearchIndexClient

ks = SearchIndexKnowledgeSource(
    name=knowledge_source_name,

    # このDescriptionをAgentが見て判断
    description="Knowledge source for Earth at night data",
    search_index_parameters=SearchIndexKnowledgeSourceParameters(
        search_index_name=index_name,
        source_data_select="id,page_chunk,page_number",
    ),
)

index_client = SearchIndexClient(endpoint=search_endpoint, credential=credential)
index_client.create_or_update_knowledge_source(knowledge_source=ks, api_version=search_api_version)
print(f"Knowledge source '{knowledge_source_name}' created or updated successfully.")
ターミナル出力
Knowledge source 'earth-knowledge-source' created or updated successfully.

4. ナレッジ エージェント作成

ナレッジソースに対してナレッジエージェントの作成。
maxOutputsizeの値を変更。この値によって、LLMに渡すToken長を制御。デフォルトの5000だと今回いまくいかなかったので、長くしました。

maxOutputSizeの プロパティによって、文字列の長さが決まります。 5,000 個のトークンをお勧めします。

from azure.search.documents.indexes.models import KnowledgeAgent, KnowledgeAgentAzureOpenAIModel, KnowledgeSourceReference, AzureOpenAIVectorizerParameters, KnowledgeAgentOutputConfiguration, KnowledgeAgentOutputConfigurationModality
from azure.search.documents.indexes import SearchIndexClient

aoai_params = AzureOpenAIVectorizerParameters(
    resource_url=aoai_endpoint,
    deployment_name=aoai_gpt_deployment,
    model_name=aoai_gpt_model,
)

# 自然言語での回答をさせる設定
output_cfg = KnowledgeAgentOutputConfiguration(
    modality=KnowledgeAgentOutputConfigurationModality.ANSWER_SYNTHESIS,
    include_activity=True,
)

agent = KnowledgeAgent(
    name=knowledge_agent_name,
    models=[KnowledgeAgentAzureOpenAIModel(azure_open_ai_parameters=aoai_params)],
    knowledge_sources=[
        KnowledgeSourceReference(
            name=knowledge_source_name,
            reranker_threshold=2.5, # Rerankerが2.5以下は除外
        )
    ],
    output_configuration=output_cfg,
    request_limits={"maxOutputsize": 10_000}  # デフォルトから変更
)

index_client = SearchIndexClient(endpoint=search_endpoint, credential=credential)
index_client.create_or_update_agent(agent, api_version=search_api_version)
print(f"Knowledge agent '{knowledge_agent_name}' created or updated successfully.")
ターミナル出力
Knowledge agent 'earth-knowledge-agent' created or updated successfully.

5. 取得パイプライン実行

取得パイプライン実行。パイプライン内では以下の動作が実行される。

  1. ユーザーの情報ニーズを推測するために、会話全体を分析します。
  2. 複合クエリを、焦点を絞ったサブクエリに分解します。
  3. サブクエリをナレッジ ソースに対して並列で実行します。
  4. セマンティック ランカーを使用して、結果を再ランク付けし、フィルター処理します。
  5. 上位の結果を統合し、自然言語による回答を生成します。

入力が完全にChatbotのものになっているのが印象的です。今回は複数質問を一度に聞いているので、Naive RAGだと答えられないやつです。

from azure.search.documents.agent import KnowledgeAgentRetrievalClient
from azure.search.documents.agent.models import KnowledgeAgentRetrievalRequest, KnowledgeAgentMessage, KnowledgeAgentMessageTextContent, SearchIndexKnowledgeSourceParams
import textwrap
import json

instructions = """
A Q&A agent that can answer questions about the Earth at night.
If you don't have the answer, respond with "I don't know".
"""

messages = [
    {
        "role": "system",
        "content": instructions
    }
]

agent_client = KnowledgeAgentRetrievalClient(endpoint=search_endpoint, agent_name=knowledge_agent_name, credential=credential)
query_1 = """
    なぜ郊外のベルト地帯は、絶対的な光量が都心部の方が高いにもかかわらず、12月の明るさの増加がより大きくなるのでしょうか?
    なぜフェニックスの夜間の街路網は宇宙からはっきりと見えるのに、米国中西部の都市間を結ぶ高速道路の広い区間は比較的暗いままなのでしょうか?
    """
messages.append({
    "role": "user",
    "content": query_1
})

req = KnowledgeAgentRetrievalRequest(
    messages=[
        KnowledgeAgentMessage(
            role=m["role"],
            content=[KnowledgeAgentMessageTextContent(text=m["content"])]
        ) for m in messages if m["role"] != "system"
    ],
    knowledge_source_params=[
        SearchIndexKnowledgeSourceParams(
            knowledge_source_name=knowledge_source_name,
            kind="searchIndex"
        )
    ]
)

result = agent_client.retrieve(retrieval_request=req, api_version=search_api_version)
print(f"Retrieved content from '{knowledge_source_name}' successfully.")

print("Response")
print(textwrap.fill(result.response[0].content[0].text, width=120))

print("Activity")
for activity in result.activity:
    if hasattr(activity, 'search_index_arguments'):
        print("search_index_param: "+str(activity.search_index_arguments))
    else:
        print(activity)

print("Results")
print(json.dumps([r.as_dict() for r in result.references], indent=2))

実行するとだいたい1分くらいかかりました。少し長いです。
activityに発行したクエリの情報があります。

ターミナル出力
Retrieved content from 'earth-knowledge-source' successfully.
Response
いくつかの観測上の要因が見つかります。まず、12月の明るさ増加については、雪(とそれに対する月光の反射)が夜間の観測で強い明るさ増加をもたらすことが記載されています。VIIRSのDay/Night
Bandは新雪の上での満月の反射などを検出でき、都市域の灯りが雪に反射して明るく見える例が示されています [ref_id:6][ref_id:3]。また、NASAのBlack Marble生成過程では季節的な植生や雪による表面反射の変化を補
正する必要があることが明記されています(雪や季節変化が放射輝度に影響するため)[ref_id:0]。これらの記述から、雪や月光の反射といった季節要因が12月の明るさ増加に寄与することは説明できます [ref_id:6][ref_id:0][
ref_id:3]。しかし、「郊外ベルトで都心より増加が大きい理由」を直接比較・説明する記述は見つかりませんでしたので、その具体的な差がなぜ生じるか(例えば照明種類や道路灯の間隔、降雪量の空間差など)については、取得した文献からは明確に答え
られません。次に、フェニックスの街路網が宇宙からはっきり見える一方で、中西部の都市間高速道路の広い区間が比較的暗い理由については、次の点が記載されています。フェニックスでは規則的な街区格子と街路灯、主要幹線(例:Grand Avenue)沿
いの産業・商業施設や交差点の明るいノードが夜間に明瞭に見えることが説明されています(街路網や幹線沿いの大きな明るい施設が可視化される)[ref_id:1][ref_id:4]。一方で、衛星夜間画像では都市は明るいクラスターとして現れ、郊外・
農地・山地などの人口希薄域は暗く残ることが示されており、都市間を結ぶ区間はそもそも連続した街路灯や都市的な照明が少ないため暗く見えることが示唆されます [ref_id:2][ref_id:4][ref_id:1]。なおVIIRSは単独の道路
灯レベルの暗い光も検出可能であるとされていますが、その一方で「なぜ特定の高速道路区間が暗いままか」の詳細(道路灯の設置間隔、照明の種類や運用、交通量の季節変動など)については、取得した資料には具体的な記述がなく、ここでは確定的な説明はできま
せん [ref_id:3][ref_id:2].
Activity
{'additional_properties': {}, 'id': 0, 'type': 'modelQueryPlanning', 'elapsed_ms': 19493, 'input_tokens': 2121, 'output_tokens': 1802}
search_index_param: {'additional_properties': {}, 'search': '夜間衛星観測 12月 季節変動 郊外 都心 明るさ増加 理由 VIIRS DMSP', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': '冬季の雪や地表反射が夜間衛星画像の輝度に与える影響 冬のアルベド効果', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': '住宅地のクリスマス照明・季節照明が夜間人工光に与える寄与 都市対郊外', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': 'フェニックス 夜間に街路網が衛星から見えやすい理由 都市形態 路灯配置 低層スプロール', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': '米国中西部 高速道路が夜間に暗い理由 高速道路照明政策 路灯配置 田舎区間', 'filter': None}
{'additional_properties': {}, 'id': 6, 'type': 'semanticReranker', 'elapsed_ms': None, 'input_tokens': 113861}
{'additional_properties': {}, 'id': 7, 'type': 'modelAnswerSynthesis', 'elapsed_ms': 29298, 'input_tokens': 5884, 'output_tokens': 2572}
Results
[
  {
    "type": "searchIndex",
    "id": "0",
    "activity_source": 2,
    "reranker_score": 2.7521834,
    "doc_key": "earth_at_night_508_page_187_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "1",
    "activity_source": 4,
    "reranker_score": 2.735878,
    "doc_key": "earth_at_night_508_page_104_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "2",
    "activity_source": 5,
    "reranker_score": 2.689797,
    "doc_key": "earth_at_night_508_page_18_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "3",
    "activity_source": 1,
    "reranker_score": 2.6819987,
    "doc_key": "earth_at_night_508_page_28_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "4",
    "activity_source": 4,
    "reranker_score": 2.6486785,
    "doc_key": "earth_at_night_508_page_105_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "5",
    "activity_source": 1,
    "reranker_score": 2.608269,
    "doc_key": "earth_at_night_508_page_189_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "6",
    "activity_source": 2,
    "reranker_score": 2.513803,
    "doc_key": "earth_at_night_508_page_84_verbalized"
  }
]

実行時のログです。5件のクエリが実行されているのがわかります。
一番上の54秒かかっているのは、時間から推測するに検索開始からLLMでの回答生成までのログかと思います(未確認)。
image.png

6. マルチターン会話

会話をマルチターンで続きます。

query_2 = "夜に溶岩を見つけるにはどうすればいいですか?"
messages.append({
    "role": "user",
    "content": query_2
})

req = KnowledgeAgentRetrievalRequest(
    messages=[
        KnowledgeAgentMessage(
            role=m["role"],
            content=[KnowledgeAgentMessageTextContent(text=m["content"])]
        ) for m in messages if m["role"] != "system"
    ],
    knowledge_source_params=[
        SearchIndexKnowledgeSourceParams(
            knowledge_source_name=knowledge_source_name,
            kind="searchIndex"
        )
    ]
)

result = agent_client.retrieve(retrieval_request=req, api_version=search_api_version)
print(f"Retrieved content from '{knowledge_source_name}' successfully.")

print("Response")
print(textwrap.fill(result.response[0].content[0].text, width=120))

print("Activity")
for activity in result.activity:
    if hasattr(activity, 'search_index_arguments'):
        print("search_index_param: "+str(activity.search_index_arguments))
    else:
        print(activity)

print("Results")
print(json.dumps([r.as_dict() for r in result.references], indent=2))
ターミナル出力
Retrieved content from 'earth-knowledge-source' successfully.
Response
夜間に溶岩を見つけるには、衛星の夜間観測で“夜光(溶岩の輝き)”を検出し、熱赤外データで確認する方法が有効であるという情報が見つかりました。VIIRSのDay/Night
Band(DNB)は夜間の溶岩や火災、ガスフレアなどの弱い光を検出でき、実際に2017年3月のエトナの夜間溶岩流がDNBで撮影されています [ref_id:0][ref_id:1]。また、Landsat
8のOLIとTIRSのような熱赤外センサーで活動的な噴出口や溶岩流の熱署名を強調表示して確認することができます(2018年12月のエトナ側噴火の事例) [ref_id:5]。したがって実務的には、まずVIIRS
DNB等の夜間光画像で輝点を探し、続いてOLI/TIRSなどの熱画像で熱的に一致するかを確認するとよい、という手順が資料に示されています
[ref_id:5][ref_id:3]。DNBは月光や空光(エアグロー)などの微弱光も利用して観測するため、これらの条件も観測結果に影響します [ref_id:5].
Activity
{'additional_properties': {}, 'id': 0, 'type': 'modelQueryPlanning', 'elapsed_ms': 9452, 'input_tokens': 2165, 'output_tokens': 997}
search_index_param: {'additional_properties': {}, 'search': '夜間に溶岩を見つける方法 現地での探し方(夜間の視認ポイント、光や熱の兆候)', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': '夜間 溶岩 サーマル 衛星検出 VIIRS MODIS 赤外線 夜間火山活動検知 方法', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': '夜間に溶岩観察を行う際の安全対策と装備(避けるべき危険、必要なギア、法的許可)', 'filter': None}
search_index_param: {'additional_properties': {}, 'search': '夜間溶岩観察が可能な観光地とガイド付きツアー(ハワイ、アイスランド等) 夜間ツアーの可否と申請情報', 'filter': None}
{'additional_properties': {}, 'id': 5, 'type': 'semanticReranker', 'elapsed_ms': None, 'input_tokens': 85511}
{'additional_properties': {}, 'id': 6, 'type': 'modelAnswerSynthesis', 'elapsed_ms': 14516, 'input_tokens': 6002, 'output_tokens': 1817}
Results
[
  {
    "type": "searchIndex",
    "id": "0",
    "activity_source": 1,
    "reranker_score": 2.6415892,
    "doc_key": "earth_at_night_508_page_64_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "2",
    "activity_source": 1,
    "reranker_score": 2.6302462,
    "doc_key": "earth_at_night_508_page_38_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "1",
    "activity_source": 2,
    "reranker_score": 2.6288283,
    "doc_key": "earth_at_night_508_page_189_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "3",
    "activity_source": 2,
    "reranker_score": 2.6111047,
    "doc_key": "earth_at_night_508_page_194_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "4",
    "activity_source": 2,
    "reranker_score": 2.6004708,
    "doc_key": "earth_at_night_508_page_28_verbalized"
  },
  {
    "type": "searchIndex",
    "id": "5",
    "activity_source": 2,
    "reranker_score": 2.5685685,
    "doc_key": "earth_at_night_508_page_46_verbalized"
  }
]
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?