I kjølvannet av all utviklingen innen store språkmodeller og KI den siste tiden har det har vært mange diskusjoner om hvor nært vi er generell kunstig intelligens.
I julehøytiden tenker jeg det er vel så greit å få en ytterst spesifikk julenisse-intelligens istedenfor, så la oss heller se hvordan du kan lage din helt egen KI-julenisse med LangGraph og språkmodeller!
Dersom du har det for travelt med å fortelle Julenissen at du har vært snill i år til å lære om LangGraph, kan du prate med ham på julenissen.streamlit.app.
Hva er LangGraph?
LangGraph er et rammeverk som gjør det lettere å utvikle KI-agenter med Python eller JavaScript. Med LangGraph kan du lage komplekse arbeidsflyter organisert som en graf (herav LangGraph), hvor flere såkalte aktører samarbeider, og innhold og beslutninger justeres over flere steg.
LangGraph er utviklet av LangChain, og skiller seg fra LangChain-biblioteket ved å ikke bare støtte linære «chains», men også sykliske strukturer, noe gjør det mulig med mer avansert agentisk oppførsel.
Over 23.000 utviklere spurt: Dette er AI-ene de liker best
LangGraph består av:
- State-graf: LangGraph benytter en «stateful» modell der tilstand flyttes og oppdateres gjennom grafens noder.
- Noder: Hver node i grafen representerer en spesifikk funksjon eller oppgave, som kan være alt fra LLM-kall til informasjonsinnhenting og interaksjon med eksterne API-er gjennom såkalte verktøy-kall.
- Kanter: Kantene i grafen forbinder nodene og styrer flyten av data og beslutninger. Med LangGraph kan du legge til betingede kanter som dynamisk velger neste steg basert på grafens state.
Denne arkitekturen er alt vi trenger for å bygge smarte agenter som kan tilpasse seg situasjoner – perfekt for vår KI-julenisse!
KI-Julenissen
La oss utforske LangGraph i skikkelig høytidsstil!
For å lage Julenissen med LangGraph må vi lage en agent-graf som inneholder:
- En node for å representere nissen
- Verktøy for å sjekke og oppdatere slemmelisten
- En verktøy-node slik at nissen kan bruke verktøyene
En LangGraph-node er en helt vanlig funksjon, som tar i mot grafens state som parameter. Denne funksjonen returnerer en state-oppdatering, før kontrollen sendes videre til neste node i grafen. Hver node gjennomfører som regel egne kall til en språkmodell eller et verktøy.
For Julenisse-noden vår er det også viktig at vi legger til et prompt slik at nissen vet at han er julenissen (stakkars, har blitt gammel nå, nissen), og hva han skal gjøre. Dette legger vi til som den første meldingen når vi kaller LLM-modellen, etterfulgt av listen over meldingene i graf-staten. Julenisse-noden kan ganske enkelt lages med Python-koden under:
llm = ChatOpenAI(model="gpt-4o")
system_prompt = "Du er julenissen, og spør alle du snakker med om hva de ønsker seg til jul. Du kan også sjekke slemmelisten ved å spørre om navnet på en person, og bruke verktøyet for dette."
def santa(state: State, config: RunnableConfig):
response = llm.invoke(
[("system", system_prompt), *state["messages"]],config)
return { "messages": [response]
Dette er egentlig alt du trenger for å kunne chatte med Julenissen, men den virkelige kraften i LangGraph kommer når vi legger til støtte for å hente informasjon fra, og lagre til, systemer utenfor språkmodellen.
Verktøy-kall
Dette kan vi gjøre ved hjelp av verktøy vi kobler til språkmodellen, hvor modellen da kan velge å generere verktøy-kall istedenfor en melding.
Jeg sier "kall" fordi modellene ikke direkte bruker verktøyene, men heller genererer opp argumenter og navngir verktøyet de ønsker å kalle basert på meldingen fra brukeren og tilgjengelige verktøy.
Deretter brukes en egen verktøy-node for å gjennomføre selve verktøykallene. Dette kan du gjøre selv, eller benytte LangGraph sin ferdigbyggede verktøynode, slik vi gjør her.
Det er veldig lett å definere slike verktøy med LangGraph. Gitt at vi har en database med liste over hvor snill og slem ulike personer har vært, kan vi for eksempel lage et verktøy for å gi nissen tilgang til denne databasen, lage en verktøynode, og registrere de tilgjengelige verktøyene hos modellen slik:
@Tool
def check_naughty_list(name: str):
"""Call with a name, to check if the name is on the naughty list."""
score = get_nice_by_name(name)
if score >= 0:
return f"{name} er på listen over snille barn."
else:
return f"{name} er på slemmelisten!"
tools = [check_naughty_list]
tool_node = ToolNode(tools)
llm = ChatOpenAI(model="gpt-4o").bind_tools(tools)
Når vi vil at en språkmodell skal returnere data i et bestemt format, slik som et objekt med definerte felt, kan vi bruke mulighetene for strukturert respons flere modeller støtter.
Dette kan vi gjøre ved å definere et skjema, som i koden under, hvor vi spesifiserer hvordan responsen fra modellen skal se ut:
llm = ChatOpenAI(model="gpt-4o").with_structured_output({
"title": "Score ",
"description": "The score of the users action",
"type": "object",
"properties": {
"nice_score": {
"title": "Nice score",
"description": "The score of the action",
"type": "number"
}
}
})
For å legge til et verktøy som oppdaterer slemme-listen, kan vi da lage en funksjon som kjører denne strukturerte språkmodellen, utenfor grafen vår, for å vurdere hvor snill eller slem en handling er, og lagrer resultatet.
I verktøyfunksjonen under bruker vi en teknikk som kalles «few-shot» prompting, som betyr at vi lager eksempler med brukermeldinger og svar fra språkmodellen. Dette øker nøyaktigheten og forståelsen modellen har for oppgaven, spesielt i tilfeller hvor vi har konkrete ønsker til modellens oppførsel:
@Tool
def register_naughty_or_nice(name: str, action: str):
"""Call with a name and action,
to update the naughty or nice score for the name."""
examples = [
HumanMessage("Jeg har spist opp grønnsakene mine", name="example_user"),
AIMessage("{ 'nice_score': 5 }", name="example_system"),
HumanMessage("Jeg har kranglet med broren min.", name="example_user"),
AIMessage("{ 'nice_score': -5 }", name="example_system")
]
system_prompt = f"""
Du er julenissen, og du skal oppdatere listen over snille barn.
Ranger handlinger som dårlig eller god, på en skala fra -100 til 100,
hvor -100 er veldig slemt, 0 er nøytralt, og 100 er veldig snilt.
Bare svar med et tall."""
prompt = ChatPromptTemplate.from_messages([
("system", system_prompt),
("placeholder", "{examples}"),
("human", "{input}")])
llm_chain = prompt | llm
result = llm_chain.invoke({ "examples": examples,
"input": f"{name}: {action}" })
nice_score = float(result["nice_score"])
update_list(name, nice_score)
return "Listen er oppdatert
Koble sammen grafen
Med det er vi klar til å koble sammen Julenisse-grafen vår! Dette gjør vi ved å opprette en StateGraph og legge noder og kanter til i denne. Vi har to noder, «santa» og «tool_node», som vi må legge til.
Når dette er gjort kan vi legge til kanter mellom nodene for å definere kontrollflyten i grafen vår. LangGraph har to spesielle noder, kalt START og END, som vi bruker for å definere starten og slutten på grafen:
Vi lager en kant fra START til «santa»-noden, som så får en betinget kant til en funksjon kalt «tools_condition». Dette er en hjelpe-funksjon fra LangGraph som sjekker om det finnes verktøykall i den siste meldingen. Dersom den finner dette sendes kontrollen videre til «tools»-noden, eller alternativt sendes vi til END.
For at Julenissen skal få bruke resultatet fra å kalle verktøy i samtalen, legger vi til en kant fra «tools»-noden tilbake til «santa»-noden. Slike betingede kanter er en viktig del av LangGraph, fordi de lar oss introdusere valgmuligheter til agentene vi lager.
På denne måten kan de velge selv hva de skal gjøre, innenfor rammene vi har bygget for dem:
# Definer state
class State(TypedDict):
messages: Annotated[list, add_messages]
graph_builder = StateGraph(State)
# Legg til noder
graph_builder.add_node("santa", santa)
graph_builder.add_node("tools", tool_node)
# Legg til kanter
graph_builder.add_edge(START, "santa")
graph_builder.add_conditional_edges("santa", tools_condition)
graph_builder.add_edge("tools", "santa")
graph = graph_builder.compile()
Julenisse med minne
For å få en julenisse uten alvorlige problemer med kortidsminnet, må vi legge til et samtaleminne, slik at vi han kan huske meldinger fra tidligere i samtalen. I mer teknisk terminologi:
Vi må lagre meldingene i state-grafen slik at vi kan kjøre gjennom grafen flere ganger, og huske hva som har blitt sagt i tidligere runder.
LangGraph har innebygget støtte for dette ved hjelp av noe som kalles en checkpointer. Den gjør nøyaktig det det høres ut som at den gjør, lager checkpoints etter hver node. LangGraph har støtte for en rekke ulike checkpoint-moduler, som Postgres i eksempelet under.
Å bruke en checkpointer er så enkelt som å kompilere grafen med checkpointeren som et argument, og legge ved en «thread_id» i konfigurasjonsargumentet når du kjører grafen:
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpointer.setup()
graph = graph_builder.compile(checkpointer=checkpointer)
thread_id = "1"
config = { "configurable": { "thread_id": thread_id } }
response = graph.invoke(
{ "messages": [(«user", "Hei, jeg heter Ola, og jeg ønsker meg en ny sykkel til jul!")] }, config)
for message in response.get("messages"):
print(message.pretty_print())
Nå vil Julenissen huske det du har sagt til ham tidligere. Dersom du kjører koden flere ganger, og endrer brukermeldingen vil Julenissen svare deg i kontekst av samtalen. Dersom du endrer «thread_id», vil du få en ny samtale.
I realiteten lager du gjerne en ny rad i en samtale-tabell, og benytter id-en på denne som «thread_id», slik at alle samtaler får egne tråder.
Drukner i bug-rapporter skrevet av AI: «Vi blir utbrent!»
God jul!
Har du kommet helt ned hit har du nå en fullstendig implementasjon av julenissen med KI! Gratulerer, dette bør gi deg mange poeng på årets viktigste liste. Jeg håper du har lært noe om LangGraph, og samtidig fått litt julestemning.
For å gjøre det hele mye morsommere har jeg laget en nettside hvor du kan teste ut koden over, med noen ekstra regler for hvordan nissen skal oppføre seg:
- Nissen er offer for effektiviseringstiltak, og har derfor besluttet å bare skrive fornavn på listen. Dette betyr at alle barn med samme fornavn blir bedømt samlet.
- Det tar for lang tid for nissen selv å finne ut om barn er snill er slem, så nissen krever derfor at du sier minst en snill eller slem handling du har gjort i år før du får vite om du får det du vil ha til jul. Du kan også sladre på andre til nissen (men kanskje det putter deg på slemmelisten også?).
- Nissen vurderer å bytte karriere til standup-komiker, og øver seg med humoristiske svar og kommentarer i samtalen med deg.
Så om du ikke trodde på ham før, vet du nå at julenissen eksisterer, i beste KI-velgående! Slå av en prat da vel, på julenissen.streamlit.app.
God jul!
PS: Hele koden finner du på github.com/oysmal/langgraph-julenissen