日本語版
最新ニュース
科学&テクノロジー

LLM から構造化された出力を得るためのあらゆる方法

サム・リジン著 - @sxlijinこの投稿は、次のような方にとって興味深いものとなるでしょう: LLMから構造化された出力を得ようとしている場合、 あなたは試した response_format: "json" 関数呼び出しをして結果に失望したことがある、 LLMからJSONを抽出するために正規表現を積み重ねるのにうんざりしている、 あなたは自分の選択肢が何であるかを考えようとしています。 実稼働環境で LLM を使用する人は、遅かれ早かれこの問題に遭遇します。私たちが本当に欲しいのは、まさに私たちが望む形式で JSON を返す魔法のブラック ボックスです。残念ながら、LLM は JSON ではなく英語を返すため、英語を JSON に変換するのはかなり難しいことがわかりました。この問題を解決するために私たちが見つけたすべてのフレームワークと、それらの比較を以下に示します。(免責事項: この分野のプレーヤーとして、私たちは少し偏りがあります!)注: LangChainをこのリストから除外したのは、本番環境で使用しているという話を聞いたことがないからです。 /r/LangChain の歴代トップ投稿。*: マイクロソフトの ここWASM ランタイムを使用して Python/JS で実装された協調制約用のシムの作成に取り組んでいます。他のものよりも低レベルであるように思われ、セットアップが非常に複雑なため、リストには含めませんでした。1: OpenAIモデルに制約を適用すると、OpenAI APIがフレームワークが制約を効果的に適用するために必要な基礎となるモデル操作に関する十分な情報を公開しないため、エラーが発生しやすくなります。 限界についての議論 LMQL ドキュメントより。2: トランスフォーマーは「ハギングフェイストランスフォーマー」を指します3: 制約付きストリーミング生成では部分オブジェクトが生成されますが、部分オブジェクトはまだ解析可能ではないため、部分オブジェクトと対話する良い方法はありません。部分オブジェクトと対話できる場合にのみ、ストリーミングをサポートするフレームワークを検討します(たとえば、プロパティを持つオブジェクトをストリーミングで返す場合)。 foo…

LLM から構造化された出力を得るためのあらゆる方法

1718690988
2024-06-18 04:01:43

サム・リジン著 – @sxlijin

この投稿は、次のような方にとって興味深いものとなるでしょう:

  • LLMから構造化された出力を得ようとしている場合、
  • あなたは試した response_format: "json" 関数呼び出しをして結果に失望したことがある、
  • LLMからJSONを抽出するために正規表現を積み重ねるのにうんざりしている、
  • あなたは自分の選択肢が何であるかを考えようとしています。

実稼働環境で LLM を使用する人は、遅かれ早かれこの問題に遭遇します。私たちが本当に欲しいのは、まさに私たちが望む形式で JSON を返す魔法のブラック ボックスです。残念ながら、LLM は JSON ではなく英語を返すため、英語を JSON に変換するのはかなり難しいことがわかりました。

この問題を解決するために私たちが見つけたすべてのフレームワークと、それらの比較を以下に示します。

(免責事項: この分野のプレーヤーとして、私たちは少し偏りがあります!)

注: LangChainをこのリストから除外したのは、本番環境で使用しているという話を聞いたことがないからです。 /r/LangChain の歴代トップ投稿

*: マイクロソフトの ここWASM ランタイムを使用して Python/JS で実装された協調制約用のシムの作成に取り組んでいます。他のものよりも低レベルであるように思われ、セットアップが非常に複雑なため、リストには含めませんでした。

1: OpenAIモデルに制約を適用すると、OpenAI APIがフレームワークが制約を効果的に適用するために必要な基礎となるモデル操作に関する十分な情報を公開しないため、エラーが発生しやすくなります。 限界についての議論
LMQL ドキュメントより。

2: トランスフォーマーは「ハギングフェイストランスフォーマー」を指します

3: 制約付きストリーミング生成では部分オブジェクトが生成されますが、部分オブジェクトはまだ解析可能ではないため、部分オブジェクトと対話する良い方法はありません。部分オブジェクトと対話できる場合にのみ、ストリーミングをサポートするフレームワークを検討します(たとえば、プロパティを持つオブジェクトをストリーミングで返す場合)。 foo そして bar、アクセスできます obj.foo 前に bar クライアントにストリーミングされました。

