こんにちは
AIチームの戸田です
LLMを使ったアプリケーション開発において、Prompt Engineeringは避けては通れません。しかしタスクやドメインによって最適なPromptは異なりますし、ほんの一文追加しただけで出力が大きく変わってしまうなど、試行錯誤にかかるコストが課題になってきます。
そこで最近Promptを自動的に最適化するツールが注目されています。有名なものだとDSPyがあります。
DSPyはLLMアプリケーションのためのPrompt最適化フレームワークです。Promptをコードとして記述することができ、タスクに応じたPromptやFew-shot Learningのサンプリングの自動最適化を行うことができます。
DSPyはPythonで実装されています。しかし現在私はTypeScriptベースの環境で開発をしており、Python を混ぜたくない、という思いがあったので、TypeScript で使える同種のツールがないか探していました。そして先日、ax というDSPyの思想をTypeScriptに移植したフレームワークを見つけました。本記事では axで簡単なPrompt最適化を試してみた内容を共有します。
環境構築
Node.js 18以上、TypeScript 5.0以上が要件です
インストール方法は以下です。私はnpmで試しました。
npm install @ax-llm/ax
# または
yarn add @ax-llm/ax
# または
pnpm add @ax-llm/axLLMプロバイダーのAPI Keyを登録します。私は.envを使いました。
OPENAI_API_KEY=your_api_key_here
# または他のプロバイダーのAPIキー検証
ax は「DSPy for TypeScript」と言われており、DSPyでできることはほぼ実装可能なようです。DSPyでどんなことができるかについては日本語だと以下の記事が良かったので、「そもそもDSPyとは?」という方はまずはこちらをご参照いただければと思います。
本記事ではDSPyの機能の中でも基本的なSignatureと呼ばれる宣言的なPrompt定義とMiPROと呼ばれるPrompt最適化手法を試してみた結果を共有します。
Signature
従来のPrompt Engineeringでは、文字列を直接記述する必要がありました。例えば極性分類を解かせようとすると以下のようなPromptが考えられます。
// 従来の方法
const prompt = `
あなたは感情分析の専門家です。
以下のテキストの感情を分析してください。
テキスト: ${text}
以下の形式で回答してください:
- 感情: positive, negative, neutralのいずれか
- 理由: 判断の理由を簡潔に
`;これでも極性分類というタスクは解けるのですが、出力フォーマットの解析が属人的で、メンテナンスが困難です。
これをaxは以下のように記述できます。
// axの場合
const classifier = ax(
'review:string -> sentiment:class "positive, negative, neutral", reason:string',
);この定義からaxはPromptを生成します。プログラミングの型定義ライクで出力フォーマットの解析が容易だと思います。
以下にシンプルな極性分類を行うTypeScriptのコードを載せます。reviewsの文章はChatGPTに適当に考えてもらいました。
import { ai, ax } from "@ax-llm/ax";
import dotenv from "dotenv";
dotenv.config();
const llm = ai({ name: "openai", apiKey: process.env.OPENAI_API_KEY! });
const classifier = ax(
'review:string -> sentiment:class "positive, negative, neutral", reason:string',
);
async function main() {
const reviews = [
"このプロダクトは素晴らしい!",
"最悪の買い物でした。二度と購入しません。",
"普通です。特に良くも悪くもない。",
"サポートが親切で大変満足しています!",
"値段の割に品質が悪すぎる。がっかりです。",
"まあまあですね。可もなく不可もなく。",
"期待以上の出来栄え!友人にもおすすめしたいです。",
"配送が遅れて困りました。商品自体は普通です。",
"コスパ最高!買ってよかった!",
"不良品が届きました。返品します。"
];
for (const review of reviews) {
const result = await classifier.forward(llm, { review });
console.log(`レビュー: ${review}`);
console.log(`極性: ${result.sentiment}`);
console.log(`理由: ${result.reason}`);
console.log("---\n");
}
}
main();実行結果
レビュー: このプロダクトは素晴らしい!
極性: positive
理由: レビューが「素晴らしい」と明確に賞賛しており、肯定的な評価を示しているため。
---
レビュー: 最悪の買い物でした。二度と購入しません。
極性: negative
理由: 「最悪の買い物でした。二度と購入しません。」と明確に不満と再購入の否定を述べているためネガティブ。
---
レビュー: 普通です。特に良くも悪くもない。
極性: neutral
理由: 「普通」「特に良くも悪くもない」と明記しており、肯定的でも否定的でもないため中立的と判断した。
---
レビュー: サポートが親切で大変満足しています!
極性: positive
理由: サポートが「親切で大変満足しています」と明確に肯定的な評価を述べているため。
---
レビュー: 値段の割に品質が悪すぎる。がっかりです。
極性: negative
理由: 価格に対して品質が低く「がっかり」と失望していると明確に述べているため。
---
レビュー: まあまあですね。可もなく不可もなく。
極性: neutral
理由: レビューは「まあまあ」「可もなく不可もなく」と中立的な表現で、明確な好意や不満が示されていないため。
---
レビュー: 期待以上の出来栄え!友人にもおすすめしたいです。
極性: positive
理由: 「期待以上の出来栄え」と満足を示し「友人にもおすすめしたい」と推奨しているため、肯定的な感想と判断できる。
---
レビュー: 配送が遅れて困りました。商品自体は普通です。
極性: negative
理由: 配送が遅れて困ったと明確な不満を述べており、商品自体は「普通」と評価しているため、全体としてネガティブな印象です。
---
レビュー: コスパ最高!買ってよかった!
極性: positive
理由: 「コスパ最高」「買ってよかった」とあり、価格対性能の良さと購入への満足を明確に示しているため。
---
レビュー: 不良品が届きました。返品します。
極性: negative
理由: 不良品が届き返品すると述べており、不満を表しているためネガティブな評価です。
---近年のLLMの性能を考えると当たり前かもしれませんが納得感のある結果になっているのではないでしょうか。ただ、Promptに関しては圧倒的に管理しやすくなっていると思います。
ちなみに使用されるLLMモデルですが、name: "openai"とするとデフォルトではGPT-5 miniが使われるようです。
MiPRO
axはDSPyと同様のOptimizer(Promptの最適化手法)を持っています。その中でもMiPRO: Multi-Prompt OptimizationはPromptとFew-shot Learningのサンプリングを同時に最適化するOptimizerになります。
MiPROはまだaxのTypeScript実装が完全ではないようで、Pythonの推論サーバーを起動する必要があります。以下の手順で起動しておきましょう。
git clone https://github.com/ax-llm/ax/tree/main
cd ax/src/optimizer
uv sync
uv run ax-optimizer server start --debugSignatureの時と同様、簡単な極性分類で試してみたいと思います。前半の実装部分はほぼ変わりません。今回はPromptの最適化を行うため、どのような入力に対してどのような出力が理想的なのかの訓練データを渡します。
import { ai, ax, MiPRO } from "@ax-llm/ax"; // AxGEPAをimport しておく
...
// 訓練データ
const examples = [
{ "review": "このプロダクトは素晴らしい!", "sentiment": "positive" },
{ "review": "最悪の買い物でした。二度と購入しません。", "sentiment": "negative" },
{ "review": "普通です。特に良くも悪くもない。", "sentiment": "neutral" },
{ "review": "サポートが親切で大変満足しています!", "sentiment": "positive" },
{ "review": "値段の割に品質が悪すぎる。がっかりです。", "sentiment": "negative" },
{ "review": "まあまあですね。可もなく不可もなく。", "sentiment": "neutral" },
{ "review": "期待以上の出来栄え!友人にもおすすめしたいです。", "sentiment": "positive" },
{ "review": "配送が遅れて困りました。商品自体は普通です。", "sentiment": "negative" },
{ "review": "コスパ最高!買ってよかった!", "sentiment": "positive" },
{ "review": "不良品が届きました。返品します。", "sentiment": "negative" }
];
// 評価関数
const metric = ({ prediction, example }) => {
return prediction.sentiment === example.sentiment ? 1 : 0;
};ChatGPT登場以前に文書分類タスクのモデルを扱った経験のある方には馴染み深いデータ形式なのではないでしょうか。そして今回は訓練データ自体にラベルが付いているので、出力がラベルと一致していれば1、異なっていれば0という評価関数になっています。関数内部は自由に設定できるので、LLM as a Judgeを導入することもできるようです。
続けて最適化関数の実行部分です。章の頭で言及したPythonの推論サーバーをoptimizerEndpointに設定します。
const optimizer = new MiPRO({
studentAI: llm,
examples,
optimizerEndpoint: "http://localhost:8000",
optimizerTimeout: 60000, // optimizerEndpointのタイムアウト(ms)
optimizerRetries: 3, // optimizerEndpointに接続できなかった時のリトライ回数
numTrials: 1, // 試行回数
verbose: true,
});最後に最適化の実行を行います。
const result = await optimizer.compile(
classifier,
examples,
metric
);
// ベストスコアを表示
console.log('Optimized score:', result.bestScore); // 1.0そもそもSignatureの時に何もしなくても100%で正解できていたので、特に最適化の余地はありませんでしたが、一通りの流れを追うことができました。
おわりに
本記事では、TypeScript版DSPyといわれているaxを使って、Signatureによる宣言的なPrompt定義とMiPROによるPrompt最適化を試してみました。
MiPROの他にもGEPAのようなより複雑なOptimizerがあったり、prompt生成時のtemperatureや一度に処理するサンプル数のminibatchなど、調整すべきパラメータもあるので、まだ検証すべきことは多そうです。一方訓練データとは別に評価用データを設定できるvalidationExamplesなどがまだ実装されていなかったり(参考)するので今後の改善にも期待です。
余談ですが、axはドキュメントとしてDevinの生成したDeepWikiを使っていました。困りごとをピンポイントで聞けたり、気になった実装に即ジャンプして確認など非常に良い使用体験でした。今後はOSSに限らず、このようなAIが対話的にナビゲーションしてくれる「動的ドキュメント」が当たり前になっていくのかもしれません。
最後までお読みいただきありがとうございました。

