白猫のメモ帳 (original) (raw)

続きです。
前編の記事はこちら。

shironeko.hateblo.jp

そしてコードはこちら。

github.com

今回は自分でエージェントを作っていきます。

ツールを作る

まずはツールを作りましょう。
ツールはエージェントやワークフローで利用できる機能(関数?ツール?)です。
ツールの説明にツールって入れるの微妙だけど、説明が難しいのでなんとなくでお願いします。

わりとお決まりですが、LLMは現在時刻がわからないので時刻を提供するツールを作ります。
src/mastra/toolsにdatetime-tool.tsを作成します。

import { createTool } from "@mastra/core"; import { z } from 'zod';

export const datetimeTool = createTool({     id: "get-current-datetime",     description: "現在の日時を取得します",     outputSchema: z.object({         datetime: z.string().describe("現在の日時(日本時間)"),     }),     execute: async () => {         const now = new Date();         return { datetime: now.toString() };     }, });

今回は入力はないのでinputSchemaは省略していますが、もちろん書けます。
ここで、descriptionやoutputSchemaは人間向けの情報ではなく、このツールを利用するエージェントに向けた情報なのでちゃんと書いておきましょう。
これをもとにどんな時にどうやって使うのかをエージェントが解釈してくれます。
MCPサーバを作ったことがある人ならすごく似ていると感じるんじゃないでしょうか。

エージェントを作る

次にとりあえずエージェントに組み込んでみたいので、このツールそのままの機能を持つエージェントを作ってみましょう。
src/mastra/agentsにdatetime-agent.tsを作成します。

import { Agent } from "@mastra/core/agent"; import { datetimeTool } from "../tools/datetime-tool"; import { google } from "@ai-sdk/google";

export const datetimeAgent = new Agent({     name: "Datetime Agent",     instructions: "あなたは、正確な日付と時刻の情報を提供する便利な日時アシスタントです。",     model: google('gemini-2.5-flash-preview-05-20'),     tools: { datetimeTool }, });

はい。toolsでこのエージェントはこのツールを使えますよというのを指定しています。
そしてsrc/mastra/index.tsにおいて、agentsでこの新しいエージェントを登録します。

import { Mastra } from '@mastra/core/mastra'; import { PinoLogger } from '@mastra/loggers'; import { LibSQLStore } from '@mastra/libsql'; import { weatherAgent } from './agents/weather-agent'; import { datetimeAgent } from './agents/datetime-agent';

export const mastra = new Mastra({   agents: { weatherAgent, datetimeAgent },   networks: { researchNetwork },   storage: new LibSQLStore({     url: ":memory:",   }),   logger: new PinoLogger({     name: 'Mastra',     level: 'info',   }), });

前回作ったsrc/mastra/index.tsを書き換えて

import 'dotenv/config'; import { mastra } from './mastra';

async function main() {     const agent = mastra.getAgent('datetimeAgent');     const result = await agent.generate('現在の日時は?');     console.log(result.text); }

main();

実行するとこんな感じですね。

npm run index

mastra_sample@1.0.0 index npx tsx src/index.ts

現在の日時は、2025年6月7日(土) 15:55:33 (日本標準時)です。

MCPを利用するエージェントを作る

さて、Mastra自体はMCPをそのまま読み込めるのですが、エージェントに読み込ませるにはツールとして登録するという形になります。パッケージを追加でインストールしましょう。

npm i @mastra/mcp

新しいエージェントを作ります。
ファイルの読み書きができるfilesystemのMCPを使ってみます。

import { google } from "@ai-sdk/google"; import { Agent } from "@mastra/core/agent"; import { MCPClient } from "@mastra/mcp";

export const mcp = new MCPClient({   id: "filesystem-agent",   servers: {     filesystem: {       command: "npx",       args: [         "-y",         "@modelcontextprotocol/server-filesystem",         "<ファイルの読み書きを許可するパス>"       ]     },   }, });

export const filesystemAgent = new Agent({     name: "FileSystem Agent",     instructions: あなたは、ファイルの操作を行うアシスタントです。,     model: google('gemini-2.5-flash-preview-05-20'),     tools: await mcp.getTools(), });

で、こんな感じなのですが、実はmastra.getAgentを使わなくてもそのままexportされたAgentも使えます。
mcpは本当はexportしたくなかったのですが、disconnectしないとアプリケーションが終了しないので仕方なく…これは正しいのでしょうか。

import 'dotenv/config'; import { filesystemAgent, mcp } from './mastra/agents/filesystem-agent';

