In Koog, how do @Tool and @LLMDescription turn a Kotlin function into an LLM tool?
answer
- ToolSet plus reflection
- annotations produce the descriptor
- parameter types become JSON schema
- defaults and nullables become optional
- description is what the model reads
basics
~20 sPut the functions in a class implementing ToolSet, mark each with @Tool, and describe the function and every parameter with @LLMDescription. asTools() reflects over the class and builds a tool descriptor per function, mapping Kotlin parameter types to the JSON schema the model is given.
solid answer
~50 sThe annotated route is the low-ceremony one. A class implements the ToolSet marker interface; each exposed method carries @Tool, and @LLMDescription supplies the human-readable text for the class, the method and each parameter. Calling asTools() on an instance produces the list of tools you hand to tools(...) inside the ToolRegistry builder. Reflection does the conversion: the method name becomes the tool name, the method's @LLMDescription becomes the tool description, and each parameter becomes a schema entry whose type comes from the Kotlin type and whose text comes from its @LLMDescription. Parameters with defaults or nullable types become optional rather than required. The important consequence is that these annotations are the API contract: the model never sees your KDoc, naming conventions or business rules, so a parameter without a description is a parameter the model has to guess at.
code
kotlin · 14 linesclass WeatherTools : ToolSet {
@Tool
@LLMDescription("Returns today's forecast for a city. Use only for weather questions.")
suspend fun forecast(
@LLMDescription("City name in English, for example Berlin") city: String,
@LLMDescription("Temperature unit to report in") unit: TempUnit = TempUnit.CELSIUS
): String = lookup(city, unit)
private suspend fun lookup(city: String, unit: TempUnit): String = "18"
}
enum class TempUnit { CELSIUS, FAHRENHEIT }
val registry = ToolRegistry { tools(WeatherTools().asTools()) }go deeper
Know the three moving parts: implement ToolSet, annotate methods with @Tool, describe the method and its parameters with @LLMDescription, then register the result of asTools().
Explain the reflection step and the type mapping: which Kotlin types become which schema shapes, and why defaults and nullables produce optional parameters.
Show that you treat descriptions as a contract under change control, and use enums and defaults to remove classes of invented-argument failures rather than describing them away.
Own the policy question: which tool surface is annotated and reviewed like an API, how description changes are regression-tested, and where the annotated route stops paying and hand-built tools start.
## The annotated path Koog offers two ways to define a tool. This is the quick one: write ordinary Kotlin functions, annotate them, and let reflection build the machine-readable declaration. Three pieces are involved. ToolSet is a marker interface your class implements; it is what makes the class eligible for conversion. @Tool marks each method that should be exposed, so unmarked methods stay private to your code. @LLMDescription carries natural language, and it is applied in three places: on the class, on each tool method, and on each parameter. The conversion happens when you call asTools() on an instance. That returns a list of tool objects, each carrying a descriptor: a name, a description, and parameter descriptors with types. You register the list with tools(...) inside the ToolRegistry builder, and from then on it is indistinguishable from a hand-written tool. ## From Kotlin types to a schema What the model receives is a JSON schema, so every parameter type must be expressible in one. Strings, numbers and booleans map directly; enums become a constrained set of allowed values, which is far more reliable than a free-text parameter plus a hopeful description; lists become arrays; a serializable data class becomes a nested object. Exotic JVM types with no JSON equivalent are where this route runs out of road, and that is one of the reasons to drop down to an explicit tool implementation instead. Optionality follows Kotlin: a parameter with a default value or a nullable type is emitted as optional, everything else as required. This matters because required parameters are where models get forced into inventing values. If a parameter genuinely has a sensible fallback, giving it a Kotlin default both simplifies your code and removes an invented-argument failure mode. Suspending functions are supported, which is the normal case for a tool that performs I/O. ## Descriptions are the contract, not documentation The most common review comment on Koog tool code is that the descriptions are too thin. The model chooses between tools using nothing but names and descriptions, and fills arguments using nothing but parameter descriptions and types. Anything unstated is guessed. Good practice is to say what the tool does, when it should be used and when it should not, what units or formats a parameter takes, and what the result looks like. Anything you would tell a new colleague in a comment belongs in the annotation instead. The corollary is that changing a description is a behavioural change, not a docs change. Reword a description and tool selection can shift on inputs that used to work, which is why these strings deserve review and, ideally, a regression test over a handful of representative prompts. ## Why this route and not the other Annotations win when the tool set is known at compile time, the parameters are ordinary types and a string answer is fine. They lose when you need to build tools dynamically, hand-tune a schema beyond what reflection infers, or return a typed structured result to the surrounding Kotlin code rather than text.
- You have a parameter that should only take one of four values. What is the better way to express that?Make it a Kotlin enum rather than a String with the values listed in the description. The enum is emitted as a constrained set of allowed values in the schema, so the provider itself restricts the output, whereas a description is only advice the model may ignore. You also get an exhaustive when in the implementation instead of parsing free text.
- What actually happens if you leave @LLMDescription off a parameter?The parameter still appears in the schema with its name and type, so calls remain structurally valid, but the model has only the identifier to go on. Ambiguous parameters like a date, an id or a units field then get filled with plausible guesses, and the failure shows up as wrong answers rather than errors. Treat a missing description as a defect.
- Do methods without @Tool in a ToolSet class get exposed?No. asTools() converts only the annotated methods, so helper functions in the same class stay internal to your implementation. That is deliberate: it lets one class hold both the exposed surface and the private machinery behind it without splitting files.
saying these in an interview costs you the question
- Assuming Kotlin parameter names alone tell the model enough
- Thinking KDoc is used as the tool description
- Believing every JVM type can become a tool parameter
- Treating a description edit as harmless documentation
- Expecting non-annotated methods to be exposed