Liquid <> .txt のコラボレーション

スマートホームシステム、産業用 IoT センサー、モバイルアシスタントなどの実用的なエッジアプリケーションでは、モデルが関数呼び出しを可能な限り正確かつ高速に生成する必要があります。これらのデバイスは、以下の 3 つの重要な課題に直面しています:

  1. 処理能力の制限:iPhone 15 Pro の性能は 2.15 TFLOPS(FP32)で、単一の H200 GPU の 31 分の 1
  2. 低遅延要件:ユーザーは 300 ミリ秒未満の即時応答を期待
  3. 高信頼性:低遅延要件を満たすため、関数呼び出しは最初から正しく動作する必要がある

従来の 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 は、明確なルールに沿って音楽を作曲する仕組みのようなものだと考えてください。技術的には、CFG は次の要素から成ります。

  • 終端記号:最終的な出力に現れる実際の文字
  • 非終端記号:規則に従って置き換えられる変数
  • 生成規則:非終端記号を終端記号や他の非終端記号に置き換えるための手順

たとえば、次の文法は play_media 関数の呼び出しが正しくなることを保証します:

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"

この例では:

  • 終端記号には "play_media"、"("、")" などの文字が含まれます
  • 非終端記号には Call、Arguments、Device などが含まれます
  • 記号 "::=" は「置き換えられる」を意味します
  • 記号 "|" は「OR」を意味します(選択肢を示します)

文法ベースの構造化生成は、次のことを保証します:

  • セキュリティ:生成可能な文字列を厳密に制限しインジェクション攻撃を防止
  • 信頼性:常にパース可能な出力を保証
  • 検証:ビジネスルールを生成時点で適用

デモ:スマートホーム制御

LFM2-350M と dotgrammar が、ローカルのハブデバイス上で実行されるスマートホームアシスタントという現実的なエッジアプリケーションでどのように連携するかを見てみましょう。

このユースケースでは、モデルがユーザーのクエリを受け取り、照明の調光、ブラインドの開閉、メディアの再生など、家庭内のさまざまなシステムの状態を設定するためのアクションを実行します。たとえば、`theater_mode()` のような関数は引数を必要としませんが、`set_display()` のような関数ではより細かい制御が必要になります。

これを、.txt の 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()` は完全に閉じますが、0 から 100 までの値を取る `percentage` という引数で割合を指定することも可能です。関数のシグネチャは `close_blinds(percentage: int = 100)` で(ただし、整数値の範囲を指定するには Pydantic スキーマのようなものが必要になります)、以下のような文法を定義できます:

percentages = list(range(101))
percentages_options = ' | '.join(percentages)

blinds_grammar = f"""
	?start: "close_blinds(" arguments )"
	?arguments: ("percentage = " percentage) 
	?percentage: {percentages_options}
"""

2 行目の括弧に引用符が付いていない点に注目してください。これは CFG において「任意」の要素を示します。また、`percentages` で指定可能な値が 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 は対応可能です。関数呼び出し全体を `&lt;|tool_call_start|&gt;` と `&lt;|tool_call_end|&gt;` で囲うことで、パースを簡単にします:

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
)

`sequence` に格納されたモデルの出力は以下の通りです:

<|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 の遅延ゼロ文法制約と組み合わせることで、エッジアプリケーションで最大のパフォーマンスを発揮します

あなた自身で試してみませんか?

Liquid AI logoPlayground で試すhugging face logo liquid AIHugging Face で試すImported from Webflowllama.cpp