Koog(4)—— Agent Skills 和 Tools

前言

三月份的时候,写了一篇文章: 拆穿名词诈骗!用大白话理解晦涩难懂的AI概念, 里面有介绍 Agent Skills 和 Tools。当时有详细解释了两个概念,Tools 相当于大模型的"手脚",让 LLM 不仅能回答问题,还能调用程序,执行动作。 Skills 是将复杂的、固定的流程打包成一个个独立的"技能包",是 Agent 走向工程化、模块化的关键一步。对于这写概念还不清楚的小伙伴,可以回头去看那篇文章。

本文将讲解如何在 Koog Agent 中使用 Skills 和 Tools。

一、 Skills

Koog skills 允许 agent 从文件系统中发现可复用的能力包,并通过生成的 prompt 段落将它们暴露给模型。

使用起来包含以下三个步骤::

  1. 从一个或多个根目录发现 skills。
  2. 根据发现的元数据生成 skills prompt 块。
  3. 将生成的块添加到 agent 的 system prompt 中,并提供 agent 可用于检查文件和执行 skill 脚本的工具。

1.1 示例:将 skills 添加到 system prompt

复制代码
import ai.koog.agents.core.agent.AIAgent
import ai.koog.agents.core.tools.ToolRegistry
import ai.koog.agents.ext.tool.file.ListDirectoryTool
import ai.koog.agents.ext.tool.file.ReadFileTool
import ai.koog.prompt.executor.clients.openai.OpenAIModels
import ai.koog.prompt.executor.llms.all.simpleOpenAIExecutor
import ai.koog.rag.base.files.JVMFileSystemProvider
import ai.koog.skills.discovery.discoverSkills
import ai.koog.skills.prompt.SkillsPromptFormat
import ai.koog.skills.prompt.generateSkillsPrompt
import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    val skillsRoot = "/absolute/path/to/skills"
    val discoveredSkills = discoverSkills(JVMFileSystemProvider.ReadOnly, listOf(skillsRoot))
    val generatedSkillsPrompt = generateSkillsPrompt(discoveredSkills, SkillsPromptFormat.XML)

    // Replace with your script execution tool implementation.
    val apiKey = System.getenv("OPENAI_API_KEY")
        ?: error("The API key is not set.")

    val agent = AIAgent(
        promptExecutor = simpleOpenAIExecutor(System.getenv("YOUR_API_KEY")),
        systemPrompt = """
                You are a careful assistant.
                Use the available skills listed below.
                Before using a skill script, disclose the skills by listing and reading files with tools.

                $generatedSkillsPrompt
                """.trimIndent(),
        llmModel = OpenAIModels.Chat.GPT4o,
        toolRegistry = ToolRegistry {
            tool(ListDirectoryTool(JVMFileSystemProvider.ReadOnly))
            tool(ReadFileTool(JVMFileSystemProvider.ReadOnly))
            // Additional tools...
        },
    )
}

1.2 必需的组成部分

  • discoverSkills(...) 会扫描配置的目录并返回发现的 skill 描述符。
  • generateSkillsPrompt(...) 会把发现的 skills 转换为 prompt 文本(SkillsPromptFormat.XML 是常见选择)。
  • 生成的文本应嵌入到 agent 的 system prompt 中,以便模型能够推理可用 skills。
  • 工具注册表必须包含你的工作流所需的工具,通常包括:
  • 文件发现/读取工具(用于透明地披露 skill),
  • 一个或多个用于执行 skill 脚本的执行工具。

1.3 行为预期

当存在 skills prompt 且注册了匹配工具时,agent 可以:

  • 发现 skill 文件,
  • 读取 skill 定义,
  • 针对相关任务运行执行工具(例如,使用适当参数运行 python 脚本)。

1.4 实用提示

  • 将 skills 保存在专用目录中,并在相对根目录可能变化的运行环境中传入绝对路径。
  • 当 skills 是静态的时,使用只读文件提供者进行发现(例如 JVMFileSystemProvider.ReadOnly)。
  • 保持脚本执行工具范围狭窄且类型安全(结构化参数/结果),并校验脚本路径处理。

二、Tools

Agent 使用 Tools 来执行特定任务或访问外部系统。Koog Agent 对 Tools 的支持如下:

  1. 创建自定义工具或使用一个内置工具。
  2. 将工具添加到工具注册表。
  3. 将工具注册表传递给智能体。
  4. 通过智能体使用该工具。

2.1 ToolRegistry

在 Agent 中使用工具之前,必须先将它添加到工具注册表。工具注册表管理智能体可用的所有工具。

