Skip to content
Embabel 文档版本 0.3.0-SNAPSHOT 源快照 bd727785 更新于 2026-09-29

13 · 工具(Tools)

工具(Tool)可以传给 LLM,让它们执行动作。 工具既可以在 JVM 进程之外(如 MCP),也可以在 JVM 进程之内(如暴露 @LlmTool 方法的领域对象)。

Embabel 允许你以两种方式向 LLM 提供工具:

  • 通过 PromptRunner 提供一个或多个进程内工具实例。 工具实例就是一个对象,它的方法上标注了 Embabel 的 @LlmTool 或 Spring AI 的 @Tool;
  • 在 action 层或 PromptRunner 层,从一个工具组(tool group)提供。

LlmReference 的实现也会暴露工具,但这件事由框架内部处理。

进程内工具:实现工具实例

在一个类上实现一个或多个标注 @LlmTool 的方法即可。 你不需要给类本身加注解。 每个被注解的方法代表一个独立的工具,会暴露给 LLM。

一个简单的工具方法示例:

java
public class MathTools {

    @LlmTool(description = "add two numbers")
    public double add(double a, double b) {
        return a + b;
    }

    // 其它工具
}

实现工具的类可以有状态,它们往往是领域对象。 放在映射实体(mapped entity)上的工具尤其有用 —— 它们可以封装那些永远不暴露给 LLM 的状态。 关于工具使用模式的讨论,见 Domain Tools: Direct Access, Zero Ceremony。

@Tool 注解来自 Spring AI。

工具方法可以是任何可见性,可以是静态或实例作用域,也允许定义在内部类上。

方法可以定义任意数量的参数(包括零参数),类型也基本不受限 (基本类型、POJO、枚举、List、数组、Map 等等)。 返回值同样支持大多数类型,包括 void。 如果方法有返回值,返回类型必须是可序列化的 —— 因为结果会被序列化并发回模型。

以下类型目前不支持作为工具的入参或返回类型:

  • Optional;
  • 异步类型(如 CompletableFuture、Future);
  • 响应式类型(如 Flow、Mono、Flux);
  • 函数式类型(如 Function、Supplier、Consumer)。

—— 引自 Spring AI

你可以在工具方法实现里通过 AgentProcess.get() 拿到当前的 AgentProcess。 这让工具能绑定到 AgentProcess,从而把对象提供给其它 action。例如:

java
@LlmTool(description = "My Tool")
public String bindCustomer(Long id) {
    var customer = customerRepository.findById(id);
    var agentProcess = AgentProcess.get();
    if (agentProcess != null) {
        agentProcess.addObject(customer);
        return "Customer bound to blackboard";
    }
    return "No agent process: Unable to bind customer";
}

在工具中接收带外上下文(Out-of-Band Context)

工具方法常常需要访问一些基础设施元数据 —— 认证 token、租户 ID、关联 ID —— 而这些不应出现在面向 LLM 的 JSON schema 里。 ToolCallContext 正是为此而生:一个不可变的键值包, 它流过整条工具流水线,而 LLM 永远看不到它。

可以把它想成请求上的 HTTP header: 调用方在边界处(REST 过滤器、事件处理器)设置它们, 链上的每个 handler 都能读到 —— 但请求体(也就是 LLM 提供的内容)不受影响。

把 ToolCallContext 注入 @LlmTool 方法

在任何 @LlmTool 方法上声明一个 ToolCallContext 参数。框架会:

  • 在调用时注入当前上下文(若未设置则为 ToolCallContext.EMPTY);
  • 把该参数从 LLM 看到的 JSON schema 中排除。
java
public class CustomerTools {

    @LlmTool(description = "Look up customer by ID")
    public String lookupCustomer(
            @LlmTool.Param(description = "Customer ID") long customerId,
            ToolCallContext context) {
        String tenantId = context.get("tenantId");
        String authToken = context.get("authToken");
        return customerService.lookup(customerId, tenantId, authToken);
    }
}

LLM 只会看到 customerId 参数,ToolCallContext 参数在工具的 schema 中是不可见的。

这一点对 KotlinMethodTool 与 JavaMethodTool 都成立 —— ToolCallContext 参数可以出现在方法签名的任意位置。

通过 ProcessOptions 设置上下文

上下文在进程边界上用 ProcessOptions.withToolCallContext() 设置。 随后它会传播到该进程内的每一次工具调用 —— 包括 MCP 工具,在那里它会桥接为 Spring AI 的 ToolContext。

java
// 在 REST controller 或事件处理器中
var processOptions = new ProcessOptions()
        .withToolCallContext(Map.of(
                "authToken", request.getHeader("Authorization"),
                "tenantId", request.getHeader("X-Tenant-Id"),
                "correlationId", UUID.randomUUID().toString()
        ));

var invocation = AgentInvocation.builder(agentPlatform)
        .options(processOptions)
        .build(CustomerReport.class);

CustomerReport report = invocation.invoke(customerQuery);

上下文在装饰器链中传播

ToolCallContext 会自动流过装饰器链。 任何实现了 DelegatingTool 的工具默认都会把上下文转发给它的 delegate。 像 ArtifactSinkingTool 与 ReplanningTool 这样的内建装饰器都遵循此模式, 因此上下文无需任何额外接线就能抵达底层工具。

每轮仅一次的工具(OneShotPerLoopTool)

有些工具最多只应在一次 agentic 循环迭代中触发一次 —— 通常是因为该调用返回的内容一旦交付,就会在本轮剩余时间里留在 LLM 的对话历史中。 最典型的例子是技能激活器(skill activator): 调用它返回技能正文供 LLM 使用;再调用一次只是返回同样的正文, 除了浪费 token 与一次往返之外毫无意义。

更强的模型能可靠遵守"每个激活器只调用一次"这样的系统提示规则。 较弱的模型(qwen、gpt-oss、更小的开源模型)则会一轮又一轮地反射性地重复调用同一个工具, 即便正文已经在对话历史里了。光靠文字是无法强制执行提示规则的 —— OneShotPerLoopTool 把这条约束变成了机械性的。

把底层工具用 OneShotPerLoopTool 包一层,并提供一个 advice 字符串, 告诉 LLM 不要再调用、而应该做什么:

java
Tool gated = new OneShotPerLoopTool(
        githubWorkflowsActivator,
        "Write your script now using the skill body above."
);

在给定循环中的第一次调用会照常委托给底层工具。 同一循环内的后续每一次调用都会被短路,返回:

ALREADY LOADED. The body of '<tool name>' was returned earlier in this turn —
read it from your conversation history above. Do not call this tool again.
<advice>

循环作用域由 LoopMemo 读取 ToolCallContext.loopId() 提供, 因此编排方必须为每一轮打上一个新的 loop id:

kotlin
val loopId = java.util.UUID.randomUUID().toString()
context.ai()
    .withLlm(myLlm)
    .withToolCallContext(mapOf(ToolCallContext.LOOP_ID_KEY to loopId))
    .withTools(gatedTools)
    .respond(messages)

如果没有打上 loop id,LoopMemo 文档化的兜底行为是 "总是发出" —— 每次调用都被当作第一次,于是这个包装退化为透传,而不是静默地锁死。

该类实现 DelegatingTool,因此底层工具可通过 delegate 访问, 而子类唯一需要覆写的就是那个规范的双参调用重载。

关于底层的记忆化原语本身(例如在工具自己的调用里实现"每轮第一次描述时发出一次规则块"),见 LoopMemo。

在框架无关的工具中使用上下文

对于程序化创建的工具,使用 Tool.ContextAwareFunction 在 handler 中接收上下文。 Tool.of() 工厂方法接受一个 ContextAwareFunction 作为最后一个参数:

java
Tool tenantAwareTool = Tool.of(
        "search",
        "Search within tenant scope",
        Tool.InputSchema.of(Tool.Parameter.string("query", "Search query")),
        Tool.Metadata.DEFAULT,
        (Tool.ContextAwareFunction) (input, context) -> {
            String tenantId = context.get("tenantId");
            return Tool.Result.text(searchService.search(input, tenantId));
        }
);

没有提供上下文时,该函数收到的是 ToolCallContext.EMPTY。

上下文是不可变的,从任何线程读取都是安全的。

如果你需要把上下文从 Web 请求一路传到工具调用,只在 ProcessOptions 上设置一次, 该进程中的每个工具都会收到它。

通过 PromptRunner 按交互设置上下文

对于只属于某一次 LLM 调用(而非整个 Agent run)的上下文, 在 @Action 方法里直接在 PromptRunner 上用 withToolCallContext()。 这是放置属于某次特定交互的领域级元数据的正确位置 —— 例如该 action 正在处理哪个实体。

java
@Action
public RelevantNewsStories findNewsStories(
        StarPerson person, Horoscope horoscope, Ai ai) {

    // 仅属于本次交互的领域特定上下文。
    // 会流到本次 createObject 调用期间被调用的所有工具,
    // 包括远程 MCP 工具 —— 在那里它会成为 MCP 的 _meta。
    var interactionContext = ToolCallContext.of(Map.of(
            "personName", person.name(),
            "starSign", person.sign(),
            "feature", "star-news-finder"
    ));

    return ai
            .withDefaultLlm()
            .withToolGroup(CoreToolGroups.WEB)
            .withToolCallContext(interactionContext) // ①
            .createObject(prompt, RelevantNewsStories.class);
}

① 为方便起见,withToolCallContext() 也接受一个普通的 Map。

上下文合并语义

来自两个来源的上下文会在 ToolLoopLlmOperations.resolveToolCallContext() 中自动合并。 冲突时以交互级的值优先。

ProcessOptionsPromptRunner最终生效的上下文
tenantId=acme—{tenantId=acme}
—authToken=xyz{authToken=xyz}
tenantId=acmeauthToken=xyz{tenantId=acme, authToken=xyz}
tenantId=acmetenantId=override{tenantId=override} —— 交互级胜出

这意味着 ProcessOptions 是放置横切基础设施关注点的正确位置 (租户路由、关联 ID、网关处注入的凭据), 而 PromptRunner.withToolCallContext() 是放置领域特定、按交互关注点的正确位置 (该 action 正在处理哪个实体)。

控制什么能跨越 MCP 边界

当 Embabel 调用远程 MCP 服务器时,ToolCallContext 的条目会作为 MCP _meta 在线上转发。 _meta 是 MCP 2025-06-18 规范中的一等字段, MCP 服务端工具可以通过 McpMeta 参数(或 Spring AI 的 ToolContext)接收它。

默认情况下所有上下文条目都会被转发(passThrough 行为)。 对于调用不受信任的第三方 MCP 服务器的生产部署, 请注册一个 ToolCallContextMcpMetaConverter bean 来控制什么能跨越进程边界。

可以把它想成反向代理上的 HTTP header 过滤器: 这个转换器决定哪些条目是安全的可以传播、哪些应当留在本地。

java
// 允许列表 —— 生产环境推荐:只有具名键能跨越边界
@Bean
public ToolCallContextMcpMetaConverter toolCallContextMcpMetaConverter() {
    return ToolCallContextMcpMetaConverter.allowKeys("tenantId", "correlationId", "locale");
}

// 或者拒绝列表 —— 除机密外全部转发
@Bean
public ToolCallContextMcpMetaConverter toolCallContextMcpMetaConverter() {
    return ToolCallContextMcpMetaConverter.denyKeys("apiKey", "authToken");
}

// 或者自定义 lambda 实现任意逻辑
@Bean
public ToolCallContextMcpMetaConverter toolCallContextMcpMetaConverter() {
    return context -> Map.of(
            "tenantId", context.get("tenantId"),
            "requestedAt", Instant.now().toString()
    );
}

若未定义任何 bean,框架出于向后兼容默认使用 passThrough()。 可用的工厂方法:

方法行为适用场景
passThrough()转发所有条目完全受信任的内部 MCP 服务器(默认)
noOp()什么都不转发零信任:阻止任何元数据跨越边界
allowKeys(vararg keys)只转发具名键生产环境(推荐):显式允许列表
denyKeys(vararg keys)除具名键外全部转发当机密项已知且可枚举时

最干净的安全做法,是一开始就只把安全的值放进 ToolCallContext —— 机密应当放在 @Value / Vault / 环境变量里,而不是这个上下文包里。 ToolCallContextMcpMetaConverter 只是第二道防线, 用于上下文本就在基础设施边界填充、而并非所有条目都该抵达第三方服务器的场景。

工具组(Tool Groups)

Embabel 引入了工具组这个概念。它是用户意图与工具选择之间的一层间接。 例如,我们不直接要求"Brave 或 Google 网页搜索",而是要求 "web" 工具 —— 它可以在不同环境中被解析成不同的东西。

工具的使用应当聚焦(focused)。

