1. Spring AI 工具调用回调与流式前端展示的完整落地方案
最近在重构一个零代码生成项目时,遇到了一个棘手的问题:如何在 Spring AI 中实现工具调用的回调机制,并将这些调用信息实时展示给前端。这让我不得不深入研究了 Spring AI 的工具调用机制,并最终找到了一套完整的解决方案。
如果你也在使用 Spring AI 开发 AI 应用,特别是需要在前端展示工具调用过程的应用,这篇文章将为你提供一套可直接落地的方案。我会从问题背景、设计思路到具体实现,一步步带你了解整个过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题背景与需求分析
2.1 为什么需要工具调用回调
在 AI 应用中,工具调用(Tool Calling)是一个非常重要的功能。它允许 AI 模型在执行过程中调用外部工具或函数来完成特定任务。比如:
- 读写文件
- 查询数据库
- 调用外部 API
- 执行特定业务逻辑
然而,Spring AI 目前(截至 2024 年)的工具调用机制相比 Langchain4j 还比较基础,缺少一些关键功能:
- 缺少回调机制:无法在工具调用前后执行自定义逻辑
- 缺少流式通知:无法将工具调用过程实时通知给前端
- 缺少上下文管理:难以获取和管理会话 ID(conversationId)
2.2 具体业务需求
在我们的零代码生成项目中,我们需要:
- 区分不同应用的生成目录:通过 conversationId 隔离每个应用的生成路径
- 记录工具调用次数:用于后续分析和优化
- 实时展示工具调用过程:让用户看到 AI 正在执行哪些操作
这些需求在 Langchain4j 中可以通过 StreamingChatResponseHandler 和 @ToolMemoryId 轻松实现,但在 Spring AI 中需要我们自己搭建这套机制。
3. 整体架构设计
3.1 核心思路
我们的解决方案基于以下核心思想:
- AOP 切面拦截:通过 Spring AOP 在工具调用前后插入自定义逻辑
- 事件发布机制:使用 Reactor 的 Sinks 实现事件发布/订阅
- 流合并:将 AI 响应流和工具调用事件流合并返回给前端
3.2 架构流程图
plaintext复制用户发请求 → Ai2ChatClient 接收 → SpringAI 处理 → 切面拦截工具调用 → 事件发布 → 实时推给前端
3.3 技术选型
- Spring AOP:用于拦截工具方法调用
- Project Reactor:实现事件流和响应流
- Caffeine Cache:用于临时存储工具调用状态
4. 核心实现细节
4.1 工具类实现示例
我们先来看一个实际的工具类实现 - TodoList 工具:
java复制@Component
public class TodolistTools extends BaseTools {
private static final Cache<String, String> TODOLIST_CACHE = Caffeine.newBuilder()
.maximumSize(10_00)
.expireAfterWrite(Duration.ofMinutes(30))
.build();
@Tool(description = "Write or update the todo list for current task.")
public String todoWrite(
@ToolParam(description = "The todo list content to save.")
String todoContent,
ToolContext toolContext
) {
String conversationId = ConversationIdUtils.getConversationId(toolContext);
if (StringUtils.isBlank(todoContent)) {
TODOLIST_CACHE.invalidate(conversationId);
return "Todo list cleared.";
}
TODOLIST_CACHE.put(conversationId, todoContent);
return "Todo list saved successfully.";
}
@Tool(description = "Read the current todo list for this conversation.")
public String todoRead(ToolContext toolContext) {
String conversationId = ConversationIdUtils.getConversationId(toolContext);
String todoContent = TODOLIST_CACHE.getIfPresent(conversationId);
if (StringUtils.isBlank(todoContent)) {
return "No todo list for this conversation.";
}
return "Current todo list:\n" + todoContent;
}
@Override
String getToolName() { return "Todo List Tool"; }
@Override
String getToolDes() { return "Read and write task todo lists"; }
}
关键点:
- 每个工具方法都接收
ToolContext参数,用于获取会话信息 - 使用 Caffeine Cache 存储临时数据,按 conversationId 隔离
