Finatext Tech Blog
❄️

AI Agent × Cortex Analyst で構造化データ検索精度を47%→97%に改善

に公開

この記事は、ナウキャスト Advent Calendar 2025の 24 日目の記事です。

はじめに

こんにちは、ナウキャストで LLM エンジニアをしている Ryotaro です。
Snowflake Cortex Analyst と AI Agent を組み合わせることで、構造化データの検索精度を大幅に向上させることができました。

この記事では、実際に構造化データ検索 AI Agent を構築し、47%だった精度を 97%まで段階的に改善した経験をもとに、実践的な Tips とつまづいた点を共有します。具体的には、AI Agent と Snowflake Cortex Analyst の組み合わせ方、精度を改善した 6 つの Tips、Agent と Snowflake 側の責務分担の考え方、そして実践的な精度検証の手法について書いていきます。

想定する読者

Snowflake Cortex Analyst を使って構造化データ検索を構築しようとしている方、特に Text2SQL の精度に課題を感じている方向けの記事です。AI Agent から Cortex Analyst を呼び出すアーキテクチャを想定していますが、Agent フレームワークの種類は問いません。

構成の全体像

今回構築した構造化データ検索 AI Agent は、LLM が外部 API を呼び出す Tools 機能のみを使用したシンプルな構成です。マルチエージェント構成やガードレールなどの複雑な機能は使用していません。

アーキテクチャ図

責務分担の考え方

このアーキテクチャでは、Agent と Snowflake Cortex の責務を以下のように分担しています:

Agent 側(OpenAI Agent SDK + Azure OpenAI)の責務:

  • ユーザーの質問を理解し、意図を解釈する
  • 取得したいデータ構造を整理する
  • ドメイン知識を保持し、ビジネスロジックを適用する
  • 構造化データと非構造化データの取得を並列で調整する
  • 複数の情報源からのデータを統合する
  • LLM を使って最終的な回答を生成する

Snowflake Cortex 側の責務:

  • Cortex Analyst: 自然言語から SQL を生成(Text2SQL)し、構造化データを取得
    • 金融機関の業績データ、財務諸表などの構造化データを検索
    • Semantic View をもとにテーブル構造を理解
  • Cortex Search: 非構造化データから関連情報を検索
    • 社内ドキュメント、過去の回答事例などから類似情報を取得
    • ベクトル検索による意味的な類似検索

精度評価の方法

改善の道のりを説明する前に、どのように精度を測定したかを説明します。

テストケースの準備

43 件のテストケースで検証しました:

  • 件数: 43 件のテストケース
  • ユースケース: アセットマネジメント会社における RFP(質問票)回答業務
    • 機関投資家・投資コンサルからの質問に対し、社内情報を収集して回答書を作成
    • 四半期または年度ごとに実施
  • 内容: 実際の業務で想定される質問を網羅
    • 単純な条件検索
    • 集計を伴う検索
    • 複数テーブルにまたがる検索
    • 日付条件を含む検索

Input の構成

各テストケースの Input は、3 つの要素で構成されています:

  1. ユーザーからの質問

    • 例:「過去 3 年間の業績について、営業利益と純資産額を教えてください」
    • 自然言語による質問文
  2. 回答フォーマット

    • 表形式: 行・列の構造を指定
      | 年度 | 営業利益 | 純資産額 |
      |------|---------|---------|
      
    • 自由記述形式: 文章での回答
  3. 前年度の回答

    • 参考情報として提供
    • 昨年度の同じ質問に対する回答内容

評価基準:「意味的に正しい」とは

今回の評価では、「意味的に正しい」 かどうかを基準にしました。

具体的には、以下のいずれかを満たす場合に「正解」と判定しています:

  1. 生成された SQL の実行結果が期待値と一致する
  2. SQL は異なるが、ユーザーの意図を正しく汲んだ回答ができている

評価プロセス

  1. 各テストケースに対して Agent を実行
  2. 生成された SQL を確認
  3. SQL の実行結果を確認
  4. 期待する結果と比較して正誤を判定
  5. 誤りの場合、原因を分析

この評価プロセスを繰り返しながら、後述する Tips を一つずつ適用していきました。

