在 Spring AI 中自定义 Tool 调用返回值——实现 TodoList 提醒注入
最近发现了一个极简 Claude Code 的文档:https://learn.shareai.run/en/s03/
其中有一个实用技巧:如何在适当时机提醒 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
完整的代码地址:https://github.com/lieeew/leikooo-code-mother
依赖版本
对应的 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;privatestaticfinalSet<String>VALID_STATUSES=Set.of("pending","in_progress","completed");privatestaticfinalMap<String,String>STATUS_MARKERS=Map.of("pending","[ ]","in_progress","[>]","completed","[x]");privaterecordTodoItem(String id,String text,String status){}privatestaticfinalCache<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.")publicStringtodoUpdate(@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{String conversationId =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);returnrender(validated);}catch(IllegalArgumentException e){return"Error: "+ e.getMessage();}}privateList<TodoItem>validateAndConvert(List<Map<String,Object>> items){int inProgressCount =0;List<TodoItem> result =newArrayList<>(items.size());for(int i =0; i < items.size(); i++){Map<String,Object> item = items.get(i);String id =String.valueOf(item.getOrDefault("id",String.valueOf(i +1))).trim();String text =String.valueOf(item.getOrDefault("text","")).trim();String status =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.")publicStringtodoRead(ToolContext toolContext){String conversationId =ConversationUtils.getToolsContext(toolContext).appId();List<TodoItem> items =TODOLIST_CACHE.getIfPresent(conversationId);return items ==null|| items.isEmpty()?"No todos.":render(items);}privateStringrender(List<TodoItem> items){if(items ==null|| items.isEmpty()){return"No todos.";}StringBuilder sb =newStringBuilder("\n\n");for(TodoItem item : items){String marker =STATUS_MARKERS.getOrDefault(item.status(),"[ ]"); sb.append(marker).append(" #").append(item.id()).append(": ").append(item.text()).append("\n\n");}long done = 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();}@OverrideStringgetToolName(){return"Todo List Tool";}@OverrideStringgetToolDes(){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 synchronousreturnFlux.deferContextual(ctx ->{ToolExecutionResult toolExecutionResult;try{ToolCallReactiveContextHolder.setContext(ctx); toolExecutionResult =this.toolCallingManager.executeToolCalls(prompt, response);}finally{ToolCallReactiveContextHolder.clearContext();}returnFlux.just(ChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build());}).subscribeOn(Schedulers.boundedElastic());}returnFlux.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 获取完整的代码。详细代码位置如下图所示:

这样写好之后就可以让工具调用信号透传到 Advisor 层,判断是否有 Tool 调用。验证用的 Advisor 如下:
@Slf4jpublicclassFindToolAdvisorimplementsStreamAdvisor{@OverridepublicFlux<ChatClientResponse>adviseStream(ChatClientRequest chatClientRequest,StreamAdvisorChain streamAdvisorChain){returnFlux.deferContextual(contextView ->{ log.info("Advising stream");return streamAdvisorChain.nextStream(chatClientRequest).doOnNext(streamResponse ->{boolean hasToolCalls = streamResponse.chatResponse().hasToolCalls(); log.info("Found tool calls: {}", hasToolCalls);});});}@OverridepublicStringgetName(){return"FindToolAdvisor";}@OverridepublicintgetOrder(){return0;}}@ComponentpublicclassStreamApplicationimplementsCommandLineRunner{@ResourceprivateChatModel chatModel;@Overridepublicvoidrun(String... args)throwsException{ChatClient chatClient =ChatClient.builder(chatModel).defaultTools(FileSystemTools.builder().build()).defaultAdvisors(newFindToolAdvisor()).build();ChatClient.StreamResponseSpec stream = chatClient.prompt(""" 帮我写一个简单的 HTML 页面,路径是 E:\\TEMPLATE\\spring-skills 不超过 300 行代码 """).stream(); stream.content().subscribe(System.out::println);}}配置文件:
spring:ai:minimax:api-key: sk-cp-xxxxx chat:options:model: MiniMax-M2.5 测试结果证明工具调用信号可以被成功拦截,方案可行:

改造项目:实现 ExecuteToolAdvisor
参考 Spring AI 社区中一个尚未合并的 PR(#5383),我们实现了 ExecuteToolAdvisor,主要做了两件事:
- 工具调用 JSON 格式容错:捕获 JSON 解析异常,最多重试 3 次再抛出,提升大模型调用 Tool 时格式不规范的容错能力。
- 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 */privatefinalCache<String,Integer> roundsSinceTodo =Caffeine.newBuilder().maximumSize(10_00).expireAfterWrite(Duration.ofMinutes(30)).build();@ResourceprivateToolCallingManager toolCallingManager;@OverridepublicFlux<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()instanceofToolCallingChatOptions)){thrownewIllegalArgumentException("ExecuteToolAdvisor requires ToolCallingChatOptions to be set in the ChatClientRequest options.");}var optionsCopy =(ToolCallingChatOptions) chatClientRequest.prompt().getOptions().copy(); optionsCopy.setInternalToolExecutionEnabled(false);returninternalStream(streamAdvisorChain, chatClientRequest, optionsCopy, chatClientRequest.prompt().getInstructions(),0);}privateFlux<ChatClientResponse>internalStream(StreamAdvisorChain streamAdvisorChain,ChatClientRequest originalRequest,ToolCallingChatOptions optionsCopy,List<Message> instructions,int jsonRetryCount){returnFlux.deferContextual(contextView ->{var processedRequest =ChatClientRequest.builder().prompt(newPrompt(instructions, optionsCopy)).context(originalRequest.context()).build();StreamAdvisorChain chainCopy = 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)));});}privateFlux<ChatClientResponse>processAggregatedResponse(ChatClientResponse aggregatedResponse,List<ChatClientResponse> chunks,ChatClientRequest finalRequest,StreamAdvisorChain streamAdvisorChain,ChatClientRequest originalRequest,ToolCallingChatOptions optionsCopy,int retryCount){if(aggregatedResponse ==null){returnFlux.fromIterable(chunks);}ChatResponse chatResponse = aggregatedResponse.chatResponse();boolean isToolCall = chatResponse !=null&& chatResponse.hasToolCalls();if(isToolCall){Assert.notNull(chatResponse,"chatResponse must not be null when hasToolCalls is true");ChatClientResponse finalAggregatedResponse = 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){returninternalStream(streamAdvisorChain, originalRequest, optionsCopy, retryInstructions, retryCount +1);}}throw e;}finally{ToolCallReactiveContextHolder.clearContext();}List<Message> historyWithReminder =injectReminderIntoConversationHistory( toolExecutionResult.conversationHistory(),getAppId(finalRequest));if(toolExecutionResult.returnDirect()){returnFlux.just(buildReturnDirectResponse(finalAggregatedResponse, chatResponse, toolExecutionResult, historyWithReminder));}returninternalStream(streamAdvisorChain, originalRequest, optionsCopy, historyWithReminder,0);});return toolCallFlux.subscribeOn(Schedulers.boundedElastic());}returnFlux.fromIterable(chunks);}/** * 获取 AppId */privateStringgetAppId(ChatClientRequest finalRequest){if(finalRequest.prompt().getOptions()instanceofToolCallingChatOptions toolCallingChatOptions){return toolCallingChatOptions.getToolContext().get(CONVERSATION_ID).toString();}thrownewBusinessException(ErrorCode.SYSTEM_ERROR);}privatestaticList<Message>buildRetryInstructions(ChatClientRequest finalRequest,ChatResponse chatResponse,Throwable error){AssistantMessage assistantMessage =extractAssistantMessage(chatResponse);if(assistantMessage ==null|| assistantMessage.getToolCalls()==null|| assistantMessage.getToolCalls().isEmpty()){returnnull;}List<Message> instructions =newArrayList<>(finalRequest.prompt().getInstructions()); instructions.add(assistantMessage);String errorMessage =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;}privatestaticAssistantMessageextractAssistantMessage(ChatResponse chatResponse){if(chatResponse ==null){returnnull;}Generation result = 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;}privatestaticStringbuildJsonErrorMessage(Throwable error){String detail =ExceptionUtils.getRootCauseMessage(error);if(detail.isBlank()){returnJSON_ERROR_MESSAGE;}returnJSON_ERROR_MESSAGE+"\nError: "+ detail;}/** * 对 conversationHistory 中的 TOOL 类消息,在其每个 ToolResponse 的 responseData 后追加提醒。 */privateList<Message>injectReminderIntoConversationHistory(List<Message> conversationHistory,String appId){if(conversationHistory ==null|| conversationHistory.isEmpty()){return conversationHistory;}if(!(conversationHistory.getLast()instanceofToolResponseMessage toolMsg)){return conversationHistory;}List<ToolResponseMessage.ToolResponse> responses = toolMsg.getResponses();if(responses.isEmpty()){return conversationHistory;}ToolResponseMessage.ToolResponse firstResponse = responses.getFirst();if(!updateRoundsAndCheckReminder(appId, firstResponse.name())){return conversationHistory;}List<ToolResponseMessage.ToolResponse> newResponses =newArrayList<>(responses);ToolResponseMessage.ToolResponse actualRes = 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。 */privatestaticChatClientResponsebuildReturnDirectResponse(ChatClientResponse aggregatedResponse,ChatResponse chatResponse,ToolExecutionResult originalResult,List<Message> historyWithReminder){ToolExecutionResult resultWithReminder =ToolExecutionResult.builder().conversationHistory(historyWithReminder).returnDirect(originalResult.returnDirect()).build();ChatResponse newChatResponse =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;}int count = roundsSinceTodo.asMap().merge(appId,1,Integer::sum);return count >=REMINDER_THRESHOLD;}@OverridepublicStringgetName(){return"ExecuteToolAdvisor";}@OverridepublicintgetOrder(){returnORDER;}}因为这个 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 */StreamAdvisorChaincopy(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();privatefinalList<CallAdvisor> originalCallAdvisors;privatefinalList<StreamAdvisor> originalStreamAdvisors;privatefinalDeque<CallAdvisor> callAdvisors;privatefinalDeque<StreamAdvisor> streamAdvisors;privatefinalObservationRegistry observationRegistry;privatefinalAdvisorObservationConvention observationConvention;DefaultAroundAdvisorChain(ObservationRegistry observationRegistry,Deque<CallAdvisor> callAdvisors,Deque<StreamAdvisor> streamAdvisors,@NullableAdvisorObservationConvention 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;}publicstaticBuilderbuilder(ObservationRegistry observationRegistry){returnnewBuilder(observationRegistry);}@OverridepublicChatClientResponsenextCall(ChatClientRequest chatClientRequest){Assert.notNull(chatClientRequest,"the chatClientRequest cannot be null");if(this.callAdvisors.isEmpty()){thrownewIllegalStateException("No CallAdvisors available to execute");}var advisor =this.callAdvisors.pop();var observationContext =AdvisorObservationContext.builder().advisorName(advisor.getName()).chatClientRequest(chatClientRequest).order(advisor.getOrder()).build();returnAdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention,DEFAULT_OBSERVATION_CONVENTION,()-> observationContext,this.observationRegistry).observe(()->{var chatClientResponse = advisor.adviseCall(chatClientRequest,this); observationContext.setChatClientResponse(chatClientResponse);return chatClientResponse;});}@OverridepublicFlux<ChatClientResponse>nextStream(ChatClientRequest chatClientRequest){Assert.notNull(chatClientRequest,"the chatClientRequest cannot be null");returnFlux.deferContextual(contextView ->{if(this.streamAdvisors.isEmpty()){returnFlux.error(newIllegalStateException("No StreamAdvisors available to execute"));}var advisor =this.streamAdvisors.pop();AdvisorObservationContext observationContext =AdvisorObservationContext.builder().advisorName(advisor.getName()).chatClientRequest(chatClientRequest).order(advisor.getOrder()).build();var observation =AdvisorObservationDocumentation.AI_ADVISOR.observation(this.observationConvention,DEFAULT_OBSERVATION_CONVENTION,()-> observationContext,this.observationRegistry); observation.parentObservation(contextView.getOrDefault(ObservationThreadLocalAccessor.KEY,null)).start();// @formatter:offFlux<ChatClientResponse> chatClientResponse =Flux.defer(()-> advisor.adviseStream(chatClientRequest,this).doOnError(observation::error).doFinally(s -> observation.stop()).contextWrite(ctx -> ctx.put(ObservationThreadLocalAccessor.KEY, observation)));// @formatter:onreturnCHAT_CLIENT_MESSAGE_AGGREGATOR.aggregateChatClientResponse(chatClientResponse, observationContext::setChatClientResponse);});}@OverridepublicCallAdvisorChaincopy(CallAdvisor after){returnthis.copyAdvisorsAfter(this.getCallAdvisors(), after);}@OverridepublicStreamAdvisorChaincopy(StreamAdvisor after){returnthis.copyAdvisorsAfter(this.getStreamAdvisors(), after);}privateDefaultAroundAdvisorChaincopyAdvisorsAfter(List<?extendsAdvisor> advisors,Advisor after){Assert.notNull(after,"The after advisor must not be null");Assert.notNull(advisors,"The advisors must not be null");int afterAdvisorIndex = advisors.indexOf(after);if(afterAdvisorIndex <0){thrownewIllegalArgumentException("The specified advisor is not part of the chain: "+ after.getName());}var remainingStreamAdvisors = advisors.subList(afterAdvisorIndex +1, advisors.size());returnDefaultAroundAdvisorChain.builder(this.getObservationRegistry()).pushAll(remainingStreamAdvisors).build();}@OverridepublicList<CallAdvisor>getCallAdvisors(){returnthis.originalCallAdvisors;}@OverridepublicList<StreamAdvisor>getStreamAdvisors(){returnthis.originalStreamAdvisors;}@OverridepublicObservationRegistrygetObservationRegistry(){returnthis.observationRegistry;}publicstaticfinalclassBuilder{privatefinalObservationRegistry observationRegistry;privatefinalDeque<CallAdvisor> callAdvisors;privatefinalDeque<StreamAdvisor> streamAdvisors;private@NullableAdvisorObservationConvention observationConvention;publicBuilder(ObservationRegistry observationRegistry){this.observationRegistry = observationRegistry;this.callAdvisors =newConcurrentLinkedDeque<>();this.streamAdvisors =newConcurrentLinkedDeque<>();}publicBuilderobservationConvention(@NullableAdvisorObservationConvention observationConvention){this.observationConvention = observationConvention;returnthis;}publicBuilderpush(Advisor advisor){Assert.notNull(advisor,"the advisor must be non-null");returnthis.pushAll(List.of(advisor));}publicBuilderpushAll(List<?extendsAdvisor> 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 instanceofCallAdvisor).map(a ->(CallAdvisor) a).toList();if(!CollectionUtils.isEmpty(callAroundAdvisorList)){ callAroundAdvisorList.forEach(this.callAdvisors::push);}List<StreamAdvisor> streamAroundAdvisorList = advisors.stream().filter(a -> a instanceofStreamAdvisor).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);}publicDefaultAroundAdvisorChainbuild(){returnnewDefaultAroundAdvisorChain(this.observationRegistry,this.callAdvisors,this.streamAdvisors,this.observationConvention);}}}效果验证&前端展示
工具调用 JSON 格式错误时自动重试:

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

前端效果:


