当前位置: 首页 > news >正文

spring-ai advisors 使用与源码分析

advisors
spring-ai v1.0.3

为什么要用 Advisors?

Spring AI Advisors API提供了一种灵活而强大的方式来拦截、修改和增强Spring应用程序中AI驱动的交互。通过利用Advisors API,开发人员可以创建更复杂、可重用和可维护的AI组件。
常见的使用场景

  • 统一拦截:把日志、memory、tool调用、检索、审计等横切逻辑从业务中剥离。
  • 解放双手:原生 OpenAI 的 tool_calls 需要你手写循环(解析→调用→回补→再问);Advisor 帮你自动闭环。

源码分析

核心接口

在这里插入图片描述

BaseAdvisor

public interface BaseAdvisor extends CallAdvisor, StreamAdvisor {@Overridedefault ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {//1. do - before callChatClientRequest processedChatClientRequest = before(chatClientRequest, callAdvisorChain);//2. execute callChatClientResponse chatClientResponse = callAdvisorChain.nextCall(processedChatClientRequest);//3. do - after callreturn after(chatClientResponse, callAdvisorChain);}default Flux<ChatClientResponse> adviseStream(ChatClientRequest chatClientRequest,StreamAdvisorChain streamAdvisorChain) {/*略...*/}ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain);ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain);
}

自定义MessageChatMemoryAdvisor - 源码分析

定义JDBC ChatMemory

