第二章:5、RAG进阶优化

一、混合检索(向量 + 关键词)

1. 纯向量检索的局限

  • 语义相近但答案靠后:向量相似度高不代表文档真正包含答案。
  • 精确关键词匹配弱:用户搜索特定术语时,向量检索可能漏掉精确匹配的文档。
  • 多路召回更可靠:结合向量检索和关键词检索,取并集后融合排序。

2. 混合检索的实现(以PGVector为例)

sql 复制代码
-- PGVector 全文检索 + 向量检索 SQL

SELECT * FROM knowledge_base
WHERE
    tsv @@ to_tsquery('chinese', :query)  -- 关键词匹配
ORDER BY
    ts_rank(tsv, to_tsquery('chinese', :query)) DESC,  -- 关键词相关性
    embedding <=> :query_vector  -- 向量相似度
LIMIT :topK;

3. Java端混合检索逻辑

核心思路:向量检索与关键词检索并行,结果融合去重(RRF排序)。

java 复制代码
@Service
public class HybridSearchService {

    @Resource
    private VectorStore vectorStore;
    @Resource
    private JdbcTemplate jdbcTemplate;

    public List<Document> hybridSearch(String query, int topK) {
        // 1. 向量检索
        List<Document> vectorResults = vectorStore.similaritySearch(
            SearchRequest.builder().query(query).topK(topK).build()
        );

        // 2. 关键词检索(PGVector全文搜索)
        String sql = """
            SELECT id, content, ts_rank(tsv, to_tsquery('chinese', ?)) AS rank
            FROM knowledge_base
            WHERE tsv @@ to_tsquery('chinese', ?)
            ORDER BY rank DESC
            LIMIT ?
            """;
        List<Document> keywordResults = jdbcTemplate.query(sql, ...);

        // 3. RRF融合排序(Reciprocal Rank fusion)
        return rrfMerge(vectorResults, keywordResults, topK);
    }

    /**
     * RRF(Reciprocal Rank Fusion)融合排序
     * 对两个检索结果按排名的倒数加权融合,得到最终排序
     */
    private List<Document> rrfMerge(
            List<Document> vectorResults, List<Document> keywordResults, int topK) {
        double k = 60.0; // RRF常量
        Map<String, Double> scores = new HashMap<>();
        Map<String, Document> docMap = new HashMap<>();

        // 向量检索结果按排名加权
        for (int i = 0; i < vectorResults.size(); i++) {
            String id = vectorResults.get(i).getId();
            double score = 1.0 / (k + i + 1);
            scores.merge(id, score, Double::sum);
            docMap.put(id, vectorResults.get(i));
        }

        // 关键词检索结果按排名加权
        for (int i = 0; i < keywordResults.size(); i++) {
            String id = keywordResults.get(i).getId();
            double score = 1.0 / (k + i + 1);
            scores.merge(id, score, Double::sum);
            docMap.put(id, keywordResults.get(i));
        }

        // 按融合分数降序取topK
        return scores.entrySet().stream()
            .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
            .limit(topK)
            .map(e -> docMap.get(e.getKey()))
            .toList();
    }
}

4. 集成到RAG流程

java 复制代码
@GetMapping("/ask-hybrid")
public String askHybrid(@RequestParam String question) {
    // Step 1: 混合检索
    List<Document> docs = hybridSearchService.hybridSearch(question, 10);

    // Step 2: 生成答案
    String context = buildContext(docs);

    return chatClient.prompt()
        .user("参考资料:\n" + context + "\n\n问题:" + question)
        .call().content();
}

二、Rerank重排序

Rerank 是对检索结果按查询相关性进行二次精排的过程。粗检索阶段多召回(topK=20),Rerank阶段精排取Top5。

1. DashScope Rerank(qwen3-rerank)

通过阿里云 DashScope API 调用 qwen3-rerank 模型,实现基于API的文档重排序。

java 复制代码
@Service
public class DashScopeDocumentReranker implements DocumentPostProcessor {
    private final RestTemplate restTemplate;
    private final String apiKey;
    private final String modelName;
    private final int topN;

