10  Verktøykall (tool-calling)

Sist endret

19.09.2025

Verktøykalling (kjent som tool-calling på engelsk) er en generell teknikk hvor vi kan lage verktøy som språkmodellen får tilgang til. Dette fungerer ved at vi gir modellen en beskrivelse av verktøyene den har tilgang til, navn og parametere, og språkmodellen kan dermed returnere at den ønsker å benytte ett eller flere verktøy. Det er viktig å påpeke at språkmodellen ikke kjører verktøyene, den kan bare “ønske” seg å benytte verktøy.

For å kunne bruke verktøykall må språkmodellen være spesifikt trent for dette. De fleste språkmodeller en vil ønske seg å bruke støtter dette, men det kan være verdt å merke seg med eldre eller små lokale språkmodeller.

Vi kommer her til å vise hvordan verktøykalling kan brukes med LangChain, men teknikken burde være overførbar til andre rammeverk og generelt uten bruk av rammeverk. Dette er fordi grensesnittet vi bruker mot språkmodellene for å gi tilgang til verktøy er lik uavhengig av rammeverk (alt er JSON).

10.1 Definere et enkelt verktøy

I LangChain finnes det enkel støtte for å definere et verktøy ved å bruke @tool dekoratoren på en “vanlig” Python funksjon.

from langchain_core.tools import tool

@tool
def add(a: int, b: int) -> str:
    """Add two numbers"""
    return str(a + b)

Det som er viktig å bemerke seg med funksjonen over er at vi definerer typene vi forventer at funksjonen skal motta, typen til resultatet av metoden, samt gir en beskrivelse av hva funksjonen gjør. Dette er påkrevd for at LangChain skal kunne opprette en beskrivelse som kan sendes til språkmodellen.

For å lære mer om typing i Python anbefales det å se på dokumentasjonen til Python samt benytte seg av Typing cheat sheet fra mypy.

Som med instrukser vi bruker til språkmodellene så kan en god beskrivelse være gull verdt. For nyere språkmodeller kan det derfor være lurt å beskrive eksempler på bruk og andre viktige instrukser for verktøyet slik at språkmodellen enklere kan tolke når og hvordan den skal bruke verktøyet.

LangChain støtter også å tolke Google formatert docstring. Dette kan gi enda mer kontekst til språkmodellen og gjøre det enklere å dokumentere funksjonen.

LangChain forventer at alle verktøy returnerer en streng som resultat slik at det kan brukes som tekstliginnhold i en ToolMessage. Dette kan være begrensende i en kjøretidsgraf hvor vi kan ønske å gi ut strukturerte data. Heldigvis har LangChain støtte for dette gjennom response_format="content_and_artifact".

Når vi har definert verktøyet vårt kan vi deretter dele det med språkmodellen som så kan velge å be oss kalle på metoden.

from langchain_openai import AzureChatOpenAI

llm = AzureChatOpenAI().bind_tools([add])
response = llm.invoke({"role": "user", "content": "Hva er 2+2?"})
assert not response.content, "Forventet ikke at språkmodellen gir et tekstlig svar"
assert response.tool_calls, "Forventet at språkmodellen returnerer ønske om verktøykall"
print(response)
AIMessage(content='', additional_kwargs={'tool_calls': [{'id':
'call_iXj4DiW1p7WLjTAQMRO0jxMs', 'function': {'arguments': '{"a":2,"b":2}',
'name': 'add'}, 'type': 'function'}], 'refusal': None})

De fleste språkmodeller støtter verktøykall ved at man kan legge ved en JSON beskrivelse av verktøyene. Det er nettopp det LangChain gjør automagisk for oss når vi bruker @tool.

Vi kan undersøke hva LangChain gjør ved å inspisere metadata lagt ved funksjoner dekorert med @tool.

