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,
)