    public DashScopeDocumentReranker(String apiKey, String modelName, int topN) {
        this.restTemplate = new RestTemplate();
        this.apiKey = apiKey;
        this.modelName = modelName;
        this.topN = topN;
    }

    @Override
    public List<Document> process(Query query, List<Document> documents) {
        if (documents == null || documents.size() <= 1) {
            return documents;
        }
        List<String> texts = documents.stream().map(Document::getText).toList();
        Map<String, Object> body = Map.of(
            "model", modelName,
            "query", query.text(),
            "documents", texts,
            "top_n", Math.min(topN, documents.size())
        );
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        headers.set("Authorization", "Bearer " + apiKey);
        try {
            ResponseEntity<RerankResponse> response = restTemplate.postForEntity(
                "https://dashscope.aliyuncs.com/api/v1/services/rerank",
                new HttpEntity<>(body, headers),
                RerankResponse.class
            );
            return reorder(documents, response.getBody());
        } catch (Exception e) {
            log.warn("DashScope rerank调用失败: {}", e.getMessage());
            return documents;
        }
    }

    /**
     * 按rerank返回的相关性顺序重排文档(结果已按relevance_score降序排列)
     */
    private List<Document> reorder(List<Document> documents, RerankResponse response) {
        if (response == null || response.results == null || response.results.isEmpty()) {
            return documents;
        }
        List<Document> reranked = new ArrayList<>();
        for (RerankResult result : response.results) {
            if (result.index != null && result.index >= 0 && result.index < documents.size()) {
                reranked.add(documents.get(result.index));
            }
        }
        return reranked.isEmpty() ? documents : reranked;
    }

    // ===== qwen3-rerank API 响应结构 =====
    static class RerankResponse {
        public List<RerankResult> results;
    }
    static class RerankResult {
        @JsonProperty("relevance_score")
        public Double relevanceScore;
        public Integer index;
    }
}

2. 硅基流动 Rerank(BAAI/bge-reranker-v2-m3)

调用硅基流动平台的 bge-reranker-v2-m3 模型进行文档重排序。

java 复制代码
@Service
public class RerankService {
    private static final String API_URL = "https://api.siliconflow.cn/v1/rerank";
    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper;

    @Value("${spring.ai.siliconflow-api-key}")
    private String apiKey;

    public RerankService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder
            .connectTimeout(Duration.ofSeconds(10))
            .readTimeout(Duration.ofSeconds(30))
            .build();
        this.objectMapper = new ObjectMapper();
    }

    public List<Document> rerank(String query, List<Document> documents, int topN) {
        if (documents == null || documents.isEmpty()) {
            return List.of();
        }
        if (documents.size() == 1) {
            return documents;
        }
        List<String> texts = documents.stream().map(Document::getText).toList();
        Map<String, Object> body = Map.of(
            "model", "BAAI/bge-reranker-v2-m3",
            "query", query,
            "documents", texts,
            "top_n", Math.min(topN, documents.size())
        );
        try {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            headers.set("Authorization", "Bearer " + apiKey);
            HttpEntity<Map<String, Object>> request = new HttpEntity<>(body, headers);
            ResponseEntity<Map> response = restTemplate.postForEntity(
                API_URL, request, Map.class
            );
            @SuppressWarnings("unchecked")
            List<Map<String, Object>> results = (List<Map<String, Object>>) response.getBody().get("results");
            if (results == null || results.isEmpty()) {
                return documents;
            }
            return results.stream().map(r -> {
                int index = (int) r.get("index");
                Document doc = documents.get(index);
                doc.getMetadata().put("rerankScore", r.get("relevance_score"));
                return doc;
            }).toList();
        } catch (Exception e) {
            log.warn("硅基流动Rerank调用失败: {}", e.getMessage());
            return documents.stream().limit(topN).toList();
        }
    }
}

3. LLM文档重排器(自定义实现)

Spring AI 2.0.1 没有内置 reranker,可以用 LLM 对检索结果按相关性重新排序。

java 复制代码
/**
 * 基于LLM的文档重排器(实现DocumentPostProcessor接口)
 */
public class LlmDocumentReranker implements DocumentPostProcessor {
    private static final Pattern INDEX_PATTERN = Pattern.compile("\\d+");
    private final ChatClient chatClient;
    private final int maxDocPreviewLength;

