使用spring-ai-alibaba重构项目

This commit is contained in:
2025-10-10 18:03:16 +08:00
parent 5711d611f2
commit fac1346104
35 changed files with 1359 additions and 904 deletions

123
pom.xml
View File

@@ -5,7 +5,7 @@
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version> <version>3.4.5</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>com.qingqiu</groupId> <groupId>com.qingqiu</groupId>
@@ -28,6 +28,9 @@
</scm> </scm>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version>
<spring-boot.version>3.4.5</spring-boot.version>
</properties> </properties>
<dependencies> <dependencies>
<!-- MyBatis-Plus --> <!-- MyBatis-Plus -->
@@ -53,26 +56,56 @@
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient --> <artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
</dependency> </dependency>
<!-- &lt;!&ndash; Spring AI Dependencies &ndash;&gt;-->
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>--> <!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>--> <!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>-->
<!-- <exclusions>--> <!-- <version>1.0.0-SNAPSHOT</version>-->
<!-- <exclusion>-->
<!-- <groupId>com.alibaba.cloud.ai</groupId>-->
<!-- <artifactId>spring-ai-alibaba-autoconfigure</artifactId>-->
<!-- </exclusion>-->
<!-- <exclusion>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-core</artifactId>-->
<!-- </exclusion>-->
<!-- </exclusions>-->
<!-- </dependency>--> <!-- </dependency>-->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>dashscope-sdk-java</artifactId> <artifactId>spring-ai-starter-model-openai</artifactId>
<version>2.21.5</version>
</dependency> </dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.2.0</version>
</dependency>
<!-- 集成redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>com.alibaba</groupId>-->
<!-- <artifactId>dashscope-sdk-java</artifactId>-->
<!-- <version>2.21.5</version>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all --> <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
@@ -129,16 +162,28 @@
</dependency> </dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>
<!-- <dependencies>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-bom</artifactId>-->
<!-- <version>1.0.0-M5</version> &lt;!&ndash; 或最新版本 &ndash;&gt;-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
<!-- </dependencies>-->
<dependencies> <dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId> <artifactId>mybatis-plus-bom</artifactId>
@@ -147,6 +192,7 @@
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
<build> <build>
@@ -167,33 +213,6 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project> </project>

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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<LLMProvider, AIClientFactory> factories;
public AIClientManager(List<AIClientFactory> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Message> messages) {
return null;
}
}

View File

@@ -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<Message> messages) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("type", "json_object");
Map<String, Object> 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;
}
}

View File

@@ -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<Message> 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);
}
}
}

View File

@@ -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;
}

View File

@@ -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<Integer> integers = tokenizer.encodeOrdinary(prompt);
return integers.size();
}
}

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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)
));
}
}

View File

@@ -0,0 +1,8 @@
package com.qingqiu.interview.common.utils;
public class UUIDUtils {
public static String getUUID() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
}

View File

@@ -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();
}
}

View File

@@ -1,14 +1,217 @@
package com.qingqiu.interview.config; 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.Bean;
import org.springframework.context.annotation.Configuration; 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 @Configuration
public class DashScopeConfig { 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 @Bean
public Generation generation() { public DashScopeApi dashScopeApi() {
return new Generation(); 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))
)
);
}
}

View File

@@ -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<T> implements RedisSerializer<T> {
// 默认编码
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
// 泛型类型,用于反序列化
private final Class<T> clazz;
public Fastjson2RedisSerializer(Class<T> 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);
}
}
}

View File

@@ -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<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// 使用Fastjson2RedisSerializer来序列化和反序列化redis的value值
Fastjson2RedisSerializer<Object> fastjson2RedisSerializer = new Fastjson2RedisSerializer<>(Object.class);
template.setValueSerializer(fastjson2RedisSerializer);
template.setHashValueSerializer(fastjson2RedisSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

@@ -1,13 +1,14 @@
package com.qingqiu.interview.controller; package com.qingqiu.interview.controller;
import com.alibaba.dashscope.common.Role;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qingqiu.interview.common.res.R; import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.entity.AiSessionLog; import com.qingqiu.interview.entity.AiSessionLog;
import com.qingqiu.interview.service.IAiSessionLogService; import com.qingqiu.interview.service.IAiSessionLogService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -38,7 +39,7 @@ public class AiSessionLogController {
return R.success(service.list( return R.success(service.list(
new LambdaQueryWrapper<AiSessionLog>() new LambdaQueryWrapper<AiSessionLog>()
.eq(AiSessionLog::getToken, sessionId) .eq(AiSessionLog::getToken, sessionId)
.ne(AiSessionLog::getRole, Role.SYSTEM.getValue()) .ne(AiSessionLog::getRole, MessageType.SYSTEM.getValue())
)); ));
} }

View File

@@ -25,5 +25,10 @@ public class InterviewStartRequest {
/** 生成的面试题目数量 */ /** 生成的面试题目数量 */
private Integer totalQuestions = 10; private Integer totalQuestions = 10;
/**
* 岗位要求
*/
private String jobRequirements;
// 简历文件通过MultipartFile单独传递 // 简历文件通过MultipartFile单独传递
} }

View File

@@ -31,6 +31,9 @@ public class InterviewSession implements Serializable {
@TableField("resume_content") @TableField("resume_content")
private String resumeContent; private String resumeContent;
@TableField("job_requirements")
private String jobRequirements;
@TableField("extracted_skills") @TableField("extracted_skills")
private String extractedSkills; private String extractedSkills;
@TableField("interview_type") @TableField("interview_type")

View File

@@ -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) {
}

View File

@@ -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<String> skills
) {
}

View File

@@ -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<String, String> technicalAssessment,
@JsonProperty("strengthsAndWeaknesses") StrengthsAndWeaknesses strengthsAndWeaknesses,
@JsonProperty("suggestions") List<String> suggestions,
@JsonProperty("hiringRecommendation") String hiringRecommendation,
@JsonProperty("hiringReason") String hiringReason
) {
/**
* 内部 Record对应 "strengthsAndWeaknesses" 对象。
*/
public record StrengthsAndWeaknesses(
@JsonProperty("strengths") List<String> strengths,
@JsonProperty("weaknesses") List<String> weaknesses
) {}
}

View File

@@ -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<Question> questions
) {}
}

View File

@@ -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;
/**
* <h1></h1>
*
* @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);
}

View File

@@ -1,15 +1,17 @@
package com.qingqiu.interview.service; package com.qingqiu.interview.service;
import com.alibaba.fastjson2.JSONObject;
import com.qingqiu.interview.entity.InterviewQuestionProgress; import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession; import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.entity.Question; 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; import java.util.List;
/** /**
* <h1> * <h1>
* 面试接入AI的接口 * 面试接入AI的接口
* </h1> * </h1>
* *
* @author qingqiu * @author qingqiu
@@ -23,38 +25,38 @@ public interface InterviewAiService {
* @param resumeContent 简历文本 * @param resumeContent 简历文本
* @return 包含技能列表的JSON对象 * @return 包含技能列表的JSON对象
*/ */
JSONObject extractSkillsFromResume(String resumeContent); List<String> extractSkillsFromResume(String resumeContent);
/** /**
* 根据技能动态生成面试题目 * 根据技能动态生成面试题目
* *
* @param skills 技能列表 * @param skills 技能列表
* @param resumeContent 简历内容 * @param resumeContent 简历内容
* @param count 需要生成的题目数量 * @param count 需要生成的题目数量
* @return 包含问题列表的JSON对象 * @return 包含问题列表的JSON对象
*/ */
JSONObject generateQuestionsOfAi(String sessionId, List<String> skills, String resumeContent, int count); List<QuestionAiRes.Question> generateQuestionsOfAi(String sessionId, List<String> skills, String jobRequirements, String resumeContent, int count);
JSONObject generateQuestionOfLocal(String sessionId, List<Question> questions, List<String> skills, String resumeContent, int count); List<QuestionAiRes.Question> generateQuestionOfLocal(String sessionId, List<Question> questions, List<String> skills, String jobRequirements, String resumeContent, int count);
/** /**
* 评估用户的回答 * 评估用户的回答
* *
* @param question 问题内容 * @param question 问题内容
* @param userAnswer 用户的回答 * @param userAnswer 用户的回答
* @param context 可选的上下文(之前的问答历史) * @param context 可选的上下文(之前的问答历史)
* @return 包含评估结果的JSON对象 * @return 包含评估结果的JSON对象
*/ */
JSONObject evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context); EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context);
/** /**
* 生成最终的面试评估报告 * 生成最终的面试评估报告
* *
* @param session 面试会话信息 * @param session 面试会话信息
* @param progressList 整个面试的问答记录 * @param progressList 整个面试的问答记录
* @return 包含最终报告的JSON对象 * @return 包含最终报告的JSON对象
*/ */
JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList); InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList);
String generateFirstQuestion(String sessionId, String candidateName, String questionContent); String generateFirstQuestion(String sessionId, String candidateName, String questionContent);

View File

@@ -3,10 +3,14 @@ package com.qingqiu.interview.service;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; 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.entity.Question;
import com.qingqiu.interview.service.llm.LlmService; import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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 org.springframework.stereotype.Service;
import java.util.ArrayList; import java.util.ArrayList;
@@ -17,10 +21,11 @@ import java.util.List;
@RequiredArgsConstructor @RequiredArgsConstructor
public class QuestionClassificationService { public class QuestionClassificationService {
private final LlmService llmService;
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private final ChatClient chatClient;
/** /**
* 使用AI对题目进行分类 * 使用AI对题目进行分类
*/ */
@@ -28,10 +33,14 @@ public class QuestionClassificationService {
log.info("开始使用AI分类题目内容长度: {}", rawContent.length()); log.info("开始使用AI分类题目内容长度: {}", rawContent.length());
String prompt = buildClassificationPrompt(rawContent); String prompt = buildClassificationPrompt(rawContent);
String aiResponse = llmService.chat(prompt); String aiResponse = chatClient.prompt()
.user(prompt)
.call()
.content();
log.info("AI分类响应: {}", aiResponse); log.info("AI分类响应: {}", aiResponse);
assert aiResponse != null;
return parseAiResponse(aiResponse); return parseAiResponse(aiResponse);
} }

View File

@@ -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;
/**
* <h1></h1>
*
* @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<Message> messages = new ArrayList<>();
AtomicInteger tokens = new AtomicInteger();
// 如果会话id不为空 则从数据库中获取会话记录
if (dto.getSessionId() != null) {
List<AiSessionLog> list = aiSessionLogService.list(
new LambdaQueryWrapper<AiSessionLog>()
.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<Message> 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 "";
}
}

View File

@@ -1,24 +1,29 @@
package com.qingqiu.interview.service.impl; 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.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.qingqiu.interview.common.constants.CommonConstant; import com.qingqiu.interview.common.constants.ChatConstant;
import com.qingqiu.interview.dto.ChatDTO; import com.qingqiu.interview.common.utils.PromptTemplateUtils;
import com.qingqiu.interview.entity.InterviewQuestionProgress; import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession; import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.entity.Question; 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.service.InterviewAiService;
import com.qingqiu.interview.vo.ChatVO; import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Lazy; 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 org.springframework.stereotype.Service;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.Map;
import java.util.UUID;
/** /**
* <h1></h1> * <h1></h1>
@@ -28,114 +33,163 @@ import java.util.stream.Collectors;
*/ */
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class InterviewAiServiceImpl implements InterviewAiService { 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 @Override
public JSONObject extractSkillsFromResume(String resumeContent) { public List<String> extractSkillsFromResume(String resumeContent) {
String prompt = "你是一位资深的IT技术招聘专家。" + ExtractSkillAiRes entity = chatClient
"请仔细阅读以下简历内容,并提取出其中所有的关键技术技能。" + .prompt(PromptTemplateUtils.getExtractSkillsPrompt(resumeContent))
"请严格按照以下JSON格式返回不要添加任何额外的解释或说明\n" + .call()
"{\"skills\": [\"技能1\", \"技能2\", \"...\"]}\n\n" + .entity(ExtractSkillAiRes.class);
"简历内容如下:\n" + resumeContent; assert entity != null;
return entity.skills();
ChatDTO chatDTO = new ChatDTO()
.setContent(prompt)
.setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO);
return JSONObject.parse(chatVO.getContent());
} }
@Override @Override
public JSONObject generateQuestionsOfAi(String sessionId, List<String> skills, String resumeContent, int count) { public List<QuestionAiRes.Question> generateQuestionsOfAi(String sessionId,
String skillsStr = String.join(", ", skills); List<String> skills,
String prompt = String.format( String jobRequirements,
"你是一位专业的软件开发岗位技术面试官。" + String resumeContent,
"请根据候选人的以下技术栈、项目经历、简历内容,生成 %d 道有深度和广度的面试题。" + int count) {
"题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。" + // String skillsStr = String.join(", ", skills);
"请严格按照以下JSON格式返回question数组中必须包含 %d 个问题对象:\n" + // String prompt = String.format(
"{\"questions\": [{\"id\": \"ai-gen-1\", \"content\": \"问题1内容...\"}, {\"id\": \"ai-gen-2\", \"content\": \"问题2内容...\"}]}\n\n" + // "你是一位专业的软件开发岗位技术面试官。" +
"候选人技术栈:%s\n" + // "请根据候选人的以下技术栈、项目经历、简历内容,生成 %d 道有深度和广度的面试题。" +
"候选人简历:%s", // "题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。" +
count, count, skillsStr, resumeContent // "请严格按照以下JSON格式返回question数组中必须包含 %d 个问题对象:\n" +
); // "{\"questions\": [{\"id\": \"ai-gen-1\", \"content\": \"问题1内容...\"}, {\"id\": \"ai-gen-2\", \"content\": \"问题2内容...\"}]}\n\n" +
// "候选人技术栈:%s\n" +
ChatDTO chatDTO = new ChatDTO() // "候选人简历:%s",
.setSessionId(sessionId) // count, count, skillsStr, resumeContent
.setContent(prompt) // );
.setRole(Role.SYSTEM.getValue()) Map<String, Object> params = MapUtil.<String, Object>builder()
.setDataType(CommonConstant.ONE); .put("count", count)
ChatVO chatVO = chatService.createChat(chatDTO); .put("jobRequirements", jobRequirements)
return JSON.parseObject(chatVO.getContent()); .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 @Override
public JSONObject generateQuestionOfLocal(String sessionId, List<Question> questions, List<String> skills, String resumeContent, int count) { public List<QuestionAiRes.Question> generateQuestionOfLocal(String sessionId,
String skillsStr = String.join(", ", skills); List<Question> questions,
// 2. 构建发送给AI的提示 List<String> skills,
String prompt = String.format(""" String jobRequirements,
你是一位专业的面试官。请根据以下候选人的技术栈、项目经历、简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。 String resumeContent,
题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。 int count) {
要求: Map<String, Object> params = MapUtil.<String, Object>builder()
1. 题目必须严格从【题库JSON】中选择。 .put("count", count)
2. 挑选的题目应根据候选人的简历内容来抽取。 .put("jobRequirements", jobRequirements)
3. 返回一个只包含所选题目ID的JSON数组格式为{"question_ids": [1, 5, 23, ...]}。 .put("skills", JSONObject.toJSONString(skills))
4. 不要返回任何多余的代码包括markdown形式的代码我只需要JSON对象请严格按照api接口形式返回 .put("resume", resumeContent)
5. 不要返回任何额外的解释或文字只返回JSON对象。 .build();
6. 严格按照前后端分离的接口形式返回JSON数据给我不要返回"```json```"
7. 请保证返回数据的完整性不要返回不完整的数据否则我的JSON解析会报错 Prompt aiInterviewerPrompt = PromptTemplateUtils.getLocalInterviewPrompt(params);
QuestionAiRes.Wrapper entity = openAiChatClient.prompt(aiInterviewerPrompt)
【候选人技术栈】: .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId))
%s .call()
【候选人简历】: .entity(QuestionAiRes.Wrapper.class);
[%s] assert entity != null;
【题库JSON】: return entity.questions();
%s // String skillsStr = String.join(", ", skills);
""", count, skillsStr, resumeContent, JSONObject.toJSONString(questions)); // // 2. 构建发送给AI的提示
ChatDTO chatDTO = new ChatDTO() // String prompt = String.format("""
.setSessionId(sessionId) // 你是一位专业的面试官。请根据以下候选人的技术栈、项目经历、简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。
.setContent(prompt) // 题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。
.setRole(Role.SYSTEM.getValue()) // 要求:
.setDataType(CommonConstant.ONE); // 1. 题目必须严格从【题库JSON】中选择。
ChatVO chatVO = chatService.createChat(chatDTO); // 2. 挑选的题目应根据候选人的简历内容来抽取。
return JSON.parseObject(chatVO.getContent()); // 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 @Override
public JSONObject evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context) { public EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context) {
// 构建上下文历史 // 构建上下文历史
String history = context.stream() // String history = context.stream()
.map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer())) // .map(p -> String.format("Q: %s\nA: %s", p.getQuestionContent(), p.getUserAnswer()))
.collect(Collectors.joining("\n---\n")); // .collect(Collectors.joining("\n---\n"));
//
String prompt = "你是一位资深的技术面试官,以严格和深入著称。" + // String prompt = "你是一位资深的技术面试官,以严格和深入著称。" +
"你需要评估候选人对以下问题的回答。请注意:\n" + // "你需要评估候选人对以下问题的回答。请注意:\n" +
"1. 如果回答模糊、不完整或有错误你可以提出一个具体的追问问题followUpQuestion来深入考察此时'continueAsking'应为true。\n" + // "1. 如果回答模糊、不完整或有错误你可以提出一个具体的追问问题followUpQuestion来深入考察此时'continueAsking'应为true。\n" +
"2. 如果回答得很好,则'continueAsking'为false'followUpQuestion'为空字符串。\n" + // "2. 如果回答得很好,则'continueAsking'为false'followUpQuestion'为空字符串。\n" +
"3. 'score'范围为0-100分。\n" + // "3. 'score'范围为0-100分。\n" +
"4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" + // "4. 'feedback'和'suggestions'需要给出专业、有建设性的意见。\n" +
"5. 追问最好有限制不要无限制的向下追问注意追问是支线而非主线追问至多3个问题之后必须切回主线\n" + // "5. 追问最好有限制不要无限制的向下追问注意追问是支线而非主线追问至多3个问题之后必须切回主线\n" +
"请严格按照以下JSON格式返回不要有任何额外说明\n" + // "请严格按照以下JSON格式返回不要有任何额外说明\n" +
"{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" + // "{\"feedback\": \"...\", \"suggestions\": \"...\", \"aiAnswer\": \"...\", \"score\": 85.5, \"continueAsking\": false, \"followUpQuestion\": \"...\"}\n\n" +
"面试历史上下文:\n" + history + "\n\n" + // "面试历史上下文:\n" + history + "\n\n" +
"当前问题:\n" + question + "\n\n" + // "当前问题:\n" + question + "\n\n" +
"候选人回答:\n" + userAnswer; // "候选人回答:\n" + userAnswer;
//
ChatDTO chatDTO = new ChatDTO() // ChatDTO chatDTO = new ChatDTO()
.setSessionId(sessionId) // .setSessionId(sessionId)
.setContent(prompt) // .setContent(prompt)
.setRole(Role.SYSTEM.getValue()) // .setRole(Role.SYSTEM.getValue())
.setDataType(CommonConstant.ONE); // .setDataType(CommonConstant.ONE);
ChatVO chatVO = chatService.createChat(chatDTO); // ChatVO chatVO = chatService.createChat(chatDTO);
return JSON.parseObject(chatVO.getContent()); Map<String, Object> params = MapUtil.<String, Object>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 @Override
public JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) { public InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) {
// String transcript = progressList.stream() // String transcript = progressList.stream()
// .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n", // .map(p -> String.format("问题: %s\n回答: %s\nAI评分: %.1f\nAI反馈: %s\n",
// p.getQuestionContent(), p.getUserAnswer(), p.getScore(), p.getFeedback())) // 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" + // "{\"overallScore\": 88.0, \"summary\": \"...\", \"strengths\": [\"...\"], \"weaknesses\": [\"...\"]}\n\n" +
// "候选人姓名:" + session.getCandidateName() + "\n" + // "候选人姓名:" + session.getCandidateName() + "\n" +
// "面试完整记录:\n" + transcript; // "面试完整记录:\n" + transcript;
// ChatDTO chatDTO = new ChatDTO()
String prompt = buildFinalReportPrompt(session, progressList); // .setRole(Role.SYSTEM.getValue())
// .setDataType(CommonConstant.ONE)
ChatDTO chatDTO = new ChatDTO() // .setContent(prompt);
.setRole(Role.SYSTEM.getValue()) // ChatVO chatVO = chatService.createChat(chatDTO);
.setDataType(CommonConstant.ONE) Map<String, Object> params = getFinalReportParams(session, progressList);
.setContent(prompt); Prompt prompt = PromptTemplateUtils.getFinalReportPrompt(params);
ChatVO chatVO = chatService.createChat(chatDTO); return openAiChatClient.prompt(prompt)
return JSON.parseObject(chatVO.getContent()); .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, UUID.randomUUID().toString().replace("-", "")))
.call()
.entity(InterviewReportAiRes.class);
} }
private String buildFinalReportPrompt(InterviewSession session, List<InterviewQuestionProgress> progressList) { @NotNull
private static Map<String, Object> getFinalReportParams(InterviewSession session, List<InterviewQuestionProgress> progressList) {
StringBuilder historyBuilder = new StringBuilder(); StringBuilder historyBuilder = new StringBuilder();
for (InterviewQuestionProgress progress : progressList) { for (InterviewQuestionProgress progress : progressList) {
historyBuilder.append( historyBuilder.append(
String.format("\n【问题】: %s\n【回答】: %s\n【AI单题反馈】: %s\n【AI单题建议】: %s\n【AI单题评分】: %s/5.0\n", 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.getUserAnswer(),
progress.getFeedback(), progress.getFeedback(),
progress.getSuggestions(), progress.getSuggestions(),
@@ -172,6 +229,27 @@ public class InterviewAiServiceImpl implements InterviewAiService {
) )
); );
} }
return MapUtil.<String, Object>builder()
.put("resume", session.getResumeContent())
.put("history", historyBuilder.toString())
.build();
}
/*
private String buildFinalReportPrompt(InterviewSession session, List<InterviewQuestionProgress> 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(""" return String.format("""
你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估给出一份全面、专业、有深度的最终面试报告。 你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估给出一份全面、专业、有深度的最终面试报告。
@@ -206,22 +284,36 @@ public class InterviewAiServiceImpl implements InterviewAiService {
%s %s
""", session.getResumeContent(), historyBuilder.toString()); """, session.getResumeContent(), historyBuilder.toString());
} }
*/
@Override @Override
public String generateFirstQuestion(String sessionId, String candidateName, String questionContent) { 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(""" String prompt = String.format("""
你是一位专业的技术面试官。现在要开始面试,候选人是 %s。 你是一位专业的技术面试官。现在要开始面试,候选人是 %s。
\s
第一个问题是:%s 第一个问题是:%s
\s
请以友好但专业的语气提出这个问题,可以适当添加一些引导性的话语。 请以友好但专业的语气提出这个问题,可以适当添加一些引导性的话语。
""", candidateName, questionContent); 严格按照JSON格式输出不得包含任何markdown标记或额外解释
ChatDTO chatDTO = new ChatDTO() 请返回JSON格式的数据:\s
.setSessionId(sessionId) {
.setRole(Role.SYSTEM.getValue()) "content": "xxx"
.setDataType(CommonConstant.ONE) }
.setContent(prompt); \s""", candidateName, questionContent);
ChatVO chatVO = chatService.createChat(chatDTO); String content = openAiChatClient.prompt()
return chatVO.getContent(); .user(prompt)
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId))
.call()
.content();
return JSON.parseObject(content).getString("content");
} }
} }

View File

@@ -37,9 +37,10 @@ public class InterviewQuestionProgressServiceImpl extends ServiceImpl<InterviewQ
InterviewQuestionProgress.Status.COMPLETED.name() InterviewQuestionProgress.Status.COMPLETED.name()
) )
) )
.orderByDesc(InterviewQuestionProgress::getCreatedTime)
.orderByAsc(InterviewQuestionProgress::getStatus) .orderByAsc(InterviewQuestionProgress::getStatus)
.orderByDesc(InterviewQuestionProgress::getUpdatedTime) .orderByDesc(InterviewQuestionProgress::getUpdatedTime)
.orderByDesc(InterviewQuestionProgress::getCreatedTime)
); );
} }

View File

@@ -12,6 +12,9 @@ import com.qingqiu.interview.dto.InterviewReportResponse;
import com.qingqiu.interview.dto.InterviewStartRequest; import com.qingqiu.interview.dto.InterviewStartRequest;
import com.qingqiu.interview.dto.SubmitAnswerDTO; import com.qingqiu.interview.dto.SubmitAnswerDTO;
import com.qingqiu.interview.entity.*; import com.qingqiu.interview.entity.*;
import com.qingqiu.interview.entity.ai.EvaluateAiRes;
import com.qingqiu.interview.entity.ai.InterviewReportAiRes;
import com.qingqiu.interview.entity.ai.QuestionAiRes;
import com.qingqiu.interview.mapper.InterviewEvaluationMapper; import com.qingqiu.interview.mapper.InterviewEvaluationMapper;
import com.qingqiu.interview.mapper.InterviewMessageMapper; import com.qingqiu.interview.mapper.InterviewMessageMapper;
import com.qingqiu.interview.mapper.InterviewSessionMapper; import com.qingqiu.interview.mapper.InterviewSessionMapper;
@@ -32,10 +35,9 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.math.BigDecimal;
import java.util.List; import java.util.*;
import java.util.Objects; import java.util.concurrent.ConcurrentHashMap;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@@ -61,6 +63,8 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
private final DocumentParserManager documentParserManager; private final DocumentParserManager documentParserManager;
private final Map<String, Integer> flowedQuestions = new ConcurrentHashMap<>();
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public InterviewSession startInterview(MultipartFile file, InterviewStartRequest dto) throws IOException { public InterviewSession startInterview(MultipartFile file, InterviewStartRequest dto) throws IOException {
@@ -76,13 +80,14 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
session.setStatus(InterviewSession.Status.ACTIVE.name()); session.setStatus(InterviewSession.Status.ACTIVE.name());
session.setTotalQuestions(dto.getTotalQuestions()); session.setTotalQuestions(dto.getTotalQuestions());
session.setModel(dto.getModel()); session.setModel(dto.getModel());
session.setJobRequirements(dto.getJobRequirements());
this.baseMapper.insert(session); // 先插入以获取ID this.baseMapper.insert(session); // 先插入以获取ID
// 2. 调用AI服务从简历提取技能 // 2. 调用AI服务从简历提取技能
JSONObject skillsJson = aiService.extractSkillsFromResume(resumeContent); List<String> skills = aiService.extractSkillsFromResume(resumeContent);
// ---> 解析AI返回的JSON数据获取技能列表 <--- // ---> 解析AI返回的JSON数据获取技能列表 <---
List<String> skills = skillsJson.getList("skills", String.class); session.setExtractedSkills(JSONObject.toJSONString(skills));
session.setExtractedSkills(skillsJson.toJSONString());
// 3. 准备面试问题(本地 + AI生成 // 3. 准备面试问题(本地 + AI生成
if (dto.getModel().equals("local")) { if (dto.getModel().equals("local")) {
@@ -94,11 +99,15 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
// 4. 更新会话信息 // 4. 更新会话信息
this.baseMapper.updateById(session); this.baseMapper.updateById(session);
InterviewQuestionProgress nextQuestion = progressService.getNextQuestion(sessionId); InterviewQuestionProgress nextQuestion = progressService.getNextQuestion(sessionId);
aiService.generateFirstQuestion(session.getSessionId(), session.getCandidateName(), nextQuestion.getQuestionContent()); String aiRes = aiService.generateFirstQuestion(session.getSessionId(),
session.getCandidateName(),
nextQuestion.getQuestionContent()
);
saveMessage(sessionId, saveMessage(sessionId,
InterviewMessage.MessageType.QUESTION.name(), InterviewMessage.MessageType.QUESTION.name(),
InterviewMessage.Sender.AI.name(), InterviewMessage.Sender.AI.name(),
nextQuestion.getQuestionContent(), // nextQuestion.getQuestionContent(),
aiRes,
nextQuestion.getId() nextQuestion.getId()
); );
return session; return session;
@@ -107,25 +116,23 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
private void aiGenerateQuestions(InterviewSession session, List<String> skills) { private void aiGenerateQuestions(InterviewSession session, List<String> skills) {
List<InterviewQuestionProgress> progressList = new ArrayList<>(); List<InterviewQuestionProgress> progressList = new ArrayList<>();
JSONObject aiQuestionsJson = aiService.generateQuestionsOfAi( List<QuestionAiRes.Question> aiQuestionRes = aiService.generateQuestionsOfAi(
session.getSessionId(), session.getSessionId(),
skills, skills,
session.getJobRequirements(),
session.getResumeContent(), session.getResumeContent(),
session.getTotalQuestions() session.getTotalQuestions()
); );
// ---> 解析AI返回的JSON数据获取问题列表 <--- if (CollectionUtil.isNotEmpty(aiQuestionRes)) {
JSONArray questions = aiQuestionsJson.getJSONArray("questions"); for (QuestionAiRes.Question aiQuestionRe : aiQuestionRes) {
if (questions != null) {
questions.forEach(item -> {
JSONObject q = (JSONObject) item;
InterviewQuestionProgress progress = new InterviewQuestionProgress(); InterviewQuestionProgress progress = new InterviewQuestionProgress();
progress.setSessionId(session.getSessionId()); progress.setSessionId(session.getSessionId());
progress.setQuestionId(0L); // AI生成的问题没有本地ID progress.setQuestionId(0L); // AI生成的问题没有本地ID
// ---> 解析单个问题内容 <--- // ---> 解析单个问题内容 <---
progress.setQuestionContent(q.getString("content")); progress.setQuestionContent(aiQuestionRe.content());
progress.setStatus(InterviewQuestionProgress.Status.DEFAULT.name()); progress.setStatus(InterviewQuestionProgress.Status.DEFAULT.name());
progressList.add(progress); progressList.add(progress);
}); }
} }
// 批量保存问题进度 // 批量保存问题进度
if (CollectionUtil.isNotEmpty(progressList)) { if (CollectionUtil.isNotEmpty(progressList)) {
@@ -158,19 +165,19 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
); );
} }
// ai调用返回的内容进行提取 // ai调用返回的内容进行提取
JSONObject jsonObject = aiService.generateQuestionOfLocal( List<QuestionAiRes.Question> questions = aiService.generateQuestionOfLocal(
session.getSessionId(), session.getSessionId(),
localQuestionDataList, localQuestionDataList,
skills, skills,
session.getJobRequirements(),
session.getResumeContent(), session.getResumeContent(),
session.getTotalQuestions() session.getTotalQuestions()
); );
JSONArray questionIds = jsonObject.getJSONArray("question_ids"); Set<String> resQuestionIds = questions.stream().map(QuestionAiRes.Question::id).collect(Collectors.toSet());
List<Long> list = questionIds.toList(Long.class);
// 查询返回的内容 并将其保存为问题进度的相关数据 // 查询返回的内容 并将其保存为问题进度的相关数据
List<Question> questionList = questionService.list( List<Question> questionList = questionService.list(
new LambdaQueryWrapper<Question>() new LambdaQueryWrapper<Question>()
.in(Question::getId, list) .in(Question::getId, resQuestionIds)
); );
List<InterviewQuestionProgress> progressList = new ArrayList<>(); List<InterviewQuestionProgress> progressList = new ArrayList<>();
questionList.forEach(q -> { questionList.forEach(q -> {
@@ -256,7 +263,8 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name()) .eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.COMPLETED.name())
.orderByAsc(InterviewQuestionProgress::getId) .orderByAsc(InterviewQuestionProgress::getId)
); );
JSONObject evalResult = aiService.evaluateAnswer( Integer flowedQuestionCount = this.flowedQuestions.getOrDefault(currentProgress.getSessionId(), 0);
EvaluateAiRes evaluateAiRes = aiService.evaluateAnswer(
currentProgress.getSessionId(), currentProgress.getSessionId(),
currentProgress.getQuestionContent(), currentProgress.getQuestionContent(),
dto.getAnswer(), dto.getAnswer(),
@@ -264,40 +272,45 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
); );
// 3. ---> 解析AI返回的JSON评估结果并存入数据库 <--- // 3. ---> 解析AI返回的JSON评估结果并存入数据库 <---
currentProgress.setFeedback(evalResult.getString("feedback")); currentProgress.setFeedback(evaluateAiRes.feedback());
currentProgress.setSuggestions(evalResult.getString("suggestions")); currentProgress.setSuggestions(evaluateAiRes.suggestions());
currentProgress.setAiAnswer(evalResult.getString("aiAnswer")); currentProgress.setAiAnswer(evaluateAiRes.aiAnswer());
currentProgress.setScore(evalResult.getBigDecimal("score")); currentProgress.setScore(BigDecimal.valueOf(evaluateAiRes.score()));
currentProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name()); currentProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name());
progressService.updateById(currentProgress); progressService.updateById(currentProgress);
// 4. 将单题评估结果存入 evaluation 表用于分析 // 4. 将单题评估结果存入 evaluation 表用于分析
saveEvaluationRecord(currentProgress, evalResult); saveEvaluationRecord(currentProgress, evaluateAiRes);
// 5. ---> 解析AI的是否追问判断并处理追问逻辑 <--- // 5. ---> 解析AI的是否追问判断并处理追问逻辑 <---
if (evalResult.getBooleanValue("continueAsking", false)) { if (evaluateAiRes.continueAsking()) {
// 创建一个新的、状态为ACTIVE的追问问题 // 创建一个新的、状态为ACTIVE的追问问题
InterviewQuestionProgress followUp = new InterviewQuestionProgress(); InterviewQuestionProgress followUp = new InterviewQuestionProgress();
followUp.setSessionId(currentProgress.getSessionId()); followUp.setSessionId(currentProgress.getSessionId());
followUp.setQuestionId(0L); // 追问问题没有本地ID followUp.setQuestionId(0L); // 追问问题没有本地ID
followUp.setQuestionContent(evalResult.getString("followUpQuestion")); followUp.setQuestionContent(evaluateAiRes.followUpQuestion());
followUp.setStatus(InterviewQuestionProgress.Status.ACTIVE.name()); // 直接设为激活状态,作为下一个问题 followUp.setStatus(InterviewQuestionProgress.Status.ACTIVE.name()); // 直接设为激活状态,作为下一个问题
progressService.save(followUp); progressService.save(followUp);
// 记录追问题目数量
flowedQuestionCount++;
this.flowedQuestions.put(currentProgress.getSessionId(), flowedQuestionCount);
return followUp; // 将这个新的追问问题返回给前端 return followUp; // 将这个新的追问问题返回给前端
} }
// 清空追问题目数量
this.flowedQuestions.put(currentProgress.getSessionId(), 0);
return currentProgress; return currentProgress;
} }
private void saveEvaluationRecord(InterviewQuestionProgress progress, JSONObject evalResult) { private void saveEvaluationRecord(InterviewQuestionProgress progress, EvaluateAiRes evaluateAiRes) {
InterviewEvaluation evaluation = new InterviewEvaluation(); InterviewEvaluation evaluation = new InterviewEvaluation();
evaluation.setSessionId(progress.getSessionId()); evaluation.setSessionId(progress.getSessionId());
evaluation.setQuestionId(progress.getQuestionId()); evaluation.setQuestionId(progress.getQuestionId());
evaluation.setUserAnswer(progress.getUserAnswer()); evaluation.setUserAnswer(progress.getUserAnswer());
// ---> 解析AI评估结果并存入分析表 <--- // ---> 解析AI评估结果并存入分析表 <---
evaluation.setAiFeedback(evalResult.getString("feedback")); evaluation.setAiFeedback(evaluateAiRes.feedback());
evaluation.setScore(evalResult.getBigDecimal("score")); evaluation.setScore(BigDecimal.valueOf(evaluateAiRes.score()));
evaluationMapper.insert(evaluation); evaluationMapper.insert(evaluation);
} }
@@ -319,12 +332,12 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
} }
// 2. 调用AI服务生成最终报告 // 2. 调用AI服务生成最终报告
JSONObject finalReportJson = aiService.generateFinalReport(session, completedProgresses); InterviewReportAiRes interviewReportAiRes = aiService.generateFinalReport(session, completedProgresses);
// 3. ---> 解析AI返回的最终报告JSON更新会话状态 <--- // 3. ---> 解析AI返回的最终报告JSON更新会话状态 <---
session.setStatus(InterviewSession.Status.COMPLETED.name()); session.setStatus(InterviewSession.Status.COMPLETED.name());
session.setScore(finalReportJson.getBigDecimal("overallScore")); session.setScore(new BigDecimal(interviewReportAiRes.overallScore()));
session.setFinalReport(finalReportJson.toJSONString()); session.setFinalReport(JSONObject.toJSONString(interviewReportAiRes));
this.baseMapper.updateById(session); this.baseMapper.updateById(session);
return session; return session;
@@ -398,7 +411,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
private InterviewMessage saveMessage(String sessionId, String messageType, String sender, private InterviewMessage saveMessage(String sessionId, String messageType, String sender,
String content, Long questionId) { String content, Long questionId) {
int nextOrder = messageMapper.selectMaxOrderBySessionId(sessionId) + 1; int nextOrder = messageMapper.selectMaxOrderBySessionId(sessionId) + 1;
InterviewMessage message = new InterviewMessage() InterviewMessage message = new InterviewMessage()
.setSessionId(sessionId) .setSessionId(sessionId)

View File

@@ -17,7 +17,6 @@ import com.qingqiu.interview.mapper.QuestionMapper;
import com.qingqiu.interview.service.IQuestionCategoryService; import com.qingqiu.interview.service.IQuestionCategoryService;
import com.qingqiu.interview.service.QuestionClassificationService; import com.qingqiu.interview.service.QuestionClassificationService;
import com.qingqiu.interview.service.QuestionService; import com.qingqiu.interview.service.QuestionService;
import com.qingqiu.interview.service.llm.LlmService;
import com.qingqiu.interview.service.parser.DocumentParser; import com.qingqiu.interview.service.parser.DocumentParser;
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO; import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -41,7 +40,6 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
private final QuestionMapper questionMapper; private final QuestionMapper questionMapper;
private final QuestionClassificationService classificationService; private final QuestionClassificationService classificationService;
private final List<DocumentParser> documentParserList; // This will be injected by Spring private final List<DocumentParser> documentParserList; // This will be injected by Spring
private final LlmService llmService;
private final IQuestionCategoryService questionCategoryService; private final IQuestionCategoryService questionCategoryService;
/** /**
@@ -119,30 +117,30 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void useAiCheckQuestionData() { public void useAiCheckQuestionData() {
// 查询数据库 // // 查询数据库
List<Question> questions = questionMapper.selectList( // List<Question> questions = questionMapper.selectList(
new LambdaQueryWrapper<Question>() // new LambdaQueryWrapper<Question>()
.orderByDesc(Question::getCreatedTime) // .orderByDesc(Question::getCreatedTime)
); // );
// 组装prompt // // 组装prompt
if (CollectionUtil.isEmpty(questions)) { // if (CollectionUtil.isEmpty(questions)) {
return; // return;
} // }
String prompt = getPrompt(questions); // String prompt = getPrompt(questions);
log.info("发送内容: {}", prompt); // log.info("发送内容: {}", prompt);
// 验证token上下文长度 // // 验证token上下文长度
Integer promptTokens = llmService.getPromptTokens(prompt); // Integer promptTokens = llmService.getPromptTokens(prompt);
log.info("当前prompt长度: {}", promptTokens); // log.info("当前prompt长度: {}", promptTokens);
String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK); // String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK);
// 调用AI // // 调用AI
log.info("AI返回内容: {}", chat); // log.info("AI返回内容: {}", chat);
JSONObject parse = JSONObject.parse(chat); // JSONObject parse = JSONObject.parse(chat);
JSONArray questionsIds = parse.getJSONArray("questions"); // JSONArray questionsIds = parse.getJSONArray("questions");
List<Long> list = questionsIds.toList(Long.class); // List<Long> list = questionsIds.toList(Long.class);
questionMapper.delete( // questionMapper.delete(
new LambdaQueryWrapper<Question>() // new LambdaQueryWrapper<Question>()
.notIn(Question::getId, list) // .notIn(Question::getId, list)
); // );
} }

View File

@@ -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);
}

View File

@@ -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<AiSessionLog> aiSessionLogs = aiSessionLogMapper.selectList(
new LambdaQueryWrapper<AiSessionLog>()
.eq(AiSessionLog::getToken, token)
.orderByDesc(AiSessionLog::getCreatedTime)
);
// 构造发给ai的消息
List<Message> 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<Integer> integers = tokenizer.encodeOrdinary(prompt);
return integers.size();
}
}

View File

@@ -1,14 +1,42 @@
dashscope:
api-key: sk-58d6fed688c54e8db02e6a7ffbfc7a5f
deepseek:
api-url: https://api.deepseek.com/chat/completions
api-key: sk-faaa2a1b485442ccbf115ff1271a3480
spring: spring:
datasource: datasource:
url: jdbc:mysql://gz-cynosdbmysql-grp-5ai5zw7r.sql.tencentcdb.com:24944/ai_interview?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8 url: jdbc:mysql://gz-cynosdbmysql-grp-5ai5zw7r.sql.tencentcdb.com:24944/ai_interview?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
username: qingqiu username: qingqiu
password: 020979hP password: 020979hP
driver-class-name: com.mysql.cj.jdbc.Driver 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: # ai:
# openai: # openai:
# api-key: sk-faaa2a1b485442ccbf115ff1271a3480 # api-key: sk-faaa2a1b485442ccbf115ff1271a3480