>>> add.args
{'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
>>> add.args_schema.model_json_schema()
{'description': 'Add two numbers', 'properties': {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}, 'required': ['a', 'b'], 'title': 'add', 'type': 'object'}

10.2 Verktøy for å hente informasjon

I Seksjon 7.1.1 så vi hvordan vi kan hente informasjon fra en vektordatabase som en node i kjøretidsgrafen. Et alternativ til å bruke en node er å strukturere uthenting av informasjon som et verktøykall.

La oss først definere et verktøy for å søke i vektordatabasen vår.

from langchain_core.tools import tool
from langchain_google_community import BigQueryVectorSearch

store = BigQueryVectorSearch.from_documents()  # Se kapittel om vektordatabase
retriever = store.as_retriever(search_kwargs=dict(k=5))

@tool
1async def search_database(query: str) -> str:
    """Search for information related to Nav.

    Args:
        query: A complete sentence describing the information to retrieve from
        Nav's database.
    """
    docs = await retriever.ainvoke(query)
    return "Context:\n\n" + "\n\n".join([doc.page_content for doc in docs])
1
Legg merke til at vi kan bruke asynkrone Python funksjoner som verktøy.

Som nevnt tidligere kan vi returnere strukturerte data ved å endre litt på @tool dekoratøren vår.

Søkefunksjonen kunne da sett slik ut:

1@tool(response_format="content_and_artifact")
2async def search_database(query: str) -> tuple[str, list[str]]:
    """Search for information related to Nav.

    Args:
        query: A complete sentence describing the information to retrieve from
        Nav's database.
    """
    docs = await retriever.ainvoke(query)
    return f"Retrieved {len(docs)} documents", [doc.page_content for doc in docs]
1
Legg merke til response_format i @tool dekoratøren.
2
Vi kan deretter endre resultatet vi gir ut.

Vi begynner deretter å definere starten på kjøretidsgrafen vår.

from langchain_core.messages import AnyMessage
from langchain_openai import AzureChatOpenAI
from langgraph.graph import MessagesState

llm = AzureChatOpenAI().bind_tools([search_database])

async def respond_or_tool(state: MessagesState) -> dict[str, list[AnyMessage]]:
    """Node i kjøretidsgrafen som enten svarer eller kaller på verktøy"""
    response = await llm.ainvoke(state["messages"])
    return {"messages": [response]}

Tilslutt definerer vi kjøretidsgrafen og benytter en ToolNode fra LangGraph som kan automatisk kalle på verktøy for oss.

from langgraph.graph import END, START, StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

# Benytter en generell tilstand som inneholder en liste med meldinger til og fra
# språkmodell(ene)
workflow = StateGraph(MessagesState)

# Legg til noder i grafen
workflow.add_node(respond_or_tool)
1workflow.add_node("run_tools", ToolNode([search_database]))

# Definer hvordan nodene henger sammen
workflow.add_edge(START, "respond_or_tool")
workflow.add_conditional_edges(
    "respond_or_tool", tools_condition, {"tools": "run_tools", END: END}
2)
3workflow.add_edge("run_tools", "respond_or_tool")

# Kompiler grafen
app = workflow.compile()
1
Oppretter en ToolNode i kjøretidsgrafen med tilgang til verktøy.
2
tools_condition sjekker om det skal gjøres verktøykall og leder oss enten til run_tools eller avslutter.
3
Etter at vi har kalt på et verktøy returnerer vi til første noden for å generere et svar til bruker.

Flyten i kjøretidsgrafen vil da se slik ut:

---
config:
  flowchart:
    curve: linear
  layout: dagre
---
flowchart TD
    __start__(["<p>__start__</p>"]) --> initial("respond_or_tool")
    initial -. &nbsp;__end__&nbsp; .-> __end__(["<p>__end__</p>"])
    initial -. &nbsp;tools&nbsp; .-> run_tools("run_tools")
    run_tools --> initial
     __start__:::first
     __end__:::last
    classDef default fill:#f2f0ff,line-height:1.2
    classDef first fill-opacity:0
    classDef last fill:#bfb6fc

I diagrammet over kan vi merke oss at vi har skapt en løkke hvor resultatet av at vi kaller på verktøyet kommer tilbake til språkmodellen og den kan benytte resultatet av verktøyet for å svare på spørsmålet.

Merk at vi ikke er begrenset til bare disse 2 nodene i kjøretidsgrafen eller at resultatet fra verktøykall må gå tilbake til samme node som vi kom fra. Fordelen med verktøykall er at modellen selv kan bestemme å kalle på verktøy og samtidig kan vi benytte deterministiske verktøy som kan gi oss større trygghet i svaret fra språkmodellene.