博客 / 詳情

返回

Spring AI Alibaba 入門指南

1. 概述

Spring AI Alibaba 開源項目基於 Spring AI 構建,是阿里雲通義系列模型及服務在 Java AI 應用開發領域的最佳實踐,提供高層次的 AI API 抽象與雲原生基礎設施集成方案和企業級 AI 應用生態集成。

在用Spring AI搭建Java AI應用的時候,會碰到了各種讓人頭疼的配置動態管理的問題. 比如像調用算法模型的“API-KEY密鑰”這類敏感配置.

還有想要模型的各類調用配置參數,以及Prompt Engineering裏的Prompt Template如何可以在不發佈重啓應用的情況下,快速修改生效來響應業務需求.

Spring AI Alibaba 將結合Nacos來一一解決

並且老外的Spring AI框架對於像Open AI , 微軟、亞馬遜、谷歌 等大模型支持較好, 對於國產AI支持則不那麼友好, 而Spring AI Alibaba 對於通義系列的大模型則是天生友好.

不過在學習這篇之前, 還是需要先了解一下Spring AI 框架. https://www.cnblogs.com/xjwhaha/p/19306045
以下是當前主流Java AI應用框架的對比

image-20251204142412657

OpenAI Api 和 阿里的DashScope(靈積)Api的區別

OpenAI API 是 OpenAI 官方提供的一個 大模型接口平台,定義了開發者通過一套標準的 HTTP 調用模板來使用:

  • GPT 系列模型(GPT-4.1 / o3 / gpt-4.1-mini 等)

  • 多模態模型(看圖、語音)

  • Embeddings(向量)

  • 文生圖(DALL·E)

  • 等等...

SpringAI框架就是使用這套API接口來進行調用, 同樣模型的廠商也需要實現此接口, 雙方通過達成一致,達到統一AI大模型訪問的目的.

DashScope 是阿里雲的 大模型 API 平台,提供“通義千問 + 多模態 + 向量 + 文生圖 + 語音”的一站式接口,類似於國內版的 OpenAI API。同時,除了阿里自己的通義系列. 包括Deepseek和月之暗面等國內大模型, 也進行了封裝, 也可以通過DashScopeAPI進行調用. SpringAIAlibaba 就支持使用DashScopeApi 來進行統一訪問國產AI大模型的能力. 當然SpringAIAlibaba 也同樣支持 OpenAIApi的訪問方式,進行訪問實現OpenAIAPI的大模型.例如OpenAI等等.

2. 快速入門示例

下面將實現一個天氣預報的小助手功能, 來快速瞭解一下SAA的各個常用功能.

  1. 詳細的 System Prom - 獲得更好的 agent 行為
  2. 創建工具 - 與外部數據集成
  3. 模型配置 - 獲得一致的響應
  4. 結構化輸出 - 獲得可預測的結果
  5. 對話記憶 - 實現類似聊天的交互
  6. 創建和運行 agent - 創建一個功能完整的 agent

2.1 依賴和配置

使用SpringAIAlibaba,先導入pom依賴:

這裏優先使用了 DashScope的方式訪大模型

<!-- Spring AI Alibaba Agent Framework -->
  <dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-agent-framework</artifactId>
    <version>1.1.0.0-M5</version>
  </dependency>

  <!-- DashScope ChatModel 支持(如果使用其他模型,請參考文檔選擇對應的 starter) -->
  <dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
    <version>1.1.0.0-M5</version>
  </dependency>

 <!-- 【可選】OpenAi ChatModel 支持(如果使用其他模型,請參考文檔選擇對應的 starter) -->
 <!--
  <dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
    <version>1.1.0-M4</version>
  </dependency>
 -->

application.yaml 配置:

指定apiKey,模型名稱和訪問路徑. 注意apiKey生產環境建議配置在環境變量中

spring:  
	ai:
    dashscope:
      api-key: sk-********
      base-url: https://dashscope.aliyuncs.com
      chat:
        options:
          model: qwen3-max

2.2 一個天氣預報助手

首先需要定義兩個工具,一個用於獲取當前用户的位置, 另外一個獲取地方天氣信息:

		public record WeatherRequest(@ToolParam(description = "城市的名稱") String location) {
    }

    // 天氣查詢工具
    public static class WeatherForLocationTool implements BiFunction<WeatherRequest, ToolContext, String> {
        @Override
        public String apply(
                @ToolParam(description = "城市的名稱") WeatherRequest city,
                ToolContext toolContext) {
            return StrUtil.equals("上海", city.location) ? "晴朗" : "小雨";
        }
    }

    // 用户位置工具 - 使用上下文
    public static class UserLocationTool implements BiFunction<WeatherRequest, ToolContext, String> {
        @Override
        public String apply(
                WeatherRequest query,
                ToolContext toolContext) {
            // 從上下文中獲取用户信息
            RunnableConfig config = (RunnableConfig) toolContext.getContext().get("_AGENT_CONFIG_");
            String userId = (String) config.metadata("user_id").orElse(null);

            if (userId == null) {
                return "User ID not provided";
            }
            System.out.println("userId: " + userId);
            return "1".equals(userId) ? "杭州" : "上海";
        }
    }

