工具系统:定义、schema 生成、注册与安全执行
30 秒导读: 工具是 agent 的手脚——让 LLM 不只是「说」,还能「做」(读文件、查数据库、调 API)。本章讲一条完整链路:一段普通 Kotlin 函数怎样变成工具、怎样把它的参数契约翻译成 LLM 能读的 JSON schema、怎样注册进表、以及被调用时怎样经过安全管线执行而不是被直接 call。
本章只讲工具本身。「LLM 在某个节点该选哪个工具」是图的 edge 谓词与 ToolSelectionStrategy 的事,见 01-graph-engine.md 与 02-dsl-and-strategies.md。「工具描述最终怎么塞进 prompt、发给哪个 Client」见 03-llm-layer.md。「工具执行时被 feature 拦截、观测」见 05-features-runtime-extensions.md。
1. 这是什么(零基础也能懂)
一句话定义: 工具(Tool)是一段带类型化输入/输出、并向 LLM 自我描述的可执行逻辑。LLM 读到它的名字和参数说明后,可以决定「调用它、传这些参数」;框架接住这个决定,真正把逻辑跑起来,再把结果喂回 LLM。
解决什么问题 / 给谁用: 纯 LLM 只会生成文本。你想让它「查一下今天订单数」「把这段文字情感打个分」「在数据库里建一条记录」——这些动作模型自己做不了,得有人替它做。工具就是那个「替它做」的东西。给谁用:写 agent 的 Kotlin/Java 工程师。
它能做什么:
- 把任意 Kotlin/Java 函数零样板变成工具(
@Tool注解 + 反射)。 - 从参数类型自动生成 LLM 能读的参数 schema。
- 用一张注册表统一管理、按名字/类型查找工具。
- 执行时走一条受控管线:类 型解码 → 执行 → 结果编码 → 事件通知 feature,任何一步出错都被转成结构化的失败结果而不是崩掉。
- 接入外部 MCP server 的工具,当成本地工具一样用。
用起来什么样: 最直接的一种——继承 SimpleTool,写一个返回字符串的工具:
// 示意,非源码:一个把文本情感打标签的工具
object ToneTool : SimpleTool<ToneTool.Args>(
argsType = typeToken<Args>(),
name = "analyze_tone",
description = "分析给定文本的情感倾向" // 这句会被 LLM 读到
) {
@Serializable
data class Args(val text: String) // 参数类型 → 自动变成 schema
override suspend fun doExecute(args: Args): String =
if ("糟糕" in args.text) "negative" else "positive"
}
val registry = ToolRegistry { tool(ToneTool) } // 注册,交给 agent
一句话直觉: 把工具想成招聘启事 + 岗位本人。招聘启事(ToolDescriptor)贴给 LLM 看,写清「岗位叫什么、需要哪些参数」;岗位本人(execute)在后台真正干活。LLM 只看启事做决定,永远不直接碰本人——中间隔着一层 HR(environment 管线)。