因此,工具组不是在 Agent 层指定,而是在单个 action 上指定。

工具组常常由 MCP 支撑。

在配置文件中配置工具组

如果你已经在应用配置中配置了 MCP 服务器, 就可以通过配置工具组来有选择地把这些服务器上的工具暴露给 Agent。 最简便的做法是在 application.yml 或 application.properties 里配置,按名字挑选工具。

例如:

yaml
embabel:
  agent:
    platform:
      tools:
        includes:
          weather:
            description: Get weather for location
            provider: Docker
            tools:
              - weather

在 Spring @Configuration 中配置工具组

你也可以用 Spring 的 @Configuration 与 @Bean 注解,以更强的控制力把 ToolGroup 暴露给 Agent 平台。 框架提供了一个默认的 ToolGroupsConfiguration,演示如何注入 MCP 服务器并有选择地暴露 MCP 工具:

java
@Configuration
public class ToolGroupsConfiguration {

    private final List<McpSyncClient> mcpSyncClients;

    public ToolGroupsConfiguration(List<McpSyncClient> mcpSyncClients) {
        this.mcpSyncClients = mcpSyncClients;
    }

    @Bean
    public MathTools mathToolGroup() {
        return new MathTools();
    }

    @Bean
    public ToolGroup mcpWebToolsGroup() { // ①
        return new McpToolGroup(
                CoreToolGroups.WEB_DESCRIPTION,
                "docker-web",
                "Docker",
                Set.of(ToolGroupPermission.INTERNET_ACCESS),
                mcpSyncClients,
                callback -> {
                    // 只暴露特定的 web 工具,排除有速率限制的那些
                    String name = callback.getToolDefinition().name();
                    return (name.contains("brave") || name.contains("fetch")) &&
                            !name.contains("brave_local_search");
                }
        );
    }
}

① 这个方法创建一个类型为 ToolGroup 的 Spring bean。 它会被 Agent 平台自动拾取,从而允许按名字(role)请求该工具组。

关键配置模式

MCP 客户端注入: 配置类通过构造器注入接收一个 List<McpSyncClient>。 Spring 会自动提供应用中已配置的全部 MCP 客户端。

选择性工具暴露: 每个 McpToolGroup 用一个过滤器 lambda来控制哪些工具从 MCP 服务器暴露给 Agent。 这让你能对工具可用性做细粒度控制,防止不需要或有问题的工具被用上。

工具组元数据: 工具组带有 name、provider、description 等描述性元数据,帮助 Agent 理解其能力。 permissions 属性声明该工具组需要什么访问权限(例如 INTERNET_ACCESS)。

创建自定义工具组配置

应用可以实现自己的 @Configuration 类来暴露自定义工具组 —— 它们可以由任何服务或资源支撑,而不限于 MCP。

java
@Configuration
public class MyToolGroupsConfiguration {

    @Bean
    public ToolGroup databaseToolsGroup(DataSource dataSource) {
        return new DatabaseToolGroup(dataSource);
    }

    @Bean
    public ToolGroup emailToolsGroup(EmailService emailService) {
        return new EmailToolGroup(emailService);
    }
}

这种做法借助 Spring 的依赖注入为工具组提供所需的服务与资源, 同时在工具配置与 Agent 逻辑之间保持干净的关注点分离。

在 action 方法中使用工具

工具是在发起 LLM 调用时在 PromptRunner 上指定的。 这让你能对每个特定提示词可用哪些工具做细粒度控制。

下面是 StarNewsFinder agent 中的一个例子,展示在 action 内使用 web 工具:

java
@Action
public RelevantNewsStories findNewsStories(
        StarPerson person, Horoscope horoscope, OperationContext context) {
    var prompt = """
            %s is an astrology believer with the sign %s.
            Their horoscope for today is:
            <horoscope>%s</horoscope>
            Given this, use web tools and generate search queries
            to find %d relevant news stories summarize them in a few sentences.
            Include the URL for each story.
            Do not look for another horoscope reading or return results directly about astrology;
            find stories relevant to the reading above.
            """.formatted(
            person.name(), person.sign(), horoscope.summary(), storyCount);

    // 工具是在 PromptRunner 上指定的
    return context.ai().withDefaultLlm()
            .withToolGroup(CoreToolGroups.WEB) // 加上 web 搜索工具
            .createObject(prompt, RelevantNewsStories.class);
}

关键工具使用模式

PromptRunner 的工具方法: 工具通过 withToolGroup()、withTools()、withToolObject() 等方法加进 PromptRunner 的流式 API。

多个工具组: 当 action 需要不同类型的能力时,可以链式调用多次 withToolGroup() 来加入多个工具组。

能感知工具的提示词: 提示词应当明确指示 LLM 使用可用工具。 例如 "use web tools and generate search queries" 就清楚地引导 LLM 去用 web 搜索能力。

更多 PromptRunner 示例

java
// 为特定提示词加入工具组
context.ai().withAutoLlm().withToolGroup(CoreToolGroups.WEB).create(
        "Given the topic, generate a detailed report using web research.\n\n" +
                "# Topic\n" +
                reportRequest.getTopic()
);

// 加入多个工具组
context.ai().withDefaultLlm()
        .withToolGroup(CoreToolGroups.WEB)
        .withToolGroup(CoreToolGroups.MATH)
        .createObject("Calculate stock performance with web data", StockReport.class);

用 @LlmTool 方法加入工具对象:

你也可以把带 @LlmTool 方法的领域对象直接提供给特定提示词:

java
context.ai()
        .withDefaultLlm()
        .withToolObject(jokerTool)
        .createObject("Create a UserInput object for fun", UserInput.class);

// 加入工具对象并带过滤与自定义命名策略
context.ai()
        .withDefaultLlm()
        .withToolObject(
                new ToolObject(calculatorService)
                        .withNamingStrategy(name -> "calc_" + name)
                        .withFilter(methodName -> methodName.startsWith("compute"))
        ).createObject("Perform calculations", Result.class);

可用的 PromptRunner 工具方法:

  • withToolGroup(String) —— 按名字加入单个工具组;
  • withToolGroup(ToolGroup) —— 加入特定的 ToolGroup 实例;
  • withToolGroups(Set) —— 加入多个工具组;
  • withTools(vararg String) —— 加入多个工具组的便捷方法;
  • withToolObject(Any) —— 加入带 @LlmTool 或 @Tool 方法的领域对象;
  • withToolObject(ToolObject) —— 加入带自定义配置的 ToolObject;
  • withTool(Tool) —— 加入框架无关的 Tool 实例;
  • withTools(List) —— 加入多个框架无关的 Tool 实例。

框架无关的工具接口(Framework-Agnostic Tool Interface)

除了 Spring AI 的 @Tool 注解,Embabel 在 com.embabel.agent.api.tool 包中提供了自己的框架无关的 Tool 接口。 这让你能创建不绑定任何特定 LLM 框架的工具,使代码更可移植、更可测试。

Tool 接口包含若干嵌套类型,以避免与框架特有类型产生命名冲突:

  • Tool.Definition —— 描述该工具(name、description、input schema);
  • Tool.InputSchema —— 定义该工具接受的参数;
  • Tool.Parameter —— 单个参数,含 name、type、description;
  • Tool.Result —— 工具返回的结果(text、artifact 或 error);
  • Tool.Handler —— 用于实现工具逻辑的函数式接口。

用程序化方式创建工具

你可以用 Tool.create() 工厂方法创建工具:

java
// 无参数的简单工具
Tool greetTool = Tool.create(
        "greet",
        "Greets the user",
        (input) -> Tool.Result.text("Hello!")
);

// 带参数的工具(用工厂方法)
Tool addTool = Tool.create(
        "add",
        "Adds two numbers together",
        Tool.InputSchema.of(
                Tool.Parameter.integer("a", "First number"),
                Tool.Parameter.integer("b", "Second number")
        ),
        (input) -> {
            // 解析输入 JSON 并计算结果
            return Tool.Result.text("42");
        }
);

// 带元数据的工具(例如:不经 LLM 处理直接返回)
Tool directTool = Tool.create(
        "lookup",
        "Looks up data directly",
        Tool.Metadata.create(true), // returnDirect = true
        (input) -> Tool.Result.text("Direct result")
);

Tool.Parameter 类为常见参数类型提供工厂方法:

  • Tool.Parameter.string(name, description) —— 字符串参数;
  • Tool.Parameter.string(name, description, required) —— 字符串 + 可选性标志;
  • Tool.Parameter.string(name, description, required, enumValues) —— 带允许取值的字符串;
  • Tool.Parameter.integer(name, description) —— 整数参数;
  • Tool.Parameter.double(name, description) —— 浮点参数。

所有工厂方法默认 required = true。

创建强类型工具

对于输入与输出结构复杂的工具,用 Tool.fromFunction() 直接操作领域对象。 输入 schema 会自动从输入类型生成,JSON 编解码也由框架处理。

java
// 定义输入与输出类型
record AddRequest(int a, int b) {}
record AddResult(int sum) {}

// 创建类型化工具 —— schema 从 AddRequest 生成
Tool addTool = Tool.fromFunction(
        "add",
        "Adds two numbers together",
        AddRequest.class,
        AddResult.class,
        input -> new AddResult(input.a() + input.b())
);

// 调用该工具 —— 输入被反序列化,输出被序列化
Tool.Result result = addTool.call("{\"a\": 5, \"b\": 3}");
// 结果包含:{"sum":8}

// 字符串输出会被直接返回(不会被二次序列化)
Tool greetTool = Tool.fromFunction(
        "greet",
        "Greets someone",
        GreetRequest.class,
        String.class,
        input -> "Hello " + input.name() + "!"
);

// 带自定义元数据
Tool directTool = Tool.fromFunction(
        "lookup",
        "Looks up data directly",
        LookupRequest.class,
        LookupResult.class,
        Tool.Metadata.create(true), // returnDirect = true
        input -> new LookupResult(findData(input))
);

你也可以直接实例化 TypedTool:

kotlin
val tool = TypedTool(
    name = "add",
    description = "Add two numbers",
    inputType = AddRequest::class.java,
    outputType = AddResult::class.java,
) { input -> AddResult(input.a + input.b) }

类型化工具的关键特性:

  • 自动生成 schema:输入 schema 由输入类型的结构推导而来;
  • JSON 编解码:输入 JSON 被反序列化成输入类型,输出从输出类型序列化;
  • 字符串直通:若输出类型是 String,直接返回,不做 JSON 序列化;
  • 结果直通:若函数返回 Tool.Result,原样使用;
  • 异常处理:函数抛出的异常会被转换为 Tool.Result.Error;
  • 控制流信号:实现 ToolControlFlowSignal 的异常(如 ReplanRequestedException)会穿透传播。

从注解方法创建工具

Embabel 提供 @LlmTool 与 @LlmTool.Param 注解,用于从注解方法创建工具。 这种做法与 Spring AI 的 @Tool 类似,但使用的是 Embabel 的框架无关抽象。

java
public class MathService {

    @LlmTool(description = "Adds two numbers together")
    public int add(
            @LlmTool.Param(description = "First number") int a,
            @LlmTool.Param(description = "Second number") int b) {
        return a + b;
    }

    @LlmTool(description = "Multiplies two numbers")
    public int multiply(
            @LlmTool.Param(description = "First number") int a,
            @LlmTool.Param(description = "Second number") int b) {
        return a * b;
    }
}

// 从一个实例上所有被注解的方法创建工具
List<Tool> mathTools = Tool.fromInstance(new MathService());

// 或者安全地创建工具(若未发现任何注解则返回空列表)
List<Tool> tools = Tool.safelyFromInstance(someObject);

@LlmTool 注解支持:

  • name —— 工具名(留空则默认用方法名);
  • description —— 该工具做什么的描述(必填);
  • returnDirect —— 是否直接返回结果、不再交给 LLM 进一步处理。

@LlmTool.Param 注解支持:

  • description —— 参数描述(帮助 LLM 理解该提供什么);
  • required —— 该参数是否必填(默认 true)。

通过 PromptRunner 加入框架无关工具

用 withTool() 或 withTools() 把框架无关工具加入 PromptRunner:

java
// 加入单个工具
Tool calculatorTool = Tool.create("calculate", "Performs calculations",
        (input) -> Tool.Result.text("Result: 42"));

context.ai()
        .withDefaultLlm()
        .withTool(calculatorTool)
        .createObject("Calculate 6 * 7", MathResult.class);

// 加入来自注解方法的工具
List<Tool> mathTools = Tool.fromInstance(new MathService());

context.ai()
        .withDefaultLlm()
        .withTools(mathTools)
        .createObject("Add 5 and 3", MathResult.class);

