DSPy — Автоматическая генерация промптов
Многие AI инженеры и не только, работающие с LLM и агентами, сталкиваются с задачами, когда корректность работы агента во многом зависит от промпта.
Для простых задач достаточно промпта в стиле "Ты умный агент, отвечай на вопросы пользователя". Но если агент должен делать сложные вызовы функций и обрабатывать нестандартные запросы, написать стабильный промпт для вызовов и для окончательного ответа непросто, а при смене LLM модели на другую ранее работавший промпт может перестать работать. Как писать такие промпты руками, разобрано в статье о промпт-инжиниринге, а как агент вызывает инструменты — в статье о RAG и вызове функций.
DSPy (GitHub) превращает это из сочинения текста в измеримый процесс. Что он делает простыми словами:
- Вместо длинного текста вы описываете задачу как форму: какие есть входы и какие нужны выходы.
- Правила вызова инструментов фиксируются кодом, а не «словами» в промпте.
- Качество меряется метрикой, и DSPy подбирает формулировки под эту метрику.
- При смене модели или функций вы запускаете настройку заново, а не переписываете всё руками.
Что это даёт:
- Масштабирование: проще добавлять инструменты и новые форматы ответов.
- Дешёвую смену модели: она превращается в повторный запуск настройки, с оценкой до и после.
- Повторяемость: промпт хранится как артефакт и восстанавливается.
- Контроль: поведение задаётся структурой и метрикой, а не «магическим текстом».
Цена — набор данных и время на настройку; о них ниже. Пример — агент, который ищет в локальной базе данных: так видно, что агенты могут быть довольно сложными.
Сигнатура и инструменты
Сигнатура говорит, какие поля приходят агенту и что он должен вернуть. Инструменты — обычные функции с чёткими входами и выходами:
import sqlite3from 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)Данные
Набор данных — это список вопросов с правильными ответами:
[ { "question": "...", "answer": "..." }]По документации DSPy, 30 примеров часто уже дают заметную пользу, но ориентироваться стоит минимум на 300, а для MIPROv2 она советует от 200, чтобы избежать переобучения. Важно, чтобы в них было разнообразие.
Разбиение обратно привычному по нейросетям: 20 процентов на обучение и 80 на валидацию, потому что оптимизаторы промптов легко переобучаются на маленькой обучающей выборке. Сверху добавляется отложенная тестовая выборка, которую оптимизатор никогда не видит, и доверять стоит именно её оценке:
import jsonimport 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) // 5testset, rest = examples[:holdout], examples[holdout:]split = len(rest) // 5trainset, valset = rest[:split], rest[split:]Метрика и настройка
MIPROv2 запускает агента, чтобы собрать удачные трассы, предлагает варианты инструкций и примеров few-shot и ищет среди них лучший по метрике:
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 — это артефакт: он лежит в репозитории и загружается без всякой настройки. При изменении данных или модели запускаем настройку заново и сравниваем оценки на тестовой выборке.
agent = dspy.ReAct(AnswerFromKb, tools=[search_kb], max_iters=5)agent.load("agent.json")print(agent(question="How long is the refund period?").answer)