    public LlmDocumentReranker(ChatClient.Builder chatClientBuilder, int maxDocPreviewLength) {
        this.chatClient = chatClientBuilder.build();
        this.maxDocPreviewLength = maxDocPreviewLength;
    }

    @Override
    public List<Document> process(Query query, List<Document> documents) {
        if (documents == null || documents.size() <= 1) {
            return documents;
        }
        StringBuilder docList = new StringBuilder();
        for (int i = 0; i < documents.size(); i++) {
            String text = documents.get(i).getText();
            if (text == null) { text = ""; }
            if (text.length() > maxDocPreviewLength) {
                text = text.substring(0, maxDocPreviewLength) + "...";
            }
            docList.append("[").append(i).append("] ").append(text).append("\n\n");
        }
        String prompt = """
            你是一个文档相关性排序助手。请根据文档与查询的相关程度重新排序。
            查询:%s
            文档列表:
            %s
            请只返回文档编号的排序结果,用英文逗号分隔。
            例如:2,0,3,1
            """.formatted(query.text(), docList);
        String result = chatClient.prompt().user(prompt).call().content();
        return reorder(documents, result);
    }

    /**
     * 解析LLM返回的编号序列,按顺序重排文档。解析失败时返回原顺序。
     */
    private List<Document> reorder(List<Document> documents, String llmOutput) {
        if (llmOutput == null || llmOutput.isBlank()) { return documents; }
        Matcher matcher = INDEX_PATTERN.matcher(llmOutput);
        List<Integer> indices = new ArrayList<>();
        while (matcher.find()) {
            int idx = Integer.parseInt(matcher.group());
            if (idx >= 0 && idx < documents.size() && !indices.contains(idx)) {
                indices.add(idx);
            }
        }
        if (indices.size() != documents.size()) { return documents; }
        List<Document> reranked = new ArrayList<>(documents.size());
        for (int idx : indices) { reranked.add(documents.get(idx)); }
        return reranked;
    }
}

4. Rerank集成方式

方式一:手动编排

java 复制代码
@GetMapping("/ask-rerank")
public String askWithRerank(@RequestParam String question) {
    // Step 1: 粗检索(多召回)
    List<Document> candidates = vectorStore.similaritySearch(
        SearchRequest.builder().query(question).topK(20).build()
    );
    // Step 2: Rerank精排(取Top5)
    List<Document> reranked = rerankService.rerank(question, candidates, 5);
    // Step 3: 生成答案
    String context = buildContext(reranked);
    return chatClient.prompt()
        .user("参考资料:\n" + context + "\n\n问题:" + question)
        .call().content();
}

方式二:Advisor集成

java 复制代码
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
    .documentRetriever(VectorStoreDocumentRetriever.builder()
        .vectorStore(vectorStore)
        .similarityThreshold(0.7)
        .topK(6)
        .build())
    .documentPostProcessors(new DashScopeDocumentReranker(apiKey, "qwen3-rerank", 3))
    .build();

三、Query改写与多查询扩展

1. Query改写的必要性

  • 用户提问可能口语化、模糊或含指代词,直接用于检索效果差。
  • 通过LLM改写为更适合知识库检索的形式,提升召回率。

2. RewriteQueryTransformer(Spring AI内置)

Spring AI 2.0 提供了内置的查询改写器,可直接集成到 Advisor 中。

java 复制代码
@Bean
public RetrievalAugmentationAdvisor ragAdvisorWithRewrite(
        ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
    RewriteQueryTransformer rewriteTransformer = RewriteQueryTransformer.builder()
        .chatClientBuilder(chatClientBuilder)
        .targetSearchSystem("vector store")
        .build();
    return RetrievalAugmentationAdvisor.builder()
        .queryTransformers(rewriteTransformer)
        .documentRetriever(VectorStoreDocumentRetriever.builder()
            .vectorStore(vectorStore)
            .topK(5)
            .build())
        .build();
}

3. 自定义多查询改写服务

生成多个改写版本,分别检索后取并集,进一步提升召回率。

java 复制代码
@Service
public class QueryRewriteService {
    @Resource
    private ChatClient ragManualChatClient;
    private final ObjectMapper objectMapper;