// 与其它工具来源组合
context.ai()
        .withDefaultLlm()
        .withToolGroup(CoreToolGroups.WEB)  // 工具组
        .withToolObject(domainObject)       // Spring AI @Tool 方法
        .withTools(mathTools)               // 框架无关工具
        .createObject("Research and calculate", Report.class);

工具结果(Tool Results)

工具返回 Tool.Result,它可以是三种类型之一:

java
// 文本结果(最常见)
Tool.Result.text("The answer is 42");

// 带 artifact 的结果(例如生成的文件、图片)
Tool.Result.withArtifact("Generated report", reportBytes);

// 错误结果
Tool.Result.error("Failed to process request", exception);

修改工具描述

工具提供 withDescription() 与 withNote() 方法,用于创建描述被修改过的副本。 当你需要在特定上下文中定制某个工具的描述、又不想改动原工具时,这很有用。

withDescription(newDescription)

创建一个完全替换描述的新工具:

java
// 替换整个描述
Tool customTool = originalTool.withDescription("Custom description for this context");

// 原工具保持不变
System.out.println(originalTool.getDefinition().getDescription()); // 原描述
System.out.println(customTool.getDefinition().getDescription());   // Custom description for this context

withNote(note)

创建一个在现有描述后追加一段注记的新工具:

java
// 在现有描述后加一条注记
Tool annotatedTool = originalTool.withNote("Use this when querying large datasets");

// 结果:"Original description. Use this when querying large datasets"
System.out.println(annotatedTool.getDefinition().getDescription());

两个方法都会保留工具的其它所有属性(name、input schema、metadata、功能):

java
Tool original = Tool.create("calculator", "Performs calculations",
        Tool.InputSchema.of(Tool.Parameter.integer("x", "Number")),
        input -> Tool.Result.text("42"));

// 创建一个定制版本
Tool customized = original
        .withDescription("Specialized math tool")
        .withNote("Optimized for financial calculations");

// 名称与功能保持不变
assert customized.getDefinition().getName().equals("calculator");
assert customized.call("{}").text().equals("42");

该用哪种方式

方式适用场景
Spring AI @Tool你熟悉 Spring AI,且希望在领域对象的工具注解上获得 IDE 支持
Tool.create() / Tool.of()你需要输入简单的程序化工具创建,想要框架独立性,或正在动态创建工具
Tool.fromFunction()你需要程序化创建、但输入输出是复杂类型,希望自动 JSON 编解码与 schema 生成
@LlmTool / @LlmTool.Param你偏好注解式工具,但想用 Embabel 的框架无关抽象
工具组(Tool Groups)你需要组织相关工具、使用 MCP 服务器,或在部署期控制工具可用性

工具装饰:扩展工具行为

Embabel 使用一套强大的装饰(decoration)模式来扩展工具行为, 既不改动底层工具,也不让 PromptRunner 变复杂。 被装饰的工具包裹另一个工具并拦截调用,以添加诸如 artifact 捕获、事件发布、黑板集成等功能。

这个模式是 Embabel 架构的基石:

  • Subagent 用装饰把 Agent 执行包装成一个工具;
  • 资产追踪用装饰捕获工具输出,供聊天机器人界面使用;
  • 黑板发布用装饰让工具结果对其余 action 可用;
  • 事件流用装饰把工具调用发布到外部系统;
  • 可观测性、异常处理等平台内部特性同样使用装饰。

DelegatingTool 接口

所有工具装饰器都实现 DelegatingTool:

java
public interface DelegatingTool extends Tool {
    Tool getDelegate();
}

这允许在需要时解开装饰器,并支持链式叠加多个装饰器。

ArtifactSinkingTool:捕获工具输出

ArtifactSinkingTool 从 Tool.Result.WithArtifact 结果中捕获 artifact,并把它们发送到 sink。 这是"让结构化工具输出在别处可用"的基础。

java
// 捕获所有 artifact 并发布到黑板
Tool wrapped = Tool.publishToBlackboard(myTool);

// 只捕获特定类型
Tool wrapped = Tool.publishToBlackboard(myTool, SearchResult.class);

// 带过滤与变换
Tool wrapped = Tool.publishToBlackboard(
        myTool,
        SearchResult.class,
        result -> result.getScore() > 0.5, // 过滤
        result -> result.getDocument()     // 变换
);

// 捕获到自定义 sink
Tool wrapped = Tool.sinkArtifacts(myTool, SearchResult.class, mySink);

内建 Sink

Embabel 提供若干 ArtifactSink 实现:

Sink用途
BlackboardSink发布到当前 AgentProcess 的黑板,使 artifact 对其余 action 可用
ListSink把 artifact 收集到一个列表,适合聚合结果
CompositeSink委托给多个 sink,实现多目的地发布

创建自定义 Sink

实现 ArtifactSink 即可创建自定义目的地:

java
// 发布到事件流
ArtifactSink eventSink = artifact -> {
    eventPublisher.publish(new ToolArtifactEvent(artifact));
};

// 与任何工具一起使用
Tool wrapped = Tool.sinkArtifacts(myTool, MyType.class, eventSink);

装饰如何支撑扩展

装饰模式让 Embabel 能添加复杂行为,同时让 PromptRunner 保持简单。 当你使用 Subagent.ofClass(MyAgent.class)(Java)或 Subagent.ofClass(MyAgent::class.java)(Kotlin)时, Embabel 创建的工具会:

  • 把 Agent 执行包装进一个 Tool.call() 方法;
  • 与子进程共享父黑板;
  • 把 Agent 的结果捕获为工具 artifact。

类似地,当你在聊天机器人中配置资产追踪时, Embabel 会用 AssetAddingTool 包装工具,把输出捕获为可查看的资产。

这种做法的关键优势:

  • 可组合:多个装饰器可以链式叠加;
  • 透明:底层工具并不知道自己被包装了;
  • 可扩展:新行为无需改动框架即可加入;
  • 类型安全:像 ArtifactSinkingTool 这样的泛型装饰器会保留类型信息。

Subagent:把 Agent 交接变成工具

Subagent 是一种特殊的 Tool,它委托给另一个 Embabel Agent。 当 LLM 调用这个工具时,它会把指定的 Agent 作为子进程运行,并共享父进程的黑板上下文。 这让 Agent 之间可以组合,实现"交接(handoff)"模式 —— 一个 Agent 把专门任务委派给另一个 Agent。

创建 Subagent

Subagent 使用流式 builder 模式。 先选择如何引用目标 Agent,然后用 consuming() 指定输入类型:

java
// 从一个 @Agent 注解的类(最常见)
Subagent.ofClass(ConcertAssembler.class).consuming(ConcertPlan.class)

// 按 Agent 名字(运行时从平台解析)
Subagent.byName("ConcertAssembler").consuming(ConcertPlan.class)

// 从已经解析好的 Agent 实例
Subagent.ofInstance(resolvedAgent).consuming(ConcertPlan.class)

// 从 @Agent 注解类的实例(例如一个 Spring bean)
Subagent.ofAnnotatedInstance(myAgentBean).consuming(ConcertPlan.class)

consuming() 方法指定 LLM 在调用该工具时会提供的输入类型。 这个类型被用来生成引导 LLM 调用工具的 JSON schema。

在 PromptRunner 中使用 Subagent

用 withTool() 把 Subagent 加进提示词:

java
@Action
public Concert assembleConcert(ConcertPlan plan, OperationContext context) {
    return context.ai()
            .withDefaultLlm()
            .withTool(Subagent.ofClass(PerformanceFinder.class)
                    .consuming(WorksToFind.class)) // ①
            .creating(Concert.class)
            .fromPrompt("Assemble a concert based on: " + plan);
}

① 现在 LLM 可以把 PerformanceFinder 当工具调用,提供 WorksToFind 输入来委派演出查找任务。

带资产追踪的 Subagent

对于需要追踪资产的聊天应用,用 AssetAddingTool 包一层 Subagent,自动追踪返回的 artifact:

java
@Action
public Concert assembleConcert(ConcertPlan plan, OperationContext context) {
    var subagent = Subagent.ofClass(PerformanceFinder.class)
            .consuming(WorksToFind.class);
    var trackedSubagent = assetTracker.addReturnedAssets(subagent); // ①

    return context.ai()
            .withDefaultLlm()
            .withTool(trackedSubagent)
            .creating(Concert.class)
            .fromPrompt("Assemble a concert based on: " + plan);
}

// 带过滤 —— 只追踪特定资产
var trackedSubagent = assetTracker.addReturnedAssets(subagent, asset ->
        asset instanceof Performance // 只追踪 Performance 资产
);

① 用 addReturnedAssets() 包装,以追踪该 Subagent 返回的 artifact。

输入类型与 JSON Schema

你用 consuming() 指定的输入类型决定了 LLM 调用该工具时看到的 JSON schema。

例如:

java
// 输入类型
public record WorksToFind(List<String> composers, String era, int maxResults) {}

// 用显式输入类型创建 subagent
Subagent.ofClass(PerformanceFinder.class).consuming(WorksToFind.class)

Subagent 工具会:

  • 用 "PerformanceFinder" 作为工具名(来自 @Agent 注解);
  • 用 "Finds performances" 作为工具描述(来自 @Agent 注解);
  • 从 WorksToFind 生成 JSON schema。

从 LLM 的视角看,Subagent 就只是另一个工具。 调用方的 LLM 看到 WorksToFind 的 JSON schema,可以直接填充:

json
{
  "composers": ["Mozart", "Beethoven"],
  "era": "Classical",
  "maxResults": 5
}

工具被调用时,Subagent 会把这段 JSON 反序列化成 WorksToFind 对象并传给目标 Agent。 输入类型应当与该 Agent 入口 action 的第一个非注入参数相匹配。

黑板共享

Subagent 运行时,它从父进程获得一个派生出来的黑板。这意味着:

  • Subagent 可以读取父进程黑板上的对象;
  • Subagent 添加的对象在它完成后对父进程可用;
  • Subagent 运行在自己的进程上下文中,但会恰当地共享状态。

Verbosity(详细程度)继承

Subagent 自动继承其父 AgentProcess 的 verbosity 设置。 若父进程以 showPrompts = true 运行,Subagent 也会显示提示词; 若父进程安静运行,Subagent 也安静。

Subagent 上没有独立的 verbosity 设置 —— 它精确镜像父级的 ProcessOptions.verbosity。 要控制 Subagent 的 verbosity,就在顶层 ProcessOptions 上设置一次:

java
// 安静的父 → 安静的子
var processOptions = new ProcessOptions()
        .withVerbosity(new Verbosity());

// 啰嗦的父 → 啰嗦的子(两处都能看到提示词)
var processOptions = new ProcessOptions()
        .withVerbosity(new Verbosity().withShowPrompts(true));

GoalTool 与 AgentTool 是入口点 —— 它们默认 showPrompts = true,且不从任何父级继承。 要覆写它们的 verbosity,请通过 GoalTool.withVerbosity() 或 AgentTool 上的 processOptionsCreator lambda 传入自定义 Verbosity。

什么时候该用 Subagent

场景建议
有自己多 action 工作流的复杂专门任务用 Subagent —— 目标 Agent 可以规划并执行多个步骤
逻辑确定的简单工具调用改用普通的 @LlmTool 方法
带子工具的 LLM 编排型迷你工作流考虑 AgenticTool —— 它工作在工具层
子任务需要 GOAP 规划的全部能力Subagent 最理想 —— 目标 Agent 会用自己的规划器

Agentic Tools(智能体工具)

agentic tool 是一个用 LLM 来编排其它工具的工具。 与执行确定性逻辑的普通工具不同,agentic tool 委托给一个 LLM, 由它基于提示词决定调用哪些子工具。

这个模式适合把一段小型编排封装成单个工具,再用于更大的工作流。

Embabel 提供三种 agentic tool 实现,它们在工具可用性的控制粒度上各不相同:

如何选用

工具类型工具可用性最适合示例用例
SimpleAgenticTool所有工具立即可用简单编排、探索型任务带 add/multiply/divide 的计算器
PlaybookTool通过条件逐步解锁(前置工具、artifact、黑板)结构化工作流、引导式流程研究工作流:搜索 → 分析 → 总结
StateMachineTool基于枚举状态的可用性形式化状态机、多阶段流程订单处理:DRAFT → CONFIRMED → SHIPPED → DELIVERED

三者都实现 AgenticTool 接口,并共享一套带 with* 方法的流式 API。

AgenticTool 接口定义:

java
public interface AgenticTool<THIS extends AgenticTool<THIS>> extends Tool {
    LlmOptions getLlm();        // LLM 配置
    int getMaxIterations();     // 工具循环最大迭代次数(默认:20)

    THIS withLlm(LlmOptions llm);
    THIS withSystemPrompt(String prompt);
    THIS withSystemPrompt(AgenticSystemPromptCreator creator); // 动态提示词
    THIS withMaxIterations(int maxIterations);
    THIS withParameter(Tool.Parameter parameter);
    THIS withToolObject(Object toolObject);
}

