DeepSeek Harness: サービス分離とスコープ
最終更新:2026-08-31
マルチエージェント、マルチセッション環境では、共有サービスは恩恵でもあり呪いでもあります——グローバルサービスは便利だが競合しやすく、分離されたサービスは安全だが通信コストが増えます。Cordis のスコープシステムは両者のバランスを取ります:デフォルトで共有、必要に応じて分離。
📋 前提知識:19-service.md の完了、Service 基底クラスを理解していること
1. 学習内容
- グローバルスコープ
- セッションスコープ
- リクエストスコープ
- isolate realm 設定
- スコープと Agent プリセット
- マルチエージェントサービス分離
2. グローバルスコープ
(1) デフォルト動作
デフォルトではすべての Service はグローバル——DSH インスタンス全体でインスタンスは1つだけ:
export default class CacheService extends Service {
constructor(ctx: Context) {
super(ctx, 'cache') // グローバルに一意
}
}
(2) グローバルサービスの特性
| 特性 | 説明 |
|---|---|
| シングルトン | プロセスにつき1インスタンスのみ |
| 共有 | すべてのセッションとリクエストが同じ状態を共有 |
| 分離なし | セッション A のデータ変更はセッション B からも見える |
(3) 適用シナリオ
- 設定管理(グローバル設定は1つだけ)
- 接続プール(共有接続の方が効率的)
- ロギングサービス(ログは一元収集すべき)
- モデルアダプタ(API 呼び出しはセッション間で再利用可能)
▶ サンプル 4:
// ❌ グローバルキャッシュはセッション間のデータ漏洩を引き起こす
export default class CacheService extends Service {
private data = new Map<string, any>()
set(key: string, value: any) {
this.data.set(key, value)
}
}
// セッション A:ctx.cache.set('temp', 'secret-data')
// セッション B:ctx.cache.get('temp') → 'secret-data' (漏洩!)
3. セッションスコープ
(1) 概念
セッションスコープは各セッションに独立したサービスインスタンスを作成:
export default class SessionCacheService extends Service {
static scope = 'session'
constructor(ctx: Context) {
super(ctx, 'session-cache')
}
}
▶ サンプル 2:
graph LR
S1[Session A 作成] --> C1[CacheService A インスタンス]
S2[Session B 作成] --> C2[CacheService B インスタンス]
S1 --> D1[Session A 破棄 → CacheService A 破棄]
S2 --> D2[Session B 破棄 → CacheService B 破棄]
(3) スコープ宣言
export default class SessionCacheService extends Service {
static scope = 'session'
// または
// static scope = Symbol('session')
}
(4) 適用シナリオ
- セッションレベルのキャッシュ
- セッションレベルの設定オーバーライド
- セッションレベルのツールセットカスタマイズ
- セッション履歴
(5) セッション間分離効果
セッション A:
ctx.sessionCache.set('key', 'value-A')
セッション B:
ctx.sessionCache.get('key') → undefined (分離されている)
セッション A:
ctx.sessionCache.get('key') → 'value-A' (セッション内ではアクセス可能)
4. リクエストスコープ
(1) 概念
リクエストスコープは各ツール呼び出しや LLM リクエストに独立したインスタンスを作成:
export default class RequestContextService extends Service {
static scope = 'request'
constructor(ctx: Context) {
super(ctx, 'request-context')
}
}
(2) リクエストスコープのライフサイクル
graph LR
R1[リクエスト 1 開始] --> S1[サービスインスタンス 1]
R2[リクエスト 2 開始] --> S2[サービスインスタンス 2]
R1 --> E1[リクエスト 1 完了 → インスタンス 1 破棄]
R2 --> E2[リクエスト 2 完了 → インスタンス 2 破棄]
(3) 適用シナリオ
- リクエストレベルのコンテキスト情報(ユーザー IP、リクエスト ID)
- リクエストレベルの権限チェック
- リクエストレベルのパフォーマンスタイミング
- リクエストレベルのログ集約
(4) 3つのスコープレベルの比較
| 次元 | グローバル | セッション | リクエスト |
|---|---|---|---|
| インスタンス | 1 | セッションごとに1 | リクエストごとに1 |
| ライフサイクル | プロセス生存期間 | セッション生存期間 | リクエスト生存期間 |
| 状態共有 | グローバル共有 | セッション内 | リクエスト内のみ |
| メモリ | 低 | 中 | 高 |
| 最適用途 | 設定/接続プール | セッションキャッシュ/履歴 | 権限/タイミング |
5. isolate Realm 設定
(1) Realm 概念
Realm は Cordis の分離ドメイン——同一 DSH インスタンス内に独立したプラグイン空間を作成:
# cordis.yml
realms:
agent-a:
isolate:['cache', 'tools']
plugins:
my-tool-a:
$insert:./plugins/tool-a
agent-b:
isolate:['cache', 'tools']
plugins:
my-tool-b:
$insert:./plugins/tool-b
(2) isolate フィールド
isolate リストはどのサービスが Realm 内で独立インスタンスを持つかを指定:
realms:
my-realm:
isolate:
- cache # cache サービスが独立インスタンスを持つ
- tools # tools サービスが独立インスタンスを持つ
# リストにないサービスはグローバル共有のまま
(3) Realm 内のサービスインスタンス
グローバル:llm(共有)
realm-a:cache(独立)、tools(独立)
realm-b:cache(独立)、tools(独立)
realm-a 内プラグイン → ctx.cache = realm-a の cache
realm-b 内プラグイン → ctx.cache = realm-b の cache
互いに影響しない
(4) Realm のユースケース
| シナリオ | 説明 |
|---|---|
| マルチエージェント | 異なる Agent が異なるツールセットとキャッシュを持つ |
| マルチテナント | 異なるテナントのサービスが相互に分離 |
| テスト | テスト Realm がプロダクション Realm に影響しない |
| A/B テスト | 2つの Realm が異なるサービス実装を使用 |
(5) Realm 間通信
Realm はデフォルトで分離されていますが、グローバルサービスを通じて通信可能:
// グローバルサービス(isolate の影響を受けない)
export default class EventBusService extends Service {
constructor(ctx: Context) {
super(ctx, 'event-bus') // isolate リストにない → グローバル共有
}
emit(event: string, data: any) { /* ... */ }
on(event: string, handler: Function) { /* ... */ }
}
// realm-a 内プラグイン
ctx.eventBus.emit('data-updated', { source: 'realm-a' })
// realm-b 内プラグイン
ctx.eventBus.on('data-updated', (data) => {
ctx.logger.info(`received from ${data.source}`)
})
6. スコープと Agent プリセット
(1) Agent プリセット概念
Agent プリセットはツールセット、モデルパラメータ、スコープ設定を含む事前定義された Agent 設定:
presets:
coder:
model:deepseek-coder
tools:[file_edit, shell, search]
mode:standard
reviewer:
model:deepseek-chat
tools:[file_edit, search]
mode:minimal
isolate:[cache]
(2) プリセットとスコープの関係
各プリセットは isolate リストを指定でき、その Agent のために独立したサービスインスタンスを作成:
coder Agent: 共有キャッシュ、独立ツール
reviewer Agent:独立キャッシュ、共有ツール
(3) マルチエージェントシナリオ
# cordis.yml
agents:
coder:
preset:coder
isolate:[tools]
reviewer:
preset:reviewer
isolate:[tools, cache]
(4) エージェント間コラボレーション
graph TB
subgraph Global
LLM[llm Service]
EVENT[event-bus Service]
end
subgraph Agent-Coder
CT[tools Service]
CC[cache Service]
end
subgraph Agent-Reviewer
RT[tools Service]
RC[cache Service]
end
CT --> LLM
RT --> LLM
CT --> EVENT
RT --> EVENT
7. マルチエージェントサービス分離
(1) 分離戦略の選択
完全共有: すべてのサービスをグローバル → シンプルだが競合の可能性
完全分離: すべてのサービスを独立 → 安全だがリソース浪費
混合分離: コアサービス共有 + ビジネスサービス分離 → バランス型
推奨される混合分離:
| サービスタイプ | 分離戦略 | 理由 |
|---|---|---|
| llm | 共有 | API 呼び出しの再利用が可能 |
| sessions | 共有 | 統一セッション管理 |
| tools | 分離 | 各 Agent が異なるツールセットを持つ |
| cache | 分離 | 各 Agent が独立したキャッシュを持つ |
| fs | 共有 | ファイルシステムは1つのみ |
▶ サンプル 2:
agents:
frontend-dev:
preset:coder
isolate:[tools, cache]
tools:
- file_edit
- shell
- search
config:
cache:
maxSize:100
backend-dev:
preset:coder
isolate:[tools, cache]
tools:
- file_edit
- shell
- search
- database
config:
cache:
maxSize:200
(3) リソース消費推定
| 分離レベル | メモリ | CPU | 接続 |
|---|---|---|---|
| グローバル共有 | 1x | 1x | 1x |
| 2エージェント分離 | ~2x | ~1.5x | ~2x |
| 5エージェント分離 | ~5x | ~3x | ~5x |
(4) 分離リーク検出
// サービスインスタンス数を監視
ctx.on('service/created', (name, instance) => {
ctx.logger.info(`service created:${name}, total instances:${countInstances(name)}`)
})
// サービスのインスタンス数がエージェント数を大幅に超える場合、リークの可能性
❓ よくある質問
static scope のない Service はデフォルトでグローバルシングルトンです。typescript constructor(ctx:Context) { super(ctx, 'cache') console.log(`cache instance created:${this.id}, scope:${this.scope}`) } 📖 まとめ
- 3つのスコープレベル:グローバル(デフォルト)、セッション(セッションごとに独立)、リクエスト(リクエストごとに独立)
- グローバルサービスは設定/接続プール、セッションサービスはキャッシュ/履歴、リクエストサービスは権限/タイミングに適する
- isolate Realm 設定は特定空間に独立したサービスインスタンスを作成
- Agent プリセットとスコープの組み合わせでマルチエージェントのツールセットとキャッシュ分離を実現
- 推奨混合分離戦略:コアサービス共有 + ビジネスサービス分離
- スコープ選択はメモリとパフォーマンスに影響;必要に応じて分離、過剰分離は避ける
📝 練習問題
1. ⭐ 基礎:セッションスコープの SessionStateService を書き、各セッションが独立したキー値ストレージを維持するようにしてください。2つのセッションを起動し、データ分離を確認すること。
2. ⭐⭐ 応用:cache と tools サービスを分離する Realm を設定してください。Realm 内外から cache にアクセスし、異なるインスタンスが得られることを確認。
3. ⭐⭐⭐ チャレンジ:デュアルエージェントシステムを設計してください:coder と reviewer。Coder は file_edit/shell ツール、Reviewer は file_edit/search ツールのみ。両者は llm と sessions を共有するが、cache とツールセットは独立して分離。確認:coder の shell ツールは reviewer から見えず、キャッシュも互いに影響しないこと。