工具應該有良好的文檔:它們的名稱、描述和參數名稱都會成為模型提示的一部分。

Spring AI 的 FunctionToolCallback 支持通過 @ToolParam 註解添加元數據,並支持通過 ToolContext 參數進行運行時注入。

構建ReactAgent, 用户訪問大模型的類:

    private final DashScopeChatModel chatModel;

    public AgentConfiguration(DashScopeChatModel chatModel) {
        this.chatModel = chatModel;
    }

    @Bean
    public ReactAgent reactAgent() {
        String SYSTEM_PROMPT = """
                你是一位天氣預報專家,説話比較幽默。
                您可以訪問兩個工具:
                                
                - get_weather_for_location:使用它來獲取指定位置的天氣
                — get_user_location:使用它來獲取用户的當前位置

                如果用户向你詢問天氣,你可以嘗試分析他需要查詢的位置。例如上海,杭州等.
                但是如果用户沒有指定位置,你需要調用get_user_location獲取此用户的當前位置,查詢此位置的天氣
                """;
        return ReactAgent.builder()
                .name("天氣預報小助手")
                .description("這是一個天氣預報小助手智能體")
                // 如果是簡短,簡單的系統提示可以用這個
//                .systemPrompt(SYSTEM_PROMPT)
                // 更詳細的指令
                .instruction(SYSTEM_PROMPT)
                .tools(FunctionToolCallback.builder("weatherForLocationTool", new WeatherForLocationTool()).description("根據城市名稱獲取當前天氣信息").inputType(WeatherRequest.class).build(),
                        FunctionToolCallback.builder("userLocationTool", new UserLocationTool()).description("獲取用户當前位置").inputType(WeatherRequest.class).build()
                )
                // 基於內存的存儲
                .saver(new MemorySaver())
                .outputType(ResponseFormat.class)
                .model(chatModel)
                .build();
    }

下面是定義大模型返回的結構體

/**
 * 使用 Java 類定義響應格式
 */
@Getter
@Setter
public class ResponseFormat {

    /**
     * 城市名稱
     */
    private String city;

    /**
     * 天氣情況
     */
    private String punnyResponse;

    /**
     * 關於該天氣的一個有趣的浪漫的簡語
     */
    private String weatherConditions;

}
  1. 使用instruction方法,定義系統提示.引導大模型的執行方式.
  2. 使用tools 方法註冊創建的函數. 使大模型具有調用本地方法的能力
  3. 使用saver方法註冊一個用於存儲歷史記錄的類,框架會自動讀取當前指定的threadId來讀取當前會話的歷史記錄, 使大模型調用具有歷史記憶功能, 並且會自動將本次調用按threadId 為key存起來 ( 這裏使用的MemorySaver 是基於內存, 生產需要使用基於持久化中間件的實現)
  4. 使用outputType 方法定義大模型返回的數據結構為此對象的結構

調用方式如下:

@RestController
@RequestMapping("/ai")
public class AiController {

    @Resource
    private ReactAgent reactAgent;


    @GetMapping
    public String ai(@RequestParam String question) throws Exception {
        RunnableConfig runnableConfig = RunnableConfig.builder().threadId("threadId").addMetadata("user_id", "1").build();
        return reactAgent.call(question, runnableConfig).getText();
    }

}
  1. 構建調用執行時配置,指定threadId,相當於會話ID
  2. 加入運行時元數據, 在調用的執行鏈中,可以通過上下文獲取

運行效果:

調用: http://127.0.0.1:8089/ai?question=上海天氣怎麼樣

響應:

可以看到返回的數據為指定的json結構,並且自動讀取問題中的城市信息,並調用了獲取天氣的方法.

再次調用: http://127.0.0.1:8089/ai?question=我這裏呢

在沒有詢問城市時, 大模型自動調用了獲取本地城市的方法,得到當前城市為杭州.

3. ReactAgent 的工作原理

在上面的案例中, 我們實現了一個簡單的天氣查詢工具類, 大模型具有調用本地方法的能力, 這背後的原理是什麼樣, ReactAgent 的執行流程是什麼, 大模型是如何調用本地方法的?

什麼是 ReactAgent?

  • Agent(智能體): 在 AI 編程中,Agent 是一個能感知環境、使用工具(如搜索、計算、API調用)、進行推理並執行任務以實現目標的程序。它不僅僅是調用大模型,而是讓大模型成為“大腦”,指揮各種工具。
  • ReAct: 是一種經典的 Agent 設計範式,代表 Reasoning + Acting。其核心思想是讓模型以“思考(Thought)- 行動(Action)- 觀察(Observation)”的循環來工作。
    1. Thought: 模型分析當前狀況,思考下一步該做什麼。
    2. Action: 根據思考,決定調用哪個工具(或直接給出最終答案)。
    3. Observation: 執行工具後,獲取結果(可能是搜索結果、代碼執行結果等)。
    4. 循環此過程,直到任務完成。

