4つのブログサイトを、ほぼ同じ振る舞いのエージェントで回しています。
違うのはシステムプロンプトの一部と、参照する MCP コネクタ、それに読み込ませるスキルだけ。それなのに、私はしばらくの間、サイトごとに丸ごと別のエージェント定義を持っていました。
共通部分を直すたびに4箇所を手で揃える。片方だけ直し忘れて、ある夜のバッチだけ古いプロンプトで走る。似た定義が少しずつずれていく感覚は、地味に神経を削ります。
Managed Agents の agent_with_overrides に出会って、この構図をやっと畳めました。土台となるエージェント定義は1つ。セッションを作るその瞬間だけ、モデル・システムプロンプト・ツール・MCP サーバー・スキルを差し替える。定義は増やさず、実行時に色を付ける発想です。
個人開発で4サイトを回す中でたどり着いた設計です。以下では、その差し替えを「事故が起きない形」に落とし込むための考え方を、実際に使っている検証コードとともに共有します。
なぜ「定義を増やす」ではなく「セッションで差し替える」のか
選択肢は大きく3つありました。
方式 共通部分の同期 差分の見通し 権限事故のリスク
サイトごとに別定義 手動・ずれやすい 良い(独立) 低いが定義爆発
1定義+分岐をプロンプト内に埋める 不要 悪い(肥大化) 中(境界が曖昧)
1定義+セッションでオーバーライド 不要 良い(差分が明示) 設計次第で低くできる
3つ目を選んだ決め手は、差分がコードとして一箇所に集まることでした。
「このサイトのときは、この MCP と、このスキルだけ」という差分が、セッション生成の直前に、読める形で並ぶ。プロンプトの奥深くに条件分岐を埋めるより、はるかに見通しが利きます。
ただし、この方式には最初に必ずつまずく一点があります。
直感に反した点:オーバーライドは「上書き」であって「統合」ではない
最初、私はオーバーライドを「土台に差分を足すもの」だと思い込んでいました。
土台エージェントには4つのツールを許可してある。あるセッションで、そのうちの1つを別ツールに替えたい。だから tools に替えたい1つだけを渡しました。
結果、そのセッションでは残り3つのツールが消えました。
agent_with_overrides で指定したフィールドは、そのフィールドまるごとを置き換えます。配列であっても、要素単位でマージされるわけではありません。tools に1要素だけ渡せば、そのセッションのツールはその1つだけになります。
オーバーライドで渡した内容 そのセッションでの結果
フィールドを省略 土台の値をそのまま継承
フィールドに空配列 [] その要素は「無し」になる(継承ではない)
フィールドに一部の要素だけ 渡した要素だけになる(残りは消える)
言われてみれば当然の仕様です。けれど「差分を足す」という語感に引きずられると、静かにツールが減った状態で夜間バッチが走ることになります。私はログを見て初めて気づきました。
ここから得た原則はひとつ。オーバーライドは常に「そのフィールドの最終形」を渡す。 部分的な足し引きをしたいなら、土台の値を読み込んで、こちらで合成してから渡す。この合成を安全にやるのが、次のファクトリの役割です。
検証付きセッションファクトリ
セッションを作るコードに、差し替えの意図と安全網を一箇所へ集約します。TypeScript(Agent SDK 想定)の実装です。
import Anthropic from "@anthropic-ai/sdk" ;
const client = new Anthropic ();
// 土台エージェントで「許可してよい」上限。オーバーライドがこれを超えたら止める。
const TOOL_ALLOWLIST = new Set ([
"web_search" ,
"bash" ,
"text_editor" ,
]);
const MCP_ALLOWLIST = new Set ([
"mcp-claudelab" ,
"mcp-gemilab" ,
"mcp-antigravitylab" ,
"mcp-rorklab" ,
]);
type SiteOverride = {
site : string ;
systemPrompt : string ;
tools : string []; // そのセッションでの最終形
mcpServers : string []; // そのセッションでの最終形
skills : string [];
model ?: string ;
};
function assertSubset ( name : string , given : string [], allow : Set < string >) {
const stray = given. filter (( v ) => ! allow. has (v));
if (stray. length > 0 ) {
// 想定外の権限拡大を、実行前に必ず落とす
throw new Error (
`[override guard] ${ name } に未許可の項目: ${ stray . join ( ", " ) }`
);
}
}
export function buildSessionAgent ( base : string , o : SiteOverride ) {
// オーバーライドは「上書き」なので、渡す前にこちら側で境界を検証する
assertSubset ( "tools" , o.tools, TOOL_ALLOWLIST );
assertSubset ( "mcpServers" , o.mcpServers, MCP_ALLOWLIST );
if (o.tools. length === 0 ) {
// 空配列は「ツール無し」を意味する。事故を疑いたいので明示ログ。
console. warn ( `[override] ${ o . site }: tools が空。意図的か確認すること` );
}
const overridden = {
type: "agent_with_overrides" as const ,
agent_id: base,
overrides: {
model: o.model, // 省略時は土台のモデルを継承
system: o.systemPrompt,
tools: o.tools. map (( name ) => ({ name })),
mcp_servers: o.mcpServers,
skills: o.skills,
},
};
// 何をどう差し替えたかを一行で残す。あとで挙動を追える。
console. info (
`[session] site=${ o . site } model=${ o . model ?? "(inherit)"} ` +
`tools=${ o . tools . length } mcp=${ o . mcpServers . join ( "+" ) }`
);
return overridden;
}
要点は3つです。
第一に、assertSubset で「土台が許した上限」を実行前に検査します。オーバーライドは上書きなので、うっかり未登録の MCP 名を渡すと、そのセッションだけ意図しない接続先になりかねません。許可集合を外れたら、セッションを作る前に落とします。
第二に、空配列を握りつぶさず警告します。「ツール無し」は正当な設定ですが、私の経験では9割方うっかりです。黙って通さないことが、翌朝の後悔を減らします。
第三に、差し替えた内容を一行のログに残します。オーバーライドはセッション限りで消えるため、あとから「あのバッチはどの構成で走ったか」を追う手掛かりが、ログ以外に残りません。
セッション側で組み立てて呼ぶ
ファクトリを使うと、サイトごとの差分は次のように短く、そして読める形になります。
const SITE_OVERRIDES : Record < string , SiteOverride > = {
claudelab: {
site: "claudelab" ,
systemPrompt: "あなたは Claude 技術ブログの編集者です。敬体で書きます。" ,
tools: [ "web_search" , "text_editor" ],
mcpServers: [ "mcp-claudelab" ],
skills: [ "article-gate" , "templating-gate" ],
},
rorklab: {
site: "rorklab" ,
systemPrompt: "あなたは Rork アプリ開発ブログの編集者です。敬体で書きます。" ,
tools: [ "web_search" , "text_editor" , "bash" ], // Rork はビルド確認で bash を使う
mcpServers: [ "mcp-rorklab" ],
skills: [ "article-gate" ],
model: "claude-opus-4-8" ,
},
};
async function runFor ( site : string ) {
const override = SITE_OVERRIDES [site];
if ( ! override) throw new Error ( `未定義のサイト: ${ site }` );
const session = await client.beta.messages.sessions. create ({
agent: buildSessionAgent ( "agent_blog_base" , override),
});
return session;
}
土台の agent_blog_base は1つだけ。共通の振る舞い、たとえば禁止語のルールや文体の基本方針は、この土台に一度だけ書きます。サイトごとに変わるのは、この表の数行に閉じ込められました。
新しいサイトのオンボーディングは、この表に1エントリ増やすだけ。以前のように定義ファイルを丸ごと複製し、共通部分の同期に神経を使う必要は、もうありません。
定義の重複は4本から1本へ。共通部分を直す箇所も4回から1回に減り、同期にかけていた手間はおよそ75%削減できました。
運用してみて分かった、公式の説明に無い勘所
本番運用で数週間回して、いくつか腑に落ちたことがあります。
オーバーライドはセッションに閉じる。だからこそ棚卸しが要る。 土台の定義は Console から一覧できますが、各セッションが実際どの構成で走ったかは、土台を見ても分かりません。私は上のログを日次で集計し、「どのサイトがどのモデルで何回走ったか」を残すようにしました。差し替えを許すなら、差し替えの記録は自分で持つ。これが抜けると、コスト按分も原因調査もできなくなります。
モデルの継承は便利だが、意図しない追従になり得る。 model を省略すると土台のモデルを継承します。土台のモデルを上げれば全サイトが一斉に上がる。便利な反面、あるサイトだけ旧モデルで検証したいときは、省略せず明示的に固定する必要があります。私は「継承していい構成」と「固定すべき構成」を表の中で分け、固定側には必ず model を書く運用を推奨します。
差し替え粒度は、変更頻度で決めると落ち着く。 よく変わるもの(プロンプトの一部・参照コネクタ)はオーバーライドへ。めったに変わらないもの(禁止語・出力形式の根本方針)は土台へ。私はこの線引きを変更頻度で引くことにして、「どこを直すべきか」で迷わなくなりました。頻繁に触る差分が土台に混ざっていると、共通部分を触る心理的なハードルが上がります。
どう使い分けるか
状況別の私なりの結論です。差し替えを導入する前に、私は次の3点を先に決めています。
どのフィールドを土台に残し、どれをオーバーライドへ出すか(変更頻度で線引きする)
許可集合(ツール・MCP)の上限をどこに置き、何を境界として落とすか
セッションごとの構成をどのログに残し、日次でどう棚卸しするか
この3点を先に決めておくと、権限事故を回避しやすくなります。
土台が2〜3個で、差分が大きいなら、無理にオーバーライドへ寄せず別定義のままで構いません。差し替えの検証コストが、同期コストを上回ります。
一方、土台が実質1つで、差分が「プロンプトの一部・コネクタ・スキル」に収まるなら、セッションオーバーライドが効きます。私の4サイトはまさにこの形でした。
そして差し替えを入れるなら、検証と記録は最初から組み込むこと。オーバーライドは強力なぶん、黙って権限や構成を変えてしまいます。落とすべき境界を assertSubset のような形で先に決めておくと、夜間の自動実行を安心して任せられます。
私自身、まだ運用しながら勘所を掴んでいる途中です。同じように複数の似たエージェントを抱えている方の、設計を畳むきっかけになればうれしく思います。お読みいただきありがとうございました。