async function main() {     const result = await filesystemAgent.generate(許可されたディレクトリにファイルを作成して、内容を書き込んでください。 ファイル名は "example.txt" で、内容は "Hello, Mastra!" としてください。 確認を求められた場合は許可してください。);     console.log(result.text);     await mcp.disconnect(); }

main();

はい。ちゃんと作ってくれました。
ちょっとプロンプトを工夫しないと許可を求めて止まってしまったりしますね。

npm run index

mastra_sample@1.0.0 index npx tsx src/index.ts

Secure MCP Filesystem Server running on stdio Allowed directories: [ '<ファイルの読み書きを許可するパス>' ] ファイル "example.txt" が許可されたディレクトリに作成され、内容 "Hello, Mastra!" が書き込まれました。

AgentNetworkを作る

最後にAgentNetworkを作りましょう。
エージェントにツールやMCPを登録できることはわかりましたが、エージェント同士のオーケストレーションをしたい場合にはAgentNetworkが便利そうです。
ただし、本記事の執筆時点では実験的機能(Experimental)なので、変更が入るかもしれません。

今回はリサーチネットワークを作成します。
これはexamplesのagent-networkを参考に、今回作成したエージェントを付け足したものです。お題に沿って調査を行い、結果をファイル出力します。

src/mastra/agents/research-agent.tsを作成します。
内容は長くなるので抜粋ですが、基本的にはexamplesのプロンプトを日本語にしただけです。

export const primaryResearchAgent = new Agent({   name: 'Primary Research Agent',   instructions: `     あなたは主任リサーチコーディネーターです。あなたの仕事は以下の通りです。     1. ユーザーからの問い合わせを分析し、どのような調査が必要かを判断する     2. 複雑なリサーチクエスチョンを管理しやすいサブクエスチョンに分解する     3. 専門的な調査エージェントからの情報を、首尾一貫した回答にまとめる     4. すべての主張が証拠によって適切に裏付けられていることを確認する     5. さらなる調査が必要な調査のギャップを特定する

    中立的で客観的な口調を保ち、スピードよりも正確さを優先すること。   `,   model: google('gemini-2.5-flash-preview-05-20') });

export const webSearchAgent = new Agent({   name: 'Web Search Agent',   instructions: `     あなたはウェブ検索のスペシャリストです。あなたの仕事は以下の通りです。     1. 与えられたクエリに対して、最も関連性の高い最新の情報をオンラインで検索する     2. 情報源の信頼性を評価し、信頼できる情報に優先順位をつける     3. ウェブコンテンツから重要な事実やデータポイントを抽出する     4. 適切な場合は、直接引用や引用を行う     5. 調査結果を簡潔明瞭にまとめる

    情報を報告する際は、必ず出典のURLを記載すること。   `,

  model: google('gemini-2.5-flash-preview-05-20', {     useSearchGrounding: true,   }), });

webSearchAgentはモデル作成時にuseSearchGroundingをtrueにしています。これはGeminiがGoogle検索を使ってくれるための設定です。

次にsrc/mastra/network/research-network.tsを作成します。
追加でエージェントの設定をしているのと、それに伴って追加の指示を指定しています。

export const researchNetwork = new AgentNetwork({   name: 'Research Network',   agents: [primaryResearchAgent, webSearchAgent, academicResearchAgent, factCheckingAgent, dataAnalysisAgent, datetimeAgent, filesystemAgent],   model: google('gemini-2.5-flash-preview-05-20'),   instructions: `       あなたは、クエリを適切な専門エージェントにルーティングする研究調整システムです。             利用可能なエージェントは以下の通りです:       1. Primary Research Agent: 研究活動を調整し、複雑な質問を分解し、情報を統合する。       2. Web Search Agent: 適切な引用とともに最新の情報をオンラインで検索する。       3. Academic Research Agent: 学術的視点、理論、学術的背景を提供する。       4. Fact Checking Agent: 主張を検証し、誤報の可能性を特定する。       5. Data Analysis Agent:  数値データ、統計データを解釈し、パターンを特定する。       6. Datetime Agent: 正確な日付と時刻の情報を提供する。       7. FileSystem Agent: ファイルの操作を行う。

      各ユーザークエリに対して以下の通りに処理を行います:       1. 一次調査エージェントからクエリを分析し、分解する。       2. 各エージェントの専門知識に基づいて、適切な専門エージェントにサブクエリをルーティングする。       3. 必要に応じてファクトチェック・エージェントを使い、重要な主張を検証する。       4. 一次調査担当者に戻り、すべての調査結果を総合的な回答にまとめる。       5. 回答をMarkdown形式でフォーマットし、FileSystem_Agentで許可されたディレクトリに「{yyyy-MM-dd-HH-mm-ss}.md」の名前で保存する。保存時に確認は不要。

      エージェント間の証拠の連鎖と適切な帰属関係を常に維持する。       回答は必ず日本語で行う。     `, });