這個循環使 Agent 能夠:

  • 將複雜問題分解為多個步驟
  • 動態調整策略基於中間結果
  • 處理需要多次工具調用的任務
  • 在不確定的環境中做出決策

而SAA框架中的ReactAgent是怎麼完成這個工作的?

Spring AI Alibaba 中的ReactAgent 內容抽象了三個模塊,由這三個模塊相互配合完成

  • Model Node (模型節點):調用 LLM 進行推理和決策(.model(chatModel)方法傳入的大模型調用類)
  • Tool Node (工具節點):執行工具調用(註冊的工具)
  • Hook Nodes (鈎子節點):在關鍵位置插入自定義邏輯

ReactAgent 的核心執行流程:

image-20251205104744350

下面通過梳理上面天氣小助手的執行流程來具體瞭解一下工作流程:

第一步: 發出提問
    ↓
第二步:SpringAI 構建一個 ChatRequest(包含tools和所有問題的上下文信息)
    ↓
第三步:序列化成 JSON
    ↓
第四步:通過 HTTP POST 調用大模型 API
    ↓
第五步:大模型進行判斷推理,是否需要執行函數,執行哪個函數,在本案例中, 如果解析出城市名稱, 則需要調用獲取天氣的函數, 如果沒有,則需要調用獲取用户位置的函數. 			 並返回執行函數的名稱+入參
    ↓
第六步:ReactAgent解析返回JSON,進行推理是否是一次 function call
    ↓
第七步:如果是,則本地反射調用該方法,例如執行了獲取用户位置的函數
    ↓
第八步:把該函數結果再封裝成 JSON 發給大模型繼續對話,大模型拿到此函數結果,繼續分析推理,拿到位置後,大模型繼續推理需要進行調用獲取天氣的函數,則繼續返回客户			端調用
    ↓
第九步:ReactAgent繼續解析返回JSON,執行獲取天氣的函數並返回
    ↓
第九步:最終返回結果給用户

在上面的流程中, 可以迅速的瞭解到上圖的含義. 在模型和工具間循環,直到模型推理出最終結果為止.

也就是説,以ReactAgent為本體:

  • 大模型 = 腦子(發出工具調用的意願)
  • SpringAI = 手(真正執行方法)
  • 你的方法 = 工具(可以被大模型調來用)

4. Hooks 和 Interceptors

在上面關於ReactAgent的工作流程的介紹中, 除了Tool NodeModel Node 之外,還有一個組件為Hooks(鈎子).

SAA框架在這些步驟的前後暴露了鈎子點Hooks 和 攔截器Interceptors,允許你

  • 監控: 通過日誌、分析和調試跟蹤 Agent 行為
  • 修改: 轉換提示、工具選擇和輸出格式
  • 控制: 添加重試、回退和提前終止邏輯
  • 強制執行: 應用速率限制、護欄和 PII 檢測

4.1 自定義鈎子

框架中提供了四個抽象類供開發者實現,並在不同的節點調用

ModelHook: 在模型調用前後執行自定義邏輯

AgentHook: 在 Agent 一次問答整體執行的開始和結束時執行:

ModelInterceptor: 攔截和修改對模型的請求和響應

ToolInterceptor:攔截和修改工具調用

下面的示例,分別實現了這四個抽象類,可以快速瞭解其使用方式:


public class MyHooks {

    private static final String CALL_COUNT_KEY = "_model_call_count_";
    private static final String START_TIME_KEY = "_call_start_time_";

    // 1. AgentHook - 在 Agent 開始/結束時執行,每次Agent調用只會運行一次
    @HookPositions({HookPosition.BEFORE_AGENT, HookPosition.AFTER_AGENT})
    public static class LoggingHook extends AgentHook {
        @Override
        public String getName() {
            return "logging";
        }

        @Override
        public CompletableFuture<Map<String, Object>> beforeAgent(OverAllState state, RunnableConfig config) {
            DateTime date = DateUtil.date();
            System.out.println("Agent 開始執行時間" + DateUtil.formatDateTime(date));
            config.context().put(START_TIME_KEY, date.getTime());
            return CompletableFuture.completedFuture(Map.of());
        }

        @Override
        public CompletableFuture<Map<String, Object>> afterAgent(OverAllState state, RunnableConfig config) {
            long startTime = (long) config.context().get(START_TIME_KEY);
            System.out.println("Agent 執行完成,耗時:" + (DateUtil.date().getTime() - startTime));
            return CompletableFuture.completedFuture(Map.of());
        }
    }