AgenticSystemPromptCreator 这个函数式接口同时接收 ExecutingOperationContext(用于访问黑板、进程选项等)与传给该工具的输入字符串:

java
tool.withSystemPrompt((ctx, input) ->
        "Context: " + ctx.getProcessContext().getProcessOptions().getContextId() +
                ". Task: " + input
);

对于有确定输出、分支逻辑、循环或状态管理的复杂工作流, 请改用 Embabel 的 GOAP 规划器、Utility AI 或 @State 工作流。 它们提供确定性、类型安全的规划,比 LLM 驱动的编排强大得多、也可预测得多。

SimpleAgenticTool:扁平的工具编排

SimpleAgenticTool 让所有子工具立即可用,LLM 基于提示词自由决定使用哪些工具。

java
import com.embabel.agent.api.tool.agentic.simple.SimpleAgenticTool;

// 创建该 agentic tool
SimpleAgenticTool mathOrchestrator = new SimpleAgenticTool("math-orchestrator", "Orchestrates math operations")
        .withTools(addTool, multiplyTool, divideTool)
        .withParameter(Tool.Parameter.string("expression", "Math expression to evaluate"))
        .withLlm(LlmOptions.withModel("gpt-4"));

// 像其它工具一样使用它
context.ai()
        .withDefaultLlm()
        .withTool(mathOrchestrator)
        .generateText("What is 5 + 3 * 2?");

PlaybookTool:条件化解锁工具

PlaybookTool 允许工具基于条件逐步解锁:

  • 前置工具(Prerequisites):在其它工具被调用后解锁;
  • Artifact:当产生某类 artifact 时解锁;
  • 黑板(Blackboard):基于进程状态解锁;
  • 自定义谓词:基于任意条件解锁。
java
import com.embabel.agent.api.tool.agentic.playbook.PlaybookTool;

// 工具逐步解锁
PlaybookTool researcher = new PlaybookTool("researcher", "Research and analyze topics")
        .withTools(searchTool, fetchTool)               // 始终可用
        .withTool(analyzeTool).unlockedBy(searchTool)    // search 之后解锁
        .withTool(summarizeTool).unlockedBy(analyzeTool) // analyze 之后解锁
        .withParameter(Tool.Parameter.string("topic", "Research topic"));

// 多个前置条件(AND)
.withTool(reportTool).unlockedByAll(searchTool, analyzeTool)

// 任意一个前置条件(OR)
.withTool(processTool).unlockedByAny(searchTool, fetchTool)

// 当某类 artifact 被产出时解锁
.withTool(formatTool).unlockedByArtifact(Document.class)

// 基于黑板状态解锁
.withTool(actionTool).unlockedByBlackboard(UserProfile.class)

// 自定义谓词
.withTool(finalizeTool).unlockedWhen(ctx -> ctx.getIterationCount() >= 3)

当 LLM 在条件尚未满足时调用了被锁定的工具,它会收到一条信息性消息,引导它先去用前置工具。

StateMachineTool:基于状态的可用性

StateMachineTool 使用由枚举定义的显式状态。 工具被注册到它们可用的特定状态上,并且可以触发向其它状态的迁移。

java
import com.embabel.agent.api.tool.agentic.state.StateMachineTool;

enum OrderState { DRAFT, CONFIRMED, SHIPPED, DELIVERED }

StateMachineTool<OrderState> orderProcessor = new StateMachineTool<>("orderProcessor", "Process orders", OrderState.class)
        .withInitialState(OrderState.DRAFT)
        .inState(OrderState.DRAFT)
        .withTool(addItemTool)
        .withTool(confirmTool).transitionsTo(OrderState.CONFIRMED)
        .inState(OrderState.CONFIRMED)
        .withTool(shipTool).transitionsTo(OrderState.SHIPPED)
        .inState(OrderState.SHIPPED)
        .withTool(deliverTool).transitionsTo(OrderState.DELIVERED)
        .inState(OrderState.DELIVERED)
        .withTool(reviewTool).build()
        .withGlobalTools(statusTool, helpTool) // 所有状态下都可用
        .withParameter(Tool.Parameter.string("orderId", "Order to process"));

startingIn(state) 方法允许在运行时从另一个状态开始:

java
// 恢复一个已经被确认的订单
Tool resumedProcessor = orderProcessor.startingIn(OrderState.CONFIRMED);

领域工具:来自被检索对象的工具

三种 agentic tool 都支持领域工具 —— 也就是放在领域对象上的 @LlmTool 方法,当该类型的单个实例被检索出来时它们就变得可用。

java
// 带 @LlmTool 方法的领域类
public class User {
    private final String id;
    private final String name;

    public User(String id, String name) {
        this.id = id;
        this.name = name;
    }

    @LlmTool(description = "Get user's profile information")
    public String getProfile() {
        return "Profile for " + name;
    }

    @LlmTool(description = "Update user's settings")
    public String updateSettings(String settings) {
        return "Settings updated for " + name;
    }
}

// 注册领域工具 —— 当检索出单个 User 时它们变得可用
PlaybookTool userManager = new PlaybookTool("userManager", "Manage users")
        .withTools(searchUserTool, getUserTool)
        .withToolChainingFrom(User.class); // getUserTool 返回单个 User 后,User 的方法可用

领域工具会立刻"声明"给 LLM,但在实例被绑定之前调用会返回错误。 当某个工具返回了已注册类型的一个 artifact(而非集合)时, 该实例被绑定,它的 @LlmTool 方法随即变为可执行。

创建 Agentic Tools

用构造器加上流式 with* 方法创建 agentic tool:

java
// 创建子工具
Tool addTool = Tool.create("add", "Adds two numbers", input -> {
    // 解析 JSON 输入并计算结果
    return Tool.Result.text("5");
});

Tool multiplyTool = Tool.create("multiply", "Multiplies two numbers", input -> {
    return Tool.Result.text("6");
});

// 创建该 agentic tool
SimpleAgenticTool mathOrchestrator = new SimpleAgenticTool("math-orchestrator", "Orchestrates math operations")
        .withTools(addTool, multiplyTool)
        .withLlm(LlmOptions.withModel("gpt-4"))
        .withSystemPrompt("Use the available tools to solve the given math problem");

// 像其它工具一样使用它
context.ai()
        .withDefaultLlm()
        .withTool(mathOrchestrator)
        .generateText("What is 5 + 3 * 2?");

withSystemPrompt 调用是可选的。

默认情况下,agentic tool 会从工具描述生成系统提示词: "You are an intelligent agent that can use tools to help you complete tasks. Use the provided tools to perform the following task: {description}"。 只有在你需要自定义编排指令时才调用 withSystemPrompt。

定义输入参数

你必须为 agentic tool 定义输入参数,这样 LLM 才知道调用它时该传什么实参。

没有参数的话,LLM 就不知道用什么输入格式。

用 withParameter 方法配合 Tool.Parameter 工厂方法做简洁的参数定义:

java
// 需要 topic 参数的研究工具
SimpleAgenticTool researcher = new SimpleAgenticTool("researcher", "Research a topic thoroughly")
        .withParameter(Tool.Parameter.string("topic", "The topic to research"))
        .withToolObjects(new SearchTools(), new SummarizerTools());

// 带多个参数的计算器
SimpleAgenticTool calculator = new SimpleAgenticTool("smart-calculator", "Perform complex calculations")
        .withParameter(Tool.Parameter.string("expression", "Mathematical expression to evaluate"))
        .withParameter(Tool.Parameter.integer("precision", "Decimal places for result", false)) // 可选
        .withToolObject(new MathTools());

可用的参数工厂方法:

  • Tool.Parameter.string(name, description, required?) —— 字符串参数;
  • Tool.Parameter.integer(name, description, required?) —— 整数参数;
  • Tool.Parameter.double(name, description, required?) —— 浮点参数。

所有工厂方法默认 required = true。 可选参数请设 required = false。

从注解对象创建 Agentic Tools

用 withToolObject 或 withToolObjects 把带 @LlmTool 注解方法的对象上的工具加进来:

java
// 带 @LlmTool 方法的工具类
public class SearchTools {
    @LlmTool(description = "Search the web")
    public String search(String query) { return "Results for: " + query; }
}

public class CalculatorTools {
    @LlmTool(description = "Add two numbers")
    public int add(int a, int b) { return a + b; }

    @LlmTool(description = "Multiply two numbers")
    public int multiply(int a, int b) { return a * b; }
}

// 用来自多个对象的工具创建 agentic tool
// 使用基于 description 的默认系统提示词
SimpleAgenticTool assistant = new SimpleAgenticTool("assistant", "Multi-capability assistant")
        .withToolObjects(new SearchTools(), new CalculatorTools());

// 带 LLM 选项与自定义系统提示词
SimpleAgenticTool smartAssistant = new SimpleAgenticTool("smart-assistant", "Smart assistant")
        .withToolObjects(new SearchTools(), new CalculatorTools())
        .withLlm(LlmOptions.withModel("gpt-4"))
        .withSystemPrompt("Use tools intelligently");

没有 @LlmTool 方法的对象会被静默忽略,因此你可以安全地混合传入。

带 Spring 依赖注入的 Agentic Tools

Agentic tool 可以通过依赖注入封装有状态的服务:

java
@Component
public class ResearchOrchestrator {

    private final WebSearchService webSearchService;
    private final SummarizerService summarizerService;

    public ResearchOrchestrator(WebSearchService webSearchService, SummarizerService summarizerService) {
        this.webSearchService = webSearchService;
        this.summarizerService = summarizerService;
    }

    @LlmTool(description = "Search the web for information")
    public List<SearchResult> search(String query) {
        return webSearchService.search(query);
    }

    @LlmTool(description = "Summarize text content")
    public String summarize(String content) {
        return summarizerService.summarize(content);
    }
}

// 在你的配置中
@Configuration
public class ToolConfiguration {

    @Bean
    public SimpleAgenticTool researchTool(ResearchOrchestrator orchestrator) {
        return new SimpleAgenticTool("research-assistant", "Research topics using web search and summarization")
                .withToolObject(orchestrator)
                .withLlm(new LlmOptions().withRole("smart"));
        // 使用基于 description 的默认系统提示词
    }
}

Agentic Tools 是如何执行的

当 agentic tool 的 call() 方法被调用时:

  • 该工具取得当前的 AgentProcess 上下文;
  • 它用指定的 LlmOptions 配置一个 PromptRunner;
  • 它把所有子工具加进该 prompt runner;
  • 它用输入执行提示词,让 LLM 去编排这些子工具;
  • 最终 LLM 的响应作为工具结果返回。

这意味着 agentic tool 会制造一次嵌套的 LLM 交互: 外层 LLM 决定调用该 agentic tool,内层 LLM 再去编排子工具。

修改 Agentic Tools

用 with* 方法创建修改过的副本:

java
SimpleAgenticTool base = new SimpleAgenticTool("base", "Base orchestrator")
        .withTools(tool1)
        .withSystemPrompt("Original prompt");

// 创建带修改的副本
SimpleAgenticTool withNewLlm = base.withLlm(new LlmOptions().withModel("gpt-4"));
SimpleAgenticTool withMoreTools = base.withTools(tool2, tool3);
SimpleAgenticTool withNewPrompt = base.withSystemPrompt("Updated prompt");

// 加入输入参数
SimpleAgenticTool withParams = base.withParameter(Tool.Parameter.string("query", "Search query"));

// 加入来自带 @LlmTool 方法对象的工具
SimpleAgenticTool withAnnotatedTools = base.withToolObject(calculatorService);

// 加入来自多个对象的工具
SimpleAgenticTool withMultipleObjects = base.withToolObjects(searchService, calculatorService);

// 基于执行上下文与输入动态生成系统提示词
SimpleAgenticTool withDynamicPrompt = base.withSystemPrompt((ctx, input) -> {
    String contextId = ctx.getProcessContext().getProcessOptions().getContextId().getId();
    return "Process requests for context " + contextId + ". Task: " + input;
});

可用的修改方法:

  • withParameter(Tool.Parameter) —— 加入一个输入参数(用 Tool.Parameter.string()、.integer()、.double());
  • withLlm(LlmOptions) —— 设置 LLM 配置;
  • withTools(vararg Tool) —— 加入额外的 Tool 实例;
  • withToolObject(Any) —— 从带 @LlmTool 方法的对象加入工具;
  • withToolObjects(vararg Any) —— 从多个注解对象加入工具;
  • withSystemPrompt(String) —— 设置固定的系统提示词;
  • withSystemPrompt((ExecutingOperationContext, String) -> String) —— 基于执行上下文与输入设置动态提示词;
  • withCaptureNestedArtifacts(Boolean) —— 控制是否捕获嵌套 agentic tool 调用产生的 artifact(默认 false);
  • withToolChainingFrom(Class) —— 注册一个类,当返回该类型的 artifact 时,它的 @LlmTool 方法变为可用;
  • withToolChainingFrom(Class, DomainToolPredicate) —— 带谓词注册,以过滤哪些实例贡献工具;
  • withToolChainingFromAny() —— 自动从任何带 @LlmTool 方法的返回 artifact 中发现工具。

