[翻訳] DSPyプログラミング

[Programming Overview \- DSPy](https://dspy.ai/learn/programming/overview/)以降のセクション[2025/1/15時点]の翻訳です。

:::note warn
本書は著者が手動で翻訳したものであり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
:::

:::note info
[\[翻訳\] DSPyを学ぶ](https://qiita.com/taka_yayoi/items/df31bd8f1918855fd552)のコンテンツです。
:::


# DSPyにおけるプログラミング

DSPyは*文字列ではなくコードの記述*にベットしています。言い換えると、適切なコントロールフローを構築することが重要です。**あなたのタスクの定義**からスタートします。あなたのシステムへの入力は何であり、そのシステムは出力として何を生成すべきですか？あるいは、翻訳のためのシステム、検索結果でスニペットをハイライトするためのもの、もしくは引用と共にレポートを生成するためのものかもしれません。

次に、**あなたの初期パイプラインを定義します**。あなたのDSPyプログラムは単一のモジュールでしょうか、あるいは複数のステップにブレークダウンする必要があるでしょうか？計算機やカレンダーAPIのような、検索や他のツールが必要でしょうか？複数の適切にスコープされたステップであなたの問題を解決するための典型的なワークフローがありますか？あるいは、あなたのタスクのためのエージェントによる、よりオープンエンドなツールを活用したいと考えていますでしょうか？これらについて考えつつも、シンプルにスタートしましょう。おそらく、単一の`dspy.ChainOfThought`モジュールからスタートし、観察に基づいて徐々に複雑性を追加しましょう。

これを行う過程で、あなたのプログラムに対する入力の**多数の例を作成、トライする**ことになります。この時点で、何が可能であるのかを理解するためだけに、パワフルなLM(言語モデル)を活用したり、複数の異なるLMを活用することを検討します。あなたが試す興味深い(簡単なものと難しいもの両方の)サンプルを記録します。これは、あとで評価と最適化を行う際に有用なものとなります。

:::note info
**優れたデザインパターンを推奨する以上に、ここでDSPyはどのような助けになるのでしょうか？**

従来のプロンプトは、新たなLMや目的、パイプラインに転用できない臨時的に選択した基本的なシステムアーキテクチャと組み合わされます。従来のプロンプトは、特定のタイプのいくつかの入力を受け取り、いくつかの出力を生成するようにLMにお願いし、特定の手段で入力をフォーマットし、正確にパースできる形態で出力をリクエストし、「ステップバイステップで考えて」のような特定の戦略を適用したり、ツール(*モジュール*のロジック)を活用するようにLMにお願いし、これらを行うようにそれぞれのLMにお願いする適切な方法を発見するための膨大なトライアンドエラーを必要とします(ある種、手動の*最適化*です)。

DSPyはこれらの心配事を分離し、あなたが検討を必要とするまで低レベルの心配事を自動化します。これによって、あなたはより高い可搬性を持つより短いコードを記述することが可能となります。例えば、DSPyモジュールを用いてプログラムを記述すると、あなたのロジックの残りの部分を変更することなしに、LMやアダプターを切り替えることができます。あるいは、お使いのシグネチャを変更することなしに、`dspy.ChainOfThought`のようなあるモジュールを、別の`dspy.ProgramOfThought`と交換することができます。オプティマイザを使う準備ができた際には、同じプログラムで最適化されたプロンプトや重みがファインチューンされたLMを使うことができます。
:::

# 言語モデル

すべてのDSPyコードの最初のステップは、あなたの言語モデルをセットアップすることです。例えば、以下のようにデフォルトのLMとしてOpenAIのGPT-4o-miniを設定することができます。

```py
# Authenticate via `OPENAI_API_KEY` env: import os; os.environ['OPENAI_API_KEY'] = 'here'
lm = dspy.LM('openai/gpt-4o-mini')
dspy.configure(lm=lm)
```

:::note info
[原文](https://dspy.ai/learn/programming/language_models/)には他のLMを設定する例が記載されています。
:::


## LMを直接コール

上で設定した`lm`を直接呼び出すのは簡単です。統合APIを提供しており、自動キャッシュのようなユーティリティのメリットを享受することができます。

```py
lm("Say this is a test!", temperature=0.7)  # => ['This is a test!']
lm(messages=[{"role": "user", "content": "Say this is a test!"}])  # => ['This is a test!']
```

## DSPyモジュールとLMを使用

慣用的なDSPyには、次のガイドで議論する*モジュール*の活用が含まれます。

```py
# Define a module (ChainOfThought) and assign it a signature (return an answer, given a question).
qa = dspy.ChainOfThought('question -> answer')

# Run with the default LM configured with `dspy.configure` above.
response = qa(question="How many floors are in the castle David Gregory inherited?")
print(response.answer)
```

**出力例:**

```
The castle David Gregory inherited has 7 floors.
```

## 複数のLMを使用

`dspy.configure`を用いて、グローバルなデフォルトLMを変更したり、`dspy.context`を用いてコードブロック内で変更することができます。

:::note info
**ティップ**
`dspy.configure`と`dspy.context`の使用はスレッドセーフです！
:::

```py
dspy.configure(lm=dspy.LM('openai/gpt-4o-mini'))
response = qa(question="How many floors are in the castle David Gregory inherited?")
print('GPT-4o-mini:', response.answer)

with dspy.context(lm=dspy.LM('openai/gpt-3.5-turbo')):
    response = qa(question="How many floors are in the castle David Gregory inherited?")
    print('GPT-3.5-turbo:', response.answer)
```

**出力例:**

```
GPT-4o: The number of floors in the castle David Gregory inherited cannot be determined with the information provided.
GPT-3.5-turbo: The castle David Gregory inherited has 7 floors.
```

## LM生成の設定

すべてのLMに対して、初期化の際や以降のそれぞれのコールの際に、以下の属性を設定することができます。

```py
gpt_4o_mini = dspy.LM('openai/gpt-4o-mini', temperature=0.9, max_tokens=3000, stop=None, cache=False)
```

デフォルトでは、DSPyでLMはキャッシュされます。同じコールを繰り返すと、同じ出力を取得します。しかし、`cache=False`を設定することでキャッシュをオフにすることができます。

## 出力と使用量メタデータの調査

すべてのLMオブジェクトは、入力、出力、トークン使用量(と$$$のコスト)、メタデータを含むインタラクションの履歴を保持します。

```py
len(lm.history)  # e.g., 3 calls to the LM

lm.history[-1].keys()  # access the last call to the LM, with all metadata
```

**出力:**

```
dict_keys(['prompt', 'messages', 'kwargs', 'response', 'outputs', 'usage', 'cost'])
```

## 上級: 顧客LMの構築、自分のアダプターを記述

あまり必要とされませんが、`dspy.BaseLM`を継承することでカスタムのLMを記述することができます。DSPyエコシステムの別の高度なレイヤーは、DSPyシグネチャとLMの間に存在する*アダプター*です。あなたがこれらを必要とすることはほとんどありませんが、このガイドの将来的なバージョンではこれらの高度な機能を議論します。

# シグネチャ

DSPyでLMにタスクを割り当てる際、必要な挙動をシグネチャとして指定します。

**シグネチャは、DSPyモジュールの入力/出力の挙動に対する宣言的な仕様です。** シグネチャによって、LMにすべきことを*どのように*お願いすべきかを指定するのではなく、*何*をする必要があるのかをLMに指示することができます。

おそらく、あなたは関数の入力と出力の引数と型を指定する関数のシグネチャには慣れ親しんでいることでしょう。DSPyシグネチャは似ていますが、いくつかの違いがあります。典型的な関数のシグネチャは物事を*記述*するだけですが、DSPyシグネチャはモジュールの挙動を*宣言*、*初期化*します。さらに、DSPyシグネチャにおいてはフィールド名が重要となります。簡単な英語で意味論的なロールを表現します: `question`は`answer`とは異なり、`sql_query`は`python_code`とは違います。

## なぜDSPyシグネチャを使わなくてはならないのか？

モジュール化されクリーンなコードにおいては、LMのコールは高品質のプロンプトに最適化(あるいは自動でのファインチューン)することができます。多くの人々は、長く不安定なプロンプトをハッキングすることで、LMにタスクを行うことを強制します。あるいは、ファインチューニングにおいてはデータを収集/生成することでこれを行います。シグネチャを記述することは、プロンプトやファインチューンをハッキングすることよりも、はるかにモジュール的で、適応的で、再現可能なものです。DSPyコンパイラは、あなたのシグネチャやデータに対して、お使いのLMに対して高度に最適化されたプロンプトをどのように構築すべきかを導出します。多くの場合、このコンパイルによって人間が記述するよりも優れたプロンプトにつながることを見出しました。これは、DSPyが人間よりも創造的である訳ではなく、単に多くのことをトライすることができ、直接メトリクスをチューニングできるからです。

## インラインDSPyシグネチャ

シグネチャは、入力/出力の意味論的なロールを定義する引数の名前と(オプションの)型を持つ、短い文字列として定義することができます。

1. Q&A: `"question -> answer"`、これはデフォルトタイプを常に`str`にする`"question: str -> answer: str"`と等価です
1. 感情分類: `"sentence -> sentiment: bool"`、例えばポジティブな場合には`True`
1. 要約: `"document -> summary"`

シグネチャには、複数の入力/出力を型と共に含めることができます:

1. 検索拡張Q&A: `"context: list[str], question: str -> answer: str"`
1. 理由付けを伴う複数選択Q&A: `"question, choices: list[str] -> reasoning: str, selection: int"`

:::note info
**ティップ**
フィールドに関しては、すべての適切な変数名で動作します！フィールド名は意味論的に意味があるものであるべきですがシンプルなものからスタートして、延々とキーワードを最適化しないようにしましょう！その種のハッキングはDSPyコンパイラに任せましょう。例えば、要約においては、`"document -> summary"`、`"text -> gist"`、`"long_context -> tldr"`などと言うので多分大丈夫です。
:::

### 例A: 感情分類

```py
sentence = "it's a charming and often affecting journey."  # example from the SST-2 dataset.

classify = dspy.Predict('sentence -> sentiment: bool')  # we'll see an example with Literal[] later
classify(sentence=sentence).sentiment
```

**出力:**

```
True
```

### 例B: 要約

```py
# Example from the XSum dataset.
document = """The 21-year-old made seven appearances for the Hammers and netted his only goal for them in a Europa League qualification round match against Andorran side FC Lustrains last season. Lee had two loan spells in League One last term, with Blackpool and then Colchester United. He scored twice for the U's but was unable to save them from relegation. The length of Lee's contract with the promoted Tykes has not been revealed. Find all the latest football transfers on our dedicated page."""

summarize = dspy.ChainOfThought('document -> summary')
response = summarize(document=document)

print(response.summary)
```

**出力例:**

```
The 21-year-old Lee made seven appearances and scored one goal for West Ham last season. He had loan spells in League One with Blackpool and Colchester United, scoring twice for the latter. He has now signed a contract with Barnsley, but the length of the contract has not been revealed.
```

(`dspy.Predict`を除く)多くのDSPyモジュールは、内部であなたのシグネチャを拡張することで、周辺の情報を返却します。

例えば、`dspy.ChainOfThought`は出力の`summary`を生成する前に、LMの理由付けを含む`reasoning`フィールドも追加します。

```py
print("Reasoning:", response.reasoning)
```

**出力例:**

```
Reasoning: We need to highlight Lee's performance for West Ham, his loan spells in League One, and his new contract with Barnsley. We also need to mention that his contract length has not been disclosed.
```

## クラスベースのDSPyシグネチャ

いくつかの高度なタスクにおいては、より冗長なシグネチャが必要となります。これは通常以下のことを行います:

1. タスクの性質に関する何かを明確にする(以下では`docstring`として表現)
1. 入力フィールドの性質に関するヒントを提供する。`dspy.InputField`の`desc`キーワードの引数として表現。
1. 出力フィールドの制約を提供する。`dspy.OutputField`の`desc`キーワードの引数として表現。

### 例C: 分類

```py
from typing import Literal

class Emotion(dspy.Signature):
    """Classify emotion."""

    sentence: str = dspy.InputField()
    sentiment: Literal['sadness', 'joy', 'love', 'anger', 'fear', 'surprise'] = dspy.OutputField()

sentence = "i started feeling a little vulnerable when the giant spotlight started blinding me"  # from dair-ai/emotion

classify = dspy.Predict(Emotion)
classify(sentence=sentence)
```

**出力例:**

```
Prediction(
    sentiment='fear'
)
```

:::note info
**ティップ**
より明確にLMにリクエストを指定することには何も悪いことはありません。クラスベースのシグネチャは、それを行う助けとなります。しかし、手でシグネチャのキーワードを延々とチューニングしないでください。DSPyオプティマイザはおそらく優れた仕事をしてくれます(そして、LM横断でうまく転用できます)。
:::

### 例D: 引用の公正性を評価するメトリクス

```py
class CheckCitationFaithfulness(dspy.Signature):
    """Verify that the text is based on the provided context."""

    context: str = dspy.InputField(desc="facts here are assumed to be true")
    text: str = dspy.InputField()
    faithfulness: bool = dspy.OutputField()
    evidence: dict[str, list[str]] = dspy.OutputField(desc="Supporting evidence for claims")

context = "The 21-year-old made seven appearances for the Hammers and netted his only goal for them in a Europa League qualification round match against Andorran side FC Lustrains last season. Lee had two loan spells in League One last term, with Blackpool and then Colchester United. He scored twice for the U's but was unable to save them from relegation. The length of Lee's contract with the promoted Tykes has not been revealed. Find all the latest football transfers on our dedicated page."

text = "Lee scored 3 goals for Colchester United."

faithfulness = dspy.ChainOfThought(CheckCitationFaithfulness)
faithfulness(context=context, text=text)
```

**出力例:**

```
Prediction(
    reasoning="Let's check the claims against the context. The text states Lee scored 3 goals for Colchester United, but the context clearly states 'He scored twice for the U's'. This is a direct contradiction.",
    faithfulness=False,
    evidence={'goal_count': ["scored twice for the U's"]}
)
```

## モジュールを構築 & コンパイルするためにシグネチャを活用

構造化された入力/出力のプロトタイピングにシグネチャは便利ですが、それだけがシグネチャを使う理由ではありません！

より大きな[DSPyモジュール](#モジュール)で複数のシグネチャを構成すべきですし、[それらのモジュールを最適化されたプロンプトやファインチューンにコンパイル](https://dspy.ai/learn/optimization/optimizers/)すべきです。

# モジュール

**DSPyモジュール**は、LMを使うプログラムにおけるビルディングブロックです。

- ビルトインモジュールのそれぞれは、(チェーンオブソートやReActのような)**プロンプティングテクニック**を抽象化します。重要なことですが、それらはすべてのシグネチャに対応できるように汎化されています。
- DSPyモジュールには(プロンプトやLMの重みから構成される小さな部品である)**学習可能なパラメーター**があり、入力を処理し、出力を返却するために呼び出すことが可能です。
- 複数のモジュールをより大きなモジュール(プログラム)に組み合わせることができます。DSPyモジュールは、PyTorchのNNモジュールによる直接的にインスパイアをされておりますが、LMプログラムに適用されるものです。

## `dspy.Predict`や`dspy.ChainOfThought`のようなビルトインのモジュールをどのように使うべきですか？

最も基本的なモジュール`dspy.Predict`からスタートしましょう。内部では、すべての他のDSPyモジュールは`dspy.Predict`を用いて構築されています。最低でもあなたはすでに、DSPyで使用するすべてのモジュールの挙動を定義する宣言型の仕様である[DSPyシグネチャ](#シグネチャ)にある程度慣れ親しんでいるものとして話を進めます。

モジュールを使うには、初めにシグネチャを与えることでモジュールを**宣言**します。そして、入力の引数と共にモジュールを**コール**し、出力フィールドを抽出します！

```py
sentence = "it's a charming and often affecting journey."  # example from the SST-2 dataset.

# 1) Declare with a signature.
classify = dspy.Predict('sentence -> sentiment: bool')

# 2) Call with input argument(s). 
response = classify(sentence=sentence)

# 3) Access the output.
print(response.sentiment)
```

**出力**

```
True
```

モジュールを宣言する際、モジュールに対する設定のキーを渡すことができます。

以下では、5つのコンプリーションをリクエストするために`n=5`を渡します。また、`temperature`や`max_len`といった設定を渡すこともできます。

`dspy.ChainOfThought`を使ってみましょう。多くの場合、`dspy.Predict`が使われている場所で`dspy.ChainOfThought`に切り替えることで、品質が改善されます。

```py
question = "What's something great about the ColBERT retrieval model?"

# 1) Declare with a signature, and pass some config.
classify = dspy.ChainOfThought('question -> answer', n=5)

# 2) Call with input argument.
response = classify(question=question)

# 3) Access the outputs.
response.completions.answer
```

**出力例:**

```
['One great thing about the ColBERT retrieval model is its superior efficiency and effectiveness compared to other models.',
 'Its ability to efficiently retrieve relevant information from large document collections.',
 'One great thing about the ColBERT retrieval model is its superior performance compared to other models and its efficient use of pre-trained language models.',
 'One great thing about the ColBERT retrieval model is its superior efficiency and accuracy compared to other models.',
 'One great thing about the ColBERT retrieval model is its ability to incorporate user feedback and support complex queries.']
```

それでは、出力オブジェクトについて議論しましょう。`dspy.ChainOfThought`モジュールは一般的に、あなたのシグネチャの出力フィールドの前に`reasoning`を注入します。

(初めての)理由付けと回答を調査してみましょう！

```py
print(f"Reasoning: {response.reasoning}")
print(f"Answer: {response.answer}")
```

**出力例:**

```
Reasoning: We can consider the fact that ColBERT has shown to outperform other state-of-the-art retrieval models in terms of efficiency and effectiveness. It uses contextualized embeddings and performs document retrieval in a way that is both accurate and scalable.
Answer: One great thing about the ColBERT retrieval model is its superior efficiency and effectiveness compared to other models.
```

1つ以上のコンプリーションをリクエストした場合には、こちらにアクセスすることができます。

また、一つの要素がそれぞれのフィールドに対応する`Prediction`のリストや、いくつかのリストとして、別のコンプリーションにアクセスすることができます。

```py
response.completions[3].reasoning == response.completions.reasoning[3]
```

**出力:**

```
True
```

## 他のDSPyモジュールにはどのようなものがありますか？どのように使いますか？

他のものも非常に似ています。それらは主に、シグネチャが実装される内部の挙動を変更します！

1. `dspy.Predict`: 基本的な予測器です。シグネチャを変更しません。学習のキーとなる形態(指示の格納とデモンストレーション、LMの更新)を取り扱います。
1. `dspy.ChainOfThought`: シグネチャのレスポンスをコミットする前に、ステップバイステップで考えるようにLMに教えます。
1. `dspy.ProgramOfThought`: 実行結果がレスポンスとなるようなコードを出力するようにLMに教えます。
1. `dspy.ReAct`: 指定されたシグネチャを実動するためにツールを使うことができるエージェントです。
1. `dspy.MultiChainComparison`: 最終的な推論結果を生成するために`ChainOfThought`からの複数の出力を比較することができます。

また、関数スタイルのモジュールも提供しています。

1. `dspy.majority`: 一連の推論結果から最も人気のレスポンスを返却するように、基本的な投票を行うことができます。

:::note info
[原文](https://dspy.ai/learn/programming/modules/)には、数学、RAGなどシンプルなタスクにおけるDSPyモジュールの例が記載されています。

**数学**

```py
math = dspy.ChainOfThought("question -> answer: float")
math(question="Two dice are tossed. What is the probability that the sum equals two?")
```

**出力例:**

```
Prediction(
    reasoning='When two dice are tossed, each die has 6 faces, resulting in a total of 6 x 6 = 36 possible outcomes. The sum of the numbers on the two dice equals two only when both dice show a 1. This is just one specific outcome: (1, 1). Therefore, there is only 1 favorable outcome. The probability of the sum being two is the number of favorable outcomes divided by the total number of possible outcomes, which is 1/36.',
    answer=0.0277776
)
```

:::

## どのようにして複数のモジュールで大きなプログラムを構成しますか？

DSPyは、あなたのLMコールをトレースするために、あなたの好きな制御フローでモジュールを使い、`compile`の際に内部的にちょっとした魔法をかけるPythonコードにすぎません。

以下に例で示しているモジュールを使う[マルチホップ検索](https://dspy.ai/tutorials/multihop_search/)のようなチュートリアルをご覧ください。

```py
class Hop(dspy.Module):
    def __init__(self, num_docs=10, num_hops=4):
        self.num_docs, self.num_hops = num_docs, num_hops
        self.generate_query = dspy.ChainOfThought('claim, notes -> query')
        self.append_notes = dspy.ChainOfThought('claim, notes, context -> new_notes: list[str], titles: list[str]')

    def forward(self, claim: str) -> list[str]:
        notes = []
        titles = []

        for _ in range(self.num_hops):
            query = self.generate_query(claim=claim, notes=notes).query
            context = search(query, k=self.num_docs)
            prediction = self.append_notes(claim=claim, notes=notes, context=context)
            notes.extend(prediction.new_notes)
            titles.extend(prediction.titles)

        return dspy.Prediction(notes=notes, titles=list(set(titles)))
```