    // 2. ModelHook - 在模型調用前後執行(例如:消息修剪),區別於AgentHook,ModelHook在一次agent調用中可能會調用多次,也就是每次 reasoning-acting 迭代都會執行
    public static class MessageTrimmingHook extends ModelHook {

        @Override
        public String getName() {
            return "message_trimming";
        }

        @Override
        public HookPosition[] getHookPositions() {
            return new HookPosition[]{HookPosition.BEFORE_MODEL, HookPosition.AFTER_MODEL};
        }

        @Override
        public CompletableFuture<Map<String, Object>> beforeModel(OverAllState state, RunnableConfig config) {
            // 這裏可以獲取到請求模型時傳入的所有message
            Optional<Object> messagesOpt = state.value("messages");
            if (messagesOpt.isPresent()) {
                List<Message> messages = (List<Message>) messagesOpt.get();
                System.out.println(messages.size());
            }
            // 增加調用次數記錄
            config.context().put(CALL_COUNT_KEY, config.context().get(CALL_COUNT_KEY) == null ? 1 : (Integer) config.context().get(CALL_COUNT_KEY) + 1);
            System.out.println("第" + config.context().get(CALL_COUNT_KEY) + "次調用模型開始");
            // 模型調用前,可以進行消息修剪,返回的Map會作為模型調用的參數
            return CompletableFuture.completedFuture(Map.of());
        }

        @Override
        public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
            System.out.println("第" + config.context().get(CALL_COUNT_KEY) + "次調用模型結束");
            return CompletableFuture.completedFuture(Map.of());
        }
    }

    public static class LoggingInterceptor extends ModelInterceptor {

        @Override
        public ModelResponse interceptModel(ModelRequest request, ModelCallHandler handler) {
            // 請求前記錄
            System.out.println("發送請求到模型: " + request.getMessages().size() + " 條消息");
            // 執行實際調用
            return handler.call(request);
        }

        @Override
        public String getName() {
            return "LoggingInterceptor";
        }
    }


    public static class ToolMonitoringInterceptor extends ToolInterceptor {

        @Override
        public ToolCallResponse interceptToolCall(ToolCallRequest request, ToolCallHandler handler) {
            String toolName = request.getToolName();
            long startTime = System.currentTimeMillis();

            System.out.println("執行工具: " + toolName + "執行參數: " + request.getArguments());

            try {
                return handler.call(request);
            } catch (Exception e) {
                long duration = System.currentTimeMillis() - startTime;
                System.err.println("工具 " + toolName + " 執行失敗 (耗時: " + duration + "ms): " + e.getMessage());

                return ToolCallResponse.of(
                        request.getToolCallId(),
                        request.getToolName(),
                        "工具執行失敗: " + e.getMessage()
                );
            }
        }

        @Override
        public String getName() {
            return "ToolMonitoringInterceptor";
        }
    }
}

註冊鈎子:

ReactAgent.builder()
                .name("天氣預報小助手")
                .description("這是一個天氣預報小助手智能體")
                // 如果是簡短,簡單的系統提示可以用這個
//                .systemPrompt(SYSTEM_PROMPT)
                // 更詳細的指令
                .instruction(SYSTEM_PROMPT)
                .tools(FunctionToolCallback.builder("weatherForLocationTool", new WeatherForLocationTool()).description("根據城市名稱獲取當前天氣信息").inputType(WeatherRequest.class).build(),
                        FunctionToolCallback.builder("userLocationTool", new UserLocationTool()).description("獲取用户當前位置").inputType(WeatherRequest.class).build()
                )
                // 基於內存的存儲
                .saver(new MemorySaver())
                .outputType(ResponseFormat.class)
  							// 註冊鈎子和攔截器
                .hooks(new MyHooks.LoggingHook(), new MyHooks.MessageTrimmingHook())
                .interceptors(new MyHooks.LoggingInterceptor(), new MyHooks.ToolMonitoringInterceptor())
                .model(chatModel)
                .build();

啓動調用: http://127.0.0.1:8089/ai?question=杭州今天天氣怎麼樣

控制枱打印:

Agent 開始執行時間2025-12-09 15:36:11
第1次調用模型開始
發送請求到模型: 1 條消息
第1次調用模型結束
執行工具: weatherForLocationTool執行參數: {"location": "杭州"}
第2次調用模型開始
發送請求到模型: 3 條消息
第2次調用模型結束
Agent 執行完成,耗時:5084

4.2 內置Hooks和Interceptors

Spring AI Alibaba 為常見用例提供了預構建的 Hooks 和 Interceptors 實現:模型調用限制(Model Call Limit),LLM Tool Selector(LLM 工具選擇器) 等等.

Human-in-the-Loop(人機協同)

在調用指定的Tool時, 暫停 Agent 執行以獲得人工批准、編輯或拒絕工具調用。

