Microsoft365 Agents Toolkitで実装するFucntionCalling
背景
Microsoft365 Agents Toolkit(旧:TeamsToolkit)を利用してチャットボットを作成する際、Function Callingでのツール呼び出しを行うTeamsBotサーバの構築を試みました。
しかし、Agents Toolkitに関する情報や実装例が少なく、理解しにくい部分が多かったため、本記事ではTeamsのチャットボットにおいてFunctionCallingを実装する方法をまとめました。
対象読者
- TeamsでAIBotを作成したい方
- Typescript 初心者~中級者
環境
項目 | バージョン |
---|---|
OS | Windows11 Pro |
ランタイム | Node.js v22.14.0, Typescript 5.5.4 |
事前準備
- Microsoft Agents Toolkitのセットアップと新規Botの作成
方法は以下にまとめてあります。
上記の手順で新規Botを作成すると以下のようなフォルダ構成になっていると思います。
├── appPackage \
├── env \
├── infra \
├── node_modules \
└── src \
└── adapter.ts
└── agent.ts
└── config.ts
└── index.ts
...
構成
Ragを用いたTeamsBotを作成しました。
今回作成した構成では、大きく分けてTeams、TeamsBotサーバ、APIサーバ、ベクトルDBの4つの構成になっています。この構成では、ユーザが質問をTeamsで送信してからLLMが回答を返すまで以下のような流れになっています。
- TeamsBotサーバからのリクエスト送信
TeamsBotサーバはユーザーの問い合わせに基づき、検索リクエストをREST API形式でAPIサーバーに送信します。 - ベクトルDBによるクエリ実行
APIサーバーは指示された内容に従い、ベクトルDBにクエリを投げ、ユーザからの質問文を基にベクトル検索します。 - 検索結果の返却
得られたデータは、APIサーバーからTeamsBotサーバに対してそのまま返却されます。APIサーバーは、DBの検索結果を加工することなく、シンプルに転送するだけの役割を果たします。
手順
1. Tool定義
APIサーバにリクエストを送信するツールを定義します。
また、タイトルやセクションを分けてベクトルDBに格納しているため、LLMに渡す前に少しデータを整形しています。
async function getKnowledge(query: string) {
const res = await fetch("http://localhost:8000/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: query }),
});
const data = await res.json();
if (Array.isArray(data)) {
if (data.length === 0) return "DBから回答が得られませんでした。";
// 検索結果を整形
const formattedResults = data.map(item => {
if (item.p) {
let result = '';
if (item.h1) result += `タイトル: ${item.h1}\n`;
if (item.h2) result += `セクション: ${item.h2}\n`;
if (item.h3) result += `サブセクション: ${item.h3}\n`;
result += `内容: ${item.p}\n`;
result += `(ID: ${item.pg_id})\n`;
return result;
}
return JSON.stringify(item);
}).join("\n\n");
return formattedResults;
}
}
2. Chat Completions APIを定義
各パラメータを少し説明すると
- message: 会話を構成するメッセージリスト
- model: 使用するモデル
- tools: モデルに提示する関数の定義
- tool_choice: ツール呼び出しの制御
という風になっています。tool_choice
は今回は"auto"にすることでLLMに自律的に判断させていますが、
tool_choice: { type: "function", function: { name: "ツール名" } }
にすることで、LLMが必ずこの関数を選択するようになります。
使用できるパラメータの説明は以下にまとめています。
const result = await client.chat.completions.create({
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: context.activity.text,
},
],
model: config.azureOpenAIDeploymentName,
tools: [
{
type: "function",
function: {
name: "get_knowledge",
description: "〇〇株式会社のナレッジDBにPOSTリクエストを送って情報を取得する",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "質問内容",
},
},
required: ["query"],
},
},
},
],
tool_choice: "auto",
});
3. 最終的な回答の形成
LLMからのレスポンスを基にユーザへ送信する回答を形成します。
Chat Completions APIのレスポンスには、finish_reason
というパラメータが含まれており、通常の回答が完了した場合はstop
、ツールの呼び出しが必要な場合はtool_calls
が設定されています。[1]
つまり、LLMからは回答の途中で外部の関数実行が要求され、その関数を実行し結果を取得した上で、再度LLMに組み込んだ回答を生成させる流れになっています。
LLMに関数の結果を組み込んで渡すためには、messages
プロパティに{ role: "tool", name: "ツール名", content: 関数の実行結果 }
を付け加えます。
let answer = "";
for (const choice of result.choices) {
if (choice.finish_reason === "tool_calls" && choice.message.tool_calls) {
for (const toolCall of choice.message.tool_calls) {
if (toolCall.function && toolCall.function.name === "get_knowledge") {
const args = toolCall.function.arguments;
const parsed = JSON.parse(args || "{}");
const knowledge = await getKnowledge(parsed.query);
const secondResult = await client.chat.completions.create({
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: context.activity.text },
{ role: "tool", name: "get_knowledge", content: knowledge },
],
model: config.azureOpenAIDeploymentName,
});
if (secondResult.choices[0]?.message?.content) {
answer += secondResult.choices[0].message.content;
} else {
answer += "失敗しました。";
}
}
}
} else if (choice.message?.content) {
answer += choice.message.content;
} else {
answer += "応答の形式が正しくありません。";
}
}
await context.sendActivity(answer);
コード全体
import { ActivityTypes } from "@microsoft/agents-activity";
import { AgentApplication, MemoryStorage, TurnContext } from "@microsoft/agents-hosting";
import { AzureOpenAI, OpenAI } from "openai";
import config from "./config";
const client = new AzureOpenAI({
apiVersion: "2024-12-01-preview",
apiKey: config.azureOpenAIKey,
endpoint: config.azureOpenAIEndpoint,
deployment: config.azureOpenAIDeploymentName,
});
const systemPrompt = "あなたはTeamsで動く〇〇のQ&Aエージェントです。\
以下のルールに従って回答してください。\
- Markdown形式で出力してください。\
- 必要なら会社のDBから情報を検索してそれを基に回答してください。DBから参照する場合は'参照: [http://localhost:60191/{pg_id}](http://localhost:60191/{pg_id})'という形で最後に示してください。\
- DBから検索した結果を用いる際は、その情報が質問内容とマッチしているかどうか考えてから回答してください。\
- 必要に応じてリストや番号付きリスト、表で表してください。表を作る場合は表の前に必ず改行を入れてください。\
- 重要な箇所は太字で強調してください。";
// Define storage and application
const storage = new MemoryStorage();
export const agentApp = new AgentApplication({
storage,
});
agentApp.conversationUpdate("membersAdded", async (context: TurnContext) => {
await context.sendActivity(`Hi there! I'm an agent to chat with you.`);
});
async function getKnowledge(query: string) {
const res = await fetch("http://localhost:8000/search", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: query }),
});
const data = await res.json();
if (Array.isArray(data)) {
if (data.length === 0) return "DBから回答が得られませんでした。";
// 検索結果を整形
const formattedResults = data.map(item => {
if (item.p) {
let result = '';
if (item.h1) result += `タイトル: ${item.h1}\n`;
if (item.h2) result += `セクション: ${item.h2}\n`;
if (item.h3) result += `サブセクション: ${item.h3}\n`;
result += `内容: ${item.p}\n`;
result += `(ID: ${item.pg_id})\n`;
return result;
}
return JSON.stringify(item);
}).join("\n\n");
return formattedResults;
}
}
// Listen for ANY message to be received. MUST BE AFTER ANY OTHER MESSAGE HANDLERS
agentApp.activity(ActivityTypes.Message, async (context: TurnContext) => {
// Echo back users request
const result = await client.chat.completions.create({
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: context.activity.text,
},
],
model: config.azureOpenAIDeploymentName,
tools: [
{
type: "function",
function: {
name: "get_knowledge",
description: "〇〇株式会社のナレッジDBにPOSTリクエストを送って情報を取得する",
parameters: {
type: "object",
properties: {
query: {
type: "string",
description: "質問内容",
},
},
required: ["query"],
},
},
},
],
tool_choice: "auto",
});
let answer = "";
for (const choice of result.choices) {
if (choice.finish_reason === "tool_calls" && choice.message.tool_calls) {
for (const toolCall of choice.message.tool_calls) {
if (toolCall.function && toolCall.function.name === "get_knowledge") {
const args = toolCall.function.arguments;
const parsed = JSON.parse(args || "{}");
const knowledge = await getKnowledge(parsed.query);
const secondResult = await client.chat.completions.create({
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: context.activity.text },
{ role: "tool", name: "get_knowledge", content: knowledge },
],
model: config.azureOpenAIDeploymentName,
});
if (secondResult.choices[0]?.message?.content) {
answer += secondResult.choices[0].message.content;
} else {
answer += "失敗しました。";
}
}
}
} else if (choice.message?.content) {
answer += choice.message.content;
} else {
answer += "応答の形式が正しくありません。";
}
}
await context.sendActivity(answer);
});
結果&まとめ
TeamsのAgentsToolkitを用いてFunctionCallingを実装しました。
今回、エージェントフレームワークなしで実装しましたが、LangChain等では抽象化されてあまり触らない部分を細かくカスタマイズできることや、LLMとのメッセージ内容を知れるのは大きなメリットだと思いました。
しかし、規模が大きくなるに伴いループや分岐でコード量がかなり膨れ上がりそうなので今後は何かしらのフレームワークを使って開発したいです。
Discussion