Wieczorem w obozie zadajesz swojemu skryptowi dwa pytania pod rząd. Pierwsze: "Jestem na safari w Kenii". Drugie: "Jakie zwierzęta tu zobaczę?". Model odpowiada uprzejmie i obszernie, tylko kompletnie nie wie, gdzie jest "tu" - wymienia pandy, tygrysy i niedźwiedzie polarne w jednym akapicie. Nic się nie zepsuło, nie zgubiłeś klucza API, nie pomyliłeś nazwy modelu. Tak to po prostu działa, a dopóki tego nie zrozumiesz, każdy Twój chatbot będzie miał pamięć jednodniowej muchy.
W tej lekcji zbudujesz Darwina, który prowadzi rozmowę. Najpierw ręcznie, na zwykłej liście Pythona, żebyś zobaczył cały mechanizm bez odrobiny magii. Potem to samo, ale gotowymi klockami LangChaina. Na końcu dołożysz mu wiedzę, której w modelu nie ma - treść przewodnika po Twoim rezerwacie - i wystawisz całość jako usługę, z której skorzysta cała stacja badawcza.
Zacznijmy od zobaczenia problemu na własne oczy. Poniżej wysyłasz dwa niezależne zapytania do tego samego modelu, w tym samym skrypcie, jedno po drugim. Pierwsze zdanie informuje o miejscu pobytu, drugie zadaje pytanie, które bez tej informacji nie ma sensu. Zwróć uwagę na to, co dokładnie ląduje w polu
messages przy drugim wywołaniu: jedno jedyne zdanie, bez śladu po poprzednim. client to znany Ci z wcześniejszych lekcji obiekt klienta, a chat.completions.create to ta sama ścieżka wywołania, którą już znasz.1from openai import OpenAI
2
3client = OpenAI(api_key="your-api-key")
4
5pierwsze = client.chat.completions.create(
6 model="gpt-5",
7 messages=[{"role": "user", "content": "Jestem na safari w Kenii."}]
8)
9
10drugie = client.chat.completions.create(
11 model="gpt-5",
12 messages=[{"role": "user", "content": "Jakie zwierzeta tu zobacze?"}]
13)Policzmy, co się między tymi wywołaniami nie zmieniło: klient jest ten sam, klucz ten sam, model ten sam, proces Pythona ten sam, a zmienna
pierwsze wciąż siedzi w pamięci Twojego programu. Zmieniło się dokładnie nic - i o to chodzi. Serwer, który obsłużył drugie zapytanie, nie ma pojęcia, że pierwsze w ogóle istniało. API czatu jest bezstanowe: każde zapytanie jest niezależną przesyłką, a model widzi wyłącznie to, co w niej włożysz.Dlatego odpowiedź "model sam pamięta wszystkie rozmowy" jest fałszywa. Model to zamrożony zestaw wag - nie zapisuje niczego po zakończeniu odpowiedzi i nie ma gdzie tego zapisać. Równie fałszywe jest "nie da się tego zrobić", bo cała reszta tej lekcji polega na tym, że jednak się da. Prawdziwa odpowiedź jest znacznie mniej efektowna, niż ludzie się spodziewają: pamięć chatbota to przechowywanie historii wiadomości po Twojej stronie i dosyłanie jej razem z każdym kolejnym zapytaniem.
Skoro model widzi tylko zawartość pola
messages, to wystarczy, że sam będziesz prowadził protokół rozmowy i wysyłał go w całości za każdym razem. Protokół to lista słowników, a każdy słownik ma dwa klucze: "role" mówi, kto się odezwał, a "content" niesie treść. Role są trzy. "system" to instrukcja dla modelu, czyli kim ma być i jak się zachowywać - stoi zawsze na początku listy. "user" to Twoje wiadomości. "assistant" to odpowiedzi modelu. Zacznijmy od samej instrukcji systemowej, jeszcze przed pierwszym pytaniem.1messages = [
2 {"role": "system", "content": "Jestes Darwin - przyjazny przewodnik safari."}
3]To jest cała pamięć Twojego chatbota w chwili startu: zwykła lista Pythona z jednym słownikiem w środku. Żadnej bazy danych, żadnej biblioteki, żadnego stanu po stronie serwera OpenAI. Instrukcja systemowa trafia na pozycję zerową i zostaje tam do końca rozmowy - to jedyna wiadomość, której nigdy nie usuwasz, bo to ona utrzymuje Darwina w roli przewodnika, zamiast pozwolić mu wrócić do bycia ogólnym asystentem.
Teraz dopisujesz pytanie użytkownika. Do dokładania elementów na koniec listy służy metoda
.append(, a wkładasz do niej nowy słownik z rolą "user". Kolejność kluczy zapisujemy zawsze tak samo: najpierw "role", potem "content". Treść pytania siedzi w zmiennej text, więc pod klucz "content" podstawiasz właśnie ją, bez cudzysłowów - to zmienna, nie napis.1text = "Jestem na safari w Kenii."
2
3messages.append({"role": "user", "content": text})
4
5print(len(messages))
6print(messages[-1])Po uruchomieniu zobaczysz
2 oraz {'role': 'user', 'content': 'Jestem na safari w Kenii.'} - lista urosła o jeden element, a [-1] wskazuje ostatni z nich. Rozłóżmy tę jedną linię na części pierwsze, bo będziesz ją pisał setki razy: messages (nazwa listy), .append( (metoda dokładająca na koniec), {"role": "user", (otwarcie słownika i rola), "content": text} (treść i zamknięcie słownika), ) (zamknięcie wywołania metody). Dokładnie w tej kolejności, bo słownik musi się domknąć wcześniej niż nawias append.Warto zauważyć, czego ta operacja nie zrobiła. Nie wysłała niczego do sieci, nie kosztowała ani jednego tokena i nie poinformowała modelu o niczym. Zmieniła się wyłącznie zawartość zmiennej w Twoim programie. Przy okazji odpada kolejna kusząca odpowiedź: ciasteczka przeglądarki. Ciasteczko żyje w przeglądarce użytkownika i nigdy nie trafia do modelu - w najlepszym razie potrafi przenieść identyfikator sesji, ale sam protokół rozmowy i tak musi leżeć po stronie serwera i zostać dołączony do zapytania. Kanał HTTP nie zna pojęcia "poprzednia wiadomość".
Teraz zamykamy pełny obrót rozmowy w funkcji. Ona dokłada pytanie do listy, wysyła całą listę do modelu, wyciąga tekst odpowiedzi znaną Ci ścieżką
odpowiedz.choices[0].message.content, a następnie dokłada tę odpowiedź z powrotem do listy pod rolą "assistant".1def zapytaj(text):
2 messages.append({"role": "user", "content": text})
3
4 odpowiedz = client.chat.completions.create(
5 model="gpt-5",
6 messages=messages
7 )
8
9 tresc = odpowiedz.choices[0].message.content
10 messages.append({"role": "assistant", "content": tresc})
11 return trescSercem tej funkcji jest zapis
messages=messages: do modelu jedzie cała dotychczasowa historia, a nie samo bieżące pytanie. Drugi append jest równie ważny i najczęściej się o nim zapomina. Gdybyś zapisywał tylko pytania, model nie widziałby własnych wypowiedzi i po kilku turach zacząłby powtarzać te same odpowiedzi albo zaprzeczać sam sobie. Rozmowa musi być kompletna z obu stron.Ma to swoją cenę i lepiej wiedzieć o niej od razu. Przy dwudziestej turze wysyłasz do modelu dziewiętnaście poprzednich wymian zdań, płacisz za wszystkie tokeny w każdym zapytaniu, a historia w końcu przestanie się mieścić w oknie kontekstu. Dlatego prawdziwe chatboty przycinają protokół: trzymają instrukcję systemową plus kilkanaście ostatnich wiadomości, a starsze streszczają albo wyrzucają. Pamięć nie jest darmowa - jest po prostu Twoja.
Ręczna lista jest doskonała do zrozumienia mechanizmu, ale w prawdziwym projekcie szybko obrasta w kod: przycinanie, wiele rozmów naraz, konwersje formatów. LangChain ma na to gotowy klocek o nazwie
ChatMessageHistory. Zanim go zaimportujesz, przeczytaj linię importu od lewej do prawej, bo jej kolejność jest zawsze taka sama: from → langchain_community.chat_message_histories → import → ChatMessageHistory. Najpierw from wskazuje moduł, potem import wskazuje nazwę, którą z tego modułu wyciągasz. Sam adres modułu też coś znaczy: langchain_community to pakiet z integracjami rozwijanymi przez społeczność, a chat_message_histories to moduł zapisany w liczbie mnogiej, bo mieszka w nim wiele rodzajów historii - w pamięci, w pliku, w bazie. Klasa na końcu idzie wielkimi literami, moduł małymi, i tej pary nie da się zamienić miejscami.1from langchain_community.chat_message_histories import ChatMessageHistory
2
3historia = ChatMessageHistory()
4historia.add_user_message("Jestem na safari w Kenii.")
5historia.add_ai_message("Swietnie! Kenia to raj dla obserwatorow zwierzat.")
6
7print(len(historia.messages))Wypisze
2. To ta sama lista co wcześniej, tylko ubrana w obiekt: add_user_message odpowiada dopisaniu roli "user", add_ai_message roli "assistant", a pole .messages daje dostęp do zgromadzonych wiadomości. Zwróć uwagę, czego tu nie ma: żadnego wywołania modelu, żadnego klucza API, żadnego kosztu. ChatMessageHistory to notatnik, nie rozmówca - trzyma zapis i nic poza tym.Reszta lekcji to złożenie działającego bota i warto od razu zapamiętać kolejność, w jakiej się to robi. Krok pierwszy: definiujesz system prompt, czyli tożsamość Darwina. Krok drugi: konfigurujesz pamięć. Krok trzeci: tworzysz chain, który spina prompt, model i pamięć w jedną całość. Krok czwarty: implementujesz pętlę rozmowy, która karmi to wszystko kolejnymi pytaniami. Nie da się tego przestawić - chain nie ma czego spiąć, dopóki nie istnieje prompt i pamięć, a pętla nie ma czego wołać, dopóki nie istnieje chain.
Krok pierwszy.
ChatPromptTemplate.from_messages buduje szablon rozmowy z listy elementów, a MessagesPlaceholder to specjalny element oznaczający dziurę, w którą zaraz wpadnie historia. Parametr variable_name nadaje tej dziurze nazwę. Ostatni wpis to bieżące pytanie użytkownika, wstawiane pod nazwą input.1from langchain_openai import ChatOpenAI
2from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
3
4llm = ChatOpenAI(model="gpt-5", temperature=0.7)
5
6system = """Jestes Darwin - przyjazny przewodnik Safari.
7Odpowiadasz entuzjastycznie i dzielisz sie ciekawostkami o zwierzetach.
8Zawsze dbasz o bezpieczenstwo turystow."""
9
10prompt = ChatPromptTemplate.from_messages([
11 ("system", system),
12 MessagesPlaceholder(variable_name="history"),
13 ("human", "{input}")
14])Szablon ma trzy elementy i każdy odgrywa inną rolę. Instrukcja systemowa jest stała i powtarza się przy każdej turze - to ona sprawia, że Darwin mówi entuzjastycznie i pilnuje bezpieczeństwa. Placeholder jest pusty przy pierwszym pytaniu i rośnie z każdą kolejną wymianą zdań. Para
("human", "{input}") to miejsce na bieżące pytanie. Nazwa "history" nie jest przypadkowa i za chwilę musi się zgadzać co do znaku z ustawieniem history_messages_key - rozjazd w tej jednej nazwie to najczęstszy powód, dla którego bot pozornie działa, a mimo to niczego nie pamięta.Krok drugi. Pamięć musi rozróżniać rozmówców, bo z Darwina będzie korzystać kilka osób naraz. Wykorzystasz do tego zwykły słownik
store, w którym kluczem jest identyfikator sesji, a wartością osobny obiekt ChatMessageHistory. Funkcja get_session_history przyjmuje identyfikator i zwraca historię tej jednej rozmowy, tworząc ją przy pierwszym kontakcie.1store = {}
2
3def get_session_history(session_id: str):
4 if session_id not in store:
5 store[session_id] = ChatMessageHistory()
6 return store[session_id]Spotkasz w internecie krótszy wariant, w którym zamiast słownika podaje się jedną globalną historię dla wszystkich. Polecam wersję ze słownikiem, bo tamta skraca kod o dwie linie, a wprowadza dwa realne problemy: dwie osoby dostają wspólny protokół, więc Darwin odpowiada turyście B, opierając się na wypowiedziach turysty A, a dodatkowo nie da się wyczyścić jednej rozmowy bez kasowania wszystkich. Ten sam
session_id to zawsze ta sama rozmowa, inny session_id to czysta karta.Krok trzeci. Operator
| znasz z poprzedniej lekcji - skleja prompt z modelem w łańcuch. Nowy jest RunnableWithMessageHistory, czyli owijka, która przed każdym wywołaniem wstrzykuje historię do placeholdera, a po odpowiedzi dopisuje obie wiadomości do właściwej sesji. Parametr input_messages_key mówi jej, które pole wejścia jest pytaniem, a history_messages_key - do której dziury wsypać historię.1from langchain_core.runnables.history import RunnableWithMessageHistory
2
3chain = prompt | llm
4
5chatbot = RunnableWithMessageHistory(
6 chain,
7 get_session_history,
8 input_messages_key="input",
9 history_messages_key="history"
10)Zauważ, że sam łańcuch
prompt | llm nie zmienił się ani o znak i nadal nic nie wie o pamięci. Owijka nie przepisuje go ani nie podmienia modelu - dokłada tylko dwa ruchy dookoła: odczyt historii przed i zapis po. Do RunnableWithMessageHistory przekazujesz samą funkcję get_session_history, bez nawiasów, bo to LangChain ma ją wywołać we właściwym momencie, podstawiając identyfikator z konfiguracji.Zanim napiszesz pętlę, sprawdźmy bota na dwóch pytaniach. Identyfikator sesji podaje się w słowniku
config, w zagnieżdżonym kluczu "configurable". Metoda invoke jest ta sama, co przy każdym innym łańcuchu - zmienia się tylko to, że dokładasz do niej config.1config = {"configurable": {"session_id": "user_123"}}
2
3odpowiedz = chatbot.invoke({"input": "Czesc! Jestem na safari!"}, config=config)
4print("Darwin:", odpowiedz.content)
5
6odpowiedz = chatbot.invoke({"input": "Jakie zwierzeta tu zyja?"}, config=config)
7print("Darwin:", odpowiedz.content)To jest ta sama para pytań, od której zaczęliśmy lekcję, i tym razem drugie ma sens - Darwin wie, że "tu" oznacza safari, o którym mowa w pierwszej wiadomości. Nie dlatego, że model zmądrzał, tylko dlatego, że przy drugim wywołaniu do serwera pojechała cała historia. Jeśli podmienisz
"user_123" na inny napis, Darwin z miejsca zapomni rozmowę, bo zajrzy do innej przegródki słownika store. Odpowiedź wciąż czytasz przez .content, dokładnie tak jak przy zwykłym łańcuchu.Krok czwarty. Pętla rozmowy to prosty program konsolowy: czytaj wiersz od użytkownika, sprawdź, czy to nie polecenie wyjścia, wyślij do bota, wypisz odpowiedź, powtarzaj. Funkcja
input czeka na tekst z klawiatury, .strip() obcina zbędne spacje, break przerywa pętlę, a continue przeskakuje do następnego obiegu bez wykonywania reszty ciała.1def uruchom_chatbota():
2 print("Safari Chatbot - wpisz quit, aby zakonczyc")
3
4 config = {"configurable": {"session_id": "cli_session"}}
5
6 while True:
7 pytanie = input("Ty: ").strip()
8
9 if pytanie.lower() in ["quit", "exit", "q"]:
10 print("Do zobaczenia na safari!")
11 break
12
13 if not pytanie:
14 continue
15
16 odpowiedz = chatbot.invoke({"input": pytanie}, config=config)
17 print("Darwin:", odpowiedz.content)
18
19if __name__ == "__main__":
20 uruchom_chatbota()Dwa warunki wyglądają podobnie, a robią coś zupełnie innego. Pierwszy kończy program, drugi tylko ignoruje puste wejście - gdybyś użył tam
break, przypadkowy Enter zamykałby całą rozmowę. Zapis pytanie.lower() pozwala wyjść zarówno przez quit, jak i QUIT. Identyfikator sesji jest tu stały, bo przy konsoli siedzi jedna osoba, i leży poza pętlą, żeby każdy obieg trafiał do tej samej historii. Masz właśnie komplet: system prompt, pamięć, chain, pętla rozmowy.Twój bot świetnie gada o sawannie w ogóle, ale zapytaj go o zasady bezpieczeństwa obowiązujące w Twoim rezerwacie, a zacznie zmyślać. Nie ma skąd ich znać - w danych treningowych nigdy nie było pliku z regulaminem obozu. Możesz dotrenować własny model, co kosztuje majątek i trzeba powtarzać przy każdej zmianie regulaminu, albo sięgnąć po RAG, czyli Retrieval Augmented Generation.
RAG to technika wzbogacania odpowiedzi modelu o informacje z Twoich dokumentów: przed zadaniem pytania wyszukujesz w nich najbardziej pasujące fragmenty i wklejasz je do promptu jako kontekst. Uporządkujmy od razu trzy nieporozumienia. RAG nie jest formatem plików - dokumenty mogą być w
.txt, .pdf czy .md i technice jest to obojętne. Nie jest typem sieci neuronowej - nie zmienia architektury ani wag modelu, który przez cały czas pozostaje dokładnie taki, jaki był. Nie jest też protokołem API - nie istnieje żaden endpoint ani standard komunikacji o tej nazwie, a cała robota dzieje się w Twoim kodzie, zanim jeszcze wyślesz zwykłe zapytanie.Pipeline ma pięć kroków w niezmiennej kolejności. Załaduj dokumenty. Podziel je na fragmenty, czyli zrób chunking. Stwórz embeddingi tych fragmentów. Wyszukaj fragmenty podobne do pytania. Wygeneruj odpowiedź z tak zdobytym kontekstem. Pierwsze trzy kroki wykonujesz raz, przy starcie; dwa ostatnie powtarzają się przy każdym pytaniu.
Krok drugi zwykle sprawia najwięcej kłopotu, więc zobaczmy go w czystym Pythonie, bez żadnej biblioteki. Poniżej tniesz tekst na kawałki po sto znaków, ale co ważne - z zakładką dwudziestu znaków, czyli każdy kolejny fragment zaczyna się dwadzieścia znaków przed końcem poprzedniego.
1tekst = "Lwy sa najbardziej aktywne o swicie i o zmierzchu. " * 8
2
3rozmiar = 100
4zakladka = 20
5fragmenty = []
6
7start = 0
8while start < len(tekst):
9 fragmenty.append(tekst[start:start + rozmiar])
10 start += rozmiar - zakladka
11
12print(len(tekst))
13print(len(fragmenty))
14print(fragmenty[0][-20:] == fragmenty[1][:20])Wynik to
408, 6 oraz True. Ostatnia linia jest tu najciekawsza: ogon pierwszego fragmentu to dosłownie ta sama treść, co początek drugiego. Po to jest zakładka - gdyby cięcie było równe, zdanie przecięte w połowie nie występowałoby w całości nigdzie i wyszukiwarka nie miałaby szans go znaleźć. Zauważ też, że krok pętli wynosi rozmiar - zakladka, czyli osiemdziesiąt, a nie sto: dlatego z 408 znaków wychodzi sześć fragmentów, a nie pięć. W prawdziwym projekcie nie piszesz tego ręcznie - używasz RecursiveCharacterTextSplitter, który dodatkowo stara się ciąć na granicach akapitów i zdań, zamiast w środku wyrazu.Wracamy do LangChaina i realizujemy kroki pierwszy i drugi.
TextLoader czyta plik tekstowy z dysku, a jego metoda load zwraca listę obiektów typu Document - to nie jest zwykły napis, tylko treść razem z metadanymi, na przykład nazwą pliku źródłowego. RecursiveCharacterTextSplitter przyjmuje dwa parametry, które właśnie poznałeś w wersji ręcznej: chunk_size to długość fragmentu w znakach, chunk_overlap to zakładka.1from langchain_community.document_loaders import TextLoader
2from langchain.text_splitter import RecursiveCharacterTextSplitter
3
4loader = TextLoader("safari_guide.txt")
5documents = loader.load()
6
7splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
8chunks = splitter.split_documents(documents)Po tych czterech liniach masz w
chunks listę kilkudziesięciu fragmentów przewodnika, każdy o długości około tysiąca znaków, z dwustuznakową zakładką - czyli dwadzieścia procent treści powtarza się między sąsiadami. Metoda nazywa się split_documents, a nie split_text, bo przyjmuje dokumenty i zwraca dokumenty, przenosząc metadane na każdy fragment. Nic z tego nie poszło jeszcze do żadnego modelu - to zwykła operacja na tekście, wykonana w całości na Twoim komputerze.Krok trzeci: embeddingi. Embedding to lista liczb opisująca znaczenie fragmentu - teksty o zbliżonym sensie dostają wektory leżące blisko siebie, nawet jeśli nie mają wspólnych słów.
OpenAIEmbeddings to model, który zamienia tekst na taki wektor, a FAISS to indeks przechowujący wektory i potrafiący błyskawicznie znaleźć najbliższe sąsiedztwo zadanego punktu.1from langchain_openai import OpenAIEmbeddings
2from langchain_community.vectorstores import FAISS
3
4embeddings = OpenAIEmbeddings()
5vectorstore = FAISS.from_documents(chunks, embeddings)Metoda
from_documents przelicza embedding dla każdego fragmentu i wkłada go do indeksu. To jedyny moment w całym pipeline, w którym płacisz za przetworzenie całego przewodnika, i dlatego robisz to raz, przy starcie aplikacji, a nie przy każdym pytaniu turysty. Warto zapamiętać, że jest to zupełnie inny model niż llm - embeddingowy nie rozmawia i nie generuje zdań, on wyłącznie mierzy znaczenie.Zostały kroki czwarty i piąty, które LangChain spina w jeden obiekt.
as_retriever robi z indeksu wyszukiwarkę, a search_kwargs z parametrem k ustala, ile najbardziej podobnych fragmentów ma ona zwracać. RetrievalQA.from_chain_type łączy tę wyszukiwarkę z modelem, przy czym chain_type="stuff" oznacza najprostszą strategię: wsadź wszystkie znalezione fragmenty do promptu naraz.1from langchain.chains import RetrievalQA
2
3qa_chain = RetrievalQA.from_chain_type(
4 llm=llm,
5 chain_type="stuff",
6 retriever=vectorstore.as_retriever(search_kwargs={"k": 3})
7)
8
9wynik = qa_chain.invoke({"query": "Jak bezpiecznie obserwowac lwy?"})
10print(wynik["result"])Przy tym jednym
invoke dzieje się wszystko naraz: pytanie zamienia się w embedding, indeks wybiera trzy najbliższe fragmenty przewodnika, trafiają one do promptu jako kontekst, a model formułuje odpowiedź. Podkreślmy, co się przy tym nie wydarzyło: model nie został niczego nauczony i za sekundę, zapytany bez RAG, znów nie będzie znał Twojego regulaminu. Cała wiedza przyszła w prompcie, na jedno zapytanie. Wejście podajesz pod kluczem "query", a gotowy tekst odbierasz spod klucza "result" - to słownik, więc .content tu nie zadziała.Wersja konsolowa działa świetnie, dopóki jedynym użytkownikiem jesteś Ty przy laptopie w namiocie. Kiedy z Darwina ma korzystać strona rezerwatu i aplikacja przewodników, potrzebujesz interfejsu HTTP.
FastAPI to framework webowy, w którym trasy opisuje się dekoratorem, a kształt danych - klasami dziedziczącymi po BaseModel z biblioteki pydantic. Taka klasa sprawdza typy za Ciebie: jeśli ktoś przyśle message jako liczbę, dostanie czytelny błąd, zanim Twój kod w ogóle wystartuje.1from fastapi import FastAPI, HTTPException
2from pydantic import BaseModel
3
4app = FastAPI()
5
6class ChatRequest(BaseModel):
7 message: str
8 session_id: str
9
10class ChatResponse(BaseModel):
11 response: str
12
13@app.post("/chat", response_model=ChatResponse)
14async def chat(request: ChatRequest):
15 config = {"configurable": {"session_id": request.session_id}}
16
17 try:
18 odpowiedz = chatbot.invoke({"input": request.message}, config=config)
19 return ChatResponse(response=odpowiedz.content)
20 except Exception as e:
21 raise HTTPException(status_code=500, detail=str(e))Najważniejsze jest to, co się nie zmieniło: ten sam obiekt
chatbot, ten sam słownik store, ten sam invoke i ten sam config. Zmieniło się wyłącznie źródło pytania - zamiast input z klawiatury przychodzi ono z ciała żądania HTTP. Identyfikator sesji przysyła teraz klient i to on decyduje, do której przegródki pamięci trafi rozmowa; przeglądarka może trzymać ten identyfikator choćby w ciasteczku, ale sam protokół rozmowy nadal leży na Twoim serwerze i nadal jest dosyłany do modelu przy każdym zapytaniu. HTTPException zamienia wyjątek w porządną odpowiedź o statusie 500, zamiast wysyłać klientowi ślad stosu. Serwer uruchamiasz poleceniem uvicorn main:app --reload. Przy większym ruchu warto jeszcze zamienić invoke na await chatbot.ainvoke(...), żeby wolne wywołanie modelu nie blokowało pozostałych żądań."role" i "content", a role są trzy: "system", "user" i "assistant".messages, .append(, {"role": "user", , "content": text}, ).from → langchain_community.chat_message_histories → import → ChatMessageHistory.RunnableWithMessageHistory owija istniejący łańcuch i sam dokłada historię wskazaną przez session_id z config.Zapamiętaj z tej lekcji jeden obraz, @name: model jest jak przewodnik z doskonałym okiem i zerową pamięcią - to Ty prowadzisz dziennik wyprawy i to Ty podsuwasz mu go przed każdym pytaniem, razem z odpowiednią stroną przewodnika po rezerwacie.