Ohhnews

分类导航

$ cd ..
Baeldung原文

使用 Embabel Agentic AI 框架实现 LLM 工具调用推理

#embabel#ai agent#工具调用推理#java#llm

[LOADING...]

1. 概述

工具调用推理通过捕捉 LLM 工具选择背后的理由,而非仅仅记录模型调用了哪个工具,从而提升 AI Agent 的可观测性。

深入了解 Agent 的工具选择决策有助于开发者调试 Agent 工作流、验证决策质量,并发现改进提示词或工具设计的机会。随着 Agent 自主性增强并与更多工具交互,这种透明度变得越来越重要。

在本教程中,我们将了解如何使用 Embabel Agentic AI 框架在 Java 应用中实现这一级别的可观测性。

2. 使用 Spring AI 进行工具调用推理

在深入探讨如何使用 Embabel 之前,我们先看看 Spring AI Tool Argument Augmenter 是如何解决这个问题的。

Tool Argument Augmenter 会动态地使用额外的推理字段(如 innerThought 或 confidence)来增强工具模式。 其结果是提升了 Agent 行为的可观测性,同时无需更改现有工具的实现。

在 Spring AI 中,工具通过定义 JSON Schema 来指定预期的参数。LLM 评估这些模式并决定调用哪个工具以及传递哪些参数。Spring AI 的增强层利用这一机制,通过向模式的属性中添加模型可见的额外属性来实现增强(即模式增强)。

一个关键的实现特性是:在工具调用时应用的真实工具定义保持不变。 增强层会拦截工具调用的负载,提取推理字段,将其移除,然后仅使用预期的参数调用实际工具。工具本身对增强过程毫不知情。

用户通过一个单独的 DTO(包含 innerThought 和 confidence 属性)获取工具调用推理的详细信息。

2.1. Spring AI 解决方案设计分析

该设计在多个方面表现出色。

首先,它提供了结构化且机器可读的可观测性。由于推理字段是模式的一部分,它们更容易被评估、存储或输入到分析管道中。

其次,它保持了高度的非侵入性。现有工具(包括 MCP 工具 和本地的 @Tool 方法)无需修改即可继续工作。

第三,该解决方案利用了现有的 LLM 能力,而不是发明专有的推理 API。任何支持结构化工具调用的模型原则上都可以支持这种方法。

然而,该设计也引入了权衡;一个关键的局限性是暴露的模式不再是实际的契约。 LLM 所见的模式包含基础工具实际从不接受的字段。从架构上看,这意味着增强后的工具模式与运行时工具方法契约并非同一回事。

该方法通过以下方式增加了额外的复杂性:

  • 增强型 DTO
  • 模式合并
  • 增强字段提取
  • 工具包装基础设施

这既增加了概念负担,也增加了与 Spring AI 内部 JSON Schema 工具模型的耦合。

3. 使用系统提示词进行工具调用推理

一个更简单的替代方案是通过提示词定义标准化的系统级协议。当调用工具时,提示词应要求 LLM:

  • 简要解释为什么需要该工具
  • 说明预期获取什么信息
  • 提供一个置信度级别

这将推理捕获从模式级契约转移到提示词级协议——从结构性强制转变为语义性指导。在实践中,模式增强强制要求一个硬性契约(“字段必须存在”),而提示词则建立一个软性约定(“按此方式行为”)。

两种方法都依赖于模型生成的解释,因此在内容和措辞上具有相同的可变性。实际区别在于输出结构:模式增强强制要求显式字段的存在,使解释更容易捕获和解析,而基于提示词的方法则将其保留在半结构化格式中。

基于提示词方法的主要优势是可移植性。 模式增强依赖于框架特定的机制(例如工具模式生成、回调提供者、参数拦截),而系统提示词仅依赖于一种通用能力:向模型发送指令。这使得它可以跨其他 Agent 框架移植,例如 Spring AI、LangChain4j、Semantic Kernel、OpenAI、Anthropic 和 Google DeepMind。

与 Spring AI 增强方法相比,基于提示词的解决方案避免了合成模式、增强 DTO、模式转换和自定义工具包装基础设施,从而保持了更简单的实现。

两种方法没有绝对的优劣——它们体现了不同的工程权衡。在实践中,混合模型通常效果最佳:使用提示词级协议进行通用指导,并在需要更强保证时选择性应用模式级增强。## 4. 使用 Embabel 进行工具调用推理