適用場景:

  • 需要人工批准的高風險操作(數據庫寫入、金融交易)
  • 人工監督是強制性的合規工作流程
  • 長期對話,使用人工反饋引導 Agent

使用示例: 將模擬一個發送郵件的Agent, 每次發送郵件,都需要手動人為審批

  1. 首先需要定義一個發送郵件的Tool.當模型判斷需要調用次方法時,會中斷流程,並等待人工審批,再繼續執行
		public record EmailRequest(@ToolParam(description = "發送郵件的信息") String message) {
    }

    // 發送email
    public static class SendEmailTool implements BiFunction<EmailRequest, ToolContext, Boolean> {
        @Override
        public Boolean apply(
                @ToolParam(description = "發送郵件的信息") EmailRequest message,
                ToolContext toolContext) {
            System.out.println("發送郵件: " + message.message);
            return true;
        }
    }
  1. 這裏構建了一個ReactAgent, 註冊Tool, 並傳入一個humanInTheLoopHook 實例, 描述調用審批的節點, 注意,這裏一定需要傳入 saver 作為檢查點, 因為中斷後再次調用,需要依賴歷史記錄message,並攜帶上下文,才能使調用前後的流程銜接, 這裏使用了測試用的實例MemorySaver,基於內存
    @Bean
    public ReactAgent emailReactAgent() {
        String SYSTEM_PROMPT = """
                你是一個工作平台的助手
                您可以訪問一個工具:
                                
                - sendEmailTool:使用該工具進行發送郵件的操作

                如果用户有需要發送郵件,可以進行操作
                """;

        // 創建人工介入Hook
        HumanInTheLoopHook humanInTheLoopHook = HumanInTheLoopHook.builder()
                .approvalOn("sendEmailTool", ToolConfig.builder()
                        .description("發送郵件需要審批")
                        .build())
                .build();
        return ReactAgent.builder()
                .name("工作助手")
                .instruction(SYSTEM_PROMPT)
                .tools(FunctionToolCallback.builder("sendEmailTool", new SendEmailTool()).description("進行發送郵件的操作").inputType(EmailRequest.class).build()
                )
                // 基於內存的存儲
                .saver(new MemorySaver())
                .hooks(humanInTheLoopHook)
                .model(chatModel)
                .build();
    }
  1. 訪問agent的方式, 和同意審批的方法, 當agent判斷中斷後, 會返回審批中提示, 之後需要管理員調用同意方法,繼續執行
    @GetMapping
    public String ai(@RequestParam String question) throws Exception {
        RunnableConfig runnableConfig = RunnableConfig.builder().threadId("threadId").build();
        Optional<NodeOutput> result = reactAgent.invokeAndGetOutput(question, runnableConfig);
        if (result.isPresent() && result.get() instanceof InterruptionMetadata) {
            System.out.println("檢測到中斷,需要人工審批");
            interruptionMetadata = (InterruptionMetadata) result.get();
            return "已發送審批中";
        }
        List<Message> list = (List<Message>) result.get().state().data().get("messages");
        return list.get(list.size() - 1).getText();
    }

    @GetMapping("agree")
    public void agree() throws Exception {

        List<InterruptionMetadata.ToolFeedback> toolFeedbacks =
                interruptionMetadata.toolFeedbacks();


        InterruptionMetadata.Builder feedbackBuilder = InterruptionMetadata.builder()
                .nodeId(interruptionMetadata.node())
                .state(interruptionMetadata.state());

        toolFeedbacks.forEach(toolFeedback -> {
            InterruptionMetadata.ToolFeedback approvedFeedback =
                    InterruptionMetadata.ToolFeedback.builder(toolFeedback)
                            .result(InterruptionMetadata.ToolFeedback.FeedbackResult.APPROVED)
                            .build();
            feedbackBuilder.addToolFeedback(approvedFeedback);
        });

        InterruptionMetadata approvalMetadata = feedbackBuilder.build();

        //第二次調用 - 使用人工反饋恢復執行, 需要指定同一個會話ID
        RunnableConfig resumeConfig = RunnableConfig.builder()
                .threadId("threadId")
                .addMetadata(RunnableConfig.HUMAN_FEEDBACK_METADATA_KEY, approvalMetadata)
                .build();

        Optional<NodeOutput> finalResult = reactAgent.invokeAndGetOutput("", resumeConfig);
        if (finalResult.isPresent()) {
            System.out.println("執行完成");
            System.out.println("最終結果: " + finalResult.get());
        }
    }

啓動程序,進行測試 :

  1. 訪問 http://127.0.0.1:8089/ai?question=我要發送郵件

響應: 請提供您要發送的郵件內容,包括具體的信息或主題,這樣我才能幫您完成發送操作。

  1. 再次訪問``http://127.0.0.1:8089/ai?question=內容:“測試一下郵件發送”`

