..

ローカルLLMのすすめ

English

ローカルLLMはとても面白い。面白すぎて何時間も無駄にしてしまった。

ローカルLLMは、クラウドLLMには触らせられないデータを触ることができ、日常の様々なタスクを自動化できる。僕はいろいろ自動化を今している途中なのだが、その過程で色々学びがあったので、今日はそれを書きたい。

クラウドLLMの問題

フロンティアモデルは何でもそつなくこなせる優等生である。特に最近のフロンティアモデルは、Office周りやGmail、Google Workspaceとの統合がスムーズで、導入も簡単で、すぐに使えることが多い。

一方で、実際にデータを渡してよいかは別の問題である。僕の大学ではレギュレーションによって渡せないデータが多くあり、かつ僕自身も自分や家族の個人情報を渡すことに抵抗があるため、フロンティアモデルに送るデータと送らないデータを分けている。

これは杞憂ではなく、実際に事故は起きている。Samsung では社員が機密のソースコードや会議録を ChatGPT に貼り付けてしまい、社内で生成AIの利用が禁止になった。ChatGPT の共有リンク機能では、数千件の会話が Google 検索に載ってしまった。つい最近も、Anthropic の Claude で共有した会話や Artifact が Google 検索に載っていたことが話題になったばかりで、その中には実在の患者の医療記録や社外秘の文書まで含まれていたという。DeepSeek に至っては、チャット履歴やAPIキーを含むデータベースを丸ごとインターネットに公開してしまっていた。一度誰かのクラウドに送ったデータは、その後どうなるか自分ではコントロールできない。

ただ、こうやってデータを分けると、LLMが使えるタスクと使えないタスクが出てくる。そして日常の多くの小さなタスクは、まさに誰かのクラウドに送れないデータばかりを扱うため、そこだけ手作業のまま残って煩雑になる。

なので、ローカルLLMを使う旨味は十分にある。

ローカルLLMの問題

じゃあそのまま使えるかというと、そうでもない。生のローカルLLMは全く使えない。ハルシネーションはたくさん起こすし、リーズニングの途中で止まってしまうし、指示文を途中で忘れて何度も同じファイルを見てしまう。

問題を整理すると、だいたい3つある。

1つ目、従える指示の数が限られている。最近のベンチマークでは、フロンティアモデルは2,000件程度の同時指示にも従えると報告されている一方、小型モデルは指示が増えると早い段階から指数関数的に精度が崩れることが知られている。体感でも、ローカルLLMに確実に守らせられる指示はせいぜい数十件で、それを超えると指示を見失う。

2つ目、ハルシネーションを起こしやすい。ローカルLLMはそもそも学習量が少なく、内部記憶もあまり持っていないので、無理やり答えを出そうとして嘘をつく。記憶力も良くないので、たくさんのターンを重ねると最初に得た情報を忘れてしまう。

3つ目、曖昧な指示にはうまく対処できない。曖昧な指示が与えられると、ローカルLLMはリーズニングで指示を解釈してプランを立てる。だが、プランの組み方が甘く、揺らぎも大きい。同じ入力から全く違うプランを立てて、失敗することが多い。

これらの3つの問題が組み合わさると、リーズニングの途中で最初の指示を忘れ、最初に読み込んだファイルも忘れ、何度も同じファイルを読み、その分リーズニングが長くなってさらに指示を忘れる、というループに入る。

逆に言えば、ここを設計でカバーできれば、可能になるタスクがたくさん増える。この工学的問題はとても面白い。

Documentation is automation

この問題を解決するにあたって、僕が根幹に据えている原則は、Thomas A. Limoncelli の Documentation Is Automation である。ここでは、自動化には4つの段階があると説明されている。これはLLMというよりは、人間のエンジニアリングのための一つのガイドだが、ローカルLLMのシステムを作る際にとても参考になるので、ここで紹介したい。

  1. Document — 手順を文書化する
  2. Find equivalents — 各手順をコマンドに置き換える
  3. Automate — コマンドをつないで自動化する
  4. Autonomous — 無監督で走るようにする

自動化をすれば、繰り返しのタスクにかける時間は減る。もちろん最初の投資の時間は必要だが、将来にわたる時間的負債を解消できると考えている。

そして、この4段階を具体的に実現する方法として僕が行き着いたのが、CLIとスキルである。僕のシステムはこの4段階にそのまま沿って組んであるので、順に見ていこう。

1. Document — まず記録して、文書にする

文書化するには、まず日々の作業を記録する必要がある。僕にとっての記録の実体は、LLMとのセッションそのものである。セッションを保存しておけば、具体的な作業ステップはすべてそこに残る。

その上で、作業が発生したらまず Forgejo(セルフホストできるGitHubのようなもの)上にタスクを作ることを癖にしている。こうしておくと、どんなタスクがあったか後から見返せる。タスクにはセッションが紐付いているので、具体的なステップを知りたければセッションを参照すればよい。あとで見返して、自動化できるポイントを探すのに役立てている。

