LLMに計算させるな — 確率論的な挙動と決定論的な挙動の線引き
はじめに
2026年7月2日、日本科学未来館で開かれた LADEC 2026 で、実験ロボットを自然言語で操作するデモを3本、実機のライブ動作として実施した。テカンジャパン様の協力を得て開発したもので、対象はテカン社の分注ロボット Cavro® Omni Flex である。デモ3本の内容と当日の様子は Science Aid のブログに記録した(実験機器を動かすAIエージェント)。
分注ロボットというのは、試薬や検体を「決められた場所へ決められた量だけ吐出する」装置だ。ピペットを持ったアームが、プレートやチューブの元に移動して液体を吸い、別の場所へ吐く。人がやると単調で時間がかかり、量のばらつきも出る作業を、正確に繰り返してくれる。
ただし動かすには、「どの座標へアームを動かし、何マイクロリットル吸って、どこへ吐くか」という命令の列を、あらかじめ人が用意しておかなければならない。この列を書けるのは、機器の仕様に相当詳しい人だけである。今回作ったのは、その命令列をLLMに組み立てさせるシステムだ。実験手順書と日本語の指示を渡すと計画が立ち、人が承認すると、実機が動く。
この記事の主題は、どこに線を引くかである。全部をプログラムで書くことはできない。全部をLLMに任せると挙動が読めない。確率論的でよい部分と、決定論的でなければならない部分の間に、どう線を引くか。そして引いた線の両側をどう繋ぐか。
以前、LLMに推論させるより実装させろ という記事を書いた。論文から化学物質名を抽出するタスクで、LLMに直接解かせるとF1=76%が限界だったが、Claude Code に問題を解くプログラムを実装させるとF1=91%になった、という話だ。今回わかったのは、同じことがロボットとの連携でも成り立つということだった。
開発の進め方(触れない実機をどう扱ったか、1人でどう回したか、APIコストがいくらかかったか)は別の記事に分けた。→ 触れない実機で開発する
全部をプログラムで書くことはできない
まず、LLMを使わない方法を先に検討しておきたい。実験ごとにプログラムを書けばいいのではないか、という反論は当然出るし、実際これまではそうしてきた。
分注ロボットで新しい実験をやりたければ、その実験用の命令列を用意する。多くの場合それはメーカーへの依頼になる。時間も費用もかかるので、一度作ったら使い続けることになる。ある研究室では、予算が潤沢だった購入時に初期設定まで依頼できたものの、その後の研究費が続かず、同じプログラムでしか動かせなくなった、という話を聞いた。
この方法に未来がないのは、実験の種類に上限がないからだ。単純分注、段階希釈、検体ごとの濃度に合わせた正規化。それぞれに関数を用意しても、次の実験には次の関数が要る。あらかじめ想像できるケースを網羅するのは不可能である。
そして、書けたとしても得られるものはプリセットの呼び出しでしかない。「引数に応じて段階希釈をする」という関数を用意すれば、そのデモは確実に動く。だがLLMを載せたところで、やることは引数をいくつか渡すだけになる。手順を組み立てたことにはならない。
機器の側には、まだ余力がある。プログラムを組み替えれば多様な実験に対応できる能力を持っているのに、プログラムのハードルが高くて使い切れていない。関数を増やす方向では、現実的に対応できない。
全部をLLMに任せると、挙動が読めない
では逆に、全部をLLMにやらせればいいか。
LLMへの期待が高い人が作るとこうなるだろう。機器のマニュアルをコンテキストに入れて渡し、ユーザーのリクエストから命令列を直接生成させる。仕様は文書にあるのだから、読んで書けるはずだ——という発想である。
これは3つの理由で筋が悪い。
コンテキストを食う。 通常、機器のマニュアルは、コマンド体系・座標系・エラーコードを含む分厚いものだ。毎回これを読ませれば、応答は遅くなり、費用も嵩む。しかも入力が長いほど注意は分散する。
型安全が保証されない。 自然言語の文書を根拠に生成された命令文字列が妥当かどうかは、実行するまでわからない。機器へ送って初めてエラーが返ってくる。物理を動かす相手に対して、これは受け入れがたい。
確率論的である。 同じ指示に対して毎回同じ命令列が出るとは限らない。挙動が読めないということは、検証もできないということだ。
そして何より、手順が決まっていてプログラムで処理できるものを、LLMに推論させる理由がない。一定の間隔で座標をずらす、マイクロリットルをポンプのステップ数へ換算する、命令の文字列を組み立てる。いずれも入力が決まれば出力が決まる処理で、関数にしてコードで計算させたほうが、正確で、速く、安い。LLMに任せる意味があるのは、そうやって関数にできない部分だけである。
どこに線を引くか — 実験を構成する要素をパッケージにする
どちらの極端も採れない。そこで両方を使い、仕事を分けた。分け方はこうだ。
確率論的にしか解けない部分(定型化できない仕事)はLLMに、決定論的に解ける部分(定型化できる仕事)はプログラムに。 そしてプログラム側をパッケージとして実装し、それをLLMに使わせる。
判断の基準は「その処理は定型化できるか」、言い換えれば「正解が一意に決まるか」だ。電卓で解けるならプログラム、解けないならLLMである。
重要なのは、プログラムにする粒度を「実験」ではなく「実験を構成する要素」にしたことだ。用意したのは、移す(transfer)・配る(distribute)・混ぜる(mix)・退避する(park)といった、目的に依らない動詞だけである。「配ってから段階希釈する」のような、手順を含んだ合成関数は作らない。動詞は可、手順は不可という線引きだ。
要素の粒度にすれば、関数の数は実験の種類に比例しない。実験が変わっても、要素の組み合わせが変わるだけである。
この基準で切ると、仕事はこう分かれる。
| 仕事 | 担当 | この案件での例 |
|---|---|---|
| 指示の解釈と対象の同定 | LLM | 自由な書き方の指示や手順書の記述を、システムが持つ試薬・器具・場所と対応づける(「赤い液」がどの試薬を指すか) |
| 手順の組み立て | LLM | どの要素をどの順で何回使うかを決める。品質のための選択もここに含まれる(交差汚染を防ぐため検体ごとにチップを替える) |
| 値の算出 | プログラム | 量・倍率・座標。正解が一意に決まる計算 |
| 機器が受理する形への変換 | プログラム | 役割名やウェル名を絶対座標へ、µL をポンプのステップ数へ、命令の文字列へ |
| 制約の検証 | プログラム | 到達範囲や上限量を超えていないか |
| 実行 | プログラム | 確定した命令列を機器へ送る |
一般化すると、LLM側に残ったのは入力を解釈することと組み合わせを決めることの2つだけである。値を出すこと、形を変えること、検証すること、実行すること——答えが一意に決まる仕事はすべてプログラム側にある。座標も分量も命令構文もLLMは触らない。LLMは数字を暗算しない。
パッケージにしたことの副次効果として、LLMに渡すものが小さくなった。マニュアルを読ませる必要はなく、見せるのは動詞のシグネチャだけである。引数の型と範囲は実行前に検査される。前節で挙げたコンテキストの消費と型安全の問題は、この分け方にした時点でほとんど解消している。
この切り分けは、実験ロボットに限らない
同じことを、私は自分の仕事の基盤でもやっている。日々の業務を回している個人のリポジトリでは、タスクの起票、リポジトリの同期、リリースの手順といった定型動作をすべてコマンドに落としてある。Claude Code はそれを呼ぶ。毎回自然言語で手順を説明するのではなく、確定している部分をコマンドとして固め、判断が要るところだけをLLMに任せる。
最近は、AIエージェント開発のプロジェクトを始めるときに最初に考えるのがここになった。決定論的な部分を、どれだけプログラムとして切り出せるか。 切り出せた分だけLLMは考えるべきことに集中することができる。対象が実験ロボットでも、コードベースでも、社内の業務でも、この構造は変わらない。
組み合わせの推論は、LLMに任せる
LLMに残した仕事は何か。ユーザーのリクエストを読み解き、それがどの要素の組み合わせで実現できるかを推論することである。
ここはプログラムでは書けない。入力の形が定まらないからだ。実験手順書のPDFが添付されることもあれば、チャットに数行書かれることもある。「このSOPで検量線スタンダードを作って」でも通るし、濃度の表を貼って「等モルにそろえて」でも通る。あらゆるフォーマットの入力を動作の列へ変換するという仕事は、まさに柔軟さが要求される場所で、LLM以外に担い手がいない。
しかも渡す手順書は、機械向けに整形した特別な文書ではない。人間の科学者に向けて書かれた現実的なSOPで、使用機器・消耗品・安全衛生上の注意まで含み、今回の実験には不要な記述も混ざっている。そこから必要な情報だけを抜き出して計画に落とす。
結果として、ユーザーは人に頼むときと同じように指示を書ける。テキストで書いてもいい、ファイルを添付してもいい、自由記述でいい。これは線をここに引いたことの見返りである。
境界をどう繋ぐか — ツール呼び出しからコード生成へ
さて、要素をパッケージにして、組み合わせの推論をLLMに任せた。残るのは境界の繋ぎ方である。LLMに、この要素をどう使わせるか。
最初に選んだのはツール呼び出しだった。LLMが transfer(A1, B1, 100, mix=3) のような関数呼び出しを1つずつ並べていく方式で、少し前までエージェント実装の標準的なやり方である。
これには構造的な弱点がある。関数と関数の間を、LLMの推論が埋めることになる。
たとえば、距離をマイルで返す関数Aと、メートルを引数に取る関数Bがあるとする。ツールとしてこの2つしか用意されていなければ、Aの出力をBに渡すための単位変換は、どこにも実装されていない。LLMが頭の中で換算して、Bの引数に数字を書き込むことになる。関数と関数の肉付けを、推論能力に頼る構造である。
繋ぎ目が単位変換ひとつなら、実害は出にくいかもしれない。だが条件によって値が変わる計算や、繰り返しが挟まると、推論に任せる範囲は一気に広がる。しかもその計算が合っていたかどうかは、実行するまでわからない。
そこでコード生成に変えた。LLMに、要素の組み合わせを1本のPythonスクリプトとして書かせる方式である。
これは新しい発想ではない。LLMの行動をJSONやテキストではなく実行可能なコードとして表現する方式は CodeAct として提案されており、17個のLLMを対象にした評価で従来手法より最大20ポイント高い成功率が報告されている(Wang et al., ICML 2024)(私が好きな論文の1つだ)。ただしそこで測られていたのは、ソフトウェアの中で完結するタスクだった。私たちがやったのは、これを物理を動かす場面へ持ち込むことである。
LLMが生成するのは、こういうコードだ。
volumes = [200, 100, 100, 100, 100, 100] # 量はコードが確定的に扱う
transfer(src='stock', dst='A1', volume=volumes[0])
for i in range(1, 6): # A1→A2→…→A6 の連鎖希釈
transfer(src=f'A{i}', dst=f'A{i+1}', volume=volumes[i], mix=3)
transfer(src='A6', dst='waste', volume=100) # 余りを廃棄
同じモデルのまま、段階希釈が最後まで正しく実行されるようになった。 ツール呼び出しでは成立しなかった6段の希釈が、各段の濃度まで狙いどおりになった。モデルを賢いものに替えたわけではない。変えたのは、要素の使わせ方だけである。
繋ぎ目そのものがコードで解ける
コード生成の効き目がはっきり出たのは、3本目のデモだった。NGSのライブラリを等モル化する、つまり検体ごとに濃度が違うライブラリを、共通のモル濃度へ揃える作業である。
ここには、要素の間に単位の異なる橋渡しがある。定量装置が返すのは「重さあたりの濃度(ng/µL)」と「平均断片長(bp)」だが、等モル化に必要なのは「モル濃度(nM)」だ。重さの濃度とモルの濃度は別物なので、断片長を使った換算を挟まなければならない。そこから目標濃度と最終量を使って、取るライブラリ量と足す希釈液量を出す。
このSOPには、こう書かれている。
nM 換算は濃度だけでなく平均断片長を要する。断片長が検体ごとに違うと、同じ ng/µL でも nM は異なる。この換算を誤るとプール比が偏るため、計算結果は実施前に確認・承認する。
つまり「関数と関数の肉付け」をしている場所で換算を誤れば、そのままプール比が偏り、実験の結果が狂う。ツール呼び出しでこれをやると、LLMは4検体それぞれについて換算と割り算をこなし、8個の数字(各検体のライブラリ量と希釈液量)を引数に埋めることになる。検体ごとに値が違うので、パターンで逃げることもできない。
コード生成なら、繋ぎ目も含めてコードになる。
libs = [('L1', 18.0, 470), ('L2', 7.5, 510), ('L3', 22.0, 430), ('L4', 11.0, 490)]
wells = ['A1', 'B1', 'C1', 'D1']
TARGET_NM, FINAL_UL = 10.0, 50.0
for (name, ng_per_ul, bp), well in zip(libs, wells):
nM = ng_per_ul * 1e6 / (660 * bp) # 重さの濃度 → モル濃度(660 は塩基対あたりの平均分子量)
lib_ul = TARGET_NM * FINAL_UL / nM # この検体から取る量
transfer(src='diluent', dst=well, volume=FINAL_UL - lib_ul)
transfer(src=name, dst=well, volume=lib_ul, mix=3)
取る量は検体ごとに 6.45µL から 22.44µL までばらける。この計算は確実だ。 そして実行前に、生成されたコードを読めば何が起きるかがわかる。
ここで混乱しやすいので、はっきり書いておく。「計算はプログラムに任せる」と言ったのに「LLMにプログラムを書かせる」のは矛盾ではない。“プログラム”が2つの別物を指しているからだ。
前者は予め作成した固定のパッケージで、座標解決・単位換算・命令構文・実行を担う。LLMは決して触らない。後者はその日の組み合わせを表す短いスクリプトで、その中に出てくる計算も、呼ばれる動詞の中身も、すべて前者に委ねられる。LLMが書くのは並べ方と、要素の間の橋渡しだけである。
なぜコードで書かせると精度が上がるのか
理由は2つある。
ひとつは、繰り返しと計算を含む構造は、コードでならそのまま書けることだ。段階希釈は「同じ操作を条件を変えて繰り返す」構造で、等モル化は「検体ごとに違う値で同じ計算を繰り返す」構造である。どちらも、自然言語や関数呼び出しの列では素直に表現できない。しかしループと配列と四則演算があれば、構造をそのまま写せる。LLMはこの写し取りが得意で、写したあとの計算はもう推論の対象ではない。
もうひとつは、確率論的な部分が「構造を書く」ことだけに縮むことだ。ツール呼び出しでは、構造の理解と数値の計算が同じ推論の中で混ざっていた。コードにすると、確率論的なのは構造の記述だけで、値はすべて決定論的に定まる。LLMとプログラムの同じ分け方が、要素の内部だけでなく、要素と要素の間にも適用できる。
この差がとりわけ大きいのは小型で安価なモデルだ。上位モデルはツール呼び出しでも正解に届くことがある。しかしコード生成なら、手元で動かせる小型モデルでも計算を含む手順を回せる。(なお本番当日のデモでは、速度と安定性を加味して上位モデルを使った。)
生成されたコードは制限付きの環境で実行する。渡すのは機器操作用のオブジェクトと数学関数だけで、ファイルシステム・ネットワーク・外部モジュールの読み込みは遮断してある。LLMが書いたコードを機械のそばで走らせるので、書ける範囲は狭いほうがよい。
承認より前に、命令列を確定させる
コード生成を採ると、ひとつ気をつけることがある。スクリプトはLLMの出力なので、実行するたびに同じとは限らない。 物理を動かす場面では、人が承認した計画と違うものが動くのは許容できない。
そこで、指示から実行までを4つのフェーズに分け、確定のタイミングを設計した。
① Request 「この手順書で検量線を作って」+ 手順書
│
▼
② Plan 機器を動かさずに計画を作り切る(3段)
│ Compose : 人が認識する粒度の手順を並べる
│ Map : 各手順を機器の命令(座標つき)へ確定変換する
│ Forecast : 変換後をシミュレートし、最終状態を予測する
▼
③ Approve Compose の手順と Forecast の予測を人が確認する
│
▼
④ Execute 承認済みの命令列をそのまま流す(再計算しない)
要点は、Map が Approve より前に終わっていることだ。命令列はすでに確定してバッファされており、承認後はそれをそのまま流す。スクリプトを承認後に再実行することはしない。だから「人が確認したもの=実行されるもの」が一致する。
Approve で見せるのは手順と予測だけにした。Map が出す座標つきの命令列は見せない。「希釈液を5つのウェルに100µLずつ配り、原液を200µL入れ、6段の連鎖希釈をする」なら人は判断できるが、座標とステップ数の羅列を見せられても判断材料にならない。
Forecast を出すのは決定論的な状態モデルである。確定した命令列を通すと、各ウェルの液量と濃度が出る。LLMが組んだ手順の結果を、LLMを介さずに検算できる形になっている。
まとめ
- 全部をプログラムで書くことはできない。 実験の種類に上限がないので、関数を増やす方向では終わらない。書けてもプリセットの呼び出しになる。
- 全部をLLMに任せると挙動が読めない。 マニュアルを都度読ませればコンテキストを食い、自然言語から命令文字列を吐かせれば型安全が保証されず、しかも毎回同じとは限らない。
- プログラムにする粒度は「実験」ではなく「実験を構成する要素」にする。 そうすれば関数の数が実験の種類に比例しない。動詞は可、手順は不可。
- 組み合わせの推論はLLMに任せる。 あらゆるフォーマットの入力を動作の列に変換する仕事は、他に担い手がいない。だからユーザーは人に頼むように指示を書ける。
そして境界の繋ぎ方が、精度を決める方法論だった。ツール呼び出しは、関数と関数の間をLLMの推論で埋めることになる。 繋ぎ目に単位換算や条件つきの計算があるほど、推論に委ねる範囲が広がり、しかもそれが合っていたかは実行するまでわからない。
コード生成に変えると、繋ぎ目そのものがコードになる。確率論的なのは構造を書くことだけで、値は決定論的に定まる。LLMが問題に追いつくのを待つのではなく、LLMが正確に扱える形へ問題を書き換えるという発想である。
冒頭に挙げた LLMに推論させるより実装させろ は、化学物質名の抽出という、ソフトウェアの中で完結するタスクの話だった。今回わかったのは、ロボットを動かすという領域でも同じ構図が成り立つということである。しかも物理では、外したときの代償が単なる精度の低下では済まず、器具の破損や試薬の消費になる。LLMに推論させるより実装させる——この主張は、物理を扱うほど強くなる。
この設計をどう作ったか——実機に触れられない前提でどう開発を進め、1人でどう回したかは、別の記事に書いた。→ 触れない実機で開発する