%%{init: {'flowchart': {'curve': 'linear'}}}%%
graph TD;
__start__([<p>__start__</p>]):::first
contextualize(contextualize)
retrieve(retrieve)
rag(rag)
__end__([<p>__end__</p>]):::last
__start__ --> contextualize;
contextualize --> retrieve;
rag --> __end__;
retrieve --> rag;
classDef default fill:#f2f0ff,line-height:1.2
classDef first fill-opacity:0
classDef last fill:#bfb6fc
7 Lage modellen
Frem til nå har denne veiledningen fokusert på kunnskapsbasen og embedding modellen som brukes sammen med vektordatabasen. Vi skal i dette kapittelet fokusere på språkmodellen, men grunnen til at dette er satt til sist er at det ikke er så mye å gjøre med språkmodellen. I de fleste tilfeller hvor man ønsker å benytte KBS vil det allerede være gitt at man bruker en av de bedre språkmodellene. Dette gjør at det ikke er så mye som skal settes opp for å komme i gang og jobben som må gjøres går mer på å forberede ledetekst (Definisjon D.8) enn å sette opp selve språkmodellen.
For å skape et system som enkelt kan utvides med funksjonalitet og flere språkmodeller som jobber sammen for å generere endelig svar kommer vi til å benytte LangGraph sammen med LangChain. I LangGraph modelleres systemet som en graf (vi kommer til å bruke begrepet kjøretidsgraf) hvor noder utfører handlinger, mens kanter dirigerer flyten mellom nodene. LangGraph gjør det enklere å modellere flyten mellom språkmodeller og samtidig blir koden ofte mer lesbar fordi det er flere “vanlige” Python metoder sammenlignet med LangChain.
Merk at LangGraph har introduksjon til flere varianter av KBS som inneholder mer detaljer enn det vi kommer til å gjenskape her.
Vi har illustrert forskjellen på ren LangChain mot LangGraph i en presentasjon du finner her.
Veiledningen brukte tidligere bare LangChain, mens nyere erfaring tilsier at LangChain + LangGraph gir mer forståelig flyt og enklere å utvide systemet.
En av de få variablene som man kan endre på som vil gi store utslag på svarene fra språkmodellen er temperatur D.10.
I et KBS system vil det være naturlig å velge lavere temperatur for å få gjentakende svar som ligner på hverandre. Bakdelen med dette er at svarene har en tendens til å bli kortere og kan oppleves kjedelig av brukeren.
7.1 Opprette en kjøretidsgraf
Det første man burde tenke over med LangGraph er hvordan tilstanden som sendes mellom nodene skal se ut. Til å begynne med har vi en enkel tilstand med spørsmål fra bruker, et sted for dokumentene vi henter fra vektordatabasen og svaret fra språkmodellen.
Vi definerer tilstanden som en TypedDict for å si at tilstanden er en dict med bare angitte nøkler.
from typing import TypedDict
class GraphState(TypedDict):
"""Tilstanden i kjøretidsgrafen"""
question: str
context: list[str]
answer: strNår vi kompilerer og bruker kjøretidsgrafen senere vil det bli tydelig hvordan denne klassen brukes i praksis.
7.1.1 Hente dokumenter
Et alternativ til å ha en egen node i kjøretidsgrafen som henter informasjon er å bruke verktøykall (forklart i Kapittel 10). Verktøykall er en kraftig generell teknikk som kan brukes til mange formål (ikke bare til å hente informasjon). Det anbefales derfor å gjøre seg kjent med verktøykalling før man designer resten av kjøretidsgrafen.
Det første vi må gjøre i KBS systemet vårt er å hente ut dokumenter slik at vi har noe språkmodellen kan jobbe med. Dette blir inngangen i kjøretidsgrafen vår og vi definerer den første noden.
Signaturen til nodene i LangGraph har formen GraphState -> Partial update. Dette tillater flere noder å parallelt produsere svar som senere blir kombinert.
from langchain_google_community import BigQueryVectorSearch
store = BigQueryVectorSearch.from_documents() # Se kapittel om vektordatabase
retriever = store.as_retriever(search_kwargs=dict(k=5))
def retrieve(state):
"""Node i grafen som henter dokumenter fra vektordatabasen
basert på spørsmål fra bruker."""
docs = retriever.invoke(state["question"])
return {"context": docs}Denne noden blir inngangen i kjøretidsgrafen vår. Merk også at i denne noden så benytter vi oss ikke av en språkmodell. Dette illustrerer styrken i LangGraph ved at vi kan kjede sammen forskjellige noder med forskjellige oppgaver.
7.1.2 Be modellen generere svar på bakgrunn av dokumentene
Det neste vi trenger å gjøre i KBS systemet er å gi språkmodellen tilgang til dokumentene fra kunnskapsbasen vår. Måten vi gir dokumenter på er å designe systemledeteksten vår på en måte hvor den inneholder dokumentene og så ber modellen svare på påfølgende spørsmål på bakgrunn av disse dokumentene.
Et eksempel på en systemledetekst er gitt under:
You are an assistant for question-answering tasks. Use the following pieces of
retrieved context to answer the question. If you don't know the answer, just
say that you don't know. Use three sentences maximum and keep the answer
concise. Answer in Norwegian, or English if the question is English.
{context}I eksempelet over ber vi systemet svare på spørsmål, men spesifiserer at den bare skal benytte konteksten vi fyller ut senere. Utformingen på ledeteksten gjør at språkmodellen prøver å benytte teksten fra kunnskapsbasen og eventuelt ikke svare heller enn å hallusinere.
Vi bruker for det meste engelsk i ledeteksten vi skriver og ber modellen svare på norsk. Grunnen til dette er at erfaring tilsier at modellene er trent for å svare på spørsmål med et spesifikt språk. Et eksempel på dette som vi kommer tilbake senere er at modellen forstår chat history mye bedre enn å benytte context.
Her gjelder rådene om utprøving og det kan godt hende at egen erfaring tilsier at norsk fungerer utmerket!
Vi gjør deretter teksten over om til en ChatPromptTemplate som så kan benyttes av LangChain.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
("human", "{question}")
]
)Vi lager deretter en funksjon som vil fungere som en node i LangGraph kjøretidsgrafen vår.
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import AzureChatOpenAI
llm = AzureChatOpenAI()
chain = prompt | llm | StrOutputParser()
def generator(state):
"""Noden som svarer på spørsmål"""
context = "\n\n".join(state["context"])
answer = chain.invoke({"context": context, "question": state["question"]})
return {"answer": answer}7.1.3 Bygge grafen
For å sette sammen stegene over bygger vi kjøretidsgrafen.
from langgraph.graph import END, START, StateGraph
# Vi benytter her tilstanden vi definerte i starten som grunnlag for grafen
workflow = StateGraph(GraphState)
# Legg til noder i grafen
workflow.add_node(retrieve)
workflow.add_node("rag", generator)
# Definer hvordan nodene henger sammen
workflow.add_edge(START, "retrieve")
workflow.add_edge("retrieve", "rag")
workflow.add_edge("rag", END)
# Kompiler grafen
app = workflow.compile()Tilslutt, for å benytte grafen:
LangGraph har støtte for å kalle på kjeden på mange forskjellige måter i forskjellige kontekster. Dette gir muligheter for å batch-e kallene eller strømme svar for bedre brukeropplevelse.
initial_state = {"question": "Hva er samordning mellom dagpenger og sykepenger?"}
final_state = app.invoke(initial_state)
final_state["answer"]Samordning mellom dagpenger og sykepenger er relevant når en person er sykmeldt, men fortsatt i stand til å jobbe 50% eller mer. I slike tilfeller vil Nav vurdere en kombinasjon av dagpenger og sykepenger. Dette innebærer at personen må sende inn søknad om sykepenger og Nav vil sende beskjed til det lokale Nav-kontoret som vurderer denne samordningen.
7.2 Forbedre spørsmål i en chat-historikk
Ved å bruke oppsette over vil man fort oppleve at det første spørsmålet gir veldig gode svar og deretter blir svarene veldig dårlige eller ikke eksisterende. Grunnen til dette er at dokumentene som blir sendt til språkmodellen ikke lengre gir mening.
7.2.1 Mangel på kontekst
For å prøve å kontekstualisere dette kan vi se på en tenkt samtale:
> (Bruker) Hva er samordning mellom dagpenger og sykepenger?
> (Språkmodell): Samordning mellom dagpenger og sykepenger er relevant når en
person er sykmeldt, men fortsatt i stand til å jobbe 50% eller mer. I slike
tilfeller vil Nav vurdere en kombinasjon av dagpenger og sykepenger. Dette
innebærer at personen må sende inn søknad om sykepenger og Nav vil sende beskjed
til det lokale Nav-kontoret som vurderer denne samordningen.
> (Bruker) Hva innebærer det for pensjon?
> (Språkmodell): Det kan jeg ikke svare på...Når vi ser på teksten som ble gitt til vektordatabasen for det andre spørsmålet til brukeren var det begrenset til Hva innebærer det for pensjon?. Dette gjør det veldig vanskelig for vektordatabasen å hente ut informasjon fordi vi manger kontekst fra det første spørsmålet. Språkmodellen mottar deretter dokumenter som ikke har så mye med samordning og pensjon, og klarer ikke å generere et svar med de begrensningene vi har gitt i systemledeteksten.
For å utbedre dette kan vi først benytte en språkmodell til å omformulere spørsmålet før vi sender spørsmålet videre til vektordatabasen.
7.2.2 Omformulere spørsmålet
Teknikken er å be en språkmodellen om å omformulere spørsmålet hvis det mangler kontekst. Dette koster oss litt ekstra tid ved å spørre en språkmodellen, men det er en robust måte å få omformulert spørsmålet slik at man ikke trenger chat historikk for å finne dokumenter i vektordatabasen.
For mindre krevende oppgaver kan det være lurt å benytte raskere modeller som GPT-4o-mini eller Gemini-1.5-Flash. Disse er mer enn kapable til å håndtere mindre oppgaver samtidig som de er billigere og mye raskere.
7.2.2.1 Oppdatere tilstand
Det første vi må gjøre for å støtte omformulering er å legge til støtte for omformulering i tilstand. Vi gjør dette for å ha mulighet for å gi språkmodellen som genererer svar enten det originale spørsmålet eller det omformulerte.
- 1
- Legger til ny variabel med omformulert spørsmål.
- 2
- Legger til chat historikk som en variabel.
7.2.2.2 Node for omformulering
Vi må nå fylle den nye tilstanden med et omformulert spørsmål. Vi lager først en ny ledetekst som ber språkmodellen om å kontekstualisere et spørsmål:
Given a chat history and the latest user question which might reference context
in the chat history, formulate a standalone question which can be understood
without the chat history. Do NOT answer the question, just reformulate it if
needed and otherwise return it as is. The answer should be in Norwegian, or
English if the question is English.Vi kombinerer det så med chat historikk:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.prompts import MessagesPlaceholder
context_prompt = ChatPromptTemplate.from_messages(
[
("system", contextualize_text),
MessagesPlaceholder("chat_history"),
("human", "{question}"),
]
)Merk at question_prompt også blir endret til å inneholde MessagesPlaceholder for å gi chat historikken til språkmodellen:
question_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt),
MessagesPlaceholder("chat_history"),
("human", "{question}")
]
)Vi lager deretter en node til kjøretidsgrafen som kan omformulere spørsmålet.
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import AzureChatOpenAI
_llm = AzureChatOpenAI()
context_chain = context_prompt | _llm | StrOutputParser()
def context(state):
"""Noden som omformulerer spørsmål hvis det trengs mer kontekst"""
new_question = context_chain.invoke(
{
"question": state["question"],
"chat_history": state["chat_history"]
}
)
return {"rewritten_q": new_question}Når dette er gjort kan vi endre hvilken state variabel vi henter spørsmål fra i retrieve funksjonen vår.
def retrieve(state):
"""Node i grafen som henter dokumenter fra vektordatabasen
basert på spørsmål fra bruker."""
1 docs = retriever.invoke(state["rewritten_q"])
return {"context": docs}- 1
- Benytte omformulert spørsmål istedenfor originalt spørsmål.
7.2.2.3 Oppdater grafen
Før vi er helt ferdig må vi legge til den nye context noden i kjøretidsgrafen.
from langgraph.graph import END, START, StateGraph
# Vi benytter her tilstanden vi definerte i starten som grunnlag for grafen
workflow = StateGraph(GraphState)
# Legg til noder i grafen
1workflow.add_node("contextualize", context)
workflow.add_node(retrieve)
workflow.add_node("rag", generator)
# Definer hvordan nodene henger sammen
2workflow.add_edge(START, "contextualize")
workflow.add_edge("contextualize", "retrieve")
workflow.add_edge("retrieve", "rag")
workflow.add_edge("rag", END)
# Kompiler grafen
app = workflow.compile()- 1
- Legger til den nye noden i grafen.
- 2
- Legger den inn som første node, før vi henter dokumenter.
Når dette er gjort kan vi holde styr på chat historikk utenfor grafen og benytte dette som input til grafen.
7.3 Kjøretidsgrafen illustrert
En liten fordel med LangGraph er at den har mulighet til å illustrere grafen. For den enkle grafen vi har bygget her gir det kanskje ikke så mye verdi, men for mer kompliserte grafer kan det være et nyttig verktøy.
app.get_graph().draw_mermaid()7.4 Sitere kilder
Det siste vi skal diskutere i dette kapittelet er hvordan man kan guide modellen til å gi kilder.
For LangChain er den beste måten å generere siteringer å benytte strukturerte svar. Med denne teknikken passer LangChain på at språkmodellen gir ut forventet JSON struktur og vi kan til og med validere dette med Pydantic.
7.4.1 Eksempel
Vi kommer til å benytte en Pydantic modell, noe som gir god sikkerhet i hva som blir produsert samtidig som det gir stor frihet i oppbygningen av svar-modellen.
Merk at både docstring og description attributtene blir sendt til språkmodellen, så disse må inneholde gode beskrivelser av hva som forventes av språkmodellen.
from pydantic import BaseModel, Field
class CitedAnswer(BaseModel):
"""Answer to the user question with citation to source"""
answer: str = Field(description="The answer to the user's question")
citation: str = Field(description="Citation from the source")Vi benytter så denne svar-modellen sammen med språkmodellen.
from langchain_google_vertexai import ChatVertexAI
llm = ChatVertexAI(model_name="gemini-1.5-flash-002")
structured_llm = llm.with_structured_output(CitedAnswer)Når vi så benytter denne modellen vil alle svar være av typen CitedAnswer og vi kan aksessere attributtene i denne svar-modellen som et vanlig Pydantic objekt.
answer = structured_llm.invoke("What is the meaning of life?")
print(f"Svaret på livet: {answer.answer}")
print(f"Begrunnelse: {answer.citation}")