控制嵌套 Agentic Tool 的 Artifact 捕获

当一个 agentic tool 编排其它工具时,那些子工具可能返回 artifact(通过 Tool.Result.WithArtifact)。 默认情况下,嵌套 agentic tool 调用产生的 artifact 不会被捕获 —— 只有最外层 agentic tool 的最终结果会被返回。

这避免了在你只关心最终结果时中间 artifact 冒泡上来。 例如,若外层 assembleConcert 工具调用了内层 findPerformances 工具, 通常你只想要最终的 Concert artifact,而不是所有中间的 Performance artifact。

如果你需要捕获嵌套 agentic tool 的 artifact,请用 withCaptureNestedArtifacts(true):

java
// 默认:嵌套 artifact 不会被捕获
SimpleAgenticTool concertAssembler = new SimpleAgenticTool("assembleConcert", "Assemble a concert program")
        .withTools(findPerformancesTool, createConcertTool);
// 只会返回 createConcert 的 Concert artifact

// 选择加入:捕获所有嵌套 artifact
SimpleAgenticTool fullCapture = concertAssembler.withCaptureNestedArtifacts(true);
// findPerformances 的 Performance artifact 与 createConcert 的 Concert 都会被捕获

⚠️ 该设置只影响嵌套 agentic tool 调用产生的 artifact。 来自普通(非 agentic)工具的 artifact 始终会被捕获。

工具链(Tool Chaining)

处理工具返回的对象时,你往往希望把这些对象上的 @LlmTool 方法作为额外工具暴露出来 —— 但只在对象被检索到之后。withToolChainingFrom() 方法正是为此。

工具链能提升确定性。 一旦某个工具返回了特定对象,LLM 就获得该对象的业务方法 —— 通过定义良好的操作在数据结构中穿行,而不是靠非结构化推理。 这让 LLM 沿着你的领域逻辑走在一条受引导的路径上。

工具链在 AgenticTool 与 PromptRunner 上都可用,经由共享的 ToolChaining 接口。 这意味着你不仅能在 agentic tool 循环里用工具链, 也能在 PromptRunner 的简单 createObject 与 generateText 调用里用。 这一点很重要:它让任何 action 都能动态发现并使用返回 artifact 上的工具,无需搭一整套 agentic tool。

当你注册一个类时,会为该类上的每个 @LlmTool 方法创建占位工具。 初始时这些工具返回 "not available yet" 消息。 当某个工具返回了匹配已注册类型的 artifact 时,占位工具变为活跃,并委托给被绑定的实例。

后来者胜(Last Wins Semantics):当同类型的多个 artifact 被返回时, 只有最近那个实例的工具处于活跃状态。这确保 LLM 始终操作"当前"的实例。

java
// 带工具方法的领域类
public class User {
    private final String id;
    private String email;

    @LlmTool("Update the user's email address")
    public String updateEmail(String newEmail) {
        this.email = newEmail;
        return "Email updated to " + newEmail;
    }
}

// 创建带工具链的 agentic tool
SimpleAgenticTool userManager = new SimpleAgenticTool("userManager", "Manage user accounts")
        .withTools(searchUserTool, getUserTool) // 用于查找/检索用户的工具
        .withToolChainingFrom(User.class);      // 检索出 User 后,User 的方法成为工具

// 流程:
// 1. LLM 调用 searchUserTool 找到用户
// 2. LLM 调用 getUserTool,返回一个 User artifact
// 3. updateEmail() 变为绑定到该 User 的可用工具
// 4. LLM 调用 updateEmail("new@example.com")

基于谓词的过滤

你可以用谓词控制哪些实例贡献工具。 该谓词接收 artifact 与当前 AgentProcess,从而支持基于对象状态或进程上下文做过滤。

java
// 只为管理员用户暴露工具
SimpleAgenticTool adminManager = new SimpleAgenticTool("adminManager", "Manage admin users")
        .withTools(searchUserTool, getUserTool)
        .withToolChainingFrom(User.class, (user, agentProcess) ->
                user.getRole().equals("admin")
        );

// 普通用户的工具不会被暴露
// 只有当检索出一个 admin User 时,updateEmail() 才会变得可用

自动发现模式

为了最大灵活性,可用 withToolChainingFromAny() 自动发现并暴露任何返回 artifact 上带 @LlmTool 方法的工具。 与"注册来源"不同,自动发现在新 artifact 被发现时会替换掉所有先前的绑定 —— 确保同一时间只有一个"当前"对象的工具处于活跃状态。

java
// 从任何返回对象自动发现工具
SimpleAgenticTool explorer = new SimpleAgenticTool("explorer", "Explore and manipulate objects")
        .withTools(searchTool, getTool)
        .withToolChainingFromAny(); // 任何返回对象上的工具都会被暴露

// 流程:
// 1. LLM 调用 getTool,返回一个 User -> User 的工具可用
// 2. LLM 再调用 getTool,返回一个 Order -> Order 的工具替换掉 User 的工具
// 3. 只有最近那个对象的工具是活跃的

这个模式适用于:

  • 对象自带操作:对象自己知道如何执行动作(如 user.updateEmail()、order.cancel());
  • 上下文相关的工具:只有在检索到某个特定实例后,这些操作才有意义;
  • 更干净的 API 设计:工具定义在类本身上,而不是拆成单独的工具类;
  • 探索型工作流:LLM 动态地与那个"当前"对象打交道。

所有 agentic tool 类型都支持工具链:

  • SimpleAgenticTool:artifact 一被返回,链式工具即可用;
  • PlaybookTool:链式工具立即可用(不受解锁条件约束);
  • StateMachineTool:链式工具全局可用(不绑定状态)。

PromptRunner 上的工具链

工具链并不限于 agentic tool。 由于 AgenticTool 与 PromptRunner 都实现了 ToolChaining 接口, 你可以直接在从 action 的 OperationContext 拿到的 PromptRunner 上使用 withToolChainingFrom() 与 withToolChainingFromAny()。

这很重要:它让简单的 createObject 与 generateText 调用也能做动态工具发现 —— 不需要包一层完整的 SimpleAgenticTool。

java
// 在一个 @Action 方法中:
PromptRunner ai = context.ai()
        .withToolChainingFrom(User.class) // 来自 User 的链式工具
        .withTools(searchUserTool, getUserTool);

// 当 getUserTool 返回一个 User artifact 时,
// User 的 @LlmTool 方法会自动变为 LLM 可调用的工具
String result = ai.generateText("Find user Alice and update her email to alice@new.com");

为资产追踪过滤 Artifact

在把工具与 AssetTracker 搭配使用时(聊天应用常见), 你可以过滤哪些 artifact 会成为被追踪的资产。 addReturnedAssets 与 addAnyReturnedAssets 方法接受一个 Predicate<Asset> 过滤器, 在 Java 与 Kotlin 中都可用:

java
// 只追踪通过过滤器的资产
Tool wrapped = assetTracker.addReturnedAssets(concertTool, asset -> {
    // 只追踪至少有 3 部作品的音乐会
    return asset instanceof Concert concert && concert.getWorks().size() >= 3;
});

// 把同一个过滤器应用到多个工具
List<Tool> wrappedTools = assetTracker.addAnyReturnedAssets(
        List.of(tool1, tool2, tool3),
        asset -> asset.getId().startsWith("important-")
);

过滤器在类型匹配之后应用,因此你可以用类型特有的判据来决定哪些 artifact 值得追踪。

从其它框架迁移过来

如果你来自 LangChain 或 Google ADK 这类框架, Embabel 的 agentic tool 提供了一个与它们的 "supervisor" 架构相似的熟悉模式:

框架模式Embabel 对应物
LangChain / LangGraphSupervisor agent + worker agentsSimpleAgenticTool + 子工具
Google ADKCoordinator + sub_agents / AgentToolSimpleAgenticTool + 子工具

关键差异:

  • 以工具为中心:Embabel 的 agentic tool 工作在工具层,而非 Agent 层。 它们轻量,可以与普通工具自由混用;
  • 模型更简单:没有基于图的工作流,也没有显式的 Sequential/Parallel/Loop 模式 —— 只有 LLM 驱动的编排;
  • 可组合:agentic tool 依然"只是一个工具",凡接受工具之处都可使用。

不过,只要超出简单编排的范围,Embabel 提供了强大得多的替代方案:

场景应该改用
有确定输出的业务流程GOAP 规划器 —— 带前置条件与效果、确定性的目标导向规划
探索型与事件驱动系统Utility AI —— 每一步选择价值最高的动作
分支、循环或有状态的工作流@State 工作流 —— 类型安全的状态机,每个状态内用 GOAP 规划

它们提供确定性、类型安全的规划,比 supervisor 风格的 LLM 编排可预测得多、也强大得多。 简单场景用 SimpleAgenticTool,结构化工作流用 PlaybookTool,形式化状态机用 StateMachineTool。 当可预测性重要时,生产工作流请升级到 GOAP、Utility 或 @State。

关于带类型化输出与完整黑板状态管理的 supervisor 风格编排,见 SupervisorInvocation。 它工作在比 agentic tool 更高的层级 —— 编排的是 @Action 方法而非 Tool 实例, 并产出带柯里化(currying)支持的类型化目标对象。

渐进式工具(Progressive Tools)

Great fleas have little fleas upon their backs to bite 'em, And little fleas have lesser fleas, and so ad infinitum. And the great fleas themselves, in turn, have greater fleas to go on; While these again have greater still, and greater still, and so on.

—— Augustus De Morgan

渐进式工具实现动态工具披露(dynamic tool disclosure) —— 先呈现一个简化的接口,然后基于上下文、或当 LLM 表达出意图时,再揭示更细粒度的工具。

渐进式工具的层级

Embabel 提供一组渐进式工具接口的层级:

  • ProgressiveTool:可以基于上下文揭示内部工具的工具的基础接口。 它的 innerTools(process: AgentProcess) 方法返回的工具可能随当前 Agent 进程状态而变化;
  • UnfoldingTool:一个拥有固定内部工具集合的 ProgressiveTool。 被调用时它**"展开"以揭示其内容 —— 就像打开一张折叠的地图看里面的细节。 这是最常用的**渐进式工具类型。

UnfoldingTool 向 LLM 呈现一个高层描述,被调用时暴露其内部工具。 这个模式适合渐进式工具披露 —— 降低初始复杂度,同时允许按需访问细节功能。

什么时候该用 UnfoldingTool

UnfoldingTool 适用于:

  • 你有许多相关工具,它们可能用太多选择淹没 LLM;
  • 你想按类别组织工具(例如"数据库操作"、"文件操作");
  • 你想让 LLM 先表达意图,再揭示细节选项;
  • 你需要减少工具描述占用的 token。

创建一个简单的 UnfoldingTool

最简单的形式是:被调用时暴露所有内部工具。

java
import com.embabel.agent.api.tool.progressive.UnfoldingTool;
import com.embabel.agent.api.tool.Tool;

// 创建内部工具
Tool queryTool = Tool.create("query_table", "Execute a SQL query",
        Tool.InputSchema.of(Tool.Parameter.string("sql", "The SQL query to execute")),
        input -> Tool.Result.text("{\"rows\": 5}")
);

Tool insertTool = Tool.create("insert_record", "Insert a new record",
        Tool.InputSchema.of(Tool.Parameter.string("table", "Table name")),
        input -> Tool.Result.text("{\"id\": 123}")
);

Tool deleteTool = Tool.create("delete_record", "Delete a record",
        Tool.InputSchema.of(Tool.Parameter.integer("id", "Record ID to delete")),
        input -> Tool.Result.text("{\"deleted\": true}")
);

// 创建 UnfoldingTool 门面
var databaseTool = UnfoldingTool.of(
        "database_operations",
        "Use this tool to work with the database. Invoke to see specific operations.",
        List.of(queryTool, insertTool, deleteTool)
);

流式 Builder API

UnfoldingTool 支持流式 builder 模式,用于组合来自多个来源的工具。 用 withTools() 加入单个工具,或用 withToolObject() 加入来自 @LlmTool 注解对象的工具:

java
import com.embabel.agent.api.tool.progressive.UnfoldingTool;

