调用DeepSeek模型集成项目(8.24)

一、Java代码测试deepseek访问

新建travel-modules-ai模块(Java项目)

(1)添加依赖
 <dependencies>
        <dependency>
            <groupId>com.mashape.unirest</groupId>
            <artifactId>unirest-java</artifactId>
            <version>1.4.9</version>
        </dependency>
    </dependencies>
(2)新建TestDeepseek测试类
public class TestDeepseek {

    public static void main(String[] args) throws UnirestException {
        Unirest.setTimeouts(0, 0);
        HttpResponse<String> response = Unirest.post("https://api.deepseek.com/chat/completions")
                .header("Content-Type", "application/json")
                .header("Accept", "application/json")
                .header("Authorization", "Bearer 你的Apikey")
                .body("{\n  \"messages\": [\n    {\n      \"content\": \"You are a helpful assistant\",\n      \"role\": \"system\"\n    },\n    {\n      \"content\": \"今天天气如何?\",\n      \"role\": \"user\"\n    }\n  ],\n  \"model\": \"deepseek-v4-pro\",\n  \"thinking\": {\n    \"type\": \"enabled\"\n  },\n  \"reasoning_effort\": \"low\",\n  \"max_tokens\": 4096,\n  \"response_format\": {\n    \"type\": \"text\"\n  },\n  \"stop\": null,\n  \"stream\": false,\n  \"stream_options\": null,\n  \"temperature\": 1,\n  \"top_p\": 1,\n  \"tools\": null,\n  \"tool_choice\": \"none\",\n  \"logprobs\": false,\n  \"top_logprobs\": null\n}")
                .asString();

        System.err.println(response.getBody());
    }
}

二、推荐页功能实现

1、推荐页 RAG 流程简述

  • 接收输入:用户在推荐页输入自然语言问题。
  • ES 检索:将问题作为查询条件,从 Elasticsearch 中召回 Top-K 条相关数据。
  • 构建上下文:将 ES 返回的数据整理拼接为字符串,作为私有知识库上下文,与用户问题一起组装成 Prompt。
  • 调用 DeepSeek 生成:将 Prompt 发送给 DeepSeek,模型基于私库数据整合生成推荐结果返回给用户。

2、AI程序搭建

(1)前端页面

​ 将static文件夹拷贝到resources下

(2)添加依赖
<dependencies>
    <!-- HTTP 客户端库,用于发送 HTTP 请求(如调用 DeepSeek API) -->
    <dependency>
        <groupId>com.mashape.unirest</groupId>
        <artifactId>unirest-java</artifactId>
        <version>1.4.9</version>
    </dependency>

    <!-- Spring Boot 测试依赖,提供 JUnit、Mockito 等测试工具 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>

    <!-- Spring Boot Web 依赖,提供内嵌 Tomcat 和 Spring MVC 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Jackson JSON 序列化/反序列化工具,用于处理 JSON 数据 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- Jakarta JSON API,提供 JSON 处理的标准接口 -->
    <dependency>
        <groupId>jakarta.json</groupId>
        <artifactId>jakarta.json-api</artifactId>
        <version>2.0.1</version>
    </dependency>

    <!-- Jakarta JSON 实现(Glassfish),为上面的 API 提供具体实现 -->
    <dependency>
        <groupId>org.glassfish</groupId>
        <artifactId>jakarta.json</artifactId>
        <version>2.0.1</version>
    </dependency>

    <!-- Elasticsearch 低级 REST 客户端,用于与 ES 进行底层通信 -->
    <dependency>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
        <version>8.18.8</version>
    </dependency>

    <!-- 项目公共模块(travel-common-core),提供通用工具类和公共配置 -->
    <dependency>
        <groupId>cn.wolfcode.wolf2w</groupId>
        <artifactId>travel-common-core</artifactId>
    </dependency>

    <!-- 搜索服务 API 模块,提供 ES 检索相关的接口和实体类 -->
    <dependency>
        <groupId>cn.wolfcode.wolf2w</groupId>
        <artifactId>travel-api-search</artifactId>
        <version>3.6.2</version>
    </dependency>

    <!-- Elasticsearch 高级 Java 客户端,提供类型安全的 ES 操作 API -->
    <dependency>
        <groupId>co.elastic.clients</groupId>
        <artifactId>elasticsearch-java</artifactId>
        <version>8.18.8</version>
    </dependency>
</dependencies>
(3)添加application.yml配置文件
server:
  port: 9096
spring:
  application:
    name: travel-ai

elasticsearch:
  url: localhost
  port: 9200
deepseek:
  baseUrl: https://api.deepseek.com
  model: deepseek-v4-pro
  temperature: 1
  thinking: enabled
  top_p: 1
  apiKey: 你的Apikey

(4)编写config.EsConfig配置类
@Configuration  // 标记为 Spring 配置类,容器启动时会自动加载
public class EsConfig {

    @Value("${elasticsearch.url}")  // 从配置文件中读取 ES 的地址(如 localhost)
    private String url;

    @Value("${elasticsearch.port}")  // 从配置文件中读取 ES 的端口号(如 9200)
    private int port;

    @Bean  // 将该方法的返回值注册为 Spring 容器中的 Bean,供其他地方注入使用
    public ElasticsearchClient elasticsearchClient() {

        // 1. 创建 ES 低级 REST 客户端,配置连接地址、端口和协议(http)
        RestClient restClient = RestClient.builder(
                new HttpHost(url, port, "http")
        ).build();

        // 2. 将低级客户端包装为高级传输层,并指定 Jackson 作为 JSON 序列化/反序列化工具
        RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());

        // 3. 基于传输层创建 ES 高级 Java 客户端并返回
        return new ElasticsearchClient(transport);
    }

}
(5)编写AiApplication启动类
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class AiApplication {
    public static void main(String[] args) {
        SpringApplication.run(AiApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

3、私库数据查询

(1)新建dto.ChatRequest类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatRequest {

  /**
     * 用户输入的原始问题文本。
     * 用于在 Elasticsearch 中进行语义检索,并作为 Prompt 的一部分发送给大模型生成回答。
     */
    private String question;

    /**
     * 检索召回的文档数量(Top K)。
     * 控制从私库中查询并返回给大模型作为参考上下文的条数。
     * 数值越大参考信息越丰富,但可能引入噪声且消耗更多 Token。
     */
    private Integer topK;
}

(2)新建dto.ChatResponse类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatResponse {

     /**
     * 大模型生成的最终回答文本。
     * 后端结合检索到的私库内容(Context)和用户问题,通过大模型总结生成的自然语言回复。
     */
    private String answer;

    /**
     * 检索到的参考策略/攻略列表(数据来源)。
     * 对应 Elasticsearch 中查出的原始文档对象列表,用于前端展示“参考来源”或实现详情跳转。
     */
    private List<StrategyEs> strategies;
}

(3)新建controller.AIController类
@RestController
public class AIController {

    // 注入 Elasticsearch 客户端,用于连接和操作 ES 数据库
    @Autowired
    private ElasticsearchClient client;

    /**
     * 聊天接口:处理用户的提问请求
     * @param request 包含用户问题(question)和检索数量(topK)的请求对象
     * @return ChatResponse 包含AI回答(answer)和参考来源(strategies)的响应对象
     */
    @RequestMapping("/chat")
    public Object getAnswer(@RequestBody ChatRequest request) throws IOException {

        // --- 第一步:从私库(ES)中检索相关数据 ---

        // 执行搜索请求:
        // 1. index("strategy"): 指定查询 "strategy" 索引(攻略库)
        // 2. from(0).size(request.getTopK()): 从第0条开始,取前 TopK 条数据
        // 3. query(...): 构建查询条件
        SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("strategy")
                .from(0).size(request.getTopK())
                .query(q -> q.multiMatch(m -> m.query(request.getQuestion())
                        // multiMatch: 多字段匹配查询
                        .fields("title", "subtitle", "summary") // 在标题、副标题、摘要中搜索
                        .analyzer("ik_max_word"))), // 使用 ik_max_word 分词器进行细粒度中文分词
                StrategyEs.class);

        // --- 第二步:解析 ES 返回结果 ---

        // 获取命中的元数据信息
        HitsMetadata<StrategyEs> hits = resp.hits();
        // 获取命中总数(当前代码未使用,可用于日志或分页)
        TotalHits total = hits.total();
        // 获取具体的命中列表
        List<Hit<StrategyEs>> hits2 = hits.hits();

        // 初始化私库数据列表,用于存储提取出的攻略对象
        List<StrategyEs> strategies = new ArrayList<>();

        // 遍历命中结果,提取 _source 原始数据
        for (Hit<StrategyEs> strategyEsHit : hits2) {
            StrategyEs es = strategyEsHit.source(); // 获取文档源对象
            strategies.add(es); // 添加到列表中
        }

        // --- 第三步:调用大模型生成回答 (待实现) ---

        // todo 发送请求到deepseek处理
        // 提示:此处应将 request.getQuestion() 和 strategies 拼接成 Prompt,
        // 调用 DeepSeek API 获取生成的 answer 字符串。

        // 封装响应对象
        // 目前 answer 为占位符,strategies 为刚才检索到的参考数据
        ChatResponse chatResponse = new ChatResponse("deepseek无结果", strategies);
        return chatResponse;
    }
}

4、实现deepseek的rag访问

(1)新建dto.OpenAIMessage类
/**
 * 大模型对话消息对象,对应 OpenAI 兼容接口中 messages 数组的单个元素。
 * 用于封装发送给大模型(如 DeepSeek)的每一条对话消息。
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OpenAIMessage {
    /**
     * 消息角色,用于标识这条消息的发送方。
     * 常见取值:
     * - "system":系统消息,用于设定 AI 的角色定位和行为规则
     * - "user":用户消息,即用户输入的原始问题
     * - "assistant":助手消息,即 AI 生成的回答
     */
    private String role;

    /**
     * 消息内容,即该角色对应的具体文本。
     * 例如:角色为 "user" 时,content 就是用户的问题;
     * 角色为 "system" 时,content 就是私库数据或角色设定指令。
     */
    private String content;
}

(2)新建dto.OpenAIChatRequest类
/**
 * 大模型对话请求体对象,封装发送给 DeepSeek(OpenAI 兼容接口)的全部参数。
 * 对应 /chat/completions 接口的 JSON 请求体结构。
 */
@Data                
@AllArgsConstructor  
@NoArgsConstructor   
public class OpenAIChatRequest {

    /**
     * 模型名称,指定调用哪个大模型。
     * 例如:"deepseek-chat"、"deepseek-reasoner" 等。
     */
    private String model;

    /**
     * 对话消息列表,按顺序包含 system、user、assistant 等角色的消息。
     * 大模型会根据消息列表的上下文来生成回答。
     * 在 RAG 场景中,通常包含:私库数据(system) + 角色设定(system) + 用户问题(user)。
     */
    private List<OpenAIMessage> messages;

    /**
     * 温度参数,控制模型输出的随机性,取值范围通常为 0~2。
     * - 值越低(如 0.1):输出越确定、越保守,适合事实性问答
     * - 值越高(如 0.9):输出越随机、越有创造性,适合创意写作
     */
    private Double temperature;

    /**
     * 思考模式配置,DeepSeek R1 系列模型特有的参数。
     * 通过 Map 传入,例如 {"type": "enabled"} 开启深度思考模式,
     * 模型会先进行内部推理再输出最终回答,提升复杂问题的回答质量。
     */
    private Map<String, Object> thinking;

    /**
     * Top-p(核采样)参数,控制模型从概率最高的前 p 比例的词中采样。
     * 取值范围 0~1,例如 0.9 表示只从累计概率达到 90% 的候选词中选择。
     * 与 temperature 配合使用,进一步控制输出的多样性和质量。
     */
    private Double top_p;
}
(3)新建dto.OpenAIChatResponse类
/**
 * 大模型对话响应体对象,封装 DeepSeek(OpenAI 兼容接口)返回的 JSON 响应。
 * 对应 /chat/completions 接口的响应结构。
 */
@Data                
@AllArgsConstructor  
@NoArgsConstructor   
public class OpenAIChatResponse {

    /**
     * 候选回答列表。
     * 大模型可能返回多个候选结果(由请求参数 n 控制),通常只取第一个(index=0)。
     * 每个 Choice 包含一条完整的助手回复消息。
     */
    private List<Choice> choices;

    /**
     * 单个候选回答,对应 choices 数组中的一个元素。
     * 内部包含大模型生成的具体消息内容。
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Choice {

        /**
         * 大模型生成的回复消息。
         * 其中 role 通常为 "assistant",content 就是 AI 生成的最终回答文本。
         * 在 Controller 中通过 choice.getMessage().getContent() 提取回答内容。
         */
        private OpenAIMessage message;
    }
}
(4)实现发送请求到deepseek处理
/**
 * AI 聊天控制器,实现 RAG(检索增强生成)流程。
 * 核心逻辑:先从 ES 私库检索相关攻略 → 组装 Prompt → 调用 DeepSeek 生成回答 → 返回结果。
 */
@RestController
public class AIController {

    // ========== 从配置文件中读取 DeepSeek 相关参数 ==========
    @Value("${deepseek.baseUrl}")
    private String baseUrl;           // DeepSeek API 基础地址
    @Value("${deepseek.model}")
    private String model;             // 模型名称,如 deepseek-chat
    @Value("${deepseek.temperature}")
    private Double temperature;       // 温度参数,控制输出随机性
    @Value("${deepseek.thinking}")
    private String thinking;          // 思考模式开关(enabled/disabled)
    @Value("${deepseek.top_p}")
    private Double top_p;             // 核采样参数,控制候选词范围
    @Value("${deepseek.apiKey}")
    private String apiKey;            // API 密钥,用于鉴权

    // HTTP 客户端,用于发送请求到 DeepSeek 接口
    @Autowired
    private RestTemplate restTemplate;

    // Elasticsearch 客户端,用于从私库中检索攻略数据
    @Autowired
    private ElasticsearchClient client;

    /**
     * 聊天主接口:接收用户提问,执行 RAG 流程并返回 AI 回答。
     * @param request 包含用户问题(question)和检索数量(topK)的请求对象
     * @return ChatResponse 包含 AI 回答(answer)和参考攻略列表(strategies)
     */
    @RequestMapping("/chat")
    public Object getAnswer(@RequestBody ChatRequest request) throws IOException {

        // --- 第一步:从 ES 私库中检索与用户问题相关的攻略数据 ---

        // 使用 multiMatch 多字段匹配查询,在 title、subtitle、summary 中搜索用户问题
        // ik_max_word 分词器:对中文进行最细粒度切分,提高召回率
        // from(0).size(topK):从第 0 条开始,取前 topK 条最相关的结果
        SearchResponse<StrategyEs> resp = client.search(sh -> sh.index("strategy")
                .from(0).size(request.getTopK())
                .query(q -> q.multiMatch(m -> m.query(request.getQuestion())
                        .fields("title", "subtitle", "summary")
                        .analyzer("ik_max_word"))), StrategyEs.class);

        // 解析 ES 返回的搜索结果
        HitsMetadata<StrategyEs> hits = resp.hits();  // 命中元数据
        TotalHits total = hits.total();               // 总命中数(可用于日志记录)
        List<Hit<StrategyEs>> hits2 = hits.hits();    // 具体的命中列表

        // 提取私库数据:遍历命中结果,取出每个文档的 _source 原始数据
        List<StrategyEs> strategies = new ArrayList<>();
        for (Hit<StrategyEs> strategyEsHit : hits2) {
            StrategyEs es = strategyEsHit.source();   // 获取文档源对象
            strategies.add(es);                       // 添加到攻略列表中
        }

        // --- 第二步:将用户问题和私库数据发送给 DeepSeek 生成回答 ---
        String answer = askAI(request.getQuestion(), strategies);

        // --- 第三步:封装响应并返回 ---
        // answer:AI 生成的回答文本
        // strategies:参考来源列表,供前端展示"引用了哪些攻略"
        ChatResponse chatResponse = new ChatResponse(answer, strategies);
        return chatResponse;
    }

    /**
     * 调用 DeepSeek 大模型生成回答。
     * 负责组装 Prompt(私库数据 + 角色设定 + 用户问题)并发送 HTTP 请求。
     * @param question   用户的原始问题
     * @param strategies 从 ES 检索到的攻略列表,作为 AI 的参考上下文
     * @return AI 生成的回答文本
     */
    private String askAI(String question, List<StrategyEs> strategies) {

        // --- 1. 将私库攻略列表拼接为 JSON 字符串,作为 RAG 上下文 ---
        StringJoiner joiner = new StringJoiner("\n\n");  // 用双换行分隔每条攻略
        for (StrategyEs strategy : strategies) {
            String strategyJson = JSON.toJSONString(strategy);  // 将攻略对象序列化为 JSON
            joiner.add(strategyJson);
        }
        String strategiesStr = joiner.toString();  // 拼接后的完整私库数据字符串

        // --- 2. 组装对话消息列表(Prompt 的核心) ---

        // 第一条 system 消息:注入私库数据,让 AI 基于这些参考资料回答
        OpenAIMessage ragMessage = new OpenAIMessage("system", strategiesStr);

        // 第二条 system 消息:设定 AI 的角色和行为规则
        // 要求 AI 扮演资深导游,结合私库数据和互联网信息给出建议,回答控制在 200 字以内
        OpenAIMessage sysMessage = new OpenAIMessage("system",
                "你是一名资深导游,请根据用户问题结合私库数据,并根据互联网信息给出旅行建议。规划路线避坑指南及旅行推荐,回答小于200字");

        // 第三条 user 消息:用户的原始问题
        OpenAIMessage userMessage = new OpenAIMessage("user", question);

        // 按顺序添加到消息列表中(顺序很重要,大模型会按顺序理解上下文)
        List<OpenAIMessage> messages = new ArrayList<>();
        messages.add(ragMessage);    // 私库数据
        messages.add(sysMessage);    // 角色设定
        messages.add(userMessage);   // 用户提问

        // --- 3. 构建请求体,设置模型参数 ---
        OpenAIChatRequest requestBody = new OpenAIChatRequest();
        requestBody.setMessages(messages);           // 设置对话消息列表
        requestBody.setModel(model);                 // 设置模型名称
        requestBody.setTemperature(temperature);     // 设置温度参数
        // 设置思考模式(DeepSeek R1 特有)
        Map<String, Object> map = new HashMap<>();
        map.put("type", thinking);                   // "enabled" 开启 / "disabled" 关闭
        requestBody.setThinking(map);
        requestBody.setTop_p(top_p);                 // 设置核采样参数

        // --- 4. 设置 HTTP 请求头 ---
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);  // 请求体格式为 JSON
        List<MediaType> mediaTypeList = new ArrayList<>();
        mediaTypeList.add(MediaType.APPLICATION_JSON);
        headers.setAccept(mediaTypeList);                    // 期望响应格式为 JSON
        headers.setBearerAuth(apiKey);                       // 设置 API Key 鉴权

        // --- 5. 发送 POST 请求到 DeepSeek 接口 ---
        HttpEntity<OpenAIChatRequest> httpEntity = new HttpEntity<>(requestBody, headers);
        String url = baseUrl + "/chat/completions";  // 拼接完整的 API 地址

        // 发送请求并接收响应
        OpenAIChatResponse response = restTemplate.postForObject(url, httpEntity, OpenAIChatResponse.class);

        // --- 6. 解析响应,提取 AI 回答内容(防御性判空) ---

        // 第一层判空:响应体、choices 列表是否为空
        if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) {
            return "AI没有返回合适的结果";
        }

        // 取第一个候选回答(通常只有一个)
        OpenAIChatResponse.Choice choice = response.getChoices().get(0);

        // 第二层判空:choice、message、content 是否为空
        if (choice == null || choice.getMessage() == null || choice.getMessage().getContent() == null) {
            return "AI没有返回合适的结果";
        }

        // 返回 AI 生成的最终回答文本
        return choice.getMessage().getContent();
    }
}
posted on 2026-08-24 21:47  冬冬咚  阅读(22)  评论(0)    收藏  举报