---
title: "DSPy — Автоматическая генерация промптов"
url: "https://romankryvolapov.com/ru/dspy/"
description: "DSPy: как заменить ручной подбор промптов сигнатурами, метрикой и настройкой MIPROv2, чтобы при смене LLM модели промпт настраивался заново, а не переписывался руками."
language: ru
published: 2025-09-22
updated: 2026-11-05
---
Многие AI инженеры и не только, работающие с LLM и агентами, сталкиваются с задачами, когда корректность работы агента во многом зависит от промпта.

Для простых задач достаточно промпта в стиле "Ты умный агент, отвечай на вопросы пользователя". Но если агент должен делать сложные вызовы функций и обрабатывать нестандартные запросы, написать стабильный промпт для вызовов и для окончательного ответа непросто, а при смене LLM модели на другую ранее работавший промпт может перестать работать. Как писать такие промпты руками, разобрано в [статье о промпт-инжиниринге](/ru/prompt-engineering/), а как агент вызывает инструменты — в [статье о RAG и вызове функций](/ru/rag-and-function-calling/).

[DSPy](https://dspy.ai) ([GitHub](https://github.com/stanfordnlp/dspy)) превращает это из сочинения текста в измеримый процесс. Что он делает простыми словами:

- Вместо длинного текста вы описываете задачу как форму: какие есть входы и какие нужны выходы.
- Правила вызова инструментов фиксируются кодом, а не «словами» в промпте.
- Качество меряется метрикой, и DSPy подбирает формулировки под эту метрику.
- При смене модели или функций вы запускаете настройку заново, а не переписываете всё руками.

Что это даёт:

- Масштабирование: проще добавлять инструменты и новые форматы ответов.
- Дешёвую смену модели: она превращается в повторный запуск настройки, с оценкой до и после.
- Повторяемость: промпт хранится как артефакт и восстанавливается.
- Контроль: поведение задаётся структурой и метрикой, а не «магическим текстом».

Цена — набор данных и время на настройку; о них ниже. Пример — агент, который ищет в локальной базе данных: так видно, что агенты могут быть довольно сложными.

## Сигнатура и инструменты

Сигнатура говорит, какие поля приходят агенту и что он должен вернуть. Инструменты — обычные функции с чёткими входами и выходами:

```python
import sqlite3
from contextlib import closing

import dspy

# A local model in LM Studio; any model LiteLLM can reach will do.
dspy.configure(
    lm=dspy.LM(
        "openai/google/gemma-3n-e4b",
        api_base="http://localhost:1234/v1",
        api_key="lm-studio",
        temperature=0,
    )
)


def search_kb(query: str) -> list[str]:
    """Searches the knowledge base and returns the matching fragments."""
    with closing(sqlite3.connect("kb.db")) as db:
        rows = db.execute(
            "SELECT text FROM articles WHERE text LIKE ? LIMIT 5", (f"%{query}%",)
        ).fetchall()
    return [row[0] for row in rows]


class AnswerFromKb(dspy.Signature):
    """Answer the question using only the knowledge base."""

    question: str = dspy.InputField()
    answer: str = dspy.OutputField(desc="a short answer, no explanations")


agent = dspy.ReAct(AnswerFromKb, tools=[search_kb], max_iters=5)
```

## Данные

Набор данных — это список вопросов с правильными ответами:

```json
[
  {
    "question": "...",
    "answer": "..."
  }
]
```

По [документации DSPy](https://dspy.ai/learn/optimization/overview/), 30 примеров часто уже дают заметную пользу, но ориентироваться стоит минимум на 300, а для MIPROv2 она [советует](https://dspy.ai/learn/optimization/optimizers/) от 200, чтобы избежать переобучения. Важно, чтобы в них было разнообразие.

Разбиение обратно привычному по нейросетям: 20 процентов на обучение и 80 на валидацию, потому что оптимизаторы промптов легко переобучаются на маленькой обучающей выборке. Сверху добавляется отложенная тестовая выборка, которую оптимизатор никогда не видит, и доверять стоит именно её оценке:

```python
import json
import random

with open("dataset.json", encoding="utf-8") as f:
    examples = [dspy.Example(**row).with_inputs("question") for row in json.load(f)]

random.Random(0).shuffle(examples)
holdout = len(examples) // 5
testset, rest = examples[:holdout], examples[holdout:]
split = len(rest) // 5
trainset, valset = rest[:split], rest[split:]
```

## Метрика и настройка

MIPROv2 запускает агента, чтобы собрать удачные трассы, предлагает варианты инструкций и примеров few-shot и ищет среди них лучший по метрике:

```python
def metric(example, prediction, trace=None):
    return example.answer.lower() in prediction.answer.lower()


evaluate = dspy.Evaluate(devset=testset, metric=metric, num_threads=4)
print("before:", evaluate(agent))

optimizer = dspy.MIPROv2(metric=metric, auto="light", num_threads=4)
tuned = optimizer.compile(agent, trainset=trainset, valset=valset)

print("after:", evaluate(tuned))
tuned.save("agent.json")
```

Запуск не бесплатен. С `auto="light"` этот агент проходит 20 попыток на мини-батчах по 35 примеров, плюс каждые 5 попыток полный прогон по валидационной выборке, до 100 примеров, а каждый запуск агента — это несколько обращений к модели. На локальной модели это долгое ожидание, на облачной — счёт. Предложения вариантов можно отдать более сильной модели, чем та, на которой работает агент, — через `prompt_model` и `task_model`.

## Использование результата

`agent.json` — это артефакт: он лежит в репозитории и загружается без всякой настройки. При изменении данных или модели запускаем настройку заново и сравниваем оценки на тестовой выборке.

```python
agent = dspy.ReAct(AnswerFromKb, tools=[search_kb], max_iters=5)
agent.load("agent.json")
print(agent(question="How long is the refund period?").answer)
```
