LayerX エンジニアブログ

LayerX の エンジニアブログです。

AI Agent時代における「使えば使うほど賢くなるAI機能」の開発

LayerX のバクラク事業部の AI・機械学習部で機械学習エンジニアをしている島越(@nt_4o54)です。こちらはLayerX AI Agent ブログリレー 31 日目の記事です。 昨日は松村 (@yu__ya4)による「Langfuse の Experiment Runner SDK を利用した AI エージェント機能の性能評価と実験管理」でした。 無事にこのブログリレーも日付換算で一ヶ月を突破しました。過去のブログ記事も知見が溢れているので、是非ご覧ください!

はじめに

今回は、AI 機能をプロダクトに組み込む上で重要な観点である「使えば使うほど賢くなる AI 機能」を開発するために必要なことについて紹介します。 「使えば使うほど賢くなる AI 機能」において大事な要素は、我々が提供している SaaS のようなプロダクトにおいては「パーソナライゼーション」だと考えます。 個々のお客様の運用ルールや業務に合わせて、最初は間違えたとしても適応していくことが重要です。

近年の AI 技術、特に大規模言語モデル(LLM)の進化は目覚ましく、LLM を組み込んだプロダクトも増えてきています。 それに伴いプロダクトにおける「AI のパーソナライゼーション」という概念も転換期を迎えているように感じます。
LLM 以前の機械学習システムにおいてパーソナライゼーションとは、個別のデータセットでモデル自体を定期的に再学習させたり、特徴量を更新したりするモデル中心のアプローチが主流でした。 しかし、LLM を基盤とした AI Agent がプロダクトの主流となる現在においては、モデルを高頻度に更新させることは計算コストや運用負荷の観点から(一部の企業を除いて)現実的ではありません。

そこで今回は、AI Agent 機能開発でどのようにパーソナライゼーションするか、具体的な手法について紹介し、最後に DSPy を用いた具体的な実装についても紹介します。

LLM 以前の機械学習システムにおけるパーソナライゼーション

LayerX では、LLM が登場する以前から AI-OCR に代表される機械学習モデルを内製してシステムに組み込んでいます。 去年には「パーソナライズド AI-OCR」というお客様の運用に合わせて必要な値を推薦するという機能をリリースしています。

bakuraku.jp

パーソナライズド AI-OCR は、ドキュメントからお客様が選択し得るあらゆる候補を抽出する「候補抽出モデル」とその中からお客様に合わせて候補を並び替える「推薦モデル」の二つのモデルで成り立っています。 この機能においては、後段の推薦モデルをお客様の過去の選択履歴などを用いて学習させ、また定期的に特徴量を更新することでパーソナライゼーションを実現しています。

このような従来の機械学習システムにおいては、「どのように学習モデルを作るか」と「モデルが認識できる形でユーザのフィードバックをどう集めるか」という部分が重要でした。 前者においては推薦モデルのような形でモデルを作成したり、(運用が複雑にはなりますが)ある程度のグループごとにモデルを個別に作成するといったことが考えられます。

また、後者においてはプロダクトの UI/UX などを意識した上で、ユーザの負担にならない形でデータを集めることが重要になります。 弊社の AI-OCR においては、以下のようにプルダウンで候補を表示することで、ユーザが負担にならない形でデータを修正できるような工夫かつ構造化されたデータを収集できるようにしています。

更に、これらのデータを用いて Vertex AI Pipelines によって定期的な更新することで「使えば使うほど賢くなる」を実現しています。

findy-tools.io

このように、従来は、ユーザのフィードバックをどのように取るかを考慮した上でのモデル設計や特徴設計を考え、更にそれらを定期的に更新するモデル中心のアプローチが主流でした。 では、これが LLM が登場したことによってどう変化しているのでしょうか。

LLM を用いたシステムにおけるパーソナライゼーション

パーソナライズド AI-OCR のような「フィードバックに基づくモデルの更新」というアプローチを LLM において取ろうとすると、計算コストや運用負荷が非常に高くなります。 そもそも、LLM というのは汎用的な知識が既にパラメータに内包されているので、わざわざファインチューニングを追加で行わなくても、プロンプトで上手に指示してあげるだけで大体のユースケース上手くいきます。 そのため、最近では「Context Engineering」と言われる手法で LLM に渡す情報を調整することで性能改善を行うのが主流です。 Context Engineering についてはこちらの記事でも詳細に説明が書かれていますので、細かい説明は省きます。