// 从基础工具开始,继续添加
var combinedTools = UnfoldingTool.of(
                "workspace",
                "Workspace operations. Invoke to see available tools.",
                List.of(baseTool))
        .withTools(searchTool, filterTool)        // 加入单个工具
        .withToolObject(new DatabaseOperations()) // 从 @LlmTool 类加入
        .withToolObject(new FileOperations());    // 链式接入多个来源

这适用于:

  • 组合已有工具:把不同来源的工具合并到一个渐进式门面里;
  • 加入临时工具:从注解工具类出发,再添加程序化工具;
  • 按上下文分组:为不同的调用上下文构建不同的工具组合。

builder 会保留原 UnfoldingTool 的所有属性(childToolUsageNotes 等)。

基于类别的工具选择

用 byCategory 基于 LLM 选择的类别暴露不同的工具:

java
import com.embabel.agent.api.tool.progressive.UnfoldingTool;
import java.util.Map;

// 按类别定义工具
Map<String, List<Tool>> toolsByCategory = Map.of(
        "read", List.of(readFileTool, listDirectoryTool, searchFilesTool),
        "write", List.of(writeFileTool, deleteFileTool, moveFileTool)
);

// 创建基于类别的 UnfoldingTool
var fileTool = UnfoldingTool.byCategory(
        "file_operations",
        "File operations. Pass category: 'read' for reading files, 'write' for modifying files.",
        toolsByCategory
);

// 该工具的 schema 会自动把 category 作为枚举参数包含进去
// 以 {"category": "read"} 调用时,只暴露 read 工具
// 以 {"category": "write"} 调用时,只暴露 write 工具

自定义选择逻辑

更复杂的选择逻辑用 selectable:

java
import com.embabel.agent.api.tool.progressive.UnfoldingTool;
import com.fasterxml.jackson.databind.ObjectMapper;

List<Tool> allTools = List.of(basicTool, advancedTool, adminTool);

var permissionBasedTool = UnfoldingTool.selectable(
        "api_operations",
        "API operations. Pass 'accessLevel': 'basic', 'advanced', or 'admin'.",
        allTools,
        Tool.InputSchema.of(
                Tool.Parameter.string("accessLevel", "Access level for operations",
                        true, List.of("basic", "advanced", "admin"))
        ),
        true, // removeOnInvoke
        input -> {
            // 自定义选择逻辑
            try {
                ObjectMapper mapper = new ObjectMapper();
                Map<String, Object> params = mapper.readValue(input, Map.class);
                String level = (String) params.get("accessLevel");
                return switch (level) {
                    case "basic" -> List.of(basicTool);
                    case "advanced" -> List.of(basicTool, advancedTool);
                    case "admin" -> allTools;
                    default -> List.of(basicTool);
                };
            } catch (Exception e) {
                return List.of(basicTool);
            }
        }
);

引导工具(Guide Tool)行为

UnfoldingTool 被调用后,它会被它的内部工具加上一个与原门面同名的引导工具所替换。 如果 LLM 在后续回合里又调用了父工具名(一个常见的工具调用错误), 该引导工具会返回可用子工具的清单,而不是以 ToolNotFoundException 失败。

这个行为是自动的 —— 无需配置。 removeOnInvoke 属性已废弃并被忽略;引导工具替换始终生效。

在工具循环中启用 UnfoldingTool

使用 Embabel 的工具循环时,UnfoldingTool 默认启用。 ToolInjectionStrategy.DEFAULT 已包含 UnfoldingToolInjectionStrategy,因此无需额外配置。

如果你需要与自定义策略组合,用 ChainedToolInjectionStrategy:

java
import com.embabel.agent.spi.loop.ChainedToolInjectionStrategy;

// 把 UnfoldingTool 支持与自定义策略组合起来
ChainedToolInjectionStrategy combined =
        ChainedToolInjectionStrategy.withUnfolding(customStrategy1, customStrategy2);

UnfoldingTool 是如何工作的

  • 初始状态:LLM 只看到门面工具(例如 "database_operations");
  • LLM 调用:LLM 带上可选参数调用该门面;
  • 策略评估:UnfoldingToolInjectionStrategy 检测到这次调用;
  • 替换工具:门面被替换为一个引导工具,同时内部工具被加入;
  • 继续:LLM 现在能看到并使用具体的内部工具。

这个流程降低了初始工具集合的复杂度,同时让 LLM 在需要时能访问细节工具。

上下文保留与用法注记

当 UnfoldingTool 被展开时,它的子工具会替换掉门面。 若不保留上下文,LLM 就会丢失"这些工具为什么被归在一起"这一重要信息。

例如,一个包含 vector_search、text_search、regex_search 的 "spotify_search" 工具, 展开后只剩下三个泛化的搜索工具 —— LLM 不会知道它们专门用于搜索 Spotify 的音乐数据。

Embabel 通过在子工具旁自动注入一个上下文工具解决了这一点。该上下文工具:

  • 保留父级的描述(如"Search Spotify for music data");
  • 列出可用的子工具;
  • 包含可选的用法注记(通过 childToolUsageNotes)。

childToolUsageNotes 参数提供何时以及如何使用子工具的指引。 这些指引只在上下文工具里出现一次,而不必在每个子工具的描述里重复:

java
var spotifySearch = UnfoldingTool.of(
        "spotify_search",
        "Search Spotify for music data including artists, albums, and tracks.",
        List.of(vectorSearchTool, textSearchTool, regexSearchTool),
        true, // removeOnInvoke
        "Try vector search first for semantic queries like 'upbeat jazz'. " +
                "Use text search for exact artist or album names. " +
                "Use regex search for pattern matching on metadata."
);

LLM 调用 spotify_search 之后,它会看到:

  • vector_search —— 实际的搜索工具;
  • text_search —— 实际的搜索工具;
  • regex_search —— 实际的搜索工具;
  • spotify_search_context —— 带描述与用法注记的上下文工具。

上下文工具的描述包含原本的用途与可用工具。 被调用时,它返回每个子工具的完整细节外加用法注记 —— 提供单一的参考点,而不污染各个工具自己的描述。

独占模式(Exclusive Mode)

默认情况下,UnfoldingTool 展开时,它的内部工具会被加在已有同级工具旁边。 某些情况下 LLM 可能忽略内部工具、转而去挑一个同级工具,让展开变得毫无意义。

设置 exclusive = true 会在 UnfoldingTool 展开时移除所有其它工具, 于是 LLM 在本次交互结束前只能看到内部工具。 当 LLM 反复选错同级工具、而不用被揭示的内部工具时,就该用这个。

java
var personalityTool = UnfoldingTool.of(
        "change_personality",
        "Change the assistant's personality. Invoke to see personality options.",
        List.of(formalTool, casualTool, technicalTool),
        true,  // removeOnInvoke
        null,  // childToolUsageNotes
        true   // exclusive —— 展开后隐藏所有其它工具
);
  • exclusive 为 false(默认)时:父工具被其内部工具替换,所有同级工具仍然可用;
  • exclusive 为 true 时:当前工具集合中的每个工具都被移除,只注入内部工具。

基于注解的 UnfoldingTool

要更声明式的做法,使用 @UnfoldingTools 类注解配合 @LlmTool 方法注解:

java
import com.embabel.agent.api.annotation.UnfoldingTools;
import com.embabel.agent.api.annotation.LlmTool;

@UnfoldingTools(
        name = "database_operations",
        description = "Database operations. Invoke to see specific tools."
)
public class DatabaseTools {

    @LlmTool(description = "Execute a SQL query")
    public QueryResult query(String sql) {
        // 实现
    }

    @LlmTool(description = "Insert a record")
    public InsertResult insert(String table, Map<String, Object> data) {
        // 实现
    }

    @LlmTool(description = "Delete a record")
    public void delete(long id) {
        // 实现
    }
}

// 从被注解的类创建 UnfoldingTool
var tool = UnfoldingTool.fromInstance(new DatabaseTools());

你也可以在注解里指定 childToolUsageNotes 来提供子工具使用指引:

java
@UnfoldingTools(
        name = "music_search",
        description = "Search music database for artists, albums, and tracks",
        childToolUsageNotes = "Try vector search first for semantic queries. " +
                "Use text search for exact artist names."
)
public class MusicSearchTools {

    @LlmTool(description = "Semantic search using embeddings")
    public List<Track> vectorSearch(String query) {
        // 实现
    }

    @LlmTool(description = "Exact match text search")
    public List<Track> textSearch(String query) {
        // 实现
    }
}

带注解的基于类别选择

在 @LlmTool 注解上添加 category,即可自动创建基于类别的 UnfoldingTool:

java
@UnfoldingTools(
        name = "file_operations",
        description = "File operations. Pass category: 'read' or 'write'."
)
public class FileTools {

    @LlmTool(description = "Read file contents", category = "read")
    public String readFile(String path) {
        return Files.readString(Path.of(path));
    }

    @LlmTool(description = "List directory contents", category = "read")
    public List<String> listDir(String path) {
        return Files.list(Path.of(path)).map(Path::toString).toList();
    }

    @LlmTool(description = "Write file contents", category = "write")
    public void writeFile(String path, String content) {
        Files.writeString(Path.of(path), content);
    }

    @LlmTool(description = "Delete a file", category = "write")
    public void deleteFile(String path) {
        Files.delete(Path.of(path));
    }
}

// 自动创建基于类别的选择
var tool = UnfoldingTool.fromInstance(new FileTools());
// 以 {"category": "read"} 调用时,只暴露 read 工具
// 以 {"category": "write"} 调用时,只暴露 write 工具

@UnfoldingTools 注解属性

属性类型默认值说明
nameString必填LLM 将看到的门面工具名
descriptionString必填说明该工具类别的描述
removeOnInvoke(已废弃)booleantrue(被忽略 —— 始终由引导工具替换)调用后是否移除门面
categoryParameterString"category"用于类别选择的参数名

@LlmTool 的 category 属性

@LlmTool 上的 category 属性在其所在类带 @UnfoldingTools 时生效:

  • 相同 category 的工具被归为一组;
  • 不带 category 的工具会被加到所有类别组以及一个 "all" 类别里;
  • 若没有任何工具带类别,则创建一个简单的(非类别式的)UnfoldingTool。

真实案例:Spotify 集成

下面是 Impromptu 聊天机器人中的一个真实例子,用 @UnfoldingTools 渐进式披露 Spotify 功能:

java
@UnfoldingTools(
        name = "spotify",
        description = "Access Spotify music features. Invoke this tool to enable Spotify " +
                "operations like playing music, searching tracks, managing playlists, " +
                "and controlling playback."
)
public record SpotifyTools(ImpromptuUser user, SpotifyService spotifyService) {

    @LlmTool(description = "Check if user has linked their Spotify account")
    public String checkSpotifyStatus() { /* ... */ }

    @LlmTool(description = "Get the user's Spotify playlists")
    public String getPlaylists() { /* ... */ }

    @LlmTool(description = "Search for tracks on Spotify by song name, artist, or both")
    public String searchTracks(String query) { /* ... */ }

    @LlmTool(description = "Play a track on Spotify by searching for it")
    public String playTrack(String query) { /* ... */ }

    @LlmTool(description = "Pause the current Spotify playback")
    public String pausePlayback() { /* ... */ }

    // ... 更多工具
}

这套配置下:

  • LLM 最初只看到一个 spotify 工具;
  • 当用户说"play some jazz"时,LLM 调用 spotify;
  • spotify 门面被替换为所有内部工具(getPlaylists、searchTracks、playTrack 等);
  • 随后 LLM 可以调用 searchTracks 或 playTrack 来完成请求。

用 Tool.fromInstance() 自动检测

当你对一个带 @UnfoldingTools 注解的类使用 Tool.fromInstance() 时,它会自动创建 UnfoldingTool:

java
// 自动检测 @UnfoldingTools 并创建 UnfoldingTool
List<Tool> tools = Tool.fromInstance(new SpotifyTools(user, service));
// 返回单个 UnfoldingTool,而不是一堆独立工具

这能与 PromptRunner 上的 withToolObject() 无缝配合:

java
context.ai()
        .withToolObject(new SpotifyTools(user, spotifyService))
        .respond("Play some classical music");
// SpotifyTools 被自动暴露为单个 UnfoldingTool 门面

用 fromToolObject() 包装工具对象

UnfoldingTool.fromInstance() 要求该类带 @UnfoldingTools 注解。 对于像带 @LlmTool 默认方法的接口实现这类你不能或不该加 @UnfoldingTools 的对象,这就不适用了。

用 fromToolObject() 可以把任何带 @LlmTool 方法的对象包装成 UnfoldingTool,显式提供名称与描述:

java
import com.embabel.agent.api.tool.progressive.UnfoldingTool;

// FileWriteTools 是一个带 @LlmTool 默认方法的接口 ——
// 它不能被标注 @UnfoldingTools
FileWriteTools fileTools = new FileWriteToolsImpl(workspace);

