在java中使用deepseek并接入联网搜索和知识库

前言

当前AI技术生态以 Python 为主导,这几天在研究用 Java 搭建知识库使用,最终都避不开 Python,于是打算记录下结果,目前是有 2 个方案,第一个方案是 在 Python 中使用 embedding嵌入模型,完成数据向量化与向量搜索,推荐使用这个方案,简单也方便。第二个方案是不使用 embedding嵌入模型,使用 es 来完成向量存储,但仍需要 Python 来完成数据的向量化。

本文分为三部分,第一部分是接入 deepseek-r1,第二部分是接入联网搜索,第三部分是使用自建知识库(两个实现方案),知识库为可选功能,并且实现起来也挺麻烦,不需要的可以直接看前两部分即可。

同时,本次的代码也已经放在了 GitHub 上,deepseek-java

前置准备

首先介绍一下本次的开发环境:

Java17 + SpringBoot 3.3.2

Python 3.11

deepseek 的 APIkeys(在官网上买就可以了)

tavily(搜索引擎,通过这个实现联网搜索,在第二部分会具体说)

项目依赖

我们需要一个 SpringBoot 的项目,具体依赖如下

xml 复制代码
<dependencies>
  <!--springBoot依赖-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
  
  <!--联网搜索部分所需依赖-->
  <dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.3</version>
  </dependency>
</dependencies>

第一部分-接入 deepseek-r1

获取 ApiKeys

操作步骤如下,进入deepseek 官网,充值余额,生成 key 即可。

编写基本代码

封裝聊天请求类,包含四个基本参数。

typescript 复制代码
public class ChatRequest {
    // 用户的问题
    private String message;
    // 是否启用联网搜索
    private boolean useSearch;
    // 是否使用知识库
    private boolean useRAG;
    // 是否启用知识库最大阈值
    private boolean maxToggle;
​
    public ChatRequest() {
    }
​
    public ChatRequest(String message, boolean useSearch, boolean useRAG, boolean maxToggle) {
        this.message = message;
        this.useSearch = useSearch;
        this.useRAG = useRAG;
        this.maxToggle = maxToggle;
    }
​
    public String getMessage() {
        return message;
    }
​
    public void setMessage(String message) {
        this.message = message;
    }
​
    public boolean isUseSearch() {
        return useSearch;
    }
​
    public void setUseSearch(boolean useSearch) {
        this.useSearch = useSearch;
    }
​
    public boolean isUseRAG() {
        return useRAG;
    }
​
    public void setUseRAG(boolean useRAG) {
        this.useRAG = useRAG;
    }
​
    public boolean isMaxToggle() {
        return maxToggle;
    }
​
    public void setMaxToggle(boolean maxToggle) {
        this.maxToggle = maxToggle;
    }
}

这里是核心功能类、实现了基本对话功能的代码,只需要配置 API_KEY 变量就可以启动测试,默认使用 deepseek-r1,你也可以改为 v3。

dart 复制代码
@RestController
@RequestMapping("/api")
public class DeepSeekController {
​
    // 存储上下文信息
    private final Deque<Map<String, String>> conversationHistory = new ArrayDeque<>();
​
    // 序列化参数
    private final ObjectMapper objectMapper = new ObjectMapper();
​
    // 设置最大的上下文信息
    private final int MAX_HISTORY = 10;
    
    // deepseek 的 API_KEY
    private final String API_KEY = "Bearer sk-xxxxxxxxxxxxx";
​
    @PostMapping("/chat")
    public SseEmitter chat(@RequestBody ChatRequest request) {
        SseEmitter emitter = new SseEmitter();
        try {
            // 创建用户消息
            Map<String, String> userMessage = new HashMap<>();
            userMessage.put("role", "user");
            userMessage.put("content", request.getMessage());
            conversationHistory.add(userMessage);
​
            // 准备请求体
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("model", "deepseek-reasoner");
//            requestBody.put("model", "deepseek-chat");
            requestBody.put("messages", new ArrayList<>(conversationHistory));
            requestBody.put("stream", true);
​
            // 创建 HTTP 客户端
            HttpClient client = HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(600))
                    .build();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(URI.create("https://api.deepseek.com/chat/completions"))
                    .header("Content-Type", "application/json")
                    .header("Authorization", API_KEY)
                    .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody)))
                    .build();
​
            // 发送请求并处理响应流
            StringBuilder aiResponseBuilder = new StringBuilder();
            StringBuilder reasoningBuilder = new StringBuilder();
            System.out.println("\n\n" + "=".repeat(20) + "思考过程" + "=".repeat(20) + "\n");
            client.send(httpRequest, HttpResponse.BodyHandlers.ofLines())
                    .body()
                    .forEach(line -> {
                        try {
                            if (line.startsWith("data: ")) {
                                String jsonData = line.substring(6);
                                if (!"[DONE]".equals(jsonData)) {
                                    Map<String, Object> response = objectMapper.readValue(jsonData, Map.class);
                                    Map<String, Object> delta = extractDeltaContent(response);
​
                                    // 处理思考过程
                                    if (delta != null && delta.containsKey("reasoning_content") && delta.get("reasoning_content") != null) {
                                        String reasoningContent = (String) delta.get("reasoning_content");
                                        reasoningBuilder.append(reasoningContent);
                                        // 直接打印思考过程
                                        System.out.print(reasoningContent);
                                        System.out.flush(); // 确保立即打印
                                        // 发送思考过程,使用不同的事件类型
                                        emitter.send(SseEmitter.event()
                                                .name("reasoning")
                                                .data(Map.of("reasoning_content", reasoningContent)));
                                    }
​
                                    // 处理回答内容
                                    if (delta != null && delta.containsKey("content") && delta.get("content") != null) {
                                        // 如果是第一个回答内容,先打印分隔线
                                        if (aiResponseBuilder.isEmpty()) {
                                            System.out.println("\n\n" + "=".repeat(20) + "思考结束" + "=".repeat(20) + "\n");
                                        }
                                        String content = (String) delta.get("content");
                                        aiResponseBuilder.append(content);
                                        // 直接打印回答内容
                                        System.out.print(content);
                                        System.out.flush(); // 确保立即打印
                                        emitter.send(SseEmitter.event()
                                                .name("answer")
                                                .data(Map.of("content", content)));
                                    }
                                }
                            }
                        } catch (Exception e) {
                            emitter.completeWithError(e);
                        }
                    });
​
            // 创建AI响应消息并添加到历史记录
            Map<String, String> aiMessage = new HashMap<>();
            aiMessage.put("role", "assistant");
            aiMessage.put("content", aiResponseBuilder.toString());
            conversationHistory.add(aiMessage);
​
            // 如果历史记录超过最大限制,移除最早的消息
            while (conversationHistory.size() > MAX_HISTORY * 2) {
                conversationHistory.pollFirst();
            }
​
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
        return emitter;
    }