工具注册表的主要功能:

  • 组织工具。
  • 支持合并多个工具注册表。
  • 提供按名称或类型检索工具的方法。

以下示例展示如何创建工具注册表并将工具添加到其中:

复制代码
val toolRegistry = ToolRegistry {
    tools(myTool)
}

要合并多个工具注册表,请执行以下操作:

复制代码
val firstToolRegistry = ToolRegistry {
    tools(firstSampleTool)
}

val secondToolRegistry = ToolRegistry {
    tools(secondSampleTool)
}

val newRegistry = firstToolRegistry + secondToolRegistry

2.2 Koog Tools 的类型

Koog 框架中有三种工具类型:

  • 内置工具: 提供智能体与用户交互以及对话管理功能。
  • 基于注解的自定义工具: 可将函数作为工具暴露给 LLM。
  • 自定义工具: 可控制工具参数、元数据、执行逻辑,以及工具的注册和调用方式。

2.2.1 内置工具

内置工具是 Koog 框架预置好的一组现成工具,随框架提供,不用你自己写实现。它们定位在 "智能体与用户之间的交互 + 对话管理",专门用于快速原型和实验(官方注明不用于生产)。它们放在独立的 agents-ext 模块里,需要额外加依赖:

复制代码
implementation("ai.koog:agents-ext")

框架提供的内置工具如下:

Tool Name Description
SayToUser __say_to_user__ 允许智能体向用户发送消息。它会使用 Agent says: 前缀将智能体消息打印到控制台。
AskUser __ask_user__ 允许智能体向用户请求输入。它会将智能体消息打印到控制台,并等待用户响应。
ExitTool __exit__ 允许智能体结束对话并终止会话。
ReadFileTool __read_file__ 读取文本文件,并可选择行范围。使用基于 0 的行索引返回带元数据的格式化内容。
EditFileTool __edit_file__ 在文件中执行一次有针对性的文本替换;也可以创建新文件或完整替换内容。
ListDirectoryTool __list_directory__ 以分层树的形式列出目录内容,并可选择控制深度和进行 glob 过滤。
WriteFileTool __write_file__ 将文本内容写入文件(必要时创建父目录)。

内置工具也必须先注册后才能使用,它们也要进 ToolRegistry,注册后 LLM 才能调用。

复制代码
// Create a tool registry with all built-in tools
val toolRegistry = ToolRegistry {
    tool(SayToUser)
    tool(AskUser)
    tool(ExitTool)
    tool(ReadFileTool(JVMFileSystemProvider.ReadOnly))
    tool(ListDirectoryTool(JVMFileSystemProvider.ReadOnly))
    tool(WriteFileTool(JVMFileSystemProvider.ReadWrite))
}

// Pass the registry when creating an agent
val agent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(apiToken),
    systemPrompt = "You are a helpful assistant.",
    llmModel = OpenAIModels.Chat.GPT4o,
    toolRegistry = toolRegistry
)

内置工具本质是"人机交互通道":当 LLM 觉得需要用户确认/输入时,它会去调用 __ask_user__ 这类工具,由内置工具负责打印消息、读取键盘输入,再把结果回传给 LLM------这就是"提供智能体与用户交互以及对话管理功能"的含义。

一句话:内置工具就是框架帮你写好的"对话/文件"工具箱,注册即用,主要用于和用户交互的交互式原型。

2.2.2 基于注解的工具

基于注解的工具是一种声明式的自定义工具:你不需要继承 Tool/SimpleTool 去手写参数类型、名字、描述,只要在普通函数上加注解,框架就会用反射自动把它们收集并包装成工具,暴露给 LLM。核心是"把已有的普通方法变成工具",适合快速把现成功能接入 LLM。

注意:基于注解的工具仅支持 JVM,不适用于其他平台。如需多平台支持,请使用基于类的工具。

2.2.2.1 两个核心注解

核心注解有两个:

Annotation Description
@Tool 标记应暴露给 LLM 的函数。
@LLMDescription 为工具及其组件提供描述性信息。
  1. @Tool: 标记哪些函数要暴露为工具,使用 @Tool 注解的函数和方法会通过反射从实现 ToolSet 接口的对象中收集。
  2. @LLMDescription ------ 给 LLM 提供描述(函数/参数/类/属性都能标),向 LLM 提供描述性信息。 这有助于 LLM 理解这些元素的用途和用法。描述信息包含:
  • 工具的目的和功能。

  • 使用工具所需的参数。

  • 每个参数可接受的值。

  • 执行后的预期返回值。

    @Target(AnnotationTarget.FUNCTION)
    annotation class Tool(val customName: String = "")
    //customName 用于指定工具的自定义名称。如果未提供,则使用函数名。

    @Target(
    AnnotationTarget.PROPERTY,
    AnnotationTarget.CLASS,
    AnnotationTarget.TYPE,
    AnnotationTarget.VALUE_PARAMETER,
    AnnotationTarget.FUNCTION
    )
    public annotation class LLMDescription(val description: String)

