LLM をクライアントにする — 自然言語で計画を埋める
主任が画面の右下、チャット欄に打ち込む。
「Z 100個、明日中に出したい。鈴木さん金曜半休ね。あと A 社の急ぎ20個忘れずに。」
3秒後、ガントが書き換わり始める。
Z は火曜午前に設備 B、メインに鈴木 (午前のみ)、サポートに高橋。
A 社の急ぎは月曜の朝イチに割り込み、影響を受ける B 社の納期は調整提案として右サイドに出る。
主任はガントを眺めて、「A 社の作業者を佐藤に変更」と打つ。
LLM は 設備割当はそのまま に、メインだけ佐藤に差し替える。
ここに至るまで、主任はマウスのドラッグを一度も使っていない。
第11章までで、ガントエディタの中身はだいたい揃った。
だがユーザーから見ると、ドロップを全部人間がやるのは正直しんどい。100ジョブの週次計画なら100回ドラッグする。
ここで現代の答えは明確だ:
エディタの操作・検証・問い合わせを API として公開 し、LLM をクライアントとして接続する。
人間は自然言語で意図を伝え、LLM が API を叩いて埋める。
Cursor がコード、Claude Code がファイル操作に対してやっている構造を、ガントに対してやる。
この章は、その設計だ。
12.1 まず構造の対応関係
| Cursor / Claude Code | ガントエディタ + LLM |
|---|---|
| エディタ = テキストバッファ | エディタ = 設備レーン + 役割スロット |
操作 = edit_file / bash | 操作 = assign / setSlot / setQty |
検証 = tsc / pytest | 検証 = 部分 CP-SAT (第11章 11.9) |
探索 = grep / read | 探索 = query (空き作業者、KPI) |
改善 = LLM の Edit 提案 | 改善 = LLM が assign を呼ぶ |
| 人間の介入 = Accept/Reject | 人間の介入 = ピン留め / 修正指示 |
Cursor がうまく機能している理由を、そのままガントに持ち込める。鍵は3つ:
- 操作が構造化 API で公開されている (テキスト diff ではなく
assign(j, m, t)) - 検証ループが組み込まれている (型エラー / 整合エラーを LLM が読める)
- 人間が任意のタイミングで割り込める (ピン留めが Accept/Reject に相当)
この3つを揃えれば、LLM クライアントの体験は十分にワークする。
12.2 エディタ API の4分類
LLM クライアントに対して公開する API は、責務で4つに分かれる。
| 種類 | 目的 | 例 |
|---|---|---|
| 操作 (mutate) | 計画を変える | assign, setSlot, setQty, split, pin |
| 問い合わせ (query) | 計画と前提を読む | listJobs, findAvailableWorkers, getKpis |
| 検証 (verify) | 操作の整合性を確かめる | validateOps, previewKpis |
| 改善 (suggest) | 候補を生成させる | suggestForJob, runLnsForScope |
それぞれを JSON-RPC か MCP ツールとして公開する。LLM はこの4種類だけを使って計画を進める。
操作 API
// 配置: ジョブを設備の時刻にドロップ (UI の D&D と同じイベントを発火)
assign(jobId: string, machineId: string, startAt: ISO8601): Result
// 役割スロット: メイン/サポート/検査などを埋める
setSlot(jobId: string, slotId: string, workerId: string): Result
// 数量: タクト × 数量を再計算してバー幅更新
setQty(jobId: string, qty: number): Result
// サブロット分割
split(jobId: string, n: number): Result
// ピン留め: LLM 自身や LNS が触れなくなる
pin(jobId: string): Result
unpin(jobId: string): Result
// 一括操作 (アトミック)
applyBatch(ops: Operation[]): Result // 全部成功するか全部ロールバック
戻り値 Result は 成功/失敗 だけでなく 理由 を返す。LLM がそれを読んで自己修正する。
{
"ok": false,
"reason": "鈴木さんは金曜14:00-17:00に既にJ7(設備B)を担当しています",
"conflicts": [{"workerId": "suzuki", "with": "J7"}],
"suggestions": [
{"action": "assign", "args": {"jobId": "Z", "startAt": "2026-05-21T09:00", "machineId": "B"}}
]
}
問い合わせ API
listJobs(filter?: { unassigned?: boolean; rush?: boolean }): Job[]
listMachines(): Machine[]
listWorkers(at?: TimeRange): Worker[] // 指定時刻に出勤している作業者
findAvailableWorkers(jobId: string, at: TimeRange): WorkerWithScore[]
getJob(jobId: string): JobDetail // 役割スロット、現在の割当、所要時間
getCalendar(workerId: string, range: TimeRange): Shift[]
getKpis(): Kpis // 総遅れ、makespan、切替、残業
getPendingViolations(): Violation[] // 現状のソフト違反
検証 API
// LLM が「これをやろうとしている」操作群を、実行前に評価
validateOps(ops: Operation[]): {
feasible: boolean,
violations: Violation[],
kpisAfter: Kpis,
conflicts: Conflict[],
}
これが生命線。LLM は操作を実行する前にこの API で予測し、ダメなら自己修正してから本物の applyBatch を叩く。Claude Code の「tsc --noEmit で型だけ先に確かめる」のと同じパターン。
改善 API
// LNS をスコープを絞って走らせて、改善候補を返す
runLnsForScope(scope: { jobs?: string[]; machines?: string[]; timeRange?: TimeRange },
timeBudget: ms): Suggestion[]
// 1ジョブの最適配置を CP-SAT で厳密計算
suggestForJob(jobId: string, timeLimit: ms): Suggestion[]
LLM が「Z の置き場所、自分で考えるより数理に聞こう」と判断したときの逃げ道。第10章のサジェスト機能を、LLM 経由でも呼べるようにする。
12.3 LLM のループ構造
LLM クライアントは、以下の 観察 → 計画 → 操作 → 検証 のループを回す。Claude Code の agentic loop と本質的に同じ。
擬似コードで書くと:
async function llmFillPlan(userIntent: string) {
const messages = [system(SYSTEM_PROMPT), user(userIntent)];
while (true) {
const response = await llm.complete(messages, { tools: EDITOR_TOOLS });
if (response.finished) return response.summary;
for (const call of response.toolCalls) {
// 操作系は validate を挟む
if (isMutation(call)) {
const preview = await editor.validateOps([call.args]);
if (!preview.feasible) {
messages.push(toolResult(call, { ok: false, reason: preview.violations }));
continue;
}
}
const result = await editor.invoke(call);
messages.push(toolResult(call, result));
}
}
}
LLM は毎ターン:
- 必要なら問い合わせ API で状況を把握
- 操作を組み立て、
validateOpsで先読み - OK なら
applyBatchで確定、NG なら LLM がエラー文を読んで再計画
12.4 システムプロンプトの骨格
LLM をうまく動かす鍵は、システムプロンプトでドメイン知識と作法を教えること。
あなたは工場の生産計画アシスタントです。
ユーザーの自然言語の指示を、エディタ API の呼び出しに翻訳します。
## あなたが扱う対象
- ジョブ: 製品 × 数量。タクトタイムと役割スロットを持つ
- 設備: 工程ごとに使える設備が決まっている
- 作業者: スキルマトリクスとシフトを持つ
- 役割スロット: メイン1人必須、サポート任意 (短縮効果あり)
## 作業の原則
1. ピン留めされたジョブは触らない
2. 設備の eligibility と作業者のスキルを必ず確認 (`findAvailableWorkers`)
3. 確定前に必ず `validateOps` で検証
4. 急ぎフラグ付きは納期最優先、それ以外は切替最小化を狙う
5. 不確実な解釈は実行せず、ユーザーに1問だけ確認質問する
## 出力規則
- 操作のたびに、なぜそうしたかを1行のコメントで添える
- 「同等の良案がある」場合は3つまで `suggestForJob` を呼んで比較する
## 安全装置
- 1バッチで触るジョブは最大 20 まで (それ以上は分割)
- KPI が悪化する操作は、必ず先にユーザーに確認する
ここに現場固有のルール (「鈴木さんは設備 B 専任」「金曜午後の段取りは前倒し可」)を追記する。これは長期記憶として持っておき、毎セッションで読ませる。
12.5 自律度のグラデーション
LLM × エディタ API の組合せは、自律度を段階的に上げられる。最初から完全自動に振らないのがコツ。
| モード | LLM の自由度 | 人間のレビュー | いつ使う |
|---|---|---|---|
| サジェスト | 提案のみ、適用しない | 1操作ずつ Accept | 立ち上げ期、信頼形成中 |
| ステップ実行 | 1操作ずつ実行、毎回確認 | 操作ごとに Y/N | 重要計画 (出荷直前など) |
| バッチ実行 | 1指示で複数操作、最後にレビュー | バッチ完了時に diff 確認 | 通常運用 |
| 連続実行 | 指示なしに継続、未割当を埋め切る | 完了通知時にレビュー | 通常運用 (慣れた後) |
| 常駐 | 新規受注を検知して自動配置 | 朝の差分レビュー | 完全自動化フェーズ |
Cursor / Claude Code が辿った道と同じ。
「ボタンを押したら全部やる」モードから始めると失敗する。提案 → ステップ → バッチ、と信頼を積み上げてから自動化を上げる。
12.6 LLM の失敗モードと守り
LLM クライアントの怖さは、もっともらしい間違いを量産すること。3層で守る。
守り 1: API レベルで物理的に不可能にする
LLM が assign(jobId, machineId) を呼んだとき、API 側で eligibility をハード弾きする。LLM がプロンプトを無視しても、API が許さない。
function assign(jobId, machineId, startAt) {
const job = getJob(jobId);
if (!job.eligibleMachines.includes(machineId)) {
return { ok: false, reason: `${jobId} は設備 ${machineId} では加工できません`,
eligibleMachines: job.eligibleMachines };
}
// ...
}
守り 2: validateOps で先読みする
「壁にぶつけてからエラー」ではなく「ぶつかる前に予報」。LLM はエラー文を読んで自己修正する。
守り 3: 取り消し可能にする
applyBatch は トランザクション にして、undo(batchId) で完全に巻き戻せる。LLM が変な束で操作しても、人間は1コマンドで戻せる。
const { batchId } = await editor.applyBatch(ops);
// ユーザーが diff を見て「No」と言ったら
await editor.undo(batchId);
守り 4: ピン留めという「触れない印」
ユーザーが手で動かしたものは自動で pin される。LLM はピン留めを 物理的に触れない (API が拒否する)。第10章のピン留め LNS と完全に同じ思想。
12.7 説明可能性 — LLM の理由を保存する
LLM が操作するたびに、理由をプランに紐付けて保存する。これは第10章 10.10 の SchedulingPlan オブジェクトの拡張。
interface Operation {
type: 'assign' | 'setSlot' | ...
args: {...}
reason: string; // LLM が出した理由
llmSessionId: string;
parentOpId?: string; // どの問い合わせに基づいたか
}
これで、
- 「なぜ Z が火曜午前なの?」 → LLM の reason を見せる
- 「あのとき LLM が何を見ていたか」 → llmSessionId のログを辿る
- 「LLM の判断と過去の人間の判断はどれくらい似ているか」 → 比較分析
操作ログは運用が進むほど資産になる。重みの校正データに使える (第10章 10.8 の校正サイクル) し、ファインチューニング素材にもなる。
12.8 アーキテクチャ全体像
ここまでをまとめると、システム全体は4階建てになる。
ポイントは:
- LLM 層と最適化エンジン層は、エディタ API 層を介して対等。どちらが上位というわけではない。LLM は問い合わせ・操作・検証・改善を呼ぶ。LNS も同じ API を呼ぶ
- 永続化層が真実の所在。LLM のセッションが切れても、Undo / Redo / What-if が成立する
- 観測層で LLM の良し悪しを測る — Undo 率が高ければプロンプトが悪い、検証失敗率が高ければシステムプロンプトを足す
12.9 ここまで来ると、いったい何が「最適化」なのか
LLM × エディタ API × 部分 CP-SAT × バックグラウンド LNS、を統合した姿を眺めると、もはや「最適化問題を解いている」という感覚は薄い。むしろ 「人間と LLM と数理ソルバーが、同じガントを共同編集している」。
古典的なスケジューリングは「与えられた問題を一発で解く」だった。
現代的なスケジューリングは 「複数の知能が継続的に共同編集するワークスペース」 で、最適化はその中の一つの参加者にすぎない。
人間が意図を入れ、LLM が文脈を解釈し、CP-SAT が局所整合を保ち、LNS が改善を試みる。
役割分担を綺麗にした瞬間に、システムが急に賢く感じる。
12.10 段階的に到達するロードマップ
最初からこの全体を作る必要はない。実務的な順序:
| 期 | 構築するもの | 触れる人 |
|---|---|---|
| Phase 1 (M1〜3) | ガントエディタ + 設備レーン D&D + KPI ライブ | 主任 |
| Phase 2 (M3〜5) | 役割スロット + 部分 CP-SAT 検証 + サジェスト | 主任 |
| Phase 3 (M5〜7) | エディタ API + LNS バックグラウンド | 主任 + 開発 |
| Phase 4 (M7〜9) | LLM クライアント (サジェストモード) | 主任 + LLM |
| Phase 5 (M9〜12) | LLM クライアント (バッチ実行 → 連続実行) | 主任 + LLM |
| Phase 6 (M12〜) | 新規受注の自動配置 (常駐モード) | LLM (主任は朝レビュー) |
各 Phase で 使える価値 が独立して出る。Phase 1 だけでも Excel からの脱却にはなるし、Phase 3 までで「裏方最適化付きエディタ」として完結する。LLM 層は最後で良い。
12.11 この章の振り返り
- ドロップを全部人間にやらせるのは重い。LLM をクライアントにする という解
- 構造の対応関係: Cursor / Claude Code がコードに対してやることを、ガントに対してやる
- エディタ API は 操作 / 問い合わせ / 検証 / 改善 の4分類
- LLM のループは 観察 → 計画 → 検証 → 操作。Claude Code の agentic loop と同型
- システムプロンプトでドメイン知識を仕込み、自律度は サジェスト → ステップ → バッチ → 連続 → 常駐 と段階上昇
- 守りは4層: API レベル拒否 / validateOps 先読み / Undo / ピン留め
- LLM の操作には 理由 を紐付けて永続化。運用が進むほど資産になる
- 全体アーキテクチャは UI / LLM クライアント / エディタ API / 最適化エンジン + 永続化 の4階建て。観測層が品質を支える
- 現代のスケジューリングは「問題を解く」ではなく 「人間 + LLM + 数理ソルバーの共同編集ワークスペース」
- 構築はフェーズ化。Phase 1 (素のエディタ) でも価値が出る。LLM 層は最後