var tool = UnfoldingTool.fromToolObject(
        fileTools,
        "file_write_tools",
        "Tools for writing and managing files. Invoke to see specific operations."
);

当你需要用显式的门面名与描述包装工具对象时,也用同一个工厂:

java
var tool = UnfoldingTool.fromToolObject(
        fileTools,
        "file_write_tools",
        "Tools for writing and managing files."
);

选型建议:

  • 当工具类是接口、第三方类,或任何加 @UnfoldingTools 不现实的类时,用 fromToolObject();
  • 当你能控制该类并且可以加上 @UnfoldingTools 注解时,用 fromInstance()。

嵌套 UnfoldingTools

UnfoldingTool 可以嵌套,实现多级渐进式披露。 这让大型工具集合可以组织成逻辑层级,由 LLM 通过调用门面工具在其中导航。

程序化嵌套

用 UnfoldingTool.of() 以程序方式创建嵌套层级:

java
// 用于用户管理的内部 UnfoldingTool
var userManagement = UnfoldingTool.of(
        "user_management",
        "User management operations",
        List.of(createUserTool, deleteUserTool, updateUserTool)
);

// 用于系统配置的内部 UnfoldingTool
var systemConfig = UnfoldingTool.of(
        "system_config",
        "System configuration operations",
        List.of(updateConfigTool, backupTool, restoreTool)
);

// 包含两者的外层 UnfoldingTool
var adminTool = UnfoldingTool.of(
        "admin_operations",
        "Administrative operations. Invoke to see categories.",
        List.of(userManagement, systemConfig)
);

// 流程:
// 1. LLM 看到:admin_operations
// 2. LLM 调用:admin_operations -> 看到:user_management, system_config
// 3. LLM 调用:user_management -> 看到:createUser, deleteUser, updateUser

用内部类做基于注解的嵌套

你也可以用内部类上的 @UnfoldingTools 注解创建嵌套层级。 调用 UnfoldingTool.fromInstance() 时,它会自动发现并包含任何同样带 @UnfoldingTools 注解的嵌套内部类:

java
@UnfoldingTools(
        name = "admin_operations",
        description = "Administrative operations. Invoke to access specific areas."
)
public class AdminTools {

    @LlmTool(description = "Get system status")
    public String getStatus() {
        return "System is healthy";
    }

    // 嵌套内部类 —— 自动被发现并作为嵌套 UnfoldingTool 包含进来
    @UnfoldingTools(
            name = "user_management",
            description = "User management operations. Invoke to see specific tools."
    )
    public static class UserManagement {

        @LlmTool(description = "Create a new user")
        public String createUser(String username) { return "Created user: " + username; }

        @LlmTool(description = "Delete a user")
        public String deleteUser(String username) { return "Deleted user: " + username; }

        // 还能嵌得更深
        @UnfoldingTools(
                name = "user_permissions",
                description = "User permission operations"
        )
        public static class Permissions {

            @LlmTool(description = "Grant permission to user")
            public String grant(String user, String permission) { return "Granted"; }

            @LlmTool(description = "Revoke permission from user")
            public String revoke(String user, String permission) { return "Revoked"; }
        }
    }

    @UnfoldingTools(
            name = "system_config",
            description = "System configuration. Invoke to see config tools."
    )
    public static class SystemConfig {

        @LlmTool(description = "Update configuration")
        public String updateConfig(String key, String value) { return "Updated"; }

        @LlmTool(description = "Backup configuration")
        public String backup() { return "Backed up"; }
    }
}

// 自动创建完整的嵌套层级
var adminTool = UnfoldingTool.fromInstance(new AdminTools());

// 流程:
// 1. LLM 看到:admin_operations
// 2. LLM 调用:admin_operations -> 看到:getStatus, user_management, system_config
// 3. LLM 调用:user_management -> 看到:createUser, deleteUser, user_permissions
// 4. LLM 调用:user_permissions -> 看到:grant, revoke

这种做法带来若干好处:

  • 封装:所有相关工具组织在单个类层级里;
  • 自动发现:无需手动接线 —— 带 @UnfoldingTools 的内部类会被自动包含;
  • 任意深度:需要多少层就嵌多少层,把工具逻辑地组织起来;
  • 混合内容:每一层都可以同时有直接的 @LlmTool 方法与嵌套的 @UnfoldingTools 类。

动态配置的内部工具

UnfoldingTool.selectable() 的一个强大用法是:基于调用门面时传入的参数来配置内部工具。 选择器函数可以创建捕获了状态、连接串或其它配置的新工具实例:

java
// 依据 connection 参数配置数据库工具的 UnfoldingTool
var databaseTool = UnfoldingTool.selectable(
        "database",
        "Database operations. Pass 'connection' to configure tools.",
        Collections.emptyList(), // 工具是动态创建的
        Tool.InputSchema.of(
                Tool.Parameter.string("connection", "Database connection string")
        ),
        true, // removeOnInvoke
        input -> {
            // 从输入解析 connection
            ObjectMapper mapper = new ObjectMapper();
            Map<String, Object> params = mapper.readValue(input, Map.class);
            String connection = (String) params.getOrDefault("connection", "localhost");

            // 创建用该连接串配置好的工具
            return List.of(
                    Tool.create("query", "Query database at " + connection, queryInput -> {
                        // 该工具已捕获了连接串
                        return Tool.Result.text("Queried " + connection + ": " + queryInput);
                    }),
                    Tool.create("insert", "Insert into database at " + connection, insertInput -> {
                        return Tool.Result.text("Inserted into " + connection);
                    })
            );
        }
);

// 当 LLM 以 {"connection": "prod-db.example.com"} 调用时
// 被注入的工具就被配置为使用那个特定的连接

这个模式适用于:

  • 多租户系统:用租户专属的凭据或端点配置工具;
  • 环境选择:让 LLM 在 dev/staging/prod 之间挑选;
  • 有状态操作:创建共享状态的工具(例如购物车的商品列表);
  • 动态服务发现:依据运行时服务位置配置工具。

示例:有状态的购物车工具

java
var cartTool = UnfoldingTool.selectable(
        "shopping_cart",
        "Shopping cart. Pass 'cart_id' to select which cart to operate on.",
        Collections.emptyList(),
        Tool.InputSchema.of(
                Tool.Parameter.string("cart_id", "Shopping cart ID")
        ),
        true,
        input -> {
            // 每次调用都创建一组带共享状态的全新工具
            String cartId = parseCartId(input);
            List<String> cartItems = new ArrayList<>(); // 共享状态

            return List.of(
                    Tool.create("add_item", "Add item to cart " + cartId,
                            Tool.InputSchema.of(Tool.Parameter.string("item", "Item name")),
                            itemInput -> {
                                String item = parseItem(itemInput);
                                cartItems.add(item); // 捕获到的状态
                                return Tool.Result.text("Added " + item + ". Total: " + cartItems.size());
                            }
                    ),
                    Tool.create("view_cart", "View cart " + cartId + " contents", viewInput -> {
                        return Tool.Result.text("Cart " + cartId + ": " + String.join(", ", cartItems));
                    }),
                    Tool.create("checkout", "Checkout cart " + cartId, checkoutInput -> {
                        String total = calculateTotal(cartItems);
                        cartItems.clear();
                        return Tool.Result.text("Checked out " + cartId + " for " + total);
                    })
            );
        }
);

与其它方案的对比

其它 Agent 框架用不同的方式处理庞大的工具集合,各有取舍:

  • Anthropic 的 Tool Search Tool:用 defer_loading: true 标志避免工具被预先加载。 工具通过一个独立的 "Tool Search Tool" 搜索工具元数据来发现。 这需要维护可搜索的工具描述,并且每次发现都增加延迟;
  • LangGraph 的动态工具调用:用向量存储与语义搜索,基于用户查询选择相关工具。 这需要嵌入基础设施、向量数据库搭建,以及相似度阈值的细致调优;
  • Google ADK 的 AgentTool:用递归委托给其它 Agent 的子 Agent,每个子 Agent 可能各有自己的工具集。 工具发现是通过 Agent 层级隐式进行的;
  • LangChain4j 的 ToolProvider:提供 ToolProvider 接口做动态工具选择, 但它在 LLM 调用之前工作 —— 通过分析进来的用户消息。 例如"若消息含 'booking',就带上 booking 工具"。 这是基于消息内容的预过滤,而不是通过工具调用实现的渐进式披露。 LangChain4j 的文档还建议用基于嵌入的分类、对工具描述做 RAG、或两趟 LLM 选择 —— 这些都需要额外基础设施或额外的 LLM 调用。

UnfoldingTool 采用的是一条根本不同的路:调用即揭示(invoke to reveal)。 它不去搜索工具元数据,而是让 LLM 直接调用门面工具来解锁其中包含的工具。

超越搜索:动态工具配置

关键在于,UnfoldingTool 远超任何基于搜索的方案所能提供的。 搜索只能找到已有的工具 —— 它无法创建新工具或修改它们的行为。 而用 UnfoldingTool.selectable(),选择器函数可以:

  • 基于运行时参数创建实现完全不同的新工具实例;
  • 把配置(连接串、凭据、端点)捕获进工具的行为里;
  • 在同一次调用创建的工具之间共享可变状态;
  • 定制工具描述以反映具体的使用上下文。

例如,当 LLM 以 {"connection": "prod-db.example.com"} 调用一个 "database" 的 UnfoldingTool 时, 返回的工具不仅描述不同 —— 它们的行为也不同,操作的是那个特定的数据库。 这在基于搜索的发现里根本不可能,因为它只能返回对预定义工具的引用。

这带来若干优势:

方面其它方案UnfoldingTool
基础设施需要向量存储、嵌入、搜索索引,或预过滤逻辑无需额外基础设施
选择时机LLM 调用之前(基于消息分析做预过滤)LLM 决定调用门面之后(LLM 驱动的发现)
延迟搜索/嵌入增加延迟;两趟选择使 LLM 调用翻倍调用即瞬间解锁
可扩展性工具集极大时搜索质量下降;需要细致调优可经嵌套扩展到任意数量工具,不退化
确定性搜索结果可能随嵌入相似度变化确定:调用门面总是揭示同一批工具
成本嵌入生成、向量搜索或额外 LLM 调用产生算力开销除工具调用本身之外无额外算力
动态行为只能返回对已有工具的引用可创建带运行时配置行为的新工具实例

UnfoldingTool 的层级嵌套能力意味着你可以把成千上万个工具组织成一棵逻辑树。 LLM 通过简单的调用来在这棵树里导航,任何层级都没有搜索开销。 例如一个顶层的 "admin_operations" 门面可以揭示 5 个类别门面, 每个再揭示 20 个具体工具 —— 最多 2 次调用即可访问 100 个工具。

UnfoldingTool 与 LlmReference 的取舍

UnfoldingTool 与 LlmReference 都向 LLM 暴露工具,但用途不同:

在以下情况用 UnfoldingTool:

  • 你有一个单一的顶层能力,LLM 可以把它当作一个工具调用;
  • 提示词贡献很短,能塞进工具描述里;
  • 例如:一个 "database" 工具,调用时揭示 query/insert/delete 工具。

在以下情况用 LlmReference:

  • 提示词贡献较长或具有全局意义(会出现在 system prompt 里);
  • 你有一批相关工具,而不只是一个顶层工具;
  • 你需要 notes() 来提供与工具描述分开的详细使用说明;
  • 该 reference 贡献的不只是工具可用性,还有上下文。

同时实现两者

像 Memory 与 ToolishRag 这样的类同时实现了 Tool 与 LlmReference,提供了最大灵活性:

java
// 作为 LlmReference 使用(加入 system prompt + 工具)
ai.withReference(memory).respond(...);

// 直接作为 Tool 使用(只是工具)
ai.withTool(memory).respond(...);
kotlin
// 作为 LlmReference 使用(加入 system prompt + 工具)
ai.withReference(memory).respond(...)

// 直接作为 Tool 使用(只是工具)
ai.withTool(memory).respond(...)

作为 LlmReference 使用时,tools() 方法直接暴露内部工具; 作为 Tool 使用时,实现会把它们包进一个 UnfoldingTool 门面。

进程内省工具(Process Introspection Tools)

Embabel 提供内建的 UnfoldingTool 实现,用于内省当前 Agent 进程及其黑板。 这些工具让 LLM 能在智能体工作流中监控自己的进度、检查资源用量、访问此前步骤的数据。

AgentProcessTools:运行时自我感知

AgentProcessTools 提供让 LLM 理解当前执行上下文的工具。 当你希望 Agent 意识到自身的运行状态时,这很有用 —— 例如在开展昂贵操作之前检查还剩多少预算,或回顾到目前为止做了哪些动作。