tech.layerx.co.jp

要するに、従来は「モデルのパラメータを学習によって調整して最適化」していましたが、LLM の登場により「モデル自体は固定した上で、入力される情報(コンテキスト)をどう設計するかによって最適化」する方向にシフトしました。例えば、顧客ごとの業務ルールや過去の承認履歴などを動的にプロンプトに含めることで、モデルを再学習せずともパーソナライゼーションが可能になります。(Data Centric AI と言うこともあります) 学習させる必要がなくなったため楽になったと感じる人もいるかもしれないですが、実際にはそんなことは全然ありません。

Context Engineering において、Prompt をどう管理・最適化するかや、個別のユースケースに対して Context をどう調整してパーソナライゼーションするか、など考えるべき課題はたくさんあります。 更に、実際の業務において課題を解決しようとする場合、単一の LLM で解決することは少なく、タスクを分解し、それぞれに専門の LLM を用いることで課題を解決することも多いので、ワークフロー的な要素も加味した上で考える必要も出てきます。

この LLM の特性を考慮しながら「使えば使うほど賢くなる」AI 機能を構築するにはどうしたらいいでしょうか。 以下で代表的な例について、論文などを参考にいくつか紹介します。

In-Context Learning (ICL): Prompt による動的適応

ICL は、 LLM が登場した当初から使われている基本的でありながら強力な手法の一つです。 これは、Prompt 内にいくつかの具体例(Few-shot Example)を提示するだけで、モデルがそのタスクのパターンを考慮した上で出力を調整します。 実務であれば、まずこれを試す方が多いと思います。

強力である一方で、ICL はユーザのフィードバックデータをどのように入力すればいいのか、どのように Prompt を記述すればいいのかは手作業で試行錯誤しなければいけません。 また、賢くさせるための要素が Few-shot Example だけになるので、最適化できる部分は限定的です。 実際には、お客様ごとに業務は異なるため、適用したい指示やルールなどの要素もパーソナライゼーションしたくなります。 そのため、より効率的にデータから体系的に Prompt を最適化するような手法が登場してきています。

Prompt の自動最適化

Prompt の自動最適化の研究は色々な方向性で行われています。 A Systematic Survey of Automatic Prompt Optimization Techniques*1によると、以下のように研究は体系化されます。

自動プロンプト最適化の系譜(*1から引用)

これらの研究は、それぞれ以下のような内容になります。

  1. Seed Prompts: 改善する初期プロンプト生成戦略
  2. Inference evaluation and feedback: どのように予測を評価するか、フィードバックを得るか
  3. Candidate prompt generation: どのようにして改善した prompt を生成するか
  4. Filter and retain promising candidates: 探索・活用技術を用いた高性能な Prompt を特定するための選択戦略
  5. Iteration depth: 最適化期間を決定する終了基準をどのように特定するか

これらを組み合わせることで以下の図のように Prompt の最適化が可能になります。

Prompt自動作成の体系化(*1から引用)

特に実務において重要になるのは、2 の要素になると考えます。

実際に最適化できるようにそもそもどうやって評価するのか、ユーザのフィードバックをどのように収集するのかをプロダクトに組み込む段階で考えなければ改善は回せません。 では、どのように評価、データ収集を行えばいいでしょうか。

既存の機械学習モデルでは、定量化した数値など微分可能な関数でしか学習を回せませんでした。 しかし、最近では評価に LLM を用いる LLM-as-a-Judge やそれを応用した TextGrad*2といった手法も登場しています。 そのため、定量化できないようなシステムにおいても「文章の簡潔さ」「正確さ」などを定義して評価することで最適化することが可能になりました。 なので、ユーザの体験として定義できる指標を準備し、それによって評価を回すことが重要になります。

また、ユーザのフィードバックは、機能によって千差万別だと思いますが、最低限ユーザがその出力を受け入れたのかなどを収集できる仕組みは必須です。もちろんテキストフィードバックがあれば、直接的な改善の指示になるので重要なのですが、ユーザに負担がかかってしまいます。負担をかけないようにしたい場合は、最低限の仕組みを作っておいて、そのデータから暗黙的に改善の指示を LLM に考えさせる、といった TextGrad 的な手法も考えられます。(余談ですが、テキストフィードバックはモデルの学習という観点では扱いにくいデータだったのですが、この辺りも時代の移り変わりを感じます。)

弊社でも使っている Greptile*3 と言う AI Code Review サービスでは、絵文字によるリアクションや PR へのコメントなどから複合的に学習を行っているとドキュメント*4に書いており、上手くフィードバック情報を利用してパーソナライゼーションを実現しているサービスの一つだと思います。

まとめると、Prompt を最適化するためにユーザのフィードバックを集め、それを評価できる仕組みを作ることでより性能の高い Prompt を生成できることが様々な研究によって明らかになっています。 これらのサイクルをプロダクトに組み込むことで、ユーザ別に Prompt を最適化させることも夢ではないと思います。 もちろん学習コストはあるのですが、機械学習モデルをユーザごとに作るのに比べると保持するのはテキストだけなので運用コストを小さくした上でパーソナライゼーションの幅が広がります。

その他の最適化手法

前節では、Prompt の最適化に焦点をおいて説明しましたが、上述したように単一の LLM モデルの Prompt をいくら最適化したとしても実行できるタスクの複雑さには限界があります。そのため、タスクを分解した上で個別の LLM モデルを繋げ、ワークフロー形式で問題解決を行うことが実際には多くなります。

このワークフロー生成を自律的に行う AFlow*5といった手法も存在します。詳細は省きますが、単一のPrompt最適化と同じように評価データをフィードバックにワークフローを実行するコードを生成させます。以下が概略図ですが、各ノードのPrompt (の一部の変数)に加え、その Node をどのように繋げるのかのコードも同時に生成して評価を繰り返します。

AFlowの概略図 (*5から引用)

国内だと Sakana AI さんが最近発表した ShinkaEvolve*6など、事前に準備しておいたPromptを用いて、プログラムを最適化していくといった手法も存在します。

このように、様々な問題解決を自動化する手法が存在します。これらをタスクに合わせて検証し、パーソナライゼーションされた体験を届けていくことが AI Agent 時代におけるプロダクト開発において重要になると予想されます。

DSPy による Prompt 最適化実践

ここからは、実際に DSPy*7というフレームワークを用いてPrompt最適化の実装を行います。 DSPy は、Databricks のリサーチサイエンティストが開発した Prompt を意識せずに LLM アプリケーションを作成できるフレームワークです。 詳細については、以下のスライドなどが分かりやすいですが、検証も手軽に実行でき非常に便利です。

speakerdeck.com

今回は、経費精算の承認作業を行う Agent を作成するというタスクで Prompt 最適化の効力を検証します。

データセット

以下のような Prompt を使い、擬似的にデータセットを 1000 件作りました。この Seed Prompts 自体も Claude Code にやってもらいましたが、ある程度 Pydantic で型を作った上で指示すれば大分まともなものを作ってくれました。

データセット作成Prompt

# バリエーションの例
variations_list = [
    "出張経費(新幹線、宿泊、食事)",
    "接待交際費(会食、手土産)",
    "オフィス消耗品購入",
    "オンライン広告費用",
    "セミナー・研修参加費",
    "タクシー代",
    "不適切: 個人的な支出が混在",
    "不適切: 金額が異常に高い",
    "不適切: 日付が未来または極端に古い",
    "不適切: 勘定科目と内容の不一致",
    "不適切: 部門と内容の不一致",
    "不適切: 使用用途がdesciptionに書かれていない",
    "不適切: 領収書必須の申請で領収書が添付されていない",
]

