【Spring】Spring AI 核心知識(一)

1. 自定義 Advisor

實際上,Advisor 可以看做是 Servlet 當中的“攔截器”,在大模型接收到 prompt 之前進行前置攔截增強(比如敏感詞校驗、記錄日志、鑒權),并在大模型返回響應之后進行后置攔截增強(比如記錄日志)官方已經提供了一系列 Advisor 可供插拔使用,但是接下來我們需要學習如何自定義 Advisor

1.1 自定義 Advisor 步驟

我們接下來就參考官方文檔來學習如何自定義 Advisor 在項目中使用:

📖 參考文檔:https://docs.spring.io/spring-ai/reference/api/advisors.html

1)步驟一:選擇合適的接口實現

  • CallAroundAdvisor:用于處理同步的請求與響應(非流式)
  • StreamAroundAdvisor:用于處理流式的請求與響應

更加建議兩者同時實現

2)步驟二:實現接口核心方法

  • 對于非流式接口 CallAroundAdvisor 來說,需要實現 nextAroundCall 方法
  • 對于流式接口 StreamAroundAdvisor 來說,需要實現 nextAroundStream 方法

3)步驟三:設置執行順序

通過重寫 getOrder 方法,返回的值越低,越先被執行

4)步驟四:設置唯一名稱

通過重寫 getName 方法,返回唯一標識符

1.2 實現 Logging Advisor

其實官方提供了一個 SimpleLoggerAdvisor 用于記錄日志,但是出于以下兩方面原因并沒有在項目中采用

  • SimpleLoggerAdvisor 源碼中使用 debug 級別輸出日志,而 SpringBoot 默認忽略 debug 以下級別日志
  • 自定義的 LoggerAdvisor 更加靈活,便于自定義日志

我們可以參考官方的示例,實現自定義的 Logging Advisor,參考代碼如下:

/*** 自定義日志Advisor* @author ricejson*/
public class MyLoggerAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {private static final Logger logger = LoggerFactory.getLogger(MyLoggerAdvisor.class);@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return 0;}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {// 調用前記錄請求日志logger.info("before req:{}", advisedRequest);// 調用執行鏈AdvisedResponse advisedResponse = chain.nextAroundCall(advisedRequest);// 調用后記錄響應日志logger.info("after resp:{}", advisedResponse);return advisedResponse;}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {// 調用前記錄請求日志logger.info("before req:{}", advisedRequest);// 調用執行鏈Flux<AdvisedResponse> flux = chain.nextAroundStream(advisedRequest);return new MessageAggregator().aggregateAdvisedResponse(flux, (advisedResponse) -> {// 調用后記錄響應日志logger.info("after resp:{}", advisedResponse);});}
}

需要特別注意的是 MessageAggregator消息聚合器對象將 Flux 類型聚合成單個 AdvisedResponse,是一種流式處理模式,我們在 ResumeApp 當中應用我們編寫的自定義 Advisior 觀察效果

public ResumeApp(ChatModel dashscopeChatModel) {ChatMemory chatMemory = new InMemoryChatMemory();this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem("你是一位資深職業顧問與AI技術融合的【簡歷輔導大師】,擁有以下核心能力:\n" +"1. HR視角:熟悉ATS(招聘系統)篩選邏輯、500+行業崗位的簡歷關鍵詞庫\n" +"2. 實戰經驗:基于10萬份真實簡歷優化案例的決策模型\n" +"3. 教練模式:通過蘇格拉底式提問引導用戶自主發現簡歷問題") // 系統預設提示詞.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多輪對話new MyLoggerAdvisor()) // 自定義日志Advisor.build();
}

最終運行測試代碼,可以發現自定義的日志 Advisor 已經生效了!

1.3 實現 Re2 Advisor

讓我們繼續趁熱打鐵,再來學習官方文檔中的示例,再來實現一個 Re2 的自定義 Advisor 叭!Re2(全稱為Re-Reading)機制,簡單來說就是讓 AI 大模型重新閱讀用戶提示詞,來提升大模型的推理能力,例如:

{input_prompt}
reading the question again: {input_prompt}

💡 注意:雖然 Re2 技術能夠有效提升大模型的推理能力,但是帶來的是雙倍的成本(雙倍token)對于 C 端產品來說需要控制成本

自定義 Re2 Advisor 相關代碼如下:

/*** 自定義 ReReading Advisor* @author ricejson*/
public class MyReReadingAdvisor implements CallAroundAdvisor, StreamAroundAdvisor {@Overridepublic String getName() {return this.getClass().getSimpleName();}@Overridepublic int getOrder() {return -1;}/*** 實現ReReading邏輯* @return 返回ReReading之后的請求*/private AdvisedRequest reReading(AdvisedRequest advisedRequest) {Map<String, Object> userParams = new HashMap<>(advisedRequest.userParams());// 設置替換模板userParams.put("input_prompt", advisedRequest.userText());return AdvisedRequest.from(advisedRequest).userText("""{input_prompt}reading the question again: {input_prompt}""").userParams(userParams).build();}@Overridepublic AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {return chain.nextAroundCall(reReading(advisedRequest));}@Overridepublic Flux<AdvisedResponse> aroundStream(AdvisedRequest advisedRequest, StreamAroundAdvisorChain chain) {return chain.nextAroundStream(reReading(advisedRequest));}
}

我們繼續在 ResumeApp 當中應用我們編寫的自定義的 Advisor 觀察效果

public ResumeApp(ChatModel dashscopeChatModel) {ChatMemory chatMemory = new InMemoryChatMemory();this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem("你是一位資深職業顧問與AI技術融合的【簡歷輔導大師】,擁有以下核心能力:\n" +"1. HR視角:熟悉ATS(招聘系統)篩選邏輯、500+行業崗位的簡歷關鍵詞庫\n" +"2. 實戰經驗:基于10萬份真實簡歷優化案例的決策模型\n" +"3. 教練模式:通過蘇格拉底式提問引導用戶自主發現簡歷問題") // 系統預設提示詞.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多輪對話new MyLoggerAdvisor(), // 自定義日志Advisornew MyReReadingAdvisor()) // 自定義ReReading Advisor.build();
}

最終運行測試代碼,可以發現自定義的 ReReading Advisor 已經生效了!

2. 結構化輸出

2.1 工作流程

Structured Output(結構化輸出)是Spring AI 另一個非常有用的機制,尤其是對于那些依賴可靠輸入的下游服務來說,結構化輸出可以將大模型的輸出結果轉化為特定的數據類型,比如 XML、JSON、POJO 等,結構化輸出的核心組件就是 Structured Output Converter(結構化輸出轉換器),核心數據流圖如下圖所示:

  • 在調用大模型之前,轉換器會在用戶提示詞后追加格式化的指令信息,指導大模型輸出期望格式
  • 在調用大模型之后,轉換器會解析文本輸出,并將其轉換為匹配的結構化實例,比如XML、JSON、POJO

? 注意:結構化輸出轉換器只是盡最大努力地將輸出結果轉換為結構化輸出,因為一方面某些大模型自身不支持結構化,另一方面大模型可能無法按照提示詞的指令轉換為結構化數據

2.2 API 設計

StructuredOutputConverter 接口繼承了兩個父接口,接口定義格式如下:

public interface StructuredOutputConverter<T> extends Converter<String, T>, FormatProvider {}
  • FormatProvider:該接口用于提供指導性的格式化指令追加在用戶提示詞之后,類似格式如下:
Your response should be in JSON format.
The data structure for the JSON should match this Java class: java.util.HashMap
Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.
  • Converter:該接口則專注于將模型輸出的文本內容轉換為特定的目標類型

SpringAI 提供了一系列的轉換器可供使用:

  • AbstructMessageOutputConverter:略
  • AbstructConversionServiceOutputConverter:略
  • MapOutputCoverter:用于將輸出轉換為 map 類型
  • BeanOutputConverter:使用 ObjectMapper 將輸出轉換為 Java Bean 類型
  • ListOutputConverter:用于將輸出轉換為 List 類型

2.3 使用示例

官方文檔提供了非常多的使用示例,下面簡單進行介紹:

1)使用 BeanOutputConverter 轉換為 Bean

// 定義動作電影類
record ActorsFilms(String actor, List<String> movies) {
}ActorsFilms actorsFilms = ChatClient.create(chatModel).prompt().user(u -> u.text("Generate the filmography of 5 movies for {actor}.").param("actor", "Tom Hanks")).call().entity(ActorsFilms.class);

其實我們還可以通過ParameterizedTypeReference構造函數來指定更加復雜的結構,比如:

// 定義動作電影類
record ActorsFilms(String actor, List<String> movies) {
}List<ActorsFilms> actorsFilms = ChatClient.create(chatModel).prompt().user("Generate the filmography of 5 movies for Tom Hanks and Bill Murray.").call().entity(new ParameterizedTypeReference<List<ActorsFilms>>() {});

2)使用 MapOutputConverter 轉換為 Map 結構

Map<String, Object> result = ChatClient.create(chatModel).prompt().user(u -> u.text("Provide me a List of {subject}").param("subject", "an array of numbers from 1 to 9 under they key name 'numbers'")).call().entity(new ParameterizedTypeReference<Map<String, Object>>() {});

3)使用 ListOutputConverter 轉換為 List 結構

List<String> flavors = ChatClient.create(chatModel).prompt().user(u -> u.text("List five {subject}").param("subject", "ice cream flavors")).call().entity(new ListOutputConverter(new DefaultConversionService()));

3. 對話記憶持久化

3.1 基礎概念

前面我們已經使用InMemoryChatMemory基于內存的方式來保存對話上下文信息,但是如果這時候服務器重啟了,對話記憶就會消失,這時候我們就會想到通過文件、數據庫、redis 中進行持久化存儲,應該怎么實現呢?

SpringAI 提供了以下兩種思路:

  1. 使用現有提供的依賴,比如官方提供了一些第三方數據庫的整合支持
  • InMemoryChatMemory:基于內存存儲
  • CassandraChatMemory:基于Cassandra 進行存儲
  • Neo4jChatMemory:基于 Neo4j 進行存儲
  • JdbcChatMeory:基于 JDBC 關系數據庫進行存儲
  1. 自定義提供 ChatMemory 的實現

這里我推薦直接造輪子,使用自定義的 ChatMemory ,Spring AI 的對話記憶功能實現的非常精巧,將記憶存儲與記憶算法解耦合,即我們只需要提供自定義的 ChatMemory 來改變存儲位置而無需關心記憶算法如何實現的,另外雖然官方文檔沒有提供自定義 ChatMemory 的使用示例,但是我們可以參考InMemoryChatMemory的源碼

其實不難發現,自定義 ChatMemory 只需要實現 ChatMeory 接口并實現相應的增刪查方法邏輯即可!下面我們就來提供一種文件存儲的方式提供自定義的 ChatMemory

3.2 自定義文件存儲

我們本能的會想到使用 JSON 序列化來保存到文件,但是實現起來非常麻煩,原因有如下幾點:

  1. Message 接口有眾多實現類,比如 UserMessage、SystemMessage
  2. 不同實現類字段都不統一
  3. 子類都沒有無參構造方法,也沒有實現 Serializable 接口

因此我們考慮使用高性能的序列化庫 Kryo 來完成序列化的任務,具體步驟如下:

1)引入 Kryo 依賴:

<!-- Kryo 序列化依賴 -->
<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo</artifactId><version>5.6.2</version>
</dependency>

2)實現自定義文件存儲記憶類:

/*** 自定義文件記憶存儲* @author ricejson*/
public class MyFileChatMemory implements ChatMemory {private static final Kryo kryo = new Kryo();private final File BASE_FILE;static {kryo.setRegistrationRequired(false);// 設置實例化策略kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());}public MyFileChatMemory(String dir) {this.BASE_FILE = new File(dir);// 如果根路徑不存在就創建if (!BASE_FILE.exists()) {BASE_FILE.mkdirs();}}@Overridepublic void add(String conversationId, Message message) {// 找到對應文件File file = getFile(conversationId);// 獲取文件中原先的消息列表List<Message> messageList = getMessageList(file);// 追加新文件后寫入messageList.add(message);saveMessageList(file, messageList);}@Overridepublic void add(String conversationId, List<Message> messages) {// 獲取目標文件File file = getFile(conversationId);// 獲取原先的消息列表List<Message> messageList = getMessageList(file);// 追加新的消息messageList.addAll(messages);// 寫入saveMessageList(file, messageList);}@Overridepublic List<Message> get(String conversationId, int lastN) {// 根據conversationId 查找對應文件File file = getFile(conversationId);List<Message> messageList = getMessageList(file);return messageList.stream().skip(Math.max(0, messageList.size() - lastN)).toList();}@Overridepublic void clear(String conversationId) {// 獲取文件File file = getFile(conversationId);// 清除文件if (file.exists()) {file.delete();}}private void saveMessageList(File file, List<Message> messageList) {try(Output output = new Output(new FileOutputStream(file))) {kryo.writeObject(output, messageList);} catch (FileNotFoundException e) {e.printStackTrace();}}private static List<Message> getMessageList(File file) {List<Message> messageList = new ArrayList<>();if (file.exists()) {// 讀取文件try (Input input = new Input(new FileInputStream(file))) {messageList = kryo.readObject(input, ArrayList.class);} catch (FileNotFoundException e) {e.printStackTrace();}}return messageList;}@NotNullprivate File getFile(String conversationId) {File file = new File(this.BASE_FILE, conversationId + ".kryo");return file;}
}

3)修改 ResumeApp 類構造方法:

public ResumeApp(ChatModel dashscopeChatModel) {// 修改成文件存儲記憶ChatMemory chatMemory = new MyFileChatMemory(System.getProperty("user.dir") + "/tmp/");this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem("你是一位資深職業顧問與AI技術融合的【簡歷輔導大師】,擁有以下核心能力:\n" +"1. HR視角:熟悉ATS(招聘系統)篩選邏輯、500+行業崗位的簡歷關鍵詞庫\n" +"2. 實戰經驗:基于10萬份真實簡歷優化案例的決策模型\n" +"3. 教練模式:通過蘇格拉底式提問引導用戶自主發現簡歷問題") // 系統預設提示詞.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多輪對話new MyLoggerAdvisor(), // 自定義日志Advisornew MyReReadingAdvisor()) // 自定義ReReading Advisor.build();
}

4)測試:

4. 提示詞模板

4.1 基礎概念

接下來要講的是 PromptTemplate(提示詞模板),這是 Spring AI 當中用于管理和構建提示詞的組件,允許用戶定義帶占位符的提示詞,然后在程序動態運行過程中替換占位符,示例代碼如下:

// 帶占位符的提示詞模板
String template = "你好,{name},今天是{day},天氣:{weather}";
PromptTemplate promptTemplate = new PromptTemplate(template);
// 準備變量映射
Map<String, Object> variables = new HashMap<>();
variables.put("name", "米飯");
variables.put("day", "星期一");
variables.put("weather", "晴朗");
// 生成最終提示文本
String prompt = promptTemplate.render(variables);

💡 提示:模板思想在編程世界中有大量運用,比如模板引擎、日志占位符、SQL預編譯語句

PromptTemplate 在以下場景中非常有用:

  1. A/B測試:能夠輕松對比測試結果
  2. 多語言支持:可重用內容,動態替換替換語言部分
  3. 用戶交互場景:根據上下文語境定制提示詞
  4. 提示詞版本管理:便于提示詞版本控制

4.2 實現原理

這里簡單介紹,Spring AI 當中的 Prompt Template 底層基于 OSS String Template 模板引擎技術 ,下圖是其類以及接口的相關依賴圖

SpringAI 還提供了一些專用的模板類,比如:

  • SystemPromptTemplate:用于系統消息
  • AssistantPromptTemplate:用于助手消息
  • FunctionPromptTemplate:目前暫時沒用

這些類可以快速構建專用的提示詞,再來介紹 PromptTemplate 另外一個特性,就是支持從文件當中讀取模板信息,示例代碼如下:

/*** 簡歷輔導應用* @author ricejson*/
@Component
public class ResumeApp {private ChatClient chatClient;@Autowiredpublic ResumeApp(ChatModel dashscopeChatModel, @Value("classpath:/prompts/system.st") Resource systemPrompt) throws IOException {// 修改成文件存儲記憶ChatMemory chatMemory = new MyFileChatMemory(System.getProperty("user.dir") + "/tmp/");this.chatClient = ChatClient.builder(dashscopeChatModel).defaultSystem(systemPrompt) // 系統預設提示詞.defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory), // 支持多輪對話new MyLoggerAdvisor(), // 自定義日志Advisornew MyReReadingAdvisor()) // 自定義ReReading Advisor.build();}
}

在我們的改造下,將原有的硬編碼的系統提示詞替換為了從文件資源中加載!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/84628.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/84628.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/84628.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

中國免稅品人工智能商城:引領免稅品市場新潮流

在全球經濟一體化的時代背景下&#xff0c;免稅品市場日益繁榮。中國免稅品人工智能商城以對標洋碼頭為目標&#xff0c;積極利用人工智能的優勢&#xff0c;結合自身特點&#xff0c;全力打造成為免稅品類的示范性商業平臺&#xff0c;為消費者帶來全新的購物體驗。 一、免稅品…

LambdaQueryWrapper、MybatisPlus提供的基本接口方法、增刪改查常用的接口方法、自定義 SQL

DAY26.2 Java核心基礎 MybatisPlus提供的基本接口方法 分頁查詢 導入依賴springboot整合Mybatis-plus <dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version&g…

謝飛機的Java面試奇遇:AIO、BIO、NIO與Netty深度解析

謝飛機的Java面試奇遇&#xff1a;AIO、BIO、NIO與Netty深度解析 在一場充滿笑料的面試中&#xff0c;謝飛機面對嚴肅的面試官&#xff0c;從Java IO的基本概念開始&#xff0c;逐步展開對AIO、BIO、NIO的理解&#xff0c;以及Netty的高級用法。 面試現場&#xff1a;第一輪&…

三、Docker目錄掛載、卷映射、網絡

目錄掛載 如果主機目錄為空&#xff0c;則容器內也為空 -v表示目錄掛載 冒號前面的是主機上的目錄&#xff0c;冒號后面的是docker容器里面的地址 修改主機上的文件&#xff0c;發現docker容器里面的內容也隨之改變。 同樣修改docker容器里面的內容&#xff0c;主機上的文件…

Linux的學習_基礎4_指令的實踐

目錄&#xff1a; 一、常用功能 二、指令實踐 1、tail命令 2、ls命令 3、ps、kill命令 4、cd、vim命令 5、root權限與用戶權限的轉換 6、獲取網卡信息 7、sudo chmodx 8、更換到別的目錄 9、獲取文件的內容 10、lsblk 查看塊設備和文件系統信息 11、man指令與指令…

深入解析Spring Boot與Redis集成:高效緩存與性能優化

深入解析Spring Boot與Redis集成&#xff1a;高效緩存與性能優化 引言 在現代Web應用中&#xff0c;緩存技術是提升系統性能的重要手段之一。Redis作為一種高性能的內存數據庫&#xff0c;廣泛應用于緩存、會話管理和消息隊列等場景。本文將詳細介紹如何在Spring Boot項目中集…

基于微信小程序的漫展系統的設計與實現

博主介紹&#xff1a;java高級開發&#xff0c;從事互聯網行業六年&#xff0c;熟悉各種主流語言&#xff0c;精通java、python、php、爬蟲、web開發&#xff0c;已經做了六年的畢業設計程序開發&#xff0c;開發過上千套畢業設計程序&#xff0c;沒有什么華麗的語言&#xff0…

藍橋杯電子賽_零基礎利用按鍵實現不同數字的顯現

目錄 一、前提 按鍵的原理圖 二、代碼配置 bsp_key.c文件 疑問 main.c文件 main.c文件的詳細講解 功能實現 注意事項 一、前提 按鍵這一板塊主要是以記憶為主&#xff0c;我直接給大家講解代碼去實現我要配置的功能。本次我要做的項目是板子上的按鍵有S4~S19&#xff…

Python常用高階函數全面解析:通俗易懂的指南

Python常用高階函數全面解析&#xff1a;通俗易懂的指南 一、什么是高階函數&#xff1f; 高階函數(Higher-order Function)是指能夠接受其他函數作為參數&#xff0c;或者將函數作為返回值的函數。在Python中&#xff0c;函數是一等公民&#xff0c;可以像普通變量一樣傳遞和…

Flume之選擇器:復制和多路復用(比喻化理解

Flume 的選擇器決定了Source 如何將數據分發到多個 Channel。這就像 “快遞員如何分配包裹到不同的運輸通道”&#xff0c;有兩種策略&#xff1a;復制和多路復用。 一、復制&#xff08;Replicating Selector&#xff09;&#xff1a;每個 Channel 都送一份 核心邏輯 將同一…

yolov5 安卓運行

參考博客&#xff1a; 通過Android Studio 將yolov5部署到手機端(新手最新適用版)_怎么將yolo部署手機-CSDN博客 總體跟隨參考博客走是沒問題&#xff0c;有些細節需要注意&#xff1a; 1 jdk 版本選擇&#xff0c;jdk需要17&#xff0c;新版的Android Studio 選擇jdk版本方式…

day021-定時任務

文章目錄 1. cron1.1 檢查是否安裝1.2 檢查是否開機自啟動1.3 配置文件與相關命令1.4 配置文件格式 2. 案例2.1 同步時間2.2 定時備份/etc和/var/log目錄2.3 定時巡檢腳本 3. 練習三劍客過濾3.1 去重統計ip數量3.2 去重統計第7列 用戶訪問的url的數量3.3 去重統計第9列 狀態碼與…

關于(stream)流

Stream 是 Java 8 引入的一個強大的功能&#xff0c;用于處理集合&#xff08;Collection&#xff09;或數組中的數據。它提供了一種聲明式的編程方式&#xff0c;可以極大地簡化對數據的操作&#xff0c;例如過濾、排序、映射和聚合等。 1. 什么是 Stream 流&#xff1f; 定義…

結課作業自選01. 內核空間 MPU6050 體感鼠標驅動程序(二)(完整實現流程)

目錄 一. 題目要求-內核空間 MPU6050 體感鼠標驅動程序 二. 偽代碼及程序運行流程 三. 主要函數詳解&#xff08;根據代碼流程進行詳解&#xff09; 3.1 module_i2c_driver宏&#xff08;對應“1”&#xff09; 3.2 mpu_of_match設備樹匹配表&#xff08;對應“2”&#x…

5G 核心網切換機制全解析:XN、N2 與移動性注冊對比

摘要 本文深入探討了 5G 核心網中的三種關鍵切換方式:基于 XN 接口的切換、基于 N2 接口的切換以及移動性注冊更新機制。通過對比分析它們的原理、應用場景和技術差異,幫助讀者全面理解 5G 網絡中用戶移動性管理的核心技術。 1. 引言 隨著 5G 技術的廣泛應用,用戶對網絡連…

用深度學習提升DOM解析——自動提取頁面關鍵區塊

一、時間軸&#xff1a;一次“抓不到重點”的二手車數據爬蟲事故 2025/03/18 09:00 產品經理希望抓取懂車帝平臺上“北京地區二手車報價”作為競品監測數據源。我們初步使用傳統XPath方案&#xff0c;試圖提取車型、年限、里程、價格等數據。2025/03/18 10:00 初版腳本運行失敗…

React與Vue的內置指令對比

React 與 Vue 不同&#xff0c;它沒有內置的模板指令系統。React 采用了 JavaScript 優先 的聲明式方式&#xff0c;使用 JSX 語法將 HTML 和 JavaScript 結合在一起。因此&#xff0c;React 中沒有類似 Vue 的 v-if、v-for、v-bind 等內置指令。 React 中的替代方案 條件渲染…

Spring聲明式事務源碼全鏈路剖析與設計模式深度解讀

Spring聲明式事務源碼全鏈路剖析與設計模式深度解讀 作者&#xff1a;AI 日期&#xff1a;2025-05-22 一、前言 Spring事務是企業級開發的基石&#xff0c;但“為什么有時事務失效&#xff1f;”、“不同傳播行為背后發生了什么&#xff1f;”、“Spring事務源碼到底如何實現&…

云原生安全基礎:深入探討容器化環境中的權限隔離與加固策略

&#x1f525;「炎碼工坊」技術彈藥已裝填&#xff01; 點擊關注 → 解鎖工業級干貨【工具實測|項目避坑|源碼燃燒指南】 在云原生環境中&#xff0c;容器化技術&#xff08;如 Docker 和 Kubernetes&#xff09;的廣泛應用帶來了靈活性與效率&#xff0c;但也引入了新的安全挑…

如何在 ONLYOFFICE 演示文稿中調整段落首行縮進

在制作演示文稿時&#xff0c;保持內容的一致性與可讀性至關重要&#xff0c;而段落首行縮進作為格式設置的關鍵環節&#xff0c;直接影響著整體呈現效果。在本文中&#xff0c;我們將介紹如何通過創建 ONLYOFFICE 宏&#xff0c;快速設置演示文稿中所有段落的首行縮進。 關于 …