乐于分享
好东西不私藏

Spring AI 工具调用改造实战

Spring AI 工具调用改造实战

最近发现了一个极简 Claude Code 的文档:https://learn.shareai.run/en/s03/[1]

其中有一个实用技巧:如何在适当时机提醒 AI 更新 TodoList? 文档中的做法是:当连续三次工具调用都没有触发 todo 更新操作时,在 Function Call 返回值的第一个位置插入一条提醒:

<reminder>Update your todos.</reminder>

对应的 Python 实现如下:

if rounds_since_todo >= 3and messages:    last = messages[-1]if last["role"] == "user"andisinstance(last.get("content"), list):        last["content"].insert(0, {"type""text","text""<reminder>Update your todos.</reminder>",        })

那么在 Spring AI 中能否实现同样的效果?经过一番研究,答案是可以的。本文记录实现过程。

代码仓库

项目完整 LangChain4j 代码实现:https://www.codefather.cn/course/1948291549923344386[2]

完整的代码地址:https://github.com/lieeew/leikooo-code-mother[3]

依赖版本

对应的 SpringAI 版本和 SpringBoot 依赖:

<properties><java.version>21</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>4.0.1</spring-boot.version><spring-ai.version>2.0.0-M2</spring-ai.version></properties><dependencies><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-minimax</artifactId></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-bom</artifactId><version>${spring-ai.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>

定义 TodoList Tool

首先需要定义供 LLM 调用的 Tool。以下是完整实现,包含读取和写入两个操作,并通过 Caffeine 本地缓存按会话隔离存储:

/** * @author <a href="https://github.com/lieeew">leikooo</a> * @date 2025/12/31 */@ComponentpublicclassTodolistToolsextendsBaseTools {privatestaticfinalintMAX_TODOS=20;privatestaticfinal Set<String> VALID_STATUSES = Set.of("pending""in_progress""completed");privatestaticfinal Map<String, String> STATUS_MARKERS = Map.of("pending""[ ]","in_progress""[>]","completed""[x]"    );privaterecordTodoItem(String id, String text, String status) {}privatestaticfinal Cache<String, List<TodoItem>> TODOLIST_CACHE = Caffeine.newBuilder()            .maximumSize(1000)            .expireAfterWrite(Duration.ofMinutes(30))            .build();@Tool(description = "Update task list. Track progress on multi-step tasks. Pass the full list of items each time (replaces previous list). " +            "Each item must have id, text, status. Status: pending, in_progress, completed. Only one item can be in_progress."    )public String todoUpdate(@ToolParam(description = "Full list of todo items. Each item: id (string), text (string), status (pending|in_progress|completed).")            List<Map<String, Object>> items,            ToolContext toolContext    ) {try {StringconversationId= ConversationUtils.getToolsContext(toolContext).appId();if (items == null || items.isEmpty()) {                TODOLIST_CACHE.invalidate(conversationId);return"No todos.";            }if (items.size() > MAX_TODOS) {return"Error: Max " + MAX_TODOS + " todos allowed";            }            List<TodoItem> validated = validateAndConvert(items);            TODOLIST_CACHE.put(conversationId, validated);return render(validated);        } catch (IllegalArgumentException e) {return"Error: " + e.getMessage();        }    }private List<TodoItem> validateAndConvert(List<Map<String, Object>> items) {intinProgressCount=0;        List<TodoItem> result = newArrayList<>(items.size());for (inti=0; i < items.size(); i++) {            Map<String, Object> item = items.get(i);Stringid= String.valueOf(item.getOrDefault("id", String.valueOf(i + 1))).trim();Stringtext= String.valueOf(item.getOrDefault("text""")).trim();Stringstatus= String.valueOf(item.getOrDefault("status""pending")).toLowerCase();if (StringUtils.isBlank(text)) {thrownewIllegalArgumentException("Item " + id + ": text required");            }if (!VALID_STATUSES.contains(status)) {thrownewIllegalArgumentException("Item " + id + ": invalid status '" + status + "'");            }if ("in_progress".equals(status)) {                inProgressCount++;            }            result.add(newTodoItem(id, text, status));        }if (inProgressCount > 1) {thrownewIllegalArgumentException("Only one task can be in_progress at a time");        }return result;    }@Tool(description = "Read the current todo list for this conversation. Use this to check progress and see what tasks remain.")public String todoRead(ToolContext toolContext) {StringconversationId= ConversationUtils.getToolsContext(toolContext).appId();        List<TodoItem> items = TODOLIST_CACHE.getIfPresent(conversationId);return items == null || items.isEmpty() ? "No todos." : render(items);    }private String render(List<TodoItem> items) {if (items == null || items.isEmpty()) {return"No todos.";        }StringBuildersb=newStringBuilder("\n\n");for (TodoItem item : items) {Stringmarker= STATUS_MARKERS.getOrDefault(item.status(), "[ ]");            sb.append(marker).append(" #").append(item.id()).append(": ").append(item.text()).append("\n\n");        }longdone= items.stream().filter(t -> "completed".equals(t.status())).count();        sb.append("\n(").append(done).append("/").append(items.size()).append(" completed)");return sb.append("\n\n").toString();    }@Override    String getToolName() { return"Todo List Tool"; }@Override    String getToolDes() { return"Read and write task todo lists to track progress"; }}

问题分析:为什么不能在普通 Advisor 中拦截工具调用?

通过阅读源码 org.springframework.ai.minimax.MiniMaxChatModel#stream 可以发现,框架内部会在 ChatModel 层直接执行 Tool 调用,而不是将其透传给 Advisor 链。核心执行逻辑如下:

// Tool 调用的核心逻辑,如下:Flux<ChatResponse> flux = chatResponse.flatMap(response -> {if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(requestPrompt.getOptions(), response)) {// FIXME: bounded elastic needs to be used since tool calling//  is currently only synchronousreturn Flux.deferContextual(ctx -> {            ToolExecutionResult toolExecutionResult;try {                ToolCallReactiveContextHolder.setContext(ctx);                toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);            }finally {                ToolCallReactiveContextHolder.clearContext();            }return Flux.just(ChatResponse.builder().from(response)                    .generations(ToolExecutionResult.buildGenerations(toolExecutionResult))                    .build());        }).subscribeOn(Schedulers.boundedElastic());    }return Flux.just(response);}).doOnError(observation::error).doFinally(signalType -> observation.stop()).contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation));

这意味着,如果我们在外层 Advisor 中尝试拦截 tool_call,此时工具已经执行完毕,并且无法识别到工具调用。所以我准备使用我自己写的 MiniMaxChatModel 覆盖掉这个源码的逻辑,之后再 Advisor 接管这个 Tool 执行。

验证思路:能否通过 Advisor 接管工具执行?

我们需要在自己的项目目录创建一个 org.springframework.ai.minimax.MiniMaxChatModel 具体文件内容可以访问 https://www.codecopy.cn/post/7lonmm[4] 获取完整的代码。详细代码位置如下图所示:

image.png

这样写好之后就可以让工具调用信号透传到 Advisor 层,判断是否有 Tool 调用。验证用的 Advisor 如下:

@Slf4jpublicclassFindToolAdvisorimplementsStreamAdvisor {@Overridepublic Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {return Flux.deferContextual(contextView -> {            log.info("Advising stream");return streamAdvisorChain.nextStream(chatClientRequest).doOnNext(streamResponse -> {booleanhasToolCalls= streamResponse.chatResponse().hasToolCalls();                log.info("Found tool calls: {}", hasToolCalls);            });        });    }@Overridepublic String getName() { return"FindToolAdvisor"; }@OverridepublicintgetOrder() { return0; }}
@ComponentpublicclassStreamApplicationimplementsCommandLineRunner {@Resourceprivate ChatModel chatModel;@Overridepublicvoidrun(String... args)throws Exception {ChatClientchatClient= ChatClient.builder(chatModel)                .defaultTools(FileSystemTools.builder().build())                .defaultAdvisors(newFindToolAdvisor())                .build();        ChatClient.StreamResponseSpecstream= chatClient.prompt("""                帮我写一个简单的 HTML 页面,路径是 E:\\TEMPLATE\\spring-skills 不超过 300 行代码                """).stream();        stream.content().subscribe(System.out::println);    }}

配置文件:

spring:ai:minimax:api-key:sk-cp-xxxxxchat:options:model:MiniMax-M2.5

测试结果证明工具调用信号可以被成功拦截,方案可行:

拦截到工具调用的日志截图

改造项目:实现 ExecuteToolAdvisor

参考 Spring AI 社区中一个尚未合并的 PR(#5383[5]),我们实现了 ExecuteToolAdvisor,主要做了两件事:

  1. 工具调用 JSON 格式容错:捕获 JSON 解析异常,最多重试 3 次再抛出,提升大模型调用 Tool 时格式不规范的容错能力。
  2. TodoList 提醒注入:连续 3 次工具调用均未触发 todoUpdate 时,在 ToolResponseMessage 的第一个位置注入提醒,引导 AI 及时更新任务列表。

⚠️ 注意 order 顺序:由于该 Advisor 接管了工具执行,它的 order 值应尽量大(即靠后执行)。若 order 较小,可能导致后续 Advisor 的 doFinally 在每次工具调用时都被触发(比如后面的 buildAdvisor、versionAdvisor 只需要执行一次),而非在整个对话结束时触发一次。本实现中使用 Integer.MAX_VALUE - 100

/** * 手动执行 tool 的 StreamAdvisor:关闭框架内部执行,自行执行并可在工具返回值中注入提醒(如更新 todo)。 * * @author <a href="https://github.com/lieeew">leikooo</a> * @date 2026/3/14 */@Slf4j@ComponentpublicclassExecuteToolAdvisorimplementsStreamAdvisor {privatestaticfinalStringTODO_REMINDER="<reminder>Update your todos.</reminder>";privatestaticfinalStringJSON_ERROR_MESSAGE="Tool call JSON parse failed. Fix and retry.\n"                    + "Rules: strict RFC8259 JSON, no trailing commas, no comments, "                    + "no unescaped control chars in strings (escape newlines as \\n, tabs as \\t), "                    + "all keys double-quoted.";privatestaticfinalintMAX_TOOL_RETRY=3;privatestaticfinalintORDER= Integer.MAX_VALUE - 100;privatestaticfinalStringTODO_METHOD="todoUpdate";privatestaticfinalintREMINDER_THRESHOLD=3;/**     * 三次工具没有使用 todoTool 那么就在 tool_result[0] 位置添加 TODO_REMINDER     */privatefinal Cache<String, Integer> roundsSinceTodo = Caffeine.newBuilder()            .maximumSize(10_00)            .expireAfterWrite(Duration.ofMinutes(30))            .build();@Resourceprivate ToolCallingManager toolCallingManager;@Overridepublic Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {        Assert.notNull(streamAdvisorChain, "streamAdvisorChain must not be null");        Assert.notNull(chatClientRequest, "chatClientRequest must not be null");if (chatClientRequest.prompt().getOptions() == null                || !(chatClientRequest.prompt().getOptions() instanceof ToolCallingChatOptions)) {thrownewIllegalArgumentException("ExecuteToolAdvisor requires ToolCallingChatOptions to be set in the ChatClientRequest options.");        }varoptionsCopy= (ToolCallingChatOptions) chatClientRequest.prompt().getOptions().copy();        optionsCopy.setInternalToolExecutionEnabled(false);return internalStream(streamAdvisorChain, chatClientRequest, optionsCopy,                chatClientRequest.prompt().getInstructions(), 0);    }private Flux<ChatClientResponse> internalStream(            StreamAdvisorChain streamAdvisorChain,            ChatClientRequest originalRequest,            ToolCallingChatOptions optionsCopy,            List<Message> instructions,int jsonRetryCount) {return Flux.deferContextual(contextView -> {varprocessedRequest= ChatClientRequest.builder()                    .prompt(newPrompt(instructions, optionsCopy))                    .context(originalRequest.context())                    .build();StreamAdvisorChainchainCopy= streamAdvisorChain.copy(this);            Flux<ChatClientResponse> responseFlux = chainCopy.nextStream(processedRequest);            AtomicReference<ChatClientResponse> aggregatedResponseRef = newAtomicReference<>();            AtomicReference<List<ChatClientResponse>> chunksRef = newAtomicReference<>(newArrayList<>());returnnewChatClientMessageAggregator()                    .aggregateChatClientResponse(responseFlux, aggregatedResponseRef::set)                    .doOnNext(chunk -> chunksRef.get().add(chunk))                    .ignoreElements()                    .cast(ChatClientResponse.class)                    .concatWith(Flux.defer(() -> processAggregatedResponse(                            aggregatedResponseRef.get(), chunksRef.get(), processedRequest,                            streamAdvisorChain, originalRequest, optionsCopy, jsonRetryCount)));        });    }private Flux<ChatClientResponse> processAggregatedResponse(            ChatClientResponse aggregatedResponse,            List<ChatClientResponse> chunks,            ChatClientRequest finalRequest,            StreamAdvisorChain streamAdvisorChain,            ChatClientRequest originalRequest,            ToolCallingChatOptions optionsCopy,int retryCount) {if (aggregatedResponse == null) {return Flux.fromIterable(chunks);        }ChatResponsechatResponse= aggregatedResponse.chatResponse();booleanisToolCall= chatResponse != null && chatResponse.hasToolCalls();if (isToolCall) {            Assert.notNull(chatResponse, "chatResponse must not be null when hasToolCalls is true");ChatClientResponsefinalAggregatedResponse= aggregatedResponse;            Flux<ChatClientResponse> toolCallFlux = Flux.deferContextual(ctx -> {                ToolExecutionResult toolExecutionResult;try {                    ToolCallReactiveContextHolder.setContext(ctx);                    toolExecutionResult = toolCallingManager.executeToolCalls(finalRequest.prompt(), chatResponse);                } catch (Exception e) {if (retryCount < MAX_TOOL_RETRY) {                        List<Message> retryInstructions = buildRetryInstructions(finalRequest, chatResponse, e);if (retryInstructions != null) {return internalStream(streamAdvisorChain, originalRequest, optionsCopy,                                    retryInstructions, retryCount + 1);                        }                    }throw e;                } finally {                    ToolCallReactiveContextHolder.clearContext();                }                List<Message> historyWithReminder = injectReminderIntoConversationHistory(                        toolExecutionResult.conversationHistory(), getAppId(finalRequest));if (toolExecutionResult.returnDirect()) {return Flux.just(buildReturnDirectResponse(finalAggregatedResponse, chatResponse,                            toolExecutionResult, historyWithReminder));                }return internalStream(streamAdvisorChain, originalRequest, optionsCopy, historyWithReminder, 0);            });return toolCallFlux.subscribeOn(Schedulers.boundedElastic());        }return Flux.fromIterable(chunks);    }/**     * 获取 AppId     */private String getAppId(ChatClientRequest finalRequest) {if (finalRequest.prompt().getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {return toolCallingChatOptions.getToolContext().get(CONVERSATION_ID).toString();        }thrownewBusinessException(ErrorCode.SYSTEM_ERROR);    }privatestatic List<Message> buildRetryInstructions(ChatClientRequest finalRequest,                                                        ChatResponse chatResponse,                                                        Throwable error) {AssistantMessageassistantMessage= extractAssistantMessage(chatResponse);if (assistantMessage == null || assistantMessage.getToolCalls() == null                || assistantMessage.getToolCalls().isEmpty()) {returnnull;        }        List<Message> instructions = newArrayList<>(finalRequest.prompt().getInstructions());        instructions.add(assistantMessage);StringerrorMessage= buildJsonErrorMessage(error);        List<ToolResponseMessage.ToolResponse> responses = assistantMessage.getToolCalls().stream()                .map(toolCall -> newToolResponseMessage.ToolResponse(                        toolCall.id(),                        toolCall.name(),                        errorMessage))                .toList();        instructions.add(ToolResponseMessage.builder().responses(responses).build());return instructions;    }privatestatic AssistantMessage extractAssistantMessage(ChatResponse chatResponse) {if (chatResponse == null) {returnnull;        }Generationresult= chatResponse.getResult();if (result != null && result.getOutput() != null) {return result.getOutput();        }        List<Generation> results = chatResponse.getResults();if (results != null && !results.isEmpty() && results.get(0).getOutput() != null) {return results.get(0).getOutput();        }returnnull;    }privatestatic String buildJsonErrorMessage(Throwable error) {Stringdetail= ExceptionUtils.getRootCauseMessage(error);if (detail.isBlank()) {return JSON_ERROR_MESSAGE;        }return JSON_ERROR_MESSAGE + "\nError: " + detail;    }/**     * 对 conversationHistory 中的 TOOL 类消息,在其每个 ToolResponse 的 responseData 后追加提醒。     */private List<Message> injectReminderIntoConversationHistory(List<Message> conversationHistory, String appId) {if (conversationHistory == null || conversationHistory.isEmpty()) {return conversationHistory;        }if (!(conversationHistory.getLast() instanceof ToolResponseMessage toolMsg)) {return conversationHistory;        }        List<ToolResponseMessage.ToolResponse> responses = toolMsg.getResponses();if (responses.isEmpty()) {return conversationHistory;        }        ToolResponseMessage.ToolResponsefirstResponse= responses.getFirst();if (!updateRoundsAndCheckReminder(appId, firstResponse.name())) {return conversationHistory;        }        List<ToolResponseMessage.ToolResponse> newResponses = newArrayList<>(responses);        ToolResponseMessage.ToolResponseactualRes= newResponses.removeFirst();        newResponses.add(newToolResponseMessage.ToolResponse(                firstResponse.id(), "text", TODO_REMINDER));        newResponses.add(actualRes);        List<Message> result = newArrayList<>(                conversationHistory.subList(0, conversationHistory.size() - 1));        result.add(ToolResponseMessage.builder().responses(newResponses).build());return result;    }/**     * 构造 returnDirect 时的 ChatClientResponse,使用注入提醒后的 conversationHistory 生成 generations。     */privatestatic ChatClientResponse buildReturnDirectResponse(            ChatClientResponse aggregatedResponse,            ChatResponse chatResponse,            ToolExecutionResult originalResult,            List<Message> historyWithReminder) {ToolExecutionResultresultWithReminder= ToolExecutionResult.builder()                .conversationHistory(historyWithReminder)                .returnDirect(originalResult.returnDirect())                .build();ChatResponsenewChatResponse= ChatResponse.builder()                .from(chatResponse)                .generations(ToolExecutionResult.buildGenerations(resultWithReminder))                .build();return aggregatedResponse.mutate().chatResponse(newChatResponse).build();    }/**     * updateRoundsAndCheckReminder     * @param appId appId     * @param methodName methodName     * @return 是否需要更新     */privatebooleanupdateRoundsAndCheckReminder(String appId, String methodName) {if (TODO_METHOD.equals(methodName)) {            roundsSinceTodo.put(appId, 0);returnfalse;        }intcount= roundsSinceTodo.asMap().merge(appId, 1, Integer::sum);return count >= REMINDER_THRESHOLD;    }@Overridepublic String getName() {return"ExecuteToolAdvisor";    }@OverridepublicintgetOrder() {return ORDER;    }}

因为这个 Advisor 也使用到了 StreamAdvisorChain  接口的 copy 所以我们需要覆盖源码的这个 StreamAdvisorChain 并且实现对应的接口,下面的代码包路径是 org.springframework.ai.chat.client.advisor.api 具体的代码:

publicinterfaceStreamAdvisorChainextendsAdvisorChain {/**     * Invokes the next {@link StreamAdvisor} in the {@link StreamAdvisorChain} with the     * given request.     */    Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest);/**     * Returns the list of all the {@link StreamAdvisor} instances included in this chain     * at the time of its creation.     */    List<StreamAdvisor> getStreamAdvisors();/**     * Creates a new StreamAdvisorChain copy that contains all advisors after the     * specified advisor.     * @param after the StreamAdvisor after which to copy the chain     * @return a new StreamAdvisorChain containing all advisors after the specified     * advisor     * @throws IllegalArgumentException if the specified advisor is not part of the chain     */    StreamAdvisorChain copy(StreamAdvisor after);}

下面的包位置是 org.springframework.ai.chat.client.advisor具体的实现代码:

/** * Default implementation for the {@link BaseAdvisorChain}. Used by the {@link ChatClient} * to delegate the call to the next {@link CallAdvisor} or {@link StreamAdvisor} in the * chain. * * @author Christian Tzolov * @author Dariusz Jedrzejczyk * @author Thomas Vitale * @since 1.0.0 */publicclassDefaultAroundAdvisorChainimplementsBaseAdvisorChain {publicstaticfinalAdvisorObservationConventionDEFAULT_OBSERVATION_CONVENTION=newDefaultAdvisorObservationConvention();privatestaticfinalChatClientMessageAggregatorCHAT_CLIENT_MESSAGE_AGGREGATOR=newChatClientMessageAggregator();privatefinal List<CallAdvisor> originalCallAdvisors;privatefinal List<StreamAdvisor> originalStreamAdvisors;privatefinal Deque<CallAdvisor> callAdvisors;privatefinal Deque<StreamAdvisor> streamAdvisors;privatefinal ObservationRegistry observationRegistry;privatefinal AdvisorObservationConvention observationConvention;  DefaultAroundAdvisorChain(ObservationRegistry observationRegistry, Deque<CallAdvisor> callAdvisors,      Deque<StreamAdvisor> streamAdvisors, @Nullable AdvisorObservationConvention observationConvention) {    Assert.notNull(observationRegistry, "the observationRegistry must be non-null");    Assert.notNull(callAdvisors, "the callAdvisors must be non-null");    Assert.notNull(streamAdvisors, "the streamAdvisors must be non-null");this.observationRegistry = observationRegistry;this.callAdvisors = callAdvisors;this.streamAdvisors = streamAdvisors;this.originalCallAdvisors = List.copyOf(callAdvisors);this.originalStreamAdvisors = List.copyOf(streamAdvisors);this.observationConvention = observationConvention != null ? observationConvention        : DEFAULT_OBSERVATION_CONVENTION;  }publicstatic Builder builder(ObservationRegistry observationRegistry) {returnnewBuilder(observationRegistry);  }@Overridepublic ChatClientResponse nextCall(ChatClientRequest chatClientRequest) {    Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");if (this.callAdvisors.isEmpty()) {thrownewIllegalStateException("No CallAdvisors available to execute");    }varadvisor=this.callAdvisors.pop();varobservationContext= AdvisorObservationContext.builder()      .advisorName(advisor.getName())      .chatClientRequest(chatClientRequest)      .order(advisor.getOrder())      .build();return AdvisorObservationDocumentation.AI_ADVISOR      .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry)      .observe(() -> {varchatClientResponse= advisor.adviseCall(chatClientRequest, this);        observationContext.setChatClientResponse(chatClientResponse);return chatClientResponse;      });  }@Overridepublic Flux<ChatClientResponse> nextStream(ChatClientRequest chatClientRequest) {    Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");return Flux.deferContextual(contextView -> {if (this.streamAdvisors.isEmpty()) {return Flux.error(newIllegalStateException("No StreamAdvisors available to execute"));      }varadvisor=this.streamAdvisors.pop();AdvisorObservationContextobservationContext= AdvisorObservationContext.builder()        .advisorName(advisor.getName())        .chatClientRequest(chatClientRequest)        .order(advisor.getOrder())        .build();varobservation= AdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention,          DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry);      observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY, null)).start();// @formatter:off      Flux<ChatClientResponse> chatClientResponse = Flux.defer(() -> advisor.adviseStream(chatClientRequest, this)            .doOnError(observation::error)            .doFinally(s -> observation.stop())            .contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation)));// @formatter:onreturn CHAT_CLIENT_MESSAGE_AGGREGATOR.aggregateChatClientResponse(chatClientResponse,          observationContext::setChatClientResponse);    });  }@Overridepublic CallAdvisorChain copy(CallAdvisor after) {returnthis.copyAdvisorsAfter(this.getCallAdvisors(), after);  }@Overridepublic StreamAdvisorChain copy(StreamAdvisor after) {returnthis.copyAdvisorsAfter(this.getStreamAdvisors(), after);  }private DefaultAroundAdvisorChain copyAdvisorsAfter(List<? extends Advisor> advisors, Advisor after) {    Assert.notNull(after, "The after advisor must not be null");    Assert.notNull(advisors, "The advisors must not be null");intafterAdvisorIndex= advisors.indexOf(after);if (afterAdvisorIndex < 0) {thrownewIllegalArgumentException("The specified advisor is not part of the chain: " + after.getName());    }varremainingStreamAdvisors= advisors.subList(afterAdvisorIndex + 1, advisors.size());return DefaultAroundAdvisorChain.builder(this.getObservationRegistry())      .pushAll(remainingStreamAdvisors)      .build();  }@Overridepublic List<CallAdvisor> getCallAdvisors() {returnthis.originalCallAdvisors;  }@Overridepublic List<StreamAdvisor> getStreamAdvisors() {returnthis.originalStreamAdvisors;  }@Overridepublic ObservationRegistry getObservationRegistry() {returnthis.observationRegistry;  }publicstaticfinalclassBuilder {privatefinal ObservationRegistry observationRegistry;privatefinal Deque<CallAdvisor> callAdvisors;privatefinal Deque<StreamAdvisor> streamAdvisors;private@Nullable AdvisorObservationConvention observationConvention;publicBuilder(ObservationRegistry observationRegistry) {this.observationRegistry = observationRegistry;this.callAdvisors = newConcurrentLinkedDeque<>();this.streamAdvisors = newConcurrentLinkedDeque<>();    }public Builder observationConvention(@Nullable AdvisorObservationConvention observationConvention) {this.observationConvention = observationConvention;returnthis;    }public Builder push(Advisor advisor) {      Assert.notNull(advisor, "the advisor must be non-null");returnthis.pushAll(List.of(advisor));    }public Builder pushAll(List<? extends Advisor> advisors) {      Assert.notNull(advisors, "the advisors must be non-null");      Assert.noNullElements(advisors, "the advisors must not contain null elements");if (!CollectionUtils.isEmpty(advisors)) {        List<CallAdvisor> callAroundAdvisorList = advisors.stream()          .filter(a -> a instanceof CallAdvisor)          .map(a -> (CallAdvisor) a)          .toList();if (!CollectionUtils.isEmpty(callAroundAdvisorList)) {          callAroundAdvisorList.forEach(this.callAdvisors::push);        }        List<StreamAdvisor> streamAroundAdvisorList = advisors.stream()          .filter(a -> a instanceof StreamAdvisor)          .map(a -> (StreamAdvisor) a)          .toList();if (!CollectionUtils.isEmpty(streamAroundAdvisorList)) {          streamAroundAdvisorList.forEach(this.streamAdvisors::push);        }this.reOrder();      }returnthis;    }/**     * (Re)orders the advisors in priority order based on their Ordered attribute.     */privatevoidreOrder() {      ArrayList<CallAdvisor> callAdvisors = newArrayList<>(this.callAdvisors);      OrderComparator.sort(callAdvisors);this.callAdvisors.clear();      callAdvisors.forEach(this.callAdvisors::addLast);      ArrayList<StreamAdvisor> streamAdvisors = newArrayList<>(this.streamAdvisors);      OrderComparator.sort(streamAdvisors);this.streamAdvisors.clear();      streamAdvisors.forEach(this.streamAdvisors::addLast);    }public DefaultAroundAdvisorChain build() {returnnewDefaultAroundAdvisorChain(this.observationRegistry, this.callAdvisors, this.streamAdvisors,this.observationConvention);    }  }}

效果验证&前端展示

工具调用 JSON 格式错误时自动重试:

工具调用报错重试截图

连续三次未更新 TodoList 时触发提醒注入:

TodoList 提醒注入截图

前端效果:

image-20260316224248596
image-20260316224535475
image-20260316224624794

References

  1. https://learn.shareai.run/en/s03/: https://learn.shareai.run/en/s03/
  2. https://www.codefather.cn/course/1948291549923344386: https://www.codefather.cn/course/1948291549923344386
  3. https://github.com/lieeew/leikooo-code-mother: https://github.com/lieeew/leikooo-code-mother
  4. https://www.codecopy.cn/post/7lonmm: https://www.codecopy.cn/post/7lonmm
  5. #5383https://github.com/spring-projects/spring-ai/pull/5383/changes#diff-bcbb3a4a1b73000ad9c6da43034ee6b679d8279315dd894ae078dbc69dfcf860
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » Spring AI 工具调用改造实战

猜你喜欢

  • 暂无文章