SYSTEMP_PROMPT = """
あなたは、経費精算フォームの申請データを擬似的に作成するエージェントです。

<purpose>
リアルで多様性のある経費精算申請データを生成し、適切なもの・不適切なものを混在させる。
</purpose>

<generation_criteria>

<diversity>
データの多様性を以下の観点で確保してください:

<input_types>

- transportation: 交通費(電車、タクシー、飛行機など)
- card: クレジットカード決済
- upload_file: 領収書アップロード
- allowance: 手当・日当
  </input_types>

<account_items>
交通費、接待交際費、会議費、消耗品費、通信費、旅費交通費、
水道光熱費、研究開発費、広告宣伝費、福利厚生費など
</account_items>

<amount_ranges>

- 小額: 100 円〜5,000 円(コーヒー代、文房具など)
- 中額: 5,001 円〜50,000 円(会食、備品購入など)
- 高額: 50,001 円〜500,000 円(出張費、大型備品など)
- 超高額: 500,001 円以上(イベント費用、設備投資など)
  </amount_ranges>

<detail_counts>

- 少量: 1-3 件
- 中量: 4-10 件
- 大量: 11-30 件
  </detail_counts>

</diversity>

<inappropriate_patterns ratio="0.3-0.4">
以下のような不適切なパターンを含めてください:

<amount_issues>

- 異常に高額な支出(例: コーヒー代 50,000 円)
- 小数点の誤り(例: 1000.555 円)
- 負の金額
- 合計金額と明細の不一致
  </amount_issues>

<date_issues>

- 未来の日付
- 極端に古い日付(5 年以上前など)
- 申請タイトルと明細日付の整合性がない
- 連続した日付で不自然な出張経路
  </date_issues>

<content_inconsistency>

- 勘定科目と取引先名が不一致(例: 交通費なのに飲食店名)
- 勘定科目と金額が不自然(例: 消耗品費で 50 万円の MacBook)
- input_type と勘定科目の不一致(例: transportation なのに接待交際費)
- 部門名と経費の関連性が薄い(例: 人事部でサーバー費用)
  </content_inconsistency>

<description_issues>

- 基本的には経費の理由を記載するが、理由が適切でない
- 備考欄が空白
- 使用用途が明確に記載されていない
- 適切でない支出のパターン(例: 何となくタクシーに乗った、役員なのでグリーン車を使った、急いでたからタクシーを使った)
  </description_issues>

<duplicates_contradictions>

- 同一日・同一金額の重複明細
- 同じ取引先への不自然な複数支払い
  </duplicates_contradictions>

</inappropriate_patterns>

<appropriate_patterns ratio="0.6-0.7">
以下のような適切なパターンを含めてください:

<common_expenses>

- 営業活動に関連する交通費・接待費
- 業務に必要な備品購入
- 出張に伴う旅費・宿泊費
- 会議・セミナー参加費
  </common_expenses>

<well_organized>

- 明確なタイトル(例: "2024 年 10 月 大阪出張経費")
- 詳細な備考(目的、参加者、成果など)
- 適切な勘定科目の選択
- 合理的な金額設定
  </well_organized>

</appropriate_patterns>

</generation_criteria>

<output_format>
FormItem モデルに準拠した JSON 形式で出力してください。
</output_format>

<constraints>
1. 日本の企業を想定したリアルな取引先名・部門名を使用
2. 日付は2025年9月〜2025年10月の範囲で設定
3. 各申請データは独立したストーリー性を持たせる
4. 適切/不適切の判断が明確になるよう特徴を際立たせる
5. エッジケースも含める(境界値、特殊文字など)
</constraints>
"""

USER_PROMPT_TEMPLATE = """
<task>
経費精算申請データを生成してください。
</task>

<requirements>
<quantity>{num}件</quantity>
<appropriate_ratio>{appropriate_ratio}%</appropriate_ratio>
<inappropriate_ratio>{inappropriate_ratio}%</inappropriate_ratio>

<distribution>
<detail_counts>
- 少量: 30%
- 中量: 50%
- 大量: 20%
</detail_counts>

<amounts>
- 小額: 40%
- 中額: 40%
- 高額: 15%
- 超高額: 5%
</amounts>
</distribution>
</requirements>

<variations>
以下のバリエーションを含めてください:
{variations}
</variations>
"""

これで作成されるデータは以下のような構造を持ちます。

class InputType(str, Enum):
    """経費精算の明細データの種類"""

    TRANSPORTATION = "transportation"
    CARD = "card"
    UPLOAD_FILE = "upload_file"
    ALLOWANCE = "allowance"


class Detail(BaseModel):
    """経費精算の明細データ"""

    detail_date: date = Field(description="明細日付")
    input_type: InputType = Field(description="明細の登録種別")
    client_name: str = Field(description="取引先名")
    section_name: str = Field(description="部門名")
    account_item: str = Field(description="勘定科目")
    payment_amount: float = Field(description="支払金額")
    description: str = Field(description="備考")
    has_receipt: bool = Field(description="領収書の添付有無")


class FormItem(BaseModel):
    """フォームの申請データ"""

    title: str = Field(description="フォームのタイトル")
    details: list[Detail] = Field(description="明細データ")


