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 fastjson2RedisSerializer = new Fastjson2RedisSerializer<>(Object.class); + + template.setValueSerializer(fastjson2RedisSerializer); + template.setHashValueSerializer(fastjson2RedisSerializer); + + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java b/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java index 4935faf..a8d51bc 100755 --- a/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java +++ b/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java @@ -1,13 +1,14 @@ package com.qingqiu.interview.controller; -import com.alibaba.dashscope.common.Role; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.qingqiu.interview.common.res.R; import com.qingqiu.interview.entity.AiSessionLog; import com.qingqiu.interview.service.IAiSessionLogService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.web.bind.annotation.GetMapping; @@ -38,7 +39,7 @@ public class AiSessionLogController { return R.success(service.list( new LambdaQueryWrapper() .eq(AiSessionLog::getToken, sessionId) - .ne(AiSessionLog::getRole, Role.SYSTEM.getValue()) + .ne(AiSessionLog::getRole, MessageType.SYSTEM.getValue()) )); } diff --git a/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java b/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java index 4d25552..46b9e00 100755 --- a/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java +++ b/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java @@ -25,5 +25,10 @@ public class InterviewStartRequest { /** 生成的面试题目数量 */ private Integer totalQuestions = 10; + /** + * 岗位要求 + */ + private String jobRequirements; + // 简历文件通过MultipartFile单独传递 } diff --git a/src/main/java/com/qingqiu/interview/entity/InterviewSession.java b/src/main/java/com/qingqiu/interview/entity/InterviewSession.java index 2b3a3a5..9342780 100755 --- a/src/main/java/com/qingqiu/interview/entity/InterviewSession.java +++ b/src/main/java/com/qingqiu/interview/entity/InterviewSession.java @@ -31,6 +31,9 @@ public class InterviewSession implements Serializable { @TableField("resume_content") private String resumeContent; + @TableField("job_requirements") + private String jobRequirements; + @TableField("extracted_skills") private String extractedSkills; @TableField("interview_type") diff --git a/src/main/java/com/qingqiu/interview/entity/ai/EvaluateAiRes.java b/src/main/java/com/qingqiu/interview/entity/ai/EvaluateAiRes.java new file mode 100644 index 0000000..41a1b30 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/entity/ai/EvaluateAiRes.java @@ -0,0 +1,11 @@ +package com.qingqiu.interview.entity.ai; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public record EvaluateAiRes(@JsonProperty("feedback") String feedback, + @JsonProperty("suggestions") String suggestions, + @JsonProperty("aiAnswer") String aiAnswer, + @JsonProperty("score") double score, + @JsonProperty("continueAsking") boolean continueAsking, + @JsonProperty("followUpQuestion") String followUpQuestion) { +} diff --git a/src/main/java/com/qingqiu/interview/entity/ai/ExtractSkillAiRes.java b/src/main/java/com/qingqiu/interview/entity/ai/ExtractSkillAiRes.java new file mode 100644 index 0000000..c6f5232 --- /dev/null +++ b/src/main/java/com/qingqiu/interview/entity/ai/ExtractSkillAiRes.java @@ -0,0 +1,10 @@ +package com.qingqiu.interview.entity.ai; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public record ExtractSkillAiRes( + @JsonProperty("skills") List skills + ) { +} diff --git a/src/main/java/com/qingqiu/interview/entity/ai/InterviewReportAiRes.java b/src/main/java/com/qingqiu/interview/entity/ai/InterviewReportAiRes.java new file mode 100644 index 0000000..806d9ec --- /dev/null +++ b/src/main/java/com/qingqiu/interview/entity/ai/InterviewReportAiRes.java @@ -0,0 +1,27 @@ +package com.qingqiu.interview.entity.ai; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +public record InterviewReportAiRes( + @JsonProperty("overallScore") String overallScore, + @JsonProperty("overallFeedback") String overallFeedback, + + @JsonProperty("technicalAssessment") Map technicalAssessment, + @JsonProperty("strengthsAndWeaknesses") StrengthsAndWeaknesses strengthsAndWeaknesses, + + @JsonProperty("suggestions") List suggestions, + @JsonProperty("hiringRecommendation") String hiringRecommendation, + @JsonProperty("hiringReason") String hiringReason +) { + + /** + * 内部 Record:对应 "strengthsAndWeaknesses" 对象。 + */ + public record StrengthsAndWeaknesses( + @JsonProperty("strengths") List strengths, + @JsonProperty("weaknesses") List weaknesses + ) {} +} diff --git a/src/main/java/com/qingqiu/interview/entity/ai/QuestionAiRes.java b/src/main/java/com/qingqiu/interview/entity/ai/QuestionAiRes.java new file mode 100644 index 0000000..f6e9a0f --- /dev/null +++ b/src/main/java/com/qingqiu/interview/entity/ai/QuestionAiRes.java @@ -0,0 +1,26 @@ +package com.qingqiu.interview.entity.ai; + + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class QuestionAiRes { + + /** + * 内部实体类:对应 JSON 数组中的每个元素。 + * { "id": "ai-gen-1", "content": "问题内容..." } + */ + public record Question( + @JsonProperty("id") String id, + @JsonProperty("content") String content + ) {} + + /** + * 外部实体类:对应整个 JSON 响应的顶层结构。 + * { "questions": [...] } + */ + public record Wrapper( + @JsonProperty("questions") List questions + ) {} +} diff --git a/src/main/java/com/qingqiu/interview/service/ChatService.java b/src/main/java/com/qingqiu/interview/service/ChatService.java deleted file mode 100644 index 90d7241..0000000 --- a/src/main/java/com/qingqiu/interview/service/ChatService.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.qingqiu.interview.service; - -import com.qingqiu.interview.dto.ChatDTO; -import com.qingqiu.interview.dto.InterviewStartRequest; -import com.qingqiu.interview.vo.ChatVO; -import org.springframework.web.multipart.MultipartFile; - -/** - *

- * - * @author qingqiu - * @date 2025/9/18 12:45 - */ -public interface ChatService { - - /** - * 创建普通会话 - * @return sessionId - */ - ChatVO createChat(ChatDTO dto); - - /** - * 创建面试会话 - * @param resume 简历 - * @param request 面试信息 - * @return sessionId - */ - String createInterviewChat(MultipartFile resume, InterviewStartRequest request); -} diff --git a/src/main/java/com/qingqiu/interview/service/InterviewAiService.java b/src/main/java/com/qingqiu/interview/service/InterviewAiService.java index d0b4724..209e966 100644 --- a/src/main/java/com/qingqiu/interview/service/InterviewAiService.java +++ b/src/main/java/com/qingqiu/interview/service/InterviewAiService.java @@ -1,15 +1,17 @@ package com.qingqiu.interview.service; -import com.alibaba.fastjson2.JSONObject; import com.qingqiu.interview.entity.InterviewQuestionProgress; import com.qingqiu.interview.entity.InterviewSession; import com.qingqiu.interview.entity.Question; +import com.qingqiu.interview.entity.ai.EvaluateAiRes; +import com.qingqiu.interview.entity.ai.InterviewReportAiRes; +import com.qingqiu.interview.entity.ai.QuestionAiRes; import java.util.List; /** *

- * 面试接入AI的接口 + * 面试接入AI的接口 *

* * @author qingqiu @@ -23,38 +25,38 @@ public interface InterviewAiService { * @param resumeContent 简历文本 * @return 包含技能列表的JSON对象 */ - JSONObject extractSkillsFromResume(String resumeContent); + List extractSkillsFromResume(String resumeContent); /** * 根据技能动态生成面试题目 * - * @param skills 技能列表 + * @param skills 技能列表 * @param resumeContent 简历内容 - * @param count 需要生成的题目数量 + * @param count 需要生成的题目数量 * @return 包含问题列表的JSON对象 */ - JSONObject generateQuestionsOfAi(String sessionId, List skills, String resumeContent, int count); + List generateQuestionsOfAi(String sessionId, List skills, String jobRequirements, String resumeContent, int count); - JSONObject generateQuestionOfLocal(String sessionId, List questions, List skills, String resumeContent, int count); + List generateQuestionOfLocal(String sessionId, List questions, List skills, String jobRequirements, String resumeContent, int count); /** * 评估用户的回答 * - * @param question 问题内容 - * @param userAnswer 用户的回答 - * @param context 可选的上下文(之前的问答历史) + * @param question 问题内容 + * @param userAnswer 用户的回答 + * @param context 可选的上下文(之前的问答历史) * @return 包含评估结果的JSON对象 */ - JSONObject evaluateAnswer(String sessionId, String question, String userAnswer, List context); + EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List context); /** * 生成最终的面试评估报告 * - * @param session 面试会话信息 - * @param progressList 整个面试的问答记录 + * @param session 面试会话信息 + * @param progressList 整个面试的问答记录 * @return 包含最终报告的JSON对象 */ - JSONObject generateFinalReport(InterviewSession session, List progressList); + InterviewReportAiRes generateFinalReport(InterviewSession session, List progressList); String generateFirstQuestion(String sessionId, String candidateName, String questionContent); diff --git a/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java b/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java index f4881fb..a7f213b 100755 --- a/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java +++ b/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java @@ -3,10 +3,14 @@ package com.qingqiu.interview.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.qingqiu.interview.common.constants.ChatConstant; +import com.qingqiu.interview.common.utils.UUIDUtils; import com.qingqiu.interview.entity.Question; -import com.qingqiu.interview.service.llm.LlmService; +import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.stereotype.Service; import java.util.ArrayList; @@ -17,10 +21,11 @@ import java.util.List; @RequiredArgsConstructor public class QuestionClassificationService { - private final LlmService llmService; - + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ChatClient chatClient; + /** * 使用AI对题目进行分类 */ @@ -28,10 +33,14 @@ public class QuestionClassificationService { log.info("开始使用AI分类题目,内容长度: {}", rawContent.length()); String prompt = buildClassificationPrompt(rawContent); - String aiResponse = llmService.chat(prompt); - + String aiResponse = chatClient.prompt() + .user(prompt) + .call() + .content(); + log.info("AI分类响应: {}", aiResponse); - + + assert aiResponse != null; return parseAiResponse(aiResponse); } diff --git a/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java deleted file mode 100644 index 31d7020..0000000 --- a/src/main/java/com/qingqiu/interview/service/impl/ChatServiceImpl.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.qingqiu.interview.service.impl; - -import cn.hutool.core.collection.CollectionUtil; -import com.alibaba.dashscope.common.Message; -import com.alibaba.dashscope.common.Role; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.qingqiu.interview.ai.factory.AIClientManager; -import com.qingqiu.interview.annotation.AiChatLog; -import com.qingqiu.interview.common.enums.LLMProvider; -import com.qingqiu.interview.common.utils.AIUtils; -import com.qingqiu.interview.dto.ChatDTO; -import com.qingqiu.interview.dto.InterviewStartRequest; -import com.qingqiu.interview.entity.AiSessionLog; -import com.qingqiu.interview.service.ChatService; -import com.qingqiu.interview.service.IAiSessionLogService; -import com.qingqiu.interview.vo.ChatVO; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import static com.qingqiu.interview.common.constants.CommonConstant.DEFAULT_TRUNCATE_RATIO; -import static com.qingqiu.interview.common.constants.CommonConstant.MAX_TOKEN; - -/** - *

- * - * @author qingqiu - * @date 2025/9/18 12:56 - */ - -@Slf4j -@Service -@RequiredArgsConstructor -public class ChatServiceImpl implements ChatService { - - private final AIClientManager aiClientManager; - - private final IAiSessionLogService aiSessionLogService; - - @Override - @AiChatLog - public ChatVO createChat(ChatDTO dto) { - LLMProvider llmProvider = LLMProvider.fromCode(dto.getAiModel()); - List messages = new ArrayList<>(); - AtomicInteger tokens = new AtomicInteger(); - // 如果会话id不为空 则从数据库中获取会话记录 - if (dto.getSessionId() != null) { - List list = aiSessionLogService.list( - new LambdaQueryWrapper() - .eq(AiSessionLog::getToken, dto.getSessionId()) - .eq(AiSessionLog::getDataType, dto.getDataType()) - .orderByAsc(AiSessionLog::getCreatedTime) - ); - if (CollectionUtil.isNotEmpty(list)) { - messages.addAll(list.stream().map(data -> { - tokens.getAndAdd(AIUtils.getPromptTokens(data.getContent())); - return AIUtils.createMessage(data.getRole(), data.getContent()); - }).toList()); - } - - } - messages.add(AIUtils.createMessage(dto.getRole(), dto.getContent())); - List finalMessage = new ArrayList<>(); - // 剪切 10%的消息 - if (tokens.get() > MAX_TOKEN) { - BigDecimal size = new BigDecimal(String.valueOf(messages.size())); - size = size.multiply(DEFAULT_TRUNCATE_RATIO).setScale(0, RoundingMode.HALF_UP); - for (int i = size.intValue(); i < messages.size(); i++) { - finalMessage.add(messages.get(i)); - } - } else { - finalMessage = messages; - } - String res = aiClientManager.getClient(llmProvider).chatCompletion(finalMessage); - - - return ChatVO.builder() - .role(Role.ASSISTANT.getValue()) - .sessionId(dto.getSessionId()) - .content(res) - .build(); - } - - @Override - public String createInterviewChat(MultipartFile resume, InterviewStartRequest request) { - return ""; - } -} diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java index d05bc91..268b2af 100644 --- a/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewAiServiceImpl.java @@ -1,24 +1,29 @@ package com.qingqiu.interview.service.impl; -import com.alibaba.dashscope.common.Role; +import cn.hutool.core.map.MapUtil; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; -import com.qingqiu.interview.common.constants.CommonConstant; -import com.qingqiu.interview.dto.ChatDTO; +import com.qingqiu.interview.common.constants.ChatConstant; +import com.qingqiu.interview.common.utils.PromptTemplateUtils; import com.qingqiu.interview.entity.InterviewQuestionProgress; import com.qingqiu.interview.entity.InterviewSession; import com.qingqiu.interview.entity.Question; -import com.qingqiu.interview.service.ChatService; +import com.qingqiu.interview.entity.ai.EvaluateAiRes; +import com.qingqiu.interview.entity.ai.ExtractSkillAiRes; +import com.qingqiu.interview.entity.ai.InterviewReportAiRes; +import com.qingqiu.interview.entity.ai.QuestionAiRes; import com.qingqiu.interview.service.InterviewAiService; -import com.qingqiu.interview.vo.ChatVO; -import lombok.RequiredArgsConstructor; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Lazy; +import org.jetbrains.annotations.NotNull; +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.prompt.Prompt; import org.springframework.stereotype.Service; import java.util.List; -import java.util.stream.Collectors; +import java.util.Map; +import java.util.UUID; /** *

@@ -28,114 +33,163 @@ import java.util.stream.Collectors; */ @Slf4j @Service -@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy}) public class InterviewAiServiceImpl implements InterviewAiService { - private final ChatService chatService; + @Resource + private ChatClient chatClient; + + @Resource(name = ChatConstant.DASH_SCOPE_CHAT_CLIENT_BEAN_NAME) + private ChatClient dashScopeChatClient; + + @Resource(name = ChatConstant.OPEN_AI_CHAT_CLIENT_BEAN_NAME) + private ChatClient openAiChatClient; @Override - public JSONObject extractSkillsFromResume(String resumeContent) { - String prompt = "你是一位资深的IT技术招聘专家。" + - "请仔细阅读以下简历内容,并提取出其中所有的关键技术技能。" + - "请严格按照以下JSON格式返回,不要添加任何额外的解释或说明:\n" + - "{\"skills\": [\"技能1\", \"技能2\", \"...\"]}\n\n" + - "简历内容如下:\n" + resumeContent; - - ChatDTO chatDTO = new ChatDTO() - .setContent(prompt) - .setRole(Role.SYSTEM.getValue()) - .setDataType(CommonConstant.ONE); - ChatVO chatVO = chatService.createChat(chatDTO); - - return JSONObject.parse(chatVO.getContent()); + public List extractSkillsFromResume(String resumeContent) { + ExtractSkillAiRes entity = chatClient + .prompt(PromptTemplateUtils.getExtractSkillsPrompt(resumeContent)) + .call() + .entity(ExtractSkillAiRes.class); + assert entity != null; + return entity.skills(); } @Override - public JSONObject generateQuestionsOfAi(String sessionId, List skills, String resumeContent, int count) { - String skillsStr = String.join(", ", skills); - String prompt = String.format( - "你是一位专业的软件开发岗位技术面试官。" + - "请根据候选人的以下技术栈、项目经历、简历内容,生成 %d 道有深度和广度的面试题。" + - "题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。" + - "请严格按照以下JSON格式返回,question数组中必须包含 %d 个问题对象:\n" + - "{\"questions\": [{\"id\": \"ai-gen-1\", \"content\": \"问题1内容...\"}, {\"id\": \"ai-gen-2\", \"content\": \"问题2内容...\"}]}\n\n" + - "候选人技术栈:%s\n" + - "候选人简历:%s", - count, count, skillsStr, resumeContent - ); - - ChatDTO chatDTO = new ChatDTO() - .setSessionId(sessionId) - .setContent(prompt) - .setRole(Role.SYSTEM.getValue()) - .setDataType(CommonConstant.ONE); - ChatVO chatVO = chatService.createChat(chatDTO); - return JSON.parseObject(chatVO.getContent()); + public List generateQuestionsOfAi(String sessionId, + List skills, + String jobRequirements, + String resumeContent, + int count) { +// String skillsStr = String.join(", ", skills); +// String prompt = String.format( +// "你是一位专业的软件开发岗位技术面试官。" + +// "请根据候选人的以下技术栈、项目经历、简历内容,生成 %d 道有深度和广度的面试题。" + +// "题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。" + +// "请严格按照以下JSON格式返回,question数组中必须包含 %d 个问题对象:\n" + +// "{\"questions\": [{\"id\": \"ai-gen-1\", \"content\": \"问题1内容...\"}, {\"id\": \"ai-gen-2\", \"content\": \"问题2内容...\"}]}\n\n" + +// "候选人技术栈:%s\n" + +// "候选人简历:%s", +// count, count, skillsStr, resumeContent +// ); + Map params = MapUtil.builder() + .put("count", count) + .put("jobRequirements", jobRequirements) + .put("skills", JSONObject.toJSONString(skills)) + .put("resume", resumeContent) + .build(); + Prompt aiInterviewerPrompt = PromptTemplateUtils.getAiInterviewerPrompt(params); + QuestionAiRes.Wrapper entity = openAiChatClient + .prompt(aiInterviewerPrompt) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) + .call() + .entity(QuestionAiRes.Wrapper.class); +// String content = openAiChatClient +// .prompt(aiInterviewerPrompt) +// .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) +// .call() +// +// .content() +// ; +// ChatDTO chatDTO = new ChatDTO() +// .setSessionId(sessionId) +// .setContent(prompt) +// .setRole(Role.SYSTEM.getValue()) +// .setDataType(CommonConstant.ONE); +// ChatVO chatVO = chatService.createChat(chatDTO); + return entity.questions(); } @Override - public JSONObject generateQuestionOfLocal(String sessionId, List questions, List skills, String resumeContent, int count) { - String skillsStr = String.join(", ", skills); - // 2. 构建发送给AI的提示 - String prompt = String.format(""" - 你是一位专业的面试官。请根据以下候选人的技术栈、项目经历、简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。 - 题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。 - 要求: - 1. 题目必须严格从【题库JSON】中选择。 - 2. 挑选的题目应根据候选人的简历内容来抽取。 - 3. 返回一个只包含所选题目ID的JSON数组,格式为:{"question_ids": [1, 5, 23, ...]}。 - 4. 不要返回任何多余的代码,包括markdown形式的代码,我只需要JSON对象,请严格按照api接口形式返回 - 5. 不要返回任何额外的解释或文字,只返回JSON对象。 - 6. 严格按照前后端分离的接口形式返回JSON数据给我,不要返回"```json```" - 7. 请保证返回数据的完整性,不要返回不完整的数据,否则我的JSON解析会报错!!! - - 【候选人技术栈】: - %s - 【候选人简历】: - [%s] - 【题库JSON】: - %s - """, count, skillsStr, resumeContent, JSONObject.toJSONString(questions)); - ChatDTO chatDTO = new ChatDTO() - .setSessionId(sessionId) - .setContent(prompt) - .setRole(Role.SYSTEM.getValue()) - .setDataType(CommonConstant.ONE); - ChatVO chatVO = chatService.createChat(chatDTO); - return JSON.parseObject(chatVO.getContent()); + public List generateQuestionOfLocal(String sessionId, + List questions, + List skills, + String jobRequirements, + String resumeContent, + int count) { + Map params = MapUtil.builder() + .put("count", count) + .put("jobRequirements", jobRequirements) + .put("skills", JSONObject.toJSONString(skills)) + .put("resume", resumeContent) + .build(); + + Prompt aiInterviewerPrompt = PromptTemplateUtils.getLocalInterviewPrompt(params); + QuestionAiRes.Wrapper entity = openAiChatClient.prompt(aiInterviewerPrompt) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) + .call() + .entity(QuestionAiRes.Wrapper.class); + assert entity != null; + return entity.questions(); +// String skillsStr = String.join(", ", skills); +// // 2. 构建发送给AI的提示 +// String prompt = String.format(""" +// 你是一位专业的面试官。请根据以下候选人的技术栈、项目经历、简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。 +// 题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。 +// 要求: +// 1. 题目必须严格从【题库JSON】中选择。 +// 2. 挑选的题目应根据候选人的简历内容来抽取。 +// 3. 返回一个只包含所选题目ID的JSON数组,格式为:{"question_ids": [1, 5, 23, ...]}。 +// 4. 不要返回任何多余的代码,包括markdown形式的代码,我只需要JSON对象,请严格按照api接口形式返回 +// 5. 不要返回任何额外的解释或文字,只返回JSON对象。 +// 6. 严格按照前后端分离的接口形式返回JSON数据给我,不要返回"```json```" +// 7. 请保证返回数据的完整性,不要返回不完整的数据,否则我的JSON解析会报错!!! +// +// 【候选人技术栈】: +// %s +// 【候选人简历】: +// [%s] +// 【题库JSON】: +// %s +// """, count, skillsStr, resumeContent, JSONObject.toJSONString(questions)); +// ChatDTO chatDTO = new ChatDTO() +// .setSessionId(sessionId) +// .setContent(prompt) +// .setRole(Role.SYSTEM.getValue()) +// .setDataType(CommonConstant.ONE); +// ChatVO chatVO = chatService.createChat(chatDTO); +// return JSON.parseObject(chatVO.getContent()); } @Override - public JSONObject evaluateAnswer(String sessionId, String question, String userAnswer, List context) { + public EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List context) { // 构建上下文历史 - String history = context.stream() - .map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer())) - .collect(Collectors.joining("\n---\n")); - - String prompt = "你是一位资深的技术面试官,以严格和深入著称。" + - "你需要评估候选人对以下问题的回答。请注意:\n" + - "1. 如果回答模糊、不完整或有错误,你可以提出一个具体的追问问题(followUpQuestion)来深入考察,此时'continueAsking'应为true。\n" + - "2. 如果回答得很好,则'continueAsking'为false,'followUpQuestion'为空字符串。\n" + - "3. 'score'范围为0-100分。\n" + - "4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" + - "5. 追问最好有限制,不要无限制的向下追问,注意追问是支线而非主线!追问至多3个问题,之后必须切回主线\n" + - "请严格按照以下JSON格式返回,不要有任何额外说明:\n" + - "{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" + - "面试历史上下文:\n" + history + "\n\n" + - "当前问题:\n" + question + "\n\n" + - "候选人回答:\n" + userAnswer; - - ChatDTO chatDTO = new ChatDTO() - .setSessionId(sessionId) - .setContent(prompt) - .setRole(Role.SYSTEM.getValue()) - .setDataType(CommonConstant.ONE); - ChatVO chatVO = chatService.createChat(chatDTO); - return JSON.parseObject(chatVO.getContent()); +// String history = context.stream() +// .map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer())) +// .collect(Collectors.joining("\n---\n")); +// +// String prompt = "你是一位资深的技术面试官,以严格和深入著称。" + +// "你需要评估候选人对以下问题的回答。请注意:\n" + +// "1. 如果回答模糊、不完整或有错误,你可以提出一个具体的追问问题(followUpQuestion)来深入考察,此时'continueAsking'应为true。\n" + +// "2. 如果回答得很好,则'continueAsking'为false,'followUpQuestion'为空字符串。\n" + +// "3. 'score'范围为0-100分。\n" + +// "4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" + +// "5. 追问最好有限制,不要无限制的向下追问,注意追问是支线而非主线!追问至多3个问题,之后必须切回主线\n" + +// "请严格按照以下JSON格式返回,不要有任何额外说明:\n" + +// "{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" + +// "面试历史上下文:\n" + history + "\n\n" + +// "当前问题:\n" + question + "\n\n" + +// "候选人回答:\n" + userAnswer; +// +// ChatDTO chatDTO = new ChatDTO() +// .setSessionId(sessionId) +// .setContent(prompt) +// .setRole(Role.SYSTEM.getValue()) +// .setDataType(CommonConstant.ONE); +// ChatVO chatVO = chatService.createChat(chatDTO); + Map params = MapUtil.builder() + .put("question", question) + .put("candidateAnswer", userAnswer) + .build(); + Prompt prompt = PromptTemplateUtils.getEvaluatePrompt(params); + return openAiChatClient + .prompt(prompt) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) + .call() + .entity(EvaluateAiRes.class); } @Override - public JSONObject generateFinalReport(InterviewSession session, List progressList) { + public InterviewReportAiRes generateFinalReport(InterviewSession session, List progressList) { // String transcript = progressList.stream() // .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n", // p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback())) @@ -148,23 +202,26 @@ public class InterviewAiServiceImpl implements InterviewAiService { // "{\"overallScore\": 88.0, \"summary\": \"...\", \"strengths\": [\"...\"], \"weaknesses\": [\"...\"]}\n\n" + // "候选人姓名:" + session.getCandidateName() + "\n" + // "面试完整记录:\n" + transcript; - - String prompt = buildFinalReportPrompt(session, progressList); - - ChatDTO chatDTO = new ChatDTO() - .setRole(Role.SYSTEM.getValue()) - .setDataType(CommonConstant.ONE) - .setContent(prompt); - ChatVO chatVO = chatService.createChat(chatDTO); - return JSON.parseObject(chatVO.getContent()); +// ChatDTO chatDTO = new ChatDTO() +// .setRole(Role.SYSTEM.getValue()) +// .setDataType(CommonConstant.ONE) +// .setContent(prompt); +// ChatVO chatVO = chatService.createChat(chatDTO); + Map params = getFinalReportParams(session, progressList); + Prompt prompt = PromptTemplateUtils.getFinalReportPrompt(params); + return openAiChatClient.prompt(prompt) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, UUID.randomUUID().toString().replace("-", ""))) + .call() + .entity(InterviewReportAiRes.class); } - private String buildFinalReportPrompt(InterviewSession session, List progressList) { + @NotNull + private static Map getFinalReportParams(InterviewSession session, List progressList) { StringBuilder historyBuilder = new StringBuilder(); for (InterviewQuestionProgress progress : progressList) { historyBuilder.append( String.format("\n【问题】: %s\n【回答】: %s\n【AI单题反馈】: %s\n【AI单题建议】: %s\n【AI单题评分】: %s/5.0\n", - progress.getQuestionContent(), + progress.getQuestionContent(), progress.getUserAnswer(), progress.getFeedback(), progress.getSuggestions(), @@ -172,6 +229,27 @@ public class InterviewAiServiceImpl implements InterviewAiService { ) ); } + return MapUtil.builder() + .put("resume", session.getResumeContent()) + .put("history", historyBuilder.toString()) + .build(); + } + + /* + private String buildFinalReportPrompt(InterviewSession session, List progressList) { + StringBuilder historyBuilder = new StringBuilder(); + for (InterviewQuestionProgress progress : progressList) { + historyBuilder.append( + String.format("\n【问题】: %s\n【回答】: %s\n【AI单题反馈】: %s\n【AI单题建议】: %s\n【AI单题评分】: %s/5.0\n", + progress.getQuestionContent(), + progress.getUserAnswer(), + progress.getFeedback(), + progress.getSuggestions(), + progress.getScore() + ) + ); + } + return String.format(""" 你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估,给出一份全面、专业、有深度的最终面试报告。 @@ -206,22 +284,36 @@ public class InterviewAiServiceImpl implements InterviewAiService { %s """, session.getResumeContent(), historyBuilder.toString()); } + */ @Override public String generateFirstQuestion(String sessionId, String candidateName, String questionContent) { + +// ChatDTO chatDTO = new ChatDTO() +// .setSessionId(sessionId) +// .setRole(Role.SYSTEM.getValue()) +// .setDataType(CommonConstant.ONE) +// .setContent(prompt); +// ChatVO chatVO = chatService.createChat(chatDTO); +// return chatVO.getContent(); String prompt = String.format(""" 你是一位专业的技术面试官。现在要开始面试,候选人是 %s。 - + \s 第一个问题是:%s - + \s 请以友好但专业的语气提出这个问题,可以适当添加一些引导性的话语。 - """, candidateName, questionContent); - ChatDTO chatDTO = new ChatDTO() - .setSessionId(sessionId) - .setRole(Role.SYSTEM.getValue()) - .setDataType(CommonConstant.ONE) - .setContent(prompt); - ChatVO chatVO = chatService.createChat(chatDTO); - return chatVO.getContent(); + 严格按照JSON格式输出,不得包含任何markdown标记或额外解释: + 请返回JSON格式的数据:\s + { + "content": "xxx" + } + \s""", candidateName, questionContent); + String content = openAiChatClient.prompt() + .user(prompt) + .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId)) + .call() + .content(); + return JSON.parseObject(content).getString("content"); + } } diff --git a/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java b/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java index a94893d..ef5e1d7 100755 --- a/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java +++ b/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java @@ -37,9 +37,10 @@ public class InterviewQuestionProgressServiceImpl extends ServiceImpl flowedQuestions = new ConcurrentHashMap<>(); + @Override @Transactional(rollbackFor = Exception.class) public InterviewSession startInterview(MultipartFile file, InterviewStartRequest dto) throws IOException { @@ -76,13 +80,14 @@ public class InterviewServiceImpl extends ServiceImpl skills = aiService.extractSkillsFromResume(resumeContent); + // ---> 解析AI返回的JSON数据,获取技能列表 <--- - List skills = skillsJson.getList("skills", String.class); - session.setExtractedSkills(skillsJson.toJSONString()); + session.setExtractedSkills(JSONObject.toJSONString(skills)); // 3. 准备面试问题(本地 + AI生成) if (dto.getModel().equals("local")) { @@ -94,11 +99,15 @@ public class InterviewServiceImpl extends ServiceImpl skills) { List progressList = new ArrayList<>(); - JSONObject aiQuestionsJson = aiService.generateQuestionsOfAi( + List aiQuestionRes = aiService.generateQuestionsOfAi( session.getSessionId(), skills, + session.getJobRequirements(), session.getResumeContent(), session.getTotalQuestions() ); - // ---> 解析AI返回的JSON数据,获取问题列表 <--- - JSONArray questions = aiQuestionsJson.getJSONArray("questions"); - if (questions != null) { - questions.forEach(item -> { - JSONObject q = (JSONObject) item; + if (CollectionUtil.isNotEmpty(aiQuestionRes)) { + for (QuestionAiRes.Question aiQuestionRe : aiQuestionRes) { InterviewQuestionProgress progress = new InterviewQuestionProgress(); progress.setSessionId(session.getSessionId()); progress.setQuestionId(0L); // AI生成的问题没有本地ID // ---> 解析单个问题内容 <--- - progress.setQuestionContent(q.getString("content")); + progress.setQuestionContent(aiQuestionRe.content()); progress.setStatus(InterviewQuestionProgress.Status.DEFAULT.name()); progressList.add(progress); - }); + } } // 批量保存问题进度 if (CollectionUtil.isNotEmpty(progressList)) { @@ -158,19 +165,19 @@ public class InterviewServiceImpl extends ServiceImpl questions = aiService.generateQuestionOfLocal( session.getSessionId(), localQuestionDataList, skills, + session.getJobRequirements(), session.getResumeContent(), session.getTotalQuestions() ); - JSONArray questionIds = jsonObject.getJSONArray("question_ids"); - List list = questionIds.toList(Long.class); + Set resQuestionIds = questions.stream().map(QuestionAiRes.Question::id).collect(Collectors.toSet()); // 查询返回的内容 并将其保存为问题进度的相关数据 List questionList = questionService.list( new LambdaQueryWrapper() - .in(Question::getId, list) + .in(Question::getId, resQuestionIds) ); List progressList = new ArrayList<>(); questionList.forEach(q -> { @@ -256,7 +263,8 @@ public class InterviewServiceImpl extends ServiceImpl 解析AI返回的JSON评估结果并存入数据库 <--- - currentProgress.setFeedback(evalResult.getString("feedback")); - currentProgress.setSuggestions(evalResult.getString("suggestions")); - currentProgress.setAiAnswer(evalResult.getString("aiAnswer")); - currentProgress.setScore(evalResult.getBigDecimal("score")); + currentProgress.setFeedback(evaluateAiRes.feedback()); + currentProgress.setSuggestions(evaluateAiRes.suggestions()); + currentProgress.setAiAnswer(evaluateAiRes.aiAnswer()); + currentProgress.setScore(BigDecimal.valueOf(evaluateAiRes.score())); currentProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name()); progressService.updateById(currentProgress); // 4. 将单题评估结果存入 evaluation 表用于分析 - saveEvaluationRecord(currentProgress, evalResult); + saveEvaluationRecord(currentProgress, evaluateAiRes); // 5. ---> 解析AI的是否追问判断,并处理追问逻辑 <--- - if (evalResult.getBooleanValue("continueAsking", false)) { + if (evaluateAiRes.continueAsking()) { // 创建一个新的、状态为ACTIVE的追问问题 InterviewQuestionProgress followUp = new InterviewQuestionProgress(); followUp.setSessionId(currentProgress.getSessionId()); followUp.setQuestionId(0L); // 追问问题没有本地ID - followUp.setQuestionContent(evalResult.getString("followUpQuestion")); + followUp.setQuestionContent(evaluateAiRes.followUpQuestion()); followUp.setStatus(InterviewQuestionProgress.Status.ACTIVE.name()); // 直接设为激活状态,作为下一个问题 progressService.save(followUp); + // 记录追问题目数量 + flowedQuestionCount++; + this.flowedQuestions.put(currentProgress.getSessionId(), flowedQuestionCount); return followUp; // 将这个新的追问问题返回给前端 } + // 清空追问题目数量 + this.flowedQuestions.put(currentProgress.getSessionId(), 0); return currentProgress; } - private void saveEvaluationRecord(InterviewQuestionProgress progress, JSONObject evalResult) { + private void saveEvaluationRecord(InterviewQuestionProgress progress, EvaluateAiRes evaluateAiRes) { InterviewEvaluation evaluation = new InterviewEvaluation(); evaluation.setSessionId(progress.getSessionId()); evaluation.setQuestionId(progress.getQuestionId()); evaluation.setUserAnswer(progress.getUserAnswer()); // ---> 解析AI评估结果并存入分析表 <--- - evaluation.setAiFeedback(evalResult.getString("feedback")); - evaluation.setScore(evalResult.getBigDecimal("score")); + evaluation.setAiFeedback(evaluateAiRes.feedback()); + evaluation.setScore(BigDecimal.valueOf(evaluateAiRes.score())); evaluationMapper.insert(evaluation); } @@ -319,12 +332,12 @@ public class InterviewServiceImpl extends ServiceImpl 解析AI返回的最终报告JSON,更新会话状态 <--- session.setStatus(InterviewSession.Status.COMPLETED.name()); - session.setScore(finalReportJson.getBigDecimal("overallScore")); - session.setFinalReport(finalReportJson.toJSONString()); + session.setScore(new BigDecimal(interviewReportAiRes.overallScore())); + session.setFinalReport(JSONObject.toJSONString(interviewReportAiRes)); this.baseMapper.updateById(session); return session; @@ -398,7 +411,7 @@ public class InterviewServiceImpl extends ServiceImpl i private final QuestionMapper questionMapper; private final QuestionClassificationService classificationService; private final List documentParserList; // This will be injected by Spring - private final LlmService llmService; private final IQuestionCategoryService questionCategoryService; /** @@ -119,30 +117,30 @@ public class QuestionServiceImpl extends ServiceImpl i @Override @Transactional(rollbackFor = Exception.class) public void useAiCheckQuestionData() { - // 查询数据库 - List questions = questionMapper.selectList( - new LambdaQueryWrapper() - .orderByDesc(Question::getCreatedTime) - ); - // 组装prompt - if (CollectionUtil.isEmpty(questions)) { - return; - } - String prompt = getPrompt(questions); - log.info("发送内容: {}", prompt); - // 验证token上下文长度 - Integer promptTokens = llmService.getPromptTokens(prompt); - log.info("当前prompt长度: {}", promptTokens); - String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK); - // 调用AI - log.info("AI返回内容: {}", chat); - JSONObject parse = JSONObject.parse(chat); - JSONArray questionsIds = parse.getJSONArray("questions"); - List list = questionsIds.toList(Long.class); - questionMapper.delete( - new LambdaQueryWrapper() - .notIn(Question::getId, list) - ); +// // 查询数据库 +// List questions = questionMapper.selectList( +// new LambdaQueryWrapper() +// .orderByDesc(Question::getCreatedTime) +// ); +// // 组装prompt +// if (CollectionUtil.isEmpty(questions)) { +// return; +// } +// String prompt = getPrompt(questions); +// log.info("发送内容: {}", prompt); +// // 验证token上下文长度 +// Integer promptTokens = llmService.getPromptTokens(prompt); +// log.info("当前prompt长度: {}", promptTokens); +// String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK); +// // 调用AI +// log.info("AI返回内容: {}", chat); +// JSONObject parse = JSONObject.parse(chat); +// JSONArray questionsIds = parse.getJSONArray("questions"); +// List list = questionsIds.toList(Long.class); +// questionMapper.delete( +// new LambdaQueryWrapper() +// .notIn(Question::getId, list) +// ); } diff --git a/src/main/java/com/qingqiu/interview/service/llm/LlmService.java b/src/main/java/com/qingqiu/interview/service/llm/LlmService.java deleted file mode 100755 index afa630d..0000000 --- a/src/main/java/com/qingqiu/interview/service/llm/LlmService.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.qingqiu.interview.service.llm; - -import com.qingqiu.interview.common.enums.LLMProvider; - -public interface LlmService { - - - /** - * 与模型进行单轮对话 - * @param prompt 提示词 - * @return ai回复 - */ - String chat(String prompt); - String chat(String prompt, LLMProvider provider); - - /** - * 与模型进行多轮对话 - * @param prompt 提示词 - * @param token 会话token - * @return ai回复 - */ - String chat(String prompt, String token); - - /** - * 与模型进行多轮对话 指定模型 - * @param prompt 提示词 - * @param model 模型名称 - * @param token 会话token - * @return ai回复 - */ - String chat(String prompt, String token, LLMProvider provider); - - Integer getPromptTokens(String prompt); -} - diff --git a/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java b/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java deleted file mode 100755 index a7a6ef2..0000000 --- a/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.qingqiu.interview.service.llm.qwen; - -import cn.hutool.core.collection.CollectionUtil; -import com.alibaba.dashscope.aigc.generation.Generation; -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 com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.qingqiu.interview.common.enums.LLMProvider; -import com.qingqiu.interview.ai.factory.AIClientManager; -import com.qingqiu.interview.entity.AiSessionLog; -import com.qingqiu.interview.mapper.AiSessionLogMapper; -import com.qingqiu.interview.service.llm.LlmService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import static com.qingqiu.interview.common.utils.AIUtils.createMessage; - -@Slf4j -@Service("qwenService") -@RequiredArgsConstructor -public class QwenService implements LlmService { - - private final Generation generation; - - private final AiSessionLogMapper aiSessionLogMapper; - - @Value("${dashscope.api-key}") - private String apiKey; - - private final AIClientManager aiClientManager; - - - @Override - public String chat(String prompt) { -// log.info("开始调用API...."); -// long l = System.currentTimeMillis(); - return chat(prompt, LLMProvider.DEEPSEEK); -// GenerationParam param = GenerationParam.builder() -// .model(DEEPSEEK_3) // 可根据需要更换模型 -// .messages(Collections.singletonList(createMessage(Role.USER.getValue(), prompt))) -// .resultFormat(GenerationParam.ResultFormat.MESSAGE) -// .apiKey(apiKey) -// .build(); -// -// GenerationResult result = null; -// try { -// result = generation.call(param); -// log.info("调用成功,耗时: {} ms", System.currentTimeMillis() - l); -// log.debug("响应结果: {}", result.getOutput().getChoices().get(0).getMessage().getContent()); -// return result.getOutput().getChoices().get(0).getMessage().getContent(); -// } catch (ApiException | InputRequiredException e) { -// throw new RuntimeException("调用AI服务失败", e); -// } catch (NoApiKeyException e) { -// throw new RuntimeException("请检查API密钥是否正确", e); -// } - } - - @Override - public String chat(String prompt, LLMProvider provider) { - return aiClientManager.getClient(provider).chatCompletion(prompt); - } - - @Override - public String chat(String prompt, String token) { - return chat(prompt, token, LLMProvider.DEEPSEEK); - -// // 调用AI模型 -// try { -// log.info("开始调用API...."); -// long l = System.currentTimeMillis(); -// GenerationParam param = GenerationParam.builder() -// .model(DEEPSEEK_3_1) // 可根据需要更换模型 -// .messages(messages) -// .resultFormat(GenerationParam.ResultFormat.MESSAGE) -// .apiKey(apiKey) -// .build(); -// -// GenerationResult result = generation.call(param); -// log.info("调用成功,耗时: {} ms", System.currentTimeMillis() - l); -// String aiResponse = result.getOutput().getChoices().get(0).getMessage().getContent(); -// log.debug("响应结果: {}", aiResponse); -// // 存储用户提问 -// AiSessionLog userLog = new AiSessionLog(); -// userLog.setToken(token); -// userLog.setRole(Role.USER.getValue()); -// userLog.setContent(prompt); -// aiSessionLogMapper.insert(userLog); -// -// // 存储AI回复 -// AiSessionLog aiLog = new AiSessionLog(); -// aiLog.setToken(token); -// aiLog.setRole(Role.ASSISTANT.getValue()); -// aiLog.setContent(aiResponse); -// aiSessionLogMapper.insert(aiLog); -// -// return aiResponse; -// } catch (ApiException | NoApiKeyException | InputRequiredException e) { -// throw new RuntimeException("调用AI服务失败", e); -// } - } - - @Override - public String chat(String prompt, String token, LLMProvider provider) { - // 根据token查询会话记录 - List aiSessionLogs = aiSessionLogMapper.selectList( - new LambdaQueryWrapper() - .eq(AiSessionLog::getToken, token) - .orderByDesc(AiSessionLog::getCreatedTime) - ); - // 构造发给ai的消息 - List messages = new ArrayList<>(); - if (CollectionUtil.isNotEmpty(aiSessionLogs)) { - // 预估tokens - StringBuilder sb = new StringBuilder(); - for (AiSessionLog aiSessionLog : aiSessionLogs) { - sb.append(aiSessionLog.getContent()); - } - // 加上本次对话内容 - sb.append(prompt); - Integer promptTokens = getPromptTokens(sb.toString()); - // 如果token大于了模型上限,则执行丢弃操作 - int size = aiSessionLogs.size(); - log.info("当前会话id: {}, tokens: {}", token, promptTokens); - - // 假设模型上限为30000个token(根据实际模型调整) - int maxTokens = 100000; - if (promptTokens > maxTokens) { - // 需要丢弃30%的会话记录(按时间倒序,丢弃最旧的) - int discardCount = (int) (size * 0.3); - // 从当前会话记录列表中移除旧的会话记录,而不是删除数据库中的记录 - for (int i = 0; i < discardCount; i++) { - aiSessionLogs.remove(aiSessionLogs.size() - 1); - } - } - // 移除旧记录后再按时间正序排序(最旧的在前面,最新的在后面) - aiSessionLogs = aiSessionLogs.stream() - .sorted((log1, log2) -> log1.getCreatedTime().compareTo(log2.getCreatedTime())) - .collect(Collectors.toList()); - for (AiSessionLog aiSessionLog : aiSessionLogs) { - messages.add( - createMessage(aiSessionLog.getRole(), aiSessionLog.getContent()) - ); - } - - - } - - messages.add( - createMessage(Role.USER.getValue(), prompt) - ); - - String aiResponse = aiClientManager.getClient(provider).chatCompletion(messages); - // 存储用户提问 - AiSessionLog userLog = new AiSessionLog(); - userLog.setToken(token); - userLog.setRole(Role.USER.getValue()); - userLog.setContent(prompt); - aiSessionLogMapper.insert(userLog); - - // 存储AI回复 - AiSessionLog aiLog = new AiSessionLog(); - aiLog.setToken(token); - aiLog.setRole(Role.ASSISTANT.getValue()); - aiLog.setContent(aiResponse); - aiSessionLogMapper.insert(aiLog); - return aiResponse; - } - - /** - * 获取prompt的token数 - * - * @param prompt 输入 - * @return tokens - */ - @Override - public Integer getPromptTokens(String prompt) { - Tokenizer tokenizer = TokenizerFactory.qwen(); - List integers = tokenizer.encodeOrdinary(prompt); - return integers.size(); - } - -} - diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f2d48f0..6e58163 100755 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,14 +1,42 @@ -dashscope: - api-key: sk-58d6fed688c54e8db02e6a7ffbfc7a5f -deepseek: - api-url: https://api.deepseek.com/chat/completions - api-key: sk-faaa2a1b485442ccbf115ff1271a3480 spring: datasource: url: jdbc:mysql://gz-cynosdbmysql-grp-5ai5zw7r.sql.tencentcdb.com:24944/ai_interview?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 username: qingqiu password: 020979hP driver-class-name: com.mysql.cj.jdbc.Driver + ai: + openai: + base-url: https://api.ruyun.fun + api-key: ${RUYUN_API_KEY} + chat: + options: + model: gemini-2.5-flash-nothinking + dashscope: + api-key: ${DASHSCOPE_API_KEY} + read-timeout: 600 + chat: + options: + model: qwen3-max + memory: + redis: + host: 127.0.0.1 + port: 6379 + password: 123456 + timeout: 6000 + data: + redis: + host: localhost + port: 6379 + password: 123456 + database: 0 + timeout: 6000 + jedis: + pool: + max-active: 16 + max-idle: 8 + min-idle: 0 + max-wait: -1ms + # ai: # openai: # api-key: sk-faaa2a1b485442ccbf115ff1271a3480