
.txt の構造化出力と Liquid Foundation Models により、エッジでの関数呼び出しがより高速かつ高精度に
はじめに
スマートホームシステム、産業用 IoT センサー、モバイルアシスタントなどのエッジアプリケーションでは、モデルが関数呼び出しを可能な限り正確かつ高速に生成する必要があります。これらのデバイスは、以下の 3 つの重要な課題に直面しています:
- 処理能力の制限:iPhone 15 Pro の性能は 2.15 TFLOPS(FP32)で、H200 GPU の 31 分の 1
- 低遅延要件:ユーザーは 300 ミリ秒未満の即時応答を期待
- 高信頼性:関数呼び出しは最初から正しく動作する必要がある
従来の LLM による関数呼び出し手法では、これらの制約を満たすことが難しいことが多く、モデルは大きすぎ、生成が遅く、出力に一貫性がありません。これは、エッジでハードウェアやソフトウェアと確実に連携する AI アシスタントの導入において大きな障壁となっています。
ソリューション:LFM2-350M + dotgrammar
LFM2-350M:エッジ対応の AI
LFM2-350M は、Liquid AI によって開発された LLM で、軽量な独自アーキテクチャにより、エッジデバイスでの高性能実行に特化しています:
- 最小限のフットプリント:量子化なしで 1GB 未満の RAM を使用、長い入力でも増加は最小限
- 高速応答:一般的なエッジハードウェアで 100ms 未満の推論
- 高品質:知識と推論の性能は、はるかに大きなモデルと同等
dotgrammar:文法ベースの生成
dotgrammar は、.txt によって開発されたライブラリで、コンテキストフリー文法(CFG)を使用した高性能な構造化出力を実現します。LFM2-350M の効率性を補完し、dotgrammar は以下の点で信頼性の高い関数呼び出しを保証します:
- CFG 制約:常に構文的に正しい出力を保証
- トークン効率の高い形式:Python 風の関数呼び出しで生成時間を短縮し応答性を向上
- 推論時オーバーヘッドゼロ:制約を付けても遅延は増えない
関数呼び出しとは?
関数呼び出しとは、LLM が自然言語ではなく構造化された関数を出力する手法です。これにより、AI アシスタントが外部ツールや API、ハードウェアと信頼性のあるプログラム的な方法でやり取りできます。
例:
User: "Play Spotify at high volume"
AI: play_media(device="speaker", source="spotify", volume=1)
LLM がアプリのネイティブ言語で話すようになり、自然言語理解と実行可能なコマンドのギャップを埋めます。
従来の JSON アプローチ
多くの LLM プロバイダは、関数呼び出しを JSON オブジェクトで実装しています。これは機能しますが、エッジでの使用には非効率的です:
{
"name": "play_media",
"parameters": {
"device": "speaker",
"source": "spotify",
"volume": 1
}
この JSON は生成に 37 トークン必要で、それぞれが遅延を増加させます。また、LLM はネスト構造の正しい形式の出力が苦手です。
Python 的な代替案
よりコンパクトな Python 風の表現では:
play_media(device="speaker", source="spotify", volume=1)
トークン数は 14。これは 2.6 倍の削減で、生成時間が短縮され、信頼性も高まります。LLM はコードで訓練されているため、この形式はより自然で堅牢です。
構造化生成の必要性
それでも LLM は間違えることがあります。エッジ環境では以下の理由で問題になります:
- パラメータには厳密な制約がある(例:volume は 0〜1)
- パラメータの組み合わせにはビジネスロジックがある(例:"speaker" と "netflix" は無効)
- 再試行の余地がないほどの低遅延要件
そのため、構造化された出力形式を決定論的に強制する必要があります。ここで CFG(コンテキストフリー文法)が活躍します。
CFG(コンテキストフリー文法)とは?
CFG は、生成が従うべき正確なルールの集合で、まるで「レールの上を走る」ように出力が導かれます。
技術的には以下を含みます:
- 終端記号:最終出力に含まれる文字列
- 非終端記号:ルールに従って置き換えられる変数
- 生成規則:非終端記号を終端記号または他の非終端記号に置き換えるルール
例:
Call ::= "play_media" "(" Arguments ")"
Arguments ::= "device=\"" Device "\"" ", " "source=\"" Source "\"" ", " "volume=" Volume
Device ::= "speaker" | "tv"
Source ::= "spotify" | "netflix" | "youtube"
Volume ::= "0" | "0.1" | "0.2" | ... | "1.0"
CFG 生成の利点
- セキュリティ:生成可能な文字列を制限しインジェクションを防止
- 信頼性:常にパース可能な出力を保証
- 検証:ビジネスルールを生成時点で適用
デモ:スマートホーム制御
LFM2-350M と dotgrammar が、ローカルのハブデバイス上で実行されるスマートホームアシスタントという現実的なエッジアプリケーションでどのように連携するかを見てみましょう。
このユースケースでは、モデルがユーザーのクエリを受け取り、照明の調光、ブラインドの開閉、メディアの再生など、家庭内のさまざまなシステムの状態を設定するためのアクションを実行します。たとえば、theater_mode()
のような関数は引数を必要としませんが、set_display()
のような関数ではより細かい制御が必要になります。
これを、dotgrammar ライブラリを使って実装します。dotgrammar は遅延ゼロの文法ベース構造化生成を提供します。
例 1:制約のない引数を持つシンプルな関数
ユーザーが映画を観終わり、その感想をメモしておきたいとします。スマートホームには save_note(str)
という関数があり、任意の文字列を引数として受け取ることができます。
この場合、関数呼び出しの形式を正しく保つために出力を制約したいものの、引数そのものは制限したくありません。これを dotgrammar では以下のように表現できます:
note_grammar = """
?start: "save_note(" UNESCAPED_STRING ")"
%import common.UNESCAPED_STRING
"""
例 2:制約のある引数を持つ関数
多くの場合、引数には制約があります。たとえば、set_display()
関数は tv
や projector
のようなリスト内の特定のデバイスに対して呼び出されます。Python ではこれは文字列リテラルで示されますが、CFG ではパイプ記号 (“|”) を使って制約します。
以下は、set_display(screen: Literal[‘tv’, ‘projector’])
という型を持つ関数の文法例です:
display_grammar = """
?start: "set_display(device = " device ")"
?device: "'tv'" | "'projector'"
"""
'tv'
と 'projector'
を囲む二重引用符は、それらが CFG において定数として扱われることを示します。一重引用符は、Python の関数呼び出しで文字列として扱うことを意味します。
次に、Boolean 引数を追加してみましょう。たとえば、表示の明るさを調整する "night_mode" 引数を加えます。この新しい関数 set_display(screen: Literal[‘tv’, ‘projector’], night_mode: bool)
に対して、文法を以下のように拡張します:
display_grammar = """
?start: "set_display(" arguments )"
?arguments: "device = " device ", night_mode = " night_mode
?device: "'tv'" | "'projector'"
?night_mode: "True" | "False"
"""
例 3:混合制約を持つ関数
制約付き生成は、オプションの引数にも対応可能です。たとえば、スマートホームがブラインドを完全に閉じたり、部分的に閉じたりできるとします。デフォルトでは close_blinds()
は完全に閉じますが、percentage
という引数で割合を指定することも可能です。関数のシグネチャは close_blinds(percentage: int = 100)
で、以下のような文法を定義できます:
percentages = list(range(101))
percentages_options = ' | '.join(percentages)
blinds_grammar = f"""
?start: "close_blinds(" arguments )"
?arguments: ("percentage = " percentage)
?percentage: {percentages_options}
"""
2 行目の括弧に引用符が付いていない点に注目してください。これは CFG において「任意」の要素を示します。また、percentage
で指定可能な値が 101 個と少数であるため、すべてを列挙しています。数値を完全に制限するのは常に可能ではありませんが、値の種類が少なければ CFG での制約も可能です。
例 4:複数関数の統合
これまで、各関数と文法を個別に扱ってきましたが、実際のスマートホームアシスタントではこれらすべての機能を同時に使用する必要があります。
それぞれの関数を「ツール」として扱い、それらを接続する文法を作成できます。1 回の呼び出しで 1 つの関数しか許可しない場合、関数をパイプで繋ぎます:
percentages = [str(i) for i in range(101)]
percentages_options = ' | '.join(percentages)
one_function_grammar = f"""
?start: "<|tool_call_start|>[" function "]<|tool_call_end|>"
?function: save_note | set_display | close_blinds
?save_note: "save_note(" UNESCAPED_STRING ")"
?set_display: "set_display(" display_arguments ")"
?display_arguments: "device = " device ", night_mode = " night_mode
?device: "'tv'" | "'projector'"
?night_mode: "True" | "False"
?close_blinds: "close_blinds(" blinds_arguments ")"
?blinds_arguments: "percentage = " percentage
?percentage: {percentages_options}
%import common.UNESCAPED_STRING
"""
複数の関数呼び出しにも dotgrammar は対応可能です。関数呼び出し全体を <|tool_call_start|>
と <|tool_call_end|>
で囲うことで、パースを簡単にします:
percentages = [str(i) for i in range(101)]
percentages_options = ' | '.join(percentages)
multi_function_grammar = f"""
?start: "<|tool_call_start|>[" tool_calls "]<|tool_call_end|>"
?tool_calls: (function ", ")* function
?function: save_note | set_display | close_blinds
?save_note: "save_note(" UNESCAPED_STRING ")"
?set_display: "set_display(" display_arguments ")"
?display_arguments: "device = " device ", night_mode = " night_mode
?device: "'tv'" | "'projector'"
?night_mode: "True" | "False"
?close_blinds: "close_blinds(" blinds_arguments ")"
?blinds_arguments: ("percentage = " percentage)
?percentage: {percentages_options}
%import common.UNESCAPED_STRING
"""
行 ?tool_calls: (function ", ")* function
では、関数が複数回出現可能であることを “*” により指定しています。
結果
それでは、このシステムが実際にどのように動作するか、2 つの実例を見てみましょう。
まずは、プロジェクターをナイトモードにして映画鑑賞の準備をするようモデルに指示します。LFM2-350M と dotgrammar を使用するには、数行のコードで十分です:
import outlines
from transformers import AutoModelForCausalLM, AutoTokenizer
from outlines.types import CFG
MODEL_NAME = "LiquidAI/LFM2-350M"
model = outlines.from_transformers(
AutoModelForCausalLM.from_pretrained(MODEL_NAME, device_map="auto"),
AutoTokenizer.from_pretrained(MODEL_NAME)
)
PROMPT = "It's movie time! Set the projector to night mode."
result = model(
PROMPT,
CFG(GRAMMAR),
max_new_tokens=64
)
出力は以下の通りです:
<|tool_call_start|>[set_display(device = 'projector', night_mode = True)]<|tool_call_end|>
ここでは、モデルが close_blinds(percentage = 90)
と set_display(device=’projector’, night_mode=True)
という 2 つの関数を正しく呼び出しました。
次に、より自由なプロンプトで試してみましょう。ユーザーが映画に関するメモを残したいと伝えたとき、内容はモデルに任せます:
PROMPT = "Create a reminder for me to look into other movies by this person."
result = model(
PROMPT,
CFG(GRAMMAR),
max_new_tokens=64
)
出力:
<|tool_call_start|>[save_note("Please remember to check other movies by this person!")]<|tool_call_end|>
モデルは適切な関数を呼び出し、ユーザーの意図を正しく要約して出力しました。
結論:ただ“動く”エッジ AI
LFM2-350M と .txt の構造化出力を組み合わせることで、エッジデバイスにおける関数呼び出しを**「実現可能」にするだけでなく、「現実的」**なものにするソリューションを構築しました:
- 超高効率:Python 風の関数呼び出しは JSON より約 2.6 倍少ないトークンで済むため、生成が高速かつリソース消費も抑えられます
- 完全な信頼性:文法ベースの構造化生成により、出力がアプリケーションの要件に常に合致します
- エッジ最適化:LFM2-350M はデバイス上での使用を前提に設計されており、.txt の遅延ゼロ文法制約と組み合わせることで、エッジアプリケーションで最大のパフォーマンスを発揮します