class ReviewResult(BaseModel):
    """レビュー結果"""

    is_approved: bool = Field(description="承認結果")
    comment: str = Field(description="コメント")
    form_item: FormItem = Field(description="フォームの申請データ")

このReviewResultis_approvedを予測できる Agent を作成するのが今回の目的になります。 複数の明細のうち一つでも不審なものがあれば差し戻しを行うというタスクになります。 学習・評価は 1000 件のうちまず 200 件をテストデータとし、残りの 800 件のうち 80%を訓練、20%を検証用のデータとしました。

推論の実装

DSPy では Signature と Module という概念があり、これらを用いて Pytorch Like に推論を組み立てることができます。 自分で書くのは以下のようなコードだけで、Prompt を意識することはありません。Prompt のようなテキストを運用するのはとても大変なので、これは助かります。 Signature の docstring を書くとその内容も後段の Prompt に記載してくれるので、自分で知識を注入したい場合は、docstring に書くと良さそうです。

class ExpenseApprovalSignature(dspy.Signature):
    """経費精算フォームを審査し、承認/否認を判定します。
    不適切な要素がある場合は'rejected'、問題がなければ'approved'と判定してください。
    判定理由も具体的に説明してください。
    """

    expense_form: str = dspy.InputField(
        desc="経費精算フォームの全情報(タイトル、明細一覧、合計金額を含む)"
    )
    approval_status: Literal["approved", "rejected"] = dspy.OutputField(
        desc="承認状態: approved(承認)またはrejected(否認)"
    )
    comment: str = dspy.OutputField(
        desc="判定理由の詳細なコメント(具体的な問題点や承認理由を明記)"
    )


class ExpenseApprovalModule(dspy.Module):
    """承認/否認を判定するモジュール"""

    def __init__(self):
        super().__init__()
        self.prog = dspy.ChainOfThought(ExpenseApprovalSignature)

    def forward(self, expense_form: str):
        return self.prog(expense_form=expense_form)

この Module を呼び出すだけで、簡単に LLM 実行を行うことができます。

lm = dspy.LM(
    "azure/gpt-4.1-mini",
    api_base=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_API_KEY"),
    model_type="responses",
    temperature=1.0,
    max_tokens=16000,
)
dspy.configure(lm=lm)
baseline_module = ExpenseApprovalModule()
pred = baseline_module(expense_form=test_examples[1])
print(pred)
# Prediction(
#    reasoning='出展費用としてのブース出展料、搬入費、現地打合せ用軽食、社員の日当といった費用内容は、東京展示会に関連するマーケティング活動として妥当である。取引先名の記載も具体的で、領収書も取得済み。合計金額も申請内容に対して適正な範囲内であるため、特に不備や不合理な点は見当たらない。したがって承認に値する。',
#    approval_status='approved',
#    comment='展示会出展に伴う広告宣伝費や会議費、日当などの経費として適切で、領収書も揃っているため問題なし。合計金額も合理的であるため承認する。'
# )

また実際に、dspy.inspect_history()でどのような Prompt が実行されているのかを確認することができます。何も最適化をしていないので、ほぼ Signature に記載されている内容だけが記載されています。 {reasoning}は Module によって内容が変わり、今回使用しているdspy.ChainOfThoughtの場合は、Reasoning: Let's think step by step in order toのような内容が追加されるようです。

System message:

Your input fields are:
1. `expense_form` (str): 経費精算フォームの全情報(タイトル、明細一覧、合計金額を含む)
Your output fields are:
1. `reasoning` (str):
2. `approval_status` (Literal['approved', 'rejected']): 承認状態: approved(承認)またはrejected(否認)
3. `comment` (str): 判定理由の詳細なコメント(具体的な問題点や承認理由を明記)
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## expense_form ## ]]
{expense_form}

