アーキテクチャ概念解説
エージェントの基本構造
市場調査エージェントは「ツール呼び出し型エージェント(Tool-Use Agent)」として設計します。Claude はユーザーからの指示を受け取り、必要に応じて Web 検索・スクレイピング・データ集計などのツールを自律的に呼び出しながら、最終的なレポートを生成します。
[スケジューラ]
↓ トリガー(定期実行)
[市場調査エージェント]
↓
┌─ web_search() … 最新ニュース・プレスリリース取得
├─ fetch_page() … 競合サイト・料金ページ取得
├─ analyze_trends() … SNS トレンド分析
└─ compare_data() … 前回データとの差分検出
↓
[Claude による分析・要約]
↓
[配信パイプライン(Slack / Notion)]
ステートフル vs ステートレス
市場調査エージェントでは「前回の状態との比較」が重要です。価格が変わったかどうかを判定するには、前回の価格データを保存しておく必要があります。
Step 1 — 競合分析エージェントの実装
ツール定義
Claude に渡すツールを定義します。Web 検索には Claude の組み込み Web 検索機能を使用します。
// src/tools/competitorTools.js
export const competitorTools = [
{
name: "web_search" ,
description: "Web を検索して最新情報を取得します。競合他社のプレスリリース、ニュース、製品アップデートの調査に使用します。" ,
input_schema: {
type: "object" ,
properties: {
query: {
type: "string" ,
description: "検索クエリ。具体的な企業名・製品名・期間を含めると精度が上がります。"
},
max_results: {
type: "number" ,
description: "取得する検索結果の最大件数(デフォルト: 5)"
}
},
required: [ "query" ]
}
},
{
name: "save_competitor_data" ,
description: "競合他社の分析データをローカルに保存します。次回実行時との差分比較に使用します。" ,
input_schema: {
type: "object" ,
properties: {
competitor_name: { type: "string" },
data: {
type: "object" ,
description: "保存する分析データ(価格・機能・ニュース等)"
},
timestamp: { type: "string" }
},
required: [ "competitor_name" , "data" , "timestamp" ]
}
},
{
name: "load_previous_data" ,
description: "前回の競合分析データを読み込みます。" ,
input_schema: {
type: "object" ,
properties: {
competitor_name: { type: "string" }
},
required: [ "competitor_name" ]
}
}
];
エージェントのコアロジック
// src/agents/competitorAgent.js
import Anthropic from "@anthropic-ai/sdk" ;
import fs from "fs/promises" ;
import path from "path" ;
const client = new Anthropic ();
const DATA_DIR = "./data/competitors" ;
// ツール実行関数
async function executeTool ( toolName , toolInput ) {
switch (toolName) {
case "save_competitor_data" : {
await fs. mkdir ( DATA_DIR , { recursive: true });
const filePath = path. join ( DATA_DIR , `${ toolInput . competitor_name }.json` );
await fs. writeFile (filePath, JSON . stringify (toolInput, null , 2 ));
return { success: true , message: `${ toolInput . competitor_name } のデータを保存しました` };
}
case "load_previous_data" : {
const filePath = path. join ( DATA_DIR , `${ toolInput . competitor_name }.json` );
try {
const raw = await fs. readFile (filePath, "utf8" );
return JSON . parse (raw);
} catch {
return { exists: false , message: "前回データなし(初回実行)" };
}
}
// web_search は Claude の組み込み機能を利用するため、
// ここでは外部APIを呼び出す想定のスタブを実装
default :
return { error: `未知のツール: ${ toolName }` };
}
}
export async function runCompetitorAnalysis ( competitors ) {
const systemPrompt = `あなたは市場調査の専門家です。与えられた競合他社リストについて、
以下の観点で徹底的に調査・分析を行ってください。
調査項目:
1. 最新のプレスリリース・ニュース(過去7日間)
2. 主要製品・サービスの機能変更
3. 価格変更の有無
4. SNS・コミュニティでの反応・評判
5. 前回データとの差分(変化点の特定)
分析にあたっては:
- 事実と推測を明確に区別する
- 重要度(高・中・低)でランク付けする
- ビジネス上の示唆を1〜2文で述べる
- 日本語で出力する` ;
const userMessage = `以下の競合他社を分析してください: ${ competitors . join ( ", " ) }
各社について web_search を使って最新情報を収集し、load_previous_data で前回データと比較した上で、
差分がある場合は save_competitor_data で最新データを保存してください。
最後に全社の分析サマリーを構造化して出力してください。` ;
let messages = [{ role: "user" , content: userMessage }];
// エージェントループ(ツール呼び出しが完了するまで繰り返す)
while ( true ) {
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 8192 ,
system: systemPrompt,
tools: competitorTools,
messages
});
// Claude の応答をメッセージ履歴に追加
messages. push ({ role: "assistant" , content: response.content });
if (response.stop_reason === "end_turn" ) {
// 最終テキスト出力を抽出して返す
const textBlock = response.content. find ( b => b.type === "text" );
return textBlock?.text ?? "分析完了(テキスト出力なし)" ;
}
if (response.stop_reason === "tool_use" ) {
const toolResults = [];
for ( const block of response.content) {
if (block.type !== "tool_use" ) continue ;
console. log ( `[ツール呼び出し] ${ block . name }:` , block.input);
const result = await executeTool (block.name, block.input);
console. log ( `[ツール結果]:` , result);
toolResults. push ({
type: "tool_result" ,
tool_use_id: block.id,
content: JSON . stringify (result)
});
}
messages. push ({ role: "user" , content: toolResults });
}
}
}
Step 2 — 価格監視エージェントの実装
価格監視は変化の検出が核心です。前回の価格と現在の価格を比較し、変化があった場合にのみアラートを送信します。
// src/agents/priceMonitorAgent.js
import Anthropic from "@anthropic-ai/sdk" ;
import fs from "fs/promises" ;
import path from "path" ;
const client = new Anthropic ();
const PRICE_DATA_DIR = "./data/prices" ;
const priceTools = [
{
name: "extract_pricing_from_page" ,
description: "URL から料金情報を抽出し、構造化データとして返します。" ,
input_schema: {
type: "object" ,
properties: {
url: { type: "string" , description: "料金ページの URL" },
product_name: { type: "string" , description: "製品名または競合社名" }
},
required: [ "url" , "product_name" ]
}
},
{
name: "compare_prices" ,
description: "現在の価格データと前回保存したデータを比較し、変更点を検出します。" ,
input_schema: {
type: "object" ,
properties: {
product_name: { type: "string" },
current_prices: { type: "object" }
},
required: [ "product_name" , "current_prices" ]
}
}
];
async function executePriceTool ( toolName , toolInput ) {
if (toolName === "compare_prices" ) {
const filePath = path. join ( PRICE_DATA_DIR , `${ toolInput . product_name }.json` );
let previous = null ;
try {
previous = JSON . parse ( await fs. readFile (filePath, "utf8" ));
} catch { /* 初回実行 */ }
// 現在データを保存
await fs. mkdir ( PRICE_DATA_DIR , { recursive: true });
const saveData = {
product_name: toolInput.product_name,
prices: toolInput.current_prices,
updated_at: new Date (). toISOString ()
};
await fs. writeFile (filePath, JSON . stringify (saveData, null , 2 ));
if ( ! previous) {
return { change_detected: false , message: "初回記録完了(比較データなし)" };
}
// 価格差分の検出
const changes = [];
for ( const [ plan , price ] of Object. entries (toolInput.current_prices)) {
const prevPrice = previous.prices?.[plan];
if (prevPrice !== undefined && prevPrice !== price) {
const diff = typeof price === "number" ? price - prevPrice : "変更あり" ;
const direction = typeof diff === "number" ? (diff > 0 ? "値上げ" : "値下げ" ) : "" ;
changes. push ({ plan, prev: prevPrice, current: price, diff, direction });
}
}
return {
change_detected: changes. length > 0 ,
changes,
previous_updated: previous.updated_at
};
}
return { error: `未知のツール: ${ toolName }` };
}
export async function runPriceMonitoring ( targets ) {
const systemPrompt = `あなたは価格監視エージェントです。与えられた SaaS・EC サイトの料金情報を
分析し、前回データとの差分を正確に報告してください。
出力形式:
- 変更があった場合: 製品名・プラン名・変更前価格・変更後価格・変更率・ビジネス的示唆
- 変更がなかった場合: 「変更なし」とのみ記載
- 通貨は原文のまま(JPY/USD 等)記載する` ;
const userMessage = `以下の製品・サービスの料金ページを調査してください: \n ${
targets . map ( t => `- ${ t . name }: ${ t . url }` ). join ( " \n " )
} \n\n web_search で料金情報を取得し、compare_prices で前回データと比較してください。` ;
let messages = [{ role: "user" , content: userMessage }];
while ( true ) {
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 4096 ,
system: systemPrompt,
tools: priceTools,
messages
});
messages. push ({ role: "assistant" , content: response.content });
if (response.stop_reason === "end_turn" ) {
const textBlock = response.content. find ( b => b.type === "text" );
return textBlock?.text ?? "価格監視完了" ;
}
if (response.stop_reason === "tool_use" ) {
const toolResults = [];
for ( const block of response.content) {
if (block.type !== "tool_use" ) continue ;
const result = await executePriceTool (block.name, block.input);
toolResults. push ({
type: "tool_result" ,
tool_use_id: block.id,
content: JSON . stringify (result)
});
}
messages. push ({ role: "user" , content: toolResults });
}
}
}
Step 3 — トレンド検出エージェントの実装
ニュース・SNS からインサイトを抽出するエージェントです。Claude の長いコンテキストウィンドウ(200K トークン)を活用して、大量のテキストを一度に処理します。
// src/agents/trendAgent.js
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
export async function runTrendDetection ( keywords , timeRange = "past_7_days" ) {
const systemPrompt = `あなたはトレンド分析の専門家です。与えられたキーワードについて、
Web 検索を駆使して以下を分析してください。
分析項目:
1. 急上昇しているキーワード・トピック(なぜ今注目されているか)
2. 主要ニュースの要約(信頼性の高いソースを優先)
3. X(旧 Twitter)・Reddit・Hacker News 上のコミュニティ反応
4. 競合他社の動向との関連性
5. 今後1〜2週間の予測(慎重かつ根拠を明示)
出力形式:
## 🔥 急上昇トレンド
## 📰 主要ニュース(上位3件)
## 💬 コミュニティ反応
## 🔮 今後の予測
## 📊 アクションアイテム(ビジネスへの示唆)` ;
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 6144 ,
system: systemPrompt,
messages: [
{
role: "user" ,
content: `以下のキーワードについて、${ timeRange } の期間で徹底的にトレンド分析を実施してください。
キーワード: ${ keywords . join ( ", " ) }
web_search を複数回使って、多角的な情報を収集してから分析・統合してください。`
}
],
tools: [
{
name: "web_search" ,
description: "Web 検索でトレンド情報を取得します。" ,
input_schema: {
type: "object" ,
properties: {
query: { type: "string" },
time_filter: {
type: "string" ,
enum: [ "past_day" , "past_week" , "past_month" ],
description: "検索期間フィルタ"
}
},
required: [ "query" ]
}
}
]
});
const textBlock = response.content. find ( b => b.type === "text" );
return textBlock?.text ?? "トレンド分析完了" ;
}
Step 4 — 統合レポートパイプラインの構築
3つのエージェントの出力を統合し、Slack と Notion へ自動配信するパイプラインです。
// src/pipeline/reportPipeline.js
import Anthropic from "@anthropic-ai/sdk" ;
import { runCompetitorAnalysis } from "../agents/competitorAgent.js" ;
import { runPriceMonitoring } from "../agents/priceMonitorAgent.js" ;
import { runTrendDetection } from "../agents/trendAgent.js" ;
const client = new Anthropic ();
// ---- 設定 ----
const CONFIG = {
competitors: [ "OpenAI" , "Google Gemini" , "Mistral AI" ],
priceTargets: [
{ name: "OpenAI ChatGPT Plus" , url: "https://openai.com/pricing" },
{ name: "Google Gemini Advanced" , url: "https://one.google.com/about/ai-premium" }
],
trendKeywords: [ "Claude AI" , "LLM 2026" , "AI エージェント" , "生成AI 料金" ]
};
// ---- Slack 送信 ----
async function sendToSlack ( report ) {
if ( ! process.env. SLACK_BOT_TOKEN ) return ;
const response = await fetch ( "https://slack.com/api/chat.postMessage" , {
method: "POST" ,
headers: {
"Content-Type" : "application/json" ,
"Authorization" : `Bearer ${ process . env . SLACK_BOT_TOKEN }`
},
body: JSON . stringify ({
channel: process.env. SLACK_CHANNEL_ID ,
text: "📊 市場調査レポート — " + new Date (). toLocaleDateString ( "ja-JP" ),
blocks: [
{
type: "section" ,
text: { type: "mrkdwn" , text: `*📊 市場調査レポート — ${ new Date (). toLocaleDateString ( "ja-JP" ) }*` }
},
{ type: "divider" },
{
type: "section" ,
text: { type: "mrkdwn" , text: report. slice ( 0 , 2900 ) } // Slack の文字数制限
}
]
})
});
const data = await response. json ();
return data.ok;
}
// ---- Notion 保存 ----
async function saveToNotion ( title , content ) {
if ( ! process.env. NOTION_TOKEN ) return ;
await fetch ( "https://api.notion.com/v1/pages" , {
method: "POST" ,
headers: {
"Authorization" : `Bearer ${ process . env . NOTION_TOKEN }` ,
"Notion-Version" : "2022-06-28" ,
"Content-Type" : "application/json"
},
body: JSON . stringify ({
parent: { database_id: process.env. NOTION_DATABASE_ID },
properties: {
title: { title: [{ text: { content: title } }] },
Date: { date: { start: new Date (). toISOString (). split ( "T" )[ 0 ] } }
},
children: [
{
object: "block" ,
type: "paragraph" ,
paragraph: {
rich_text: [{ type: "text" , text: { content: content. slice ( 0 , 2000 ) } }]
}
}
]
})
});
}
// ---- 統合レポート生成 ----
async function generateIntegratedReport ( competitorReport , priceReport , trendReport ) {
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 4096 ,
messages: [
{
role: "user" ,
content: `以下の3つのレポートを統合して、エグゼクティブサマリーを作成してください。
## 競合分析レポート
${ competitorReport }
## 価格監視レポート
${ priceReport }
## トレンドレポート
${ trendReport }
統合レポートには以下を含めてください:
1. **今週の最重要インサイト**(3箇条)
2. **緊急対応が必要な変化**(あれば)
3. **来週のアクションアイテム**
4. **中長期的な示唆**
読者はビジネスオーナーを想定し、簡潔かつ判断に直結する情報を優先してください。`
}
]
});
return response.content. find ( b => b.type === "text" )?.text ?? "" ;
}
// ---- メインパイプライン ----
export async function runMarketResearchPipeline () {
console. log ( "🚀 市場調査パイプライン開始:" , new Date (). toLocaleString ( "ja-JP" ));
try {
// 3エージェントを並列実行してパフォーマンスを最大化
const [ competitorReport , priceReport , trendReport ] = await Promise . all ([
runCompetitorAnalysis ( CONFIG .competitors),
runPriceMonitoring ( CONFIG .priceTargets),
runTrendDetection ( CONFIG .trendKeywords)
]);
console. log ( "✅ 3エージェント完了 — 統合レポート生成中..." );
const integratedReport = await generateIntegratedReport (
competitorReport, priceReport, trendReport
);
const title = `市場調査レポート — ${ new Date (). toLocaleDateString ( "ja-JP" ) }` ;
// 非同期で Slack・Notion に配信
await Promise . allSettled ([
sendToSlack (integratedReport),
saveToNotion (title, integratedReport)
]);
console. log ( "✅ レポート配信完了" );
return integratedReport;
} catch (error) {
console. error ( "❌ パイプラインエラー:" , error);
throw error;
}
}
スケジュール実行の設定
// src/scheduler.js
import cron from "node-cron" ;
import { runMarketResearchPipeline } from "./pipeline/reportPipeline.js" ;
// 毎週月曜日 8:00(JST)に実行
cron. schedule ( "0 8 * * 1" , runMarketResearchPipeline, {
timezone: "Asia/Tokyo"
});
// 価格監視は毎日 9:00 に実行(より頻度を高めたい場合)
// cron.schedule("0 9 * * *", runPriceOnlyPipeline, { timezone: "Asia/Tokyo" });
console. log ( "⏰ 市場調査スケジューラ起動完了" );
// 即時テスト実行
// runMarketResearchPipeline();
実行方法:
# 通常起動
node src/scheduler.js
# 即時テスト実行
node -e "import('./src/pipeline/reportPipeline.js').then(m => m.runMarketResearchPipeline())"
よくあるエラーと対処法
エラー 1: overloaded_error — API レート制限
複数エージェントを並列実行すると、Claude API のレート制限に引っかかることがあります。
// リトライ付きAPIコールのラッパー
async function callWithRetry ( fn , maxRetries = 3 , delayMs = 2000 ) {
for ( let i = 0 ; i < maxRetries; i ++ ) {
try {
return await fn ();
} catch (err) {
if (err.status === 529 && i < maxRetries - 1 ) {
console. warn ( `レート制限: ${ delayMs }ms 待機後リトライ (${ i + 1 }/${ maxRetries })` );
await new Promise ( r => setTimeout (r, delayMs * (i + 1 )));
} else {
throw err;
}
}
}
}
// 使用例
const report = await callWithRetry (() => runCompetitorAnalysis (competitors));
エラー 2: エージェントループが終了しない
ツール呼び出しが無限ループに陥るケースを防ぐため、最大ステップ数を設定します。
const MAX_STEPS = 20 ;
let steps = 0 ;
while (steps < MAX_STEPS ) {
steps ++ ;
const response = await client.messages. create ({ ... });
// ... ループ処理 ...
if (response.stop_reason === "end_turn" ) break ;
}
if (steps >= MAX_STEPS ) {
console. warn ( "⚠️ 最大ステップ数到達 — 処理を強制終了しました" );
}
エラー 3: ツール結果が JSON 解析できない
Claude へのツール結果は必ず文字列にシリアライズします。
// ❌ 誤り
content : result // オブジェクトをそのまま渡すとエラー
// ✅ 正しい
content : JSON . stringify (result) // 必ず文字列化
エラー 4: Slack 送信失敗(文字数超過)
Slack のブロック API は 1 ブロックあたり 3000 文字制限があります。長いレポートは分割します。
function chunkText ( text , maxLength = 2800 ) {
const chunks = [];
for ( let i = 0 ; i < text. length ; i += maxLength) {
chunks. push (text. slice (i, i + maxLength));
}
return chunks;
}
応用・発展的な活用法
マルチモーダル対応: スクリーンショット分析
Claude のビジョン機能を活用すると、料金ページのスクリーンショットから視覚的に価格情報を抽出できます。Playwright でキャプチャした PNG を Base64 エンコードして渡します。
import { chromium } from "playwright" ;
import fs from "fs" ;
async function screenshotAndAnalyze ( url ) {
const browser = await chromium. launch ();
const page = await browser. newPage ();
await page. goto (url, { waitUntil: "networkidle" });
const screenshotBuffer = await page. screenshot ({ fullPage: true });
await browser. close ();
const base64Image = screenshotBuffer. toString ( "base64" );
const response = await client.messages. create ({
model: "claude-sonnet-4-6" ,
max_tokens: 2048 ,
messages: [
{
role: "user" ,
content: [
{
type: "image" ,
source: { type: "base64" , media_type: "image/png" , data: base64Image }
},
{
type: "text" ,
text: "このスクリーンショットから価格プラン情報を全て抽出し、JSON 形式で返してください。"
}
]
}
]
});
return response.content. find ( b => b.type === "text" )?.text;
}
Cloudflare Workers へのデプロイ
エージェントをエッジで動かすと、世界中どこからでも低レイテンシで実行できます。価格データの保存には Cloudflare KV を使用します。
// workers/market-research.js
export default {
async scheduled ( event , env , ctx ) {
const { runMarketResearchPipeline } = await import ( "./pipeline/reportPipeline.js" );
ctx. waitUntil ( runMarketResearchPipeline (env));
} ,
async fetch ( request , env ) {
// 手動トリガー用エンドポイント
if (request.method === "POST" && new URL (request.url).pathname === "/run" ) {
const { runMarketResearchPipeline } = await import ( "./pipeline/reportPipeline.js" );
const report = await runMarketResearchPipeline (env);
return new Response (report, { headers: { "Content-Type" : "text/plain" } });
}
return new Response ( "Market Research Agent" , { status: 200 });
}
} ;
wrangler.toml で Cron トリガーを設定します(Cloudflare Workers Free プランでも利用可能)。
name = "market-research-agent"
main = "workers/market-research.js"
compatibility_date = "2026-04-01"
[[ triggers . crons ]]
crons = [ "0 0 * * 1" ] # 毎週月曜 UTC 0:00(JST 9:00)
出力のカスタマイズ: 業界特化プロンプト
System Prompt を業界に合わせてカスタマイズすることで、精度が飛躍的に向上します。
const INDUSTRY_PROMPTS = {
saas: "SaaS 業界の価格モデル(月額/年額・席数課金・使用量課金)の変化に特に注目してください。" ,
ecommerce: "EC 業界の送料・ポイント還元率・セール戦略の変化を重点的に分析してください。" ,
ai: "AI モデルの性能ベンチマーク・APIレート・コンテキスト長の変化を最優先で追跡してください。"
};
const systemPrompt = BASE_SYSTEM_PROMPT + " \n\n " + ( INDUSTRY_PROMPTS [industry] ?? "" );
公式ドキュメントには書かれていない、運用してみて気づいた6つの落とし穴
ここからは、Cloudflare Workers と Claude API でこの市場調査エージェントを 2026 年 1 月から実運用してきて、Anthropic の公式ドキュメントには載っていない泥臭い気づきを 6 つに整理します。同じ構成で組まれる方の遠回りを少しでも減らせれば嬉しいです。
1. Web Search Tool の結果は同じクエリでも日次で揺らぐ
同一クエリで Claude の Web Search Tool を翌日呼ぶと、上位 10 件のうち 3 〜 4 件が入れ替わることがあります。素朴に「URL 集合の差分 = 業界の変化」と判定するとノイズが 8 割を占めて、本当に検知したい「競合の新機能リリース」が埋もれます。
対策は、前回と今回の上位 5 件の URL 集合を SimHash で比較し、Jaccard 係数 0.7 以上なら「実質変化なし」として LLM 判定をスキップする 2 段構えです。
import { createHash } from "node:crypto" ;
function urlSetSimHash ( urls ) {
const tokens = urls. flatMap (( u ) => new URL (u).hostname. split ( "." ));
const bits = new Array ( 64 ). fill ( 0 );
for ( const token of tokens) {
const hash = createHash ( "md5" ). update (token). digest ();
for ( let i = 0 ; i < 64 ; i ++ ) {
const bit = (hash[Math. floor (i / 8 )] >> (i % 8 )) & 1 ;
bits[i] += bit ? 1 : - 1 ;
}
}
return BigInt ( "0b" + bits. map (( b ) => (b > 0 ? "1" : "0" )). join ( "" ));
}
function jaccardSimilarity ( setA , setB ) {
const a = new Set (setA);
const b = new Set (setB);
const intersection = [ ... a]. filter (( x ) => b. has (x)). length ;
const union = new Set ([ ... a, ... b]).size;
return intersection / union;
}
export function shouldRunLlmDiff ( prevUrls , currUrls ) {
const sim = jaccardSimilarity (prevUrls, currUrls);
return sim < 0.7 ; // 0.7 未満なら「変化あり」として LLM 判定へ
}
この 2 段構えで、月間の LLM 呼び出し回数を実測 78% 削減できました。
2. Cloudflare Workers の CPU time 30 秒制約に並列実行は収まらない
3 エージェント(競合分析 / 価格監視 / トレンド検出)を Promise.all で並列実行すると、Claude API の応答待ちで Workers の CPU time 30 秒上限を簡単に超えます。本記事の Step 4 では 1 リクエストで完結する形で書きましたが、実運用では Cloudflare Queues に詰めてステップ分割するのが正解でした。
[[ queues . producers ]]
binding = "MARKET_RESEARCH_QUEUE"
queue = "market-research-jobs"
[[ queues . consumers ]]
queue = "market-research-jobs"
max_batch_size = 1
max_batch_timeout = 10
export default {
async scheduled ( event , env ) {
await env. MARKET_RESEARCH_QUEUE . send ({ type: "competitor" , at: Date. now () });
await env. MARKET_RESEARCH_QUEUE . send ({ type: "pricing" , at: Date. now () });
await env. MARKET_RESEARCH_QUEUE . send ({ type: "trends" , at: Date. now () });
} ,
async queue ( batch , env ) {
for ( const msg of batch.messages) {
const job = msg.body;
if (job.type === "competitor" ) await runCompetitorAgent (env);
else if (job.type === "pricing" ) await runPricingAgent (env);
else if (job.type === "trends" ) await runTrendsAgent (env);
msg. ack ();
}
}
} ;
Queue 経由にすると 1 リクエストあたりの CPU time は 5 〜 8 秒に収まり、3 エージェント合計の月額コストは Workers Paid プラン込みで 約 ¥412 で運用できています(2026 年 4 月実測、リクエスト数 約 8,400 件/月)。
3. Slack の Block Kit には 1 section 3,000 文字制限がある
トレンドレポートを Slack に投げると、section ブロックの本文が 3,000 文字を超えた瞬間にエラーで全文落ちます。対策は、Markdown のレポートを 2,500 文字単位で chunk 分割し、section を複数ブロックとして posting することです。
function chunkForSlack ( report , chunkSize = 2500 ) {
const blocks = [];
for ( let i = 0 ; i < report. length ; i += chunkSize) {
blocks. push ({
type: "section" ,
text: { type: "mrkdwn" , text: report. slice (i, i + chunkSize) }
});
}
return blocks;
}
4. Privacy Policy・Terms 変更を見逃さない別エージェントを切る
差分判定を LLM に任せると、Privacy Policy の細かい改訂(クッキー方針・データ保持期間など)は「本文の意味は変わっていない」と判定されて見逃されます。法務リスクに直結するので、固定 URL の HTML を MD5 でハッシュ化して「ビット単位で変わったら通知」する別エージェントを切るのが安全です。
私の運用では、競合 5 社 × /privacy・/terms・/pricing の固定 3 URL を毎週月曜の 09:00 JST にスキャンしています。月に 1 〜 2 件は何かしらヒットしますが、そのうち本当に重要だったのは過去 6 ヶ月で 3 件でした。
5. 競合のプライシング変更は週末・月末・四半期末に偏る
AdMob で eCPM が急変動するタイミング(広告主予算の四半期締め)と似た理屈で、競合の値上げ・値下げは木曜深夜〜日曜朝、月末最終週、四半期末(3/6/9/12 月の最終週)に集中します。私が個人開発で 2014 年から iOS/Android アプリを運用してきた中での実感です。
検出頻度を曜日で重み付けすると、Anthropic API の呼び出しコストを抑えながら拾い漏れを減らせます。
function getPollingInterval ( date ) {
const day = date. getDay (); // 0=日曜
const dom = date. getDate ();
const month = date. getMonth () + 1 ;
const isQuarterEnd = [ 3 , 6 , 9 , 12 ]. includes (month) && dom >= 25 ;
const isMonthEnd = dom >= 28 ;
const isWeekend = day === 0 || day === 6 || day === 5 ;
if (isQuarterEnd) return 30 * 60 * 1000 ; // 30 分間隔
if (isMonthEnd || isWeekend) return 2 * 60 * 60 * 1000 ; // 2 時間間隔
return 6 * 60 * 60 * 1000 ; // 平日は 6 時間間隔
}
6. Tier 1 のレート制限 50 req/min は並列 3 サイト × 5 競合で簡単に超える
Anthropic API の Tier 1(クレジット $5 以上)で 50 req/min は十分に見えますが、3 エージェント × 5 競合を Promise.all で同時に走らせると瞬間最大値で 15 req が一気に出て、retry-after 付きの 429 が返ります。Sliding window で 12 req/min に絞り込み、超過分はキューに退避する制御を入れています。
class RateLimitedQueue {
constructor ( maxPerMin = 12 ) {
this .maxPerMin = maxPerMin;
this .timestamps = [];
}
async waitForSlot () {
const now = Date. now ();
this .timestamps = this .timestamps. filter (( t ) => now - t < 60_000 );
if ( this .timestamps. length >= this .maxPerMin) {
const wait = 60_000 - (now - this .timestamps[ 0 ]);
await new Promise (( r ) => setTimeout (r, wait + 100 ));
return this . waitForSlot ();
}
this .timestamps. push (Date. now ());
}
}
個人開発 12 年の視点で考える「市場調査の優先順位」
ここまでは実装の話でしたが、最後にもう一段メタな話を残させてください。私が iOS/Android アプリ開発を 2014 年から個人で続けてきた中で、市場調査エージェントを「何のために」回すべきかを言語化したものです。
価格より「機能のセグメンテーション」を追う
私が 2018 年頃に運用していた壁紙アプリで、競合が「写真自動切り替え」機能を追加した翌月、自社アプリの 7 日継続率が 18% 落ちたことがあります。当時は「価格を下げれば取り返せる」と判断して月額を ¥240 から ¥120 に下げましたが、結果として ARPU が半減しただけで、流出は止まりませんでした。
そこから学んだのは、競合の「価格」を真似ても CAC は悪化するだけで、本当に追うべきは「機能セグメンテーションの境界線」だ、ということです。無料 / Pro / Pro+ の境界に何を置いているかが、その競合の収益構造を雄弁に語ります。Claude のプロンプトには feature_segmentation_diff という観点を必ず入れています。
const PRIORITY_PROMPT = `
変化を以下の優先度で分類してください:
- P0: 機能セグメンテーションの境界線変更(無料↔有料、Tier 1↔Tier 2)
- P1: 新機能リリース、特に AI/自動化機能
- P2: UI/UX の主要な刷新
- P3: 価格変更
- P4: マーケティングコピーの変更(基本的に通知不要)
` ;
AdMob・Stripe・Web 検索のデータを足し算で並べない
Dolice Labs として Claude Lab / Gemini Lab / Antigravity Lab / Rork Lab の 4 サイトを並行運用していると、AdMob(アプリ広告収益)・Stripe(メンバーシップ課金)・Web 検索(競合の動向)の 3 つのデータソースが日々入ってきます。これを Notion の同じデイリーレポートに足し算で並べてしまうと「ノイズ過多」で意思決定が止まります。
私が運用している優先度ルールは単純です。
高(当日対応): Stripe の MRR が前週比 -10% 以下に急落したとき
中(翌週レビュー): AdMob の eCPM が前週比 ±20% を超えて変動したとき
低(翌月レビュー): 競合が新機能をリリースしたとき
通知のみ(四半期レビュー): 競合が価格を改定した、または UI を刷新したとき
Claude エージェントの出力を Notion の Linked Database に流す際、この優先度を必ずタグ付けしてから流すようにしています。これだけで、Slack の通知疲れが激減しました。
AI ブログ運営の競合は週次で 1 回で十分
Dolice Labs 4 サイトの競合(AI 技術解説ブログ)は、Claude API でほぼ毎日記事が増えています。最初は「毎日チェックすべきか」と思いましたが、実際に SimHash 差分判定を 1 日 1 回で 3 ヶ月回してみた結果、本当に「自社の意思決定が変わった」事例は 6 件でした。
その 6 件のうち 5 件は週末(金〜日)に投稿されていたので、現在は毎週月曜の 07:00 JST に 1 回だけ走らせる設定に落ち着いています。API コストは 1/7 に、Slack の通知も 1/7 になり、それでもインサイト密度は変わりませんでした。
「リアルタイム性」と「シグナル密度」は反比例することが多く、市場調査エージェントの設計で最も大事な判断は「どこまで遅らせるか」だと考えています。
月収 100 万円時代の反省 — 競合より自社ユーザーを見る
私が AdMob で月収 100 万円を超えていた 2016 〜 2019 年頃、競合分析にかなりの時間を割いていましたが、振り返ると数字を動かしたのは競合分析ではなく「自社ユーザーへの最適化」でした。具体的には、Crashlytics のクラッシュ修正と、リテンション 30 日のコホート分析です。
市場調査エージェントは「自社の判断材料を取捨選択する」プロセスに過ぎず、最終的な意思決定は自社のユーザーデータが主役であるべきだと考えています。本記事のエージェントを実運用する際は、出力に「で、自社の D7 リテンションには何が効くのか」というセルフレビュー欄を必ず添えてください。
まとめ
構築したシステムのポイントを振り返ると:
競合分析エージェント : ステートフルな差分検出により「変化点」を的確に抽出
価格監視エージェント : 前回データとの自動比較で値上げ・値下げを即検知
トレンド検出エージェント : 200K コンテキストを活かした大量情報の一括処理
統合パイプライン : 3エージェントの並列実行と Slack・Notion への自動配信
このシステムをそのまま使っても良いですし、業界特化のプロンプトを追加したり、エージェントをさらに細分化して精度を上げることも可能です。市場の変化を誰よりも早くキャッチし、ビジネス判断に活かしてください。
エージェント設計を自動化ビジネス全体に広げたい方には、Claude 自動化収益システム完全ガイド もあわせてご覧ください。また、Web 監視自動化の基礎については Cowork Web モニタリング自動化ガイド も参考になります。
さらに深い情報収集・フィルタリング・引用付き出力の実装については、Claude API Web 検索完全ガイド をご参照ください。