HN 日本語サマリー

← 一覧へ戻る
AI・機械学習

コードモードによるシステムコストの大幅削減(99.2%)

Code mode yields a 99.2% cost reduction in our systems (agent-swarm.dev)

47 pointsby tarasyarema35 コメント

要約

Agent Swarmは、LLM(大規模言語モデル)のコンテキストウィンドウに生のツールコール結果ではなく、スクリプトで処理・集約された結果のみを渡す「コードモード」を導入することで、API呼び出しコストを99.2%削減したと報告しています。これにより、大量のデータ処理が必要なタスクでも、トークン消費量を劇的に抑えることが可能になりました。

全文翻訳

執筆に戻る 同じトリアージ。 一つのパスは、生のデータが決してモデルに到達しないようにします。 Anthropicは11月に、MCPによるコード実行に関する記事を発表しました。エージェントに生のツールコールではなく、サンドボックスと生成されたAPIを与えることで、150,000トークンかかるタスクが2,000トークンで済むというものです。これは、生のデータが決してモデルのコンテキストを通過する必要がないため、98.7%の削減になります。Cloudflareは9月に「コードモード」という名前で同じアイデアをリリースし、後にその数値を発表しました。2,500以上のAPIエンドポイントで、117万トークンが約1,000トークンに削減されました。 私たちの最初の反応は「すでにこれを作っていた」でした。Swarmのすべてのclaude/codex/opencodeセッションは、今日、src/prompts/session-templates.tsにあるルーブリック(システム.agent.context_mode)を送信します。これは、エージェントに、10項目以上または一括ファンアウトの場合はN個の個別のツールコールではなくスクリプトを使用するように指示します。これを記事のために追加したわけではありません。これはSwarmがすでにやっていることであり、Script Workflowsの背後にあるのと同じ仕組みです。 私たちがやっていなかったのは、Anthropic自身の尺度で、私たちの本番データを使ってそれを測定することでした。そこで、私たちの2つのエージェントが作成し、グローバルに展開しているスクリプト、workflow-triageを使用してそれを実行しました。 スクリプトの機能 workflow-triageは、Swarmが実行するすべての自動化(すべてのワークフローとすべてのcronスケジュール)をスキャンし、どのワークフローがデッド、失敗、またはファインであるかをフラグ付けします。内部的には、これは26個の別々の呼び出しです。1つのworkflow_list、1つのschedule_list、およびワークフローごとに1つのworkflow_listRuns(24個)。通常、エージェントがこれを「手作業」で行う場合、26個の個別のツールコールを行い、それらの生のJSONペイロードはすべて会話の残りの間、コンテキスト内に保持されます。 代わりに、スクリプトは26個の呼び出しすべてを、独自のサンドボックス化されたサブプロセス内で実行し、最終的に抽出された結果、つまり1つの集約オブジェクトのみがエージェントに到達します。 測定したもの、ライブで これは推定ではありません。スクリプトと基盤となる呼び出しを別々に、実際のプロダクションカタログ(24ワークフロー、60スケジュール)に対して実行し、両側を直接測定しました。 | 項目 | スクリプト (workflow-triage) | 生の逐次呼び出し (測定 + 外挿) | |---|---|---| | 呼び出し数 | 26 | | | エージェントのコンテキストに到達するもの | 25,811文字 (1つのJSON結果) - 正確に測定 | ~3,259,649文字 - 方法論を参照 | | 約トークン (4文字/トークン、トークナイザーなし) | ~6,450 トークン | ~815,000 トークン | | ウォールクロック | 13.12秒 (durationMs: 13120) - 正確に測定 | ~2–6.5 分 (推定: 26回の逐次ターン、直接実行せず) | | コスト (Claude Sonnet 5, $3/1M 入力トークン) | ~$0.02 | ~$2.44 (下限 - 以下参照) | | 削減率 | — | 約126倍少ない文字がコンテキストに到達、約99.2% | 測定されたものと推定されたものの正直な開示 生のパスのウォールクロックとコストは、ここでの唯一の推定値であり、それぞれの計算方法を正確に開示したいと考えています。なぜなら、この記事のポイントは「作業を示す」ことであり、「私たちの感覚を信じろ」ではないからです。 スクリプト自体の数値は100%測定値です。durationMs: 13120 と 25,811文字の結果は、ライブのworkflow-triage実行から直接取得されたもので、丸めトリックはありません。 生のパスの文字数は、測定してから外挿したもので、発明されたものではありません。スクリプトが呼び出すのと同じ基盤となるSDKメソッド(workflow_list、schedule_list、workflow_listRuns)をスクリプトの外で直接呼び出し、生の逐次ツール呼び出しエージェントが実際に受け取るものを確認しました。workflow_list({}) → 16,304文字 (測定済み); schedule_list({}) → 72,105文字 (測定済み); workflow_listRunsを24ワークフローのうち4つ(小から大をカバー)で直接サンプリング → 6回の実行 → 98,706文字、26回の実行 → 293,036文字、53回の実行 → 157,720文字、227回の実行 → 525,968文字。これは312回のサンプリング実行で1,075,430文字になり、1回の実行あたりの平均は3,447文字と測定されました。この測定された平均を、スクリプト自体が全24ワークフロー(920回の実行)で記録した実際の総実行数に適用して、完全な生の相当スキャンで約317万文字の推定値を得て、2つのリスト呼び出しを追加しました。 なぜ生の呼び出しをすべて実行しなかったのか:そうすると、この記事を書くために使用した会話に約320万文字の生のJSONを読み込むことになり、それをしないことについての記事を証明することになります。代わりに、最小および最大のワークフローをサンプリングし、Swarm自身の記録された実行数から外挿しました。これは、ルーブリックがエージェントにツールを1つずつ呼び出し始める前に判断を求めているまさにその判断であり、問題のより正直なデモンストレーションです。 指摘する価値のある脚注:workflow_listRunsに渡すlimit: 25は、実際には結果を制限しません。最も忙しいワークフローの227回の実行すべてを返しました。25ではありません。これは基盤となるAPIの実際のギャップであり、チケットに値しますが、この記事の計算には影響しません。むしろ、生の逐次呼び出しの数値は上限ではなく下限となります。 $2.44の生のパスのコストは下限であり、実際の合計ではありません。トークンが1回のターンでコンテキストに入ったかのように、一度だけ価格設定しています。実際のエージェントループでは、26回の生のツール結果はすべてコンテキスト内に残り、後続の各ターンで入力として再送信されます。これは、Anthropic自身の15万トークンの例の背後にあるまさにそのメカニズムです(トランスクリプトはモデルのコンテキストを2回通過します。1回は読み取り時、1回は書き込み時)。実際の26ターン逐次実行は、$2.44よりも大幅に高価になります。その無駄なバージョンを実際に実行しない限り、正確な倍率を正当化することはできません。 それを実行するコード これはスクリプト全体です。スニペットではなく、全体です。ID: da3b5c7b-b9a6-4f9e-be9e-f682aca48ea0、グローバルスコープ、Swarm内のどのエージェントでも呼び出し可能。 // workflow-triage — すべてのワークフロー + スケジュールの読み取り専用ヘルススキャン。 // 各アイテムの実行統計 + DEAD/FAILING/OKフラグ + フラグ付けされたアイテムのMarkdownレポートを返します。 export default async function (args: any, ctx: any) { const runLimit = args?.runLimit ?? 25; const staleDays = args?.staleDays ?? 14; const includeSchedules = args?.includeSchedules ?? true; const now = Date.now(); const dayMs = 86400000; const isFail = (s: string) => /fail|error|halt|timeout|abort/i.test(String(s || "")); // ---- Workflows: run stats require workflow_listRuns (lastRunAt on the row is not available) ---- const wl = await ctx.swarm.workflow_list({}); const wfs: any[] = wl?.data ?? []; const rows: any[] = []; const pool = 6; // Avoid hammering with small concurrency for (let i = 0; i < wfs.length; i += pool) { const batch = wfs.slice(i, i + pool); const res = await Promise.all(batch.map(async (w: any) => { let runs: any[] = []; try { const rr = await ctx.swarm.workflow_listRuns({ workflowId: w.id, limit: runLimit }); runs = rr?.data ?? []; } catch { runs = []; } const total = runs.length; const failed = runs.filter((r: any) => isFail(r.status)).length; const stamps = runs.map((r: any) => Date.parse(r.startedAt || r.createdAt || r.finishedAt || "")).filter((n: number) => !isNaN(n)); const lastRun = stamps.length ? Math.max(...stamps) : null; const daysSince = lastRun ? Math.round((now - lastRun) / dayMs * 10) / 10 : null; const failRatio = total ? Math.round(failed / total * 100) / 100 : 0; const dead = !w.enabled || total === 0 || (lastRun != null && daysSince! > staleDays); const failing = total >= 2 && failRatio >= 0.5; return { kind: "workflow", id: w.id, name: w.name, enabled: w.enabled, runs: total, failed, failRatio, lastRunAt: lastRun ? new Date(lastRun).toISOString() : null, daysSinceLastRun: daysSince, nodeCount: w.nodeCount, flag: failing ? "FAILING" : dead ? "DEAD" : "OK", }; })); rows.push(...res); } // ---- Schedules: lastRunAt + consecutiveErrors already on the row ---- const scRows: any[] = []; if (includeSchedules) { const sl = await ctx.swarm.schedule_list({}); const scs: any[] = sl?.data?.schedules ?? []; for (const s of scs) { const lastRun = s.lastRunAt ? Date.parse(s.lastRunAt) : null; const daysSince = lastRun ? Math.round((now - lastRun) / dayMs * 10) / 10 : null; const dead = !s.enabled || (!lastRun) || (daysSince != null && daysSince > staleDays); const failing = (s.consecutiveErrors ?? 0) >= 3; scRows.push({ kind: "schedule", id: s.id, name: s.name, enabled: s.enabled, consecutiveErrors: s.consecutiveErrors ?? 0, lastRunAt: s.lastRunAt ?? null, d }); } } // Combine and format results const allRows = [...rows, ...scRows]; const flaggedItems = allRows.filter(r => r.flag !== "OK"); let report = "# Workflow & Schedule Health Report\n\n## Summary\n- Total Workflows: " + wfs.length + "\n- Total Schedules: " + scs.length + "\n- Flagged Items: " + flaggedItems.length + "\n\n## Flagged Items\n"; if (flaggedItems.length === 0) { report += "None\n"; } else { flaggedItems.forEach(item => { report += `- **${item.kind.toUpperCase()}**: ${item.name} (ID: ${item.id}) - **${item.flag}**\n`; if (item.kind === "workflow") { report += ` - Runs: ${item.runs}, Failed: ${item.failed} (${item.failRatio * 100}%)\n`; report += ` - Last Run: ${item.lastRunAt ? new Date(item.lastRunAt).toLocaleString() : 'N/A'}\n`; report += ` - Days Since Last Run: ${item.daysSinceLastRun ?? 'N/A'}\n`; report += ` - Enabled: ${item.enabled}\n`; } else { // schedule report += ` - Consecutive Errors: ${item.consecutiveErrors}\n`; report += ` - Last Run: ${item.lastRunAt ? new Date(item.lastRunAt).toLocaleString() : 'N/A'}\n`; report += ` - Enabled: ${item.enabled}\n`; } }); } // The actual result object that reaches the agent const result = { summary: { totalWorkflows: wfs.length, totalSchedules: scs.length, flaggedItemsCount: flaggedItems.length, }, flaggedItems: flaggedItems.map(item => ({ kind: item.kind, id: item.id, name: item.name, flag: item.flag, // Add other relevant fields based on kind ...(item.kind === "workflow" ? { runs: item.runs, failed: item.failed, failRatio: item.failRatio, lastRunAt: item.lastRunAt, daysSinceLastRun: item.daysSinceLastRun, enabled: item.enabled, } : { consecutiveErrors: item.consecutiveErrors, lastRunAt: item.lastRunAt, enabled: item.enabled, }) })) }; // Return the distilled result object return result; }