什么时候用 AgentProcessTools:

  • 预算敏感的 Agent:在执行昂贵操作前检查剩余成本或 token 预算;
  • 长时运行的工作流:监控已耗时间与动作历史;
  • 调试与日志:了解用到了哪些模型与工具;
  • 自我反思:需要对自身行为进行推理的 Agent。

暴露的子工具:

工具名用途
process_status当前进程 ID、状态、运行时长与目标信息
process_budget预算限制(成本、token、动作数)与剩余容量
process_cost总成本(LLM 与 embedding 调用)、调用次数、详细 token 用量
process_history到目前为止执行过的动作列表及其执行时间
process_tools_stats工具使用统计(每个工具的调用次数)
process_models被调用过的所有模型(LLM 与 embedding)
java
import com.embabel.agent.tools.process.AgentProcessTools;
import com.embabel.agent.api.tool.progressive.UnfoldingTool;

// 创建该工具 —— 通常加到一个 agentic tool 里
var processTools = new AgentProcessTools().create();

// 加入 SimpleAgenticTool
var assistant = new SimpleAgenticTool("assistant", "...")
        .withTools(processTools);

⚠️ 这些工具需要有一个活跃的 AgentProcess 上下文。 若在 Agent 执行之外被调用,它们会返回一条错误消息,表明没有可用的进程。

BlackboardTools:访问工作流数据

BlackboardTools 提供让 LLM 访问当前进程黑板上的对象的工具。 黑板是 Embabel 的共享上下文机制 —— 它持有此前动作产生的 artifact、工具输出(在使用 ArtifactSink 时),以及任何绑定到该进程的对象。

什么时候用 BlackboardTools:

  • 多步工作流:无需重新执行就能访问先前 action 的结果;
  • 访问工具输出:当工具用 ArtifactSink 发布结构化数据时,BlackboardTools 让 LLM 能取回它;
  • 上下文感知:让 LLM 探索当前上下文中有什么数据可用;
  • 调试:开发期间检查黑板内容。

暴露的子工具:

工具名用途
blackboard_list列出黑板上所有对象及其类型与索引
blackboard_get按绑定名取对象(如 "user"、"searchResults")
blackboard_last取某类型的最近一个对象(匹配简单名或全限定名)
blackboard_describe按绑定名获取某对象的详细描述/格式化结果
blackboard_count统计黑板上某类型对象的数量
java
import com.embabel.agent.tools.blackboard.BlackboardTools;
import com.embabel.agent.api.tool.progressive.UnfoldingTool;

// 用默认格式化创建
var blackboardTools = new BlackboardTools().create();

// 或者为黑板条目使用自定义格式化
var blackboardTools = new BlackboardTools().create(myCustomFormatter);

// 加入 SimpleAgenticTool
var assistant = new SimpleAgenticTool("assistant", "...")
        .withTools(blackboardTools);

格式化黑板条目:

默认情况下,BlackboardTools 使用 DefaultBlackboardEntryFormatter,它:

  • 对实现 HasInfoString 的对象使用 infoString();
  • 对实现 HasContent 的对象使用 content 属性;
  • 其它对象回退到 toString()。

你可以提供自定义的 BlackboardEntryFormatter 来控制对象如何呈现给 LLM。

类型匹配:

blackboard_last 与 blackboard_count 按以下方式匹配类型:

  • 简单名:"Person" 匹配任何名为 Person 的类;
  • 全限定名:"com.example.Person" 精确匹配那个类。

这种灵活性让 LLM 可以用最方便的名字来查询。

组合使用进程内省工具

对于需要完整态势感知的 Agent,把两个工具组合起来:

java
SimpleAgenticTool awarenessAgent = new SimpleAgenticTool(
        "aware_assistant",
        "An assistant that can check its own status and access previous results")
        .withTools(
                new AgentProcessTools().create(),
                new BlackboardTools().create()
        );

进程通信工具(Process Communication Tools)

Embabel 提供两个内建工具,让 LLM 能在 Agent 执行过程中与用户通信。 两者都把消息经由当前 AgentProcess 的 output channel 路由,但在意图与呈现方式上不同。

工具用途呈现方式
progress在长时工作中报告瞬时的状态更新呈现为进度横幅(短暂的)
communicate向用户发送永久性消息呈现为 assistant 聊天气泡(持久的)

ProgressTool

ProgressTool 让 LLM 在长时运行的 action 中报告它当前在做什么。 进度消息是瞬时的 —— 它们表示有活动,但不构成最终对话输出的一部分。

java
import com.embabel.agent.api.tool.ProgressTool;
import com.embabel.agent.api.tool.Tool;

Tool progressTool = ProgressTool.create();

// 加入 SimpleAgenticTool
var assistant = new SimpleAgenticTool("assistant", "...")
        .withTools(progressTool);

当 LLM 调用 progress 工具时,它会向 output channel 发送一个带简短状态消息的 ProgressOutputChannelEvent。 如果当前线程上没有活跃的 AgentProcess,该工具会记录一条警告并优雅返回 —— 不会中断 Agent 执行。

CommunicateTool

CommunicateTool 让 LLM 向用户发送永久性消息。 与进度更新不同,communicate 消息会呈现为 assistant 聊天气泡,并留在对话中。 适用于汇报结果、分享链接(例如 PR URL),或告知用户重要结论。

java
import com.embabel.agent.api.tool.CommunicateTool;
import com.embabel.agent.api.tool.Tool;

Tool communicateTool = CommunicateTool.create();

// 加入 SimpleAgenticTool
var assistant = new SimpleAgenticTool("assistant", "...")
        .withTools(communicateTool);

当 LLM 调用 communicate 工具时,它会向 output channel 发送一个包含 AssistantMessage 的 MessageOutputChannelEvent。与 ProgressTool 一样,它也能优雅处理"没有活跃 AgentProcess"的情况。

组合使用通信工具

对于既需要瞬时进度报告、又需要持久消息的 Agent,请同时提供两个工具:

java
var assistant = new SimpleAgenticTool(
        "assistant",
        "An assistant that reports progress and communicates results")
        .withTools(
                ProgressTool.create(),
                CommunicateTool.create()
        );

⚠️ 两个工具都需要一个活跃的 AgentProcess 上下文才能把消息投递给用户。 若在 Agent 执行之外被调用,它们会返回一个软确认并记录警告 —— 不会抛异常,也不会中断 Agent 执行。

OutputChannelHighlightingEventListener 会自动抑制 progress 与 communicate 工具的原始工具调用进度横幅, 因为这两个工具自己就会产出用户可见的输出。

工具组的即时初始化(Just-in-Time Tool Group Initialization)

默认情况下,Embabel 在应用启动时就初始化 MCP 工具组。 这会让一类部署失败:MCP 服务器用调用方通过 Authorization header 转发的 OAuth token 来认证请求 —— 而启动时根本不存在用户 token。

要把 MCP 握手推迟到第一个 Agent 请求,请把这三个属性一起设置:

yaml
spring:
  ai:
    mcp:
      client:
        initialized: false
      toolcallback:
        enabled: false

embabel:
  agent:
    platform:
      tools:
        lazy-init: true

启用 lazy init 后,启动日志会确认启动时没有发生任何 MCP 流量:

INFO ToolGroupsConfiguration - MCP is available (lazy-init mode). Found 1 client(s).
     Tool groups will be initialized on first use.

MCP 握手只在第一个需要 MCP 支撑工具组的 Agent action 执行时才触发 —— 而那时用户的 OAuth token 已经存在于安全上下文中。

McpToolFactory:MCP 工具集成

McpToolFactory 是一个接口,提供把模型上下文协议(MCP)工具集成进应用的便捷方式。 它从 MCP 服务器创建 Embabel Tool 实例,并支持过滤工具与把它们包进 UnfoldingTool 门面。

SpringAiMcpToolFactory 是基于 Spring AI 的实现。

创建 McpToolFactory

SpringAiMcpToolFactory 需要一个 McpSyncClient 列表, 通常由 Spring 的 MCP 自动配置提供:

java
import com.embabel.agent.tools.mcp.McpToolFactory;
import com.embabel.agent.spi.support.springai.SpringAiMcpToolFactory;
import io.modelcontextprotocol.client.McpSyncClient;

@Configuration
public class ToolConfiguration {

    @Bean
    public McpToolFactory mcpToolFactory(List<McpSyncClient> clients) {
        return new SpringAiMcpToolFactory(clients);
    }
}

MCP 客户端在 application.yml 中配置。配置细节见 MCP 集成。

获取单个 MCP 工具

用 toolByName 按精确名字取单个 MCP 工具:

java
// 找不到则返回 null
Tool braveSearch = mcpToolFactory.toolByName("brave_web_search");
if (braveSearch != null) {
    ai.withTool(braveSearch).generateText("Search for recent news about AI");
}

// 找不到则抛 IllegalArgumentException(带有用的错误消息)
Tool requiredTool = mcpToolFactory.requiredToolByName("brave_web_search");

从 MCP 创建 UnfoldingTool 门面

McpToolFactory 可以把一组 MCP 工具包进 UnfoldingTool 门面,实现渐进式披露。 当你有很多 MCP 工具、却想把它们呈现为逻辑类别时,这很有用。

按精确工具名:

java
// 用指定的工具名创建 UnfoldingTool
var wikipediaTool = mcpToolFactory.unfoldingByName(
        "wikipedia",
        "Search and find content from Wikipedia",
        Set.of("search_wikipedia", "get_article", "get_related_topics", "get_summary")
);

按正则模式:

java
import java.util.regex.Pattern;

// 用正则模式匹配工具
var dbTool = mcpToolFactory.unfoldingMatching(
        "database_operations",
        "Database operations. Invoke to access database tools.",
        List.of(Pattern.compile("^db_.*"), Pattern.compile(".*query.*"))
);

自定义过滤器:

java
// 自定义过滤谓词
var webTool = mcpToolFactory.unfolding(
        "web_operations",
        "Web operations. Invoke to access web tools.",
        callback -> callback.getToolDefinition().name().startsWith("web_")
);

门面替换

在 LLM 调用一个 MCP 支撑的 UnfoldingTool 之后, Embabel 总是把门面替换为一个引导工具外加匹配的内部工具。 请使用不传 removeOnInvoke 的那些重载。

java
var wikipediaTool = mcpToolFactory.unfoldingByName(
        "wikipedia",
        "Search Wikipedia",
        Set.of("search_wikipedia", "get_article")
);

传入已废弃的 removeOnInvoke 参数没有任何效果。

真实案例:带 MCP 工具的聊天机器人

下面是一个生产聊天机器人的真实例子,用 McpToolFactory 集成 MCP 工具并优雅降级:

java
@Configuration
public class ChatConfiguration {

    @Bean
    public McpToolFactory mcpToolFactory(List<McpSyncClient> clients) {
        return new SpringAiMcpToolFactory(clients);
    }

    @Bean
    public CommonTools commonTools(McpToolFactory mcpToolFactory) {
        var deferMessage = "Use this tool only after trying local sources";
        var tools = new LinkedList<>();

        // 单个 MCP 工具 —— 优雅处理"工具不存在"
        var braveSearch = mcpToolFactory.toolByName("brave_web_search");
        if (braveSearch != null) {
            tools.add(braveSearch.withNote(deferMessage));
        }

        // 把相关 Wikipedia MCP 工具归组的 UnfoldingTool
        var wikipediaTool = mcpToolFactory.unfoldingByName(
                "wikipedia",
                "Search and find content from Wikipedia: " + deferMessage,
                Set.of("search_wikipedia", "get_article", "get_related_topics", "get_summary")
        );
        if (!wikipediaTool.getInnerTools().isEmpty()) {
            tools.add(wikipediaTool);
        }

        return new CommonTools(tools);
    }
}

这个模式:

  • 在 MCP 工具不可用时优雅降级(例如测试环境);
  • 用 UnfoldingTool 把相关工具归到一个有描述的门面之后;
  • 用 withNote() 加入使用提示,引导 LLM 何时该用外部工具;
  • 在加入工具之前先检查结果是否为空,避免出现空门面。

McpToolFactory 方法一览

方法说明
toolByName(String)按精确名字取单个 MCP 工具。未找到返回 null。
requiredToolByName(String)按精确名字取单个 MCP 工具。未找到抛 IllegalArgumentException,错误消息中列出可用工具。
unfoldingByName(name, description, toolNames)创建包含名字精确匹配的工具的 UnfoldingTool。
unfoldingMatching(name, description, patterns)创建包含匹配任一正则模式的工具的 UnfoldingTool。
unfolding(name, description, filter)用自定义过滤谓词创建 UnfoldingTool。

BytesRouter 是社区翻译站点,所有译文仅供参考,一切以官方英文原文为准。