@rex0220/llm-task-router 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/LICENSE +21 -0
- package/README.ja.md +147 -0
- package/README.md +147 -0
- package/config/criteria/default.md +43 -0
- package/config/criteria/note.md +32 -0
- package/config/models.yaml +111 -0
- package/config/profiles/blog.yaml +9 -0
- package/config/profiles/note.yaml +11 -0
- package/config/profiles/qiita.yaml +13 -0
- package/config/profiles/zenn.yaml +9 -0
- package/dist/llm-task-router.js +1465 -0
- package/docs/api-smoke-test.md +291 -0
- package/docs/thin-model-router-design.md +1069 -0
- package/docs/thin-model-router-implementation-plan.md +479 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
# 薄い ModelRouter 設計書
|
|
2
|
+
|
|
3
|
+
作成日: 2026-06-16
|
|
4
|
+
目的: Qiita記事作成などの個人・小規模用途で、複数の生成AIを安全に使い分けるための最小限のModelRouterを設計する。
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## 1. 背景
|
|
9
|
+
|
|
10
|
+
ChatGPT、Claude、Geminiなどの生成AIには、Web版の時間制限、週制限、混雑、モデルごとの得意不得意がある。
|
|
11
|
+
また、LiteLLMのような高機能プロキシは便利だが、APIキーやプロンプトを集約するため、セキュリティ上の攻撃面が広がる。
|
|
12
|
+
|
|
13
|
+
そこで、Qiita記事作成や技術記事作成に必要な範囲に限定した、薄いModelRouterを自作する。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. 設計方針
|
|
18
|
+
|
|
19
|
+
### 2.1 やること
|
|
20
|
+
|
|
21
|
+
- タスク別のモデル選択
|
|
22
|
+
- 複数AIへのフォールバック
|
|
23
|
+
- リトライ
|
|
24
|
+
- タイムアウト
|
|
25
|
+
- 途中成果物の保存
|
|
26
|
+
- 出力スキーマ検証
|
|
27
|
+
- 最小限の使用ログ
|
|
28
|
+
- コスト概算
|
|
29
|
+
- APIキーの分離管理
|
|
30
|
+
|
|
31
|
+
### 2.2 やらないこと
|
|
32
|
+
|
|
33
|
+
- Web管理画面
|
|
34
|
+
- 外部公開API
|
|
35
|
+
- ユーザー管理
|
|
36
|
+
- 動的な設定変更API
|
|
37
|
+
- 任意コード実行
|
|
38
|
+
- 任意URLアクセス
|
|
39
|
+
- プラグイン実行
|
|
40
|
+
- DBへの全文プロンプト保存
|
|
41
|
+
- 複雑な認証基盤
|
|
42
|
+
|
|
43
|
+
薄く作ることで、セキュリティリスクと保守コストを抑える。
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 3. 想定ユースケース
|
|
48
|
+
|
|
49
|
+
主な用途はQiita記事作成。
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
記事テーマ
|
|
53
|
+
↓
|
|
54
|
+
Article Brief作成
|
|
55
|
+
↓
|
|
56
|
+
構成案作成
|
|
57
|
+
↓
|
|
58
|
+
コード例作成
|
|
59
|
+
↓
|
|
60
|
+
Qiita向けMarkdown生成
|
|
61
|
+
↓
|
|
62
|
+
技術レビュー
|
|
63
|
+
↓
|
|
64
|
+
リライト
|
|
65
|
+
↓
|
|
66
|
+
final.md出力
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 4. 全体アーキテクチャ
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
Article Workflow
|
|
75
|
+
↓
|
|
76
|
+
ModelRouter
|
|
77
|
+
↓
|
|
78
|
+
Provider
|
|
79
|
+
├─ OpenAIProvider
|
|
80
|
+
├─ AnthropicProvider
|
|
81
|
+
├─ GeminiProvider
|
|
82
|
+
└─ LocalProvider
|
|
83
|
+
↓
|
|
84
|
+
SchemaValidator
|
|
85
|
+
↓
|
|
86
|
+
RunStore
|
|
87
|
+
↓
|
|
88
|
+
Markdown / JSON成果物
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 5. 推奨技術スタック
|
|
94
|
+
|
|
95
|
+
### 5.1 言語
|
|
96
|
+
|
|
97
|
+
TypeScriptを推奨する。
|
|
98
|
+
|
|
99
|
+
理由:
|
|
100
|
+
|
|
101
|
+
- JSON / YAML / Zodとの相性が良い
|
|
102
|
+
- CLIツール化しやすい
|
|
103
|
+
- Codex / Claude Code のどちらでも扱いやすい
|
|
104
|
+
- 将来的にWeb UIへ拡張しやすい
|
|
105
|
+
- Qiita記事生成やMarkdown処理と相性が良い
|
|
106
|
+
|
|
107
|
+
### 5.2 主なライブラリ候補
|
|
108
|
+
|
|
109
|
+
| 用途 | 候補 |
|
|
110
|
+
|---|---|
|
|
111
|
+
| スキーマ検証 | Zod |
|
|
112
|
+
| YAML読み込み | yaml |
|
|
113
|
+
| CLI | commander / cac |
|
|
114
|
+
| ファイル操作 | Node.js fs/promises |
|
|
115
|
+
| ログ | pino または独自JSONログ |
|
|
116
|
+
| OpenAI API | openai |
|
|
117
|
+
| Anthropic API | @anthropic-ai/sdk |
|
|
118
|
+
| Gemini API | @google/genai |
|
|
119
|
+
| ローカルLLM | Ollama APIなど |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 6. ディレクトリ構成
|
|
124
|
+
|
|
125
|
+
```text
|
|
126
|
+
model-router/
|
|
127
|
+
package.json
|
|
128
|
+
tsconfig.json
|
|
129
|
+
.env.example
|
|
130
|
+
config/
|
|
131
|
+
models.yaml
|
|
132
|
+
src/
|
|
133
|
+
index.ts
|
|
134
|
+
router/
|
|
135
|
+
ModelRouter.ts
|
|
136
|
+
config.ts
|
|
137
|
+
errors.ts
|
|
138
|
+
types.ts
|
|
139
|
+
providers/
|
|
140
|
+
ModelProvider.ts
|
|
141
|
+
OpenAIProvider.ts
|
|
142
|
+
AnthropicProvider.ts
|
|
143
|
+
GeminiProvider.ts
|
|
144
|
+
LocalProvider.ts
|
|
145
|
+
schemas/
|
|
146
|
+
ArticleBriefSchema.ts
|
|
147
|
+
ArticleOutlineSchema.ts
|
|
148
|
+
ReviewResultSchema.ts
|
|
149
|
+
workflows/
|
|
150
|
+
createQiitaArticle.ts
|
|
151
|
+
resumeQiitaArticle.ts
|
|
152
|
+
storage/
|
|
153
|
+
RunStore.ts
|
|
154
|
+
logger/
|
|
155
|
+
RunLogger.ts
|
|
156
|
+
utils/
|
|
157
|
+
cost.ts
|
|
158
|
+
hash.ts
|
|
159
|
+
json.ts
|
|
160
|
+
timeout.ts
|
|
161
|
+
runs/
|
|
162
|
+
.gitkeep
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 7. タスク定義
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
export type ModelTask =
|
|
171
|
+
| "article_brief"
|
|
172
|
+
| "outline"
|
|
173
|
+
| "draft_markdown"
|
|
174
|
+
| "technical_review"
|
|
175
|
+
| "rewrite"
|
|
176
|
+
| "markdown_format"
|
|
177
|
+
| "title_suggestions";
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
タスクを分けることで、モデルごとの得意不得意に応じてルーティングできる。
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 8. 共通リクエスト型
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
export type ModelRequest = {
|
|
188
|
+
task: ModelTask;
|
|
189
|
+
input: string;
|
|
190
|
+
system?: string;
|
|
191
|
+
schemaName?: string;
|
|
192
|
+
maxTokens?: number;
|
|
193
|
+
temperature?: number;
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
MVPでは `priority` は実装しない。
|
|
198
|
+
品質・コスト・速度の切り替えが必要になった時点で、`models.yaml` 側に候補グループを追加してから型へ戻す。
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 9. 共通レスポンス型
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
export type ModelResponse = {
|
|
206
|
+
provider: string;
|
|
207
|
+
model: string;
|
|
208
|
+
text: string;
|
|
209
|
+
usage?: {
|
|
210
|
+
inputTokens?: number;
|
|
211
|
+
outputTokens?: number;
|
|
212
|
+
costUsd?: number;
|
|
213
|
+
};
|
|
214
|
+
elapsedMs: number;
|
|
215
|
+
};
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 10. Providerインターフェース
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
export type ProviderRequest = {
|
|
224
|
+
model: string;
|
|
225
|
+
system?: string;
|
|
226
|
+
input: string;
|
|
227
|
+
temperature?: number;
|
|
228
|
+
maxTokens?: number;
|
|
229
|
+
timeoutMs?: number;
|
|
230
|
+
abortSignal?: AbortSignal;
|
|
231
|
+
responseFormat?: {
|
|
232
|
+
type: "text" | "json_schema";
|
|
233
|
+
schemaName?: string;
|
|
234
|
+
jsonSchema?: unknown;
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export type ProviderResponse = {
|
|
239
|
+
text: string;
|
|
240
|
+
usage?: {
|
|
241
|
+
inputTokens?: number;
|
|
242
|
+
outputTokens?: number;
|
|
243
|
+
costUsd?: number;
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export interface ModelProvider {
|
|
248
|
+
generate(request: ProviderRequest): Promise<ProviderResponse>;
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
ModelRouterは各AIのAPI仕様を直接知らない。
|
|
253
|
+
OpenAI、Claude、Geminiなどの差分はProvider側に閉じ込める。
|
|
254
|
+
|
|
255
|
+
ProviderはSDK固有の例外も正規化する。
|
|
256
|
+
RouterはOpenAI / Anthropicなどの例外型や生レスポンスを直接扱わず、`RouterErrorKind` のような安全な分類だけを見てフォールバック可否を判断する。
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
export type RouterErrorKind =
|
|
260
|
+
| "rate_limit"
|
|
261
|
+
| "timeout"
|
|
262
|
+
| "overloaded"
|
|
263
|
+
| "service_unavailable"
|
|
264
|
+
| "connection"
|
|
265
|
+
| "auth"
|
|
266
|
+
| "billing_quota"
|
|
267
|
+
| "context_length"
|
|
268
|
+
| "schema_validation"
|
|
269
|
+
| "bad_request"
|
|
270
|
+
| "config"
|
|
271
|
+
| "unknown";
|
|
272
|
+
|
|
273
|
+
export class RouterError extends Error {
|
|
274
|
+
constructor(
|
|
275
|
+
message: string,
|
|
276
|
+
public readonly kind: RouterErrorKind,
|
|
277
|
+
public readonly statusCode?: number
|
|
278
|
+
) {
|
|
279
|
+
super(message);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
一部Providerやモデルでは `temperature`、`top_p`、`top_k` などの推論パラメータを受け付けない場合がある。
|
|
285
|
+
Provider側はモデル仕様に応じて未対応パラメータを送らない、または設定エラーとして明示的に扱う。
|
|
286
|
+
|
|
287
|
+
Anthropic APIのように `max_tokens` が必須のProviderでは、`maxTokens` 未指定時にProvider側の安全なデフォルト値を使う。
|
|
288
|
+
|
|
289
|
+
`responseFormat` は将来的にOpenAI / Anthropicなどの構造化出力を使うための拡張点である。MVPではプロンプト + JSON parse + Zod検証でもよいが、Providerインターフェース上は構造化出力へ寄せられる余地を残す。
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 11. models.yaml 設定例
|
|
294
|
+
|
|
295
|
+
```yaml
|
|
296
|
+
providers:
|
|
297
|
+
openai:
|
|
298
|
+
api_key_env: OPENAI_API_KEY_ARTICLE
|
|
299
|
+
anthropic:
|
|
300
|
+
api_key_env: ANTHROPIC_API_KEY_ARTICLE
|
|
301
|
+
|
|
302
|
+
prices:
|
|
303
|
+
openai:
|
|
304
|
+
gpt-5.5:
|
|
305
|
+
input_usd_per_1m_tokens: 0
|
|
306
|
+
output_usd_per_1m_tokens: 0
|
|
307
|
+
anthropic:
|
|
308
|
+
claude-opus:
|
|
309
|
+
input_usd_per_1m_tokens: 0
|
|
310
|
+
output_usd_per_1m_tokens: 0
|
|
311
|
+
|
|
312
|
+
defaults:
|
|
313
|
+
timeout_ms: 120000
|
|
314
|
+
|
|
315
|
+
tasks:
|
|
316
|
+
article_brief:
|
|
317
|
+
primary:
|
|
318
|
+
provider: openai
|
|
319
|
+
model: gpt-5.5
|
|
320
|
+
fallback:
|
|
321
|
+
- provider: anthropic
|
|
322
|
+
model: claude-opus
|
|
323
|
+
- provider: gemini
|
|
324
|
+
model: gemini-pro
|
|
325
|
+
temperature: 0.4
|
|
326
|
+
max_tokens: 4000
|
|
327
|
+
|
|
328
|
+
outline:
|
|
329
|
+
primary:
|
|
330
|
+
provider: anthropic
|
|
331
|
+
model: claude-opus
|
|
332
|
+
fallback:
|
|
333
|
+
- provider: openai
|
|
334
|
+
model: gpt-5.5
|
|
335
|
+
- provider: gemini
|
|
336
|
+
model: gemini-pro
|
|
337
|
+
temperature: 0.4
|
|
338
|
+
max_tokens: 4000
|
|
339
|
+
|
|
340
|
+
draft_markdown:
|
|
341
|
+
primary:
|
|
342
|
+
provider: openai
|
|
343
|
+
model: gpt-5.5
|
|
344
|
+
fallback:
|
|
345
|
+
- provider: anthropic
|
|
346
|
+
model: claude-sonnet
|
|
347
|
+
- provider: local
|
|
348
|
+
model: qwen-local
|
|
349
|
+
temperature: 0.6
|
|
350
|
+
max_tokens: 12000
|
|
351
|
+
timeout_ms: 180000
|
|
352
|
+
|
|
353
|
+
technical_review:
|
|
354
|
+
primary:
|
|
355
|
+
provider: anthropic
|
|
356
|
+
model: claude-opus
|
|
357
|
+
fallback:
|
|
358
|
+
- provider: openai
|
|
359
|
+
model: gpt-5.5
|
|
360
|
+
- provider: gemini
|
|
361
|
+
model: gemini-pro
|
|
362
|
+
temperature: 0.2
|
|
363
|
+
max_tokens: 6000
|
|
364
|
+
|
|
365
|
+
markdown_format:
|
|
366
|
+
primary:
|
|
367
|
+
provider: local
|
|
368
|
+
model: qwen-local
|
|
369
|
+
fallback:
|
|
370
|
+
- provider: openai
|
|
371
|
+
model: mini
|
|
372
|
+
temperature: 0.2
|
|
373
|
+
max_tokens: 8000
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
実際のモデル名は利用時点のAPIで確認して設定する。
|
|
377
|
+
`prices` はコスト概算用の任意設定であり、未設定または `0` の場合は `costUsd` を出さないか、ベストエフォートの概算として扱う。
|
|
378
|
+
単価は頻繁に変わるため、コードに固定せず設定ファイル側で管理する。
|
|
379
|
+
|
|
380
|
+
`providers.*.api_key_env` が指定されている場合はそのenv名を優先する。
|
|
381
|
+
未指定時は `OPENAI_API_KEY`、`ANTHROPIC_API_KEY`、`GEMINI_API_KEY` のようなProvider標準名を使う。
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 12. ModelRouterの中核処理
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
export class ModelRouter {
|
|
389
|
+
constructor(
|
|
390
|
+
private providers: Record<string, ModelProvider>,
|
|
391
|
+
private config: RouterConfig,
|
|
392
|
+
private logger: RunLogger
|
|
393
|
+
) {}
|
|
394
|
+
|
|
395
|
+
async run(request: ModelRequest): Promise<ModelResponse> {
|
|
396
|
+
const taskConfig = this.config.tasks[request.task];
|
|
397
|
+
|
|
398
|
+
const candidates = [
|
|
399
|
+
taskConfig.primary,
|
|
400
|
+
...(taskConfig.fallback ?? []),
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
let lastError: unknown;
|
|
404
|
+
|
|
405
|
+
for (const candidate of candidates) {
|
|
406
|
+
const provider = this.providers[candidate.provider];
|
|
407
|
+
|
|
408
|
+
if (!provider) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const startedAt = Date.now();
|
|
414
|
+
|
|
415
|
+
const response = await provider.generate({
|
|
416
|
+
model: candidate.model,
|
|
417
|
+
input: request.input,
|
|
418
|
+
system: request.system,
|
|
419
|
+
temperature: request.temperature ?? taskConfig.temperature,
|
|
420
|
+
maxTokens: request.maxTokens ?? taskConfig.max_tokens,
|
|
421
|
+
timeoutMs: taskConfig.timeout_ms ?? this.config.defaults.timeout_ms,
|
|
422
|
+
responseFormat: resolveResponseFormat(request.schemaName),
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const result: ModelResponse = {
|
|
426
|
+
provider: candidate.provider,
|
|
427
|
+
model: candidate.model,
|
|
428
|
+
text: response.text,
|
|
429
|
+
usage: response.usage,
|
|
430
|
+
elapsedMs: Date.now() - startedAt,
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const validated = await this.validateAndMaybeRepair(
|
|
434
|
+
request,
|
|
435
|
+
candidate,
|
|
436
|
+
result
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
await this.logger.logSuccess(request, validated);
|
|
440
|
+
return validated;
|
|
441
|
+
} catch (error) {
|
|
442
|
+
const normalized = normalizeProviderError(error);
|
|
443
|
+
lastError = normalized;
|
|
444
|
+
await this.logger.logFailure(request, candidate, normalized);
|
|
445
|
+
|
|
446
|
+
if (!this.shouldFallback(normalized.kind)) {
|
|
447
|
+
throw normalized;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
throw new Error(`All model candidates failed: ${String(lastError)}`);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private shouldFallback(kind: RouterErrorKind): boolean {
|
|
456
|
+
return [
|
|
457
|
+
"rate_limit",
|
|
458
|
+
"timeout",
|
|
459
|
+
"overloaded",
|
|
460
|
+
"service_unavailable",
|
|
461
|
+
"connection",
|
|
462
|
+
"schema_validation",
|
|
463
|
+
].includes(kind);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
`normalizeProviderError()` は文字列マッチではなく、Provider側でSDKの型付き例外、HTTP status、エラーコードを見て分類する。
|
|
469
|
+
ログには正規化済みの `kind`、安全な短い `message`、status code程度だけを保存し、生のSDK例外やヘッダを丸ごと保存しない。
|
|
470
|
+
|
|
471
|
+
SDK内リトライとRouterのフォールバックは役割を分ける。
|
|
472
|
+
MVPではSDKの標準リトライをProvider内リトライ、候補モデルの切り替えをRouterの責務とする。必要ならProvider初期化時にSDKリトライ回数を明示する。
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## 13. フォールバック方針
|
|
477
|
+
|
|
478
|
+
### 13.1 フォールバックするエラー
|
|
479
|
+
|
|
480
|
+
- rate limit
|
|
481
|
+
- timeout
|
|
482
|
+
- overloaded
|
|
483
|
+
- 5xx
|
|
484
|
+
- temporary unavailable
|
|
485
|
+
- API connection error
|
|
486
|
+
- 出力JSONのparse / Zod検証失敗後、同一candidateでの修復も失敗した場合
|
|
487
|
+
|
|
488
|
+
### 13.2 フォールバックしないエラー
|
|
489
|
+
|
|
490
|
+
- APIキー未設定
|
|
491
|
+
- APIキー不正
|
|
492
|
+
- 認証エラー
|
|
493
|
+
- 入力が長すぎる
|
|
494
|
+
- schemaName未定義・不正などの設定ミス
|
|
495
|
+
- 禁止された内容
|
|
496
|
+
- 課金枠不足
|
|
497
|
+
- 料金上限超過
|
|
498
|
+
- 設定ミス
|
|
499
|
+
|
|
500
|
+
フォールバックしないエラーまで別モデルに投げると、無駄な課金や意図しない情報送信が起きる。
|
|
501
|
+
|
|
502
|
+
`quota` という文字列だけでは判定しない。
|
|
503
|
+
rate limitのような一時的な制限はフォールバック対象だが、課金枠不足、支払い上限、プロジェクト上限などのbilling quota系エラーはフォールバックしない。
|
|
504
|
+
|
|
505
|
+
スキーマ系エラーは2種類に分ける。
|
|
506
|
+
`schemaName` が未定義・不正など、設定側が間違っている場合は `config` として即終了し、フォールバックしない。AIの出力がJSON parseまたはZod検証に失敗し、同一candidateでの修復1回にも失敗した場合は `schema_validation` として次candidateへ進む。
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## 14. Qiita記事作成ワークフロー
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
async function createQiitaArticle(topic: string) {
|
|
514
|
+
const brief = await router.run({
|
|
515
|
+
task: "article_brief",
|
|
516
|
+
input: `
|
|
517
|
+
次のテーマでQiita記事のArticle Briefを作成してください。
|
|
518
|
+
|
|
519
|
+
テーマ:
|
|
520
|
+
${topic}
|
|
521
|
+
|
|
522
|
+
出力はJSON形式。
|
|
523
|
+
`,
|
|
524
|
+
schemaName: "ArticleBrief",
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
await store.save("brief.json", brief.text);
|
|
528
|
+
|
|
529
|
+
const outline = await router.run({
|
|
530
|
+
task: "outline",
|
|
531
|
+
input: `
|
|
532
|
+
次のArticle BriefからQiita記事の構成を作ってください。
|
|
533
|
+
|
|
534
|
+
${brief.text}
|
|
535
|
+
`,
|
|
536
|
+
schemaName: "ArticleOutline",
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
await store.save("outline.json", outline.text);
|
|
540
|
+
|
|
541
|
+
const draft = await router.run({
|
|
542
|
+
task: "draft_markdown",
|
|
543
|
+
input: `
|
|
544
|
+
次の構成からQiita向けMarkdown本文を書いてください。
|
|
545
|
+
|
|
546
|
+
${outline.text}
|
|
547
|
+
`,
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
await store.save("draft.md", draft.text);
|
|
551
|
+
|
|
552
|
+
const review = await router.run({
|
|
553
|
+
task: "technical_review",
|
|
554
|
+
input: `
|
|
555
|
+
次のQiita記事を技術レビューしてください。
|
|
556
|
+
問題点、改善案、修正すべき箇所をJSONで返してください。
|
|
557
|
+
|
|
558
|
+
${draft.text}
|
|
559
|
+
`,
|
|
560
|
+
schemaName: "ReviewResult",
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
await store.save("review.json", review.text);
|
|
564
|
+
|
|
565
|
+
const final = await router.run({
|
|
566
|
+
task: "rewrite",
|
|
567
|
+
input: `
|
|
568
|
+
次のレビューを反映して、Qiita記事を改善してください。
|
|
569
|
+
|
|
570
|
+
記事:
|
|
571
|
+
${draft.text}
|
|
572
|
+
|
|
573
|
+
レビュー:
|
|
574
|
+
${review.text}
|
|
575
|
+
`,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
await store.save("final.md", final.text);
|
|
579
|
+
|
|
580
|
+
return final.text;
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## 15. 状態保存
|
|
587
|
+
|
|
588
|
+
制限やエラーで途中停止しても再開できるように、各工程の成果物を保存する。
|
|
589
|
+
|
|
590
|
+
```json
|
|
591
|
+
{
|
|
592
|
+
"runId": "2026-06-16-ai-ir-article",
|
|
593
|
+
"topic": "AI向けIRを設計する",
|
|
594
|
+
"steps": {
|
|
595
|
+
"brief": {
|
|
596
|
+
"status": "done",
|
|
597
|
+
"file": "brief.json"
|
|
598
|
+
},
|
|
599
|
+
"outline": {
|
|
600
|
+
"status": "done",
|
|
601
|
+
"file": "outline.json"
|
|
602
|
+
},
|
|
603
|
+
"draft": {
|
|
604
|
+
"status": "done",
|
|
605
|
+
"file": "draft.md"
|
|
606
|
+
},
|
|
607
|
+
"review": {
|
|
608
|
+
"status": "pending"
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
再開コマンド例:
|
|
615
|
+
|
|
616
|
+
```bash
|
|
617
|
+
npm run article:resume -- --run 2026-06-16-ai-ir-article
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## 16. 出力スキーマ検証
|
|
623
|
+
|
|
624
|
+
Zodで中間成果物を検証する。
|
|
625
|
+
`schemaName` が指定された場合の検証責務は `ModelRouter.run()` に置く。
|
|
626
|
+
ワークフロー層は検証済みの `ModelResponse.text` だけを保存し、未検証JSONを次工程へ渡さない。
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
import { z } from "zod";
|
|
630
|
+
|
|
631
|
+
export const ArticleBriefSchema = z.object({
|
|
632
|
+
title: z.string(),
|
|
633
|
+
targetReaders: z.array(z.string()),
|
|
634
|
+
goal: z.array(z.string()),
|
|
635
|
+
mainClaim: z.string(),
|
|
636
|
+
sections: z.array(
|
|
637
|
+
z.object({
|
|
638
|
+
heading: z.string(),
|
|
639
|
+
points: z.array(z.string()),
|
|
640
|
+
})
|
|
641
|
+
),
|
|
642
|
+
codeExamples: z.array(
|
|
643
|
+
z.object({
|
|
644
|
+
language: z.string(),
|
|
645
|
+
purpose: z.string(),
|
|
646
|
+
})
|
|
647
|
+
),
|
|
648
|
+
});
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
処理方針:
|
|
652
|
+
|
|
653
|
+
```text
|
|
654
|
+
AI出力
|
|
655
|
+
↓
|
|
656
|
+
JSON parse
|
|
657
|
+
↓
|
|
658
|
+
Zod検証
|
|
659
|
+
├─ OK → 次の工程
|
|
660
|
+
└─ NG → 同一candidateへ修復依頼(最大1回)
|
|
661
|
+
├─ OK → 次の工程
|
|
662
|
+
└─ NG → fallback model
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
修復依頼は無制限に繰り返さない。
|
|
666
|
+
MVPでは各candidateにつき最大1回までとし、修復失敗時は `schema_validation` として正規化する。次のcandidateへ進むかどうかは設定で制御してもよいが、初期実装では「同一candidateの修復1回、それでも失敗したらfallback候補へ1回ずつ進む」方針にする。
|
|
667
|
+
|
|
668
|
+
ここでいう `schema_validation` は「AI出力が期待スキーマに合わない」ことを指す。
|
|
669
|
+
`schemaName` がregistryに存在しない、schema定義自体が壊れている、といった設定ミスは `config` として扱い、修復依頼もフォールバックも行わない。
|
|
670
|
+
|
|
671
|
+
OpenAI / AnthropicなどProvider側でJSON schema形式の構造化出力を使える場合は、`ProviderRequest.responseFormat` にschema情報を渡す。
|
|
672
|
+
ただしMVPではProviderごとの差分を小さくするため、構造化出力は必須ではなく、プロンプト + JSON parse + Zod検証を基準実装とする。
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
676
|
+
## 17. ログ設計
|
|
677
|
+
|
|
678
|
+
### 17.1 基本方針
|
|
679
|
+
|
|
680
|
+
ログには全文プロンプトを保存しない。
|
|
681
|
+
成果物は `runs/` 配下に保存するが、ログはメタ情報中心にする。
|
|
682
|
+
|
|
683
|
+
```json
|
|
684
|
+
{
|
|
685
|
+
"task": "draft_markdown",
|
|
686
|
+
"provider": "openai",
|
|
687
|
+
"model": "gpt-5.5",
|
|
688
|
+
"status": "success",
|
|
689
|
+
"input_hash": "sha256:xxxx",
|
|
690
|
+
"elapsed_ms": 12000,
|
|
691
|
+
"input_tokens": 5000,
|
|
692
|
+
"output_tokens": 3000,
|
|
693
|
+
"cost_usd": 0.12
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### 17.2 保存しないもの
|
|
698
|
+
|
|
699
|
+
- APIキー
|
|
700
|
+
- `.env` の内容
|
|
701
|
+
- 認証トークン
|
|
702
|
+
- 外部サービスの秘密情報
|
|
703
|
+
- 機密性の高い入力本文
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## 18. セキュリティ設計
|
|
708
|
+
|
|
709
|
+
### 18.1 外部公開しない
|
|
710
|
+
|
|
711
|
+
MVPではCLIのみ。
|
|
712
|
+
HTTPサーバー化しない。
|
|
713
|
+
管理画面を作らない。
|
|
714
|
+
|
|
715
|
+
### 18.2 APIキー管理
|
|
716
|
+
|
|
717
|
+
`.env` に保存し、Git管理しない。
|
|
718
|
+
|
|
719
|
+
```env
|
|
720
|
+
OPENAI_API_KEY=...
|
|
721
|
+
ANTHROPIC_API_KEY=...
|
|
722
|
+
GEMINI_API_KEY=...
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
`.env.example` のみコミットする。
|
|
726
|
+
|
|
727
|
+
### 18.3 権限分離
|
|
728
|
+
|
|
729
|
+
可能であれば用途ごとにAPIキーを分ける。
|
|
730
|
+
|
|
731
|
+
```env
|
|
732
|
+
OPENAI_API_KEY_ARTICLE=...
|
|
733
|
+
OPENAI_API_KEY_EXPERIMENT=...
|
|
734
|
+
ANTHROPIC_API_KEY_ARTICLE=...
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### 18.4 任意コード実行を入れない
|
|
738
|
+
|
|
739
|
+
ModelRouter本体に任意シェル実行機能を入れない。
|
|
740
|
+
コード検証が必要な場合は、別プロセス・別サンドボックスで行う。
|
|
741
|
+
|
|
742
|
+
### 18.5 外部URL取得を勝手にしない
|
|
743
|
+
|
|
744
|
+
外部URL取得機能はMVPでは持たない。
|
|
745
|
+
将来的に入れる場合は許可ドメイン制にする。
|
|
746
|
+
|
|
747
|
+
---
|
|
748
|
+
|
|
749
|
+
## 19. CLI仕様案
|
|
750
|
+
|
|
751
|
+
コマンドは Qiita 専用ではないため `article:*` を正式名とする。設定(`config/`)・`.env`・`runs/` はすべて**カレントディレクトリ相対**で解決する。
|
|
752
|
+
|
|
753
|
+
### 19.0 init(設定雛形の展開)
|
|
754
|
+
|
|
755
|
+
グローバル導入時、作業ディレクトリに設定が無いと動かないため、同梱テンプレを展開する。
|
|
756
|
+
|
|
757
|
+
```bash
|
|
758
|
+
llm-task-router init # config/ と .env.example を cwd へコピー
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
- コピー対象: 同梱の `config/`(models.yaml・profiles/・criteria/)と `.env.example`。コピー元は bin 位置からパッケージroot相対に解決。
|
|
762
|
+
- 書き込み先は **cwd 配下の固定パス**(任意パスは受け取らない)。既存ファイルは `--force` 無しでは上書きしない。`.env` は生成しない(`.env.example` のみ)。
|
|
763
|
+
- 展開後の流れ: `cp .env.example .env` → キー設定 → `config/models.yaml` のモデルID調整 → `article:create`。
|
|
764
|
+
|
|
765
|
+
### 19.1 新規記事作成
|
|
766
|
+
|
|
767
|
+
短いテーマはインラインで指定する。
|
|
768
|
+
|
|
769
|
+
```bash
|
|
770
|
+
npm run article:create -- --topic "AIが解釈しやすい中間言語を設計する"
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
長文の指示(対象読者・論点・制約など)はテキストファイルで渡す。
|
|
774
|
+
|
|
775
|
+
```bash
|
|
776
|
+
npm run article:create -- --topic-file topics/ai-ir.txt
|
|
777
|
+
```
|
|
778
|
+
|
|
779
|
+
- `--topic` と `--topic-file` のどちらかを指定する(両方指定時はエラー)。
|
|
780
|
+
- `--topic-file` のときの `runId` はファイル名ベースで生成する(例: `ai-ir.txt` → `2026-06-16-ai-ir`)。
|
|
781
|
+
- `--run <runId>` で `runId` を明示固定できる。
|
|
782
|
+
- `--profile <name>`(既定 `qiita`)で `config/profiles/<name>.yaml` のプロファイルを選ぶ。プロファイルは `platform`(プロンプトのラベル)、`style`(admonition記法・front-matter作法などをdraft/final/reviseの本文生成に注入)、`language` を持つ。同梱は `qiita` / `zenn` / `blog`。`--platform <name>` はラベルのみ上書き。解決した `platform` と `style` は `meta.json` に保存され、resume/review/revise/evaluate が自動継承する。
|
|
783
|
+
- プラットフォーム別の評価は対応する `--criteria-file` を併用する。プロファイルは「作法(コードを増やさず外部YAMLで管理)」、criteria は「評価観点」を担い、models.yaml(モデル選択)と合わせて設定3点で多目的化する。
|
|
784
|
+
- 段階的拡張:プラットフォーム固有の記法変換や `markdown_format` タスクの整形ステップ昇格は、必要になった時点で追加する(現状は未使用)。
|
|
785
|
+
|
|
786
|
+
### 19.2 途中再開
|
|
787
|
+
|
|
788
|
+
```bash
|
|
789
|
+
npm run article:resume -- --run 2026-06-16-ai-ir-article
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### 19.3 レビューのみ再実行
|
|
793
|
+
|
|
794
|
+
draft.md から自動レビュー → rewrite をやり直す(利用者の指示は使わない)。
|
|
795
|
+
|
|
796
|
+
```bash
|
|
797
|
+
npm run article:review -- --run 2026-06-16-ai-ir-article
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### 19.4 final.md への修正指示
|
|
801
|
+
|
|
802
|
+
現在の `final.md` に、利用者の自由な修正指示を反映して書き直す。
|
|
803
|
+
|
|
804
|
+
```bash
|
|
805
|
+
npm run article:revise -- --run 2026-06-16-ai-ir-article --instruction "前半を簡潔に。専門用語は初出で1行説明"
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
長文の指示はファイルでも渡せる。
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
npm run article:revise -- --run 2026-06-16-ai-ir-article --instruction-file work/revise.md
|
|
812
|
+
```
|
|
813
|
+
|
|
814
|
+
- `--instruction` と `--instruction-file` のどちらか必須(両方指定時はエラー)。
|
|
815
|
+
- `rewrite` タスクで現在の `final.md` + 指示を処理し、`final.md` を上書きする。
|
|
816
|
+
- 上書き前の版は `final.bak.md` に退避する。繰り返し実行可能。
|
|
817
|
+
|
|
818
|
+
### 19.5 final.md の評価と修正指示の生成
|
|
819
|
+
|
|
820
|
+
現在の `final.md` を評価し、結果を `final-review.json` に保存する。評価で見つかった指摘から修正指示ファイル `revise-instruction.md` を生成する(自動でrewriteはしない)。
|
|
821
|
+
|
|
822
|
+
```bash
|
|
823
|
+
npm run article:evaluate -- --run 2026-06-16-ai-ir-article --min-severity major
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
評価観点は run の profile から自動解決される(指定不要)。一回限り別観点で見たいときだけ明示する。
|
|
827
|
+
|
|
828
|
+
```bash
|
|
829
|
+
# 観点は profile(meta.json)→ criteria_file から自動解決
|
|
830
|
+
npm run article:evaluate -- --run 2026-06-16-ai-ir-article
|
|
831
|
+
# 一回だけ別観点にしたいとき(インライン / ファイルで上書き)
|
|
832
|
+
npm run article:evaluate -- --run 2026-06-16-ai-ir-article --criteria "正確性とコード例の動作を重視"
|
|
833
|
+
npm run article:evaluate -- --run 2026-06-16-ai-ir-article --criteria-file config/criteria/note.md
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
- 審査役は本文の書き手と**別系統のモデル**にする(`models.yaml` の `final_review` タスク。既定は anthropic 主審査、本文の `rewrite` は openai 主体)。
|
|
837
|
+
- `--min-severity`(`critical|major|minor|suggestion`、既定 `suggestion`=全件)で指示に含める指摘を絞る。
|
|
838
|
+
- 出力は `runs/<runId>/` 固定で3つ:`final-review.json`(生スコアカード)、`final-review.md`(人が読むサマリ=判定・severity別件数・全指摘)、`revise-instruction.md`(`--min-severity` で絞った修正指示)。
|
|
839
|
+
- `final-review.md` は severityフィルタを掛けず全指摘を含める(人の確認用)。`revise-instruction.md` のみフィルタ済み(rewriteへの入力用)。
|
|
840
|
+
- 評価結果からの指示生成は**ローカル整形で追加APIコール無し**(評価1回のみ課金)。
|
|
841
|
+
- 生成された `revise-instruction.md` は草案。人が確認・編集してから `article:revise --instruction-file` で適用する(自動適用はしない)。
|
|
842
|
+
- 指定 severity 以上の指摘が無い場合は `revise-instruction.md` を作らない。
|
|
843
|
+
- 評価観点は `config/criteria/*.md` に置き、profile の `criteria_file` で対象に紐づける。`evaluate` は run の profile(`meta.json`)から観点を**自動解決**する。解決順は `--criteria` > `--criteria-file` > profile の `criteria_file` > なし。共通デフォルトは `config/criteria/default.md`(qiita/zenn/blog)、note は読み物重視の `config/criteria/note.md`。観点を対象ごとに固定することで LLM-as-judge の揺れを抑え、評価を比較可能にする。
|
|
844
|
+
|
|
845
|
+
連携フロー:
|
|
846
|
+
|
|
847
|
+
```bash
|
|
848
|
+
npm run article:evaluate -- --run <runId> --min-severity major
|
|
849
|
+
# revise-instruction.md を確認・編集
|
|
850
|
+
npm run article:revise -- --run <runId> --instruction-file runs/<runId>/revise-instruction.md
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### 19.6 最終記事のエクスポート
|
|
854
|
+
|
|
855
|
+
完成した `final.md` を、指定したパスへ書き出す。
|
|
856
|
+
|
|
857
|
+
```bash
|
|
858
|
+
npm run article:export -- --run <runId> --out ../zenn-content/articles/my-article.md
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
- 対象は **`final.md` のみ**(中間成果物は出さない)。
|
|
862
|
+
- ガード: 出力先が `.env` 等の秘密ファイル名なら拒否、既存ファイルは `--force` 無しでは上書きしない、ワークスペース外は警告(拒否しない)。親ディレクトリは自動作成。
|
|
863
|
+
- これは「CLI引数から任意の書き込み先を受け取らない」原則の**明示的な例外**。内部成果物は `runs/<runId>/` に閉じたままで、ユーザーが明示した出力だけを許可する(モデル設定不要・API不要のファイル操作)。
|
|
864
|
+
|
|
865
|
+
### 19.7 実行推移の表示
|
|
866
|
+
|
|
867
|
+
全コマンドは工程の進捗を **stderr** に出力する。`runId` と `final` パスは **stdout** に出すため、スクリプトでの解析と進捗表示が混ざらない。
|
|
868
|
+
|
|
869
|
+
```text
|
|
870
|
+
[1/5] brief (article_brief) ...
|
|
871
|
+
[1/5] brief - done via openai/gpt-5.4 (2310ms, ~$0.0123)
|
|
872
|
+
[2/5] outline (outline) ...
|
|
873
|
+
[2/5] outline - done via anthropic/claude-opus-4-8 (4120ms, ~$0.0456)
|
|
874
|
+
...
|
|
875
|
+
total: ~$0.1240 (estimate)
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
- 各工程の **使用provider/model・所要時間・概算コスト** を表示し、最後に **run合計** を出す。設定上のprimaryと異なるproviderが表示された場合はフォールバックが起きたと判断できる。
|
|
879
|
+
- `article:resume` / `article:review` では完了済み工程を `skip (done)` と表示する。
|
|
880
|
+
- 進捗は補助情報であり、機械処理が必要な値(`runId` など)は stdout 側に出す。
|
|
881
|
+
|
|
882
|
+
#### 出力ガード
|
|
883
|
+
|
|
884
|
+
本文(スキーマ無し)工程には保存前の軽量ガードを設ける。
|
|
885
|
+
|
|
886
|
+
- **truncation検知**: Provider応答が `max_tokens` / `max_output_tokens` で打ち切られた場合、その工程に警告を表示する(`models.yaml` の `max_tokens` を増やす目安になる)。
|
|
887
|
+
- **全体コードフェンス除去**: モデルが本文全体を ` ``` ` で囲んで返した場合のみ、外側フェンスを剥がして保存する。文中の正当なコードブロックや、複数コードブロックを含む本文には手を加えない。
|
|
888
|
+
- **ラップ文検知(警告のみ)**: 本文が見出し(#)で始まらない(前置きの疑い)、または末尾が追加提案・問いかけ(『…で出し直せます』等)になっている場合に警告する。自然文は正当な導入/結論と区別しにくいため**自動削除はせず**、修正は人に委ねる(プロンプト硬化が主防御、本ガードは回帰検知の安全網)。
|
|
889
|
+
- スキーマ工程(`brief`/`outline`/`review`)は検証済みJSONを保存するためガード対象外。
|
|
890
|
+
|
|
891
|
+
#### コスト概算について
|
|
892
|
+
|
|
893
|
+
- コストはレスポンス同梱の `usage`(トークン数)× `models.yaml` の `prices`(USD/1Mトークン)による**ローカル概算**で、表示のための追加API(count_tokens等)は呼ばない。
|
|
894
|
+
- `prices` 未設定または `0` のモデルはコストを出さない(`total` も加算されない)。
|
|
895
|
+
- 単価は価格改定でドリフトするため設定ファイルで管理する。プロンプトキャッシュ等の割引は概算に含めない。
|
|
896
|
+
- 修復(schema検証失敗時の再生成)が走った場合、検証失敗した初回コール分のトークンは概算に含まれない(やや過小評価)。
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
## 20. MVPスコープ
|
|
901
|
+
|
|
902
|
+
### 20.1 実装する
|
|
903
|
+
|
|
904
|
+
- TypeScript CLI
|
|
905
|
+
- models.yaml読み込み
|
|
906
|
+
- OpenAIProvider
|
|
907
|
+
- AnthropicProvider
|
|
908
|
+
- タスク別モデル選択
|
|
909
|
+
- フォールバック
|
|
910
|
+
- Zod検証
|
|
911
|
+
- ファイル保存
|
|
912
|
+
- JSONメタログ
|
|
913
|
+
- Qiita記事作成ワークフロー
|
|
914
|
+
|
|
915
|
+
### 20.2 後回し
|
|
916
|
+
|
|
917
|
+
- GeminiProvider
|
|
918
|
+
- LocalProvider
|
|
919
|
+
- Web UI
|
|
920
|
+
- Qiita API投稿
|
|
921
|
+
- GitHub連携
|
|
922
|
+
- LangGraph連携
|
|
923
|
+
- Dify連携
|
|
924
|
+
- 複雑なコスト最適化
|
|
925
|
+
|
|
926
|
+
---
|
|
927
|
+
|
|
928
|
+
## 21. Codex と Claude Code のどちらで実装するか
|
|
929
|
+
|
|
930
|
+
### 21.1 結論
|
|
931
|
+
|
|
932
|
+
このMVPは **Codex を第一候補** にする。
|
|
933
|
+
|
|
934
|
+
理由:
|
|
935
|
+
|
|
936
|
+
- TypeScriptのCLIプロジェクトを段階的に作る用途に合う
|
|
937
|
+
- 設計書からファイル構成、テスト、リファクタリングまで進めやすい
|
|
938
|
+
- OpenAI API連携やStructured Outputs周辺との相性が良い
|
|
939
|
+
- 今回の設計では「仕様通りに薄く作る」ことが重要で、Codexの明示的な実装・レビュー・修正の流れと相性が良い
|
|
940
|
+
|
|
941
|
+
Claude Code は第二候補として使う。
|
|
942
|
+
|
|
943
|
+
Claude Codeが向いている場面:
|
|
944
|
+
|
|
945
|
+
- 既存コードベースを広く読ませたい
|
|
946
|
+
- 長い設計書や仕様書を踏まえて改善させたい
|
|
947
|
+
- CLIで対話しながら一気に実装したい
|
|
948
|
+
- 文章、README、Qiita本文、設計意図の整理も同時に進めたい
|
|
949
|
+
|
|
950
|
+
### 21.2 おすすめ分担
|
|
951
|
+
|
|
952
|
+
| 作業 | 推奨 |
|
|
953
|
+
|---|---|
|
|
954
|
+
| 初期プロジェクト作成 | Codex |
|
|
955
|
+
| 型定義・Provider実装 | Codex |
|
|
956
|
+
| テスト追加 | Codex |
|
|
957
|
+
| README整備 | Claude Code |
|
|
958
|
+
| 設計書レビュー | Claude Code |
|
|
959
|
+
| Qiita記事化 | Claude Code または GPT |
|
|
960
|
+
| セキュリティ観点レビュー | Claude Code + 人間レビュー |
|
|
961
|
+
|
|
962
|
+
### 21.3 実務上の最適解
|
|
963
|
+
|
|
964
|
+
どちらか一方に固定しない。
|
|
965
|
+
最初はCodexでMVPを作り、その後Claude Codeでレビュー・README・改善提案を行う。
|
|
966
|
+
|
|
967
|
+
```text
|
|
968
|
+
Codex:
|
|
969
|
+
仕様通りに実装する担当
|
|
970
|
+
|
|
971
|
+
Claude Code:
|
|
972
|
+
仕様の穴を探す、説明を整える、改善案を出す担当
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
## 22. Codex向け初回プロンプト例
|
|
978
|
+
|
|
979
|
+
```text
|
|
980
|
+
このリポジトリに、TypeScript製の薄いModelRouter CLIを実装してください。
|
|
981
|
+
|
|
982
|
+
目的:
|
|
983
|
+
- Qiita記事作成フローでOpenAI/Anthropicをタスク別に呼び分ける
|
|
984
|
+
- LiteLLMのような高機能プロキシにはしない
|
|
985
|
+
- 外部公開API、Web UI、任意コード実行は実装しない
|
|
986
|
+
|
|
987
|
+
実装範囲:
|
|
988
|
+
- src/router/ModelRouter.ts
|
|
989
|
+
- src/router/config.ts
|
|
990
|
+
- src/router/errors.ts
|
|
991
|
+
- src/router/types.ts
|
|
992
|
+
- src/providers/ModelProvider.ts
|
|
993
|
+
- src/providers/OpenAIProvider.ts
|
|
994
|
+
- src/providers/AnthropicProvider.ts
|
|
995
|
+
- src/storage/RunStore.ts
|
|
996
|
+
- src/logger/RunLogger.ts
|
|
997
|
+
- src/workflows/createQiitaArticle.ts
|
|
998
|
+
- config/models.yaml
|
|
999
|
+
- .env.example
|
|
1000
|
+
- README.md
|
|
1001
|
+
|
|
1002
|
+
要件:
|
|
1003
|
+
- models.yamlでタスク別primary/fallbackを定義
|
|
1004
|
+
- rate limit / timeout / overloaded / 一時的な5xx の場合のみfallback
|
|
1005
|
+
- 認証エラー、入力過大、schemaName不正、課金枠不足、設定ミスではfallbackしない
|
|
1006
|
+
- AI出力のJSON parse / Zod検証失敗は同一candidateで最大1回修復し、失敗したらschema_validationとして次candidateへ進める
|
|
1007
|
+
- SDK固有例外はProvider側で正規化し、Routerは文字列マッチでfallback判定しない
|
|
1008
|
+
- runs/<runId>/ に brief.json, outline.json, draft.md, review.json, final.md, meta.json を保存
|
|
1009
|
+
- ログにAPIキーや全文プロンプトを保存しない
|
|
1010
|
+
- ZodでArticleBriefとReviewResultを検証し、schemaName指定時はRouter内で最大1回だけ修復依頼する
|
|
1011
|
+
- npm scriptで article:create と article:resume を用意する
|
|
1012
|
+
- テストを追加する
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
---
|
|
1016
|
+
|
|
1017
|
+
## 23. Claude Code向けレビュー依頼例
|
|
1018
|
+
|
|
1019
|
+
```text
|
|
1020
|
+
このModelRouter実装をレビューしてください。
|
|
1021
|
+
|
|
1022
|
+
観点:
|
|
1023
|
+
- 設計書に対して過剰実装になっていないか
|
|
1024
|
+
- セキュリティ上危険な機能が入っていないか
|
|
1025
|
+
- APIキーやプロンプト本文がログに漏れないか
|
|
1026
|
+
- fallbackすべきでないエラーをfallbackしていないか
|
|
1027
|
+
- Qiita記事作成フローとして再開可能になっているか
|
|
1028
|
+
- READMEが利用者にとって十分か
|
|
1029
|
+
- TypeScriptの型設計が保守しやすいか
|
|
1030
|
+
|
|
1031
|
+
出力:
|
|
1032
|
+
- 重大な問題
|
|
1033
|
+
- 改善した方がよい点
|
|
1034
|
+
- 後回しでよい点
|
|
1035
|
+
- 具体的な修正案
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
---
|
|
1039
|
+
|
|
1040
|
+
## 24. 今後の拡張候補
|
|
1041
|
+
|
|
1042
|
+
1. GeminiProvider追加
|
|
1043
|
+
2. LocalProvider追加
|
|
1044
|
+
3. Ollama対応
|
|
1045
|
+
4. GitHubへの成果物保存
|
|
1046
|
+
5. Qiita API下書き投稿
|
|
1047
|
+
6. 記事テンプレート切り替え
|
|
1048
|
+
7. コスト上限
|
|
1049
|
+
8. LangGraph連携
|
|
1050
|
+
9. Dify連携
|
|
1051
|
+
10. Web UI
|
|
1052
|
+
|
|
1053
|
+
---
|
|
1054
|
+
|
|
1055
|
+
## 25. まとめ
|
|
1056
|
+
|
|
1057
|
+
このModelRouterは、複数AIを扱うための最小限の制御層である。
|
|
1058
|
+
|
|
1059
|
+
```text
|
|
1060
|
+
薄いModelRouter
|
|
1061
|
+
= タスク別モデル選択
|
|
1062
|
+
+ フォールバック
|
|
1063
|
+
+ 成果物保存
|
|
1064
|
+
+ スキーマ検証
|
|
1065
|
+
+ 最小限ログ
|
|
1066
|
+
```
|
|
1067
|
+
|
|
1068
|
+
MVPでは外部公開やWeb UIを避け、CLIとファイル保存に限定する。
|
|
1069
|
+
実装はCodexを第一候補、Claude Codeをレビュー・ドキュメント改善担当として併用するのが最も現実的である。
|