第5回 Web検索機能によって生成AIとの会話中の知識を強化する
本エントリはUbie 生成AI Advent Calendar 2024の14日目、「社内用生成AI Webアプリケーションをどのように作っているか」の第5回です。
前回は、第4回 Slackから生成AIを呼び出せるようにするについて説明しました。今回は、検索機能によって会話中の知識を強化する方法について解説します。
最新の情報を生成AIに渡したい
「第3回 生成AIのモデルと外部データを連携可能にする」で、生成AIのモデル(LLM:Large Language Model=大規模言語モデル)は外部と通信する能力を持たないという話をしました。LLMとの対話のプロセスを整理すると、必要なタイミングで外部情報を渡せば良いことがわかりました。そして、Function Callingを用いて、LLM自身に外部情報の取得の要否を判断させられるという例を紹介しました。
紹介した例では、一般的なWebページの他にNotionやSlack、GithubなどのインターナルなURLの内容を取得可能にする仕組みを作りました。この仕組みを用いるには、常にURLの指定が必要となります。「◯◯の情報が知りたい」といった、取得すべき情報が曖昧なユースケースをカバーできません。
すでにFunction Callingによって、LLM自身に情報取得等の判断をさせることができることがわかりました。Web検索の機能を提供すれば、最新の情報を生成AIで取り扱えそうです。
インターネットに繋げたいね
Web検索をする際の入力を考える
Web検索の機能をLLMに提供したとして、LLMはその機能をどのように呼び出せばよいでしょうか。現在利用できるWeb検索APIの中には、LLMを組み込んだものがあり自由入力文をそのまま渡せるものもありますが、ここではより一般的なアプローチを前提に考えたいと思います。
LLMにWeb検索をして答えてもらう仕組みの実現方法が知りたい
場合、人間(主に日本語話者)はどのように検索をするでしょうか。これもLLMに聞いてみましょう。
いつから単語で区切って検索するようになったんだろう
LLM Web検索 連携 実装方法
というワードが得られました。これを使ってWeb検索をしてみた結果が以下です。
それっぽいのが見つかりそうだ
LLMに必要に応じて検索クエリを考えてもらうアプローチは概ねうまくいきそうですね。
Web検索を実現する
次はWeb検索そのものの実現です。ほとんどの場合、既存のWeb検索APIを利用すればユースケースを満たせそうです。Web検索APIは沢山ありますが、気軽に試せるものとしては以下があります。他にもおすすめがあればぜひ教えてください。
-
Tavily
- LLMと統合されており、自由入力文での検索も可能
- 月間のFree枠が1000回
-
Brave Search API
- 月間のFree枠が2000回
UbieではTavilyを利用しています。利用量としては無料枠は超えてしまいますが、社内利用なのでそこまで大きく膨らむこともないため、コスト面では問題ないと判断しました。Tavilyで検索を利用する例は以下の通りです。LangchainのCommunityライブラリにTavilyのラッパーが存在するので簡単に利用できます。
import { TavilySearchResults } from "@langchain/community/tools/tavily_search";
type WebSearchResult = {
title: string;
url: string;
content: string;
};
async function search(input: { query: string }): Promise<WebSearchResult[]> {
const tavily = new TavilySearchResults({
maxResults: 3,
});
const result = JSON.parse(
await tavily.invoke({
input: input.query,
}),
) as WebSearchResult[];
return result;
}
Tavilyは検索結果のURLやページタイトルだけでなく、ページの内容も取得できるため便利です。
Web検索をFunction Callingで利用可能にする
Web検索の入力と出力が実現できたので、Function CallingでLLMから利用可能にしていきます。以下はLLMに渡す、Web検索機能の定義です。
import { tool } from "@langchain/core/tools";
const searchTool = tool(
// LLMから呼び出されたときの処理
async (input) => {
try {
const output = await search({
query: input.query as string,
});
return output;
} catch (e) {
throw new Error("Failed to search");
}
},
{
// ツールの名前、LLMはこの名前を指定して呼び出す
name: "tavily_search",
// ツールの説明、LLMはこの説明を見てツールを選択する
description: "This is a web search engine. It is useful when you need to answer questions about the latest events. The input is a search query and the output is a JsonArray of the top 3 first results with url. The search query should be entered in the language of the user prompt.",
// ツールのパラメータのスキーマ、LLMはこのスキーマに従った引数を渡す
schema: z.object({
query: z.string().describe("search query"),
}),
},
);
あとはAgentに渡して実行するだけです。各種コード例は色々と省略しているのでご注意ください。
import { createToolCallingAgent } from "langchain/agents";
const agent = createToolCallingAgent({
llm: llm,
tools: [searchTool],
prompt: prompt,
});
const result = await agent.invoke({});
LLMにWeb検索をして答えてもらう仕組みの実現方法が知りたい
と尋ねると、検索した結果を使って回答してくれました。
精度はともかく曖昧な入力が可能なのはうれしい
生成AIの振る舞いをユーザに伝える
前項で示した検索を使った結果のキャプチャには、いくつかの工夫が含まれています。もし画面上で、裏側の処理について何も説明しないとすると以下のような表示になるでしょう。
LLMは検索したと言い張っているが...
この画面では、どのように検索したか、検索結果がどのようなものかが示されていません。この場合LLMが適当に言っている可能性を否定できません。そこでFunction Callingの履歴を保存し、画面上で表示するようにしています。
Function Callingの履歴をユーザに示す
この辺りは生成AIを用いる際のUXに関するテーマであり、今後も様々なアプローチが生まれるのだろうなと思います。以下はFeloの動作例です。何をやっているかプロアクティブに見せてくれます。
UIのアニメーションは今後より重要になってきそう
まとめ
LLMにWeb検索機能を統合することで、最新の情報に基づいた回答を可能にする方法を解説しました。曖昧な質問に対してLLM自身に検索クエリを生成させ、検索結果を利用して回答してもらうことで、人間の手による検索をある程度代替してくれます。
ただ、このアプローチは飽くまでワンショットの検索を代行しているに過ぎません。より複雑な探索的な調査などは、Plan-and-Executeのようなアプローチが必要になるでしょう。ただこの場合も、要素としてのWeb検索機能は必要なので、まずはワンショットの検索機能を設けること自体は無駄にはならないでしょう。
Discussion