Структурирани изходи и оценка на промпти
Моделът по природа пише свободен текст, а програмата отсреща очаква точни полета. В първата половина превръщаме изхода в проверени данни с JSON схема, Pydantic и Instructor. Във втората спираме да гадаем кой промпт е по-добър и започваме да мерим: A/B тест, модел-съдия, RAGAS и регресионен тест преди всяко пускане.
format, а vLLM — в response_format. Старите параметри guided_json във vLLM са махнати от v0.12.0. Примерът с RAGAS е пренаписан за новия API: ragas.metrics.collections и ascore, защото evaluate() и LangchainLLMWrapper вече са отбелязани като остарели. Добавен е deepeval (GEval). Изключението на Instructor вече се внася от instructor.core. Фактурите са в евро: от 01.01.2026 в България се плаща в евро. Проверката на сумата е в model_validator и е пусната локално. Процентите за надеждност на методите и „реалният резултат“ на счетоводна фирма не можеха да се проверят, затова са махнати или отбелязани като илюстрация. Тестовите данни вече са от измислен вътрешен правилник, а не от „наредби“ с несъществуващи членове.
01Какво ще научиш
- Четирите стъпала на надеждност: инструкция в промпта, JSON mode, JSON схема с ограничено генериране и схема с валидация и повторни опити.
- Как да опишеш данните с Pydantic, включително бизнес правило като „общо = основа × (1 + ДДС)“.
- Как Instructor връща типизиран обект, прави повторни опити и какво става, когато и те не стигнат.
- Кои метрики мерят качеството на отговор и на RAG система, и какво значи всяка от тях.
- Как да сравниш два промпта с данни, как да ползваш модел-съдия, RAGAS и deepeval, и как да спираш пускане при влошаване.
02Преди да започнеш
- Минал си Блок 2 · Част 1 (промпт инженеринг): системен промпт, few-shot, температура.
- Python 3.10 или по-нов и виртуална среда:
pip install "pydantic>=2" instructor openai ollama ragas deepeval. - Локален модел-сървър: Ollama (
ollama pullна модел по избор) или vLLM от Блок 1. Двата дават OpenAI-съвместим адрес/v1. - За модела-съдия — по-силен модел от този, който оценяваш. Може да е локален или облачен. Облачният праща данните навън, затова не му давай лични данни.
- Основи на RAG от Блок 0: какво са чънк, embedding и извличане на контекст.
03Стъпки
-
Защо свободният текст чупи системите
Представи си анализатор на фактури, написан с
text.split("Сума:"). При повечето фактури работи. При останалите или гърми, или тихо записва грешна сума в счетоводството. Защо е опасно? Защото грешката не се вижда, докато някой не сравни числата. Структурираният изход мести проверката там, където ѝ е мястото: в схемата, преди данните да тръгнат нататък.Стъпало Какво гарантира Кога Инструкция
„Върни само JSON“Нищо. Моделът често добавя обяснения или Markdown ограждения. Бърз прототип JSON mode format="json"·{"type":"json_object"}Синтактично валиден JSON, но с каквито полета реши моделът. Прости структури JSON схема format=схема·json_schemaСървърът ограничава генерирането, за да спазва схемата: имена, типове, изброявания. Почти винаги — стандартът днес Схема + Pydantic + повторен опит
InstructorФормата и бизнес правилата. При грешка моделът получава обяснението и опитва пак. Production, пари, документи ✅Правило: схемата пази формата, валидаторът пази смисълаОграниченото генериране гарантира, че ще получиш число в полетоtotal_amount. То не гарантира, че числото е вярно. Затова аритметиката, датите и регистрите се проверяват в Pydantic. -
JSON mode и JSON схема с Ollama и vLLM
Започваме с данните. Схемата описваме веднъж в Pydantic и я подаваме на сървъра. Ollama я приема в параметъра
format, а vLLM — вresponse_format, както при OpenAI. Температура 0 прави извличането повторяемо.Python · schema.py (пуснат локално с Pydantic 2.13.5)from datetime import date from enum import Enum from typing import Optional from pydantic import BaseModel, Field, model_validator class Currency(str, Enum): EUR = "EUR" # от 01.01.2026 в България се плаща в евро USD = "USD" class Invoice(BaseModel): vendor_name: str = Field(description="Пълното наименование на доставчика") vendor_eik: Optional[str] = Field(default=None, pattern=r"^\d{9}(\d{4})?$", description="ЕИК/БУЛСТАТ — 9 или 13 цифри") invoice_date: date = Field(description="Дата на фактурата") amount_without_vat: float = Field(gt=0, description="Данъчна основа") vat_rate: float = Field(default=0.20, ge=0, le=1, description="Ставка на ДДС (0.20 = 20%)") total_amount: float = Field(gt=0, description="Обща сума с ДДС") currency: Currency = Currency.EUR description: Optional[str] = None @model_validator(mode="after") def check_total(self): # бизнес правило: общо = основа × (1 + ставка) expected = round(self.amount_without_vat * (1 + self.vat_rate), 2) if abs(self.total_amount - expected) > 0.01: raise ValueError(f"Общата сума {self.total_amount} не е равна на {expected:.2f}") return self⚠️Капан: поле, кръстено като типСтарата версия имаше полеdate: date. В Pydantic така името на полето засенчва типа и следващите полета сdateсе чупят. Кръщавай полетата по смисъл:invoice_date.Python · extract.py (три нива)import json import ollama from openai import OpenAI from schema import Invoice INVOICE_TEXT = """ Фактура № 0000001234 Доставчик: Примерна фирма ООД, ЕИК 123456789 Дата: 15.03.2026 Описание: Консултантски услуги за внедряване на AI Данъчна основа: 2000.00 EUR ДДС 20%: 400.00 EUR Общо: 2400.00 EUR """ # измислени данни за упражнение MODEL = "llama3.1:8b" # или друг модел, изтеглен с ollama pull # ── Ниво 1: JSON mode — валиден JSON, но полетата не са гарантирани ── def extract_json_mode(text: str) -> dict: r = ollama.generate(model=MODEL, format="json", options={"temperature": 0}, prompt=f"Извлечи данните от фактурата като JSON:\n{text}") return json.loads(r["response"]) # ── Ниво 2: JSON схема в Ollama — генерирането следва схемата ── def extract_schema_ollama(text: str) -> Invoice: r = ollama.chat(model=MODEL, messages=[{"role": "user", "content": f"Извлечи данните от фактурата:\n{text}"}], format=Invoice.model_json_schema(), options={"temperature": 0}) return Invoice.model_validate_json(r["message"]["content"]) # тук работи и правилото за сумата # ── Ниво 3: JSON схема във vLLM (OpenAI-съвместим API) ── def extract_schema_vllm(text: str) -> Invoice: client = OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed") r = client.chat.completions.create( model="llama3", # --served-model-name от Блок 1 messages=[{"role": "user", "content": f"Извлечи данните от фактурата:\n{text}"}], response_format={"type": "json_schema", "json_schema": {"name": "invoice", "schema": Invoice.model_json_schema()}}, temperature=0) return Invoice.model_validate_json(r.choices[0].message.content) if __name__ == "__main__": print(extract_schema_ollama(INVOICE_TEXT).model_dump_json(indent=2))очакван изход{ "vendor_name": "Примерна фирма ООД", "vendor_eik": "123456789", "invoice_date": "2026-03-15", "amount_without_vat": 2000.0, "vat_rate": 0.2, "total_amount": 2400.0, "currency": "EUR", "description": "Консултантски услуги за внедряване на AI" }💡Защо пак пишем полетата в промптаДокументацията на vLLM препоръчва да опишеш схемата и в промпта. Ограничението казва на модела какво не може да напише, а промптът му казва какво трябва. Заедно дават по-добри стойности. -
Instructor: типизиран обект и повторни опити
Instructor обвива клиента на модела. Подаваш му Pydantic модел и получаваш обект от този тип, не речник. Ако валидацията падне, например заради сбъркана сума, Instructor връща грешката на модела и го пита пак.
max_retries=3означава един опит и до три повторни. Ако и те не стигнат, хвърляInstructorRetryException.Python · extract_typed.pyimport instructor from instructor.core import InstructorRetryException from schema import Invoice from extract import INVOICE_TEXT # Ollama: адресът по подразбиране е http://localhost:11434/v1 client = instructor.from_provider("ollama/llama3.1:8b") # vLLM или друг OpenAI-съвместим сървър: # from openai import OpenAI # client = instructor.from_openai(OpenAI(base_url="http://localhost:8000/v1", api_key="not-needed")) def extract_invoice_typed(text: str) -> Invoice: return client.chat.completions.create( response_model=Invoice, # ← Pydantic моделът max_retries=3, # ← 1 опит + до 3 повторни messages=[{"role": "user", "content": f"Извлечи данните:\n{text}"}], temperature=0, # при from_openai добави и model="llama3" ) try: invoice = extract_invoice_typed(INVOICE_TEXT) print(invoice.vendor_name, invoice.invoice_date, invoice.total_amount) except InstructorRetryException as e: # не приемай, че повторните опити винаги успяват: запиши и прати за ръчен преглед print("Неуспех след повторните опити:", e)⚠️Капан: режимът на Instructor при локални моделиПри Ollama Instructor избира сам: извикване на инструменти (tools) за модели, които го поддържат, и JSON за останалите. Ако малък модел връща празни или странни полета, опитай изричноmode=instructor.Mode.JSON. -
Пример: пакетна обработка на фактури с човешки преглед
Истинската полза идва, когато моделът не само извлича полета, но и маркира какво да погледне човек. Тук проверяваме задължителните реквизити на фактура по чл. 114 от ЗДДС и предлагаме счетоводна сметка. Последната дума остава за счетоводителя.
Python · invoice_batch.pyimport json from datetime import date from pathlib import Path from typing import Optional import instructor from instructor.core import InstructorRetryException from pydantic import BaseModel, Field class InvoiceResult(BaseModel): vendor_name: str vendor_eik: Optional[str] = None invoice_date: date total_eur: float = Field(gt=0) vat_included: bool gl_account: str = Field(description="Предложена счетоводна сметка (напр. 602, 401)") requisites_ok: bool = Field(description="True, ако има всички реквизити по чл. 114 ЗДДС") issues: list[str] = Field(default_factory=list, description="Липсващи реквизити или съмнения") SYSTEM = """Ти си помощник на счетоводител. Проверяваш дали фактурата съдържа реквизитите по чл. 114 от ЗДДС и предлагаш сметка по сметкоплана. Работиш само с текста на фактурата. Не измисляш данни — ако нещо липсва, пиши го в issues.""" client = instructor.from_provider("ollama/llama3.1:8b") def process_invoice(text: str, name: str) -> Optional[InvoiceResult]: try: r = client.chat.completions.create( response_model=InvoiceResult, max_retries=3, temperature=0, messages=[{"role": "system", "content": SYSTEM}, {"role": "user", "content": f"Фактура:\n{text}"}]) mark = "✅" if r.requisites_ok and not r.issues else "👀" print(f"{mark} {name}: {r.vendor_name} — {r.total_eur:.2f} EUR") return r except InstructorRetryException as e: print(f"❌ {name}: за ръчна обработка — {e}") return None def process_directory(folder: str) -> None: ok, review = [], [] for f in sorted(Path(folder).glob("*.txt")): r = process_invoice(f.read_text(encoding="utf-8"), f.name) if r and r.requisites_ok and not r.issues: ok.append(r.model_dump(mode="json")) else: review.append({"file": f.name, "result": r.model_dump(mode="json") if r else None}) Path("processed.json").write_text(json.dumps(ok, ensure_ascii=False, indent=2), encoding="utf-8") Path("for_review.json").write_text(json.dumps(review, ensure_ascii=False, indent=2), encoding="utf-8") print(f"📊 Автоматично: {len(ok)} · за преглед: {len(review)}")💡Как да прецениш ползата (илюстрация)Ако счетоводител обработва на ръка няколкостотин фактури месечно, а системата отделя за преглед само съмнителните, времето отива за тях, а не за преписване. Колко точно спестява — измери върху своите фактури с тестовия набор от следващите стъпки. Не приемай чужди проценти наготово.⛔Фактурите са лични и търговски данниОбработвай ги с локален модел 🔒. Ако ползваш облачен модел 🌐, провери договора за обработка на данни и не пращай файлове с лични данни без правно основание. -
Какво мерим: четирите основни метрики
„Този промпт изглежда по-добре“ е мнение. „Промпт Б дава по-висока вярност на нашите 50 тестови случая“ са данни. За да сравняваш, първо се договори какво мериш. Всички метрики по-долу са от 0 до 1.
Метрика Въпрос Ориентир Faithfulness (вярност) Подкрепено ли е всяко твърдение в отговора от дадения контекст? колкото по-близо до 1 Answer relevancy (релевантност) Отговаря ли директно на зададения въпрос? високо Context recall (покритие) Намери ли извличането всичко нужно за верния отговор? ниско → проблем в извличането Context precision (точност на контекста) Колко от намерените чънкове наистина са полезни и стоят ли най-отгоре? ниско → шум в контекста ✅Прагове се договарят, не се преписватСтарата версия даваше „faithfulness над 0.90“ като универсален праг. Такъв праг няма. Измери отправна точка (baseline) на своя набор и реши колко е приемливо за задачата: за правен отговор търпимостта е много по-малка, отколкото за чернова на пост. -
A/B тест на два промпта
Логиката е като при A/B тест на сайт: сменяш едно нещо, пускаш и двата варианта върху едни и същи случаи при температура 0 и решаваш по числата. Тестовите случаи пазиш в git като код. Три случая стигат колкото да видиш как работи; за решение трябват десетки.
Python · prompt_ab_test.pyimport ollama from statistics import mean MODEL = "llama3.1:8b" # Измислен вътрешен правилник — учебни данни, не правна справка TEST_CASES = [ {"question": "Колко дни платен отпуск се полагат на нов служител?", "context": "Правилник, т. 4.1: Всеки служител има право на 25 работни дни платен отпуск годишно.", "ground_truth": "25 работни дни"}, {"question": "Какво трябва да донеса за достъп до сървърното помещение?", "context": "Правилник, т. 7.3: Достъпът изисква служебна карта и писмено одобрение от ръководител.", "ground_truth": "служебна карта и писмено одобрение от ръководител"}, {"question": "В какъв срок се подава отчет за командировка?", "context": "Правилник, т. 9.2: Отчетът за командировка се подава до 5 работни дни след връщането.", "ground_truth": "до 5 работни дни"}, ] PROMPT_A = """Отговори на въпроса на база контекста. Контекст: {context} Въпрос: {question}""" PROMPT_B = """Отговаряй САМО от контекста. Ако отговорът липсва, кажи „Не се съдържа в документите.“ Посочи точката от правилника. Контекст: {context} Въпрос: {question} Кратък и точен отговор:""" def answer(template: str, case: dict) -> str: r = ollama.generate(model=MODEL, prompt=template.format(**case), options={"temperature": 0}) return r["response"].strip() def score(ans: str, case: dict) -> dict: """Груби евристики за бърз ориентир; за решение ползвай модел-съдия или RAGAS.""" gt = set(case["ground_truth"].lower().split()) a = set(ans.lower().split()) ctx = set(case["context"].lower().split()) accuracy = len(gt & a) / len(gt) if gt else 0.0 extra = a - ctx - gt faithfulness = 1.0 - min(1.0, len(extra) / max(1, len(a))) conciseness = 1.0 if len(ans.split()) < 50 else 0.7 return {"accuracy": accuracy, "faithfulness": faithfulness, "conciseness": conciseness, "composite": (accuracy + faithfulness + conciseness) / 3} def run(template: str) -> dict: rows = [score(answer(template, c), c) for c in TEST_CASES] return {k: round(mean(r[k] for r in rows), 3) for k in rows[0]} if __name__ == "__main__": # за да може regression_test.py да внася данните без да пуска теста for name, tpl in (("Промпт А", PROMPT_A), ("Промпт Б", PROMPT_B)): res = run(tpl) print(name, " ".join(f"{k}={v:.3f}" for k, v in res.items()))⚠️Капан: победа от шумаСтарата версия показваше „Промпт Б печели с +24%“ като очакван резултат. Това е илюстрация, не измерване. На малък набор разлика от няколко процента може да е случайна. Гледай и отделните случаи: ако Б печели средно, но проваля важен случай, той не е победител. -
Модел-съдия (LLM-as-Judge)
Ръчната оценка на стотици отговори отнема часове. Затова по-силен модел оценява отговорите на по-слаб по ясна рубрика. Оценката също е структуриран изход: съдията връща числа в граници и кратка обосновка, а не есе.
Python · llm_judge.pyimport instructor import ollama from pydantic import BaseModel, Field class JudgeScore(BaseModel): faithfulness: float = Field(ge=0, le=1, description="Всичко ли е подкрепено от контекста") relevance: float = Field(ge=0, le=1, description="Отговаря ли директно на въпроса") completeness: float = Field(ge=0, le=1, description="Пропуснато ли е нещо важно") reasoning: str = Field(description="Обосновка в 1–2 изречения") JUDGE_SYSTEM = """Ти оценяваш отговори на RAG система по три критерия от 0.0 до 1.0. FAITHFULNESS: 1.0 = всичко е от контекста · 0.5 = половината е предположение · 0.0 = игнорира контекста RELEVANCE: 1.0 = директен, точен отговор · 0.5 = частично · 0.0 = извън темата COMPLETENESS: 1.0 = всичко важно е включено · 0.5 = липсват детайли · 0.0 = липсват ключови данни Бъди строг. Първо обоснови, после дай числата.""" JUDGE_MODEL = "llama3.1:70b" # по-силен от оценявания ANSWER_MODEL = "llama3.1:8b" judge = instructor.from_provider(f"ollama/{JUDGE_MODEL}") def judge_answer(question: str, context: str, answer: str) -> JudgeScore: return judge.chat.completions.create( response_model=JudgeScore, max_retries=2, temperature=0, messages=[{"role": "system", "content": JUDGE_SYSTEM}, {"role": "user", "content": f"ВЪПРОС: {question}\nКОНТЕКСТ:\n{context}\nОТГОВОР:\n{answer}"}]) def evaluate_dataset(cases: list[dict]) -> float: total = [] for c in cases: ans = ollama.generate(model=ANSWER_MODEL, options={"temperature": 0}, prompt=f"Контекст: {c['context']}\nВъпрос: {c['question']}")["response"] s = judge_answer(c["question"], c["context"], ans) comp = (s.faithfulness + s.relevance + s.completeness) / 3 total.append(comp) flag = "✅" if comp > 0.8 else ("⚠️" if comp > 0.6 else "❌") print(f"{flag} {comp:.2f} | {s.reasoning[:70]}") avg = sum(total) / len(total) print(f"📊 Средно: {avg:.3f}") return avg⚠️Капан: съдията също грешиИзследванията върху модели-съдии описват устойчиви изкривявания: предпочитат по-дългите отговори, първия от два варианта и отговорите на собствения си модел. Затова: разменяй реда при сравнение по двойки, давай строга рубрика и сверявай поне 20–30 оценки с човек, преди да се довериш. -
RAGAS: готови метрики за RAG
RAGAS е специализиран за RAG и изчислява метриките от стъпка 5 с модел-съдия и embeddings. От версия 0.4 метриките се внасят от
ragas.metrics.collectionsи се викат поотделно сascore. С локален Ollama нищо не излиза навън.Python · ragas_eval.py (RAGAS 0.4.3)import asyncio from openai import AsyncOpenAI from ragas.llms import llm_factory from ragas.embeddings import embedding_factory from ragas.metrics.collections import Faithfulness, AnswerRelevancy, ContextRecall, ContextPrecision # Ollama през OpenAI-съвместимия адрес — без облак ollama_client = AsyncOpenAI(base_url="http://localhost:11434/v1", api_key="ollama") llm = llm_factory("llama3.1:70b", client=ollama_client, temperature=0) # съдията emb = embedding_factory("openai", model="nomic-embed-text", client=ollama_client) metrics = { "faithfulness": Faithfulness(llm=llm), "answer_relevancy": AnswerRelevancy(llm=llm, embeddings=emb), "context_recall": ContextRecall(llm=llm), "context_precision": ContextPrecision(llm=llm), } SAMPLES = [ # измислени учебни данни {"user_input": "Колко дни платен отпуск се полагат?", "response": "Полагат се 25 работни дни платен отпуск годишно (т. 4.1).", "retrieved_contexts": ["Правилник, т. 4.1: Всеки служител има право на 25 работни дни платен отпуск годишно."], "reference": "25 работни дни годишно"}, ] async def main(): for s in SAMPLES: f = await metrics["faithfulness"].ascore(user_input=s["user_input"], response=s["response"], retrieved_contexts=s["retrieved_contexts"]) a = await metrics["answer_relevancy"].ascore(user_input=s["user_input"], response=s["response"]) r = await metrics["context_recall"].ascore(user_input=s["user_input"], reference=s["reference"], retrieved_contexts=s["retrieved_contexts"]) p = await metrics["context_precision"].ascore(user_input=s["user_input"], reference=s["reference"], retrieved_contexts=s["retrieved_contexts"]) print(f"{s['user_input'][:40]} | faith={f.value:.2f} rel={a.value:.2f} " f"recall={r.value:.2f} prec={p.value:.2f}") asyncio.run(main())⚠️Капан: стари примери за RAGASПовечето примери в интернет ползватfrom ragas.metrics import faithfulness,evaluate(dataset=...)иLangchainLLMWrapper. В 0.4 те още работят, но с предупреждение, че ще бъдат махнати. Колоните също са преименувани:question→user_input,answer→response,contexts→retrieved_contexts,ground_truth→reference.🔑Кога RAGAS, кога собствен съдияRAGAS — когато имаш работеща RAG система и искаш стандартни, сравними метрики. Заcontext_recallти трябва еталонен отговор (reference). Собствен съдия — когато прототипираш или мериш нещо специфично за областта ти, например „цитира ли точно точката от правилника“. Там не е нужен еталон. -
deepeval: собствен критерий с GEval
deepeval прави оценката да изглежда като unit тест.
GEvalприема критерий на обикновен език и сам го превръща в стъпки за съдията. Удобно е, когато критерият е твой и не е сред стандартните.Python · test_answers.py (deepeval 4.2.7)from deepeval.metrics import GEval from deepeval.models import OllamaModel from deepeval.test_case import LLMTestCase, SingleTurnParams judge = OllamaModel(model="llama3.1:70b", base_url="http://localhost:11434", temperature=0) cites_rule = GEval( name="Цитира точката", criteria="Отговорът посочва точката от правилника, на която се основава, и не твърди нищо извън контекста.", evaluation_params=[SingleTurnParams.INPUT, SingleTurnParams.ACTUAL_OUTPUT, SingleTurnParams.RETRIEVAL_CONTEXT], model=judge, threshold=0.7, ) case = LLMTestCase( input="Колко дни платен отпуск се полагат?", actual_output="Полагат се 25 работни дни годишно (т. 4.1).", retrieval_context=["Правилник, т. 4.1: Всеки служител има право на 25 работни дни платен отпуск годишно."], ) cites_rule.measure(case) print(cites_rule.score, cites_rule.reason)В по-стари примери ще видиш
LLMTestCaseParams. Името е сменено наSingleTurnParams; старото още работи, но с предупреждение. -
Регресионен тест: спирачка преди пускане
Всяка промяна на промпт или модел подобрява едни случаи и може да влоши други. Регресионният тест сравнява новия резултат със записаната отправна точка и спира пускането, ако спадът е над прага. Отправната точка се обновява само при успех.
Python · regression_test.pyimport json import sys from datetime import datetime from pathlib import Path import ollama from llm_judge import judge_answer from prompt_ab_test import TEST_CASES, PROMPT_B MODEL = "llama3.1:8b" BASELINE = Path("baseline_scores.json") THRESHOLD = 0.05 # допустим спад: 0.05 от скалата 0–1 def run(prompt: str) -> list[float]: out = [] for c in TEST_CASES: ans = ollama.generate(model=MODEL, prompt=prompt.format(**c), options={"temperature": 0})["response"] s = judge_answer(c["question"], c["context"], ans) out.append((s.faithfulness + s.relevance + s.completeness) / 3) return out def main(prompt: str) -> bool: scores = run(prompt) avg = sum(scores) / len(scores) if BASELINE.exists(): base = json.loads(BASELINE.read_text(encoding="utf-8"))["avg_score"] delta = avg - base print(f"Отправна точка: {base:.3f} · нов: {avg:.3f} · разлика: {delta:+.3f}") if delta < -THRESHOLD: print("❌ РЕГРЕСИЯ — пускането се спира") return False else: print("ℹ️ Няма отправна точка — записваме текущия резултат") BASELINE.write_text(json.dumps({"avg_score": avg, "n_cases": len(scores), "scores": scores, "timestamp": datetime.now().isoformat()}, indent=2), encoding="utf-8") return True if __name__ == "__main__": sys.exit(0 if main(PROMPT_B) else 1) # код 1 спира CI/CD🛡️Какво поправихме спрямо старата версияСтарият скрипт ползвашеollamaиjudge_answer, без да ги внася, и записваше новия резултат като отправна точка дори при регресия. Сега при провал старата отправна точка остава.
04Проверка
Чеклист
- Модел-сървърът отговаря на
/v1(Ollama или vLLM). - Извличането със схема връща изход, който минава през
Invoice.model_validate_json. - Нарочно сбъркана обща сума дава грешка при валидация.
- Instructor връща типизиран обект, а пътят при неуспех хваща
InstructorRetryException. - Имаш тестов набор с поне 10 случая (по-добре 50+) и еталонни отговори, пазен в git.
- A/B тестът е пуснат при температура 0 и резултатите по случаи са записани.
- Рубриката на съдията е написана и поне част от оценките са сверени с човек.
- RAGAS метриките са изчислени за твоя RAG.
baseline_scores.jsonе в git, а регресионният тест е вързан в CI.
Тест
1. Instructor е с max_retries=3 и валидацията пада при всеки опит. Какво става накрая?
2. RAGAS дава context_recall = 0.42. Коя е най-вероятната причина?
3. Нов промпт вдига faithfulness, но сваля answer relevancy. Какво показва това?
4. Кое кара изхода да следва JSON схемата още докато моделът генерира?
05Какво следва
С това Блок 2 е завършен: техниките на промптинг, системните промпти, структурираните изходи и оценката с данни.
06Източници
- Ollama: структурирани изходи —
format="json"и JSON схема. - vLLM: Structured Outputs —
response_format,structured_outputs, махнатитеguided_*. - Pydantic: валидатори —
field_validatorиmodel_validator. - Instructor: документация и интеграция с Ollama.
- Outlines — ограничено генериране, когато моделът върви в твоя процес.
- RAGAS: метрики — faithfulness, answer relevancy, context recall и precision.
- deepeval: първи стъпки — тестове и GEval.
- Es et al. (2023). Ragas: Automated Evaluation of Retrieval Augmented Generation — статията, въвела метриките.
- Zheng et al. (2023). Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena — изкривяванията на модела-съдия.