響應: 已發送審批中

  1. 調用審批接口http://127.0.0.1:8089/ai/agree

大模型返回: 郵件已成功發送!, 並且發送郵件的方法打印日誌 : 發送郵件: 測試一下郵件發送

整體流程如上

5. 檢索增強生成(RAG)

大型語言模型(LLM)雖然強大,但有兩個關鍵限制:

  • 有限的上下文——它們無法一次性攝取整個語料庫
  • 靜態知識——它們的訓練數據在某個時間點被凍結

檢索通過在查詢時獲取相關的外部知識來解決這些問題。這是檢索增強生成(RAG)的基礎:使用特定上下文的信息來增強 LLM 的回答。

SAA的RAG 可以以多種方式實現,具體取決於你的系統需求。

5.1 兩步 RAG:

兩步 RAG中,檢索步驟總是在生成步驟之前執行。這種架構簡單且可預測,適合許多應用,其中檢索相關文檔是生成答案的明確前提。

代碼示例:

首先需要構建一個檢索庫,並指定一個向量模型(這裏使用的仍然是通義的模型),,並從外部讀取一個公司規章制度的文檔,將其內容向量化, 作為AI的外部知識庫. 並給Agent設置好提示詞

@Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        SimpleVectorStore simpleVectorStore =
                SimpleVectorStore.builder(embeddingModel).build();
        // 1. 加載文檔
        Resource resource = new FileSystemResource("/Users/hehe/Downloads/text.txt");
        TextReader textReader = new TextReader(resource);
        List<Document> documents = textReader.get();
        // 2. 分割文檔為塊
        TokenTextSplitter splitter = new TokenTextSplitter();
        List<Document> chunks = splitter.apply(documents);

        //向量化存儲
        simpleVectorStore.add(chunks);
        return simpleVectorStore;
    }

    @Bean
    public ReactAgent ragReactAgent() {
        String SYSTEM_PROMPT = """
                你是一個公司內部智能助手,你需要根據公司規章制度文檔,來回答公司員工的問題.
                """;
        return ReactAgent.builder()
                .name("工作助手")
                .instruction(SYSTEM_PROMPT)
                // 基於內存的存儲
                .saver(new MemorySaver())
                .model(chatModel)
                .build();
    }

公司規章制度如下:

考勤制度
一、為加強考勤管理,維護工作秩序,提高工作效率,特制定本制度。

二、公司員工必須自覺遵守勞動紀律,按時上下班,不遲到,不早退,工作時間不得擅自離開工作崗位,外出辦理業務前,須經本部門負責人同意。

三、週一至週六為工作日,週日為休息日。公司機關週日和夜間值班由辦公室統一安排,市場營銷部、項目技術部、投資發展部、會議中心週日值班由各部門自行安排,報分管領導批准後執行。因工作需要週日或夜間加班的,由各部門負責人填寫加班審批表,報分管領導批准後執行。節日值班由公司統一安排。

四、嚴格請、銷假制度。員工因私事請假1天以內的(含1天),由部門負責人批准;3天以內的(含3天),由副總經理批准;3天以上的,報總經理批准。副總經理和部門負責人請假,一律由總經理批准。請假員工事畢向批准人銷假。未經批准而擅離工作崗位的按曠工處理。

五、上班時間開始後5分鐘至30分鐘內到班者,按遲到論處;超過30分鐘以上者,按曠工半天論處。提前30分鐘以內下班者,按早退論處;超過30分鐘者,按曠工半天論處。

六、1個月內遲到、早退累計達3次者,扣發5天的基本工資;累計達3次以上5次以下者,扣發10天的基本工資;累計達5次以上10次以下者,扣發當月15天的基本工資;累計達10次以上者,扣發當月的基本工資。

七、曠工半天者,扣發當天的基本工資、效益工資和獎金;每月累計曠工1天者,扣發5天的基本工資、效益工資和獎金,並給予一次警告處分;每月累計曠工2天者,扣發10天的基本工資、效益工資和獎金,並給予記過1次處分;每月累計曠工3天者,扣發當月基本工資、效益工資和獎金,並給予記大過1次處分;每月累計曠工3天以上,6天以下者,扣發當月基本工資、效益工資和獎金,第二個月起留用察看,發放基本工資;每月累計曠工6天以上者(含6天),予以辭退。

八、工作時間禁止打牌、下棋、串崗聊天等做與工作無關的事情。如有違反者當天按曠工1天處理;當月累計2次的,按曠工2天處理;當月累計3次的,按曠工3天處理。

九、參加公司組織的會議、培訓、學習、考試或其他團隊活動,如有事請假的,必須提前向組織者或帶隊者請假。在規定時間內未到或早退的,按照本制度第五條、第六條、第七條規定處理;未經批准擅自不參加的,視為曠工,按照本制度第七條規定處理。

十、員工按規定享受探親假、婚假、產育假、結育手術假時,必須憑有關證明資料報總經理批准;未經批准者按曠工處理。員工病假期間只發給基本工資。