Embabel 是一个由 Spring 框架创始人 Rod Johnson 创立的 Agentic AI 框架。关于 Embabel 的介绍信息,请参考 Baeldung 的文章《使用 Embabel Agent 框架》。你可以在此处找到官方文档,或在 Medium 上获取更多资源。

4.1. Embabel 模式快速介绍

Embabel 是一个用于在 JVM 上开发 Agentic AI 应用程序的综合性框架。其关键特性之一是在所有模式中采用具有结构化输出的流畅 API——这是 JVM 上 Agentic AI 领域的一个重要特性:

  • 基础模式(无“思考”或流式输出)
  • 思考模式(详情见下文)
  • 流式模式

我们将探讨“思考”模式,该模式是工具调用推理解决方案的基础。

4.2. Embabel Agentic AI 依赖

首先,定义测试所需的主要 Embabel 依赖项:

$ xml
<!-- Embabel Agent Starter -->
<dependency>
    <groupId>com.embabel.agent</groupId>
    <artifactId>embabel-agent-starter-openai</artifactId>
    <version>${embabel-agent.version}</version>
</dependency>
<!-- Embabel Agent Test Support -->
<dependency>
    <groupId>com.embabel.agent</groupId>
    <artifactId>embabel-agent-test-internal</artifactId>
    <version>${embabel-agent.version}</version>
    <scope>test</scope>
</dependency>

embabel-agent-starter-openai 依赖提供使用 Embabel 配合 OpenAI 模型所需的核心类。

我们还引入了 embabel-agent-test-internal,以便在 @SpringBootTest 中使用 AgentTestApplication 测试支持类。

4.3. 思考模式流畅 API 概念

接下来引入 Embabel “思考”模式流畅 API 的关键概念:

$ java
ThinkingResponse<ParkingRecommendation> result = ai.withDefaultLlm()
  .withToolObject(tools)
  .withToolLoopInspectors(callbackTracker, loggingInspector)
  .withToolLoopTransformers(systemMessageTransformer)
  .thinking()
  .createObject(prompt, ParkingRecommendation.class);
  • Ai 是 LLM 连接的网关;我们可以在配置文件中定义默认的 Ai LLM,或显式提供。
  • ThinkingResponse 是一个包装对象,包含对底层业务对象 ParkingRecommendation 的引用,以及包含 LLM 推理内容的所谓 ThinkingBlock
  • withToolObject() 是对 ParkingTooling 工具对象的引用(详情见后文)。
  • withToolInspectors() 指可选的回调函数,有助于额外的日志记录、指标收集等。CallbackTracker 收集统计数据(LLM 调用次数、工具调用次数、已调用工具列表),而 ToolLoopLoggingInspector 记录 LLM 和工具调用的详细信息(例如:ToolLoopLoggingInspector -- afterLlmCall: iteration=1, toolCalls=2, contentLength=1063, usage=prompt=834, completion=260)。
  • withToolLoopTransformers() 演示了通过 ToolLoopTransformer 进行用户控制的对话历史管理。在本例中,转换器配置了两条系统消息,并始终将它们置于 LLM 调用前的对话历史顶部。
  • thinking() 表示流畅 API 的子类型为“思考模式”,并输出 ThinkingResponse,该响应包装了由 API 的 createObject() 方法构建的业务对象。
  • 最后,prompt 变量包含我们的用户消息。

思考模式还包括其他一些 API,其中非常有用的是 createObjectIfPossible()。如果 LLM 无法创建对象,ThinkingResponse 仍会包含其背后的推理信息。

4.4. 用例

我们的用例是一个受时间和成本限制的最优停车方案查找器。我们将使用三个工具:

  • 免费街边停车定位器
  • 计时停车定位器
  • 车库预约

最终的思考块将解释基于通过探查多个工具收集的信息而给出的推荐理由。

我们将提示分为两部分:一个通用的、与领域无关的系统消息(可用于多种业务场景),以及一个特定领域的用户消息。

先定义系统消息:

$ java
"""
关键工作流程 - 两阶段决策过程:
=== 阶段 1:工具选择(首次响应) ===
1. 对于计划调用的每个工具,发出一个独立的 <tool_use_reasoning> 块:
    <tool_use_reasoning>
    工具:[工具名称]
    为何选择此工具:[解释为什么需要此特定工具]
    预期信息:[此工具将揭示什么]
    相对于替代方案的优势:[为什么此工具优于其他]
    对此工具选择的信心 [confidence=0.XX]
    </tool_use_reasoning>
    由于必须至少调用 2 个工具,因此必须至少发出 2 个独立块。
2. 调用至少两个工具以收集全面信息
    - 必须调用至少 2 个工具以探查不同方面。
    - 工具是信息收集的探查器,而非最终决策。
    - 多次探查可提供更好的决策质量。
=== 阶段 2:最终决策(收到工具结果后) ===
1. 发出最终决策推理:
    <final_decision_reasoning>
    - 每个工具探查揭示了什么
    - 探查结果如何影响你的分析
    - 基于探查数据和约束选择此选项的原因
    信心:[confidence=0.XX]
    </final_decision_reasoning>
2. 然后提供最终结构化输出
    - 你的最终推荐应综合多次探查的见解。
    - 切勿将推理块复制到最终结构化对象中。
提醒:每个工具调用对应一个 <tool_use_reasoning> 块。至少 2 个工具 = 至少 2 个块。
在两个阶段均发出推理。
"""

再定义用户消息:

$ java
"""
场景:
一位顾问正驱车前往曼哈顿中城参加客户会议。
约束条件:
- 距离会议开始还有 30 分钟
- 迟到不可接受
- 会议预计持续约 3 小时
停车选项:
- 街边停车:免费,但不确定
- 计时停车:每小时 5 美元,通常限制为 2 小时
- 车库停车:每小时 30 美元,保证有空位
重要决策因素:
- 会议前可用的时间
- 迟到的风险
- 街边、计时和车库停车之间的权衡
推荐最佳停车选项。
可用工具:%s
"""

接下来将提供可用工具。

4.5. 工具定义

定义工具对象:

$ java
static class ParkingTooling {
    @LlmTool(description = "查找免费街边停车。不确定且可能花费时间。")
    public String findStreetParking(String location, int maxMinutes) {
        boolean found = ThreadLocalRandom.current().nextDouble() < 0.3; // 低概率
        if (found) {
            return "在 " + location + " 附近找到街边停车(免费)";
        }
        return "在 " + maxMinutes + " 分钟内未找到街边停车";
    }
    @LlmTool(description = "查找计时停车。中等费用和中等可用性。可能存在时间限制。")
    public String findMeterParking(String location, int maxMinutes) {
        boolean found = ThreadLocalRandom.current().nextDouble() < 0.6; // 中等概率
        if (found) {
            return "在 " + location + " 附近找到计时停车($5/小时,2 小时限制)";
        }
        return "在 " + maxMinutes + " 分钟内未找到计时停车";
    }
    @LlmTool(description = "在目的地附近预约有保障的车库停车。")
    public String reserveGarage(String location) {
        return "在 " + location + " 附近已预约车库($30/小时,有保障)";
    }
}

此处,LlmTool 是 Embabel 的工具注解,包含工具描述。 Embabel 允许我们通过实现 Embabel 的 Tool 接口,或使用带注解的 Java 方法来向 LLM 暴露工具。此处我们采用后一种方法。

4.6. 测试结果讨论

我们的测试类 ToolCallReasoningIntegrationTest 调用 LLM(LLM 再调用工具并总结工具选择推荐),并使用自定义日志函数记录结果。

我们有一个 reportToolCallPattern() 方法,该方法以 CallbackTracker 为参数,记录工具模式和统计信息:

$ plaintext
17:26:33.876 [main] INFO  ToolCallReasoningIntegrationTest - === 工具调用模式分析 ===
LLM 调用总数:2
唯一工具:[findStreetParking, findMeterParking]
调用总数:2
工具结果回调:2
包含工具调用的迭代次数:1
17:26:33.876 [main] INFO  ToolCallReasoningIntegrationTest - 模式:所有 2 个工具在同一迭代中调用(并行)
17:26:33.876 [main] INFO  ToolCallReasoningIntegrationTest - 捕获的思考块:3
17:26:33.876 [main] INFO  ToolCallReasoningIntegrationTest -   块 1:tagType=TAG, tagValue=tool_use_reasoning, contentLength=481
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -     工具:functions.findStreetParking
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -     属性:{confidence=0.85}
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -   块 2:tagType=TAG, tagValue=tool_use_reasoning, contentLength=494
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -     工具:functions.findMeterParking
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -     属性:{confidence=0.90}
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -   块 3:tagType=TAG, tagValue=final_decision_reasoning, contentLength=755
17:26:33.877 [main] INFO  ToolCallReasoningIntegrationTest -     属性:{confidence=0.95}