2.2.2.2 使用方法

使用方法分为三步:

  1. 类实现 ToolSet 接口(容器标记,反射收集的入口):

    class WeatherToolSet : ToolSet { ... }

  2. 函数加 @Tool(+ @LLMDescription),参数用 @LLMDescription 说明含义:

    @LLMDescription("获取天气的工具集")
    class WeatherToolSet : ToolSet {

    复制代码
     @Tool
     @LLMDescription("获取某地当前天气")
     fun getWeather(
         @LLMDescription("城市和国家/地区") location: String
     ): String = "The weather in $location is sunny and 72°F"

    }

  3. 注册给智能体(注意是 tools(...) 批量,区别于类式工具的 tool(...) 单个):

    val agent = AIAgent(
    promptExecutor = ...,
    toolRegistry = ToolRegistry {
    tools(WeatherToolSet()) // 反射收集所有 @Tool 方法
    }
    )

说明: Kotlin 支持顶层函数,可以对单个函数直接注册,而无需显示实现 ToolSet 类。ToolSet 是为了批量收集多个 @Tool 方法的容器;单个顶层函数用 tool(::fun) 单点注册即可。可以参考前面文章的示例。

2.2.2.3 原理

ToolSet.asTools() 通过 this::class.asTools(thisRef) 反射扫描实例上所有标了 @Tool 的方法,把函数签名(参数、默认值)自动生成工具 schema,包成 ToolFromCallable。所以名字、描述、参数 schema 全部自动推导,不需要手写 typeToken。

2.2.2.4 最佳实践

  • 提供清晰描述:编写清晰、简洁的描述,说明工具、参数和返回值的目的与行为。
  • 描述所有参数:为所有参数添加 @LLMDescription,帮助 LLM 理解每个参数的用途。
  • 使用一致命名:对工具和参数使用一致的命名约定,使其更直观。
  • 分组相关工具:在同一个 ToolSet 实现中分组相关工具,并提供类级别描述。
  • 返回有信息量的结果:确保工具返回值能清楚说明操作结果。
  • 妥善处理错误:在工具中包含错误处理,并返回有信息量的错误消息。
  • 记录默认值:当参数有默认值(Kotlin)或重载(Java)时,在描述中说明这一点。
  • 保持工具聚焦:每个工具都应执行一个具体且定义明确的任务,而不是试图做太多事情。

2.2.2.5 常见问题排查

使用工具注解时,可能会遇到一些常见问题。

工具未被识别

如果智能体无法识别你的工具,请检查以下内容:

  • 类实现了 ToolSet 接口。
  • 所有工具函数或方法都使用了 @Tool 注解。
  • 工具函数或方法具有合适的返回类型(为简单起见,建议使用 String)。
  • 工具已正确注册到智能体中。
工具描述不清晰

如果 LLM 没有正确使用工具,或误解了它们的用途,请尝试以下做法:

  • 尽可能使用基本参数类型(Kotlin 中的 String、Boolean、Int,或 Java 中的 String、boolean、int)。
  • 在参数描述中清楚说明预期格式。
  • 对于复杂类型,考虑使用带特定格式的 String 参数,并在工具中解析它们。
  • 在参数描述中包含有效输入示例。
  • 注意 Java 不支持默认参数。请改用方法重载。
参数类型问题

如果 LLM 提供了错误的参数类型,请尝试以下做法:

  • 尽可能使用简单参数类型(String、Boolean、Int)。
  • 在参数描述中清楚说明预期格式。
  • 对于复杂类型,考虑使用带特定格式的 String 参数,并在工具中解析它们。
  • 在参数描述中包含有效输入示例。
性能问题

如果工具导致了性能问题,请尝试以下做法:

  • 保持工具实现轻量。
  • 对于资源密集型操作,考虑实现异步处理。
  • 在适当时缓存结果。
  • 记录工具使用情况,以识别瓶颈。

2.2.3 基于类的工具

基于类的工具是 Koog 提供的最底层的自定义工具 API:通过继承框架的基类来编写工具,可以完全控制工具的参数、元数据、执行逻辑以及注册调用方式。该方式相对基于注解的工具更为灵活,拓展性更强,这种级别的控制非常适合创建可扩展基本用例的复杂工具,从而实现与座席会话和工作流的无缝集成。

提示:基于类的工具适用于 KMP 的多平台。

2.2.3.1 使用示例

  1. Tool<Args, Result> ------ 全控制,适合返回非文本/复杂结构

需要自定义:argsType(参数类型)、resultType(结果类型)、descriptor(name/description/参数表)、execute()(执行逻辑)。甚至可以自定义结果序列化(encodeResultToString 等)来控制喂给 LLM 的文本格式。

  1. SimpleTool ------ Tool<Args, String> 的快捷版,结果固定文本。

  2. AgentContextAwareTool<Args, Result> ------ 需要感知 agent 运行状态的工具(日志、追踪、上下文敏感逻辑)

    // ---------- 1. SimpleTool 例子:结果固定为 String ----------

    @Serializable
    data class WeatherArgs(
    val location: String,
    val unit: String = "C"
    )

    class WeatherTool : SimpleTool(
    argsType = typeToken(),
    name = "get_weather",
    description = "获取某地当前天气,参数 location 是城市名,unit 是温度单位 C 或 F。"
    ) {
    override suspend fun execute(args: WeatherArgs): String {
    val temp = if (args.unit.equals("F", ignoreCase = true)) 72 else 22
    return "{args.location} 当前天气晴朗,{temp}°${args.unit.uppercase()}"
    }
    }

    // ---------- 2. Tool 例子:返回结构化对象 + 自定义展示格式 ----------

    @Serializable
    data class WeatherResult(
    val location: String,
    val tempC: Int,
    val condition: String
    )

    class WeatherToolV2 : Tool<WeatherArgs, WeatherResult>(
    argsType = typeToken(),
    resultType = typeToken(), // 显式声明结果类型
    name = "get_weather_v2",
    description = "获取某地当前天气(结构化返回),参数 location 是城市名,unit 是温度单位 C 或 F。"
    ) {
    override suspend fun execute(args: WeatherArgs): WeatherResult {
    val tempC = if (args.unit.equals("F", ignoreCase = true)) 22 else 22
    return WeatherResult(location = args.location, tempC = tempC, condition = "晴朗")
    }

    复制代码
     // 可选:自定义发给 LLM 的文本格式(默认是 WeatherResult 的 JSON 序列化结果)
     override fun encodeResultToString(result: WeatherResult, serializer: JSONSerializer): String =
         "${result.location} 当前${result.condition},${result.tempC}°C"

    }

    // ---------- 3. AgentContextAwareTool 例子:execute 额外拿到 AIAgentContext ----------

    class ContextWeatherTool : AgentContextAwareTool<WeatherArgs, WeatherResult>(
    argsType = typeToken(),
    resultType = typeToken(),
    name = "ctx_weather",
    description = "获取天气,并在返回中带上当前 agent 运行的上下文信息(runId、agentId)。"
    ) {
    override suspend fun execute(args: WeatherArgs, context: AIAgentContext): WeatherResult {
    val tempC = if (args.unit.equals("F", ignoreCase = true)) 22 else 22
    println("[ContextWeatherTool] 被 run '{context.runId}' 的 agent '{context.agentId}' 调用")
    return WeatherResult(location = args.location, tempC = tempC, condition = "晴朗")
    }

    复制代码
     override fun encodeResultToString(result: WeatherResult, serializer: JSONSerializer): String =
         "${result.location} 当前${result.condition},${result.tempC}°C"

    }

    val weatherTool = WeatherTool()
    val weatherToolV2 = WeatherToolV2()
    val contextWeatherTool = ContextWeatherTool()

    val agent = AIAgent(
    promptExecutor = MultiLLMPromptExecutor(deepSeekClient),
    llmModel = DeepSeekModels.DeepSeekV4Flash,
    strategy = createCallToolStrategy(weatherTool, weatherToolV2, contextWeatherTool),
    maxIterations = 5,
    toolRegistry = ToolRegistry {
    tool(weatherTool)
    tool(weatherToolV2)
    tool(contextWeatherTool)
    }
    )

2.2.3.2 基类体系(对应本地源码 agents-tools)

复制代码
ToolBase<TArgs, TResult>            // 最底层运行时契约:execute(args, metadata)
   ├── Tool<TArgs, TResult>         // 标准形态:只需重写 execute(args),自动丢弃 metadata
   │     ├── SimpleTool<TArgs>      // 简化版:结果固定为 String,只重写 execute 返回文本
   │     └── (自定义类:你完全控制)
   └── AgentContextAwareTool        // 可在工具里拿到当前 AIAgentContext

基于类的工具,实现 Tool 和 SimpleTool 的核心区别就在结果的类型和处理上。看源码定义(SimpleTool.kt):

复制代码
public abstract class SimpleTool<TArgs>(
    argsType: TypeToken,
    name: String,
    description: String,
) : Tool<TArgs, String>(                 // ← 结果类型固定为 String
    argsType = argsType,
    resultType = typeToken<String>(),
    name = name,
    description = description,
) {
    override fun encodeResultToString(result: String, serializer: JSONSerializer): String = result
    // ↑ 结果字符串原样返回,不需要序列化
}

AgentContextAwareTool 是 Tool 之外的另一个工具基类,继承它之后,execute 的签名多出一个 context: AIAgentContext 参数。普通 Tool<Args, Result> 的 execute(args) 只拿到参数,看不到 agent 运行时的任何状态。但有些工具需要访问 agent 的完整状态------比如:

  • 当前这次运行的 runId(做日志、追踪、关联)
  • LLM 上下文 / 配置
  • agent 的 storage(存储)

核心区别

特性 ToolBase<TArgs, TResult> Tool<TArgs, TResult> SimpleTool<TArgs> AgentContextAwareTool<TArgs, TResult>
execute 签名 execute(args, metadata) execute(args) execute(args) → String execute(args, context)
能拿到什么 原始 metadata Map 只参数 只参数 AIAgentContext(runId、配置、存储等)
何时用 极少见,读取 trace ID 等 常规工具(含结构化返回) 结果就是文本时 工具需要感知 Agent 运行状态时
直接父类 ---(根) ToolBase Tool ToolBase
返回类型 任意 Result 任意 Result 固定 String 任意 Result
metadata 丢弃? 保留 丢弃 丢弃 丢弃(改为注入 context)

如何选择

  • 用 SimpleTool:工具就是"给参数 → 返回一段文本"。比如 WeatherTool,返回 "北京 当前天气晴朗,22°C" 这种给人/给 LLM 看的一句话。

  • 用 Tool<Args, Result>:

  1. 结果是有结构的数据,想以 JSON 形式交给 LLM,或后续程序再解析。例如返回 WeatherResult(tempF, condition)。
  2. 需要自定义发给 LLM 的文本格式(如 Markdown),覆写 encodeResultToString。
  3. 需要 resultType 配合 JSON schema 让 LLM 理解返回结构。

SimpleTool 是 Tool<TArgs, String> 的便捷子类:帮你把 resultType 固定成 String 并让结果原样文本化,省去序列化。

  • 用 AgentContextAwareTool ,是希望获取到 agent 运行时的状态。

两个注意点:

  1. AgentContextAwareTool 必须在 agent 运行中调用:如果脱离 agent 运行直接调用这种工具,没有注入的 context,execute 会抛 IllegalStateException。
  2. 如果想拿到的是 feature 注入的原始 metadata(比如 trace span id),而不只是 agent context,那就直接继承更低层的一档 ToolBase 读 ToolCallMetadata 这个 Map。

2.2.3.3 一个类 = 一个工具

基于类的工具,execute() 是唯一的执行入口,所以一个类只能对应一个工具。这是类式 API 的设计:每个工具是独立类,靠继承 Tool/SimpleTool 定义。

前面讲到的注解式(ToolSet),一个类放多个 @Tool 方法。这是官方推荐"把多个工具组织在一个类"的方式。

2.2.3.4 总结一下

基于类的工具适用于"需要更高灵活性和自定义行为的场景"。具体来说:

  • 要自定义结果发给 LLM 的格式(比如 Markdown,而非 JSON)→ 继承 Tool,覆写 encodeResultToString 或实现 ToolResult.TextSerializable。
  • 要在工具里读取当前 agent 上下文(runId、配置、存储)→ 继承 AgentContextAwareTool。
  • 要读取每次调用的原始 metadata(如 trace span id)→ 直接继承 ToolBase。
  • 注解式做不了以上这些,就得用类式。
对比项 内置工具 基于注解 基于类(你项目在用)
作者 框架自带 你写普通函数 + 注解 你写类继承基类
工具名/描述/参数 框架定死 注解/反射自动推导 手动传参,完全掌控
控制力 最低 中(受限于函数签名) 最高(可定制序列化、metadata、上下文)
跨平台 ✅ ❌ JVM-only ✅ 跨平台

2.3 ToolDescriptorSchemer

ToolDescriptorSchemer 是一个扩展点,用于将 ToolDescriptor 转换为与特定 LLM 提供方兼容的 JSON Schema 对象。

