前言
三月份的时候,写了一篇文章: 拆穿名词诈骗!用大白话理解晦涩难懂的AI概念, 里面有介绍 Agent Skills 和 Tools。当时有详细解释了两个概念,Tools 相当于大模型的"手脚",让 LLM 不仅能回答问题,还能调用程序,执行动作。 Skills 是将复杂的、固定的流程打包成一个个独立的"技能包",是 Agent 走向工程化、模块化的关键一步。对于这写概念还不清楚的小伙伴,可以回头去看那篇文章。
本文将讲解如何在 Koog Agent 中使用 Skills 和 Tools。
一、 Skills
Koog skills 允许 agent 从文件系统中发现可复用的能力包,并通过生成的 prompt 段落将它们暴露给模型。
使用起来包含以下三个步骤::
- 从一个或多个根目录发现 skills。
- 根据发现的元数据生成 skills prompt 块。
- 将生成的块添加到 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 的支持如下:
- 创建自定义工具或使用一个内置工具。
- 将工具添加到工具注册表。
- 将工具注册表传递给智能体。
- 通过智能体使用该工具。
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 |
为工具及其组件提供描述性信息。 |
- @Tool: 标记哪些函数要暴露为工具,使用 @Tool 注解的函数和方法会通过反射从实现 ToolSet 接口的对象中收集。
- @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 使用方法
使用方法分为三步:
-
类实现 ToolSet 接口(容器标记,反射收集的入口):
class WeatherToolSet : ToolSet { ... }
-
函数加 @Tool(+ @LLMDescription),参数用 @LLMDescription 说明含义:
@LLMDescription("获取天气的工具集")
class WeatherToolSet : ToolSet {@Tool @LLMDescription("获取某地当前天气") fun getWeather( @LLMDescription("城市和国家/地区") location: String ): String = "The weather in $location is sunny and 72°F"}
-
注册给智能体(注意是 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 使用示例
- Tool<Args, Result> ------ 全控制,适合返回非文本/复杂结构
需要自定义:argsType(参数类型)、resultType(结果类型)、descriptor(name/description/参数表)、execute()(执行逻辑)。甚至可以自定义结果序列化(encodeResultToString 等)来控制喂给 LLM 的文本格式。
-
SimpleTool ------ Tool<Args, String> 的快捷版,结果固定文本。
-
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>:
- 结果是有结构的数据,想以 JSON 形式交给 LLM,或后续程序再解析。例如返回 WeatherResult(tempF, condition)。
- 需要自定义发给 LLM 的文本格式(如 Markdown),覆写 encodeResultToString。
- 需要 resultType 配合 JSON schema 让 LLM 理解返回结构。
SimpleTool 是 Tool<TArgs, String> 的便捷子类:帮你把 resultType 固定成 String 并让结果原样文本化,省去序列化。
- 用 AgentContextAwareTool ,是希望获取到 agent 运行时的状态。
两个注意点:
- AgentContextAwareTool 必须在 agent 运行中调用:如果脱离 agent 运行直接调用这种工具,没有注入的 context,execute 会抛 IllegalStateException。
- 如果想拿到的是 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)。两个方式:
- AIAgentService.fromAgent(agent) ------ 把一个已存在的 FunctionalAIAgent / GraphAIAgent 实例转换成 service。
- 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 实用选型建议
- 先想清楚要"包"还是要"函数":固定流程、跨任务复用 → Skills;原子操作、单点能力 → Tools。
- 优先用基于类的工具:官方推荐多平台、控制力最强,你项目里也正在用这套。
- 能用 SimpleTool 就别用 Tool:结果就是一段文本时,省去序列化,代码最简。
- 需要交互再上内置工具 :
__ask_user__这类是"人机通道",只服务于交互式原型,生产环境谨慎。 - 层级 Agent 是治理复杂度的利器:子任务拆给专门 Agent,主 Agent 专注协调,天然模块化、可复用。
到这里,Skills 和 Tools 的基本用法就讲完了。下一篇文章可以聊聊 Koog Agent 中的 Strategies。