十一、經總經理或分管領導批准,決定假日加班工作或值班的每天補助20元;夜間加班或值班的,每個補助10元;節日值班每天補助40元。未經批准,值班人員不得空崗或遲到,如有空崗者,視為曠工,按照本制度第七條規定處理;如有遲到者,按本制度第五條、第六條規定處理。

十二、員工的考勤情況,由各部門負責人進行監督、檢查,部門負責人對本部門的考勤要秉公辦事,認真負責。如有弄虛作假、包庇袒護遲到、早退、曠工員工的,一經查實,按處罰員工的雙倍予以處罰。凡是受到本制度第五條、第六條、第七條規定處理的員工,取消本年度先進個人的評比資格。

使用時,按照兩步 RAG的使用方式, 需要先根據問題,在向量庫中檢索與問題相關的內容,並攜帶到問題的上下文中.

    @GetMapping
    public String ai(@RequestParam String question) throws Exception {
        RunnableConfig runnableConfig = RunnableConfig.builder().threadId("threadId").build();

        List<Message> messages = new ArrayList<>();
      	// 根據問題檢索內容
        List<Document> documents = vectorStore.similaritySearch(question);
        if (CollectionUtil.isNotEmpty(documents)) {
            // 構建上下文
            String context = documents.stream()
                    .map(Document::getText)
                    .collect(Collectors.joining("""
                                                        
                            """));
            Message contextMessage = new UserMessage("請根據以下上下文,回答問題:" + context);
            messages.add(contextMessage);
        }
        messages.add(new UserMessage(question));
        return reactAgent.call(messages, runnableConfig).getText();
    }

啓動調用:http://127.0.0.1:8089/ai?question=一個月可以遲到幾次

響應:

根據所提供的《考勤制度》第六條規定: > 六、1個月內遲到、早退累計達3次者,扣發5天的基本工資;累計達3次以上5次以下者,扣發10天的基本工資;累計達5次以上10次以下者,扣發當月15天的基本工資;累計達10次以上者,扣發當月的基本工資。 從制度內容可以看出: - 公司並未規定“允許”遲到的具體次數,而是對遲到行為設定了逐級處罰措施。 - 即使遲到1次,也屬於違紀行為(按第五條定義為“遲到”),只是在第6條中從累計達3次起開始經濟處罰。 - 因此,理想情況下,一個月應遲到0次。 - 但若從“不被扣工資”的角度理解“可以遲到幾次”,那麼最多可遲到2次(因為第3次起就要扣工資)。 結論: 嚴格來説,公司不允許遲到;但從處罰起點看,一個月內遲到不超過2次不會觸發第六條的工資扣罰,但依然屬於違反考勤紀律的行為。

可以看到,大模型成功的回答出了他本身認知之外的問題, 讀取了公司內部的文檔

5.2 Agentic RAG

Agentic 檢索增強生成(RAG)將檢索增強生成的優勢與基於 Agent 的推理相結合。Agent(由 LLM 驅動)不是在回答之前檢索文檔,而是逐步推理並決定在交互過程中何時以及如何檢索信息。

示例:

同樣需要構建一個存儲庫, 並加載文檔. 再建一個Tool, 供Agent查詢文檔使用

    @Bean
    public VectorStore vectorStore(EmbeddingModel embeddingModel) {
        SimpleVectorStore simpleVectorStore =
                SimpleVectorStore.builder(embeddingModel).build();
        // 1. 加載文檔
        Resource resource = new FileSystemResource("/Users/hehe/Downloads/text.txt");
        TextReader textReader = new TextReader(resource);
        List<Document> documents = textReader.get();
        // 2. 分割文檔為塊
        TokenTextSplitter splitter = new TokenTextSplitter();
        List<Document> chunks = splitter.apply(documents);

        //向量化存儲
        simpleVectorStore.add(chunks);
        return simpleVectorStore;
    }

    public record SearchRequest(@ToolParam(description = "檢索文檔的問題") String question) {
    }

    // 可以檢索公司文檔
    public static class SearchDocumentTool implements BiFunction<SearchRequest, ToolContext, String> {
        @Override
        public String apply(
                @ToolParam(description = "檢索文檔的問題") SearchRequest question,
                ToolContext toolContext) {
            List<Document> documents = SpringUtil.getBean("vectorStore", VectorStore.class).similaritySearch(question.question);
            if (documents.isEmpty()) {
                return "沒有找到相關的文檔";
            }
            //返回檢索到的數據
            return documents.stream().map(Document::getText).collect(Collectors.joining("""
                                        
                    """));
        }
    }