私たちの基準のほとんどは説明不要ですが、特に強調したい基準が 2 つあります。

不正な形式の JSON を処理/防止しますか? そうであれば、どのようにしますか?

LLM は、JSON を生成するときに人間が犯すのと同じ間違いを多く犯します (例: 間違った場所に } がある、またはカンマが抜けている)。そのため、フレームワークがこれらのエラーの処理を支援できることが重要です。

多くのフレームワークは、不正な形式の JSON を LLM に送り返し、JSON を修復するように要求することで、この問題を解決します。これはある程度機能しますが、速度が遅く、コストもかかります。LLM 呼び出しが個別にすでに数秒かかる場合、さらに速度を遅くすることは望ましくありません。

これを処理または防止するための手法は 2 つあります。不正な JSON を実際に解析する (BAML はこのアプローチを採用) か、LLM のトークン生成を制限して有効な JSON が生成されることを保証する (これは、Outlines、Guidance、およびその他のいくつかの手法で行われていることです) です。

不正なJSONを解析することが私たちの推奨するアプローチです: LLM の設計目的 (トークンの発行) に最も近い、高速 (数マイクロ秒で完了)、柔軟性 (どの LLM でも動作可能) を備えています。ただし、制限もあります。まったく意味のない JSON を魔法のように理解することはできません。

対照的に、LLM トークン生成に制約を適用すると堅牢になりますが、独自の問題があります。これを効率的に実行するには、モデル自体にランタイム変換を適用する必要があるため、これは自己ホスト型モデル (Llama、Transformers など) でのみ機能し、OpenAI の ChatGPT や Anthropic の Claude などのモデルでは機能しません。

実際のプロンプトを見ることができますか? プロンプトを完全に制御できますか?

これは覚えているかもしれない 「くそったれ、プロンプトを見せろ」

プロンプトは、LLM を「プログラム」して出力を生成する方法です。

LLMに構造化されたデータを返すための最良の方法は、特定のスキーマに一致するデータを返すように設計されたプロンプトを作成することです。そのためには、

  1. 実際にChatGPTに送信されるプロンプトを確認し、
  2. さまざまなプロンプトを試してください。

残念ながら、ほとんどのフレームワークには、これを防ぐハードコードされたテンプレートが組み込まれています。

サンプルコード

上記の各フレームワークには、フレームワークのドキュメントから、その使用方法を示すサンプル コードが含まれています。


BAML (Python)

から baml-examples/fastapi-starter/fast_api_starter/app.py:

from baml_client import b
 
resume = """John Doe [...] Experience: Software Engineer Intern [...]"""
 
async def async_call():
  parsed = await b.ExtractResume(resume)
 
async def streamed_call():
  stream = b.stream.ExtractResume(resume)
  async for partial in stream:
    print(partial) # This is an object with auto complete for the partial Resume type
  response = await stream.get_final_result() # auto complete here to the full Resume type
 

から baml-examples/fastapi-starter/baml_src/extract_resume.baml:

class Resume {
  name string
  education Education[]
  skills string[]
}
 
class Education {
  school string
  degree string
  year int
}
 
function ExtractResume(raw_text: string) -> Resume {
  client GPT4
  prompt #"
    Parse the following resume and return a structured representation of the data in the schema below.
 
    Resume:
    ---
    {{raw_text}}
    ---
 
    Output JSON format (only include these fields, and no others):
    {{ ctx.output_format(prefix=null) }}
 
    Output JSON:
  "#
}

BAML(TS)

から baml-examples/nextjs-starter/app/api/example_baml/route.ts:

import b from './baml_client'
import { Role } from './baml_client/types';
 
// Async call
const result = await b.ClassifyMessage({
    convo: [
        {
            role: Role.Customer,
            content: "I want to cancel my subscription"
        }
    ]
});
 
// Streamed call
const stream = b.stream.ClassifyMessage({
    convo: [
        {
            role: Role.Customer,
            content: "I want to cancel my subscription"
        }
    ]
});
 
for await (const partial of stream) {
    console.log(partial); // Autocompletes to a Category[]
}
const final = await stream.get_final_result(); // Autocompletes to a Category[]

