diff --git a/pom.xml b/pom.xml
index dbe1d3c..3a6b3ad 100755
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.5.0
+ 3.4.5
com.qingqiu
@@ -28,6 +28,9 @@
17
+ 1.0.0
+ 1.0.0.2
+ 3.4.5
@@ -53,26 +56,56 @@
spring-boot-starter-webflux
-
-
-
-
-
-
-
-
-
-
-
+
+
- com.alibaba
- dashscope-sdk-java
- 2.21.5
+ org.springframework.ai
+ spring-ai-starter-model-openai
+
+
+ com.alibaba.cloud.ai
+ spring-ai-alibaba-starter-dashscope
+
+
+
+ com.alibaba.cloud.ai
+ spring-ai-alibaba-starter-memory
+
+
+
+ com.alibaba.cloud.ai
+ spring-ai-alibaba-starter-memory-jdbc
+
+
+
+
+ com.alibaba.cloud.ai
+ spring-ai-alibaba-starter-memory-redis
+
+
+ redis.clients
+ jedis
+ 5.2.0
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+
+
+
+
+
cn.hutool
@@ -129,16 +162,28 @@
-
-
-
-
-
-
-
-
-
+
+ com.alibaba.cloud.ai
+ spring-ai-alibaba-bom
+ ${spring-ai-alibaba.version}
+ pom
+ import
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+ org.springframework.ai
+ spring-ai-bom
+ ${spring-ai.version}
+ pom
+ import
+
com.baomidou
mybatis-plus-bom
@@ -147,6 +192,7 @@
import
+
@@ -167,33 +213,6 @@
-
-
- spring-snapshots
- Spring Snapshots
- https://repo.spring.io/snapshot
-
- false
-
-
-
- spring-milestones
- Spring Milestones
- https://repo.spring.io/milestone
-
- false
-
-
-
-
-
- spring-snapshots
- Spring Snapshots
- https://repo.spring.io/snapshot
-
- false
-
-
-
+
diff --git a/src/main/java/com/qingqiu/interview/ai/entity/Message.java b/src/main/java/com/qingqiu/interview/ai/entity/Message.java
deleted file mode 100755
index 0370f3c..0000000
--- a/src/main/java/com/qingqiu/interview/ai/entity/Message.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.qingqiu.interview.ai.entity;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-import lombok.experimental.Accessors;
-
-import java.io.Serial;
-import java.io.Serializable;
-
-@Data
-@Accessors(chain = true)
-@AllArgsConstructor
-@NoArgsConstructor
-public class Message implements Serializable {
- @Serial
- private static final long serialVersionUID = 1L;
-
- private String role;
-
- private String content;
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
deleted file mode 100755
index a7c737a..0000000
--- a/src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.qingqiu.interview.ai.factory;
-
-import com.qingqiu.interview.common.enums.LLMProvider;
-import com.qingqiu.interview.ai.service.AIClientService;
-
-public interface AIClientFactory {
- AIClientService createAIClient();
-
- // 支持的提供商
- LLMProvider getSupportedProvider();
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java b/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
deleted file mode 100755
index fb79371..0000000
--- a/src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.qingqiu.interview.ai.factory;
-
-import com.qingqiu.interview.common.enums.LLMProvider;
-import com.qingqiu.interview.ai.service.AIClientService;
-import org.springframework.stereotype.Service;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-
-@Service
-public class AIClientManager {
-
- private final Map factories;
-
- public AIClientManager(List strategies) {
- this.factories = strategies.stream()
- .collect(Collectors.toMap(
- AIClientFactory::getSupportedProvider,
- Function.identity()
- ));
- }
-
- public AIClientService getClient(LLMProvider provider) {
-// String factoryName = aiType + "ClientFactory";
- AIClientFactory factory = factories.get(provider);
- if (factory == null) {
- throw new IllegalArgumentException("不支持的AI type: " + provider);
- }
- return factory.createAIClient();
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
deleted file mode 100755
index c60d5e4..0000000
--- a/src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.qingqiu.interview.ai.factory;
-
-import com.qingqiu.interview.common.enums.LLMProvider;
-import com.qingqiu.interview.ai.service.AIClientService;
-import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
-import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
-import org.springframework.stereotype.Service;
-
-@Service
-public class DeepSeekClientFactory implements AIClientFactory{
- @Override
- public AIClientService createAIClient() {
- return SpringApplicationContextUtil.getBean(DeepSeekClientServiceImpl.class);
- }
-
- @Override
- public LLMProvider getSupportedProvider() {
- return LLMProvider.DEEPSEEK;
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java b/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
deleted file mode 100755
index aeca885..0000000
--- a/src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package com.qingqiu.interview.ai.factory;
-
-import com.qingqiu.interview.common.enums.LLMProvider;
-import com.qingqiu.interview.ai.service.AIClientService;
-import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
-import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
-import org.springframework.stereotype.Service;
-
-@Service
-public class QwenClientFactory implements AIClientFactory{
- @Override
- public AIClientService createAIClient() {
- return SpringApplicationContextUtil.getBean(QwenClientServiceImpl.class);
- }
-
- @Override
- public LLMProvider getSupportedProvider() {
- return LLMProvider.QWEN;
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/service/AIClientService.java b/src/main/java/com/qingqiu/interview/ai/service/AIClientService.java
deleted file mode 100755
index da699a0..0000000
--- a/src/main/java/com/qingqiu/interview/ai/service/AIClientService.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.qingqiu.interview.ai.service;
-
-import com.alibaba.dashscope.common.Message;
-
-import java.util.List;
-
-public abstract class AIClientService {
- public abstract String chatCompletion(String prompt);
-
- public String chatCompletion(List messages) {
- return null;
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/service/impl/DeepSeekClientServiceImpl.java b/src/main/java/com/qingqiu/interview/ai/service/impl/DeepSeekClientServiceImpl.java
deleted file mode 100755
index 2e19ce8..0000000
--- a/src/main/java/com/qingqiu/interview/ai/service/impl/DeepSeekClientServiceImpl.java
+++ /dev/null
@@ -1,65 +0,0 @@
-package com.qingqiu.interview.ai.service.impl;
-
-import com.alibaba.dashscope.common.Message;
-import com.alibaba.fastjson2.JSONArray;
-import com.alibaba.fastjson2.JSONObject;
-import com.qingqiu.interview.ai.service.AIClientService;
-import com.qingqiu.interview.common.service.HttpService;
-import lombok.RequiredArgsConstructor;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import static com.qingqiu.interview.common.utils.AIUtils.createUserMessage;
-
-/**
- * deepseek 接入
- */
-@Service
-@RequiredArgsConstructor
-public class DeepSeekClientServiceImpl extends AIClientService {
-
- private final HttpService httpService;
-
- @Value("${deepseek.api-url}")
- private String apiUrl;
-
- @Value("${deepseek.api-key}")
- private String apiKey;
-
-
- @Override
- public String chatCompletion(String prompt) {
- return chatCompletion(Collections.singletonList(createUserMessage(prompt)));
- }
-
- @Override
- public String chatCompletion(List messages) {
- JSONObject jsonObject = new JSONObject();
- jsonObject.put("type", "json_object");
- Map requestBody = Map.of(
- "model", "deepseek-chat",
- "messages", messages,
- "max_tokens", 8192,
- "response_format", Map.of("type", "json_object")
- );
- String res = httpService.postWithAuth(
- apiUrl,
- requestBody,
- String.class,
- "Bearer " + apiKey
- ).block();
- if (StringUtils.isNotBlank(res)) {
- JSONObject jsonRes = JSONObject.parse(res);
- JSONArray choices = jsonRes.getJSONArray("choices");
- JSONObject resContent = choices.getJSONObject(0);
- JSONObject message = resContent.getJSONObject("message");
- return message.getString("content");
- }
- return null;
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java b/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java
deleted file mode 100755
index 6a15725..0000000
--- a/src/main/java/com/qingqiu/interview/ai/service/impl/QwenClientServiceImpl.java
+++ /dev/null
@@ -1,62 +0,0 @@
-package com.qingqiu.interview.ai.service.impl;
-
-import com.alibaba.dashscope.aigc.generation.Generation;
-import com.alibaba.dashscope.aigc.generation.GenerationParam;
-import com.alibaba.dashscope.aigc.generation.GenerationResult;
-import com.alibaba.dashscope.common.Message;
-import com.alibaba.dashscope.common.ResponseFormat;
-import com.alibaba.dashscope.exception.ApiException;
-import com.alibaba.dashscope.exception.InputRequiredException;
-import com.alibaba.dashscope.exception.NoApiKeyException;
-import com.qingqiu.interview.ai.service.AIClientService;
-import com.qingqiu.interview.common.res.ResultCode;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-
-import java.util.Collections;
-import java.util.List;
-
-import static com.qingqiu.interview.common.constants.QwenModelConstant.QWEN_PLUS_LATEST;
-import static com.qingqiu.interview.common.utils.AIUtils.createUserMessage;
-
-@Slf4j
-@Service
-@RequiredArgsConstructor
-public class QwenClientServiceImpl extends AIClientService {
-
- @Value("${dashscope.api-key}")
- private String apiKey;
-
- private final Generation generation;
-
- @Override
- public String chatCompletion(String prompt) {
- return chatCompletion(Collections.singletonList(createUserMessage(prompt)));
- }
-
- @Override
- public String chatCompletion(List messages) {
-
- GenerationParam param = GenerationParam.builder()
- .model(QWEN_PLUS_LATEST) // 可根据需要更换模型
- .messages(messages)
- .resultFormat(GenerationParam.ResultFormat.MESSAGE)
- .responseFormat(ResponseFormat.builder().type(ResponseFormat.JSON_OBJECT).build())
- .apiKey(apiKey)
- .build();
-
- GenerationResult result = null;
- try {
- result = generation.call(param);
- return result.getOutput().getChoices().get(0).getMessage().getContent();
- } catch (NoApiKeyException e) {
- log.error("没有api key,请先确认配置!");
- throw new com.qingqiu.interview.common.ex.ApiException(ResultCode.INTERNAL);
- } catch (ApiException | InputRequiredException e) {
- log.error("调用AI服务失败", e);
- throw new com.qingqiu.interview.common.ex.ApiException(ResultCode.INTERNAL);
- }
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/common/constants/ChatConstant.java b/src/main/java/com/qingqiu/interview/common/constants/ChatConstant.java
new file mode 100644
index 0000000..97dd541
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/common/constants/ChatConstant.java
@@ -0,0 +1,44 @@
+package com.qingqiu.interview.common.constants;
+
+/**
+ * 聊天相关常量类
+ * 定义了聊天功能中使用的各种常量值
+ */
+public class ChatConstant {
+
+ /**
+ * MySQL聊天记录存储的Bean名称
+ */
+ public static final String MYSQL_CHAT_MEMORY_BEAN_NAME = "mysql-chat-memory";
+
+ /**
+ * Redis聊天记录存储的Bean名称
+ */
+ public static final String REDIS_CHAT_MEMORY_BEAN_NAME = "redis-chat-memory";
+
+ /**
+ * 聊天记录最大保存消息数
+ */
+ public static final Integer MAX_MESSAGES = 100;
+
+ /**
+ * DashScope聊天模型的Bean名称
+ */
+ public static final String DASH_SCOPE_CHAT_MODEL_BEAN_NAME = "dash-scope-chat-model";
+ public static final String OPEN_AI_CHAT_MODEL_BEAN_NAME = "open-ai-chat-model";
+
+ /**
+ * DashScope聊天客户端的Bean名称
+ */
+ public static final String DASH_SCOPE_CHAT_CLIENT_BEAN_NAME = "dash-scope-chat-client";
+
+ /**
+ * OpenAI聊天客户端的Bean名称
+ */
+ public static final String OPEN_AI_CHAT_CLIENT_BEAN_NAME = "open-ai-chat-client";
+
+ /**
+ * 最大补全token数量
+ */
+ public static final Integer MAX_COMPLETION_TOKENS = 8192;
+}
\ No newline at end of file
diff --git a/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java b/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
deleted file mode 100755
index d59b1f7..0000000
--- a/src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
+++ /dev/null
@@ -1,41 +0,0 @@
-package com.qingqiu.interview.common.utils;
-
-import com.alibaba.dashscope.common.Message;
-import com.alibaba.dashscope.common.Role;
-import com.alibaba.dashscope.tokenizers.Tokenizer;
-import com.alibaba.dashscope.tokenizers.TokenizerFactory;
-
-import java.util.List;
-
-public class AIUtils {
-
- public static Message createMessage(String role, String content) {
- return Message.builder()
- .role(role)
- .content(content)
- .build();
- }
-
- public static Message createUserMessage(String prompt) {
- return createMessage(Role.USER.getValue(), prompt);
- }
-
- public static Message createAIMessage(String prompt) {
- return createMessage(Role.ASSISTANT.getValue(), prompt);
- }
-
- public static Message createSystemMessage(String prompt) {
- return createMessage(Role.SYSTEM.getValue(), prompt);
- }
-
- /**
- * 获取prompt的token数
- * @param prompt 输入
- * @return tokens
- */
- public static Integer getPromptTokens(String prompt) {
- Tokenizer tokenizer = TokenizerFactory.qwen();
- List integers = tokenizer.encodeOrdinary(prompt);
- return integers.size();
- }
-}
diff --git a/src/main/java/com/qingqiu/interview/common/utils/PromptTemplateUtils.java b/src/main/java/com/qingqiu/interview/common/utils/PromptTemplateUtils.java
new file mode 100644
index 0000000..d60c7ee
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/common/utils/PromptTemplateUtils.java
@@ -0,0 +1,430 @@
+package com.qingqiu.interview.common.utils;
+
+import org.springframework.ai.chat.messages.SystemMessage;
+import org.springframework.ai.chat.messages.UserMessage;
+import org.springframework.ai.chat.prompt.Prompt;
+import org.springframework.ai.chat.prompt.PromptTemplate;
+import org.springframework.ai.chat.prompt.SystemPromptTemplate;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 提示模板工具类
+ */
+public class PromptTemplateUtils {
+
+ /**
+ * 获取提取技能的提示
+ *
+ * @param resumeContent 简历内容
+ * @return 提示
+ */
+ public static Prompt getExtractSkillsPrompt(String resumeContent) {
+ SystemMessage systemMessage = new SystemMessage("""
+ 你是一位资深的技术面试官,以严格和深入著称。
+ 你需要从提供的简历内容中,提取出所有与职位相关的技能。
+ 请按照以下JSON格式返回:
+ {"skills": ["技能1", "技能2", "..."]}
+ """);
+ UserMessage userMessage = new UserMessage(resumeContent);
+ return new Prompt(List.of(userMessage, systemMessage));
+ }
+
+ /**
+ * 获取AI面试官的提示
+ *
+ * @param params 参数
+ * @return 提示
+ */
+ public static Prompt getAiInterviewerPrompt(Map params) {
+ SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""
+ 你是一位经验丰富的软件开发技术面试官,具备以下特质:
+
+ ## 专业能力
+ - 拥有10年以上软件开发和团队管理经验
+ - 熟悉各种主流技术栈和架构模式
+ - 擅长通过技术面试评估候选人的真实能力
+ - 能够设计多层次、递进式的面试问题
+
+ ## 面试原则
+ 1. **精准匹配**: 问题必须紧密结合岗位要求和候选人背景
+ 2. **层次递进**: 从基础概念到深度应用,从理论到实践
+ 3. **场景化考察**: 基于真实项目场景设计问题
+ 4. **全面评估**: 涵盖技术深度、问题解决能力、系统思维
+ ## 本岗位招聘要求
+ [{jobRequirements}]
+
+ ## 输出要求
+ - 严格按照指定的JSON格式输出
+ - 不得包含任何JSON格式之外的内容
+ - 不得添加代码块标记(```json)或其他解释性文字
+ - 确保生成的问题数量与要求完全一致
+ """);
+ PromptTemplate userPromptTemplate = new PromptTemplate("""
+ 请根据岗位招聘要求设计 {count} 道技术面试题。
+
+ ## 候选人信息
+
+ ### 技术栈
+ {skills}
+
+ ### 简历内容
+ {resume}
+
+ ## 面试题设计要求
+
+ ### 覆盖维度
+ 1. **基础理论** (20%): 核心概念、原理机制
+ 2. **项目实践** (40%): 具体项目中的技术难点和解决方案
+ 3. **系统设计** (20%): 架构思维、技术选型、性能优化
+ 4. **问题解决** (20%): 调试能力、故障排查、代码优化
+
+ ### 难度分布
+ - 基础题 (30%): 验证核心技能掌握情况
+ - 进阶题 (50%): 考察深度理解和实际应用
+ - 高阶题 (20%): 评估架构能力和创新思维
+
+ ### 问题类型
+ - **概念阐述**: "请解释..."、"什么是..."
+ - **场景分析**: "在你的XX项目中..."、"如果遇到XX问题..."
+ - **方案设计**: "如何设计..."、"请给出..."
+ - **比较选择**: "XX和YY的区别..."、"为什么选择..."
+
+ ## 输出格式
+ 必须严格按照以下JSON格式输出,不得有任何偏差:
+
+ {jsonRes}
+
+ 请立即开始生成面试题:
+ """);
+ String s = """
+ {
+ "questions": [
+ {
+ "id": "ai-gen-1",
+ "content": "问题内容..."
+ }
+ ]
+ }
+ """;
+ params.put("jsonRes", s);
+ return new Prompt(List.of(userPromptTemplate.createMessage(params), systemPromptTemplate.createMessage(params)));
+ }
+
+ public static Prompt getLocalInterviewPrompt(Map params) {
+ SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""
+ 你是一位资深的技术面试专家。
+ ## 你的专业背景
+ - 10年+ 软件开发和技术管理经验
+ - 熟悉前端、后端、全栈、嵌入式、移动端、DevOps、大数据、AI等各技术领域
+ - 精通各种技术栈的深度和广度评估
+ - 擅长根据岗位特点和候选人背景进行精准匹配
+ ## 通用岗位分类及评估重点
+ ### 后端开发岗位
+ - 重点:框架熟练度、数据库设计、API设计、性能优化、并发处理
+ - 核心技能:编程语言基础、框架应用、系统设计、问题排查
+ ### 前端开发岗位
+ - 重点:UI/UX实现、性能优化、跨浏览器兼容、现代框架使用
+ - 核心技能:HTML/CSS/JS、框架应用、工程化、用户体验
+ ### 全栈开发岗位
+ - 重点:前后端技术栈、系统架构、DevOps流程
+ - 核心技能:多技术栈掌握、系统整合、项目管理
+ ### 架构师岗位
+ - 重点:系统设计、技术选型、性能调优、团队技术规划
+ - 核心技能:架构设计、技术决策、团队领导、业务理解
+ ## 筛选策略框架
+ ### 匹配维度权重
+ 1. **技术栈匹配度** (35%): 候选人技能与岗位要求的重合度
+ 2. **项目经验相关性** (30%): 过往项目与岗位场景的匹配度
+ 3. **技能深度评估** (20%): 根据经验年限判断技能掌握深度
+ 4. **发展潜力考量** (15%): 学习能力和技术视野的考察
+ ## 本岗位招聘要求
+ [{jobRequirements}]
+
+ ## 输出规范
+ - 必须输出标准JSON格式:{jsonRes}
+ - 不得包含任何解释、注释或代码块标记
+ - 确保所选题目ID真实存在于题库中
+ - 保证题目数量与要求完全一致
+ """);
+
+ PromptTemplate userPromptTemplate = new PromptTemplate("""
+ 请根据岗位招聘要求,从题库中筛选筛选 {count} 道技术面试题。
+ ## 候选人档案
+ ### 技术栈
+ {skills}
+ ### 简历内容
+ {resume}
+ ## 筛选要求
+ 请根据以上信息,结合你的专业框架,从题库中筛选出最能评估该候选人是否适合此岗位的面试题。
+ ### 重点考虑因素
+ 1. 岗位核心技术要求与候选人技能的匹配度
+ 2. 候选人项目经验与岗位应用场景的关联性
+ 3. 题目难度与候选人经验水平的适配性
+ 4. 题目类型的多样性(理论+实践+设计)
+ """
+ );
+ params.put("jsonRes",
+ """
+ {
+ "questions": [
+ {
+ "id": "1",
+ "content": "问题内容..."
+ },
+ {
+ "id": "3",
+ "content": "问题内容..."
+ }
+ ...
+ ]
+ }
+ """
+ );
+ return new Prompt(List.of(
+ userPromptTemplate.createMessage(params),
+ systemPromptTemplate.createMessage(params)
+ ));
+
+ }
+
+
+ public static Prompt getEvaluatePrompt(Map params) {
+// SystemMessage systemMessage = new SystemMessage("""
+// 你是一位经验丰富的高级技术面试官,以公正、严谨、深入的评估风格著称。
+// ## 你的专业特质
+// - 拥有15年+ 技术开发和面试经验
+// - 善于通过追问挖掘候选人的真实技术水平
+// - 能够准确识别标准答案、实际经验和深度理解的区别
+// - 注重考察解决问题的思路和实际应用能力
+//
+// """);
+
+ PromptTemplate promptTemplate = new PromptTemplate("""
+ 请对候选人的回答进行专业评估。
+ ## 评估维度及权重
+ ### 技术准确性 (30%)
+ - 概念理解的正确性
+ - 技术细节的准确程度
+ - 是否存在明显错误或误解
+ ### 深度与广度 (25%)
+ - 知识的深入程度
+ - 相关技术的关联理解
+ - 是否能举一反三
+ ### 实践经验 (25%)
+ - 是否有真实项目经验支撑
+ - 能否结合具体场景说明
+ - 对技术选型和权衡的理解
+ ### 表达能力 (20%)
+ - 逻辑清晰度
+ - 表达的完整性
+ - 专业术语使用的准确性
+ ## 评分标准
+ ### 优秀 (85-100分)
+ - 回答准确、深入、有见解
+ - 能结合实际项目经验
+ - 表达清晰、逻辑严密
+ - 展现出深度思考和实践能力
+ ### 良好 (70-84分)
+ - 回答基本正确,有一定深度
+ - 有实际应用经验
+ - 表达较为清晰
+ - 个别地方可能需要补充
+ ### 及格 (60-69分)
+ - 回答基本正确但较浅显
+ - 缺乏深入理解或实践经验
+ - 表达尚可但不够完整
+ - 需要进一步考察
+ ### 不及格 (0-59分)
+ - 回答错误或严重不完整
+ - 基础概念理解有误
+ - 表达混乱或逻辑不清
+ - 明显缺乏相关经验
+ ## 追问策略
+ ### 何时追问
+ 1. **概念模糊**: 候选人给出的概念定义不够准确或完整
+ 2. **缺少细节**: 回答过于宽泛,缺乏具体的技术细节
+ 3. **经验质疑**: 怀疑候选人是否有真实的项目经验
+ 4. **深度探索**: 基础回答正确,想考察更深层次的理解
+ ### 追问类型
+ 1. **澄清追问**: "你刚才提到XX,能具体解释一下吗?"
+ 2. **场景追问**: "在实际项目中,你是如何处理XX问题的?"
+ 3. **对比追问**: "XX和YY有什么区别?你会如何选择?"
+ 4. **深度追问**: "如果遇到XX情况,你会如何优化?"
+ ### 追问限制
+ - 单个问题最多追问3次
+ - 追问应该递进深入,不重复
+ - 追问后必须给出综合评估
+ - 避免过度纠缠细节,影响整体面试节奏
+ ## 当前问题
+ {question}
+
+ ## 候选人回答
+ {candidateAnswer}
+
+ ## 评估任务
+
+ ### 请分析以下方面
+ 1. **技术准确性**: 回答是否正确,有无技术错误
+ 2. **完整性**: 是否涵盖了问题的关键要点
+ 3. **深度**: 是否展现了深入的理解和思考
+ 4. **实践性**: 是否结合了实际项目经验
+ 5. **表达质量**: 逻辑是否清晰,表达是否准确
+
+ ### 追问决策逻辑
+ - 如果回答存在明显不足,制定一个精准的追问来深入考察
+ - 如果已经追问过2次,本次必须结束追问(continueAsking: false)
+ - 追问应该针对性强,避免过于宽泛
+ - 优先考察核心技术能力,避免偏离主题
+
+ ### AI标准答案要求
+ 请提供一个简洁、准确的技术答案作为参考,突出关键要点。
+
+ ## 输出格式要求
+ 严格按照以下JSON格式输出,确保字段完整且格式正确:
+
+ {jsonRes}
+
+ 开始评估:
+ """);
+ params.put(
+ "jsonRes",
+ """
+ {
+ "feedback": "对回答的具体评价,指出优点和不足",
+ "suggestions": "具体的改进建议和学习方向",
+ "aiAnswer": "简洁准确的标准答案要点",
+ "score": 75.5,
+ "continueAsking": true,
+ "followUpQuestion": "具体的追问问题(如果不追问则为空字符串)"
+ }
+ """
+ );
+ return new Prompt(List.of(
+ promptTemplate.createMessage(params)
+ ));
+ }
+
+ /**
+ * 获取最终报告
+ *
+ * @param params 参数
+ * @return 提示
+ */
+ public static Prompt getFinalReportPrompt(Map params) {
+// SystemMessage systemMessage = new SystemMessage("""
+// 你是一位资深的技术面试官和HR专家,具备丰富的候选人评估经验。你的职责是:
+//
+// ## 核心职能:
+// - 基于面试数据进行客观、全面的技术能力评估
+// - 提供标准化的面试结果分析和建议
+// - 支持招聘决策制定
+//
+// ## 评估原则:
+// 1. **客观性**:基于实际表现数据,避免主观推测
+// 2. **全面性**:覆盖技术能力、沟通表达、问题解决等维度
+// 3. **标准化**:使用统一的评分体系和输出格式
+// 4. **实用性**:提供可执行的改进建议和明确的录用建议
+//
+// ## 输出要求:
+// 严格按照JSON格式输出,不得包含任何markdown标记或额外解释:
+//
+// {
+// "overallScore": <1-100整数>,
+// "overallFeedback": "<综合评价,客观描述候选人整体表现,150-200字>",
+// "technicalAssessment": {
+// "Java基础": "掌握良好,对集合框架理解深入。",
+// "Spring框架": "熟悉基本使用,但对底层原理理解不足。",
+// "数据库": "能够编写常规SQL,但在索引优化方面知识欠缺。",
+// "<技术领域1>": "<该领域的具体评估>",
+// ...
+// },
+// "strengthsAndWeaknesses": {
+// "strengths": ["<具体优势1>", "<具体优势2>", "<具体优势3>"],
+// "weaknesses": ["<具体不足1>", "<具体不足2>", "<具体不足3>"]
+// },
+// "suggestions": [
+// "<具体可执行的改进建议1>",
+// "<具体可执行的改进建议2>",
+// "<具体可执行的改进建议3>",
+// "<具体可执行的改进建议4>",
+// "<具体可执行的改进建议5>"
+// ],
+// "hiringRecommendation": "<强烈推荐|推荐|待考虑|不推荐>",
+// "hiringReason": "<录用建议的具体理由,50-80字>"
+// }
+//
+// ## 评分标准:
+// - 90-100分:技术优秀,表达清晰,思维敏捷,超出岗位要求
+// - 80-89分:技术良好,基本满足岗位要求,有培养潜力
+// - 70-79分:技术一般,需要指导和培养
+// - 60-69分:技术较弱,存在明显知识盲区
+// - 60分以下:技术不足,不符合岗位要求
+// """);
+
+ PromptTemplate promptTemplate = new PromptTemplate("""
+ 请根据以下候选人信息和面试记录,生成标准化的面试评估报告:
+ ## 核心职能:
+ - 基于面试数据进行客观、全面的技术能力评估
+ - 提供标准化的面试结果分析和建议
+ - 支持招聘决策制定
+
+ ## 评估原则:
+ 1. **客观性**:基于实际表现数据,避免主观推测
+ 2. **全面性**:覆盖技术能力、沟通表达、问题解决等维度
+ 3. **标准化**:使用统一的评分体系和输出格式
+ 4. **实用性**:提供可执行的改进建议和明确的录用建议
+
+ ## 输出要求:
+ 严格按照JSON格式输出,不得包含任何markdown标记或额外解释:
+
+ {jsonRes}
+
+ ## 评分标准:
+ - 90-100分:技术优秀,表达清晰,思维敏捷,超出岗位要求
+ - 80-89分:技术良好,基本满足岗位要求,有培养潜力
+ - 70-79分:技术一般,需要指导和培养
+ - 60-69分:技术较弱,存在明显知识盲区
+ - 60分以下:技术不足,不符合岗位要求
+
+ ## 候选人简历信息:
+ {resume}
+
+ ## 完整面试记录:
+ {history}
+
+ 请开始评估并输出JSON格式的报告。
+ """);
+ params.put("jsonRes", """
+ {
+ "overallScore": <1-100整数>,
+ "overallFeedback": "<综合评价,客观描述候选人整体表现,150-200字>",
+ "technicalAssessment": {
+ "Java基础": "掌握良好,对集合框架理解深入。",
+ "Spring框架": "熟悉基本使用,但对底层原理理解不足。",
+ "数据库": "能够编写常规SQL,但在索引优化方面知识欠缺。",
+ "<技术领域1>": "<该领域的具体评估>",
+ ...
+ },
+ "strengthsAndWeaknesses": {
+ "strengths": ["<具体优势1>", "<具体优势2>", "<具体优势3>"],
+ "weaknesses": ["<具体不足1>", "<具体不足2>", "<具体不足3>"]
+ },
+ "suggestions": [
+ "<具体可执行的改进建议1>",
+ "<具体可执行的改进建议2>",
+ "<具体可执行的改进建议3>",
+ "<具体可执行的改进建议4>",
+ "<具体可执行的改进建议5>"
+ ],
+ "hiringRecommendation": "<强烈推荐|推荐|待考虑|不推荐>",
+ "hiringReason": "<录用建议的具体理由,50-80字>"
+ }
+ """);
+ return new Prompt(List.of(
+ promptTemplate.createMessage(params)
+ ));
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/common/utils/UUIDUtils.java b/src/main/java/com/qingqiu/interview/common/utils/UUIDUtils.java
new file mode 100644
index 0000000..2cd5e14
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/common/utils/UUIDUtils.java
@@ -0,0 +1,8 @@
+package com.qingqiu.interview.common.utils;
+
+public class UUIDUtils {
+
+ public static String getUUID() {
+ return java.util.UUID.randomUUID().toString().replace("-", "");
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/config/ChatMemoryConfig.java b/src/main/java/com/qingqiu/interview/config/ChatMemoryConfig.java
new file mode 100644
index 0000000..9a1fdf2
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/config/ChatMemoryConfig.java
@@ -0,0 +1,73 @@
+package com.qingqiu.interview.config;
+
+import com.alibaba.cloud.ai.memory.jdbc.MysqlChatMemoryRepository;
+import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
+import com.qingqiu.interview.common.constants.ChatConstant;
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.memory.MessageWindowChatMemory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import javax.sql.DataSource;
+
+import static com.qingqiu.interview.common.constants.ChatConstant.MAX_MESSAGES;
+
+/**
+ * 聊天记忆相关配置
+ */
+@Configuration
+public class ChatMemoryConfig {
+
+ @Value("${spring.ai.memory.redis.host}")
+ private String redisHost;
+ @Value("${spring.ai.memory.redis.port}")
+ private int redisPort;
+ @Value("${spring.ai.memory.redis.password}")
+ private String redisPassword;
+ @Value("${spring.ai.memory.redis.timeout}")
+ private int redisTimeout;
+
+ @Resource
+ private DataSource dataSource;
+
+
+ @Bean
+ public MysqlChatMemoryRepository mysqlChatMemoryRepository() {
+ JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
+ return MysqlChatMemoryRepository.mysqlBuilder()
+ .jdbcTemplate(jdbcTemplate)
+ .build();
+ }
+
+ @Primary
+ @Bean(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
+ public MessageWindowChatMemory mysqlChatMemory(MysqlChatMemoryRepository mysqlChatMemoryRepository) {
+ return MessageWindowChatMemory.builder()
+ .chatMemoryRepository(mysqlChatMemoryRepository)
+ .maxMessages(MAX_MESSAGES)
+ .build();
+ }
+
+ @Bean
+ public RedisChatMemoryRepository redisChatMemoryRepository() {
+ return RedisChatMemoryRepository.builder()
+ .host(redisHost)
+ .port(redisPort)
+ // 若没有设置密码则注释该项
+ .password(redisPassword)
+ .timeout(redisTimeout)
+ .build();
+ }
+
+ @Bean(name = ChatConstant.REDIS_CHAT_MEMORY_BEAN_NAME)
+ public MessageWindowChatMemory redisChatMemory(RedisChatMemoryRepository redisChatMemoryRepository) {
+ return MessageWindowChatMemory.builder()
+ .chatMemoryRepository(redisChatMemoryRepository)
+ .maxMessages(MAX_MESSAGES)
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java b/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
index 2bd2038..31cb174 100755
--- a/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
+++ b/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
@@ -1,14 +1,217 @@
package com.qingqiu.interview.config;
-import com.alibaba.dashscope.aigc.generation.Generation;
+import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
+import com.alibaba.cloud.ai.dashscope.api.DashScopeResponseFormat;
+import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
+import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
+import com.qingqiu.interview.common.constants.ChatConstant;
+import io.netty.channel.ChannelOption;
+import jakarta.annotation.Resource;
+import org.springframework.ai.chat.client.ChatClient;
+import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
+import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
+import org.springframework.ai.chat.memory.MessageWindowChatMemory;
+import org.springframework.ai.chat.model.ChatModel;
+import org.springframework.ai.openai.OpenAiChatModel;
+import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.openai.api.ResponseFormat;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.http.client.ReactorClientHttpRequestFactory;
+import org.springframework.http.client.reactive.ReactorClientHttpConnector;
+import org.springframework.web.client.RestClient;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.netty.http.client.HttpClient;
+import java.time.Duration;
+
+/**
+ * DashScope相关配置类
+ * 主要配置阿里云百炼平台(DashScope)和OpenAI的大模型服务
+ * 包括API客户端、聊天模型和聊天客户端的配置
+ *
+ * @author qingqiu
+ */
@Configuration
public class DashScopeConfig {
+ // 从配置文件中读取DashScope API密钥
+ @Value("${spring.ai.dashscope.api-key}")
+ private String dashScopeApiKey;
+
+ @Value("${spring.ai.dashscope.chat.options.model}")
+ private String dashScopeChatModelName;
+
+ @Value("${spring.ai.openai.chat.options.model}")
+ private String openAiChatModelName;
+
+ @Value("${spring.ai.openai.base-url}")
+ private String openAiBaseUrl;
+
+ @Value("${spring.ai.openai.api-key}")
+ private String openAiApiKey;
+
+ @Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
+ private MessageWindowChatMemory mysqlChatMemory;
+
+ @Resource(name = ChatConstant.REDIS_CHAT_MEMORY_BEAN_NAME)
+ private MessageWindowChatMemory redisChatMemory;
+
+ /**
+ * 创建DashScopeApi Bean实例
+ * 配置了HTTP客户端连接参数,包括连接超时和响应超时时间
+ *
+ * @return DashScopeApi实例
+ */
@Bean
- public Generation generation() {
- return new Generation();
+ public DashScopeApi dashScopeApi() {
+ return DashScopeApi.builder()
+ .apiKey(dashScopeApiKey)
+ .restClientBuilder(getRestClientBuilder())
+ .webClientBuilder(getWebClientBuilder())
+ .build();
}
-}
+
+
+ /**
+ * 创建DashScope聊天模型Bean实例
+ * 配置了最大token数、使用的模型以及返回格式为JSON对象
+ *
+ * @return ChatModel实例
+ */
+ @Bean(name = ChatConstant.DASH_SCOPE_CHAT_MODEL_BEAN_NAME)
+ public ChatModel dashScopeChatModel() {
+ return DashScopeChatModel.builder()
+ .dashScopeApi(dashScopeApi())
+ .defaultOptions(
+ DashScopeChatOptions.builder()
+ .withMaxToken(ChatConstant.MAX_COMPLETION_TOKENS)
+ .withModel(dashScopeChatModelName)
+ .withResponseFormat(
+ DashScopeResponseFormat.builder()
+ .type(DashScopeResponseFormat.Type.JSON_OBJECT)
+ .build()
+ )
+ .build()
+ )
+ .build();
+ }
+
+ /**
+ * 创建OpenAI聊天模型Bean实例(主模型)
+ * 配置了最大完成token数和响应格式为JSON Schema
+ *
+ * @return ChatModel实例
+ */
+ @Bean(name = ChatConstant.OPEN_AI_CHAT_MODEL_BEAN_NAME)
+ @Primary
+ public ChatModel openAiChatModel() {
+ return OpenAiChatModel.builder()
+ .openAiApi(
+ OpenAiApi.builder()
+ .apiKey(openAiApiKey)
+ .baseUrl(openAiBaseUrl)
+ .webClientBuilder(getWebClientBuilder())
+ .restClientBuilder(getRestClientBuilder())
+ .build()
+ )
+ .defaultOptions(
+ OpenAiChatOptions.builder()
+ .model(openAiChatModelName)
+ .maxCompletionTokens(ChatConstant.MAX_COMPLETION_TOKENS)
+
+ .responseFormat(
+ ResponseFormat.builder()
+ .type(ResponseFormat.Type.JSON_SCHEMA)
+ .jsonSchema(
+ ResponseFormat.JsonSchema
+ .builder()
+ .build()
+ ).build()
+ )
+ .build()
+ )
+ .build();
+ }
+
+ /**
+ * 创建默认的聊天客户端Bean实例(使用DashScope模型)
+ * 添加了日志记录功能
+ *
+ * @return ChatClient实例
+ */
+ @Bean
+ @Primary
+ public ChatClient chatClient() {
+ return ChatClient
+ .builder(dashScopeChatModel())
+ .defaultAdvisors(
+ new SimpleLoggerAdvisor()
+ )
+ .build();
+ }
+
+ /**
+ * 创建基于MySQL存储聊天历史的DashScope聊天客户端Bean实例
+ * 配置了消息内存管理和日志记录功能
+ *
+ * @return ChatClient实例
+ */
+ @Bean(name = ChatConstant.DASH_SCOPE_CHAT_CLIENT_BEAN_NAME)
+ public ChatClient dashScopeChatClient() {
+ return ChatClient
+ .builder(dashScopeChatModel())
+ .defaultAdvisors(
+ MessageChatMemoryAdvisor.builder(mysqlChatMemory).build(),
+ new SimpleLoggerAdvisor()
+ )
+ .build();
+ }
+
+ /**
+ * 创建基于MySQL存储聊天历史的OpenAI聊天客户端Bean实例
+ * 配置了消息内存管理和日志记录功能
+ *
+ * @return ChatClient实例
+ */
+ @Bean(name = ChatConstant.OPEN_AI_CHAT_CLIENT_BEAN_NAME)
+ public ChatClient openAiChatClient() {
+ return ChatClient
+ .builder(openAiChatModel())
+ .defaultAdvisors(
+ MessageChatMemoryAdvisor.builder(mysqlChatMemory).build(),
+ new SimpleLoggerAdvisor()
+ )
+ .build();
+ }
+
+
+ /**
+ * 创建全局默认的WebClient Bean实例
+ * 配置了连接超时和响应超时时间
+ *
+ * @return WebClient实例
+ */
+ public WebClient.Builder getWebClientBuilder() {
+ HttpClient httpClient = HttpClient.create()
+ .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 连接超时
+ .responseTimeout(Duration.ofMillis(10000)); // 读取超时
+
+ return WebClient.builder()
+ .clientConnector(new ReactorClientHttpConnector(httpClient))
+ ;
+ }
+
+ private RestClient.Builder getRestClientBuilder() {
+ return RestClient.builder()
+ .requestFactory(
+ new ReactorClientHttpRequestFactory(
+ HttpClient.create()
+ .responseTimeout(Duration.ofMinutes(5))
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/qingqiu/interview/config/Fastjson2RedisSerializer.java b/src/main/java/com/qingqiu/interview/config/Fastjson2RedisSerializer.java
new file mode 100644
index 0000000..49a7a47
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/config/Fastjson2RedisSerializer.java
@@ -0,0 +1,57 @@
+package com.qingqiu.interview.config;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public class Fastjson2RedisSerializer implements RedisSerializer {
+ // 默认编码
+ public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+ // 泛型类型,用于反序列化
+ private final Class clazz;
+
+ public Fastjson2RedisSerializer(Class clazz) {
+ super();
+ this.clazz = clazz;
+ }
+
+ /**
+ * 序列化:将对象转换为字节数组
+ */
+ @Override
+ public byte[] serialize(T t) throws SerializationException {
+ if (t == null) {
+ return new byte[0];
+ }
+ try {
+ // 使用 Fastjson2 序列化对象,并写入字节数组
+ // 配置写入特性:WriteClassName 确保反序列化时能识别类型,如果不需要,可以移除。
+ return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
+ } catch (Exception ex) {
+ throw new SerializationException("Could not serialize object with Fastjson2", ex);
+ }
+ }
+
+ /**
+ * 反序列化:将字节数组转换为对象
+ */
+ @Override
+ public T deserialize(byte[] bytes) throws SerializationException {
+ if (bytes == null || bytes.length <= 0) {
+ return null;
+ }
+ try {
+ // 使用 Fastjson2 反序列化字节数组
+ // 配置读取特性:SupportAutoType,确保可以正确读取带有类名信息的JSON
+ return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType);
+ } catch (Exception ex) {
+ throw new SerializationException("Could not deserialize object with Fastjson2", ex);
+ }
+ }
+}
diff --git a/src/main/java/com/qingqiu/interview/config/RedisConfig.java b/src/main/java/com/qingqiu/interview/config/RedisConfig.java
new file mode 100644
index 0000000..f69ba89
--- /dev/null
+++ b/src/main/java/com/qingqiu/interview/config/RedisConfig.java
@@ -0,0 +1,32 @@
+package com.qingqiu.interview.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(redisConnectionFactory);
+
+ // 使用StringRedisSerializer来序列化和反序列化redis的key值
+ StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+ template.setKeySerializer(stringRedisSerializer);
+ template.setHashKeySerializer(stringRedisSerializer);
+
+ // 使用Fastjson2RedisSerializer来序列化和反序列化redis的value值
+
+ Fastjson2RedisSerializer