註冊Tool,並指示模型調用

    @Bean
    public ReactAgent ragReactAgent() {
        String SYSTEM_PROMPT = """
                你是一個公司內部智能助手
                你可以根據以下工具檢索公司的文檔,來提供上下文:
                
                - searchDocumentTool: 通過該工具檢索公司文檔
                
                你需要根據公司規章制度文檔,來回答公司員工的問題.
                """;

        return ReactAgent.builder()
                .name("工作助手")
                .instruction(SYSTEM_PROMPT)
                .tools(FunctionToolCallback.builder("searchDocumentTool", new SearchDocumentTool()).description("檢索文檔").inputType(SearchRequest.class).build())
                // 基於內存的存儲
                .saver(new MemorySaver())
                .model(chatModel)
                .build();
    }

使用方式:

    @GetMapping
    public String ai(@RequestParam String question) throws Exception {
        RunnableConfig runnableConfig = RunnableConfig.builder().threadId("threadId").build();
        return reactAgent.call(question, runnableConfig).getText();
    }

啓動調用: http://127.0.0.1:8089/ai?question=節假日上班補貼多少

返回響應:

根據提供的考勤制度,關於節假日上班的補貼標準如下: - 假日加班或值班:每天補助 20元。 - 夜間加班或值班:每個補助 10元。 - 節日值班:每天補助 40元。 需要注意的是,所有加班或值班必須經過總經理或分管領導批准,未經批准不得擅自離崗或遲到,否則將按曠工處理。

成功讀取文檔內容

5.3 混合 RAG

混合 RAG 結合了兩步 RAG 和 Agentic RAG 的特點。它引入了中間步驟,如查詢預處理、檢索驗證和生成後檢查。這些系統比固定管道提供更多靈活性,同時保持對執行的一定控制。

典型組件包括:

  • 查詢增強:修改輸入問題以提高檢索質量。這可能涉及重寫不清晰的查詢、生成多個變體或用額外上下文擴展查詢。
  • 檢索驗證:評估檢索到的文檔是否相關且充分。如果不夠,系統可能會優化查詢並再次檢索。
  • 答案驗證:檢查生成的答案的準確性、完整性以及與源內容的一致性。如果需要,系統可以重新生成或修訂答案。

官網的概念性示例:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import java.util.List;
import java.util.stream.Collectors;

class HybridRAGSystem {
  private final ChatModel chatModel;
  private final VectorStore vectorStore;

  public HybridRAGSystem(ChatModel chatModel, VectorStore vectorStore) {
      this.chatModel = chatModel;
      this.vectorStore = vectorStore;
  }

  public String answer(String userQuestion) {
      // 1. 查詢增強
      String enhancedQuery = enhanceQuery(userQuestion);

      int maxAttempts = 3;
      for (int attempt = 0; attempt < maxAttempts; attempt++) {
          // 2. 檢索文檔
          List<Document> docs = vectorStore.similaritySearch(enhancedQuery);

          // 3. 檢索驗證
          if (!isRetrievalSufficient(docs)) {
              enhancedQuery = refineQuery(enhancedQuery, docs);
              continue;
          }

          // 4. 生成答案
          String answer = generateAnswer(userQuestion, docs);

          // 5. 答案驗證
          ValidationResult validation = validateAnswer(answer, docs);
          if (validation.isValid()) {
              return answer;
          }

          // 6. 根據驗證結果決定下一步
          if (validation.shouldRetry()) {
              enhancedQuery = refineBasedOnValidation(enhancedQuery, validation);
          } else {
              return answer; // 返回當前最佳答案
          }
      }

      return "無法生成滿意的答案";
  }

  private String enhanceQuery(String query) {
      return query; // 實現查詢增強邏輯
  }

  private boolean isRetrievalSufficient(List<Document> docs) {
      return !docs.isEmpty() && calculateRelevanceScore(docs) > 0.7;
  }

  private double calculateRelevanceScore(List<Document> docs) {
      return 0.8; // 實現相關性評分邏輯
  }

  private String refineQuery(String query, List<Document> docs) {
      return query; // 實現查詢優化邏輯
  }

  private String generateAnswer(String question, List<Document> docs) {
      String context = docs.stream()
          .map(Document::getText)
          .collect(Collectors.joining("

"));

      ChatClient client = ChatClient.builder(chatModel).build();
      return client.prompt()
          .system("基於以下上下文回答問題:
" + context)
          .user(question)
          .call()
          .content();
  }

  private ValidationResult validateAnswer(String answer, List<Document> docs) {
      // 實現答案驗證邏輯
      return new ValidationResult(true, false);
  }

  private String refineBasedOnValidation(String query, ValidationResult validation) {
      return query; // 基於驗證結果優化查詢
  }

  class ValidationResult {
      private boolean valid;
      private boolean shouldRetry;

      public ValidationResult(boolean valid, boolean shouldRetry) {
          this.valid = valid;
          this.shouldRetry = shouldRetry;
      }

      public boolean isValid() { return valid; }
      public boolean shouldRetry() { return shouldRetry; }
  }
}
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.