​
    // 解析响应
    private Map<String, Object> extractDeltaContent(Map<String, Object> response) {
        List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
        if (choices != null && !choices.isEmpty()) {
            return (Map<String, Object>) choices.get(0).get("delta");
        }
        return null;
    }
​
    // 清除上下文信息
    @PostMapping("/clean")
    public void clearHistory() {
        conversationHistory.clear();
    }
}
​

第二部分-接入 联网搜索

联网搜索我们需要借助一个免费的ai搜索引擎实现,每个月有 1000 次搜索次数,已经足够日常使用了。官网: Tavily 注册完成后创建一个 ApiKey 即可。

添加一个搜索类,负责实现我们的搜索逻辑,同样只需要替换 apiKey 的变量即可。

typescript 复制代码
@Component
public class SearchUtils {
    // 搜索引擎
    private String baseUrl = "https://api.tavily.com/search";
    // apikey
    private String apiKey = "tvly-dev-xxxxxxxxxx";
    private final OkHttpClient client;
    private final ObjectMapper objectMapper;
​
    public SearchUtils() {
        this.client = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build();
        this.objectMapper = new ObjectMapper();
    }
​
    public List<Map<String, String>> tavilySearch(String query) {
        List<Map<String, String>> results = new ArrayList<>();
        try {
            Map<String,String> requestBody = new HashMap<String, String>();
            requestBody.put("query", query);
            Request request = new Request.Builder()
                    .url(baseUrl)
                    .post(RequestBody.create(MediaType.parse("application/json"), objectMapper.writeValueAsString(requestBody)))
                    .header("Content-Type", "application/json")
                    .header("Authorization", "Bearer" + apiKey)
                    .build();
​
​
            try (Response response = client.newCall(request).execute()) {
                if (!response.isSuccessful()) throw new IOException("请求失败: " + response);
​
                JsonNode jsonNode = objectMapper.readTree(response.body().string()).get("results");
​
                if (!jsonNode.isEmpty()) {
                    jsonNode.forEach(data -> {
                        Map<String, String> processedResult = new HashMap<>();
                        processedResult.put("title", data.get("title").toString());
                        processedResult.put("url", data.get("url").toString());
                        processedResult.put("content", data.get("content").toString());
                        results.add(processedResult);
                    });
                }
            }
        } catch (Exception e) {
            System.err.println("搜索时发生错误: " + e.getMessage());
        }
        return results;
    }
}

然后在核心类中引入搜索类,并在向 ai 提问前先去搜索并提前加入到 prompt 中,此时核心类如下:

dart 复制代码
@RestController
@RequestMapping("/api")
public class DeepSeekController {
​
    // 存储上下文信息
    private final Deque<Map<String, String>> conversationHistory = new ArrayDeque<>();
​
    // 序列化参数
    private final ObjectMapper objectMapper = new ObjectMapper();
​
    // 设置最大的上下文信息
    private final int MAX_HISTORY = 10;
​
    // deepseek 的 apikey
    private final String API_KEY = "Bearer sk-xxxxxxxxxxxxxxxxx";
​
    private final SearchUtils searchUtils;
​
    public DeepSeekController(SearchUtils searchUtils) {
        this.searchUtils = searchUtils;
    }
​
    @PostMapping("/chat")
    public SseEmitter chat(@RequestBody ChatRequest request) {
        SseEmitter emitter = new SseEmitter();
        try {
            // 获取搜索结果
            StringBuilder context = new StringBuilder();
            if (request.isUseSearch()) {
                List<Map<String, String>> searchResults = searchUtils.tavilySearch(request.getMessage());
                if (!searchResults.isEmpty()) {
                    System.out.println("search results size(联网搜索个数): " + searchResults.size());
                    context.append("\n\n联网搜索结果:\n");
                    for (int i = 0; i < searchResults.size(); i++) {
                        Map<String, String> result = searchResults.get(i);
                        context.append(String.format("\n%d. %s\n", i + 1, result.get("title")));
                        context.append(String.format("   %s\n", result.get("content")));
                        context.append(String.format("   来源: %s\n", result.get("url")));
                    }
                }
            }
            // 如果有上下文,添加系统消息
            if (context.length() > 0) {
                Map<String, String> systemMessage = new HashMap<>();
                systemMessage.put("role", "system");
                systemMessage.put("content", "请基于以下参考信息回答用户问题:\n" + context.toString());
                conversationHistory.add(systemMessage);
            }
​
            // 创建用户消息
            Map<String, String> userMessage = new HashMap<>();
            userMessage.put("role", "user");
            userMessage.put("content", request.getMessage());
            conversationHistory.add(userMessage);
​
            // 准备请求体
            Map<String, Object> requestBody = new HashMap<>();
            requestBody.put("model", "deepseek-reasoner");
//            requestBody.put("model", "deepseek-chat");
            requestBody.put("messages", new ArrayList<>(conversationHistory));
            requestBody.put("stream", true);
​
            // 创建 HTTP 客户端
            HttpClient client = HttpClient.newBuilder()
                    .connectTimeout(Duration.ofSeconds(600))
                    .build();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                    .uri(URI.create("https://api.deepseek.com/chat/completions"))
                    .header("Content-Type", "application/json")
                    .header("Authorization", API_KEY)
                    .POST(HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(requestBody)))
                    .build();
​
            // 发送请求并处理响应流
            StringBuilder aiResponseBuilder = new StringBuilder();
            StringBuilder reasoningBuilder = new StringBuilder();
            System.out.println("\n\n" + "=".repeat(20) + "思考过程" + "=".repeat(20) + "\n");
            client.send(httpRequest, HttpResponse.BodyHandlers.ofLines())
                    .body()
                    .forEach(line -> {
                        try {
                            if (line.startsWith("data: ")) {
                                String jsonData = line.substring(6);
                                if (!"[DONE]".equals(jsonData)) {
                                    Map<String, Object> response = objectMapper.readValue(jsonData, Map.class);
                                    Map<String, Object> delta = extractDeltaContent(response);
​
                                    // 处理思考过程
                                    if (delta != null && delta.containsKey("reasoning_content") && delta.get("reasoning_content") != null) {
                                        String reasoningContent = (String) delta.get("reasoning_content");
                                        reasoningBuilder.append(reasoningContent);
                                        // 直接打印思考过程
                                        System.out.print(reasoningContent);
                                        System.out.flush(); // 确保立即打印
                                        // 发送思考过程,使用不同的事件类型
                                        emitter.send(SseEmitter.event()
                                                .name("reasoning")
                                                .data(Map.of("reasoning_content", reasoningContent)));
                                    }
​
                                    // 处理回答内容
                                    if (delta != null && delta.containsKey("content") && delta.get("content") != null) {
                                        // 如果是第一个回答内容,先打印分隔线
                                        if (aiResponseBuilder.isEmpty()) {
                                            System.out.println("\n\n" + "=".repeat(20) + "思考结束" + "=".repeat(20) + "\n");
                                        }
                                        String content = (String) delta.get("content");
                                        aiResponseBuilder.append(content);
                                        // 直接打印回答内容
                                        System.out.print(content);
                                        System.out.flush(); // 确保立即打印
                                        emitter.send(SseEmitter.event()
                                                .name("answer")
                                                .data(Map.of("content", content)));
                                    }
                                }
                            }
                        } catch (Exception e) {
                            emitter.completeWithError(e);
                        }
                    });
​
            // 创建AI响应消息并添加到历史记录
            Map<String, String> aiMessage = new HashMap<>();
            aiMessage.put("role", "assistant");
            aiMessage.put("content", aiResponseBuilder.toString());
            conversationHistory.add(aiMessage);
​
            // 如果历史记录超过最大限制,移除最早的消息
            while (conversationHistory.size() > MAX_HISTORY * 2) {
                conversationHistory.pollFirst();
            }
​
            emitter.complete();
        } catch (Exception e) {
            emitter.completeWithError(e);
        }
        return emitter;
    }