此外,我们的 reportThinkingBlocks() 方法从结果中提取 ParkingRecommendation 对象和 ThinkingBlock 项,格式化并记录它们。

下面查看 reportThinkingBlocks() 产生的日志:

$ plaintext
推荐:ParkingRecommendation[chosenOption=GARAGE, location=Midtown Manhattan, estimatedTotalCost=90, summary=30 分钟内无法找到街边停车,计时停车限制为 2 小时,不足以满足 3 小时会议需求。车库停车提供有保障的可用性并确保准时到达,尽管成本较高,但仍是最佳选项。]
推理:
  ThinkingBlock(content=工具:functions.findStreetParking
为何选择此工具:检查在 30 分钟时间限制内在曼哈顿中城附近找到免费街边停车的可能性和可行性。
预期信息:街边停车是否实际可用以及找到车位可能需要多长时间。
相对于替代方案的优势:街边停车免费且是最便宜的选项,但高度不确定。在考虑更昂贵的选项之前,测试其可行性是关键。
对此工具选择的信心 [confidence=0.9], tagType=TAG, tagValue=tool_use_reasoning)
  ThinkingBlock(content=工具:functions.findMeterParking
为何选择此工具:评估在 30 分钟时间范围内,考虑到 2 小时限制和 $5/小时的成本,计时停车的可用性和可行性。
预期信息:确认附近是否有计时停车位可用以及潜在的成本和时间限制。
相对于替代方案的优势:计时停车平衡了中等成本和可用性,但存在时间限制,因此检查是否符合会议时长很重要。
对此工具选择的信心 [confidence=0.9], tagType=TAG, tagValue=tool_use_reasoning)
  ThinkingBlock(content=- 街边停车探查显示,在关键的 30 分钟到达窗口内无可用免费街边停车,考虑到零容忍的迟到要求,此选项风险过大。
- 计时停车在目的地附近可用,但限制为 2 小时,费用为 $5/小时。该时间限制不足以满足 3 小时会议,因此此选项存在停车违规或中断的风险。
- 虽然尚未探查车库,但车库停车保证可用性,可以覆盖整个 3 小时会议,消除了迟到或停车问题的风险,但成本较高($30/小时)。
- 考虑到准时到达和会议时长的重要性,尽管成本较高,车库停车仍是最可靠、最合适的选择。
信心:[confidence=0.95], tagType=TAG, tagValue=final_decision_reasoning)

Embabel 返回了一个类型为 ParkingRecommendation 的结构化对象,包含所有预期字段,包括 summary

每个 ThinkingBlock 包含工具选择的解释;tagType 指定 LLM 推理格式——本例中为 XML TAG(也可能是 PREFIX 或无标签),tagValue=tool_use_reasoningtagValue=final_decision_reasoning,这些是我们系统提示中定义的动态用户自定义 XML 标签。请注意,每个工具调用对应两个 ThinkingBlock

最后,输出了包含推荐的 ThinkingBlock,并按预期替换了占位符 confidence

使用占位符可以进一步自动化处理工具选择推理。方法 extractAttributes()(请参考文章关联的代码)扫描 ThinkingBlock 中的 [key=value] 格式属性,并将其作为键值对映射返回。例如,它将文本 [confidence=0.9] 转换为条目 "confidence" -> 0.9

因此,Embabel 的思考模式输出是半结构化的。

5. 结论

在本文中,我们看到了 Embabel 如何通过其“思考”API 提供工具选择推理的解决方案。

虽然 Spring AI 提供了开箱即用的工具选择推理支持,但该解决方案存在明显的权衡。使用 Embabel 的替代解决方案非常简单,与 Spring AI 不同,它不需要隐藏的模式操作或引入自定义 DTO,完全基于现有的 Embabel 基础 API 构建。

使用 Embabel,还可以通过 LLM 工具循环回调实现另一种解决方案,但这超出了本文的范围。

文章代码可在 GitHub 上获取。

作者感谢 Rod JohnsonAjen Poutsma 的审阅和建议。

文章《LLM Tool Call Reasoning Using Embabel Agentic AI Framework》首发于 Baeldung