    public QueryRewriteService() {
        this.objectMapper = new ObjectMapper();
    }

    public List<String> rewrite(String originalQuery, String conversationHistory) {
        String prompt = String.format("""
            你是一个搜索查询优化专家。请将用户的问题改写为更适合在知识库中检索的形式。
            要求:
            1. 生成3个不同角度的改写版本
            2. 用JSON数组格式返回
            3. 保留原始问题的核心意图
            4. 如果有对话历史,结合上下文补全指代
            对话历史:%s
            用户问题:%s
            请直接返回JSON数组,不要包含其他文字。
            """, conversationHistory, originalQuery);
        String result = ragManualChatClient.prompt().user(prompt).call().content();
        return parseJsonArray(result);
    }

    /**
     * 将大模型返回的JSON数组字符串解析为List<String>
     * 大模型返回格式示例:["改写版本1", "改写版本2", "改写版本3"]
     */
    private List<String> parseJsonArray(String json) {
        if (json == null || json.isBlank()) { return List.of(); }
        try {
            String cleaned = json.trim();
            if (cleaned.startsWith("```json")) {
                cleaned = cleaned.substring(7);
            } else if (cleaned.startsWith("```")) {
                cleaned = cleaned.substring(3);
            }
            if (cleaned.endsWith("```")) {
                cleaned = cleaned.substring(0, cleaned.length() - 3);
            }
            cleaned = cleaned.trim();
            return objectMapper.readValue(cleaned, new TypeReference<List<String>>() {});
        } catch (Exception e) {
            return Arrays.stream(json.split("\n"))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .map(s -> s.replaceAll("^\\d+[\\.\\)\u3001]\u0020*", ""))
                .filter(s -> !s.startsWith("["))
                .toList();
        }
    }
}

4. 多查询检索流程

java 复制代码
@GetMapping("/ask-multi-query")
public Flux<String> askMultiQuery(
    @RequestParam String message,
    @RequestParam(defaultValue = "default") String conversationId) {
    // Step 1: 改写问题(生成3个版本)
    List<String> queries = queryRewriteService.rewrite(message, "");
    // Step 2: 分别检索并去重
    Set<String> seen = new HashSet<>();
    List<Document> allDocs = new ArrayList<>();
    for (String q : queries) {
        List<Document> docs = knowledgeService.search(q, 5, 0.6);
        for (Document doc : docs) {
            if (seen.add(doc.getId())) {
                allDocs.add(doc);
            }
        }
    }
    // Step 3: 生成答案
    String context = buildContext(allDocs);
    return chatClient.prompt()
        .user("参考资料:\n" + context + "\n\n问题:" + message)
        .stream().content();
}

四、闲聊感知查询改写

问题背景:用户可能输入闲聊内容(如 "你好"),此时不应触发RAG检索流程,避免浪费资源和产生无关检索结果。

java 复制代码
public class ChitchatAwareQueryTransformer implements QueryTransformer {
    private final RewriteQueryTransformer delegate;

    public ChitchatAwareQueryTransformer(ChatClient.Builder chatClientBuilder) {
        this.delegate = RewriteQueryTransformer.builder()
            .chatClientBuilder(chatClientBuilder)
            .targetSearchSystem("vector store")
            .build();
    }

    @Override
    public Query transform(Query query) {
        String text = query.text();
        if (isChitchat(text)) {
            return query; // 闲聊直接透传
        }
        return delegate.transform(query); // 知识性问题交给改写器
    }

    private boolean isChitchat(String text) {
        if (text == null || text.isBlank()) return true;
        String trimmed = text.trim();
        if (trimmed.length() <= 6) return true;
        String lower = trimmed.toLowerCase();
        return lower.matches(".*(你好|您好|hi|hello|谢谢|感谢|再见|你是谁).*")
            && !lower.matches(".*(什么是|怎么|如何|为什么|区别|原理|实现).*");
    }
}

集成到RAG Advisor

java 复制代码
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
    .queryTransformers(new ChitchatAwareQueryTransformer(ChatClient.builder(chatModel)))
    .documentRetriever(VectorStoreDocumentRetriever.builder()
        .vectorStore(vectorStore)
        .similarityThreshold(0.7)
        .topK(6)
        .build())
    .build();