から baml-examples/nextjs-starter/baml_src/classify_message.baml:

enum Category {
    Refund
    CancelOrder
    TechnicalSupport
    AccountIssue
    Question
}
 
class Message {
  role Role
  content string
}
 
enum Role {
  Customer
  Assistant
}
 
template_string PrintMessage(msg: Message, prefix: string?) #"
  {{ _.role('user' if msg.role == "Customer" else 'assistant') }}
  {% if prefix %}
  {{ prefix }}
  {% endif %}
  {{ msg.content }}
"#
 
function ClassifyMessage(convo: Message[]) -> Category[] {
  client GPT4
  prompt #"
    {# 
      Prompts are auto-dedented and trimmed.
      We use JINJA for our prompt syntax
      (but we added some static analysis to make sure it's valid!)
    #}
 
    {{ ctx.output_format(prefix="Classify with the following json:") }}
 
    {% for c in convo %}
    {{ PrintMessage(c, 
      'This is the message to classify:' if loop.last and convo|length > 1 else null
    ) }}
    {% endfor %}
 
    {{ _.role('assistant') }}
    JSON array of categories that match:
  "#
}

BAML (ルビー)

から baml-ruby-starter/examples.rb:

require_relative "baml_client/client"
 
b = Baml::BamlClient.from_directory("baml_src")
 
input = "Can't access my account using my usual login credentials"
classified = b.ClassifyMessage(input: input)
 
puts classified.categories

から baml-ruby-starter/baml_src/classify_message.baml:

enum Category {
    Refund
    CancelOrder
    TechnicalSupport
    AccountIssue
    Question
}
 
class MessageFeatures {
    categories Category[]
}
 
function ClassifyMessage(input: string) -> MessageFeatures {
  client GPT4Turbo
 
  prompt #"
    {# _.role("system") starts a system message #}
    {{ _.role("system") }}
 
    Classify the following INPUT.
 
    {{ ctx.output_format }}
 
    {# This starts a user message #}
    {{ _.role("user") }}
 
    INPUT: {{ input }}
 
    Response:
  "#
}

インストラクター(Python)

から simple_prediction.py:

class Labels(str, enum.Enum):
    SPAM = "spam"
    NOT_SPAM = "not_spam"
 
class SinglePrediction(BaseModel):
    """
    Correct class label for the given text
    """
 
    class_label: Labels
 
def classify(data: str) -> SinglePrediction:
    return client.chat.completions.create(
        model="gpt-3.5-turbo-0613",
        response_model=SinglePrediction,
        messages=[
            {
                "role": "user",
                "content": f"Classify the following text: {data}",
            },
        ],
    )  # type: ignore
 
prediction = classify("Hello there I'm a nigerian prince and I want to give you money")
assert prediction.class_label == Labels.SPAM

インストラクター-js

から simple_prediction/index.ts:

import { z } from "zod"
 
enum CLASSIFICATION_LABELS {
  "SPAM" = "SPAM",
  "NOT_SPAM" = "NOT_SPAM"
}
 
const SimpleClassificationSchema = z.object({
  class_label: z.nativeEnum(CLASSIFICATION_LABELS)
})
 
const createClassification = async (data: string) => {
  const classification = await client.chat.completions.create({
    messages: [{ role: "user", content: `"Classify the following text: ${data}` }],
    model: "gpt-3.5-turbo",
    response_model: { schema: SimpleClassificationSchema, name: "SimpleClassification" },
    max_retries: 3,
    seed: 1
  })
 
  return classification
}
 
const classification = await createClassification(
  "Hello there I'm a nigerian prince and I want to give you money"
)
// OUTPUT: { class_label: 'SPAM' }
 
console.log({ classification })
 
assert(
  classification?.class_label === CLASSIFICATION_LABELS.SPAM,
  `Expected ${classification?.class_label} to be ${CLASSIFICATION_LABELS.SPAM}`
)

TypeChat (Python)

から examples/sentiment/demo.py:

import asyncio
 
import sys
from dotenv import dotenv_values
import schema as sentiment
from typechat import Failure, TypeChatJsonTranslator, TypeChatValidator, create_language_model, process_requests
 
async def main():    
    env_vals = dotenv_values()
    model = create_language_model(env_vals)
    validator = TypeChatValidator(sentiment.Sentiment)
    translator = TypeChatJsonTranslator(model, validator, sentiment.Sentiment)
 
    async def request_handler(message: str):
        result = await translator.translate(message)
        if isinstance(result, Failure):
            print(result.message)
        else:
            result = result.value
            print(f"The sentiment is {result.sentiment}")
 
    file_path = sys.argv[1] if len(sys.argv) == 2 else None
    await process_requests("😀> ", file_path, request_handler)
 
 
if __name__ == "__main__":
    asyncio.run(main())

から examples/sentiment/schema.py:

from dataclasses import dataclass
from typing_extensions import Literal, Annotated, Doc
 
@dataclass
class Sentiment:
    """
    The following is a schema definition for determining the sentiment of a some user input.
    """
 
    sentiment: Annotated[Literal["negative", "neutral", "positive"],
                         Doc("The sentiment for the text")]

タイプチャット (TypeScript)

から examples/sentiment/src/main.ts:

import { createJsonTranslator, createLanguageModel } from "typechat";
import { processRequests } from "typechat/interactive";
import { createTypeScriptJsonValidator } from "typechat/ts";
import { SentimentResponse } from "./sentimentSchema";
 
const dotEnvPath = findConfig(".env");
assert(dotEnvPath, ".env file not found!");
dotenv.config({ path: dotEnvPath });
 
const model = createLanguageModel(process.env);
const schema = fs.readFileSync(path.join(__dirname, "sentimentSchema.ts"), "utf8");
const validator = createTypeScriptJsonValidatorSentimentResponse>(schema, "SentimentResponse");
const translator = createJsonTranslator(model, validator);
 
// Process requests interactively or from the input file specified on the command line
processRequests("😀> ", process.argv[2], async (request) => {
    const response = await translator.translate(request);
    if (!response.success) {
        console.log(response.message);
        return;
    }
    console.log(`The sentiment is ${response.data.sentiment}`);
});

から examples/sentiment/src/sentimentSchema.ts:

export interface SentimentResponse {
    sentiment: "negative" | "neutral" | "positive";  // The sentiment of the text
}

TypeChat (C#/.NET)

から examples/Sentiment/Program.cs:

using Microsoft.TypeChat;
 
namespace Sentiment;
 
public class SentimentApp : ConsoleApp
{
    JsonTranslatorSentimentResponse> _translator;
 
    public SentimentApp()
    {
        OpenAIConfig config = Config.LoadOpenAI();
        // Although this sample uses config files, you can also load config from environment variables
        // OpenAIConfig config = OpenAIConfig.LoadFromJsonFile("your path");
        // OpenAIConfig config = OpenAIConfig.FromEnvironment();
        _translator = new JsonTranslatorSentimentResponse>(new LanguageModel(config));
    }
 
    public override async Task ProcessInputAsync(string input, CancellationToken cancelToken)
    {
        SentimentResponse response = await _translator.TranslateAsync(input, cancelToken);
        Console.WriteLine($"The sentiment is {response.Sentiment}");
    }
}

から examples/Sentiment/SentimentSchema.cs:

using System.Text.Json.Serialization;
using Microsoft.TypeChat.Schema;
 
namespace Sentiment;
 
public class SentimentResponse
{
    [JsonPropertyName("sentiment")]
    [JsonVocab("negative | neutral | positive")]
    public string Sentiment { get; set; }
}

マーヴィン

から マーヴィンドキュメント:

import marvin
from pydantic import BaseModel
 
class Recipe(BaseModel):
    name: str
    cook_time_minutes: int
    ingredients: list[str]
    steps: list[str]
 
@marvin.fn
def recipe(
    ingredients: list[str], 
    max_cook_time: int = 15, 
    cuisine: str = "North Italy", 
    experience_level:str = "beginner"
) -> Recipe:
    """
    Returns a complete recipe that uses all the `ingredients` and 
    takes less than `max_cook_time`  minutes to prepare. Takes 
    `cuisine` style and the chef's `experience_level` into account 
    as well.
    """


#LLM #から構造化された出力を得るためのあらゆる方法

執筆者について: nipponese

Nipponese News編集部は、国内外のニュースを日本語で分かりやすくお届けします。