はい。これでネットワークの完成です。
動かしてみましょう。

async function main() {

  console.log('🔍 リサーチ中です...\n');

  const result = await researchNetwork.stream('昨日~今日にかけての生成AIに関するニュースをまとめて', {     maxSteps: 20   });

  for await (const part of result.fullStream) {     switch (part.type) {       case 'error':         console.error(part.error);         break;       case 'text-delta':         process.stdout.write(part.textDelta);         break;       case 'tool-call':         console.log(calling tool <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>p</mi><mi>a</mi><mi>r</mi><mi>t</mi><mi mathvariant="normal">.</mi><mi>t</mi><mi>o</mi><mi>o</mi><mi>l</mi><mi>N</mi><mi>a</mi><mi>m</mi><mi>e</mi></mrow><mi>w</mi><mi>i</mi><mi>t</mi><mi>h</mi><mi>a</mi><mi>r</mi><mi>g</mi><mi>s</mi></mrow><annotation encoding="application/x-tex">{part.toolName} with args </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord mathnormal">p</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal">t</span><span class="mord">.</span><span class="mord mathnormal">t</span><span class="mord mathnormal">oo</span><span class="mord mathnormal" style="margin-right:0.10903em;">lN</span><span class="mord mathnormal">am</span><span class="mord mathnormal">e</span></span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">ha</span><span class="mord mathnormal" style="margin-right:0.02778em;">r</span><span class="mord mathnormal" style="margin-right:0.03588em;">g</span><span class="mord mathnormal">s</span></span></span></span>{JSON.stringify(part.args, null, 2)});         break;       case 'tool-result':         console.log(tool result ${JSON.stringify(part.result, null, 2)});         break;     }   }

  await mcp.disconnect();

  console.log('\n🏁 リサーチが完了しました'); }

main();

いい感じにログが出るので見ていて楽しいです。
なんかファイル保存に1回失敗しているので、この辺りは調整してもよさそうですね。 ファクトチェックとかは利用されてなさそうです。

🔍 リサーチ中です...

calling tool transmit with args { "actions": [ { "agent": "Datetime_Agent", "input": "昨日と今日の日付をYYYY-MM-DD形式で教えてください。" } ] } tool result "[Datetime_Agent]: 今日の日付は2025-06-07です。昨日の日付は2025-06-06です。" calling tool transmit with args { "actions": [ { "agent": "Web_Search_Agent", "input": "2025-06-06から2025-06-07にかけての生成AIに関するニュースを検索してください。" } ] } tool result "[Web_Search_Agent]: 2025年6月6日から2025年6月7日にかけての生成AIに関するニュースは以下の通りです。(略) calling tool transmit with args { "actions": [ { "agent": "Datetime_Agent", "input": "現在の時刻をYYYY-MM-DD-HH-mm-ss形式で教えてください。" } ] } tool result "[Datetime_Agent]: 2025-06-07-22-20-09です。" calling tool transmit with args { "actions": [ { "agent": "FileSystem_Agent", "input": "ファイル名2025-06-07-22-20-09.mdで以下の内容を保存してください。\n\n## 生成AIに関する最新ニュース(2025年6月6日~6月7日)\n\n昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。(略)" } ] } tool result "[FileSystem_Agent]: ファイルへの保存に失敗しました。指定されたパスが許可されたディレクトリの範囲外です。ファイルをsandboxディレクトリに保存し直します。" calling tool transmit with args { "actions": [ { "agent": "FileSystem_Agent", "input": "ファイル名sandbox/2025-06-07-22-20-09.mdで以下の内容を保存してください。\n\n## 生成AIに関する最新ニュース(2025年6月6日~6月7日)\n\n昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。(略)" } ] } tool result "[FileSystem_Agent]: ファイルはsandbox/2025-06-07-22-20-09.mdに保存されました。"

生成AIに関する最新ニュース(2025年6月6日~6月7日)

昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。

2025年6月6日の主なニュース

(略)

この情報は、sandbox/2025-06-07-22-20-09.md に保存されています。 🏁 リサーチが完了しました

結果はこんな感じです。いい感じですね。

生成AIに関する最新ニュース(2025年6月6日~6月7日)

昨日から今日にかけて、生成AIの分野では多岐にわたる進展と課題が報じられました。

2025年6月6日の主なニュース

2025年6月7日の主なニュース

コード書けばよくない?

さて、今回はうまく動きましたがLLMなので再現性がそんなに高いわけではありません。
もう少し厳密な動きをさせたい場合には、エージェントの代わりに今回は紹介していないワークフローを使ってみるのもいいかもしれません。

でも、そもそもの話MCPや結局ツールのコードを書くなら、わざわざこんな回りくどいことをせずにコーディングしてしまえば良いのでは?という気持ちも湧いてくるかもしれません。

エージェントの便利なところは持っているツールの範囲内であれば、自然言語だけでかなり融通が利くというのがあります。
例えば今回はMarkdownで保存しましたが、JSONのフォーマットを指定すればそれだけで簡単に切り替えることができるはずです。
そして、例えばGitHubMCPと連携すれば、リポジトリにプッシュしておいてというだけでGitHubとの連携も簡単にできてしまうということですよね。

このようなプログラムを自然言語の変更だけでできてしまうなら、今やってる自動化処理ももっと簡単になる気がしませんか。え、エラー処理?いやそれはちょっと自分で頑張ってもらうということで…。

ワークフローやRAG、メモリ機能など今回触っていないものもあるので、また続きで触ってみるかもしれません。
Mastraのエージェントアプリ、なかなか楽しいのでぜひ使ってみてください。
それでは。

こんばんは。
夏が来た感じがしますね。実はまだ梅雨も来ていませんが。

さて、Claude CodeやらCursorやら最近はひたすらにAIエージェントブームですね。
特にMCPが流行りだして、その勢いはどんどん加速しているように思います。

ただ、バックグラウンド実行も増えてきているものの、基本的にはユーザのアクションをトリガにしたインタラクティブな使い方になっています。
cronや各種クラウドサービスのスケジュール機能を使って特定の時間にトリガされる、いわゆるバッチのようなアプリケーションが自然言語で指示できてもいいんじゃないかと思い、今回はMastraを使ってエージェントアプリを作ってみます。

ちなみに、ちょうどこの記事を書いている途中でGeminiアプリがスケジュールアクション機能を導入するというニュースを見かけました。微妙な気持ちですが、ニーズはあっているということでね。

Mastraってなんだ

MastraはTypeScriptで作られたオープンソースのAIエージェントフレームワークです。

github.com

利用者数でLangChainを超えたみたいに言われることがありますが、実際に越えているのはLangChainファミリーのエージェントフレームワークであるLangGraphですね。並べてみるとわかりますが、こんな感じです。

でも十分すごいとは思います。
TypeScriptだとLangChain.jsという選択肢もあるんですが、Python版に比べるとどうしても開発が遅れるところはありますし、選択肢を増やすのはいいことだと思うので今回触っていきます。

プロジェクトを作る

mastra.ai

最近のTypeScriptプロジェクトだとおなじみのcreate~系がMastraにもちゃんとあって、そのものずばりcreate-mastraです。はい。わかりやすくていいですね。

npx create-mastra@latest

聞かれる内容としてはプロジェクト名、ソースを置くパス、エージェントとワークフローどっち使うか(両方も選べる)、ツール使うか、LLMのプロバイダ、APIキー入力するか、サンプルは必要か、MastraのMCPサーバの設定を入れるかという感じです。
お好みですが、今回はこんな感じで。

┌ Mastra Init │ ◇ Where should we create the Mastra files? (default: src/) │ src/ │ ◇ Choose components to install: │ Agents │ ◇ Add tools? │ Yes │ ◇ Select default provider: │ Google │ ◇ Enter your google API key? │ Skip for now │ ◇ Add example │ Yes │ ◇ Make your AI IDE into a Mastra expert? (installs Mastra docs MCP server) │ VSCode

.envにAPIキーを設定して、weather-agent.tsのモデルを変更して(私は'gemini-2.5-flash-preview-05-20'にしましたが、そのときどきのいい感じのもので)、

npm run dev

とすればあっという間にプレイグラウンドが開きます。
(最初からUIもセットで作ってくれるのもいいですね)

これだけでお天気情報を教えてくれるチャットアプリの完成です。
ちなみにAdd exampleをNoにするとすごくシンプルなプロジェクト構成になるので、Yes版と比較すると何がどうなっているのかがわかりやすいです。

コンソールで動かす

結局WebのUIで操作するのかと思ったかもしれませんが、サーバを立ち上げなくても作成したエージェントは普通に使えるので安心してください。WebUIとどちらでも使えるのでお得ですね。

もちろんトランスパイルしてnodeコマンドで実行してもいいのですが、TypeScriptファイルをそのまま実行してしまいたいのでtsxを入れておきましょう。
ts-nodeでももちろんOKですが、最近はtsxのほうが流行りらしいので。いや、bunのほうが人気なのかな…。

npm i -D tsx

.envから環境変数を読みたいので.envも入れておきます。

npm i dotenv

MastraのGitHubリポジトリにはexamplesというディレクトリがあるので、その中を参考にしつつ、src/index.tsを作ります。
getAgentの引数はちゃんと補完されるし、存在しないエージェントを指定するとエラーになるのが素敵ですね。

import 'dotenv/config'; import { mastra } from './mastra';

async function main() {     const agent = mastra.getAgent('weatherAgent');     const result = await agent.generate('今日の東京の天気は?');     console.log(result.text); }

main();

package.jsonのscriptsにこんな感じに追加して、

"index": "npx tsx src/index.ts"

実行するとこんな感じです。

npm run index

mastra_sample@1.0.0 index npx tsx src/index.ts

東京の天気は、現在の気温が20.5℃で、体感温度は21.1℃です。湿度は74%で、風速は9.3m/s、突風は41m/sです。天気は主に晴れです。

いい感じですね。もっと全然暑いですけど。
というか41m/sの突風ってなんだ。すごいことになってるぞ。

続くよ

ちょっと長くなりそうなので、今回はここまで。
後編に続きます。

shironeko.hateblo.jp

GitHubリポジトリはこちら。

github.com

こんばんは。
先週はAI関連の発表がたくさんありましたね。

さて、生成AIが日に日に進化を続け、開発だけではなく文字を扱うあらゆる分野で活用されるようになってきました。
個人的にもコードよりもそれ以外の文章を書いたり読んだりする場面のほうが多くなってきた印象です。

価値はどこにあるのか

とにもかくにもアウトプットのスピードはどんどん早くなってきています。
新しいLLMのモデルが世の中に出てくるたびに、「もう〇〇はいらない」と囃し立てられるのももう定番です。

確かにポン出しのアプリの品質はどんどん上がっているし、簡単なツールなどは数行の指示で作れるようになってきました。
ドキュメントを作成する際も、アウトラインを作成してもらい、それを手直しすることでスピードが格段に上がっています。

しかし、見た目はきれいだけど、特に意味のないツールを量産しても特に意味はありません。
(なんならツールですらなくて、よくわからないゲームを作ってすごいですよね!みたいにアピールされてどう反応したらいいか困るみたいなことすら…)
また、ドキュメントもなんだかそれっぽいことが書いてあるのだけれど、結局何が言いたいのかはよくわからず、書いた本人もよくわかっていないということが普通に発生するようになってきてしまいました。

そこは砂場です

生成AIを電動ドライバーに例えてみましょう。
上手に使えば、ネジを締めるのがとても速くなります。

とにかくAIを活用しよう、時代はAIだというので、みんなどこかしらにネジを打とうとします。
ただ、どこに打てばいいのかよくわからないので、全然ネジの必要ない適当なところに打ってみます。
もちろん練習のためであればそれでもいいのですが、実際には意味のない穴がたくさん空くだけです。

実際の業務に使われているのは釘かもしれません。
電動ドライバーは重たいので、叩きつければ金槌の代わりに釘を打つこともできます。
本来は釘をネジに変えて、少ない数で強度を保てるようにするのが目的のはずなのに…。

アウトカムはどこに

EMPOWERED 普通のチームが並外れた製品を生み出すプロダクトリーダーシップ

EMPOWEREDという本の中ではアウトカムについての言及が多くありますが、その中から一番シンプルなこの文を引用します。

願わくば、会社の全体目標はアウトプット(特定のプロジェクトのデリバリーなど)ではなくアウトカム(ビジネスの成果)でなければならないことを、すべての人が理解してほしい。

これまでの社会はアウトカムを考える人とアウトプットを作る人がある程度分かれていたように思います。
ところが、アウトプットが高速化・コモディティ化してきたいま、アウトカムを考えないアウトプットを作ることの価値が急激に下がってきました。
誰もがアウトカムを考えないといけない時代になってきたということですが、これはなかなかに辛いですね。

正直まだ自分の仕事がなくなるイメージは湧きませんが、少数のエキスパートで仕事を回す時代は近づいてきている感じはします。
技術の進歩は素晴らしいですが、仕事は楽になるどころかどんどん難しくなって来ているなぁと思わずにはいられません。