[[ ## reasoning ## ]]
{reasoning}

[[ ## approval_status ## ]]
{approval_status}        # note: the value you produce must exactly match (no extra characters) one of: approved; rejected

[[ ## comment ## ]]
{comment}

[[ ## completed ## ]]
In adhering to this structure, your objective is:
        経費精算フォームを審査し、承認/否認を判定します。
        不適切な要素がある場合は'rejected'、問題がなければ'approved'と判定してください。
        判定理由も具体的に説明してください。


User message:

[[ ## expense_form ## ]]
【申請タイトル】
2025年9月 東京展示会 出展費用(マーケティング)

【明細一覧】

■ 明細 1
  日付: 2025-09-25
  登録種別: card
  取引先名: 展示会メーカー株式会社
  部門名: マーケティング部
  勘定科目: 広告宣伝費
  金額: ¥450,000
  備考: ブース出展料(10/1開催分)
  領収書: あり

■ 明細 2
  日付: 2025-09-24
  登録種別: upload_file
  取引先名: 搬入業者 佐藤商会
  部門名: マーケティング部
  勘定科目: 広告宣伝費
  金額: ¥52,000
  備考: 展示物搬入費
  領収書: あり

■ 明細 3
  日付: 2025-09-25
  登録種別: card
  取引先名: カフェ・ミヤコ
  部門名: マーケティング部
  勘定科目: 会議費
  金額: ¥3,200
  備考: 現地打合せ用軽食
  領収書: あり

■ 明細 4
  日付: 2025-09-26
  登録種別: allowance
  取引先名: 社内
  部門名: マーケティング部
  勘定科目: 旅費交通費
  金額: ¥6,000
  備考: 日当(外勤スタッフ2名分)
  領収書: なし

【合計金額】 ¥511,200
【明細数】 4件

Respond with the corresponding output fields, starting with the field `[[ ## reasoning ## ]]`, then `[[ ## approval_status ## ]]` (must be formatted as a valid Python Literal['approved', 'rejected']), then `[[ ## comment ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.

Prompt 最適化検証

以下のように approval_status を正しく予測できてるかを評価する関数を作成すれば準備完了です。DSPy には標準で様々な Optimizer が準備されているので、それを用いて評価・比較を行います。

def approval_accuracy_metric(example: dspy.Example, pred: dspy.Prediction, trace=None) -> float:
    """
    承認/否認の判定精度を評価

    Args:
        example: 正解データ
        pred: 予測結果
        trace: トレース情報(オプション)

    Returns:
        1.0(正解)または0.0(不正解)
    """
    # approval_statusが一致するかチェック
    is_correct = example.approval_status == pred.approval_status
    return float(is_correct)

今回は、以下の 3 つの Optimizer を比較しました。

Optimizer 内容 アルゴリズム
BootstrapFewShotWithRandomSearch 訓練セットから Random に Few-shot を複数生成し、検証セットで最も性能の良かったものを選択 Few-shot 自動化
KNNFewShot 訓練セットから予測するセットの近傍にいるものを KNN で抽出 Few-shot 自動化
MIPROv2 ベイズ最適化を用いて効率的に Prompt と Few-shot をデータ駆動で評価指標から生成 Few-shot & Instruction 自動化

これらを用いて、以下のように最適化を行います。また、モデルとしてはコストやレイテンシ的な観点から GPT4.1-mini を使いました。全ての実験を実行するのに 1 時間程度では終わりました。

## 実験1
bootstrap_random_search = BootstrapFewShotWithRandomSearch(
    metric=approval_accuracy_metric,
    max_bootstrapped_demos=4,  # 生成するデモの数
    max_labeled_demos=10,  # トレーニングセットから選択するデモの数
    num_candidate_programs=8,  # 評価する候補プログラムの数
    num_threads=20,
)

# 最適化実行(validation setで候補を選択)
optimized_module_bsrs = bootstrap_random_search.compile(
    student=ExpenseApprovalModule(),
    trainset=train_examples,  # トレーニングセット
    valset=val_examples,  # Validationセット
)

# 実験2
knn_fewshot = KNNFewShot(
    metric=approval_accuracy_metric,
    k=10,
    trainset=train_examples,
    vectorizer=dspy.Embedder(SentenceTransformer("hotchpotch/static-embedding-japanese").encode),
    max_bootstrapped_demos=4,  # 生成するデモの数
    max_labeled_demos=10,  # トレーニングセットから選択するデモの数
)

# 最適化実行
optimized_module_bs = knn_fewshot.compile(student=ExpenseApprovalModule())

# 実験3
mipro = MIPROv2(
    metric=approval_accuracy_metric,
    num_candidates=5,  # 候補数
    init_temperature=1.0,
    auto=None,
)

# 最適化実行
optimized_module_mipro = mipro.compile(
    student=ExpenseApprovalModule(),
    trainset=train_examples,  # トレーニングセット
    valset=val_examples,  # Validationセット
    num_trials=10,  # 試行回数
    max_bootstrapped_demos=4,
    max_labeled_demos=10,
)

結果

結果は以下のようになりました。確かにベースラインに比べると、今回のようなタスクでも性能が伸びている様子が確認できます。 このような技術を運用に載せることで、AI Agent のような機能においても「使えば使うほど賢くなる AI 機能」を実現できそうです。

Oprimizer 正解率 ベースラインからの差分
なし 87.94% +0.00%
BootstrapFewShotWithRandomSearch 92.46% +4.52%
KNNFewShot 91.46% +3.52%
MIPROv2 92.96% +5.02%

実際に、Prompt がどのように変わってるかというと、Few-shot expample 最適化の手法は、基本的に System message の後に、以下のような内容が N 回続くというような形になっているだけでした。

User message:
This is an example of the task, though some input or output fields are not supplied.

...事例が記載

Assistant message:

.. 正解ラベルが記載

一方で、MIPROv2 のような Instruction の最適化も同時に行うような手法の場合、以下のように指示の文章も変わってました。内容としても、実際にデータセット作成時に指示した悪い経費精算の例に沿った内容になってそうです。

Before
    経費精算フォームを審査し、承認/否認を判定します。
    不適切な要素がある場合は'rejected'、問題がなければ'approved'と判定してください。
    判定理由も具体的に説明してください。
After
    あなたは企業の経費精算担当者として、経費精算フォームを厳密かつ公正に審査する役割を担っています。企業の財務健全性と不正防止を守るため、日付、取引先、勘定科目、金額、領収書の有無、備考の内容など全ての情報を慎重に検証してください。
    不正確な日付や金額の異常、取引先と経費種別の不整合、領収書の欠落、過去の期限切れの申請など、どんな不適切な要素も見逃さずに発見し、「approved」(承認)または「rejected」(否認)を決定してください。
    判定結果に至るまでの考察はステップバイステップで思考過程として詳細に記述し、具体的かつ説得力のあるコメントで理由を明確に説明してください。限られた情報と複雑な状況の中で、企業の信頼とコンプライアンスを守るため、最も妥当な判断を行うよう努めてください。

ただし、こういうデータだと従来の機械学習のような万単位のデータで学習することなどはコスト的に厳しいため、どうしても局所解に陥ってしまうのではないかと感じました。そのため、使い道としては局所的にパーソナライゼーションを行いたいような部分で用いるのが良いのではないかと思います。

最後に

今回は、AI Agent 時代における「使えば使うほど賢くなる AI 機能」の開発というテーマで、LLM 以前、そして LLM 以後どのようにパーソナライゼーションを実現していくのかを論文や実際のサービスを引用しながら紹介しました。 更に、その中の一要素として重要な Prompt 最適化という技術について DSPy を用いた検証を行い、一定の効果が見えることが分かりました。 LLM 以前とは考え方も変わり、私自身も日々アンラーニングせねばという毎日ですが、開発としてやれることの幅は確実に広がっており楽しい時代だなと感じます。

実務だと、そもそも LLM に渡せていないコンテキストをどのように取得するのか、評価指標が単純な正解率でいいのか、どのタイミングで最適化をかけるのか、どの単位で最適化するのか、など考えることは多く単純ではないですが、このような手法を運用に載せていくことで「業務の完全自動運転化」に近づけていきます!

また、今回はLLMをFine Tuningしない前提での話をしましたが、もちろんLLMのFine Tuningも今後活用していく可能性は高いです。適材適所でToolを使い分けて開発を進めていくという話を以前にもしたので、以下のスライドも是非ご覧ください。

speakerdeck.com

このような AI Agent 開発を行い、運用に載せていく仲間を全方面で募集中です! 少しでも興味のある方、X の DM や以下のフォームからでも構わないので、お気軽にお声がけください!

jobs.layerx.co.jp jobs.layerx.co.jp

*1:A Systematic Survey of Automatic Prompt Optimization Techniques, https://arxiv.org/abs/2502.16923

*2:TextGrad: Automatic "Differentiation" via Text, https://arxiv.org/abs/2406.07496

*3:https://www.greptile.com/

*4:https://www.greptile.com/docs/how-greptile-works/memory-and-learning

*5:AFlow: Automating Agentic Workflow Generation, https://arxiv.org/abs/2410.10762

*6:ShinkaEvolve: Towards Open-Ended And Sample-Efficient Program Evolution, https://arxiv.org/abs/2509.19349

*7:https://dspy.ai/