LLM-Context und Tools

Jean-Claude Brantschen

Im letzten Artikel (https://javapro.io/de/llm-context) haben wir untersucht, wie LLMs mithilfe des Contexts ganze Chat-Verläufe verarbeiten können. Doch die Möglichkeiten des Contexts gehen weit darüber hinaus. In diesem Artikel schauen wir uns an, wie wir der LLM über den Context Funktionen bereitstellen können, um qualitativ hochwertigere Antworten zu erhalten. Dieses Konzept ist bekannt als LLM-Context und Tools.

Vorbemerkungen

  • Die Code-Beispiele basieren auf dem Wissen aus dem Artikel https://javapro.io/de/llm-context. Daher wäre es sinnvoll, zuerst diesen zu lesen.
  • Alle Beispiele stehen in GitHub-Repos zur Verfügung.

Beispiel Kurzreise nach Paris

Wir möchten eine Kurzreise nach Paris machen und fragen uns, was wir da alles mitnehmen sollten. Im AI-Zeitalter starten wir Ollama mit dem Llama3.2-Modell und machen folgende Anfrage: What kind of clothes do I need for a short trip to Paris?

Oder wir machen die gleiche Anfrage mit einem Skript (z.B. in Python).

PS: Detaillierte Erklärungen zum Code gibt es im Artikel https://javapro.io/de/ollama-rest-api

import requests

def chat(model_name: str, question: str) -> tuple[str, str]:
    url = "http://localhost:11434/api/chat"
    data = {
        "model": model_name,
        "messages": [
            {
                "role": "user",
                "content": question
            }
        ],
        "stream": False
    }

    response_chat = requests.post(url, json=data)
    content = _extract_chat_content(response_chat.json())
    return model_name, content

def _extract_chat_content(json_data: dict) -> str:
    return json_data["message"]['content']


if __name__ == '__main__':
    my_question = 'What kind of clothes do I need for a short trip to Paris? Please answer in max 5 sentences.'
    response = chat('llama3.2', my_question)
    print(response)

LLM Antwort

For a short trip to Paris, it’s best to pack versatile and layer-friendly clothing that can be easily mixed and matched. Consider bringing 3-4 tops or blouses, 2-3 pairs of pants or trousers, and 1-2 dresses or skirts. Don’t forget a warm coat or jacket for chilly mornings and evenings, as well as comfortable walking shoes or boots. A lightweight scarf or hat can also add a stylish touch to your outfit. Pack light essentials like socks, undergarments, and a few accessories like belts and jewellery.

Bewertung der Antwort

In beiden Fällen erhalten wir eine korrekte Antwort. Allerdings fallen die Antworten sehr generisch aus: Weder das aktuelle Datum noch konkrete Wetterinformationen wurden berücksichtigt. Der Grund dafür ist, dass standardmäßig eine LLM selbst keinen direkten Zugriff auf das Live-Internet hat. Das Wissen einer LLM ist auf die Daten beschränkt, mit denen die LLM trainiert wurde.

Geht das nicht besser? Doch – und der Schlüssel dazu lautet: LLM-Context und Tools.

LLM-Context und Tools – Grundidee

Die Grundidee ist ziemlich clever. Bei einer Anfrage an die LLM kann im Context eine Liste von Funktionsbeschreibungen mitgegeben werden. Die LLM kann dann selber entscheiden, ob sie eine oder mehrere dieser Funktionen bei der Erstellung der Antwort einbeziehen möchte. Falls ja, teilt sie uns in der Antwort mit, welche Resultate von welchen Funktionen (mit welchen Parameterwerten) sie haben möchte. Wir als Client führen diese Funktionen daraufhin lokal aus und senden die Ergebnisse in einer weiteren Anfrage zurück an die LLM. Diese kann die erhaltenen Informationen dann in die endgültige Antwort einbeziehen.

LLM-Context und Tools – Ablauf

Im Folgenden wird der Ablauf von LLM-Context und Tools anhand unseres Beispiels der Kurzreise nach Paris dargestellt. Als Client für die Anfragen an die LLM dient dabei ein Python-Skript:

Ein paar Erklärungen zu den einzelnen Schritten:

  • Der Client macht die Anfrage What kind of clothes do I need for a short trip to Paris an die LLM. Der Context der Anfrage enthält neben der Frage noch einen Bereich tools mit folgenden Informationen:
Name der Funktionget_forecast
Kurze Beschreibung der FunktionThe weather forecast for a city
Parameter – Namecity
Parameter – Typstring
Parameter – BeschreibungThe name of the city
Funktion Informationen
  • Die LLM analysiert die Anfrage und möchte Wetterdaten zu Paris haben. Sie teilt dem Client in der Antwort mit, dass sie den Output von der Funktion get_forecast für den Wert Paris haben möchte.
  • Der Client ruft lokal get_forecast(Paris) auf und erhält die aktuellen Wetterdaten (z. B. 30 °C).
  • Der Client erweitert den Context mit den Wetterdaten und der Antwort der LLM und macht dann eine neue Anfrage an die LLM.
  • Die LLM hat die Anfrage und die Wetterdaten und kann damit eine für den Client hochwertige Antwort erstellen.

Implementierung mit Python

Code | Tools

Startpunkt für die Implementierung sind die Definitionen und die Implementierungen der Funktionen, die der LLM zur Verfügung gestellt werden sollen. Der Code dafür ist in tools.py.

# definition of the supported tools
forecast_function = {
    'type': 'function',
    'function': {
        'name': 'get_forecast',
        'description': 'The weather forecast for a city',
        'parameters': {
            'type': 'object',
            'required': ['city'],
            'properties': {
                'city': {'type': 'string', 'description': 'The name of the city'},
            },
        },
    },
}

# implementation of the supported tools
def get_forecast(city: str) -> str:
    if city == 'Paris':
        forecasts = [
            'temperature: 30 celsius',
            'wind: 5 km/h',
            'precipitation: 0%',
        ]
        return "\n---\n".join(forecasts)
    elif city == 'London':
        forecasts = [
            'temperature: 20 celsius',
            'wind: 20 km/h',
            'precipitation: 80%',
        ]
        return "\n---\n".join(forecasts)
    elif city == 'Berlin':
        forecasts = [
            'temperature: 15 celsius',
            'wind: 10 km/h',
            'precipitation: 0%',
        ]
        return "\n---\n".join(forecasts)
    else:
        return "\n---\n".join([])

forecast_function ist die Definition der Funktion, die aus folgenden Teilen besteht:

  • der Name der Funktion: get_forecast
  • eine Beschreibung, was die Funktion macht: The weather forecast for a city
  • die Parameter: Für jeden Parameter muss der Name, Typ und eine Beschreibung angegeben werden: city vom Typ string mit The name of the city

Der Name der Funktion und die Beschreibungen sind sehr wichtig. Aufgrund dieser Informationen entscheidet die LLM, ob sie eine oder mehrere dieser Funktionen in die Antwort einbeziehen möchte.

get_forecast(city) ist die Implementierung der forecast_function. In diesem Fall ist es nur eine Dummy-Implementierung mit hartkodierten Werten. Für eine produktive Implementierung würde man sich die Wetterdaten von einem Third-Party-Service holen.

Code | Context

Nächster Baustein ist context.py. Es ist ein Wrapper für die JSON-Daten, die bei jeder Anfrage dem Modell mitgeschickt werden müssen. Mehr Informationen dazu gibt es im Artikel https://javapro.io/de/llm-context

Neu hat der Context in dieser Implementierung neben den messages auch noch tools. Hier wird die in tools.py definierte Funktion verwendet. Die restlichen Methoden dienen dazu, den Context schrittweise mit zusätzlichen Daten (messages) zu füllen.

from via_rest.weather.with_tools.tools import forecast_function

class Context:

    def __init__(self, model_name: str):
        self.model_name = model_name
        self.data = {
            "model": model_name,
            "messages": [
            ],
            "tools": [forecast_function],
            "stream": False
        }

    def add_message(self, message):
        self.data["messages"].append(message)

    def add_request_message(self, question: str):
        message = {
            "role": "user",
            "content": question
        }
        self.add_message(message)

    def add_tool_message_with_tool_results(self, function_name: str, tool_result: str):
        message = {
            'role': 'tool',
            'content': tool_result,
            'tool_name': function_name}
        self.add_message(message)

Code | Main

In weather_with_tools.py ist die Hauptlogik implementiert. Zentral ist dabei die chat_with_tools() Funktion. Dort werden Anfragen an die LLM gesendet, Antworten ausgewertet und (falls von der LLM gewünscht) die in tools.py definierten Funktionen ausgeführt und (ganz wichtig) der Context gepflegt.

import requests
from via_rest.weather.with_tools.context import Context
from via_rest.weather.with_tools.tools import get_forecast

def _send_request(data: dict) -> dict:
    url = "http://localhost:11434/api/chat"
    return requests.post(url, json=data).json()


available_functions = {
    'get_forecast': get_forecast,
}


def chat_with_tools(model_name: str, question: str) -> str:
    context = Context(model_name)
    context.add_request_message(question)
    first_response = _send_request(context.data)

    # check, if we should call a tool for the model
    if tool_call_requests := first_response['message']['tool_calls']:
        for tool_call_request in tool_call_requests:
            function_name = tool_call_request['function']['name']
            function_arguments = tool_call_request['function']['arguments']

            # check if the function is available
            if function_name not in available_functions:
                continue
            else:
                # call the function locally
                function_to_call = available_functions.get(function_name)
                output = function_to_call(**function_arguments)

                # add response message of model
                context.add_message(first_response['message'])

                # add function result
                context.add_tool_message_with_tool_results(function_name, str(output))

    if tool_call_requests:
        final_response = _send_request(context.data)
        return final_response['message']['content']
    else:
        return ''


if __name__ == '__main__':
    my_question = 'What kind of clothes do I need for a short trip to Paris? Please answer in max 5 sentences.'
    response = chat_with_tools('llama3.2', my_question)
    print('Final LLM response:', response)

Hier die relevanten Schritte:

  • Zu Beginn enthält der Context nur den Namen der gewünschten LLM in Ollama und eine Liste mit den Definitionen der Funktionen, die der LLM zur Verfügung gestellt werden sollen (hier forecast_function). In einem ersten Schritt wird die aktuelle Frage (What kind of clothes do I need for a short trip to Paris) dem Context hinzugefügt und eine erste Anfrage an die LLM gemacht.
  • Die LLM entscheidet nun, ob bzw. an welchen Funktionen (mit welchen Werten für die Parameter) sie interessiert ist und schickt eine Antwort.
  • Aus der Antwort werden für die gewünschten Funktionsaufrufe jeweils der Funktionsname und die Argumente extrahiert. Falls der Funktionsname bekannt ist (d.h. in available_functions ist), wird die Funktion mit den Parametern aufgerufen und das Resultat in der Variablen output gespeichert.
  • Anschließend wird der Context gepflegt, indem die Antwort von der LLM und das Resultat des Funktionsaufrufs hinzugefügt werden.
  • Das Handling der gewünschten Funktionsaufrufe wird in einer Schleife gemacht, denn die LLM könnte ja an mehreren Funktionen interessiert sein. Wenn alle Funktionsaufrufe abgearbeitet sind, wird nochmals eine Anfrage an die LLM geschickt. Diesmal sind im Context der ganze Chat-Verlauf und die Resultate der Funktionsaufrufe dabei.
  • Die LLM erstellt jetzt die finale Antwort auf Basis aller vorhandenen Daten.

Und so sieht die Antwort aus mithilfe von LLM-Context und Tools:

Based on the forecast, Paris is expected to be warm and sunny during your trip. For a short trip, consider packing lightweight and versatile clothing such as t-shirts, tank tops, shorts, and dresses that can keep you cool in the daytime. You may also want to bring a light sweater or cardigan for cooler evenings. Comfortable walking shoes are a must for exploring the city. Don’t forget to pack a hat and sunglasses for sun protection!

Ablauf Im Detail

Um den Ablauf noch etwas anschaulicher zu machen, sind hier die Anfragen an die LLM, die Antworten und die Funktionsaufrufe für das ganze Beispiel aufgelistet. In jedem Schritt sieht man genau, wie der Context aussieht. Zudem wächst der Context bei jeder neuen Anfrage.

Erste Anfrage

  • Context mit der initialen Frage an die LLM
  • und der Definition von get_forecast und deren Parametern
[REQUEST] make initial request with tool definitions
  messages
    {
      "role": "user",
      "content": "What kind of clothes do I need for a short trip to Paris? Please answer in max 5 sentences."
    }
  tools
    {
      "type": "function",
      "function": {
        "name": "get_forecast",
        "description": "The weather forecast for a city",
        "parameters": {
          "type": "object",
          "required": [
            "city"
          ],
          "properties": {
            "city": {
              "type": "string",
              "description": "The name of the city"
            }
          }
        }
      }
    }

Erste Antwort

  • Die Antwort der LLM enthält die Anweisung, get_forecast mit dem Parameter Paris aufzurufen.
[RESPONSE] from LLM
  message:
    {
      "role": "assistant",
      "content": "",
      "tool_calls": [
        {
          "function": {
            "name": "get_forecast",
            "arguments": {
              "city": "Paris"
            }
          }
        }
      ]
    }

Tools Handling

  • extrahieren von Funktionsnamen und Parameterwerten
  • aufrufen der Funktion get_forecast mit dem Wert Paris
[TOOL] Extracted tool call request
  function_name: get_forecast
  arguments: {'city': 'Paris'}

[TOOL]
  calling function get_forecast with arguments {'city': 'Paris'}

[TOOL]
  function output:
    temperature: 30 celsius
    ---
    wind: 5 km/h
    ---
    precipitation: 0%

Zweite Anfrage

  • Context mit der eigentlichen Frage an die LLM
  • der ersten Antwort der LLM
  • und dem Resultat des Aufrufs von get_forecast(Paris)
[REQUEST] make request with tool results
  messages
    {
      "role": "user",
      "content": "What kind of clothes do I need for a short trip to Paris? Please answer in max 5 sentences."
    },
    {
      "role": "assistant",
      "content": "",
      "tool_calls": [
        {
          "id": "call_rng1wyam",
          "function": {
            "index": 0,
            "name": "get_forecast",
            "arguments": {
              "city": "Paris"
            }
          }
        }
      ]
    },
    {
      "role": "tool",
      "content": "temperature: 30 celsius\n---\nwind: 5 km/h\n---\nprecipitation: 0%",
      "tool_name": "get_forecast"
    }

  tools
    {
      "type": "function",
      "function": {
        "name": "get_forecast",
        "description": "The weather forecast for a city",
        "parameters": {
          "type": "object",
          "required": [
            "city"
          ],
          "properties": {
            "city": {
              "type": "string",
              "description": "The name of the city"
            }
          }
        }
      }
    }

Finale Antwort

[RESPONSE]
  message:
    {
      "role": "assistant",
      "content": "Based on the forecast, Paris is expected to be warm and sunny during your trip. For a short trip, consider packing lightweight and versatile clothing such as t-shirts, tank tops, shorts, and dresses that can keep you cool in the daytime. You may also want to bring a light sweater or cardigan for cooler evenings. Comfortable walking shoes are a must for exploring the city. Don't forget to pack a hat and sunglasses for sun protection!"
    }

Den kompletten Source gibt es unter https://github.com/clean-coder/rest_tools_py.

Implementierung mit Java

Wie sieht es mit einer Implementierung in Java aus? Hierzu zwei mögliche Wege:

  • Als Basis kann der Code aus dem Artikel https://javapro.io/de/llm-context genommen werden. Zusätzlich müssen noch das Tool Handling und die erweiterte Pflege des Contexts implementiert werden.
  • Der AI-Weg: den Python-Code von einer AI nach Java konvertieren lassen.

Da dies ein Artikel über AI ist, habe ich mich für den AI-Weg entschieden. Ich habe Claude gebeten, den Python-Code in Java zu konvertieren. Zusätzlich habe ich noch:

  • spezifiziert, was für Libraries benutzt werden sollen
  • und den Python-Source-Code angehängt

Erstaunlich: Im ersten Wurf hat Claude den Java-Code inkl. einem Maven-POM erstellt. Der Code ist ohne Anpassungen lauffähig und verhält sich analog zur Python-Version. Und es wurden genau die beiden von mir gewünschten Libraries verwendet. Umso bemerkenswerter, da die LLM zwar den Code generiert hat, ihn selbst aber nicht ausführen kann. Da kann ich nur sagen: Danke Claude !!!

Zur Anschauung hier der Java-Code für die Hauptlogik. Das erinnert schon stark an weather_with_tools.py. Den kompletten Source gibt es unter https://github.com/clean-coder/rest_tools_java.

package viaRest.weather;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import static viaRest.weather.Tools.getForecast;

public class WeatherWithTools {

    private static final Gson gson = new Gson();
    private static final HttpClient httpClient = HttpClient.newHttpClient();
    private static final String URL = "http://localhost:11434/api/chat";

    private static JsonObject sendRequest(JsonObject data) throws Exception {
        String jsonData = gson.toJson(data);

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(URL))
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonData))
                .build();

        HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        return gson.fromJson(response.body(), JsonObject.class);
    }

    private static final Map<String, Function<Map<String, String>, String>> availableFunctions = new HashMap<>();

    static {
        availableFunctions.put("get_forecast", args -> getForecast(args.get("city")));
    }

    public static String chatWithTools(String modelName, String question) throws Exception {
        Context context = new Context(modelName);
        context.addRequestMessage(question);
        JsonObject firstResponse = sendRequest(context.getData());

        // Check if we should call a tool for the model
        JsonElement toolCallsElement = firstResponse.getAsJsonObject("message").get("tool_calls");
        JsonArray toolCallRequests = null;

        if (toolCallsElement != null && !toolCallsElement.isJsonNull()) {
            toolCallRequests = toolCallsElement.getAsJsonArray();
        }

        if (toolCallRequests != null && toolCallRequests.size() > 0) {
            for (JsonElement toolCallElement : toolCallRequests) {
                JsonObject toolCallRequest = toolCallElement.getAsJsonObject();

                JsonObject functionObj = toolCallRequest.getAsJsonObject("function");
                String functionName = functionObj.get("name").getAsString();
                JsonObject functionArguments = functionObj.getAsJsonObject("arguments");

                Map<String, Object> argsMap = new HashMap<>();
                argsMap.put("function_name", functionName);
                argsMap.put("arguments", functionArguments);

                // Check if the function is available
                if (!availableFunctions.containsKey(functionName)) {
                    continue;
                } else {
                    // Call the function locally
                    Function<Map<String, String>, String> functionToCall = availableFunctions.get(functionName);

                    // Convert JsonObject arguments to Map<String, String>
                    Map<String, String> argMap = new HashMap<>();
                    for (String key : functionArguments.keySet()) {
                        argMap.put(key, functionArguments.get(key).getAsString());
                    }

                    String output = functionToCall.apply(argMap);

                    // Add response message of model
                    context.addMessage(firstResponse.getAsJsonObject("message"));

                    // Add function result
                    context.addToolMessageWithToolResults(functionName, output);
                }
            }
        }

        if (toolCallRequests != null && toolCallRequests.size() > 0) {
            JsonObject finalResponse = sendRequest(context.getData());
            return finalResponse.getAsJsonObject("message").get("content").getAsString();
        } else {
            return "";
        }
    }

    public static void main(String[] args) {
        try {
            String myQuestion = "What kind of clothes do I need for a short trip to Paris?";
            String response = chatWithTools("llama3.2", myQuestion);
            System.out.println("Final LLM response: " + response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Fazit

Wir haben eine neue Facette des LLM-Contexts kennengelernt: Mithilfe von Tools können wir der LLM Funktionen anbieten, die für eine Antwort hilfreich sind und deren Qualität deutlich verbessern können. Die LLM entscheidet dabei selbstständig, ob und welche Funktionen sie in die Antwort einbeziehen möchte. Ein entscheidender Punkt: Die Funktionen werden auf dem Client und nicht direkt auf der LLM ausgeführt. Die Anwendungsmöglichkeiten sind vielfältig – von Echtzeit-Abfragen (wie in unserem Beispiel) über Datenbankzugriffe bis hin zu geschäftskritischen Berechnungen, deren Logik man nicht mit einer LLM teilen möchte.

Oder in einem Satz: Context Is King

Artikel

Repos

Context is King: https://www.linkedin.com/in/aleksanderstensby

Total
0
Shares
Previous Post

Wenn Entwickler auf Designer treffen: Erkenntnisse aus Vibe Coding und der Kunst des kreativen Ausdrucks

Next Post

Java 26 übernimmt HTTP/3 mit der Weiterentwicklung des HttpClient

Related Posts

Die Zukunft von Containern – Was kommt als Nächstes?

Vielleicht haben Sie schon die Schlagworte gehört, die in aller Munde sind, wenn es um die Zukunft von Containern geht. Seltsame Namen wie "Micro-VMs"… "Unikernel"… "Sandboxes"… Haben Sie sich gefragt, was diese Dinge sind und wie Sie sie nutzen können? Oder sollten Sie diese überhaupt verwenden?
Read More