复制代码
ToolDescriptorSchemaGenerator(文档说的 ToolDescriptorSchemer,同名接口)
        │  interface 契约
        ├── fun generate(toolDescriptor: ToolDescriptor): JsonObject
        │
        ├── OpenAICompatibleToolDescriptorSchemaGenerator   ← OpenAI/DeepSeek/... 用
        └── OllamaToolDescriptorSchemaGenerator              ← Ollama 用

作用

把 Koog 内部统一的、与厂商无关的 ToolDescriptor(工具名/描述/参数)翻译成各 LLM 厂商 API 需要的 JSON Schema。Koog 客户端内部会自动使用,你通常不用直接调用,只有在给新厂商写自定义转换时才实现这个接口。

复制代码
class MinimalSchemer : ToolDescriptorSchemaGenerator {
    override fun generate(toolDescriptor: ToolDescriptor): JsonObject = buildJsonObject {
        put("type", "object")
        putJsonObject("properties") {
            (toolDescriptor.requiredParameters + toolDescriptor.optionalParameters).forEach { p ->
                put(p.name, buildJsonObject {
                    put("description", p.description)
                    when (val t = p.type) {
                        ToolParameterType.String -> put("type", "string")
                        ToolParameterType.Integer -> put("type", "integer")
                        is ToolParameterType.Enum -> {
                            put("type", "string")
                            putJsonArray("enum") { t.entries.forEach { add(JsonPrimitive(it)) } }
                        }
                        else -> put("type", "string") // fallback for brevity
                    }
                })
            }
        }
        putJsonArray("required") { toolDescriptor.requiredParameters.forEach { add(JsonPrimitive(it.name)) } }
    }
}

2.4 将工具传递给智能体

要让智能体使用某个工具,在创建智能体时需要提供包含该工具的工具注册表作为参数:

复制代码
// Agent initialization
val agent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
    systemPrompt = "You are a helpful assistant with strong mathematical skills.",
    llmModel = OpenAIModels.Chat.GPT4o,
    // Pass your tool registry to the agent
    toolRegistry = toolRegistry
)

2.5 调用工具

在智能体代码中调用工具有多种方式。推荐的方法是使用智能体上下文中提供的方法,而不是直接调用工具,因为这样可以确保在智能体环境中正确处理工具操作。

提示:请确保已在工具中实现适当的错误处理,以防止智能体失败。在后面的文章中会再讲解。

工具在由 AIAgentLLMWriteSession 表示的特定会话上下文中调用。 它提供了多个调用工具的方法,可以:

  • 使用给定参数调用工具。

  • 按工具名称和给定参数调用工具。

  • 按提供的工具类和参数调用工具。

  • 使用给定参数调用指定类型的工具。

  • 调用返回原始字符串结果的工具。

    @Serializable
    data class GreetArgs(val name: String = "Koog")

    class GreetTool : SimpleTool(
    argsType = typeToken(),
    name = "greet",
    description = "向某人打招呼,参数 name 是要打招呼的对象。"
    ) {
    override suspend fun execute(args: GreetArgs): String = "你好,${args.name}!"
    }

    class AnyArgsTool : Tool<Any?, Any?>(
    argsType = typeToken<Any?>(),
    resultType = typeToken<Any?>(),
    name = "anyArgs",
    description = "接受任意参数并原样返回。"
    ) {
    override suspend fun execute(args: Any?): Any? = "anyArgs 收到: $args"
    }

    private fun createCallToolStrategy(greetTool: GreetTool) =
    functionalStrategy<String, String>("Call tool demo") { input ->
    llm.writeSession {
    // 1. 使用给定参数调用工具(工具实例)
    val byInstance = callTool(greetTool, GreetArgs("实例调用")).content

    复制代码
              // 2. 按工具名称和给定参数调用工具
              val byName = callTool("greet", GreetArgs("名称调用")).content
    
              // 3. 按提供的工具类和参数调用工具
              val byClass = callTool(GreetTool::class, GreetArgs("类调用")).content
    
              // 4. 使用给定参数调用指定类型的工具(reified 泛型)
              val byType = callTool<AnyArgsTool>("泛型调用").content
    
              // 5. 调用返回原始字符串结果的工具
              val byRaw = callToolRaw("greet", GreetArgs("原始字符串"))
    
              buildString {
                  appendLine("输入: $input")
                  appendLine("1. 实例调用: $byInstance")
                  appendLine("2. 名称调用: $byName")
                  appendLine("3. 类调用: $byClass")
                  appendLine("4. 泛型调用: $byType")
                  appendLine("5. 原始字符串: $byRaw")
              }
          }
      }

