Open3

「DSPy 3.0」を改めて試す ④Tutorials: Build AI Programs with DSPy

kun432kun432

ちょっと順番が前後しちゃってるけども、前回の続き。

https://zenn.dev/kun432/scraps/e8b13dcfe7a08c

引き続き、DSPyをアプリケーションフレームワークとして使う、という観点で見ていく。

DSPy公式チュートリアルの「Build AI Programs with DSPy」にはアプリケーション作成にフォーカスしたチュートリアルが12個ほど用意されている。

https://dspy.ai/tutorials/

  • 会話履歴の管理
  • DSPy を使った AI エージェントの構築
  • DSPyモジュールをカスタマイズしてAIアプリケーションを構築する
  • 検索拡張生成(RAG)
  • エージェントとしてのRAGシステム構築
  • エンティティ抽出
  • 分類タスク
  • マルチホップ RAG
  • プライバシー保護を考慮した委任処理
  • 思考プログラム
  • 画像生成プロンプトの反復処理
  • 音声

全部は多分やれないと思うけど、気になったものをピックアップしてやっていくつもり。

kun432kun432

会話履歴の管理

https://dspy.ai/tutorials/conversation_history/

DSPyではdspy.Moduleで自動的に会話履歴を管理するような機能はないが、会話履歴管理を支援するユーティリティとしてdspy.Historyがある。

事前準備

Colaboratoryで試す

!pip install -U dspy
!pip show dspy
出力
Name: dspy
Version: 3.0.3
Summary: DSPy
Home-page: https://github.com/stanfordnlp/dspy
Author: 
Author-email: Omar Khattab <okhattab@stanford.edu>
License: 
Location: /usr/local/lib/python3.12/dist-packages
Requires: anyio, asyncer, backoff, cachetools, cloudpickle, diskcache, gepa, joblib, json-repair, litellm, magicattr, numpy, openai, optuna, orjson, pydantic, regex, requests, rich, tenacity, tqdm, xxhash
Required-by: 
from google.colab import userdata
import os

os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')

dspy.Historyを使った会話履歴管理

dspy.History クラスは、messages: list[dict[str, Any]]で会話履歴を保存するための箱となる。で、

  • 入力フィールドとしてdspy.History型の入力フィールドをシグネチャに指定する
  • 会話履歴インスタンスを作成して、新しい会話履歴のターンを追加する。追加する各エントリは、関連するすべての入出力フィールドを含める必要がある。

となる。

import dspy

dspy.settings.configure(lm=dspy.LM("openai/gpt-4o-mini"))

class QA(dspy.Signature):
    question: str = dspy.InputField()
    history: dspy.History = dspy.InputField()
    answer: str = dspy.OutputField()

predict = dspy.Predict(QA)

# 会話履歴を初期化
history = dspy.History(messages=[])

print("会話を開始します。終了するときは、'quit' / 'q' を入力してください。")
while True:
    question = input("ユーザ:")
    if question in ["quit", "q", "終了"]:
        break
    # historyパラメータで会話履歴を有効化して推論
    outputs = predict(question=question, history=history)
    print(f"AI: {outputs.answer}\n")

    # このターンの入出力を会話履歴に保存
    history.messages.append({"question": question, **outputs})

なるほど、この仕組みだと確かに保存は自分でやらないといけないね。

実行結果

出力
会話を開始します。終了するときは、'quit' / 'q' を入力してください。
ユーザ:おはようございます。私の名前は太郎です。
AI: おはようございます、太郎さん!今日はどんなことをお話ししましょうか?

ユーザ:私の趣味は競馬なんですよ。
AI: 競馬が趣味なんですね!どのようなレースが好きですか?また、好きな馬や騎手はいますか?

ユーザ:私について教えて。
AI: あなたは競馬が趣味の太郎さんですね!競馬に興味を持っているということは、レースや馬についての知識が豊富かもしれません。どのようなことに特に興味がありますか?

ユーザ:quit

dspy.inspect_history()を見てみる。

出力
[2025-10-03T15:03:33.725531]

System message:

Your input fields are:
1. `question` (str): 
2. `history` (History):
Your output fields are:
1. `answer` (str):
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## question ## ]]
{question}

