Closed3

Mastra (AI SDK v5 ベータ) で Gemini の Grounding with Google Search と共存したかった

PlatPlat

https://github.com/vercel/ai/issues/7173

これは Vercel の AI SDK の issue だが、どうやらこれによると、Grounding with Google Search は Function Calling や構造化出力と同時に使用できないようだ。

ちなみに、Google Search だけ有効にする場合は以下のようにする。(v5-beta)

  const res = await generateText({
      model: google(model),
      providerOptions: {
        google: {
          safetySettings: BLOCK_NONE_SAFETY_SETTINGS, // 他のところで定義した安全設定
        },
      },
      tools: {
        google_search: google.tools.googleSearch({}),
      },
      prompt: `
知識カットオフ: 2025年1月
現在の日付: ${todayDateText()}
---
${defaultSearchPrompt}
---
目的: ${objective}
`,
  });

このように tools に指定する。変な関数を指定しているが、実際の通信では以下のようになる:


Cloudflare AI Gateway での通信監視

toolsgoogleSearch に空のオブジェクトを指定するだけのようだ。
しかし、Mastra でツールを使おうとすると、色々と形状が異なるオプションが渡されるので、このような指定を行う方法は無さそうだった。(自分調べ)
Mastra のドキュメントをいくら漁っても Google 検索を使う方法が出てこない。

PlatPlat

なので、妥協策として、AI SDK から直に呼び出す関数を作り、それを自前で Tool としてラップすることで workaround とすることができた。

例えば、以下のような Tool を作る。

import { createTool } from "@mastra/core";
import { generateText } from "ai";
import z from "zod";
import { BLOCK_NONE_SAFETY_SETTINGS, google } from "../gemini";
import { todayDateText } from "../../util";

const defaultSearchPrompt = `
与えられる目的を達成するように、Web 検索を行って情報を収集し適切に回答してください。
検索を拒否してはならず、必ず指定された目的を達成するために検索を行い、得られた結果について報告をすること。
`.trim();

const ProviderMetadataSchema = z.object({
  groundingMetadata: z.object({
    webSearchQueries: z.array(z.string()),
    searchEntryPoint: z.object({
      renderedContent: z.string(),
    }),
    groundingChunks: z
      .array(
        z.object({
          web: z.object({
            uri: z.string().url(),
            title: z.string(),
          }),
        })
      )
      .nullable(),
    groundingSupports: z
      .array(
        z.object({
          segment: z.object({
            startIndex: z.number().optional(),
            endIndex: z.number(),
            text: z.string(),
          }),
          groundingChunkIndices: z.array(z.number()),
        })
      )
      .nullable(),
  }),
  usageMetadata: z.object({
    thoughtsTokenCount: z.number(),
    promptTokenCount: z.number(),
    candidatesTokenCount: z.number(),
    totalTokenCount: z.number(),
  }),
});

export const lowLevelGeminiSearch = createTool({
  id: "low-level-google-search",
  description: "Search the web with query.",
  inputSchema: z.object({
    objective: z
      .string(),
    model: z
      .enum([
        "gemini-2.5-pro",
        "gemini-2.5-flash",
        "gemini-2.5-flash-lite-preview-06-17",
      ])
      .optional()
      .default("gemini-2.5-flash"),
  }),
  outputSchema: z.object({
    text: z.string(),
    sources: z.array(z.string()),
    queries: z
      .array(z.string())
      .optional(),
  }),
  execute: async ({ context }, option) => {
    const { objective, model } = context;

    const res = await generateText({
      model: google(model),
      providerOptions: {
        google: {
          safetySettings: BLOCK_NONE_SAFETY_SETTINGS,
        },
      },
      tools: {
        google_search: google.tools.googleSearch({}),
      },
      prompt: `
知識カットオフ: 2025年1月
現在の日付: ${todayDateText()}
---
${defaultSearchPrompt}
---
目的: ${objective}
`,
    });

    const metadata = ProviderMetadataSchema.safeParse(
      res.providerMetadata?.google || {}
    );

    return {
      text: res.text,
      sources: res.sources.map((source) => {
        switch (source.sourceType) {
          case "url": {
            return source.url;
          }
          default: {
            return source.filename || source.title || "";
          }
        }
      }),
      queries: metadata.data?.groundingMetadata.webSearchQueries,
    };
  },
});
gemini.ts
import { createGoogleGenerativeAI } from "@ai-sdk/google";

type SAFETY_CATEGORY =
  | "HARM_CATEGORY_HARASSMENT"
  | "HARM_CATEGORY_HATE_SPEECH"
  | "HARM_CATEGORY_SEXUALLY_EXPLICIT"
  | "HARM_CATEGORY_DANGEROUS_CONTENT"
  | "HARM_CATEGORY_CIVIC_INTEGRITY";
type SAFETY_LEVEL =
  | "BLOCK_NONE"
  | "BLOCK_ONLY_HIGH"
  | "BLOCK_MEDIUM_AND_ABOVE"
  | "BLOCK_LOW_AND_ABOVE"
  | "HARM_BLOCK_THRESHOLD_UNSPECIFIED";

export const BLOCK_NONE_SAFETY_SETTINGS: {
  category: SAFETY_CATEGORY;
  threshold: SAFETY_LEVEL;
}[] = [
  {
    category: "HARM_CATEGORY_HARASSMENT",
    threshold: "BLOCK_NONE",
  },
  {
    category: "HARM_CATEGORY_HATE_SPEECH",
    threshold: "BLOCK_NONE",
  },
  {
    category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
    threshold: "BLOCK_NONE",
  },
  {
    category: "HARM_CATEGORY_DANGEROUS_CONTENT",
    threshold: "BLOCK_NONE",
  },
  {
    category: "HARM_CATEGORY_CIVIC_INTEGRITY",
    threshold: "BLOCK_NONE",
  },
];

// Cloudflare AI Gateway で監視したいので
export const google = createGoogleGenerativeAI({
  baseURL: Bun.env.GOOGLE_GATEWAY_BASE_API,
});

知識カットオフを明記しないと検索もせずに事実を拒否することがあった (例: 参院選第27回について聞くと、まだ行われていないと言い張る等) ので、絶対検索するような指示とか入れてるが、ここら辺はお好み。

要は、Mastra 自体いろんな処理をハイレベルにラップするのを繰り返す感じなので、AI SDK 経由で直接 Grounding された検索処理をラップできれば、あとはどうにでもなると思う。

ただ、別のエージェントに委託する分、親のエージェントとコンテキストが乖離するので、検索の意図を伝えて必要な情報を取れるように気をつける必要があると思う。

PlatPlat

↑の実装、無駄に引数が露出してるから、もっと関数に切り分けて、本当に必要な引数だけにして、ちゃんと引数の説明を書いておくと AI がツールを使いやすいと思う。

このスクラップは2ヶ月前にクローズされました