47% → 97% への改善の道のり

ここからは、精度を段階的に改善していった過程を、6 つの Tips として紹介します。

各 Tips では、どのような課題があったか(問題)、どのように対応したか(解決策)、そしてどの程度精度が向上したか(精度への影響)を記載します。

初期状態:精度 47%(43 件中 20 件正解)

最初は Semantic View を基本的な設定で作成し、Agent から素直に質問を投げていました。

この状態ではこんな問題が頻発していました:

  • 期待するカラムが SELECT されない
  • WHERE 句の条件が不正確
  • 集計関数の使い方が間違っている

Tips 1: Markdown テーブル形式でデータ構造を指定する

精度への影響:47% → 67%(+20 ポイント)

最初は「過去 3 年間の業績データを取得してください」とシンプルに質問を投げていました。しかし、これだと期待通りのデータが返ってこないことが多かったです。必要な「営業利益」カラムが SELECT されなかったり、逆に不要なカラムまで含まれていたり...

試行錯誤していく中で、Markdown テーブル形式でカラムを明示的に指定する方法にたどり着きました。これが驚くほど効果的で、精度が 47% から 67% まで一気に 20 ポイントも向上しました。

具体的には、こんな感じで指定します。

改善前:

過去3年間の業績データを取得してください

改善後:

以下の構造でデータを取得してください:

| 年度 | 営業利益 | 経常利益 | 純資産額 |
|------|---------|---------|---------|

条件: 過去3年間の業績データ