そして、繰り返し発生する作業には、その作業を円滑に行うためのスキルを作る。つまり、Forgejo(タスクとセッションの記録)とスキル(手順の文書)の2段構えで、僕はドキュメンテーションをやっている。

2. Find equivalents — 手順やスキルをCLIに置き換える

次に、文書化した手順やスキルを、なるべくCLIに置き換えていく。

ローカルLLMは信頼ならないので、すべての作業を委任してしまうとタスクが完了されなかったり、思わぬところでループにはまったりする。そのため、自動化できる部分はコマンドラインツールで自動化して、LLMの負担を下げる。具体的には、僕は Gmail のルックアップやドラフトの作成、カレンダーの時間の衝突確認、Google Drive のアクセス等々を全てCLIで自動化している。

このCLI化は、ハルシネーション対策の中核でもある。あるべきタスクの手順をすべてCLI側に移してしまえば、モデルが手順を自分でひねり出す余地がなくなり、そもそもハルシネーションが起こらない。

もう一つ大事なのが、CLIツールをローカルLLMファーストで設計することである。実際にモデルを走らせ、ツールコールに失敗した記録を取り、どの間違いを何度もするかを観察する。そして、その間違いに合わせてツールを再設計する。アーギュメントを直したり、search を lookup に変えたり、といった具合である。

例えば、モデルが mail search を何度も mail lookup と打ち間違えるので、コマンドを lookup に改名し、search もエイリアスとして受け付けるようにした。

3. Automate — メタスキルでつなぐ

現実のタスクは行儀よく一つずつやってこない。複数のタスクにまたがるものがあったり、新しいタスクが既存のタスクを含む形でやってきたりする。そこで僕は、メタスキルというものを作っている。メタスキルとは、スキルやコマンドを使うためのスキルである。どのタイミングでどのCLIやスキルを使うかという全体のワークフローを、メタスキルに書いておく。

スキルの書き方にはコツがある。前述の通り、ローカルLLMはたくさんの指示には従えない。なので、仕事のほとんどをCLI側に押し込んで、スキルは短く保つ。

CLIの使い方は、理想的にはCLIのヘルプに全部書くべきなのだが、そうするとLLMが毎回コマンドを叩く必要があって時間のロスになる。なので、あえてスキルに書いている。

加えて、LLMに書かせたスキルファイルは不要なデコレーションが多く指示も冗長で、コンテキストを圧迫する。なので僕は電報式でスキルを書いている。これでかなりのトークンを節約できる。

例えば、LLMにスキルを書かせるとこうなる。

特定の送信者からのメールを直近1週間分探したい場合は、search サブコマンドと –from オプションを使うことができます。–from にはメールアドレスまたは名前を指定できます。なお、結果が多すぎるとコンテキストを圧迫しますので、結果の件数を絞ることをおすすめします。

これを電報式にするとこうなる。

送信者・直近1週間でメール検索: mail search --from <person> --newer-than 7d。結果は5件まで。

トークン数にして、ざっくり5分の1になる。

また、スキルはプロジェクトのポリシーとして、システム全般に浸透するようにしている。方針から外れたスキルを作り始めると、頭を悩ませることになり、メンテナンスも大変だからである。

4. Autonomous — 無監督で走らせる

ここまで揃うと、LLMが完全に自動でタスクをこなせるようになる。Forgejo のタスクを渡せば、メタスキルに書かれたワークフローに沿って、CLIを叩きながら最後まで走ってくれる。

無監督で走らせるにあたって大事なのが安全策である。僕は、自動化する部分としない部分をまず決め、CLIには自動化してよい操作だけを入れ、させたくない操作はあえて入れない。例えば、誤ってメールを送らないように送信機能は実装していないし、勝手にインビテーションを送らないようにインビテーション機能も入れていない。

僕の境界線は、やり直せるかどうかである。やり直せる仕事は自動化し、やり直せない仕事は自動化しない。プロンプトで「送るな」と念押しするより、CLIのレベルで境界線を引くほうがずっと確実である。実際、これまで一度もLLMがこの境界線を超えて勝手に何かをしたことはない。

Optional: 検索にgrepを使わせない

これはオプションだが、効果が大きかったものを一つ。Wikiの検索にgrepを使わせないことである。grepはLLMが好んで使う検索コマンドだが、大量のテキストを吐き出すので、すぐにコンテキストがいっぱいになり、LLMの性能がガタ落ちする。そうならないように、僕は rtk というコマンドを使ったり、独自のベクターデータベースを作ったりして、そこから情報を引き出させるようにしている。rtk についてはこのブログが、ベクターデータベースについてはこの記事が詳しいので、参考にされたい。

おわりに

現在、様々なローカルLLMが利用可能になっており、今後も新しいモデルやチェックポイントが出てくる。ローカルLLMの性能が向上すればするほど、任せられるタスクが増えるので、今後もとても楽しみである。