Kurs LLM Basics, zdjęcie laptopa.

Modele językowe (LLM Basics) – Lab 2: Hello LLM

Prompt Engineering – Jak przejąc kontrolę nad chaosem?

Witaj w drugim etapie naszego kursu budowania aplikacji AI wykorzystującej modele językowe.

W Lab 1 nauczyliśmy się uruchamiać silnik – potrafimy wysłać zapytanie do API lub modelu lokalnego i odebrać odpowiedź. Ale czy ta odpowiedź jest użyteczna? Często nie. Modele językowe z natury są „gadatliwe”, nieprzewidywalne i mają tendencję do lania wody.

Dzisiaj zamienimy LLM z „chatbota” w bardziej przewidywalny komponent oprogramowania. To, co zrobimy dzisiaj jest fundamentem pod RAG i Function Calling, które wdrożymy w kolejnych częściach kursu.

🎯 Cel na dziś
Zrozumieć, że prompt to nie „magiczne zaklęcie”, ale kod. Nauczysz się:

  • System Prompting: Jak nadać modelowi rolę i ograniczenia.
  • Few-Shot Prompting: Jak uczyć model na przykładach bez trenowania go?
  • Chain-of-Thought (CoT): Kiedy warto kazać modelowi „myśleć krok po kroku”, a kiedy to przepalanie pieniędzy.
  • JSON Enforcement: Najważniejsza umiejętność inżynierska – jak zmusić model, by zwracał dane, które Twój kod w Pythonie zrozumie (i jak naprawić błędy, gdy model zawiedzie).

🛠️ Przygotowanie
W tym laboratorium używasz tego samego kodu co w Lab 1 (API albo lokalnie) tj. metod local_generate, gemini_generate, groq_generate. Przygotujemy funkcję chat_once, która ukrywa wybór trybu pracy z modelami (odpowiednio ustawiając zmienną środowiskową MODEL_MODE).

MODEL_MODE = os.getenv("MODEL_MODE", "gemini")

def chat_once(
    prompt: str,
    system: str = "You are a helpful assistant.",
    max_output_tokens: int = 128,
    temperature: float = 0.0,
    top_p: float = 0.9,
    top_k: Optional[int] = None,
) -> Dict[str, Any]:
    if MODEL_MODE == "gemini":
        return gemini_generate(
            prompt=prompt,
            system=system,
            max_output_tokens=max_output_tokens,
            temperature=temperature,
            top_p=top_p,
            top_k=top_k,
        )
    elif MODEL_MODE == "groq":
        return groq_generate(
            prompt=prompt,
            system=system,
            max_output_tokens=max_output_tokens,
            temperature=temperature,
            top_p=top_p,
        )
    else:
        return local_generate(
            prompt=prompt,
            system=system,
            max_output_tokens=max_output_tokens,
            temperature=temperature,
            top_p=top_p,
            top_k=top_k,
        )

Eksperyment 1: Precyzja ma znaczenie

Zacznijmy od klasycznego problemu: chcemy podsumowanie tekstu. Zbadamy, jak treść polecenia wpływa na odpowiedź modelu językowego.

⚠️UWAGA: W tej części kursu zachęcam do korzystania z API Gemini – odpowiedzi są zwykle szybkie, łatwo podejrzeć wyniki eksperymentów, a dodatkowo w praktyce możesz uruchamiać to bez kosztów dzięki darmowemu dostępowi przez API.

prompts = [
    "Summarize this text.",
    "Summarize this text in one sentence.",
    "Summarize this text in one sentence using simple English and output as JSON {summary: ...}"
]

text = """
Artificial intelligence (AI) is a field of computer science that builds systems able to perform tasks that typically require human intelligence-such as understanding language, learning from data, reasoning, and perception.
What AI can do: perception (vision/speech), reasoning, learning, interaction (natural language), and planning/control.
How it works (at a glance):
- Symbolic AI: hand-written rules and logic.
- Machine learning: models learn patterns from data.
- Deep learning: multi-layer neural networks for images, speech, and text.
"""