​
    // 解析响应
    private Map<String, Object> extractDeltaContent(Map<String, Object> response) {
        List<Map<String, Object>> choices = (List<Map<String, Object>>) response.get("choices");
        if (choices != null && !choices.isEmpty()) {
            return (Map<String, Object>) choices.get(0).get("delta");
        }
        return null;
    }
​
    // 清除上下文信息
    @PostMapping("/clean")
    public void clearHistory() {
        conversationHistory.clear();
    }
}

此时可以看到,在创建用户消息前,请求参数中就已经存在联网搜索的结果。

第三部分-接入 自建知识库

知识库部分的实现有 2 种方式,推荐采用 embedding方案(效果更优且维护成本低),ES方案适用于已有ES技术栈的场景

通过 embedding嵌入模型 实现

首先我们要先搞明白,什么是embedding嵌入模型?embedding 是机器学习的核心技术之一,通过将离散(文字、图片等)的数据转为连续的向量空间,并捕获数据特征。例如我们搜索的时候, 输入可爱的猫图,那么 embedding 就会把这五个字转为数字数组得到向量,再根据这个向量和所有的数据向量距离进行对比,数值越近说明越相关。最后按照相似度把数据返回给我们。

数据向量化我们需要借助 python 实现,python 项目结构如下,忽略 Dockerfile。app.py 是代码的主体,config.json 是配置文件,我们只需要改动这个地方即可。data 下存放的是我们知识库元文件及索引文件(刚开始没有 index 文件属于正常的,因为 index 是基于 json 格式的知识库生成的)。model 则是我下载的embedding 模型,最后 requirements 则是依赖表。

下面介绍具体的使用方式:

创建项目,下载 m3e-base 向量模型

bash 复制代码
git clone https://huggingface.co/moka-ai/m3e-base ./model/m3e-base

执行完成后注意,需额外手动下载两个配置文件。进入 huggingface 的地址:m3e-base,手动将这两个文件下载下来,放到 model 目录内。

创建 app.py 文件,不需要改任何地方

python 复制代码
import os
import json
import tempfile
​
import faiss
from flask import Flask, request, jsonify, send_file
from sentence_transformers import SentenceTransformer
from pathlib import Path
from typing import List, Dict
​
app = Flask(__name__)
​
# 加载配置文件
with open('config.json', 'r', encoding='utf-8') as f:
    CONFIG = json.load(f)
​
data_fields = CONFIG["data_fields"]
required_data_fields = ["metadata_fields", "content_field"]
for field in required_data_fields:
    if field not in data_fields:
        raise KeyError(f"Missing required data_fields config: {field}")
metadata_fields = data_fields["metadata_fields"]
content_field = data_fields["content_field"]
​
# 初始化模型
if not Path(CONFIG["model_path"]).exists():
    raise FileNotFoundError(f"模型未找到: {CONFIG['model_path']}")
model = SentenceTransformer(CONFIG["model_path"])
​
class VectorSearchSystem:
    def __init__(self):
        self.index = None
        self.documents = []
        self._auto_load()
​
    def _auto_load(self):
        """自动加载持久化数据"""
        try:
            # 加载FAISS索引
            if os.path.exists(CONFIG["index_file"]):
                self.index = faiss.read_index(CONFIG["index_file"])
            else:
                self.initialize_index()
​
            # 加载文档元数据
            if os.path.exists(CONFIG["json_data_file"]):
                with open(CONFIG["json_data_file"], 'r', encoding='utf-8') as f:
                    self.documents = json.load(f)
            else:
                self.documents = []
​
        except Exception as e:
            print(f"[ERROR] 数据加载失败: {str(e)}")
            self.initialize_index()
            self.documents = []
​
    def initialize_index(self):
        """创建新索引"""
        self.index = faiss.IndexFlatIP(CONFIG["vector_dim"])
​
    def search(self, query: str, top_k: int = None) -> List[Dict]:
        top_k = top_k or CONFIG["default_top_k"]
        query_vector = model.encode([query], normalize_embeddings=True).astype('float32')
        distances, indices = self.index.search(query_vector, top_k*2)  # 扩大召回范围
        results = []
        for idx, score in zip(indices[0], distances[0]):
            if score < CONFIG["similarity_threshold"]:
                continue  # 关键点:严格阈值过滤
            if 0 <= idx < len(self.documents):
                results.append({
                    **self.documents[idx],
                    "similarity_score": float(score)
                })
​
        # 二次排序并截断
        return sorted(results, key=lambda x: x["similarity_score"], reverse=True)[:top_k]
​
# 初始化系统
search_system = VectorSearchSystem()
​
def format_search_result(result: Dict) -> str:
    """格式化单个搜索结果"""
    content_text = result.get(content_field, "")
​
    # 处理元数据字段
    metadata_lines = []
    for field in metadata_fields:
        value = result.get(field, "")
        if value:
            metadata_lines.append(f"{value}\n")
​
    # 组合元数据和内容
    formatted_metadata = "".join(metadata_lines)
    formatted_content = f"{content_text}\n" if content_text else ""
​
    return f"{formatted_metadata}{formatted_content}"
​
@app.route('/api/search', methods=['GET'])
def handle_search():
    """搜索接口"""
    query = request.args.get('query')
    top_k = request.args.get('top_k', type=int)
​
    if not query:
        return jsonify({"error": "Missing query parameter"}), 400
​
    try:
        results = search_system.search(query, top_k=top_k)
        # 按照相似度分数排序
        sorted_results = sorted(results, key=lambda x: x["similarity_score"], reverse=True)
        # 格式化输出
        formatted_output = "\n".join([format_search_result(r) for r in sorted_results])
        return app.response_class(
            response=formatted_output,
            status=200,
            mimetype='text/plain'
        )
    except Exception as e:
        return jsonify({"error": str(e)}), 500
​
@app.route('/api/generate-index', methods=['POST'])
def generate_index():
    """
    生成临时索引接口
    接收JSON文件 → 生成FAISS索引 → 返回索引文件和对应的处理后的JSON
    """
    if 'file' not in request.files:
        return jsonify({"error": "No file uploaded"}), 400
​
    file = request.files['file']
    if file.filename == '':
        return jsonify({"error": "Empty filename"}), 400
​
    try:
        # 使用临时目录处理
        with tempfile.TemporaryDirectory() as tmp_dir:
            # 解析输入数据
            documents = json.load(file)
​
            # 验证数据格式
            required_fields = metadata_fields + [content_field]
            for doc in documents:
                if not all(field in doc for field in required_fields):
                    missing = [field for field in required_fields if field not in doc]
                    raise ValueError(f"Document missing fields: {missing}")
​
            # 生成向量
            contents = [doc[content_field] for doc in documents]
            vectors = model.encode(contents, normalize_embeddings=True).astype('float32')
​
            # 创建临时索引
            tmp_index_path = Path(tmp_dir) / "temp_index.index"
            index = faiss.IndexFlatIP(CONFIG["vector_dim"])
            index.add(vectors)
            faiss.write_index(index, str(tmp_index_path))
​
            # 生成带ID的元数据
            processed_data = [
                {**{field: doc[field] for field in metadata_fields},
                 content_field: doc[content_field],
                 "vector_id": idx}
                for idx, doc in enumerate(documents)
            ]
​
            # 保存临时JSON
            tmp_json_path = Path(tmp_dir) / "processed_data.json"
            with open(tmp_json_path, 'w', encoding='utf-8') as f:
                json.dump(processed_data, f, ensure_ascii=False)
​
​
            index = faiss.IndexFlatIP(CONFIG["vector_dim"])
            index.add(vectors)
            faiss.write_index(index, str(CONFIG['faiss_dir']))
​
            # 打包返回文件(示例保留索引文件)
            return "200"
            # return send_file(
            #     tmp_index_path,
            #     mimetype='application/octet-stream',
            #     as_attachment=True,
            #     download_name="generated_index.index"
            # )
​
    except json.JSONDecodeError:
        return jsonify({"error": "Invalid JSON format"}), 400
    except Exception as e:
        return jsonify({"error": str(e)}), 500
​
if __name__ == '__main__':
    os.makedirs('data', exist_ok=True)
    app.run(host='0.0.0.0', port=CONFIG["server_port"], debug=CONFIG["debug"])

创建 config.json 文件,下面是每个参数具体的意思:

  • model_path:向量的预训练模型的路径

  • index_file:搜索时使用的向量索引的文件路径

  • json_data_file:搜索时使用的原始数据的 JSON 文件路径

  • faiss_dir:根据原始数据 JSON 生成的 faiss 文件存储路径

  • vector_dim:向量的维度大小

  • default_top_k:默认情况下返回的最相似结果的数量

  • similarity_threshold:相似度阈值(仅返回高于该值的结果)

  • server_port:服务端口号

  • debug:调试模式

  • result_format:

    • include_score:返回的结果中是否包含相似度得分
    • max_content_length:返回结果中内容的最大长度,超出部分会被截断
  • data_fields:

    • metadata_fields:原始数据中,除向量字段外的所有字段
    • content_field:原始数据中的向量字段(只能有一个)

下面我简单说一下要如何配置,model_path 是我们的向量模型路径,这里我用的是 m3e-base 模型,模型小而且效果不错,后面会使用该模型做演示并下载到本地,index_filejson_data_file 是我们在做向量查询时使用的文件。其中 index 作为索引,json 作为元数据使用。faiss_dir 则是我们调用 generate-index 接口后生成的索引文件路径。vector_dim 向量维度是跟向量模型本身挂钩的,例如 m3e-base 这个模型的维度就是 768,不可以设置别的值。下面几个参数上面也说的很清楚了。最后一个参数是重点,这个是负责匹配我们知识库元文件字段的,目前大部分源文件格式都是 json,但是字段不可能都一样,所以这里需要配置各自的字段,例如我的元文件字段有四个,需要按照 content 字段做搜索,那么这个字段就是向量字段。

json 复制代码
{
  "model_path": "./model/m3e-base",
  "index_file": "./data/generated_index.index",
  "json_data_file": "./data/jsonData.json",
  "faiss_dir": "./data/faiss_temp.index",
  "vector_dim": 768,
  "default_top_k": 5,
  "similarity_threshold": 0.7,
  "server_port": 5001,
  "debug": true,
  "result_format": {
    "include_score": false,
    "max_content_length": 1000
  },
  "data_fields": {
    "metadata_fields": ["doc_name", "chapter", "item_number"],
    "content_field": "content"
  }
}

最后则是requirements 文件,创建后,直接 pip install -r requirements.txt安装依赖即可。

ini 复制代码
blinker==1.9.0
certifi==2025.1.31
charset-normalizer==3.4.1
click==8.1.8
faiss-cpu==1.7.4
filelock==3.17.0
Flask==3.0.2
fsspec==2025.3.0
huggingface-hub==0.29.2
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
joblib==1.4.2
MarkupSafe==3.0.2
mpmath==1.3.0
networkx==3.4.2
nltk==3.9.1
numpy==1.26.4
packaging==24.2
pillow==11.1.0
PyYAML==6.0.2
regex==2024.11.6
requests==2.32.3
safetensors==0.5.3
scikit-learn==1.6.1
scipy==1.15.2
sentence-transformers==3.4.1
sentencepiece==0.2.0
sympy==1.13.1
threadpoolctl==3.5.0
tokenizers==0.21.0
torch==2.6.0
torchvision==0.21.0
tqdm==4.67.1
transformers==4.49.0
typing_extensions==4.12.2
urllib3==2.3.0
Werkzeug==3.1.3

现在我们可以开始启动了,刚刚我们已经安装了 m3e 向量模型,并下载了依赖。先简单介绍下逻辑和使用方法:

项目在启动时会加载当前目录下的 config.json 配置文件,随后读取 data 目录下的索引和元数据,启动成功后对外暴露 2 个接口,分别是 /api/search 向量查询接口 和 /api/generate-index 生成 index 索引接口。前者为 get 请求,参数为 query,返回跟 query 相近的数据。后者参数为文件,入参名为 file,传入元数据后,生成对应的 index 索引。

使用方法: 配置config.json,项目启动后,调用 /api/generate-index 接口,参数是你的知识库 json 元文件,然后把生成的 index 索引以及你的 json 原文件放到 data 目录下,修改config.json 中的 index_filejson_data_file ,将这两个变量指向 data 目录下的索引和元文件 (在文章的最后提供了测试用的元数据)

到这里,python 的部分就完成了,下面只需要在 Java 核心类中调用 Python 的 /api/search 向量化查询用户的问题,就可以启用知识库了

erlang 复制代码
// 获取搜索结果(如果已经添加了联网搜索,不要加入这一行代码)
StringBuilder context = new StringBuilder();
// 是否启用知识库
if (request.isUseRAG()) {
  HttpClient client = HttpClient.newHttpClient();
  String encodedMsg = URLEncoder.encode(request.getMessage(), StandardCharsets.UTF_8);
  HttpRequest vectorRequest = HttpRequest.newBuilder()
    .uri(URI.create("http://localhost:5001/api/search?query=" + encodedMsg + "&top_k=" + (request.isMaxToggle() ? 10 : 5)))
    .build();
​
  HttpResponse<String> response = client.send(vectorRequest, HttpResponse.BodyHandlers.ofString());
  if (response.statusCode() != 200) {
    throw new IOException("Failed to get vector: HTTP " + response.statusCode());
  }
  String body = response.body();
​
  System.out.println("知识库参考: " + body);
  if (!body.isEmpty()) {
    context.append("\n\n知识库参考:\n");
    context.append(body);
  }
}
​
// 如果有上下文,添加系统消息(如果已经添加了联网搜索,不要加入下面这一段代码)
if (context.length() > 0) {
  Map<String, String> systemMessage = new HashMap<>();
  systemMessage.put("role", "system");
  systemMessage.put("content", "请基于以下参考信息回答用户问题:\n" + context.toString());
  conversationHistory.add(systemMessage);
}

知识库查询测试,入参为 蒙娜丽莎是谁? 成功匹配知识库内容

通过 elasticsearch 向量搜索实现

首先我们需要安装好 Elasticsearch 8.17.2 + Kibana 8.17.2

Java 中引入es依赖:

xml 复制代码
  <!--知识库依赖(注意 es 的版本与自己的对应)-->
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-client</artifactId>
  <version>8.17.2</version>
</dependency>
<dependency>
  <groupId>org.elasticsearch.client</groupId>
  <artifactId>elasticsearch-rest-high-level-client</artifactId>
  <version>7.17.0</version>
</dependency>

继续新增 es 搜索类

scss 复制代码
@Configuration
public class ElasticsearchKnnSearch {
​
    private final RestHighLevelClient esClient;
    private final ObjectMapper objectMapper = new ObjectMapper();
​
    // es 索引名
    @Value("${esKnn.index-name:sora_vector_index}")
    private String indexName;
    // es 匹配的字段名
    @Value("${esKnn.es-field:content}")
    private String content;
    // es 匹配的向量字段名
    @Value("${esKnn.es-vector-field:content_vector}")
    private String contentVector;
    // 匹配方式,使用 match 匹配
    @Value("${esKnn.match:match}")
    private String match;
    // 单词匹配比例 一句话中 45% 以上的单词匹配
    @Value("${esKnn.work-check:45}")
    private String workCheck;
    // 匹配逻辑,使用 and
    @Value("${esKnn.rule:and}")
    private String rule;
​
​
    public ElasticsearchKnnSearch() {
        // 初始化带认证的ES客户端
        CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
        credentialsProvider.setCredentials(
                AuthScope.ANY,
                new UsernamePasswordCredentials("xx", "xxxxx")
        );
​
        RestClientBuilder builder = RestClient.builder(
                        new HttpHost("localhost", 9200, "http"))
                .setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder
                        .setDefaultCredentialsProvider(credentialsProvider));
​
        this.esClient = new RestHighLevelClient(builder);
    }
​
    // 从接口获取向量数组
    private List<Float> getVectorFromAPI(String message) throws IOException, InterruptedException {
        HttpClient client = HttpClient.newHttpClient();
        String encodedMsg = URLEncoder.encode(message, StandardCharsets.UTF_8);
        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("http://localhost:5001/msg_to_vector?msg=" + encodedMsg))
                .build();
​
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() != 200) {
            throw new IOException("Failed to get vector: HTTP " + response.statusCode());
        }
​
        JsonNode root = objectMapper.readTree(response.body());
        JsonNode vectorNode = root.get("vector");
        List<Float> vector = new ArrayList<>(vectorNode.size());
        for (JsonNode value : vectorNode) {
            vector.add(value.floatValue());
        }
        return vector;
    }
​
    // 执行kNN搜索
    public SearchResponse executeKnnSearch(int k, String msg) throws Exception {
        // 1. 获取查询向量
        List<Float> queryVector = getVectorFromAPI(msg);
​
        // 2. 构建搜索请求
        SearchRequest searchRequest = new SearchRequest(indexName);
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
​
        // 3. 构建kNN查询
        // 使用 XContentBuilder 安全构建
        XContentBuilder xContentBuilder = XContentFactory.jsonBuilder();
        xContentBuilder.startObject()
                .startObject("knn")
                .field("field", contentVector)
                .array("query_vector", queryVector.toArray())
                .field("k", k)
                .field("num_candidates", 50)
                .startObject("filter")
                .startObject(match)
                .startObject(content)
                .field("query", msg)
                .field("operator", rule)
                .field("minimum_should_match", workCheck + "%")
                .endObject()
                .endObject()
                .endObject()
                .endObject()
                .endObject();
​
        // 打印生成的JSON
        String queryJson = Strings.toString(xContentBuilder);
        System.out.println("Generated Query:\n" + queryJson);
​
        sourceBuilder.query(QueryBuilders.wrapperQuery(queryJson));
​
        searchRequest.source(sourceBuilder);
​
        // 4. 执行搜索
        return esClient.search(searchRequest, RequestOptions.DEFAULT);
    }
​
    public void close() throws IOException {
        esClient.close();
    }
​
    // List<EsVectorResponse>
    public List<String> vectorSearch(int k, String msg) throws Exception {
        ArrayList<String> vectorList = new ArrayList<>();
        try {
            SearchResponse response = executeKnnSearch(k,msg);
            // 处理搜索结果
            System.out.println("Search hits: " + response.getHits().getTotalHits().value);
            response.getHits().forEach(hit -> 
                System.out.println("Hit: " + hit.getSourceAsString()));
​
​
            // 遍历搜索结果
            for (SearchHit hit : response.getHits().getHits()) {
                Map<String, Object> sourceMap = hit.getSourceAsMap();
                if (sourceMap.containsKey(content)) {
                    // 这里是汇总所有的信息,请灵活修改,对应 es 的字段
                    Object contentText = sourceMap.get(content);
                    String doc_name = sourceMap.get("doc_name") == null ? "" : sourceMap.get("doc_name") + "\n";
                    String chapter = sourceMap.get("chapter") == null ? "" : sourceMap.get("chapter") + "\n";
                    String item_number = sourceMap.get("item_number") == null ? "" : sourceMap.get("item_number") + "\n";
                    if (contentText != null) {
                        String result = doc_name + chapter + item_number + contentText + "\n";
                        vectorList.add(result);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return vectorList;
    }
}

搜索类加入知识库逻辑(注意位置):

erlang 复制代码
// 是否启用知识库
if (request.isUseRAG()) {
    List<String> vectorSearch = elasticsearchKnnSearch.vectorSearch(request.isMaxToggle() ? 10 : 5, request.getMessage());
    System.out.println("知识库参考个数: " + vectorSearch.size());
    if (!vectorSearch.isEmpty()) {
      context.append("\n\n知识库参考:\n");
    }
    vectorSearch.forEach(data -> {
      context.append(data + "\n");
    });
}
​
// 如果有上下文,添加系统消息
if (context.length() > 0) {
  Map<String, String> systemMessage = new HashMap<>();
  systemMessage.put("role", "system");
  systemMessage.put("content", "请基于以下参考信息回答用户问题:\n" + context.toString());
  conversationHistory.add(systemMessage);
}

python代码如下,完成对 json 原数据存入 es 以及查询向量化

python 复制代码
import json
​
from flask import Flask, request, jsonify
import uuid
import numpy as np
import faiss
from sentence_transformers import SentenceTransformer
from elasticsearch.helpers import bulk
from elasticsearch import Elasticsearch
import os
import tempfile
app = Flask(__name__)
​
# 全局初始化组件 灵活配置
es = Elasticsearch(
    hosts=["http://localhost:9200"],
    basic_auth=("xx", "xxx")
)
model_path = "models/all-MiniLM-L6-v2"
# 使用模型
model = SentenceTransformer(model_path)
​
# 与模型输出维度一致
dimension = 384
# 索引名
index_name = "sora_vector_index"
​
# 初始化FAISS索引
faiss_index = faiss.IndexFlatL2(dimension)
​
# 确保索引存在
if not es.indices.exists(index=index_name):
    es.indices.create(index=index_name, body={
        "settings": {
            "analysis": {
                "analyzer": {
                    "ik_analyzer": {"type": "custom", "tokenizer": "ik_max_word"}
                }
            }
        },
        "mappings": {
            "properties": {
                "doc_name": {"type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart"},
                "chapter": {"type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart"},
                "item_number": {"type": "keyword"},
                "content": {"type": "text", "analyzer": "ik_max_word", "search_analyzer": "ik_smart"},
                "content_vector": {"type": "dense_vector", "dims": dimension}
            }
        }
    })
    print(f"{index_name}索引不存在,已创建")
​
# 将解析后 txt 文件上传到 es 里
def txt_uploaded_file(file_path):
    """处理上传文件的核心逻辑(按四行结构解析)"""
    with open(file_path, 'r', encoding='utf-8') as f:
        text = f.read()
​
    # 按行处理,过滤空行并去除首尾空格
    lines = [line.strip() for line in text.split('\n') if line.strip()]
​
    documents = []
    # 按每四行分割为一条记录
    for i in range(0, len(lines), 4):
        # 确保有足够四行数据
        if i + 3 >= len(lines):
            break  # 跳过不完整的记录
​
        doc_name = lines[i]
        chapter = lines[i+1]
        item_number = lines[i+2]
        content = lines[i+3]
​
        documents.append({
            "doc_name": doc_name,
            "chapter": chapter,
            "item_number": item_number,
            "content": content
        })
​
    # 生成向量并更新FAISS
    contents = [doc["content"] for doc in documents]
    embeddings = model.encode(contents)
    faiss_index.add(embeddings.astype(np.float32))
​
    # 添加向量到文档数据
    for doc, vector in zip(documents, embeddings):
        doc["content_vector"] = vector.tolist()
​
    # 批量导入ES
    actions = [{
        "_index": index_name,
        "_source": {
            **doc,
            "doc_id": str(uuid.uuid4())  # 添加唯一ID
        }
    } for doc in documents]
​
    success, _ = bulk(es, actions)
    return success, len(documents)
​
@app.route('/upload_save_to_es', methods=['POST'])
def upload_save_to_es():
    """文件上传处理端点"""
    if 'file' not in request.files:
        return jsonify({"error": "No file uploaded"}), 400
​
    file = request.files['file']
    if file.filename == '':
        return jsonify({"error": "Empty filename"}), 400
​
    # 保存临时文件
    _, temp_path = tempfile.mkstemp()
    file.save(temp_path)
​
    try:
        success_count, total_count = txt_uploaded_file(temp_path)
        return jsonify({
            "status": "success",
            "ingested": success_count,
            "total": total_count
        })
    except Exception as e:
        return jsonify({"error": str(e)}), 500
    finally:
        os.remove(temp_path)
​
​
​
@app.route('/save_to_es', methods=['POST'])
def upload_json():
    """处理JSON文件上传(自动补充向量字段)"""
    if 'json' not in request.files:
        return jsonify({"error": "No JSON file uploaded"}), 400
​
    file = request.files['json']
​
    try:
        # 解析JSON文件
        documents = json.load(file)
    except Exception as e:
        return jsonify({"error": f"无效的JSON格式: {str(e)}"}), 400
​
    try:
        # 校验基础字段
        required_fields = {'doc_name', 'chapter', 'item_number', 'content'}
        need_vectors = []  # 需要生成向量的文档索引
​
        for idx, doc in enumerate(documents):
            # 检查必需字段
            missing = required_fields - doc.keys()
            if missing:
                return jsonify({"error": f"文档 {idx} 缺少字段: {', '.join(missing)}"}), 400
​
            # 标记需要生成向量的文档
            if 'content_vector' not in doc or not isinstance(doc['content_vector'], list):
                need_vectors.append(idx)
​
        # 批量生成缺失的向量
        if need_vectors:
            contents = [documents[i]['content'] for i in need_vectors]
            embeddings = model.encode(contents)
​
            # 更新FAISS索引
            faiss_index.add(embeddings.astype(np.float32))
​
            # 回填向量到文档
            for vec_idx, doc_idx in enumerate(need_vectors):
                documents[doc_idx]['content_vector'] = embeddings[vec_idx].tolist()
​
        # 准备ES数据
        actions = [{
            "_index": index_name,
            "_source": {
                **doc,
                "doc_id": str(uuid.uuid4())  # 始终生成新ID
            }
        } for doc in documents]
​
        # 批量写入ES
        success, _ = bulk(es, actions)
        return jsonify({
            "status": "success",
            "ingested": success,
            "total": len(documents),
            "vectors_generated": len(need_vectors)
        })
​
    except Exception as e:
        return jsonify({"error": f"处理失败: {str(e)}"}), 500
​
# 外部调用使用
@app.route('/msg_to_vector', methods=['GET', 'POST'])
def encode_text():
    """将消息文本转换为向量"""
    msg = request.args.get('msg') if request.method == 'GET' else request.json.get('msg')
​
    if not msg:
        return jsonify({"error": "Missing 'msg' parameter"}), 400
​
    try:
        vector = model.encode(msg).tolist()
        return jsonify({"vector": vector, "dimension": len(vector)}), 200
    except Exception as e:
        return jsonify({"error": str(e)}), 500
​
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=True)
​

python 依赖:

ini 复制代码
aiohappyeyeballs==2.5.0
aiohttp==3.11.13
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.8.0
asgiref==3.8.1
attrs==25.1.0
backoff==2.2.1
bcrypt==4.3.0
blinker==1.9.0
build==1.2.2.post1
cachetools==5.5.2
certifi==2025.1.31
charset-normalizer==3.4.1
chroma-hnswlib==0.7.6
chromadb==0.6.3
click==8.1.8
coloredlogs==15.0.1
dataclasses-json==0.6.7
Deprecated==1.2.18
distro==1.9.0
document==1.0
durationpy==0.9
elastic-transport==8.17.0
elasticsearch==8.17.1
faiss-cpu==1.9.0
fastapi==0.115.11
filelock==3.17.0
Flask==3.1.0
flatbuffers==25.2.10
frozenlist==1.5.0
fsspec==2025.2.0
google-auth==2.38.0
googleapis-common-protos==1.69.1
grpcio==1.70.0
h11==0.14.0
httpcore==1.0.7
httptools==0.6.4
httpx==0.28.1
huggingface-hub==0.29.1
humanfriendly==10.0
idna==3.10
importlib_metadata==8.5.0
importlib_resources==6.5.2
itsdangerous==2.2.0
Jinja2==3.1.5
joblib==1.4.2
jsonpatch==1.33
jsonpointer==3.0.0
kubernetes==32.0.1
langchain-community==0.0.28
langchain-core==0.3.41
langsmith==0.1.147
markdown-it-py==3.0.0
MarkupSafe==3.0.2
marshmallow==3.26.1
mdurl==0.1.2
mmh3==5.1.0
monotonic==1.6
mpmath==1.3.0
multidict==6.1.0
mypy-extensions==1.0.0
networkx==3.4.2
numpy==1.26.4
oauthlib==3.2.2
onnxruntime==1.20.1
opentelemetry-api==1.30.0
opentelemetry-exporter-otlp-proto-common==1.30.0
opentelemetry-exporter-otlp-proto-grpc==1.30.0
opentelemetry-instrumentation==0.51b0
opentelemetry-instrumentation-asgi==0.51b0
opentelemetry-instrumentation-fastapi==0.51b0
opentelemetry-proto==1.30.0
opentelemetry-sdk==1.30.0
opentelemetry-semantic-conventions==0.51b0
opentelemetry-util-http==0.51b0
orjson==3.10.15
overrides==7.7.0
packaging==23.2
pillow==11.1.0
posthog==3.19.0
propcache==0.3.0
protobuf==5.29.3
pyasn1==0.6.1
pyasn1_modules==0.4.1
pydantic==2.10.6
pydantic_core==2.27.2
Pygments==2.19.1
PyPika==0.48.9
pyproject_hooks==1.2.0
python-dateutil==2.9.0.post0
python-dotenv==1.0.0
PyYAML==6.0.2
regex==2024.11.6
requests==2.32.3
requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
rich==13.9.4
rsa==4.9
safetensors==0.5.3
scikit-learn==1.6.1
scipy==1.15.2
sentence-transformers==3.4.1
shellingham==1.5.4
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.38
starlette==0.46.0
sympy==1.13.1
tenacity==8.5.0
threadpoolctl==3.5.0
tokenizers==0.21.0
torch==2.6.0
tqdm==4.67.1
transformers==4.49.0
typer==0.15.2
typing-inspect==0.9.0
typing_extensions==4.12.2
urllib3==2.3.0
uvicorn==0.34.0
uvloop==0.21.0
watchfiles==1.0.4
websocket-client==1.8.0
websockets==15.0.1
Werkzeug==3.1.3
wrapt==1.17.2
yarl==1.18.3
zipp==3.21.0

使用方法:

  1. 修改 es 搜索类和 Python 脚本中的 es 地址
  2. 调用 Python 的 save_to_es 接口,参数名为 file,类型是 json 文件。将测试文件加入到 es 的索引中(测试数据放下面)
  3. 调用 Python 的 msg_to_vector 接口,参数为 msg,查看是否正常
  4. 正常使用,知识库接入完成

测试元数据

json 复制代码
[
  {
    "doc_name": "量子物理导论",
    "chapter": "第一章 波粒二象性",
    "item_number": "1.1a",
    "content": "薛定谔方程描述了微观粒子的波函数演化,其数学形式为iℏ∂ψ/∂t = Ĥψ。该方程在量子力学中的地位相当于经典力学中的牛顿第二定律。"
  },
  {
    "doc_name": "文艺复兴艺术史",
    "chapter": "第三章 达芬奇研究",
    "item_number": "MonaLisa",
    "content": "蒙娜丽莎的微笑因其微妙的表情变化闻名,X光扫描显示画作下方存在多个草稿层,证明达芬奇曾多次修改人物面部结构。"
  },
  {
    "doc_name": "加密货币白皮书",
    "chapter": "附录B 共识算法",
    "item_number": "PoS-2023",
    "content": "权益证明(PoS)通过验证者抵押代币来维护网络安全,相比工作量证明(PoW)可降低99.95%的能源消耗,但可能引发富者愈富的中心化问题。"
  },
  {
    "doc_name": "南极科考日志",
    "chapter": "极端环境生存",
    "item_number": "EM-0042",
    "content": "在-89.2℃的低温条件下,普通润滑油会完全凝固,必须使用特制的氟化液体系润滑剂。科考站门锁需要每日加热除冰三次以上。"
  },
  {
    "doc_name": "分子美食手册",
    "chapter": "液氮应用",
    "item_number": "LN2-7",
    "content": "使用液氮(-196℃)瞬间冷冻芒果泥可形成直径小于50μm的冰晶,配合超声波震荡可获得类似鱼子酱的爆浆口感。"
  },
  {
    "doc_name": "甲骨文破译笔记",
    "chapter": "商代祭祀",
    "item_number": "甲-2317",
    "content": "''字经红外扫描确认描绘了三人持戈环绕祭坛的场景,可能与《周礼》记载的'大傩'驱疫仪式存在渊源关系。"
  },
  {
    "doc_name": "火星地质报告",
    "chapter": "奥林匹斯山",
    "item_number": "MARS-OL-01",
    "content": "太阳系最高火山奥林匹斯山基底直径达600公里,高度21.9公里,其缓坡结构表明火星曾存在低粘度玄武质熔岩流。"
  },
  {
    "doc_name": "歌剧演唱技巧",
    "chapter": "呼吸控制",
    "item_number": "BELCANTO-3",
    "content": "横膈膜下沉式呼吸可使肺活量提升40%,配合喉头稳定技术,能持续发出110分贝的强共鸣音而不损伤声带。"
  },
  {
    "doc_name": "古生物图谱",
    "chapter": "寒武纪大爆发",
    "item_number": "CB-009",
    "content": "奇虾(Anomalocaris)化石显示其复眼由16000个晶状体组成,视敏度是现代蜻蜓的3倍,是已知最早的高阶捕食者。"
  },
  {
    "doc_name": "人工智能伦理",
    "chapter": "自主武器系统",
    "item_number": "AWS-ETHICS",
    "content": "致命性自主武器(LAWS)的敌我识别错误率超过0.7%即可能违反国际人道法,需建立全球性的算力追踪监管体系。"
  },
  {
    "doc_name": "中世纪炼金术",
    "chapter": "贤者之石",
    "item_number": "PHIL-λ",
    "content": "牛顿手稿显示其相信通过汞-硫二元体系在七阶蒸馏过程中可制备出'红色方解石',即传说中的物质转化媒介。"
  },
  {
    "doc_name": "深海生物图鉴",
    "chapter": "超深渊带",
    "item_number": "Hadal-888",
    "content": "马里亚纳狮子鱼在11000米深度进化出凝胶状身体,骨骼孔隙率高达90%,可承受1.1吨/平方厘米的水压。"
  },
  {
    "doc_name": "纳米材料学报",
    "chapter": "石墨烯应用",
    "item_number": "GR-2D-45",
    "content": "缺陷工程处理的氧化石墨烯薄膜可实现97%的光子透过率与85%的导电率,适合用作柔性触摸屏的透明电极。"
  },
  {
    "doc_name": "敦煌壁画研究",
    "chapter": "飞天形象演变",
    "item_number": "DH-飞-09",
    "content": "北魏时期的飞天多呈现V型强烈动态,至唐代逐渐发展为C型优雅曲线,反映佛教艺术本土化过程中的审美变迁。"
  },
  {
    "doc_name": "疫苗研发日志",
    "chapter": "mRNA技术",
    "item_number": "VAC-mRNA-2020",
    "content": "核苷酸修饰使mRNA的半衰期从2小时延长至24小时以上,LNP包裹效率达到98.3%,有效提升抗原表达量。"
  },
  {
    "doc_name": "暗物质探测报告",
    "chapter": "液氙实验",
    "item_number": "XENON1T-2022",
    "content": "1.3吨超纯液氙探测器观测到电子反冲异常信号,可能与轴子粒子相关,置信度3.5σ,需进一步排除氚污染可能。"
  },
  {
    "doc_name": "茶叶品鉴指南",
    "chapter": "普洱茶发酵",
    "item_number": "TEA-7749",
    "content": "渥堆过程中嗜热菌属占比超过60%,分泌的果胶酶使茶多酚转化率高达80%,形成独特的陈香和红褐汤色。"
  },
  {
    "doc_name": "空间站设计手册",
    "chapter": "辐射防护",
    "item_number": "ISS-Φ12",
    "content": "10厘米厚聚乙烯防护层可将银河宇宙射线剂量降低75%,结合水墙和选择性磁屏蔽可满足长期驻留安全标准。"
  },
  {
    "doc_name": "恐龙灭绝假说",
    "chapter": "希克苏鲁伯撞击",
    "item_number": "K-Pg-1980",
    "content": "铱异常层厚度分析表明,小行星撞击瞬间释放4.2×10²³焦耳能量,引发持续数十年的'撞击冬天',地表温度下降20℃。"
  },
  {
    "doc_name": "脑机接口进展",
    "chapter": "神经解码",
    "item_number": "BCI-007",
    "content": "使用128通道微电极阵列可实时解码初级运动皮层中手指运动的θ波段(4-8Hz)神经振荡信号,准确率达92%。"
  },
  {
    "doc_name": "香料贸易史",
    "chapter": "黑胡椒战争",
    "item_number": "SP-1498",
    "content": "15世纪威尼斯商人通过垄断印度胡椒贸易获取400%利润,直接推动葡萄牙探索绕道非洲的新航路。"
  },
  {
    "doc_name": "超导材料研究",
    "chapter": "高压氢化物",
    "item_number": "SC-275GPa",
    "content": "碳质硫氢化物在275GPa压力下实现15℃超导,但亚稳态维持时间不足1微秒,距实用化仍有量级差距。"
  },
  {
    "doc_name": "甲骨病诊疗",
    "chapter": "马蹄形病变",
    "item_number": "HOOF-EM",
    "content": "马属动物第三趾骨缺血性坏死可通过热成像早期诊断,配合高压氧舱治疗可使痊愈率从35%提升至78%。"
  },
  {
    "doc_name": "虚拟现实心理学",
    "chapter": "恐怖谷效应",
    "item_number": "VR-UN-03",
    "content": "当虚拟人像面部保真度达到92%时,用户焦虑指数骤增300%,但超过97%后接受度又会回升至正常水平。"
  },
  {
    "doc_name": "古罗马建筑",
    "chapter": "混凝土技术",
    "item_number": "ROM-CON",
    "content": "维苏威火山灰与石灰反应生成的钙长石晶体,使罗马混凝土经过2000年海水侵蚀后强度反而提升50%。"
  },
  {
    "doc_name": "蜂群崩溃研究",
    "chapter": "新烟碱类农药",
    "item_number": "CCD-2021",
    "content": "吡虫啉暴露使蜜蜂舞蹈通讯错误率增加40%,蜂群觅食效率下降75%,是导致群体崩溃失调的重要因素。"
  },
  {
    "doc_name": "超音速客机设计",
    "chapter": "音爆控制",
    "item_number": "SST-2025",
    "content": "采用30米级细长机身设计可将地面感知噪音从105PLdB降至75PLdB,满足FAA的日间运营标准。"
  },
  {
    "doc_name": "玛雅天文研究",
    "chapter": "金星历法",
    "item_number": "MAYA-VEN",
    "content": "德累斯顿抄本显示玛雅人计算出金星会合周期为583.92天,与现代测量值583.92天完全一致。"
  },
  {
    "doc_name": "仿生机器人",
    "chapter": "猎豹运动控制",
    "item_number": "BIO-CHEETAH",
    "content": "基于中枢模式发生器的控制算法,配合碳纤维肌腱可实现3Hz的腿部摆动频率,最高时速达38km/h。"
  },
  {
    "doc_name": "葡萄酒酿造",
    "chapter": "橡木桶陈化",
    "item_number": "WINE-OAK",
    "content": "中度烘烤的法国橡木每升酒液贡献2.1mg香草醛,同时促进单宁聚合,使酒体更加柔顺饱满。"
  }
]

更多知识请移步个人博客:33sora.com

相关推荐
mmmu6 小时前
网页快速接入 Deepseek,是如此简单!分分钟带你搞定!
前端·deepseek
QBorfy6 小时前
07篇 AI从零开始 - LangChain学习与实战(4) LangServer部署
前端·人工智能·deepseek
量子位7 小时前
DeepSeek 玩家能提前拿苹果新品!只要 15 万元,在家跑满血版 R1
人工智能·deepseek
Java水解7 小时前
DeepSeek架构革命:动态异构计算
rxjava·deepseek
Bigger8 小时前
Tauri(十三)—— 给 Coco AI 加上外接大脑 RAG 🧠
aigc·openai·deepseek
哪吒编程13 小时前
Nature最新报道:分析四大主流AI工具、性能测评、推荐使用场景
chatgpt·claude·deepseek
meisongqing13 小时前
DeepSeek与剪映短视频创作指南
人工智能·剪映·deepseek
程序员小台14 小时前
DeepSeek R1 14B + LM Studio 本地大模型实测
deepseek
程序员小台14 小时前
【大模型统一集成项目】从零开始打造你的专属大模型集成平台
deepseek