  JdbcChatMemoryRepository chatMemoryRepository = ...; //3ChatMemory chatMemory = MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository) //2.maxMessages(10).build();ChatClient chatClient = ChatClient.builder(openAiChatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) // 1.build();

BaseChatMemoryAdvisor & MessageChatMemoryAdvisor

public interface BaseChatMemoryAdvisor extends BaseAdvisor {default String getConversationId(Map<String, Object> context, String defaultConversationId) {//ChatMemory.CONVERSATION_ID = "chat_memory_conversation_id" return context.containsKey(ChatMemory.CONVERSATION_ID) ? context.get(ChatMemory.CONVERSATION_ID).toString(): defaultConversationId;}}public final class MessageChatMemoryAdvisor implements BaseChatMemoryAdvisor {//constructor 注入private final ChatMemory chatMemory;public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {//0.获取converstationIdString conversationId = getConversationId(chatClientRequest.context(), this.defaultConversationId);// 1. 从chatMemory获取当前conversationId的messages -- todoList<Message> memoryMessages = this.chatMemory.get(conversationId);// 2. 将#1和promote中message合并List<Message> processedMessages = new ArrayList<>(memoryMessages);processedMessages.addAll(chatClientRequest.prompt().getInstructions());// 3.更新request中的messageChatClientRequest processedChatClientRequest = chatClientRequest.mutate().prompt(chatClientRequest.prompt().mutate().messages(processedMessages).build()).build();// 4. Add the new user message to the conversation memory.UserMessage userMessage = processedChatClientRequest.prompt().getUserMessage();this.chatMemory.add(conversationId, userMessage);return processedChatClientRequest;}public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {List<Message> assistantMessages = new ArrayList<>();if (chatClientResponse.chatResponse() != null) {assistantMessages = chatClientResponse.chatResponse().getResults().stream().map(g -> (Message) g.getOutput()).toList();}//添加message 至chatMemorythis.chatMemory.add(this.getConversationId(chatClientResponse.context(), this.defaultConversationId),assistantMessages);return chatClientResponse;}
}

ChatMemory & MessageWindowChatMemory

public interface ChatMemory {String CONVERSATION_ID = "chat_memory_conversation_id";void add(String conversationId, List<Message> messages);List<Message> get(String conversationId);void clear(String conversationId);
}public final class MessageWindowChatMemory implements ChatMemory {private final ChatMemoryRepository chatMemoryRepository;private final int maxMessages;//单个conversationId,最大的messages 条数public List<Message> get(String conversationId) {return this.chatMemoryRepository.findByConversationId(conversationId);}public void clear(String conversationId) {this.chatMemoryRepository.deleteByConversationId(conversationId);}public void add(String conversationId, List<Message> messages) {List<Message> memoryMessages = this.chatMemoryRepository.findByConversationId(conversationId);List<Message> processedMessages = process(memoryMessages, messages); //processthis.chatMemoryRepository.saveAll(conversationId, processedMessages);}private List<Message> process(List<Message> memoryMessages, List<Message> newMessages) {//合并memoryMessages + newMessages, 若超过maxMessages,则需要按照规则remove历史message}}

ChatMemoryRepository & JdbcChatMemoryRepository

public final class JdbcChatMemoryRepository implements ChatMemoryRepository {public List<Message> findByConversationId(String conversationId) {//执行this.dialect.getSelectMessagesSql()中定义的sqlreturn this.jdbcTemplate.query(this.dialect.getSelectMessagesSql(), new MessageRowMapper(), new Object[]{conversationId});}//findConversationIds(),saveAll(),deleteByConversationId()方法略...
}

JdbcChatMemoryRepositoryDialect & PostgresChatMemoryRepositoryDialect

public interface JdbcChatMemoryRepositoryDialect {String getSelectMessagesSql();String getInsertMessageSql();String getSelectConversationIdsSql();String getDeleteMessagesSql();}public class PostgresChatMemoryRepositoryDialect implements JdbcChatMemoryRepositoryDialect {public String getSelectMessagesSql() {return "SELECT content, type FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ? ORDER BY \"timestamp\"";}public String getInsertMessageSql() {return "INSERT INTO SPRING_AI_CHAT_MEMORY (conversation_id, content, type, \"timestamp\") VALUES (?, ?, ?, ?)";}public String getSelectConversationIdsSql() {return "SELECT DISTINCT conversation_id FROM SPRING_AI_CHAT_MEMORY";}public String getDeleteMessagesSql() {return "DELETE FROM SPRING_AI_CHAT_MEMORY WHERE conversation_id = ?";}
}


ChatModelCallAdvisor & ChatModelStreamAdvisor(略)

public final class ChatModelCallAdvisor implements CallAdvisor {public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {Assert.notNull(chatClientRequest, "the chatClientRequest cannot be null");ChatClientRequest formattedChatClientRequest = augmentWithFormatInstructions(chatClientRequest);//调用chat-model去执行requestChatResponse chatResponse = this.chatModel.call(formattedChatClientRequest.prompt());return ChatClientResponse.builder().chatResponse(chatResponse).context(Map.copyOf(formattedChatClientRequest.context())).build();}
}

ChatClient执行advisors全流程

完整实例

@Test
public void test_jdbc() {ChatMemory chatMemory = MessageWindowChatMemory.builder().chatMemoryRepository(chatMemoryRepository).maxMessages(10).build();ChatClient chatClient = ChatClient.builder(openAiChatModel).defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()).build();String conversationId = "007";ChatResponse response = chatClient.prompt().user("who am i?").advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) //在context中定义CONVERSATION_ID.call() // 1.chatResponse(); //2log.info("response: " + response);}

chatClient.prompt()…call()

public class DefaultChatClient implements ChatClient {public static class DefaultChatClientRequestSpec implements ChatClientRequestSpec {private final List<Advisor> advisors = new ArrayList<>();private final Map<String, Object> advisorParams = new HashMap<>();//1.advisorpublic ChatClientRequestSpec advisors(Consumer<ChatClient.AdvisorSpec> consumer) {var advisorSpec = new DefaultAdvisorSpec();consumer.accept(advisorSpec); //添加param -> DefaultAdvisorSpecthis.advisorParams.putAll(advisorSpec.getParams()); // param -> advisorParamsthis.advisors.addAll(advisorSpec.getAdvisors());return this;}//2public CallResponseSpec call() {//2.1 build advisorChain by using :advisors + ChatModelCallAdvisor , ChatModelStreamAdvisorBaseAdvisorChain advisorChain = buildAdvisorChain(); //2.2 DefaultChatClientUtils.toChatClientRequest(this)//2.3 build DefaultCallResponseSpecreturn new DefaultCallResponseSpec(DefaultChatClientUtils.toChatClientRequest(this), advisorChain,this.observationRegistry, this.observationConvention);}//2.1private BaseAdvisorChain buildAdvisorChain() {// At the stack bottom add the model call advisors.// They play the role of the last advisors in the advisor chain.this.advisors.add(ChatModelCallAdvisor.builder().chatModel(this.chatModel).build());this.advisors.add(ChatModelStreamAdvisor.builder().chatModel(this.chatModel).build());return DefaultAroundAdvisorChain.builder(this.observationRegistry).pushAll(this.advisors).templateRenderer(this.templateRenderer).build();}}
}//2.2
final class DefaultChatClientUtils {static ChatClientRequest toChatClientRequest(DefaultChatClient.DefaultChatClientRequestSpec inputRequest) {List<Message> processedMessages = new ArrayList<>();// System Text => First in the listString processedSystemText = inputRequest.getSystemText();if (StringUtils.hasText(processedSystemText)) {processedMessages.add(new SystemMessage(processedSystemText));}// Messages => In the middle of the listif (!CollectionUtils.isEmpty(inputRequest.getMessages())) {processedMessages.addAll(inputRequest.getMessages());}// User Text => Last in the listString processedUserText = inputRequest.getUserText();if (StringUtils.hasText(processedUserText)) {processedMessages.add(UserMessage.builder().text(processedUserText).media(inputRequest.getMedia()).build());}//tools 相关.. SKIP....return ChatClientRequest.builder().prompt(Prompt.builder().messages(processedMessages).chatOptions(processedChatOptions).build()).context(new ConcurrentHashMap<>(inputRequest.getAdvisorParams())) //将avisorParams 放至 contxt.build();}}

chatClient.prompt()…call().chatResponse


public class DefaultChatClient implements ChatClient {public static class DefaultChatClientRequestSpec implements ChatClientRequestSpec {//3.public ChatResponse chatResponse() {return doGetObservableChatClientResponse(this.request).chatResponse(); //3.1}//3.1private ChatClientResponse doGetObservableChatClientResponse(ChatClientRequest chatClientRequest,@Nullable String outputFormat) {//context中添加OUTPUT_FORMATif (outputFormat != null) {chatClientRequest.context().put(ChatClientAttributes.OUTPUT_FORMAT.getKey(), outputFormat);}ChatClientObservationContext observationContext = ChatClientObservationContext.builder().request(chatClientRequest).advisors(this.advisorChain.getCallAdvisors()).stream(false).format(outputFormat).build();var observation = ChatClientObservationDocumentation.AI_CHAT_CLIENT.observation(this.observationConvention,DEFAULT_CHAT_CLIENT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry);var chatClientResponse = observation.observe(() -> {//4 advisorChain.nextCallreturn this.advisorChain.nextCall(chatClientRequest);});return chatClientResponse != null ? chatClientResponse : ChatClientResponse.builder().build();}}
}

ChatClientObservationDocumentation & ChatClientObservationContext – 略…

advisorChain.nextCall(chatClientRequest)

public class DefaultAroundAdvisorChain implements BaseAdvisorChain {public ChatClientResponse nextCall(ChatClientRequest chatClientRequest) {var advisor = this.callAdvisors.pop(); //pop advisorvar observationContext = AdvisorObservationContext.builder().advisorName(advisor.getName()).chatClientRequest(chatClientRequest).order(advisor.getOrder()).build();return AdvisorObservationDocumentation.AI_ADVISOR.observation(null, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry).observe(() -> advisor.adviseCall(chatClientRequest, this)); //advisor.adviseCall().}}


Tools源码分析

完整实例

定义Tool

public class DateTimeTools {@Tool(description = "Get the current date and time in the user's timezone")String getCurrentDateTime() {return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}
}

示例

@Test
public void test_tools() {String response = ChatClient.create(openAiChatModel).prompt("What day is tomorrow?").tools(new DateTimeTools()).call().content();System.out.printf("response: %s%n", response); //response: Today is October 30, 2025. Therefore, tomorrow will be October 31, 2025. If you need to know the day of the week for tomorrow, please let me know!}

manual执行tool

上述例子中,tool不需要人工的接入,会被自动的调用,如何设置为manual执行呢?

   @Testpublic void test_tools_manual() {ChatOptions opts = ToolCallingChatOptions.builder().toolCallbacks(ToolCallbacks.from(new DateTimeTools())).internalToolExecutionEnabled(false).build();ChatResponse response = ChatClient.create(openAiChatModel).prompt("What day is tomorrow?").options(opts).tools(new DateTimeTools()).call().chatResponse();System.out.println(response.getResults().get(0).getOutput());}

输出

AssistantMessage [messageType=ASSISTANT, toolCalls=[ToolCall[id=call_dVZAUN1LsodBQYhLjjb8MjYB, type=function, name=getCurrentDateTime, arguments={}]], textContent=null, metadata={role=ASSISTANT, messageType=ASSISTANT, finishReason=TOOL_CALLS, refusal=, index=0, annotations=[], id=chatcmpl-CWJFVxCDmBjGAupsZ6QPghBUS3FGP}]

这个才是我们所期待的输出,spring-ai什么样的机制让ToolCall自动执行了呢?

ToolCallingChatOptions

类图

在这里插入图片描述

SetUp ToolCallingChatOptions

上文我们已经分析过DefaultChatClientUtils.toChatClientRequest(),其中tool部分前文没有过多解读,在这里我们分析下:

final class DefaultChatClientUtils {static ChatClientRequest toChatClientRequest(DefaultChatClient.DefaultChatClientRequestSpec inputRequest) {//..... SKIP//for Tools -- 将request中的 toolnames, callbacks,toolContext 存放到request.chatOptions即ToolCallingChatOptionsChatOptions processedChatOptions = inputRequest.getChatOptions(); //OpenAiChatOptionsif (processedChatOptions instanceof ToolCallingChatOptions toolCallingChatOptions) {if (!inputRequest.getToolNames().isEmpty()) {Set<String> toolNames = ToolCallingChatOptions.mergeToolNames(new HashSet<>(inputRequest.getToolNames()), toolCallingChatOptions.getToolNames());toolCallingChatOptions.setToolNames(toolNames);}if (!inputRequest.getToolCallbacks().isEmpty()) {List<ToolCallback> toolCallbacks = ToolCallingChatOptions.mergeToolCallbacks(inputRequest.getToolCallbacks(), toolCallingChatOptions.getToolCallbacks());ToolCallingChatOptions.validateToolCallbacks(toolCallbacks);toolCallingChatOptions.setToolCallbacks(toolCallbacks);}if (!CollectionUtils.isEmpty(inputRequest.getToolContext())) {Map<String, Object> toolContext = ToolCallingChatOptions.mergeToolContext(inputRequest.getToolContext(),toolCallingChatOptions.getToolContext());toolCallingChatOptions.setToolContext(toolContext);}}//将request.chatOptions即ToolCallingChatOptions  --> prompt.optionsreturn ChatClientRequest.builder().prompt(Prompt.builder().messages(processedMessages).chatOptions(processedChatOptions).build()).context(new ConcurrentHashMap<>(inputRequest.getAdvisorParams())).build();}
}

Using ToolCallingChatOptions (ChatModelCallAdvisor#call())

ToolCallingChatOptions会在ChatModelCallAdvisor#call())中会被调用.

public class OpenAiChatModel implements ChatModel {public ChatResponse call(Prompt prompt) {Prompt requestPrompt = buildRequestPrompt(prompt); //1.build new prompt with requestOptionsreturn this.internalCall(requestPrompt, null);//2}//1Prompt buildRequestPrompt(Prompt prompt) {// Process runtime optionsOpenAiChatOptions runtimeOptions = null;if (prompt.getOptions() != null) {if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) {runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatOptions, ToolCallingChatOptions.class,OpenAiChatOptions.class);}else {runtimeOptions = ModelOptionsUtils.copyToTarget(prompt.getOptions(), ChatOptions.class,OpenAiChatOptions.class);}}// Define request options by merging runtime options and default optionsOpenAiChatOptions requestOptions = ModelOptionsUtils.merge(runtimeOptions, this.defaultOptions,OpenAiChatOptions.class);// set **optionsrequestOptions.setHttpHeaders(this.defaultOptions.getHttpHeaders());requestOptions.setInternalToolExecutionEnabled(this.defaultOptions.getInternalToolExecutionEnabled());requestOptions.setToolNames(this.defaultOptions.getToolNames());requestOptions.setToolCallbacks(this.defaultOptions.getToolCallbacks());requestOptions.setToolContext(this.defaultOptions.getToolContext());//setreturn new Prompt(prompt.getInstructions(), requestOptions);}//2.public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse) {ChatCompletionRequest request = createRequest(prompt, false);ChatModelObservationContext observationContext = ChatModelObservationContext.builder().prompt(prompt).provider(OpenAiApiConstants.PROVIDER_NAME).build();//2.1 call open ai ChatResponse response = ChatModelObservationDocumentation.CHAT_MODEL_OPERATION.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry).observe(() -> {//open-ai do requestResponseEntity<ChatCompletion> completionEntity = this.retryTemplate.execute(ctx -> this.openAiApi.chatCompletionEntity(request, getAdditionalHttpHeaders(prompt)));var chatCompletion = completionEntity.getBody();List<Choice> choices = chatCompletion.choices();List<Generation> generations = choices.stream().map(choice -> {Map<String, Object> metadata = Map.of("id", chatCompletion.id() != null ? chatCompletion.id() : "","role", choice.message().role() != null ? choice.message().role().name() : "","index", choice.index() != null ? choice.index() : 0,"finishReason", getFinishReasonJson(choice.finishReason()),"refusal", StringUtils.hasText(choice.message().refusal()) ? choice.message().refusal() : "","annotations", choice.message().annotations() != null ? choice.message().annotations() : List.of(Map.of()));return buildGeneration(choice, metadata, request);}).toList();// Current usage,rateLimit,....ChatResponse chatResponse = new ChatResponse(generations,from(chatCompletion, rateLimit, accumulatedUsage));return chatResponse;});/*** 2.2 DefaultToolExecutionEligibilityPredicate* - isInternalToolExecutionEnabled is true* - and response.hasToolCalls*/ if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired(prompt.getOptions(), response)) {//2.3 DefaultToolCallingManager.executeToolCallsvar toolExecutionResult = this.toolCallingManager.executeToolCalls(prompt, response);if (toolExecutionResult.returnDirect()) {// Return tool execution result directly to the client.return ChatResponse.builder().from(response).generations(ToolExecutionResult.buildGenerations(toolExecutionResult)).build();}else {// Send the tool execution result back to the model.return this.internalCall(new Prompt(toolExecutionResult.conversationHistory(), prompt.getOptions()),response);}}return response;}}
DefaultToolExecutionEligibilityPredicate.isToolExecutionRequired()
public interface ToolExecutionEligibilityPredicate extends BiPredicate<ChatOptions, ChatResponse> {default boolean isToolExecutionRequired(ChatOptions promptOptions, ChatResponse chatResponse) {return test(promptOptions, chatResponse);//sub-class}
}public class DefaultToolExecutionEligibilityPredicate implements ToolExecutionEligibilityPredicate {public boolean test(ChatOptions promptOptions, ChatResponse chatResponse) {return ToolCallingChatOptions.isInternalToolExecutionEnabled(promptOptions) && chatResponse != null&& chatResponse.hasToolCalls();}
}
DefaultToolCallingManager.executeToolCalls()
public final class DefaultToolCallingManager implements ToolCallingManager {//2.3public ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse) {Optional<Generation> toolCallGeneration = chatResponse.getResults().stream().filter(g -> !CollectionUtils.isEmpty(g.getOutput().getToolCalls())).findFirst();AssistantMessage assistantMessage = toolCallGeneration.get().getOutput();//2.3.1 build tool-context-mapToolContext toolContext = buildToolContext(prompt, assistantMessage);//2.3.2InternalToolExecutionResult internalToolExecutionResult = executeToolCall(prompt, assistantMessage,toolContext);List<Message> conversationHistory = buildConversationHistoryAfterToolExecution(prompt.getInstructions(),assistantMessage, internalToolExecutionResult.toolResponseMessage());return ToolExecutionResult.builder().conversationHistory(conversationHistory).returnDirect(internalToolExecutionResult.returnDirect()).build();}//2.3.1private static ToolContext buildToolContext(Prompt prompt, AssistantMessage assistantMessage) {Map<String, Object> toolContextMap = Map.of();if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions&& !CollectionUtils.isEmpty(toolCallingChatOptions.getToolContext())) {//init with tool-contexttoolContextMap = new HashMap<>(toolCallingChatOptions.getToolContext());//add history messagestoolContextMap.put(ToolContext.TOOL_CALL_HISTORY,buildConversationHistoryBeforeToolExecution(prompt, assistantMessage));}return new ToolContext(toolContextMap);}//2.3.2private InternalToolExecutionResult executeToolCall(Prompt prompt, AssistantMessage assistantMessage,ToolContext toolContext) {//2.3.2.1. 加载定义的callbacksList<ToolCallback> toolCallbacks = toolCallingChatOptions.getToolCallbacks();List<ToolResponseMessage.ToolResponse> toolResponses = new ArrayList<>();Boolean returnDirect = null;for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {String toolName = toolCall.name();String toolInputArguments = toolCall.arguments();//构造tool的 inputArgumentsfinal String finalToolInputArguments = toolInputArguments;//根据toolname找到对应的callbackToolCallback toolCallback = toolCallbacks.stream().filter(tool -> toolName.equals(tool.getToolDefinition().name())).findFirst().orElseGet(() -> this.toolCallbackResolver.resolve(toolName));// returnDirect是在Callback.ToolMetadata中定义的,用来表明无论response是什么,都直接返回returnDirect = returnDirect && toolCallback.getToolMetadata().returnDirect(); String toolCallResult = ToolCallingObservationDocumentation.TOOL_CALL.observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext,this.observationRegistry).observe(() -> {//执行callbackString toolResult = toolCallback.call(finalToolInputArguments, toolContext);return toolResult;});toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolName,toolCallResult != null ? toolCallResult : ""));}return new InternalToolExecutionResult(new ToolResponseMessage(toolResponses, Map.of()), returnDirect);}}
http://www.dtcms.com/a/553383.html

相关文章:

  • 关键词解释:点积(Dot Product)在深度学习中的意义
  • 本地部署DeepSeek-OCR:打造高效的PDF文字识别服务
  • 机器视觉系统中工业相机的常用术语解读
  • 【论文精读】GenRec:基于扩散模型统一视频生成与识别任务
  • seo提高网站排名wordpress内容页不显示
  • Velero(原名Heptio Ark) 是一个专为 Kubernetes 设计的开源备份恢复工具
  • 企业网站模板中文 产品列表深圳福田区住房和建设局网站
  • 制作网站的价格一般由什么组成
  • Spring MVC 架构总览与请求处理流程
  • 网站推广的优势有做二手厨房设备的网站吗
  • 请问聊城做网站wordpress模板个人博客
  • 蒲福风力等级表
  • 小小电脑安装logisim-evolution
  • C# 六自由度机械臂正反解计算
  • 【开题答辩全过程】以 基于Java的旅游网站的设计与开发为例,包含答辩的问题和答案
  • 【深入学习Vue丨第一篇】Props 完全指南
  • U-net 系列算法总结
  • 什么网站可以做模型挣钱网站建设公司有多少家
  • 网站建设的杂志建筑专业网站建设
  • vue3+ts面试题(一)JSX,SFC
  • 网站开发 数字证书网站制作设计方案
  • PCB设计----阻抗不连续的解决方法
  • 网站制作北京网站建设公司哪家好安平县哪家做网站
  • 14. setState是异步更新
  • 22. React中CSS使用方案
  • 深度对比 ArrayList 与 LinkedList:从底层数据结构到增删查改的性能差异实测
  • 信任的重构:S11e Protocol 如何以算法取代中介
  • Python 第二十五节 常用练习问题(三)
  • Spring AI Alibaba 【五】
  • 什么网站是用html做的2023年时政热点事件