2.6 并行调用工具

可以使用 toParallelToolCallsRaw 扩展并行调用工具。toParallelToolCallsRaw 是 AIAgentLLMWriteSession 上的一个 Flow 扩展函数:

复制代码
public inline fun <reified TArgs, reified TResult> Flow<TArgs>.toParallelToolCallsRaw(
    toolClass: KClass<out ToolBase<TArgs, TResult>>,   // 或 SafeTool / tool 实例
    concurrency: Int = 16
): Flow<String>

它的作用是把上游发出的每个参数分别作为一次工具调用,并发(parallel)执行,然后只保留每个调用的原始字符串结果(.content),以 Flow 继续向下游发射。

  • Flow:上游是参数流,每个元素是一组工具入参
  • flatMapMerge(concurrency):让多个工具调用并行跑(默认并发 16),不是串行
  • toParallelToolCalls:发射 SafeTool.Result(带包装);toParallelToolCallsRaw:直接发射 String 原始结果

使用示例:

复制代码
llm.writeSession {
    val results: Flow<String> = listOf(
        WeatherArgs("北京"), WeatherArgs("上海"), WeatherArgs("深圳")
    ).asFlow().toParallelToolCallsRaw(
        WeatherTool::class,   // 按工具类批量并发调用
        concurrency = 3
    )
    val all = results.toList()   // 三个城市同时查天气
}

对比上面的 callTool 是单次、串行地调用一个工具;toParallelToolCallsRaw 是批量 + 并发地把整个参数流分发到同一个工具执行,结果以纯字符串形式流出。适合「对 N 个输入做同样处理(如批量查询、批量转换)」的场景。

2.7 调用工具的节点

构建带节点的智能体工作流时,可以使用特殊节点来调用工具:

  • nodeExecuteTool: 调用单个工具调用并返回其结果。
  • nodeExecuteSingleTool: 使用提供的参数调用特定工具。
  • nodeExecuteMultipleTools: 执行多个工具调用并返回其结果。
  • nodeLLMSendToolResult: 将工具结果发送给 LLM 并获取响应。
  • nodeLLMSendMultipleToolResults: 将多个工具结果发送给 LLM。

2.8 将智能作为工具使用

Koog 允许你把一个 AIAgent 包装成一个 Tool,这样它就能像普通工具一样,被注册进其它 Agent 的 ToolRegistry 里,由另一个 Agent 通过工具调用的方式触发执行。本质上是一种层级式 Agent(hierarchical agents)------主 Agent 把子任务委托给专门的子 Agent,子 Agent 就是"工具"。

2.8.1 将智能体转换为工具

讲一个 AIAgent 转成 Tool 的不是单个 AIAgent,而是 AIAgentService(因为需要一个工厂能反复创建 agent)。两个方式:

  1. AIAgentService.fromAgent(agent) ------ 把一个已存在的 FunctionalAIAgent / GraphAIAgent 实例转换成 service。
  2. AIAgentService/FunctionalAgentServiceBuilder / GraphAgentServiceBuilder 构造方法创建一个 AIAgentService ------ 直接按 Agent 的方式配置一个 service(含 strategy、promptExecutor、toolRegistry 等)。

然后调用 createAgentTool(...) 得到 Tool:

复制代码
// Create a specialized agent service, responsible for creating financial analysis agents.
val analysisAgentService = AIAgentService(
    promptExecutor = simpleOpenAIExecutor(apiKey),
    llmModel = OpenAIModels.Chat.GPT4o,
    systemPrompt = "You are a financial analysis specialist.",
    toolRegistry = analysisToolRegistry
)

// Create a tool that would run financial analysis agent once called.
val analysisAgentTool = analysisAgentService.createAgentTool(
    agentName = "analyzeTransactions",
    agentDescription = "Performs financial transaction analysis",
    inputDescription = "Transaction analysis request",
    inputType = typeToken<String>(),
)

2.8.2 在其它智能体中使用智能体工具

转换为工具后,可以将该智能体工具添加到另一个智能体的工具注册表中:

复制代码
// Create a coordinator agent that can use specialized agents as tools
val coordinatorAgent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(apiKey),
    llmModel = OpenAIModels.Chat.GPT4o,
    systemPrompt = "You coordinate different specialized services.",
    toolRegistry = ToolRegistry {
        tool(analysisAgentTool)
        // Add other tools as needed
    }
)

2.8.3 智能体工具执行

当调用智能体工具时:

  • 根据输入描述符反序列化参数。
  • 使用反序列化后的输入执行被包装的智能体。
  • 将智能体的输出序列化,并作为工具结果返回。

2.8.4 将智能体用作工具的优势

  • 模块化: 将复杂工作流拆分为专门化智能体。
  • 可重用性: 在多个协调智能体中使用同一个专门化智能体。
  • 关注点分离: 每个智能体都可以专注于自己的特定领域。

三、总结

3.1 Skills vs Tools,一句话分清

前面开头提到了两个概念的区别,这里用一张表再理一遍:

对比项 Skills Tools
本质 打包好的"技能包"(脚本 + 元数据 + prompt) 一个个可调用的"函数/动作"
关注点 干什么事(流程、方法论) 怎么干(具体执行动作)
在 Agent 中的形态 通过 prompt 段落暴露给模型,由模型按需去读文件、跑脚本 注册进 ToolRegistry,由模型直接调用
复用粒度 整包复用,面向固定流程 单点复用,面向原子操作
典型例子 "数据分析技能包""报告生成技能包" get_weather、read_file、ask_user

一句话:Skills 是"做什么的说明书 + 配套脚本",Tools 是"具体动手的执行器";模型靠 Skills prompt 知道有哪些招,靠 Tools 真正把招使出来。

3.2 核心要点回顾

Skills

  • 三步走:discoverSkills(...) 发现 → generateSkillsPrompt(...) 生成 prompt → 塞进 system prompt + 注册配套工具(文件读取 + 脚本执行)。
  • 注意点:skills 目录传绝对路径;静态 skills 用只读文件提供者;脚本执行工具要保持窄且类型安全。

Tools

  • 使用四步:创建/选用工具 → 加入 ToolRegistry → 传给 Agent → 让 Agent 调用。
  • 三种工具类型,按控制力从低到高:
特性 内置工具 基于注解的工具 基于类的工具
写法 注册即用,框架自带 ToolSet + @Tool + @LLMDescription 继承 Tool / SimpleTool / AgentContextAwareTool
控制力 最低 中 最高
跨平台 ✅ ❌ JVM-only ✅
适用场景 快速原型、人机交互、文件读写 把现成普通方法快速暴露给 LLM 复杂结构返回、自定义序列化、感知 Agent 上下文
  • 基于类的工具怎么选:返回纯文本用 SimpleTool;结构化返回 / 自定义展示格式用 Tool<Args, Result>;要拿 runId / storage 等运行状态用 AgentContextAwareTool;要读原始 metadata(如 trace span id)用 ToolBase。
  • 工具调用方式:会话内 callTool / callToolRaw(单次串行)、toParallelToolCallsRaw(批量并发)、工作流节点(nodeExecuteTool 等)。
  • 进阶玩法:用 AIAgentService.createAgentTool(...) 把子 Agent 包装成 Tool,实现层级式 Agent。

3.3 实用选型建议

  1. 先想清楚要"包"还是要"函数":固定流程、跨任务复用 → Skills;原子操作、单点能力 → Tools。
  2. 优先用基于类的工具:官方推荐多平台、控制力最强,你项目里也正在用这套。
  3. 能用 SimpleTool 就别用 Tool:结果就是一段文本时,省去序列化,代码最简。
  4. 需要交互再上内置工具 :__ask_user__ 这类是"人机通道",只服务于交互式原型,生产环境谨慎。
  5. 层级 Agent 是治理复杂度的利器:子任务拆给专门 Agent,主 Agent 专注协调,天然模块化、可复用。

到这里,Skills 和 Tools 的基本用法就讲完了。下一篇文章可以聊聊 Koog Agent 中的 Strategies。

相关推荐
七夜zippoe2 小时前
Agent 事件驱动架构:从轮询到事件触发的架构升级
ai·架构·agent·轮询·事件驱动·事件触发
yuzhiboyouye3 小时前
longchain就像已做好的项目和大模型的连接器一样
agent
yuzhiboyouye3 小时前
langchain和spring-boot功能上有什么区别
agent
笑小枫3 小时前
AgentScope 学习系列 - 04:Tool 工具调用实战
agent·智能体开发·agentscope·java开发智能体
流浪0013 小时前
大模型技术全景(二十三):RAG 应用阶段的检索、提示词增强与生成
llm·rag·提示词增强·rag检索
为你学会写情书4 小时前
别再让你的 Agent 当“黑盒”了:LangSmith 如何用可观测性、评估和洞察闭环驯服生产级智能体
agent
故作春风4 小时前
Agent开发-实现一个对话循环
agent
Yuzhiyuxia4 小时前
【面试系列】MCP面试知识点
llm
墨心@4 小时前
Bash 工具讲解
agent·harness