for p in prompts:
    print("\n---\nPrompt:", p)
    response = chat_once(f"{p}\n\n{text}")
    print("---\nAnswer:", response['text'])
    print(f"---\n⏱ {response['latency_s']}s | Tokens: {response['usage']['total_tokens']}")

Co zauważysz?
Pierwszy prompt da Ci „ścianę tekstu”. Trzeci prompt (z prośbą o JSON) jest tym, czego szukamy w programowaniu. Chcemy danych, a nie opinii modelu.

💡Wniosek. Traktuj prompt jak specyfikację funkcji. Musisz zdefiniować nie tylko „co” model ma zrobić, ale także „w jakim formacie” ma to zwrócić.


Eksperyment 2: System Prompt („Osobowość”)

System prompt to instrukcja „nadrzędna”, która ustawia kontekst całej rozmowy. To tutaj definiujemy zasady, których użytkownik nie powinien móc nadpisać.

roles = [
    "You are a sarcastic assistant.",
    "You are a formal university lecturer.",
    "You are a motivational coach."
]

question = "Explain recursion in one sentence."

for r in roles:
    print("\n---\nRole:", r)
    print("---\nQuestion:", question)
    response = chat_once(f"{question}", system=r, temperature=0.3)
    print("---\nAnswer:", response['text'])
    print(f"---\n⏱ {response['latency_s']}s | Tokens: {response['usage']['total_tokens']}")

W Twoim projekcie końcowym w system_prompt wylądują instrukcje bezpieczeństwa (Guardrails) oraz definicja dostępnych narzędzi.


Eksperyment 3: Few-Shot Prompting (Uczenie na przykładach)

Zamiast pisać modelowi elaborat o tym, jak ma wyglądać odpowiedź, po prostu mu ją pokaż. To jedna z najpotężniejszych technik promptowania.

question = """
Translate English → Polish:
Input: Good morning → Output: Dzień dobry
Input: Thank you → Output: Dziękuję
Input: See you later → Output:
"""

print("---\nQuestion:", question)
response = chat_once_gemini(f"{question}", temperature=0.3)
print("---\nAnswer:", response['text'])
print(f"---\n⏱ {response['latency_s']}s | Tokens: {response['usage']['total_tokens']}")

Model „łapie wzorzec” (Input → Output) i kontynuuje go, zamiast odpowiadać pełnym zdaniem typu "The translation for 'See you later' is [...]".


Eksperyment 4: Chain-of-Thought (Myśl powoli – krok po kroku)

Czasami model popełnia błędy w prostych zadaniach logicznych lub matematycznych, bo próbuje zgadnąć odpowiedź „od razu”. Magiczna fraza „Think step by step” (Myśl krok po kroku) zmusza go do wygenerowania ścieżki rozumowania.

question = "If there are 3 red and 5 blue balls, and you take one randomly, what is the probability it’s red?"


print("---\nQuestion:", question)
response = chat_once_gemini(f"{question}", temperature=0.3)
print("---\nAnswer (without CoT):", response['text'])
print(f"---\n⏱ {response['latency_s']}s | Tokens: {response['usage']['total_tokens']}")


response = chat_once_gemini(f"{question}\nThink step by step.", temperature=0.3)
print("---\nAnswer (with CoT):", response['text'])
print(f"---\n⏱ {response['latency_s']}s | Tokens: {response['usage']['total_tokens']}")

⚠️Uwaga na koszty: CoT generuje znacznie więcej tokenów. W produkcji używaj go tylko tam, gdzie logika jest kluczowa. W prostych zadaniach to strata pieniędzy i czasu (latency).

Jeżeli chcesz zobaczyć różnicę w odpowiedzi dobierz model, który nie jest modelem rozumującym (reasoning model).


Eksperyment 5: Walidacja i naprawa JSON

