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)