Chapter 12

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つ:

  1. 操作が構造化 API で公開されている (テキスト diff ではなく assign(j, m, t))
  2. 検証ループが組み込まれている (型エラー / 整合エラーを LLM が読める)
  3. 人間が任意のタイミングで割り込める (ピン留めが 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 と本質的に同じ。

① 観察listJobs / getKpis② 計画LLM 推論③ 検証validateOps④ 操作applyBatch違反があれば LLM が修正案を再計画人間の割込み: 自然言語 / ピン留め
図 12.1 — LLM クライアントのループ。Cursor / 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 は毎ターン:

  1. 必要なら問い合わせ API で状況を把握
  2. 操作を組み立て、validateOps で先読み
  3. 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;     // どの問い合わせに基づいたか
}

これで、

操作ログは運用が進むほど資産になる。重みの校正データに使える (第10章 10.8 の校正サイクル) し、ファインチューニング素材にもなる。

12.8 アーキテクチャ全体像

ここまでをまとめると、システム全体は4階建てになる。

UI 層ガントエディタ (D&D) + チャット欄React / Web Worker (LNS) / WebSocketLLM クライアント層自然言語 → tool call ループClaude / GPT, MCP クライアント, セッション履歴エディタ API 層 (本章の主役)操作 / 問い合わせ / 検証 / 改善FastAPI or MCP server, 認可, 監査ログ最適化エンジン層部分 CP-SAT (検証 500ms)バックグラウンド LNS (3s+)辞書順最適化 (バッチ夜間)永続化層計画スナップショット (理由 / 差分 / parent)操作ログ (LLM session 紐付け)マスタ (設備 / 作業者 / スキル / カレンダー)観測層LLM 呼び出し回数 / 検証失敗率 / Undo率 / KPI 改善ログ
図 12.2 — 全体アーキテクチャ。LLM 層と最適化エンジン層が、エディタ API 層を介して同じ世界を共有する。

ポイントは:

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 この章の振り返り