[[ ## history ## ]]
{history}

[[ ## answer ## ]]
{answer}

[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        Given the fields `question`, `history`, produce the fields `answer`.


User message:

[[ ## question ## ]]
おはようございます。私の名前は太郎です。


Assistant message:

[[ ## answer ## ]]
おはようございます、太郎さん!今日はどんなことをお話ししましょうか?

[[ ## completed ## ]]


User message:

[[ ## question ## ]]
私の趣味は競馬なんですよ。


Assistant message:

[[ ## answer ## ]]
競馬が趣味なんですね!どのようなレースが好きですか?また、好きな馬や騎手はいますか?

[[ ## completed ## ]]


User message:

[[ ## question ## ]]
私について教えて。

Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.


Response:

[[ ## answer ## ]]
あなたは競馬が趣味の太郎さんですね!競馬に興味を持っているということは、レースや馬についての知識が豊富かもしれません。どのようなことに特に興味がありますか?

[[ ## completed ## ]]

ドキュメントには

言語モデルに実際に送信されるプロンプトは、dspy.inspect_history の出力が示すように、複数ターンにわたるメッセージです。各会話ターンは、ユーザーのメッセージに続いてアシスタントのメッセージという形で表現されます。

とある。historyがどこにも展開されていないんだけど、そこはマルチターンメッセージとして表現される、ってことなのかな?

ちょっとこのあとの章にも気になることが書いてあるんだけど、一旦ローカルのMacでMLflowのトレーシングを有効にして、同じことをやってみた。

import dspy
import mlflow

# トレーシングの設定(各環境ごとに適宜設定)
mlflow.set_tracking_uri("http://host.docker.internal:5000")
mlflow.set_experiment("DSPy")
mlflow.dspy.autolog()

dspy.settings.configure(lm=dspy.LM("openai/gpt-4o-mini"))

class QA(dspy.Signature):
    question: str = dspy.InputField()
    history: dspy.History = dspy.InputField()
    answer: str = dspy.OutputField()

predict = dspy.Predict(QA)

history = dspy.History(messages=[])

print("会話を開始します。終了するときは、'quit' / 'q' を入力してください。")
while True:
    question = input("ユーザ:")
    if question in ["quit", "q", "終了"]:
        break
    outputs = predict(question=question, history=history)
    print(f"AI: {outputs.answer}\n")

    history.messages.append({"question": question, **outputs})

実行結果

出力
会話を開始します。終了するときは、'quit' / 'q' を入力してください。
ユーザ: おはよう!私の名前は太郎です。
AI: おはよう、太郎さん!今日はどんなことを話しましょうか?

ユーザ: 私の趣味は競馬です。
AI: 競馬が趣味なんですね!どのレースや馬が特に好きですか?

ユーザ: 私について知ってることを教えて。
AI: あなたの名前は太郎さんで、趣味は競馬だということを知っています。他に何か教えてもらえますか?

ユーザ: q

MLFlowで最後のリクエストのトレース野中のLLMとの入出力の部分はこんな感じ。

どうやらマルチターンで展開されるように見える。シグネチャの定義は何かしらプロンプト内でゴニョゴニョされるものだと思ってるんだけど(ただし開発者はそれを意識しないのがDSPy)、その点を踏まえると若干モヤるが・・・

とりあえずdspy.inspect_history()である程度確認はできそう。

Few-Shotにおける会話履歴

プロンプトの入力フィールドセクションには履歴が表示されないことに気付くかもしれません。システムメッセージでは入力フィールドとしてリストされているにもかかわらずです(例: 「2. history (History):」)。これは意図的な仕様です: 会話履歴を含むFew Shotの例をフォーマットする際、DSPyは履歴を複数のターンに展開しません。代わりに、OpenAI標準フォーマットとの互換性を保つため、各Few Shotの例は単一のターンとして表現されます。

説明を読んでも何がいいたいのか自分にはさっぱりわからなかった。とりあえずサンプルコードを実行してみる。引き続き、MLflowでトレーシングを有効化してある。

import dspy

mlflow.set_tracking_uri("http://host.docker.internal:5000")
mlflow.set_experiment("DSPy")
mlflow.dspy.autolog()

dspy.settings.configure(lm=dspy.LM("openai/gpt-4o-mini"))

class QA(dspy.Signature):
    question: str = dspy.InputField()
    history: dspy.History = dspy.InputField()
    answer: str = dspy.OutputField()


predict = dspy.Predict(QA)
history = dspy.History(messages=[])

predict.demos.append(
    dspy.Example(
        question="フランスの首都は?",
        history=dspy.History(
            messages=[{"question": "ドイツの首都は?", "answer": "ドイツの首都はベルリンです。"}]
        ),
        answer="フランスの首都はパリです。",
    )
)

predict(question="日本の首都は?", history=dspy.History(messages=[]))
dspy.inspect_history()

とりあえず知らないものがでてきた。以下。

predict.demos.append(
    dspy.Example(
        question="フランスの首都は?",
        history=dspy.History(
            messages=[{"question": "ドイツの首都は?", "answer": "ドイツの首都はベルリンです。"}]
        ),
        answer="フランスの首都はパリです。",
    )
)

以下を読むとどうやらこれがFew Shotになるみたい。

https://dspy.ai/learn/evaluation/data/#dspy-example-objects

そして、モジュールにはdemosという属性があって、これはどうやらdspy.Adapterに渡されて、LLMへのプロンプト内にFew Shotとして埋め込まれる、ということになるみたい。

上記の例がややこしいのはFew Shotの中に会話履歴があるんだよね。ただこれは追加されることがない会話履歴なので、つまり会話履歴を踏まえたFew Shotというふうに自分は感じた。

とりあえず実行してみる。

出力
[2025-10-03T16:03:41.040369]

System message:

Your input fields are:
1. `question` (str): 
2. `history` (History):
Your output fields are:
1. `answer` (str):
All interactions will be structured in the following way, with the appropriate values filled in.

[[ ## question ## ]]
{question}

[[ ## history ## ]]
{history}

[[ ## answer ## ]]
{answer}

[[ ## completed ## ]]
In adhering to this structure, your objective is: 
        Given the fields `question`, `history`, produce the fields `answer`.


User message:

[[ ## question ## ]]
フランスの首都は?

[[ ## history ## ]]
{"messages": [{"question": "ドイツの首都は?", "answer": "ドイツの首都はベルリンです。"}]}


Assistant message:

[[ ## answer ## ]]
フランスの首都はパリです。

[[ ## completed ## ]]


User message:

[[ ## question ## ]]
日本の首都は?

Respond with the corresponding output fields, starting with the field `[[ ## answer ## ]]`, and then ending with the marker for `[[ ## completed ## ]]`.


Response:

[[ ## answer ## ]]
日本の首都は東京です。

[[ ## completed ## ]]

ややこしいのだが、ここがFew Shot。で、これはマルチターンで展開されているように見える。

出力
User message:

[[ ## question ## ]]
フランスの首都は?

[[ ## history ## ]]
{"messages": [{"question": "ドイツの首都は?", "answer": "ドイツの首都はベルリンです。"}]}


Assistant message:

[[ ## answer ## ]]
フランスの首都はパリです。

[[ ## completed ## ]]

上記の中のFew Shot内の会話履歴はマルチターンで展開されずにhistoryフィールドにJSONオブジェクトして埋め込まれている。

出力
User message:

[[ ## question ## ]]
フランスの首都は?

[[ ## history ## ]]
{"messages": [{"question": "ドイツの首都は?", "answer": "ドイツの首都はベルリンです。"}]}

MLflowのトレース。

うーん、改めてこの章の意図がさっぱりわからない。要は

  • 会話履歴はdspy.Historyで管理して、これを入力フィールドとしてシグネチャに含めれば、OpenAIなどで一般的なマルチターンのmessages形式に良しなに展開してくれる。(ただし追加は自分で管理)
  • Few Shotも同様にマルチターンの会話履歴として展開される。
  • ただしFew Shot内に会話履歴を含めると、その会話履歴については展開されない。

ってことが言いたいのかな?


とりあえずこのチュートリアルの章、文章の書き方がハイコンテキスト過ぎて、めちゃめちゃわかりにくい・・・「評価」のところを先にやったら理解できたのかな?と思って読み直してみたけど、やっぱりわからん・・・