五、查询增强与空上下文处理

ContextualQueryAugmenter 负责将检索到的文档与用户查询拼装为完整的 Prompt。

allowEmptyContext 配置

  • 默认情况下,无检索结果时会抛出异常。
  • 设置 allowEmptyContext(true) 后,无检索结果时仍能正常生成回答。
java 复制代码
@Bean
public RetrievalAugmentationAdvisor ragAdvisor(
        VectorStore vectorStore) {
    return RetrievalAugmentationAdvisor.builder()
        .documentRetriever(VectorStoreDocumentRetriever.builder()
            .vectorStore(vectorStore)
            .topK(5)
            .build())
        .queryAugmenter(ContextualQueryAugmenter.builder()
            .allowEmptyContext(true)  // 允许空上下文
            .build())
        .build();
}

六、幻觉检测与答案质量保障

1. 基于引用验证的幻觉检测

幻觉检测的核心思路:检查模型生成的答案是否完全基于参考资料,识别不受支持的说法。

java 复制代码
@Service
public class HallucinationDetector {
    private final ChatClient chatClient;
    private final ObjectMapper objectMapper;

    public HallucinationDetector(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("你是一个严格的事实核查专家。")
            .build();
        this.objectMapper = new ObjectMapper();
    }

    public Map<String, Object> detect(String answer, String context) {
        String prompt = String.format("""
            请判断以下答案是否完全基于参考资料得出。
            参考资料:%s
            模型答案:%s
            请用JSON格式返回:
            {"reliable": true/false, "reason": "判断理由", "unsupportedClaims": [...]}
            """, context, answer);
        String result = chatClient.prompt().user(prompt).call().content();
        return parseResult(result);
    }

    /**
     * 将大模型返回的幻觉检测结果JSON字符串解析为Map
     */
    private Map<String, Object> parseResult(String json) {
        Map<String, Object> defaultResult = Map.of(
            "reliable", true,
            "reason", "解析失败,默认标记为可信"
        );
        if (json == null || json.isBlank()) { return defaultResult; }
        try {
            String cleaned = json.trim();
            if (cleaned.startsWith("```json")) {
                cleaned = cleaned.substring(7);
            } else if (cleaned.startsWith("```")) {
                cleaned = cleaned.substring(3);
            }
            if (cleaned.endsWith("```")) {
                cleaned = cleaned.substring(0, cleaned.length() - 3);
            }
            cleaned = cleaned.trim();
            JsonNode node = objectMapper.readTree(cleaned);
            boolean reliable = node.has("reliable") && node.get("reliable").asBoolean();
            String reason = node.has("reason") ? node.get("reason").asText() : "";
            List<String> unsupportedClaims = new ArrayList<>();
            if (node.has("unsupportedClaims") && node.get("unsupportedClaims").isArray()) {
                for (JsonNode claim : node.get("unsupportedClaims")) {
                    unsupportedClaims.add(claim.asText());
                }
            }
            return Map.of("reliable", reliable, "reason", reason, "unsupportedClaims", unsupportedClaims);
        } catch (Exception e) {
            return defaultResult;
        }
    }
}

2. RAG流程集成

java 复制代码
@GetMapping("/ask-safe")
public Map<String, Object> askSafe(
    @RequestParam String message,
    @RequestParam(defaultValue = "default") String conversationId) {
    // Step 1: 检索文档
    List<Document> docs = vectorStore.similaritySearch(
        SearchRequest.builder().query(message).topK(5).build()
    );
    // Step 2: 生成答案
    String context = buildContext(docs);
    String answer = chatClient.prompt()
        .user("参考资料:\n" + context + "\n\n问题:" + message)
        .call().content();
    // Step 3: 幻觉检测
    Map<String, Object> result = hallucinationDetector.detect(answer, context);
    // Step 4: 组装返回
    Map<String, Object> response = new HashMap<>();
    response.put("answer", answer);
    response.put("reliable", result.get("reliable"));
    response.put("reason", result.get("reason"));
    response.put("unsupportedClaims", result.get("unsupportedClaims"));
    return response;
}