さらに試行錯誤を重ねた結果、Markdown テーブルを コードブロック(```)で囲む と LLM がテーブル構造をより正確に認識できることが分かりました。

こちらの方がさらに精度が高くなります

以下の構造でデータを取得してください:

```md
| 年度 | 営業利益 | 経常利益 | 純資産額 |
|------|---------|---------|---------|
```

条件: 過去3年間の業績データ

なぜこれが効くのかというと、Markdown テーブル形式でカラムを明示することで、Cortex Analyst に「どのカラムが必要か」「どの順序で欲しいか」を明確に伝えられます。複雑なテーブル構造を持つデータベースでは特に効果的でした。

ただ、本来であればデータ構造の知識は Snowflake 側(Semantic View)に閉じ込めて、Agent は「業績データが欲しい」と伝えるだけで適切なデータが返ってくるのが理想です。実際にそれを試してみたのですが、Text2SQL の精度がまだ十分ではなく、Agent 側でデータ構造を明示する必要がありました。このあたりの「知識の境界線をどこに引くか」という話は、後ほど詳しく考察します。

Tips 2: ドメイン知識は Agent 側に入れる

精度への影響:67% → 77%(+10 ポイント)

次につまずいたのが、ビジネス固有のルールや用語の扱いでした。

たとえば「運用部門スタッフ」と言われたとき、これが「運用部門と分析部門の両方の人員」を意味している場合があります。もちろんこの内容は AI が知識として持っていないため、個別その内容を指示する必要があります。最初はこういった情報を Semantic View の custom_instructions に書いていましたが、生成される SQL に反映されないことが多く困っていました。

いろいろ試した結果、ドメイン知識は Agent の system prompt に記載 して、Agent が質問を解釈する段階で適切に変換する方が確実だと分かりました。

Agent の system prompt 例:

あなたは構造化データを検索する AI Agent です。

以下のドメイン知識を理解した上で、ユーザーの質問を解釈してください:

## ビジネス用語の定義
- 「運用部門スタッフ」: 運用部門と分析部門の両方に所属する人員を指す
- 「主要ファンド」: 運用残高が100億円以上のファンドを指す
- 「アクティブ顧客」: 過去1年以内に取引実績がある顧客を指す

## データ取得のルール
- 人員数は期末時点のスナップショット値を使用する
- 運用残高は四半期末の評価額を使用する
- 部門間の兼務者は主担当部門でカウントする

ユーザーの質問を受け取ったら、これらのルールを適用して Cortex Analyst Tool に適切な質問を投げてください。

実際の動きとしては、ユーザーから「運用部門スタッフの人数を教えてください」という質問が来たら、Agent が system prompt のドメイン知識を使って「運用部門と分析部門の両方の人員数」に変換してから Cortex Analyst に投げる、という流れです。

以下の条件で人員データを取得してください:
- 対象部門: 運用部門、分析部門
- 集計方法: 両部門の合計人数

| 部門名 | 人数 |
|-------|------|

もちろん、本来であれば Semantic View の custom_instructions にドメイン知識を記載して、Cortex Analyst がそれを理解して SQL を生成してくれるのが理想です。将来的に精度が向上すれば、この知識を Snowflake 側に移行できるかもしれません。

Tips 3: sample_values と is_enum で WHERE 句の精度を上げる

精度への影響:77% → 87%(+10 ポイント)

WHERE 句の条件がおかしくなる問題も頻発していました。たとえば、ステータスのカラムで存在しない値を条件に指定するパターンです。

これは Semantic View の dimension 定義で sample_values と is_enum を設定 することで解決できました。

Semantic View の設定例:

tables:
  - name: sales
    base_table:
      database: DEMO_DB
      schema: SALES
      table: FACT_SALES
    dimensions:
      - name: status
        synonyms: ["ステータス", "状態"]
        data_type: varchar
        sample_values: ["completed", "pending", "cancelled"]
        is_enum: true # 追加:この値のみ許可
      - name: product_category
        synonyms: ["商品カテゴリ", "カテゴリ"]
        data_type: varchar
        sample_values: ["Electronics", "Clothing", "Food"]
        is_enum: true

is_enum: true を設定することで、Cortex Analyst は sample_values に記載された値以外を WHERE 句に使わなくなります。存在しない status 値を指定するミスがなくなり、WHERE 句の精度が大幅に向上しました。

ただし注意点として、is_enum は dimension でしか機能しません。fact で同じ設定を試してみたのですが、効果がありませんでした。

たとえば、こんな設定は 効かない です:

# これは効かない
tables:
  - name: sales
    facts:
      - name: amount
        sample_values: [100, 200, 300]
        is_enum: true # fact では無視される

fact で特定の値に制限したい場合は、custom_instructions や Agent 側で制御する必要があります。

Tips 4: よく使う集計は metrics で定義しておく

精度への影響:87% → 90%(+3 ポイント)

集計関数の使い方でもミスが出ていました。「売上合計」と言ったときに、間違ったカラムを SUM してしまったり、COUNT(*) と COUNT(DISTINCT) を混同したり。

こういった頻出の集計パターンは、Semantic View で metrics として事前に定義 しておくと確実です。

Semantic View の設定例:

tables:
  - name: sales
    base_table:
      database: DEMO_DB
      schema: SALES
      table: FACT_SALES
    facts:
      - name: amount
        expr: amount
        data_type: number
    metrics:
      - name: total_sales
        synonyms: ["売上合計", "総売上"]
        definition: SUM(amount)
        description: "売上金額の合計"
      - name: average_sales
        synonyms: ["平均売上", "売上平均"]
        definition: AVG(amount)
        description: "売上金額の平均"
      - name: unique_customers
        synonyms: ["ユニーク顧客数", "顧客数"]
        definition: COUNT(DISTINCT customer_id)
        description: "購入した顧客のユニーク数"

これにより、ユーザーが「売上合計を教えて」と質問すれば、Cortex Analyst は自動的に SUM(amount) を使った SQL を生成してくれます。複雑な集計ロジックを Semantic View 側で一元管理できるのも便利でした。

Tips 5: 日付は Agent 側で統一フォーマットに変換

精度への影響:90% → 93%(+3 ポイント)

日付の扱いも厄介でした。「今月」「先月」といった相対的な期間指定や、YYYY と YYYY-MM-DD のようにテーブルごとに保持する日付データの粒度がまちまちなため、条件がうまく作れないケースが多いです。

これは Agent 側で 日付条件を統一フォーマットに変換 してから Cortex Analyst に投げることで解決しました。

日付正規化の例:

ユーザーの表現 正規化後の表現
今月 2024-12-01 ~ 2024-12-31
先月 2024-11-01 ~ 2024-11-30
2024 年度 2024-04-01 ~ 2025-03-31
2024 年 1 月 2024-01-01 ~ 2024-01-31

正規化前の質問:

今年度の業績データを取得してください

正規化後の質問:

以下の期間の業績データを取得してください:
- 期間: 2024-04-01 ~ 2025-03-31

| 年度 | 営業利益 | 経常利益 |
|------|---------|---------|

これで日付条件の解釈ミスが大幅に減り、「今年度」のような相対的な期間指定も正確に処理できるようになりました。

Tips 6: 複数テーブルは別々に取得して統合する

精度への影響:93% → 97%(+4 ポイント)

最後に、複数テーブルにまたがる情報を取得するときの問題です。

最初は「業績データと人員データを一緒に取得してください」と一度に問い合わせていたのですが、join が必要ではない場合は別々に取得する方が確実です。

よく観察してみると、問題が起きるケースには共通点がありました:

  1. テーブルごとに集計処理が違う:業績データは年度ごとの「合計」、人員データは期末時点の「スナップショット」
  2. 期間情報の持ち方が違う:業績データは fiscal_year、人員データは snapshot_date で期間を表現

こういったケースでは、複数のテーブル情報を別々に Cortex Analyst に問い合わせて、Agent 側で統合する方が確実でした。

たとえば、業績データと人員データを別々に取得する場合:

# 業績データ
以下の構造で過去3年間の業績データを取得してください:
| 年度 | 営業利益 | 経常利益 |
|------|---------|---------|

# 人員データ
以下の構造で過去3年間の人員データを取得してください:
| 年度 | 部門名 | 従業員数 |
|------|-------|---------|

取得した 2 つのデータを Agent 側で年度をキーにして結合します。業績データは年度ごとの累計値、人員データは年度末時点のスナップショット値という異なる集計ロジックを持つため、別々に取得する方が精度が高くなるわけです。

この方法は Cortex Analyst を複数回呼び出すためレイテンシが増加するというデメリットはありますが、精度向上のメリットの方が大きいと判断しました。SQL も単純になるのでデバッグしやすくなります。

最終結果:精度 97%(43 件中 42 件正解)

6 つの Tips をすべて適用した結果、精度 47% → 97% と、50 ポイントの改善を達成しました。

改善の内訳:

  • Tips 1(Markdown テーブル): +20 ポイント
  • Tips 2(ドメイン知識): +10 ポイント
  • Tips 3(sample_values & is_enum): +10 ポイント
  • Tips 4(metrics 定義): +3 ポイント
  • Tips 5(日付条件統一): +3 ポイント
  • Tips 6(複数テーブル分割): +4 ポイント

知識の境界線についての考察

ここまで紹介してきた Tips の中で、特に Tips 1、2、5、6 は 「Agent 側に知識を持たせる」 アプローチでした。

本来、理想的なアーキテクチャはこんな感じです:

理想的な責務分担

理想の世界では:

  • Agent はビジネスロジックだけを持つ
  • データ構造、ドメイン知識、SQL 生成はすべて Snowflake 側(Semantic View)に閉じ込める
  • Agent は「売上データが欲しい」と伝えるだけで、適切なデータが返ってくる

現実の責務分担

しかし現実には、こんな理由で Agent 側に知識を持たせる必要がありました:

現実の制約:

  1. Text2SQL の精度が不十分

    • Semantic View の custom_instructions が効かないことが多い
    • 複雑なビジネスルールを SQL に変換できない
  2. データ構造の明示が必要

    • 期待するカラムを明示しないと、不正確な結果が返ってくる
    • Markdown テーブル形式での指定が効果的
    • Semantic View の synonyms を定義しても、必要なカラムが SELECT されなかったり、不要なカラムまで含まれるケースがあった
  3. 複数テーブルの JOIN が苦手

    • Relationship が定義されていても JOIN が不適切になることがある
    • Agent 側で分割して問い合わせる方が確実
    • VQR(Verified Query Repository)も検討したが、特定のクエリパターンに過度に適合(オーバーフィット)するリスクがあり、汎用性の観点から採用を見送った

この「知識の境界線をどこに引くか」という問題は、AI Agent と外部システムを組み合わせる際の普遍的な課題です。今回の経験が、同様のシステムを構築する方々の参考になれば幸いです。

まとめ

この記事では、OpenAI Agent SDK と Snowflake Cortex Analyst を組み合わせて構造化データ検索 AI Agent を構築し、精度を 47% から 97% まで改善した過程を紹介しました。

OpenAI Agent SDK の Tools 機能のみを使ったシンプルな構成で、43 件のテストケースに対して 97% の精度を達成できました。6 つの実践的な Tips を段階的に適用していった結果です。

特に効果が大きかったのは、Markdown テーブル形式でデータ構造を明示する方法で、これだけで +20 ポイントの精度向上がありました。日本語カラム名を使って Agent 側でカラムを明示的に指定し、Cortex Analyst が synonyms を参照して英語に変換するという流れです。

次にドメイン知識を Agent の system prompt に持たせることで +10 ポイント改善しました。Semantic View の custom_instructions が効かないケースが多かったため、Agent 側で柔軟に管理する方が確実だったんです。

Semantic View 側では sample_values と is_enum で WHERE 句の精度を上げたり、metrics で集計ロジックを一元管理することで合計 +13% の改善。そして Agent 側での日付条件の正規化や複数テーブル情報の分割取得で +7% の改善と、地道な積み重ねが効いています。

今後の展望

正直なところ、今の構成は Agent 側にかなり知識を寄せている状態です。Cortex Analyst の custom_instructions がもう少し効くようになれば、ドメイン知識も Snowflake 側に移行できるんですが、現時点では Agent 側で管理するのが安定しています。

将来的には、Cortex Analyst の精度が向上して、すべての知識を Snowflake 側に集約できるといいなと思っています。そうなれば Agent はビジネスロジックだけに集中できるので、システム全体がシンプルになります。

また、2025 年 11 月に GA された Snowflake Managed MCP Server も注目です。MCP(Model Context Protocol)標準に準拠したこのサーバーを使えば、Cortex Analyst と Cortex Search を Agent から呼び出せる Tool として定義できます。REST API を直接呼び出す代わりに、MCP 対応クライアント(Claude Desktop など)から標準化された方法で統合でき、OAuth 認証や RBAC によるガバナンスも組み込まれています。

Snowflake Cortex Analyst と AI Agent を組み合わせることで、構造化データの検索を自然言語で行える強力なシステムを構築できます。この記事が、同様のシステムを構築する方々の一助となれば幸いです。

付録

Cortex Analyst REST API の実行方法

Agent から Cortex Analyst を呼び出す際の、REST API の基本構造を説明します。

エンドポイント:

POST https://{ACCOUNT_NAME}.snowflakecomputing.com/api/v2/cortex/analyst/message

リクエストボディ:

{
  "messages": [
    {
      "role": "user",
      "content": [
        {
          "type": "text",
          "text": "2024年の売上合計を教えてください"
        }
      ]
    }
  ],
  "semantic_view": "{DATABASE}.{SCHEMA}.{SEMANTIC_VIEW_NAME}"
}

ヘッダー:

Authorization: Snowflake Token="{YOUR_TOKEN}"
Content-Type: application/json

レスポンスの構造:

Cortex Analyst API からのレスポンスには、こんな情報が含まれます:

{
  "request_id": "75d343ee-699c-483f-83a1-e314609fb563",
  "message": {
    "role": "analyst",
    "content": [
      {
        "type": "text",
        "text": "We interpreted your question as ..."
      },
      {
        "type": "sql",
        "statement": "SELECT * FROM table"
      }
    ]
  },
  "response_metadata": {
    "model_names": ["claude-3-5-sonnet"],
    "question_category": "CLEAR_SQL"
  }
}

重要な注意点:ACCOUNT_NAME のアンダースコア問題

ACCOUNT_NAME に _ が含まれている場合、SSL 証明書取得エラーが発生することがあります。

# ❌ これはエラーになる可能性がある
ACCOUNT_NAME = "my_account_name"

# ✅ アンダースコアをハイフンに変換
ACCOUNT_NAME = "my-account-name"

Snowflake 公式でも「アンダースコアが対応していないシーンでは、ハイフンを使用する」と明記されています。

よくあるエラーと対応策

エラー 1: "Information is insufficient" / "情報が不足しています"

質問文が曖昧すぎたり、必要なテーブルやカラムの情報が Semantic View に定義されていないときに発生します。

対応としては、Tips 1 で紹介した Markdown テーブルでデータ構造を明示する方法が効果的です。また、Semantic View に必要な dimension/fact/metrics が足りていないか確認してください。

注意点として、質問文にテーブル名やカラム名を直接入れると Cortex Analyst が混乱することがあるので、明示的に入れないようにしましょう。

悪い例:

業績テーブルから 2024 年度のデータを取得してください

良い例:

以下の構造で 2024 年度の業績データを取得してください:
| 年度 | 営業利益 | 経常利益 | 純資産額 |
|------|---------|---------|---------|

エラー 2: "Sorry, but the question is not clear"

質問文が複雑すぎたり、複数のテーブルにまたがる複雑な JOIN を一度に要求していたり、曖昧な表現が含まれているときに発生します。

対応策としては、まず質問を段階的に分解してみてください。Tips 6 で紹介したように、複数テーブルの情報は別々に取得する方が確実です。一度に取得するデータの範囲を絞ることも効果的です。また、曖昧な表現は Agent 側で前処理して具体的な条件に変換しましょう。

エラー 3: WHERE 句の条件が不正確

enum 的なカラムで存在しない値を指定していたり、日付フォーマットが統一されていないときに発生します。

対応としては、Semantic View の dimension に sample_values と is_enum を設定してください(Tips 3 参照)。また、日付条件は Agent 側で統一フォーマットに変換することで精度が上がります(Tips 5 参照)。

エラー 4: 集計結果がおかしい

集計関数の使い方が不適切だったり、間違ったカラムを集計しているときに発生します。

対応としては、Semantic View に metrics を事前定義しておくことが効果的です(Tips 4 参照)。また、集計ロジックを質問文で明確に指定することで精度が向上します。

前年度回答を使用する際の注意点

本記事で紹介したシステムでは、Input の一部として 前年度の回答 を提供しています。これは回答の一貫性を保つために有効ですが、いくつかのリスクも伴います。

リスク 1: 古い情報の参照

前年度の回答を Input に含めることで、Agent が最新のデータではなく、古い情報を参照してしまう可能性があります。

具体例:

質問:「2024 年度の営業利益を教えてください」
前年度回答:「2023 年度の営業利益は100億円でした」

→ Agent が前年度回答に引っ張られて、2023 年度のデータを返してしまう

リスク 2: ハルシネーション(幻覚)の誘発

LLM は与えられた情報を信頼する傾向があるため、前年度の回答に誤りや古い情報が含まれていると、それをもとにハルシネーションを起こす可能性があります。

具体例:

前年度回答に誤った数値が含まれていた場合:
「2023 年度の営業利益は100億円でした」(実際は80億円)

→ Agent が前年度の誤った数値を参照し、最新データと混同した回答を生成してしまう

対策:Prompt による制御

これらのリスクに対して、本システムでは Prompt による制御 で対処しています。

Agent の system prompt 例:

あなたは RFP 質問に回答する AI Agent です。

## 重要な注意事項
1. **必ず最新データを参照してください**
   - 前年度の回答は「参考情報」として提供されています
   - 回答する際は、必ず Cortex Analyst から最新のデータを取得してください
   - 前年度の回答をそのまま使用してはいけません

2. **前年度回答の使い方**
   - 回答のフォーマットや構成の参考にする
   - 質問の意図を理解する補助とする
   - 数値や事実は必ず最新データで上書きする

3. **データの鮮度を意識する**
   - 質問で指定された年度・期間のデータを正確に取得する
   - 前年度との比較が必要な場合は、両方のデータを明示的に取得する

## 処理フロー
1. ユーザーの質問を理解する
2. 前年度回答から「回答フォーマット」を把握する
3. Cortex Analyst Tool を使って**最新データ**を取得する
4. 取得したデータをもとに回答を生成する

効果

この Prompt による制御により、前年度回答を「フォーマットの参考」として活用しつつ、データは必ず最新のものを Cortex Analyst から取得し、ハルシネーションのリスクを最小化する仕組みを実現しています。

Finatext Tech Blog
Finatext Tech Blog

Discussion