Claude Code の deep-research を解剖する — エージェント基盤に持ち帰る7つの設計判断
Claude Code 組み込みの deep-research スキルは、5フェーズの Workflow script として実装されている。中を開くと、フェーズ境界に Schema を置く・Pipeline を default にする・敵対的検証で棄権を「失敗」として扱うなど、汎用エージェント基盤に丸ごと転用できる設計判断が並んでいた。100 近いエージェント呼び出しが走る deep-research のソースを節ごとに開き、抽出できる7原則をまとめる。
← 前の記事: 部品表を1本に組み立てる——借りる部品と自作部品で最小パイプラインを建て、引用ゲートが「幽霊の出典」を突き返すまでClaude Code には deep-research という組み込みスキルがある。「質問を投げると Web を多角的に検索し、出典付きでファクトチェック済みのレポートを返す」あれだ。スキルとしての入口は1つで、テーマを渡せば完結する。
普通に「便利」で終わってもいいのだが、中身を覗くと話が変わる。deep-research の実体は、起動時に生成される JavaScript の Workflow script だ。100 エージェント近くが連携して動く設計図がそこに書いてあり、汎用のエージェント基盤を作る側の目で読むと、これは設計判断の見本市だった。
本記事は機能解説ではなく、deep-research のソースを節ごとに開いて、エージェント基盤に持ち帰れる設計パターンを抽出する。フェーズ境界・パイプライン化・敵対的検証・サルベージ経路まで、7つの判断を順に見ていく。
全体像——5フェーズと4つのツマミ
deep-research の Workflow script は冒頭で自分を5つのフェーズに分けている。
export const meta = {
name: 'deep-research',
phases: [
{ title: "Scope", detail: "Decompose question into 5 search angles" },
{ title: "Search", detail: "5 parallel WebSearch agents, one per angle" },
{ title: "Fetch", detail: "URL-dedup, fetch top 15 sources, extract claims" },
{ title: "Verify", detail: "3-vote adversarial verification per claim" },
{ title: "Synthesize", detail: "Merge dupes, rank by confidence, cite sources" },
],
}そして、ハードコードされたツマミが4つ。
const VOTES_PER_CLAIM = 3 // 1主張あたり何人の検証者を立てるか
const REFUTATIONS_REQUIRED = 2 // 反論2票で却下(3-0 と 2-1 のみ生存)
const MAX_FETCH = 15 // 全アングル合計の fetch 上限
const MAX_VERIFY_CLAIMS = 25 // 検証フェーズに通す主張上限ヘッダーのコメントに「bughunter からの移植」と明記されている。deep-research 用に新規設計したのではなく、別ドメイン用の同型ハーネスを WebSearch / WebFetch に差し替えて流用したものだ。つまりこの構造はドメイン依存のレシピではなく、ファクトチェック型タスクの汎化形として既に検証されている。読む価値がある所以だ。
以降、設計の節を1つずつ開いていく。
1. フェーズ境界に JSON Schema を置く
deep-research は5つの schema を冒頭で宣言し、各フェーズの I/O を強制する。たとえば「主張抽出」フェーズの schema はこうなっている。
const EXTRACT_SCHEMA = {
type: "object", required: ["claims", "sourceQuality"],
properties: {
sourceQuality: { enum: ["primary", "secondary", "blog", "forum", "unreliable"] },
publishDate: { type: "string" },
claims: { type: "array", maxItems: 5, items: {
type: "object", required: ["claim", "quote", "importance"],
properties: {
claim: { type: "string" },
quote: { type: "string" },
importance: { enum: ["central", "supporting", "tangential"] },
},
}},
},
}これを agent(prompt, { schema: EXTRACT_SCHEMA }) の形で渡すと、エージェントは StructuredOutput tool call を強制され、schema 不一致のときは tool 層で retry が走る。
利点が3つある。第一に、下流の検証フェーズは生テキストを parse しない。claim.sourceQuality === "primary" で済む。第二に、retry がスクリプト側に漏れてこない(tool 層で完結する)。第三に、フェーズ間の契約が schema に集約され、設計を読み解くときの起点になる。
設計判断: フェーズ境界は契約。契約は schema で書け。
2. Pipeline を default に、Barrier は必要な場所だけ
5アングルの検索結果を全部待ってから Fetch を始める——という barrier 設計を、deep-research は明示的に避けている。Scope の角度を pipeline() で流し、各アングルが終わった瞬間から Fetch に流れる。
const searchResults = await pipeline(
scope.angles,
angle => agent(SEARCH_PROMPT(angle), {
label: "search:" + angle.label, phase: "Search", schema: SEARCH_SCHEMA,
}),
searchResult => parallel(novel.map(source => () =>
agent(FETCH_PROMPT(source, searchResult.angle), {
label: "fetch:" + host, phase: "Fetch", schema: EXTRACT_SCHEMA,
})
)),
)ここでの「pipelining」とは、stage A → stage B → stage C を各アイテム独立に流すことだ。アングル X はもう Verify 直前まで進んでいる一方で、アングル Y はまだ Search 中、という状態が許される。一番遅い Search を待たないので、wall-clock が大幅に縮む。
唯一の barrier は Verify の手前にある。コードのコメントが意図を明示している。
// Verify: 3-vote adversarial
// Barrier here is intentional — claim pool must be fully assembled before
// ranking/verification.
phase("Verify")なぜここだけ barrier かというと、Verify は「全主張を集めて importance + sourceQuality でランクし、トップ25だけ通す」という cross-item の操作が必要だからだ。ここだけは barrier の正当な理由がある。
設計判断: Pipeline を default にし、cross-item の操作が必要な場所だけ barrier する。
3. URL 正規化 + Budget-aware drop(しかも捨てたものを別計上)
5アングルの検索結果には当然、同じ URL が複数アングルから出てくる。deep-research の dedup はこう書かれている。
const normURL = u => {
try {
const p = new URL(u)
return (p.hostname.replace(/^www\./, "") + p.pathname.replace(/\/$/, "")).toLowerCase()
} catch { return u.toLowerCase() }
}
const seen = new Map()
const dupes = []
const budgetDropped = []
let fetchSlots = MAX_FETCHhostname + pathname を正規化キーにする(プロトコル・クエリ・末尾スラッシュ・大文字小文字を吸収)。同一キーが来たら dupes に積む。fetchSlots が枯渇したあとの medium / low は budgetDropped に積む。捨てたものは捨てっぱなしにせず、別に計上する。
これが効くのは後段だ。最終 return の stats に urlDupes: dupes.length と budgetDropped: budgetDropped.length が並ぶ。チューニングするとき「fetch slots を増やすべきか?」を判断する材料がここに残っている。
設計判断: 捨てたものを別計上せよ。観測できないものはチューニングできない。
4. Source quality と Importance を上流で強制ラベル
EXTRACT_SCHEMA をもう一度見ると、sourceQuality と importance が enum で必須化されている。
sourceQuality: { enum: ["primary", "secondary", "blog", "forum", "unreliable"] },
importance: { enum: ["central", "supporting", "tangential"] },これは「ラベル付けの責任を上流に押し付ける」設計だ。Fetch + Extract のフェーズで強制的にラベルを付けさせる代わり、後段は cap や filter をラベルだけで書ける。
const impRank = { central: 0, supporting: 1, tangential: 2 }
const qualRank = { primary: 0, secondary: 1, blog: 2, forum: 3, unreliable: 4 }
const rankedClaims = [...allClaims]
.sort((a, b) =>
(impRank[a.importance] - impRank[b.importance]) ||
(qualRank[a.sourceQuality] - qualRank[b.sourceQuality]))
.slice(0, MAX_VERIFY_CLAIMS)ランクテーブルは2行。importance を主キー、sourceQuality を tiebreaker にして、上位25件だけを Verify に通す。ラベルさえ正しく振られていれば、後段の選定ロジックは数行で済む。
設計判断: 分類はできるだけ上流で強制し、enum で語彙を縛れ。
5. Adversarial verify と「棄権を失敗扱いに」する細部(中核)
deep-research の心臓はここだ。各 claim に3票の検証者を立て、2票以上の反論で却下する。検証者のプロンプトは「反論しろ」と明示する。
const VERIFY_PROMPT = (claim, v) =>
"## Adversarial Claim Verifier\n" +
"Be SKEPTICAL. Try to REFUTE this claim. ≥2/3 refutations kill it.\n" +
...
"**refuted=true** if: unsupported by quote / contradicted / low-quality\n" +
" source for strong claim / outdated / marketing fluff.\n" +
"**refuted=false** ONLY if: claim is well-supported, current, and source\n" +
" quality matches claim strength.\n" +
"Default to refuted=true if uncertain."LLM のデフォルトの "be helpful" 傾向に逆らって、懐疑がデフォルトであることを言葉で繰り返している。「迷ったら refuted=true」という1行が効く。
しかし、ここで多くのナイーブな実装が踏む地雷がある。棄権(abstention)の扱いだ。deep-research はそこを明示的に塞いでいる。
const valid = verdicts.filter(Boolean)
const refuted = valid.filter(v => v.refuted).length
const abstained = VOTES_PER_CLAIM - valid.length
const survives =
valid.length >= REFUTATIONS_REQUIRED && // ★ ここがミソ
refuted < REFUTATIONS_REQUIRED検証者が user-skip またはエラーで null を返すケースがある。これを「反論しなかった = 支持した」と数えてしまうと、3人とも棄権の場合に refuted = 0 になり「通過」してしまう。deep-research は「有効投票が定足数に届いていない claim は通さない」という条件を明示的に足している。コードコメントも残っている。
// Too many abstentions = unverified, which must NOT pass into the report
// (otherwise all-abstain → refuted=0 → false survive).これは敵対的検証を組む人が確実に踏むバグであり、ここを潰しているか否かで品質ゲートの厳密さが決まる。自分で同じ仕掛けを書くなら、まずこの1行を写経してから先に進むべきだ。
設計判断: 検証は敵対的に設計し、棄権は「支持」ではなく「失敗」として扱え。
6. Fail-soft 経路を各フェーズに用意する
ロングランの Workflow は途中で何かが壊れる前提で書いた方がいい。deep-research はそれを徹底している。
- Scope 失敗: 角度が返らなければ
{ error: ... }を即返す - 全 claim 却下: confirmed が 0 になった場合、
{ summary: "All N claims refuted ...", refuted: [...] }で「inconclusive」と明示して返す - Synthesis 失敗: 統合エージェントが skip / エラーになった場合、verified claims を生の配列として返すサルベージ経路がある
if (!report) {
// Synthesis skipped/errored — salvage the verified claims raw rather
// than throwing on report.findings and discarding the whole run.
return {
question: QUESTION,
summary: "Synthesis step was skipped or failed — returning " +
confirmed.length + " verified claims unmerged.",
confirmed: confirmed.map(c => ({ claim: c.claim, source: c.sourceUrl, ... })),
...
}
}これは「途中で壊れたら最初からやり直し」という素朴な実装の対極にある。上流の高コストな仕事を、下流の失敗で全部捨てない。Verify まで残った25個の主張は、たとえ Synthesis が落ちても、生で返ってくる。呼び出し側はそれを使って判断できる。
設計判断: throw で全部捨てるな。サルベージ経路を各フェーズに用意せよ。
7. Stats を必ず返す
最終 return には統計が並ぶ。
stats: {
angles: scope.angles.length,
sourcesFetched: allSources.length,
claimsExtracted: allClaims.length,
claimsVerified: voted.length,
confirmed: confirmed.length,
killed: killed.length,
afterSynthesis: report.findings.length,
urlDupes: dupes.length,
budgetDropped: budgetDropped.length,
agentCalls: 1 + scope.angles.length + allSources.length +
(voted.length * VOTES_PER_CLAIM) + 1,
}最後の agentCalls を見てほしい。1 (scope) + 5 (search) + 15 (fetch) + 25 × 3 (verify) + 1 (synthesize) = 97 エージェント呼び出し。これがハイレベル設計の総コストだ。
stats が返ってくると、チューニングの議論ができる。「MAX_FETCH を 20 にしたら agentCalls は 22 増える、それに見合うか?」を数値で議論できる。逆に stats を返さないと、改善の議論は印象論になる。
設計判断: 観測できないものはチューニングできない。stats を返せ。
弱点と限界
ここまで deep-research の良さを並べてきたが、万能ではない。3つほど痛い点を挙げておく。
WebSearch の質に丸ごと依存する。Search フェーズで SEO ゴミが high 評価で混入したら、それを Fetch して Verify する羽目になる。Extract 段の sourceQuality 判定と Verify 段の skeptical voter で殺す前提だが、これらの judgment は LLM 任せだ。一次資料が薄い分野(最新業界動向など)では、エコーチェンバー耐性が落ちる。
英語圏前提の Scope プロンプト例示。Scope のプロンプトには "state-of-art" "academic refs" "industry adoption" のような例示が並ぶ。日本固有のドメイン(判例リサーチ、和書ベースの調査)では角度の取り方が不適切になりやすい。ローカライズした派生スキルを作る価値がある。
コストが安くない。97 エージェント呼び出しは、軽量タスクに使う規模ではない。ファクトチェック型の重い質問でしか元が取れない。
まとめ——deep-research から持ち帰る7原則
deep-research のソースから抽出した7つを再掲する。
- フェーズ境界は契約。契約は schema で書け。
- Pipeline を default に、Barrier は cross-item の操作が必要な場所だけ。
- 捨てたものを別計上せよ。観測できないものはチューニングできない。
- 分類はできるだけ上流で強制し、enum で語彙を縛れ。
- 検証は敵対的に設計し、棄権は「支持」ではなく「失敗」として扱え。
- throw で全部捨てるな。サルベージ経路を各フェーズに用意せよ。
- Stats を必ず返せ。チューニングは観測の上にしか乗らない。
これらは deep-research 固有の事情ではなく、汎用のエージェント基盤を作る側がそのまま持ち帰れる原則だ。とくに5番(棄権の扱い)と6番(fail-soft)は、自分で書いたパイプラインが本番で「なぜか結果が空」「なぜか途中で全消えする」となる原因のほぼ全てを占める箇所だと思っている。
deep-research を「便利な調査スキル」として使うのも結構だが、一度ソースを開いてみると、見えるものが変わる。Claude Code の組み込みスキルは、エージェント基盤の参考実装としても読める。