3. System Prompt约束建议

  • 只基于提供的参考资料回答问题,不要编造信息。
  • 如果参考资料中没有相关内容,请明确告知用户。
  • 引用参考资料时标注来源。

七、RAG Advisor完整优化流程

核心流程

查询改写 → 检索 → 重排 → 增强 → 生成

完整配置示例

java 复制代码
@Bean
public ChatClient ragOptimizedChatClient(
    ChatClient.Builder builder, VectorStore vectorStore, ChatModel chatModel) {
    String promptTemplate = """
        你是一个知识库助手。请严格遵循以下规则:
        1. 只基于提供的参考资料回答问题,不要编造信息
        2. 如果参考资料中没有相关内容,请明确告知用户
        3. 引用参考资料时标注来源
        参考资料:
        {context}
        问题:{question}
        """;
    RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
        .queryTransformers(new ChitchatAwareQueryTransformer(builder))
        .documentRetriever(VectorStoreDocumentRetriever.builder()
            .vectorStore(vectorStore)
            .similarityThreshold(0.7)
            .topK(6)
            .build())
        .documentPostProcessors(new DashScopeDocumentReranker(apiKey, "qwen3-rerank", 3))
        .queryAugmenter(ContextualQueryAugmenter.builder()
            .allowEmptyContext(true)
            .build())
        .build();
    return builder.defaultAdvisors(advisor).build();
}

八、常用工具方法

buildContext:文档列表拼接为上下文字符串

将检索到的 Document 列表拼接为一段完整的上下文文本。

java 复制代码
private String buildContext(List<Document> documents) {
    if (documents == null || documents.isEmpty()) { return ""; }
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < documents.size(); i++) {
        Document doc = documents.get(i);
        sb.append("--- 文档").append(i + 1).append(" ---\n");
        Object fileName = doc.getMetadata().get("fileName");
        if (fileName != null) {
            sb.append("来源:").append(fileName).append("\n");
        }
        sb.append(doc.getText()).append("\n\n");
    }
    return sb.toString();
}

extractCitations:从响应中提取引用来源

解析 Spring AI ChatResponse 中的 citations 信息。

java 复制代码
/**
 * 从ChatResponse中提取引用的文档信息
 */
private List<Map<String, Object>> extractCitations(ChatResponse response) {
    List<Map<String, Object>> citations = new ArrayList<>();
    if (response == null || response.getMetadata() == null) { return citations; }
    Map<String, Object> metadata = response.getMetadata();
    Object docsObj = metadata.get("documents");
    if (docsObj instanceof List<?>) {
        for (Object obj : (List<?>) docsObj) {
            if (obj instanceof Document doc) {
                Map<String, Object> citation = new HashMap<>();
                citation.put("content", doc.getText());
                citation.put("metadata", doc.getMetadata());
                citations.add(citation);
            }
        }
    }
    return citations;
}

总结

结合混合检索、Rerank重排序、Query改写、闲聊感知、查询增强和幻觉检测,可显著提升知识库问答系统的检索效果与答案质量。

建议:在实际应用中分阶段引入上述机制,持续优化用户体验和结果可靠性。

相关推荐
今年下半年1 小时前
【低成本埋点统计】Spring Boot+vue2前后端分离项目统计系统访问、使用、登录次数
spring boot·后端·状态模式
codigger1 小时前
Java 26 深度解析:一次“性能兑现 + 契约收紧”的版本
java·后端·编程·编程语言
盖世英雄酱581362 小时前
格式化成日期为什么提前一天呢,同样的时间点还是非必现???
java·后端
威哥爱编程2 小时前
【AI全栈12-05】Spring Boot 用 Tool Calling 让 AI 查实时订单与库存
java·spring boot·后端
用户298698530142 小时前
Python 实战:将 HTML 内容保存为图像文件
后端·python·html
挖掘狂人2 小时前
Java 26 深度解析:一次“性能兑现 + 契约收紧”的版本
javascript·后端·性能优化
威哥爱编程2 小时前
【AI全栈12-06】Spring Boot 用 RAG 搭企业私有知识库问答
java·spring boot·后端
HjhIron2 小时前
深入浅出 LangGraph:从单 Agent 痛点走向多 Agent 复杂协作编排实战
ai编程