To jest główna część tej części kursu, to klucz pod kolejne laboratoria (tools, RAG, guardrails). W większości przypadków, gdy budujesz aplikację AI, oczekujesz JSON-a. Ale modele lubią robić błędy, np. dodawać ```json na początku albo zapominać domknąć klamrę.

Naiwna próba: JSON w promptcie
Często przy lepszych modelach wystarczające.

import json

prompt = "Classify the sentiment of the text as positive, negative, or neutral. Return JSON {\"sentiment\": \"...\"}."
text = "I love how easy this app is to use!"

r = chat_once(f"{prompt}\n\nTEXT: {text}", temperature=0.0)
print(r["text"])

try:
    data = json.loads(r["text"])
    print("Parsowanie OK:", data)
except json.JSONDecodeError:
    print("Nie jest to czysty JSON. Potrzebujesz ostrzejszego formatowania + naprawy.")

Ostrzejsze wymuszenie + „repair prompt”
To najprostszy wzorzec, który często realnie działa w aplikacjach: prosisz o JSON, parsujesz, jeśli fail prosisz o poprawę TYLKO JSON.

import json

def get_json_or_repair(user_task: str, temperature: float = 0.0, max_repairs: int = 1):
    strict = (
        "Return ONLY valid JSON. "
        "No markdown. No code fences. No comments. No trailing text."
    )
    r = chat_once(user_task + "\n\n" + strict, temperature=temperature)
    txt = r["text"]

    for _ in range(max_repairs + 1):
        try:
            return json.loads(txt), r
        except json.JSONDecodeError as e:
            repair = (
                strict
                + f"\nThe previous output was not valid JSON. Fix it.\n"
                + f"JSON error: {str(e)}\n"
                + "Previous output:\n"
                + txt
            )
            r = chat_once(repair, temperature=0.0)
            txt = r["text"]

    raise ValueError("Failed to obtain valid JSON")

task = (
    "Classify the sentiment of the text as positive, negative, or neutral.\n"
    "Return JSON with exactly one key: sentiment.\n"
    "Text: I love how easy this app is to use!"
)

data, meta = get_json_or_repair(task, temperature=0.0, max_repairs=2)
print("JSON:", data)
print("Tokens:", (meta["usage"] or {}).get("total_tokens"))

Walidacja wartości (mini-schemat)
JSON ma być nie tylko parsowalny, ale też poprawny semantycznie. W takiej sytuacji w bardziej zaawansowanych systemach (i w Twoim projekcie) użyjemy biblioteki Pydantic, aby walidować nie tylko czy to jest JSON, ale czy ma odpowiednie pola (np. czy sentiment to faktycznie jedno ze słów: positive, negative, neutral).

from pydantic import BaseModel
from typing import Literal

class SentimentOut(BaseModel):
    sentiment: Literal["positive", "negative", "neutral"]

task = """
Classify the sentiment of the text. Return ONLY JSON with:
{"sentiment": "positive"|"negative"|"neutral"}
Text: I love how easy this app is to use!
"""

r = chat_once(task, temperature=0.0)
obj = SentimentOut.model_validate_json(json_data=r["text"])
print(obj.model_dump())

Gdyby pojawiły się problemy z tym, że model zwraca dodatkowe frazy tak, jak ```json można wykorzystać wyrażenia regularne.

import re
text = r["text"]
match = re.search(r'```json\s*([\s\S]*?)\s*```', text)
if match:
    text = match.group(1).strip()

Wymuszenie JSON przez API
Ostatnim sposobem jest wymuszenie odpowiedzi w formacie JSON za pomocą odpowiednich parametrów dla konfiguracji lub generowania. Poniżej przykład dla Gemini.

config = genai.types.GenerateContentConfig(
    system_instruction=system,
    temperature=temperature,
    top_p=top_p,
    top_k=top_k,
    max_output_tokens=max_output_tokens,
    stop_sequences=["<END>"],
    response_mime_type="application/json",
    response_schema=Sentiment,
)
# lub
config = genai.types.GenerateContentConfig(
    system_instruction=system,
    temperature=temperature,
    top_p=top_p,
    top_k=top_k,
    max_output_tokens=max_output_tokens,
    stop_sequences=["<END>"],
    response_mime_type="application/json",
    response_json_schema={
        "type": "object",
        "properties": {
        	"sentiment": {
    	            "type": "string",
    	            "enum": ["positive", "negative", "neutral"]
    	        }
            }, 
        "required": ["sentiment"]
    }
)

resp = client.models.generate_content(
	model=GEMINI_MODEL,
	contents=prompt,
	config=config,
)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *