Compare commits
1 Commits
alibaba-re
...
langchain4
| Author | SHA1 | Date | |
|---|---|---|---|
| 1815553d25 |
120
pom.xml
120
pom.xml
@@ -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.4.5</version>
|
<version>3.5.0</version>
|
||||||
<relativePath/> <!-- lookup parent from repository -->
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<groupId>com.qingqiu</groupId>
|
<groupId>com.qingqiu</groupId>
|
||||||
@@ -13,7 +13,6 @@
|
|||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
<name>AI-Interview</name>
|
<name>AI-Interview</name>
|
||||||
<description>AI-Interview</description>
|
<description>AI-Interview</description>
|
||||||
<!-- TODO: 考虑删除空的元数据元素 -->
|
|
||||||
<url/>
|
<url/>
|
||||||
<licenses>
|
<licenses>
|
||||||
<license/>
|
<license/>
|
||||||
@@ -29,9 +28,6 @@
|
|||||||
</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 -->
|
||||||
@@ -57,58 +53,11 @@
|
|||||||
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- TODO: 考虑删除已注释的依赖 -->
|
|
||||||
<!-- <dependency>-->
|
|
||||||
<!-- <groupId>org.springframework.ai</groupId>-->
|
|
||||||
<!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>-->
|
|
||||||
<!-- <version>1.0.0-SNAPSHOT</version>-->
|
|
||||||
<!-- </dependency>-->
|
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.ai</groupId>
|
<groupId>com.alibaba</groupId>
|
||||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
<artifactId>dashscope-sdk-java</artifactId>
|
||||||
|
<version>2.21.5</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.alibaba.cloud.ai</groupId>
|
|
||||||
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- TODO: 检查内存相关依赖是否都需要 -->
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- TODO: 考虑删除已注释的依赖 -->
|
|
||||||
<!-- <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>
|
||||||
@@ -165,28 +114,8 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<dependencyManagement>
|
<dependencyManagement>
|
||||||
|
|
||||||
<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>
|
||||||
@@ -194,20 +123,24 @@
|
|||||||
<type>pom</type>
|
<type>pom</type>
|
||||||
<scope>import</scope>
|
<scope>import</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>dev.langchain4j</groupId>
|
||||||
|
<artifactId>langchain4j-bom</artifactId>
|
||||||
|
<version>1.0.0-beta3</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
<finalName>ai-interview</finalName>
|
<finalName>ai-interview</finalName>
|
||||||
<plugins>
|
<plugins>
|
||||||
<!-- TODO: 检查Spring Boot插件版本是否与父POM一致 -->
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
<version>3.5.0</version>
|
<version>3.5.0</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
<!-- TODO: 考虑在正式环境中启用测试 -->
|
|
||||||
<!-- maven 打包时跳过测试 -->
|
<!-- maven 打包时跳过测试 -->
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
@@ -218,6 +151,33 @@
|
|||||||
</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>
|
||||||
|
|||||||
22
src/main/java/com/qingqiu/interview/ai/entity/Message.java
Executable file
22
src/main/java/com/qingqiu/interview/ai/entity/Message.java
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
11
src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
Executable file
11
src/main/java/com/qingqiu/interview/ai/factory/AIClientFactory.java
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
33
src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
Executable file
33
src/main/java/com/qingqiu/interview/ai/factory/AIClientManager.java
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
Executable file
20
src/main/java/com/qingqiu/interview/ai/factory/DeepSeekClientFactory.java
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
Executable file
20
src/main/java/com/qingqiu/interview/ai/factory/QwenClientFactory.java
Executable file
@@ -0,0 +1,20 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main/java/com/qingqiu/interview/ai/service/AIClientService.java
Executable file
13
src/main/java/com/qingqiu/interview/ai/service/AIClientService.java
Executable file
@@ -0,0 +1,13 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.qingqiu.interview.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1></h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 12:58
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface AiChatLog {
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.qingqiu.interview.aspect;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.dto.ChatDTO;
|
||||||
|
import com.qingqiu.interview.entity.AiSessionLog;
|
||||||
|
import com.qingqiu.interview.service.IAiSessionLogService;
|
||||||
|
import com.qingqiu.interview.vo.ChatVO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Pointcut;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <h1>
|
||||||
|
* ai聊天的切面
|
||||||
|
* </h1>
|
||||||
|
*
|
||||||
|
* @author qingqiu
|
||||||
|
* @date 2025/9/18 13:00
|
||||||
|
*/
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class AiChatLogAspect {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private IAiSessionLogService aiSessionLogService;
|
||||||
|
|
||||||
|
public AiChatLogAspect() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Pointcut("@annotation(com.qingqiu.interview.annotation.AiChatLog)")
|
||||||
|
public void logPointCut() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Around("logPointCut()")
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||||
|
|
||||||
|
Object[] args = point.getArgs();
|
||||||
|
ChatDTO arg = (ChatDTO) args[0];
|
||||||
|
if (StringUtils.isNoneBlank(arg.getSessionId())) {
|
||||||
|
AiSessionLog userSessionLog = new AiSessionLog();
|
||||||
|
userSessionLog
|
||||||
|
.setRole(arg.getRole())
|
||||||
|
.setDataType(arg.getDataType())
|
||||||
|
.setContent(arg.getContent())
|
||||||
|
.setToken(arg.getSessionId())
|
||||||
|
;
|
||||||
|
aiSessionLogService.save(userSessionLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Object result = point.proceed();
|
||||||
|
|
||||||
|
ChatVO chatVO = (ChatVO) result;
|
||||||
|
if (StringUtils.isNotBlank(chatVO.getSessionId())) {
|
||||||
|
AiSessionLog aiSessionLog = new AiSessionLog();
|
||||||
|
aiSessionLog
|
||||||
|
.setRole(chatVO.getRole())
|
||||||
|
.setContent(chatVO.getContent())
|
||||||
|
.setToken(chatVO.getSessionId())
|
||||||
|
;
|
||||||
|
aiSessionLogService.save(aiSessionLog);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
41
src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
Executable file
41
src/main/java/com/qingqiu/interview/common/utils/AIUtils.java
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,430 +0,0 @@
|
|||||||
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)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,12 +6,12 @@ import org.springframework.context.ApplicationContextAware;
|
|||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class SpringApplicationContextUtils implements ApplicationContextAware {
|
public class SpringApplicationContextUtil implements ApplicationContextAware {
|
||||||
private static ApplicationContext applicationContext;
|
private static ApplicationContext applicationContext;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
SpringApplicationContextUtils.applicationContext = applicationContext;
|
SpringApplicationContextUtil.applicationContext = applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T> T getBean(Class<T> beanClass) {
|
public static <T> T getBean(Class<T> beanClass) {
|
||||||
2
src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java → src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/common/utils/TreeUtils.java → src/main/java/com/qingqiu/interview/common/utils/TreeUtil.java
Normal file → Executable file
@@ -7,7 +7,7 @@ import java.util.Map;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class TreeUtils {
|
public class TreeUtil {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通用树形结构构建方法
|
* 通用树形结构构建方法
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package com.qingqiu.interview.common.utils;
|
|
||||||
|
|
||||||
public class UUIDUtils {
|
|
||||||
|
|
||||||
public static String getUUID() {
|
|
||||||
return java.util.UUID.randomUUID().toString().replace("-", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.qingqiu.interview.common.utils;
|
|
||||||
|
|
||||||
|
|
||||||
import io.netty.channel.ChannelOption;
|
|
||||||
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;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @program: ai-interview
|
|
||||||
* @description:
|
|
||||||
* @author: huangpeng
|
|
||||||
* @create: 2025-11-06 20:04
|
|
||||||
**/
|
|
||||||
public class WebClientUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建全局默认的WebClient Bean实例
|
|
||||||
* 配置了连接超时和响应超时时间
|
|
||||||
*
|
|
||||||
* @return WebClient实例
|
|
||||||
*/
|
|
||||||
public static WebClient.Builder getWebClientBuilder() {
|
|
||||||
HttpClient httpClient = HttpClient.create()
|
|
||||||
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) // 连接超时
|
|
||||||
.responseTimeout(Duration.ofMillis(30000)); // 读取超时
|
|
||||||
|
|
||||||
return WebClient.builder()
|
|
||||||
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
|
||||||
;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static RestClient.Builder getRestClientBuilder() {
|
|
||||||
return RestClient.builder()
|
|
||||||
.requestFactory(
|
|
||||||
new ReactorClientHttpRequestFactory(
|
|
||||||
HttpClient.create()
|
|
||||||
.responseTimeout(Duration.ofMinutes(30))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.qingqiu.interview.config;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|
||||||
|
|
||||||
@Configuration
|
|
||||||
public class CorsConfig implements WebMvcConfigurer {
|
|
||||||
@Override
|
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
|
||||||
registry.addMapping("/**").
|
|
||||||
allowedOriginPatterns("*") //允许跨域的域名,可以用*表示允许任何域名使用
|
|
||||||
// allowedOrigins("*"). //在Springboot2.4对应Spring5.3后在设置allowCredentials(true)的基础上不能直接使用通配符设置allowedOrigins,而是需要指定特定的URL。如果需要设置通配符,需要通过allowedOriginPatterns指定
|
|
||||||
.allowedMethods("GET", "POST", "DELETE", "PUT") //允许任何方法(post、get等)
|
|
||||||
.allowedHeaders("*") //允许任何请求头
|
|
||||||
.allowCredentials(true) //带上cookie信息
|
|
||||||
.exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L); //maxAge(3600)表明在3600秒内,不需要再发送预检验请求,可以缓存该结果
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,120 +1,14 @@
|
|||||||
package com.qingqiu.interview.config;
|
package com.qingqiu.interview.config;
|
||||||
|
|
||||||
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
|
import com.alibaba.dashscope.aigc.generation.Generation;
|
||||||
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 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.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 static com.qingqiu.interview.common.utils.WebClientUtils.getRestClientBuilder;
|
|
||||||
import static com.qingqiu.interview.common.utils.WebClientUtils.getWebClientBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
|
|
||||||
private MessageWindowChatMemory mysqlChatMemory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建DashScopeApi Bean实例
|
|
||||||
* 配置了HTTP客户端连接参数,包括连接超时和响应超时时间
|
|
||||||
*
|
|
||||||
* @return DashScopeApi实例
|
|
||||||
*/
|
|
||||||
@Bean
|
@Bean
|
||||||
public DashScopeApi dashScopeApi() {
|
public Generation generation() {
|
||||||
return DashScopeApi.builder()
|
return new Generation();
|
||||||
.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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建默认的聊天客户端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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package com.qingqiu.interview.config;
|
|
||||||
|
|
||||||
|
|
||||||
import com.qingqiu.interview.common.constants.ChatConstant;
|
|
||||||
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 static com.qingqiu.interview.common.utils.WebClientUtils.getRestClientBuilder;
|
|
||||||
import static com.qingqiu.interview.common.utils.WebClientUtils.getWebClientBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @program: ai-interview
|
|
||||||
* @description: openai聊天配置
|
|
||||||
* @author: huangpeng
|
|
||||||
* @create: 2025-11-06 20:03
|
|
||||||
**/
|
|
||||||
@Configuration
|
|
||||||
public class OpenAiChatConfig {
|
|
||||||
|
|
||||||
|
|
||||||
@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;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建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();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建基于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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
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;
|
||||||
@@ -39,7 +38,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, MessageType.SYSTEM.getValue())
|
.ne(AiSessionLog::getRole, Role.SYSTEM.getValue())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -123,11 +122,4 @@ public class InterviewController {
|
|||||||
public R<InterviewReportResponse> getInterviewReportDetail(@PathVariable String sessionId) {
|
public R<InterviewReportResponse> getInterviewReportDetail(@PathVariable String sessionId) {
|
||||||
return R.success(interviewService.getInterviewReport(sessionId));
|
return R.success(interviewService.getInterviewReport(sessionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/read-pdf")
|
|
||||||
public R<?> readPdf(@RequestParam MultipartFile file) throws IOException {
|
|
||||||
String readPdfFile = interviewService.readPdfFile(file);
|
|
||||||
log.info("resume content: {}", readPdfFile);
|
|
||||||
return R.success(readPdfFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package com.qingqiu.interview.controller;
|
|||||||
|
|
||||||
import com.qingqiu.interview.common.res.R;
|
import com.qingqiu.interview.common.res.R;
|
||||||
import com.qingqiu.interview.dto.QuestionProgressPageParams;
|
import com.qingqiu.interview.dto.QuestionProgressPageParams;
|
||||||
import com.qingqiu.interview.entity.InterviewQuestionProgress;
|
|
||||||
import com.qingqiu.interview.service.IInterviewQuestionProgressService;
|
import com.qingqiu.interview.service.IInterviewQuestionProgressService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -25,7 +27,6 @@ public class InterviewQuestionProgressController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 面试问题进度列表
|
* 面试问题进度列表
|
||||||
*
|
|
||||||
* @param params 查询参数
|
* @param params 查询参数
|
||||||
* @return data
|
* @return data
|
||||||
*/
|
*/
|
||||||
@@ -34,16 +35,4 @@ public class InterviewQuestionProgressController {
|
|||||||
return R.success(service.pageList(params));
|
return R.success(service.pageList(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取面试问题进度详情
|
|
||||||
*
|
|
||||||
* @param id id
|
|
||||||
* @return data
|
|
||||||
*/
|
|
||||||
@GetMapping("/{id}")
|
|
||||||
public R<InterviewQuestionProgress> getDetail(@PathVariable Long id) {
|
|
||||||
return R.success(service.getById(id));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,5 @@ public class InterviewStartRequest {
|
|||||||
/** 生成的面试题目数量 */
|
/** 生成的面试题目数量 */
|
||||||
private Integer totalQuestions = 10;
|
private Integer totalQuestions = 10;
|
||||||
|
|
||||||
/**
|
|
||||||
* 岗位要求
|
|
||||||
*/
|
|
||||||
private String jobRequirements;
|
|
||||||
|
|
||||||
// 简历文件通过MultipartFile单独传递
|
// 简历文件通过MultipartFile单独传递
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,6 @@ 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")
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
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) {
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
package com.qingqiu.interview.entity.ai;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public record ExtractSkillAiRes(
|
|
||||||
@JsonProperty("skills") List<String> skills
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
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
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
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
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package com.qingqiu.interview.entity.ai;
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @program: ai-interview
|
|
||||||
* @description: 题型分类ai返回
|
|
||||||
* @author: huangpeng
|
|
||||||
* @create: 2025-11-06 20:08
|
|
||||||
**/
|
|
||||||
public class QuestionClassificationAiRes {
|
|
||||||
|
|
||||||
public record Item(@JsonProperty("content") String content,
|
|
||||||
@JsonProperty("category") String category,
|
|
||||||
@JsonProperty("difficulty") String difficulty,
|
|
||||||
@JsonProperty("tags") String tags) {}
|
|
||||||
|
|
||||||
public record Wrapper(@JsonProperty("questions")List<Item> questions) {}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package com.qingqiu.interview.entity.ai;
|
|
||||||
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
||||||
import redis.clients.jedis.graph.Statistics;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @program: ai-interview
|
|
||||||
* @description: 题目去重AI返回结果
|
|
||||||
* @author: huangpeng
|
|
||||||
* @create: 2025-11-07 10:37
|
|
||||||
**/
|
|
||||||
public record QuestionDeduplicationAiRes(
|
|
||||||
@JsonProperty("questionIds") String questionIds
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
29
src/main/java/com/qingqiu/interview/service/ChatService.java
Normal file
29
src/main/java/com/qingqiu/interview/service/ChatService.java
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
60
src/main/java/com/qingqiu/interview/service/DashboardService.java
Normal file → Executable file
60
src/main/java/com/qingqiu/interview/service/DashboardService.java
Normal file → Executable file
@@ -1,15 +1,59 @@
|
|||||||
package com.qingqiu.interview.service;
|
package com.qingqiu.interview.service;
|
||||||
|
|
||||||
|
import com.qingqiu.interview.mapper.InterviewSessionMapper;
|
||||||
|
import com.qingqiu.interview.mapper.QuestionMapper;
|
||||||
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DashboardService {
|
||||||
|
|
||||||
|
private final QuestionMapper questionMapper;
|
||||||
|
private final InterviewSessionMapper sessionMapper;
|
||||||
|
|
||||||
|
public DashboardStatsResponse getDashboardStats() {
|
||||||
|
DashboardStatsResponse stats = new DashboardStatsResponse();
|
||||||
|
|
||||||
|
// 1. 获取核心KPI
|
||||||
|
stats.setTotalQuestions(questionMapper.selectCount(null));
|
||||||
|
stats.setTotalInterviews(sessionMapper.selectCount(null));
|
||||||
|
|
||||||
|
// 2. 获取题库分类统计
|
||||||
|
stats.setQuestionCategoryStats(questionMapper.countByCategory());
|
||||||
|
|
||||||
|
// 3. 获取最近7天的面试统计,并补全没有数据的日期
|
||||||
|
List<DashboardStatsResponse.DailyStat> recentStats = sessionMapper.countRecentInterviews(7);
|
||||||
|
stats.setRecentInterviewStats(fillMissingDates(recentStats, 7));
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @program: ai-interview
|
* 填充最近几天内没有面试数据的日期,补0
|
||||||
* @description: 工作台接口
|
*/
|
||||||
* @author: huangpeng
|
private List<DashboardStatsResponse.DailyStat> fillMissingDates(List<DashboardStatsResponse.DailyStat> existingStats, int days) {
|
||||||
* @create: 2025-11-07 14:54
|
Map<String, Long> statsMap = existingStats.stream()
|
||||||
**/
|
.collect(Collectors.toMap(DashboardStatsResponse.DailyStat::getDate, DashboardStatsResponse.DailyStat::getCount));
|
||||||
public interface DashboardService {
|
|
||||||
|
|
||||||
DashboardStatsResponse getDashboardStats();
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
|
|
||||||
|
return IntStream.range(0, days)
|
||||||
|
.mapToObj(i -> LocalDate.now().minusDays(i))
|
||||||
|
.map(date -> {
|
||||||
|
String dateString = date.format(formatter);
|
||||||
|
long count = statsMap.getOrDefault(dateString, 0L);
|
||||||
|
return new DashboardStatsResponse.DailyStat(dateString, count);
|
||||||
|
})
|
||||||
|
.sorted((d1, d2) -> d1.getDate().compareTo(d2.getDate())) // 按日期升序排序
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
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;
|
||||||
|
|
||||||
@@ -25,7 +23,7 @@ public interface InterviewAiService {
|
|||||||
* @param resumeContent 简历文本
|
* @param resumeContent 简历文本
|
||||||
* @return 包含技能列表的JSON对象
|
* @return 包含技能列表的JSON对象
|
||||||
*/
|
*/
|
||||||
List<String> extractSkillsFromResume(String resumeContent);
|
JSONObject extractSkillsFromResume(String resumeContent);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据技能动态生成面试题目
|
* 根据技能动态生成面试题目
|
||||||
@@ -35,9 +33,9 @@ public interface InterviewAiService {
|
|||||||
* @param count 需要生成的题目数量
|
* @param count 需要生成的题目数量
|
||||||
* @return 包含问题列表的JSON对象
|
* @return 包含问题列表的JSON对象
|
||||||
*/
|
*/
|
||||||
List<QuestionAiRes.Question> generateQuestionsOfAi(String sessionId, List<String> skills, String jobRequirements, String resumeContent, int count);
|
JSONObject generateQuestionsOfAi(String sessionId, 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);
|
JSONObject generateQuestionOfLocal(String sessionId, List<Question> questions, List<String> skills, String resumeContent, int count);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评估用户的回答
|
* 评估用户的回答
|
||||||
@@ -47,7 +45,7 @@ public interface InterviewAiService {
|
|||||||
* @param context 可选的上下文(之前的问答历史)
|
* @param context 可选的上下文(之前的问答历史)
|
||||||
* @return 包含评估结果的JSON对象
|
* @return 包含评估结果的JSON对象
|
||||||
*/
|
*/
|
||||||
EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context);
|
JSONObject evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成最终的面试评估报告
|
* 生成最终的面试评估报告
|
||||||
@@ -56,7 +54,7 @@ public interface InterviewAiService {
|
|||||||
* @param progressList 整个面试的问答记录
|
* @param progressList 整个面试的问答记录
|
||||||
* @return 包含最终报告的JSON对象
|
* @return 包含最终报告的JSON对象
|
||||||
*/
|
*/
|
||||||
InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList);
|
JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList);
|
||||||
|
|
||||||
String generateFirstQuestion(String sessionId, String candidateName, String questionContent);
|
String generateFirstQuestion(String sessionId, String candidateName, String questionContent);
|
||||||
|
|
||||||
|
|||||||
@@ -54,17 +54,8 @@ public interface InterviewService extends IService<InterviewSession> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取面试报告
|
* 获取面试报告
|
||||||
*
|
|
||||||
* @param sessionId
|
* @param sessionId
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
InterviewReportResponse getInterviewReport(String sessionId);
|
InterviewReportResponse getInterviewReport(String sessionId);
|
||||||
|
|
||||||
/**
|
|
||||||
* 读取pdf文件数据
|
|
||||||
*
|
|
||||||
* @param file 文件
|
|
||||||
* @return 文件内容
|
|
||||||
*/
|
|
||||||
String readPdfFile(MultipartFile file) throws IOException;
|
|
||||||
}
|
}
|
||||||
|
|||||||
140
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Normal file → Executable file
140
src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
Normal file → Executable file
@@ -1,18 +1,138 @@
|
|||||||
package com.qingqiu.interview.service;
|
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.entity.Question;
|
import com.qingqiu.interview.entity.Question;
|
||||||
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
|
import com.qingqiu.interview.service.llm.LlmService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
@Slf4j
|
||||||
* @program: ai-interview
|
@Service
|
||||||
* @description: 题型分类
|
@RequiredArgsConstructor
|
||||||
* @author: huangpeng
|
public class QuestionClassificationService {
|
||||||
* @create: 2025-11-06 19:59
|
|
||||||
**/
|
|
||||||
public interface QuestionClassificationService {
|
|
||||||
|
|
||||||
List<QuestionClassificationAiRes.Item> classifyQuestions(String rawContent);
|
private final LlmService llmService;
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用AI对题目进行分类
|
||||||
|
*/
|
||||||
|
public List<Question> classifyQuestions(String rawContent) {
|
||||||
|
log.info("开始使用AI分类题目,内容长度: {}", rawContent.length());
|
||||||
|
|
||||||
|
String prompt = buildClassificationPrompt(rawContent);
|
||||||
|
String aiResponse = llmService.chat(prompt);
|
||||||
|
|
||||||
|
log.info("AI分类响应: {}", aiResponse);
|
||||||
|
|
||||||
|
return parseAiResponse(aiResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildClassificationPrompt(String content) {
|
||||||
|
return """
|
||||||
|
请分析以下面试题内容,将其分类并提取信息。请严格按照以下JSON格式返回结果:
|
||||||
|
|
||||||
|
{
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"content": "题目内容",
|
||||||
|
"category": "分类(如:Java基础、Spring框架、数据库、算法、系统设计等)",
|
||||||
|
"difficulty": "难度(Easy、Medium、Hard)",
|
||||||
|
"tags": "相关标签,用逗号分隔"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
分类规则:
|
||||||
|
1. category应该是具体的技术领域,如:Java基础、Spring框架、MySQL、Redis、算法与数据结构、系统设计、网络协议等
|
||||||
|
2. difficulty根据题目复杂度判断:Easy(基础概念)、Medium(实际应用)、Hard(深入原理或复杂场景)
|
||||||
|
3. tags包含更细粒度的标签,如:多线程、JVM、事务、索引等
|
||||||
|
4. 如果内容包含多个独立的题目,请分别提取
|
||||||
|
5. 只返回JSON,不要其他解释文字
|
||||||
|
|
||||||
|
待分析内容:
|
||||||
|
""" + content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Question> parseAiResponse(String aiResponse) {
|
||||||
|
List<Question> questions = new ArrayList<>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 清理响应,移除可能的markdown标记
|
||||||
|
String cleanResponse = aiResponse.trim();
|
||||||
|
if (cleanResponse.startsWith("```json")) {
|
||||||
|
cleanResponse = cleanResponse.substring(7);
|
||||||
|
}
|
||||||
|
if (cleanResponse.endsWith("```")) {
|
||||||
|
cleanResponse = cleanResponse.substring(0, cleanResponse.length() - 3);
|
||||||
|
}
|
||||||
|
cleanResponse = cleanResponse.trim();
|
||||||
|
|
||||||
|
JsonNode rootNode = objectMapper.readTree(cleanResponse);
|
||||||
|
JsonNode questionsNode = rootNode.get("questions");
|
||||||
|
|
||||||
|
if (questionsNode != null && questionsNode.isArray()) {
|
||||||
|
for (JsonNode questionNode : questionsNode) {
|
||||||
|
Question question = new Question()
|
||||||
|
.setContent(getTextValue(questionNode, "content"))
|
||||||
|
.setCategoryName(getTextValue(questionNode, "category"))
|
||||||
|
.setDifficulty(getTextValue(questionNode, "difficulty"))
|
||||||
|
.setTags(getTextValue(questionNode, "tags"));
|
||||||
|
|
||||||
|
if (isValidQuestion(question)) {
|
||||||
|
questions.add(question);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("成功解析出 {} 个题目", questions.size());
|
||||||
|
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
log.error("解析AI响应失败: {}", e.getMessage());
|
||||||
|
log.error("原始响应: {}", aiResponse);
|
||||||
|
|
||||||
|
// 降级处理:如果AI返回格式不正确,尝试简单分割
|
||||||
|
questions.addAll(fallbackParsing(aiResponse));
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getTextValue(JsonNode node, String fieldName) {
|
||||||
|
JsonNode fieldNode = node.get(fieldName);
|
||||||
|
return fieldNode != null ? fieldNode.asText("") : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isValidQuestion(Question question) {
|
||||||
|
return question.getContent() != null && !question.getContent().trim().isEmpty()
|
||||||
|
&& question.getCategoryName() != null && !question.getCategoryName().trim().isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Question> fallbackParsing(String content) {
|
||||||
|
log.warn("使用降级解析策略");
|
||||||
|
List<Question> questions = new ArrayList<>();
|
||||||
|
|
||||||
|
// 简单的降级策略:按行分割,每行作为一个题目
|
||||||
|
String[] lines = content.split("\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
line = line.trim();
|
||||||
|
if (!line.isEmpty() && line.length() > 10) { // 过滤太短的内容
|
||||||
|
Question question = new Question()
|
||||||
|
.setContent(line)
|
||||||
|
.setCategoryName("未分类")
|
||||||
|
.setDifficulty("Medium")
|
||||||
|
.setTags("待分类");
|
||||||
|
questions.add(question);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return questions;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
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 "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package com.qingqiu.interview.service.impl;
|
|
||||||
|
|
||||||
import com.qingqiu.interview.mapper.InterviewSessionMapper;
|
|
||||||
import com.qingqiu.interview.mapper.QuestionMapper;
|
|
||||||
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
|
||||||
import com.qingqiu.interview.service.DashboardService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class DashboardServiceImpl implements DashboardService {
|
|
||||||
|
|
||||||
private final QuestionMapper questionMapper;
|
|
||||||
private final InterviewSessionMapper sessionMapper;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DashboardStatsResponse getDashboardStats() {
|
|
||||||
DashboardStatsResponse stats = new DashboardStatsResponse();
|
|
||||||
|
|
||||||
// 1. 获取核心KPI
|
|
||||||
stats.setTotalQuestions(questionMapper.selectCount(null));
|
|
||||||
stats.setTotalInterviews(sessionMapper.selectCount(null));
|
|
||||||
|
|
||||||
// 2. 获取题库分类统计
|
|
||||||
stats.setQuestionCategoryStats(questionMapper.countByCategory());
|
|
||||||
|
|
||||||
// 3. 获取最近7天的面试统计,并补全没有数据的日期
|
|
||||||
List<DashboardStatsResponse.DailyStat> recentStats = sessionMapper.countRecentInterviews(7);
|
|
||||||
stats.setRecentInterviewStats(fillMissingDates(recentStats, 7));
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 填充最近几天内没有面试数据的日期,补0
|
|
||||||
*/
|
|
||||||
private List<DashboardStatsResponse.DailyStat> fillMissingDates(List<DashboardStatsResponse.DailyStat> existingStats, int days) {
|
|
||||||
Map<String, Long> statsMap = existingStats.stream()
|
|
||||||
.collect(Collectors.toMap(DashboardStatsResponse.DailyStat::getDate, DashboardStatsResponse.DailyStat::getCount));
|
|
||||||
|
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
|
||||||
|
|
||||||
return IntStream.range(0, days)
|
|
||||||
.mapToObj(i -> LocalDate.now().minusDays(i))
|
|
||||||
.map(date -> {
|
|
||||||
String dateString = date.format(formatter);
|
|
||||||
long count = statsMap.getOrDefault(dateString, 0L);
|
|
||||||
return new DashboardStatsResponse.DailyStat(dateString, count);
|
|
||||||
})
|
|
||||||
.sorted((d1, d2) -> d1.getDate().compareTo(d2.getDate())) // 按日期升序排序
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,24 @@
|
|||||||
package com.qingqiu.interview.service.impl;
|
package com.qingqiu.interview.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.map.MapUtil;
|
import com.alibaba.dashscope.common.Role;
|
||||||
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.ChatConstant;
|
import com.qingqiu.interview.common.constants.CommonConstant;
|
||||||
import com.qingqiu.interview.common.utils.PromptTemplateUtils;
|
import com.qingqiu.interview.dto.ChatDTO;
|
||||||
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.service.ChatService;
|
||||||
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 jakarta.annotation.Resource;
|
import com.qingqiu.interview.vo.ChatVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
import org.springframework.context.annotation.Lazy;
|
||||||
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.Map;
|
import java.util.stream.Collectors;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <h1></h1>
|
* <h1></h1>
|
||||||
@@ -33,168 +28,114 @@ import java.util.UUID;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||||
public class InterviewAiServiceImpl implements InterviewAiService {
|
public class InterviewAiServiceImpl implements InterviewAiService {
|
||||||
|
|
||||||
@Resource
|
private final ChatService chatService;
|
||||||
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 List<String> extractSkillsFromResume(String resumeContent) {
|
public JSONObject extractSkillsFromResume(String resumeContent) {
|
||||||
ExtractSkillAiRes entity = chatClient
|
String prompt = "你是一位资深的IT技术招聘专家。" +
|
||||||
.prompt(PromptTemplateUtils.getExtractSkillsPrompt(resumeContent))
|
"请仔细阅读以下简历内容,并提取出其中所有的关键技术技能。" +
|
||||||
.call()
|
"请严格按照以下JSON格式返回,不要添加任何额外的解释或说明:\n" +
|
||||||
.entity(ExtractSkillAiRes.class);
|
"{\"skills\": [\"技能1\", \"技能2\", \"...\"]}\n\n" +
|
||||||
assert entity != null;
|
"简历内容如下:\n" + resumeContent;
|
||||||
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 List<QuestionAiRes.Question> generateQuestionsOfAi(String sessionId,
|
public JSONObject generateQuestionsOfAi(String sessionId, List<String> skills, String resumeContent, int count) {
|
||||||
List<String> skills,
|
String skillsStr = String.join(", ", skills);
|
||||||
String jobRequirements,
|
String prompt = String.format(
|
||||||
String resumeContent,
|
"你是一位专业的软件开发岗位技术面试官。" +
|
||||||
int count) {
|
"请根据候选人的以下技术栈、项目经历、简历内容,生成 %d 道有深度和广度的面试题。" +
|
||||||
// TODO: 考虑删除这些注释掉的旧代码实现
|
"题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。" +
|
||||||
// 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);
|
|
||||||
// TODO: 考虑删除这些注释掉的旧代码实现
|
|
||||||
// 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 List<QuestionAiRes.Question> generateQuestionOfLocal(String sessionId,
|
public JSONObject generateQuestionOfLocal(String sessionId, List<Question> questions, List<String> skills, String resumeContent, int count) {
|
||||||
List<Question> questions,
|
String skillsStr = String.join(", ", skills);
|
||||||
List<String> skills,
|
// 2. 构建发送给AI的提示
|
||||||
String jobRequirements,
|
String prompt = String.format("""
|
||||||
String resumeContent,
|
你是一位专业的面试官。请根据以下候选人的技术栈、项目经历、简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。
|
||||||
int count) {
|
题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。
|
||||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
要求:
|
||||||
.put("count", count)
|
1. 题目必须严格从【题库JSON】中选择。
|
||||||
.put("jobRequirements", jobRequirements)
|
2. 挑选的题目应根据候选人的简历内容来抽取。
|
||||||
.put("skills", JSONObject.toJSONString(skills))
|
3. 返回一个只包含所选题目ID的JSON数组,格式为:{"question_ids": [1, 5, 23, ...]}。
|
||||||
.put("resume", resumeContent)
|
4. 不要返回任何多余的代码,包括markdown形式的代码,我只需要JSON对象,请严格按照api接口形式返回
|
||||||
.build();
|
5. 不要返回任何额外的解释或文字,只返回JSON对象。
|
||||||
|
6. 严格按照前后端分离的接口形式返回JSON数据给我,不要返回"```json```"
|
||||||
|
7. 请保证返回数据的完整性,不要返回不完整的数据,否则我的JSON解析会报错!!!
|
||||||
|
|
||||||
Prompt aiInterviewerPrompt = PromptTemplateUtils.getLocalInterviewPrompt(params);
|
【候选人技术栈】:
|
||||||
QuestionAiRes.Wrapper entity = openAiChatClient.prompt(aiInterviewerPrompt)
|
%s
|
||||||
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, sessionId))
|
【候选人简历】:
|
||||||
.call()
|
[%s]
|
||||||
.entity(QuestionAiRes.Wrapper.class);
|
【题库JSON】:
|
||||||
assert entity != null;
|
%s
|
||||||
return entity.questions();
|
""", count, skillsStr, resumeContent, JSONObject.toJSONString(questions));
|
||||||
// TODO: 考虑删除这些注释掉的旧代码实现
|
ChatDTO chatDTO = new ChatDTO()
|
||||||
// String skillsStr = String.join(", ", skills);
|
.setSessionId(sessionId)
|
||||||
// // 2. 构建发送给AI的提示
|
.setContent(prompt)
|
||||||
// String prompt = String.format("""
|
.setRole(Role.SYSTEM.getValue())
|
||||||
// 你是一位专业的面试官。请根据以下候选人的技术栈、项目经历、简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。
|
.setDataType(CommonConstant.ONE);
|
||||||
// 题目应覆盖候选人的主要技术领域,并能考察其解决问题的能力。
|
ChatVO chatVO = chatService.createChat(chatDTO);
|
||||||
// 要求:
|
return JSON.parseObject(chatVO.getContent());
|
||||||
// 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
|
@Override
|
||||||
public EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context) {
|
public JSONObject evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context) {
|
||||||
// TODO: 考虑删除这些注释掉的旧代码实现
|
|
||||||
// 构建上下文历史
|
// 构建上下文历史
|
||||||
// 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);
|
||||||
Map<String, Object> params = MapUtil.<String, Object>builder()
|
return JSON.parseObject(chatVO.getContent());
|
||||||
.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 InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) {
|
public JSONObject generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList) {
|
||||||
// TODO: 考虑删除这些注释掉的旧代码实现
|
|
||||||
// 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()))
|
||||||
@@ -207,40 +148,17 @@ 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()
|
|
||||||
// .setRole(Role.SYSTEM.getValue())
|
String prompt = buildFinalReportPrompt(session, progressList);
|
||||||
// .setDataType(CommonConstant.ONE)
|
|
||||||
// .setContent(prompt);
|
ChatDTO chatDTO = new ChatDTO()
|
||||||
// ChatVO chatVO = chatService.createChat(chatDTO);
|
.setRole(Role.SYSTEM.getValue())
|
||||||
Map<String, Object> params = getFinalReportParams(session, progressList);
|
.setDataType(CommonConstant.ONE)
|
||||||
Prompt prompt = PromptTemplateUtils.getFinalReportPrompt(params);
|
.setContent(prompt);
|
||||||
return openAiChatClient.prompt(prompt)
|
ChatVO chatVO = chatService.createChat(chatDTO);
|
||||||
.advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, UUID.randomUUID().toString().replace("-", "")))
|
return JSON.parseObject(chatVO.getContent());
|
||||||
.call()
|
|
||||||
.entity(InterviewReportAiRes.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private static Map<String, Object> getFinalReportParams(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 MapUtil.<String, Object>builder()
|
|
||||||
.put("resume", session.getResumeContent())
|
|
||||||
.put("history", historyBuilder.toString())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
private String buildFinalReportPrompt(InterviewSession session, List<InterviewQuestionProgress> progressList) {
|
private String buildFinalReportPrompt(InterviewSession session, List<InterviewQuestionProgress> progressList) {
|
||||||
StringBuilder historyBuilder = new StringBuilder();
|
StringBuilder historyBuilder = new StringBuilder();
|
||||||
for (InterviewQuestionProgress progress : progressList) {
|
for (InterviewQuestionProgress progress : progressList) {
|
||||||
@@ -255,7 +173,6 @@ public class InterviewAiServiceImpl implements InterviewAiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return String.format("""
|
return String.format("""
|
||||||
你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估,给出一份全面、专业、有深度的最终面试报告。
|
你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估,给出一份全面、专业、有深度的最终面试报告。
|
||||||
|
|
||||||
@@ -289,36 +206,22 @@ 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
|
|
||||||
请以友好但专业的语气提出这个问题,可以适当添加一些引导性的话语。
|
|
||||||
严格按照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");
|
|
||||||
|
|
||||||
|
第一个问题是:%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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,11 @@ import java.io.IOException;
|
|||||||
public class InterviewChatServiceImpl implements InterviewChatService {
|
public class InterviewChatServiceImpl implements InterviewChatService {
|
||||||
|
|
||||||
private final DocumentParserManager documentParserManager;
|
private final DocumentParserManager documentParserManager;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException {
|
public void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException {
|
||||||
log.info("开始新面试会话,当前模式: {}, 候选人: {}, 默认AI模型: {}", request.getModel(), request.getCandidateName(), AIStrategyConstant.QWEN);
|
log.info("开始新面试会话,当前模式: {}, 候选人: {}, 默认AI模型: {}", request.getModel(), request.getCandidateName(), AIStrategyConstant.QWEN);
|
||||||
// 1. 解析简历
|
// 1. 解析简历
|
||||||
String resumeContent = parseResume(resume);
|
String resumeContent = parseResume(resume);
|
||||||
// TODO: 检查这个空if语句是否需要实现逻辑或删除
|
|
||||||
// 判断是否使用本地题库
|
// 判断是否使用本地题库
|
||||||
if (request.getModel().equals("local")) {
|
if (request.getModel().equals("local")) {
|
||||||
|
|
||||||
|
|||||||
@@ -37,10 +37,9 @@ 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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.qingqiu.interview.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import cn.hutool.core.io.file.FileNameUtil;
|
import cn.hutool.core.io.file.FileNameUtil;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
@@ -11,9 +12,6 @@ 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;
|
||||||
@@ -34,9 +32,10 @@ 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.math.BigDecimal;
|
import java.util.ArrayList;
|
||||||
import java.util.*;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.Objects;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -62,14 +61,12 @@ 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 {
|
||||||
// 1. 创建并保存会话主记录
|
// 1. 创建并保存会话主记录
|
||||||
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
||||||
String resumeContent = readPdfFile(file);
|
String resumeContent = parseResume(file);
|
||||||
InterviewSession session = new InterviewSession();
|
InterviewSession session = new InterviewSession();
|
||||||
|
|
||||||
session.setSessionId(sessionId);
|
session.setSessionId(sessionId);
|
||||||
@@ -79,14 +76,13 @@ 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服务从简历提取技能
|
||||||
List<String> skills = aiService.extractSkillsFromResume(resumeContent);
|
JSONObject skillsJson = aiService.extractSkillsFromResume(resumeContent);
|
||||||
|
|
||||||
// ---> 解析AI返回的JSON数据,获取技能列表 <---
|
// ---> 解析AI返回的JSON数据,获取技能列表 <---
|
||||||
session.setExtractedSkills(JSONObject.toJSONString(skills));
|
List<String> skills = skillsJson.getList("skills", String.class);
|
||||||
|
session.setExtractedSkills(skillsJson.toJSONString());
|
||||||
|
|
||||||
// 3. 准备面试问题(本地 + AI生成)
|
// 3. 准备面试问题(本地 + AI生成)
|
||||||
if (dto.getModel().equals("local")) {
|
if (dto.getModel().equals("local")) {
|
||||||
@@ -98,15 +94,11 @@ 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);
|
||||||
String aiRes = aiService.generateFirstQuestion(session.getSessionId(),
|
aiService.generateFirstQuestion(session.getSessionId(), session.getCandidateName(), nextQuestion.getQuestionContent());
|
||||||
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;
|
||||||
@@ -115,23 +107,25 @@ 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<>();
|
||||||
List<QuestionAiRes.Question> aiQuestionRes = aiService.generateQuestionsOfAi(
|
JSONObject aiQuestionsJson = aiService.generateQuestionsOfAi(
|
||||||
session.getSessionId(),
|
session.getSessionId(),
|
||||||
skills,
|
skills,
|
||||||
session.getJobRequirements(),
|
|
||||||
session.getResumeContent(),
|
session.getResumeContent(),
|
||||||
session.getTotalQuestions()
|
session.getTotalQuestions()
|
||||||
);
|
);
|
||||||
if (CollectionUtil.isNotEmpty(aiQuestionRes)) {
|
// ---> 解析AI返回的JSON数据,获取问题列表 <---
|
||||||
for (QuestionAiRes.Question aiQuestionRe : aiQuestionRes) {
|
JSONArray questions = aiQuestionsJson.getJSONArray("questions");
|
||||||
|
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(aiQuestionRe.content());
|
progress.setQuestionContent(q.getString("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)) {
|
||||||
@@ -164,19 +158,19 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// ai调用返回的内容进行提取
|
// ai调用返回的内容进行提取
|
||||||
List<QuestionAiRes.Question> questions = aiService.generateQuestionOfLocal(
|
JSONObject jsonObject = aiService.generateQuestionOfLocal(
|
||||||
session.getSessionId(),
|
session.getSessionId(),
|
||||||
localQuestionDataList,
|
localQuestionDataList,
|
||||||
skills,
|
skills,
|
||||||
session.getJobRequirements(),
|
|
||||||
session.getResumeContent(),
|
session.getResumeContent(),
|
||||||
session.getTotalQuestions()
|
session.getTotalQuestions()
|
||||||
);
|
);
|
||||||
Set<String> resQuestionIds = questions.stream().map(QuestionAiRes.Question::id).collect(Collectors.toSet());
|
JSONArray questionIds = jsonObject.getJSONArray("question_ids");
|
||||||
|
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, resQuestionIds)
|
.in(Question::getId, list)
|
||||||
);
|
);
|
||||||
List<InterviewQuestionProgress> progressList = new ArrayList<>();
|
List<InterviewQuestionProgress> progressList = new ArrayList<>();
|
||||||
questionList.forEach(q -> {
|
questionList.forEach(q -> {
|
||||||
@@ -262,8 +256,7 @@ 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)
|
||||||
);
|
);
|
||||||
Integer flowedQuestionCount = this.flowedQuestions.getOrDefault(currentProgress.getSessionId(), 0);
|
JSONObject evalResult = aiService.evaluateAnswer(
|
||||||
EvaluateAiRes evaluateAiRes = aiService.evaluateAnswer(
|
|
||||||
currentProgress.getSessionId(),
|
currentProgress.getSessionId(),
|
||||||
currentProgress.getQuestionContent(),
|
currentProgress.getQuestionContent(),
|
||||||
dto.getAnswer(),
|
dto.getAnswer(),
|
||||||
@@ -271,45 +264,40 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 3. ---> 解析AI返回的JSON评估结果并存入数据库 <---
|
// 3. ---> 解析AI返回的JSON评估结果并存入数据库 <---
|
||||||
currentProgress.setFeedback(evaluateAiRes.feedback());
|
currentProgress.setFeedback(evalResult.getString("feedback"));
|
||||||
currentProgress.setSuggestions(evaluateAiRes.suggestions());
|
currentProgress.setSuggestions(evalResult.getString("suggestions"));
|
||||||
currentProgress.setAiAnswer(evaluateAiRes.aiAnswer());
|
currentProgress.setAiAnswer(evalResult.getString("aiAnswer"));
|
||||||
currentProgress.setScore(BigDecimal.valueOf(evaluateAiRes.score()));
|
currentProgress.setScore(evalResult.getBigDecimal("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, evaluateAiRes);
|
saveEvaluationRecord(currentProgress, evalResult);
|
||||||
|
|
||||||
|
|
||||||
// 5. ---> 解析AI的是否追问判断,并处理追问逻辑 <---
|
// 5. ---> 解析AI的是否追问判断,并处理追问逻辑 <---
|
||||||
if (evaluateAiRes.continueAsking()) {
|
if (evalResult.getBooleanValue("continueAsking", false)) {
|
||||||
// 创建一个新的、状态为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(evaluateAiRes.followUpQuestion());
|
followUp.setQuestionContent(evalResult.getString("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, EvaluateAiRes evaluateAiRes) {
|
private void saveEvaluationRecord(InterviewQuestionProgress progress, JSONObject evalResult) {
|
||||||
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(evaluateAiRes.feedback());
|
evaluation.setAiFeedback(evalResult.getString("feedback"));
|
||||||
evaluation.setScore(BigDecimal.valueOf(evaluateAiRes.score()));
|
evaluation.setScore(evalResult.getBigDecimal("score"));
|
||||||
evaluationMapper.insert(evaluation);
|
evaluationMapper.insert(evaluation);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,12 +319,12 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 调用AI服务生成最终报告
|
// 2. 调用AI服务生成最终报告
|
||||||
InterviewReportAiRes interviewReportAiRes = aiService.generateFinalReport(session, completedProgresses);
|
JSONObject finalReportJson = aiService.generateFinalReport(session, completedProgresses);
|
||||||
|
|
||||||
// 3. ---> 解析AI返回的最终报告JSON,更新会话状态 <---
|
// 3. ---> 解析AI返回的最终报告JSON,更新会话状态 <---
|
||||||
session.setStatus(InterviewSession.Status.COMPLETED.name());
|
session.setStatus(InterviewSession.Status.COMPLETED.name());
|
||||||
session.setScore(new BigDecimal(interviewReportAiRes.overallScore()));
|
session.setScore(finalReportJson.getBigDecimal("overallScore"));
|
||||||
session.setFinalReport(JSONObject.toJSONString(interviewReportAiRes));
|
session.setFinalReport(finalReportJson.toJSONString());
|
||||||
this.baseMapper.updateById(session);
|
this.baseMapper.updateById(session);
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
@@ -399,8 +387,7 @@ public class InterviewServiceImpl extends ServiceImpl<InterviewSessionMapper, In
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
private String parseResume(MultipartFile resume) throws IOException {
|
||||||
public String readPdfFile(MultipartFile resume) throws IOException {
|
|
||||||
// 获取文件扩展名
|
// 获取文件扩展名
|
||||||
String extName = FileNameUtil.extName(resume.getOriginalFilename());
|
String extName = FileNameUtil.extName(resume.getOriginalFilename());
|
||||||
// 1. 获取简历解析器
|
// 1. 获取简历解析器
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.qingqiu.interview.service.impl;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.qingqiu.interview.entity.Question;
|
|
||||||
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
|
|
||||||
import com.qingqiu.interview.service.QuestionClassificationService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
|
||||||
import org.springframework.ai.chat.prompt.Prompt;
|
|
||||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
|
||||||
import org.springframework.ai.template.st.StTemplateRenderer;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class QuestionClassificationServiceImpl implements QuestionClassificationService {
|
|
||||||
|
|
||||||
private final ChatClient chatClient;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用AI对题目进行分类
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public List<QuestionClassificationAiRes.Item> classifyQuestions(String rawContent) {
|
|
||||||
log.info("开始使用AI分类题目,内容长度: {}", rawContent.length());
|
|
||||||
|
|
||||||
|
|
||||||
QuestionClassificationAiRes.Wrapper entity = chatClient.prompt()
|
|
||||||
.user(buildClassificationPrompt(rawContent))
|
|
||||||
.call()
|
|
||||||
.entity(QuestionClassificationAiRes.Wrapper.class);
|
|
||||||
assert entity != null;
|
|
||||||
return entity.questions();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildClassificationPrompt(String content) {
|
|
||||||
|
|
||||||
PromptTemplate prompt = PromptTemplate.builder()
|
|
||||||
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build())
|
|
||||||
.template(
|
|
||||||
"""
|
|
||||||
请分析以下面试题内容,将其分类并提取信息。请严格按照以下JSON格式返回结果:
|
|
||||||
|
|
||||||
{
|
|
||||||
"questions": [
|
|
||||||
{
|
|
||||||
"content": "题目内容",
|
|
||||||
"category": "分类(如:Java基础、Spring框架、数据库、算法、系统设计等)",
|
|
||||||
"difficulty": "难度(Easy、Medium、Hard)",
|
|
||||||
"tags": "相关标签,用逗号分隔"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
分类规则:
|
|
||||||
1. category应该是具体的技术领域,如:Java基础、Spring框架、MySQL、Redis、算法与数据结构、系统设计、网络协议等
|
|
||||||
2. difficulty根据题目复杂度判断:Easy(基础概念)、Medium(实际应用)、Hard(深入原理或复杂场景)
|
|
||||||
3. tags包含更细粒度的标签,如:多线程、JVM、事务、索引等
|
|
||||||
4. 如果内容包含多个独立的题目,请分别提取
|
|
||||||
5. 只返回JSON,不要其他解释文字
|
|
||||||
|
|
||||||
待分析内容:
|
|
||||||
<content>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
.build();
|
|
||||||
return prompt.render(Map.of("content", content));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +1,29 @@
|
|||||||
package com.qingqiu.interview.service.impl;
|
package com.qingqiu.interview.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
|
||||||
import cn.hutool.core.collection.CollectionUtil;
|
import cn.hutool.core.collection.CollectionUtil;
|
||||||
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
|
|
||||||
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.qingqiu.interview.common.constants.CommonConstant;
|
|
||||||
import com.qingqiu.interview.common.enums.LLMProvider;
|
import com.qingqiu.interview.common.enums.LLMProvider;
|
||||||
import com.qingqiu.interview.common.utils.TreeUtils;
|
import com.qingqiu.interview.common.constants.CommonConstant;
|
||||||
|
import com.qingqiu.interview.common.utils.TreeUtil;
|
||||||
import com.qingqiu.interview.dto.QuestionOptionsDTO;
|
import com.qingqiu.interview.dto.QuestionOptionsDTO;
|
||||||
import com.qingqiu.interview.dto.QuestionPageParams;
|
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||||
import com.qingqiu.interview.entity.Question;
|
import com.qingqiu.interview.entity.Question;
|
||||||
import com.qingqiu.interview.entity.QuestionCategory;
|
import com.qingqiu.interview.entity.QuestionCategory;
|
||||||
import com.qingqiu.interview.entity.ai.QuestionClassificationAiRes;
|
|
||||||
import com.qingqiu.interview.entity.ai.QuestionDeduplicationAiRes;
|
|
||||||
import com.qingqiu.interview.mapper.QuestionMapper;
|
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.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 jakarta.annotation.Resource;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.ai.chat.client.ChatClient;
|
|
||||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
|
||||||
import org.springframework.ai.template.TemplateRenderer;
|
|
||||||
import org.springframework.ai.template.st.StTemplateRenderer;
|
|
||||||
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.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -46,12 +39,11 @@ import java.util.stream.Collectors;
|
|||||||
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||||
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
|
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements QuestionService {
|
||||||
private final QuestionMapper questionMapper;
|
private final QuestionMapper questionMapper;
|
||||||
private final QuestionClassificationServiceImpl 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;
|
||||||
|
|
||||||
private final ChatClient chatClient;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页查询题库
|
* 分页查询题库
|
||||||
*/
|
*/
|
||||||
@@ -106,19 +98,19 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
|||||||
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
|
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
|
||||||
|
|
||||||
String content = parser.parse(file.getInputStream());
|
String content = parser.parse(file.getInputStream());
|
||||||
List<QuestionClassificationAiRes.Item> items = classificationService.classifyQuestions(content);
|
List<Question> questionsFromAi = classificationService.classifyQuestions(content);
|
||||||
|
|
||||||
int newQuestionsCount = 0;
|
int newQuestionsCount = 0;
|
||||||
for (QuestionClassificationAiRes.Item item : items) {
|
for (Question question : questionsFromAi) {
|
||||||
try {
|
try {
|
||||||
validateQuestion(item.content(), null);
|
validateQuestion(question.getContent(), null);
|
||||||
questionMapper.insert(BeanUtil.toBean(item, Question.class));
|
questionMapper.insert(question);
|
||||||
newQuestionsCount++;
|
newQuestionsCount++;
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("跳过重复题目: {}", item.content());
|
log.warn("跳过重复题目: {}", question.getContent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, items.size() - newQuestionsCount);
|
log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, questionsFromAi.size() - newQuestionsCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,18 +129,16 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String prompt = getPrompt(questions);
|
String prompt = getPrompt(questions);
|
||||||
|
log.info("发送内容: {}", prompt);
|
||||||
|
// 验证token上下文长度
|
||||||
QuestionDeduplicationAiRes entity = chatClient.prompt().user(prompt).call().entity(QuestionDeduplicationAiRes.class);
|
Integer promptTokens = llmService.getPromptTokens(prompt);
|
||||||
assert entity != null;
|
log.info("当前prompt长度: {}", promptTokens);
|
||||||
|
String chat = llmService.chat(prompt, LLMProvider.DEEPSEEK);
|
||||||
// 调用AI
|
// 调用AI
|
||||||
log.info("AI返回内容: {}", JSONObject.toJSONString(entity));
|
log.info("AI返回内容: {}", chat);
|
||||||
String s = entity.questionIds();
|
JSONObject parse = JSONObject.parse(chat);
|
||||||
List<String> list = Arrays.asList(s.split(","));
|
JSONArray questionsIds = parse.getJSONArray("questions");
|
||||||
// TODO: 检查这些注释代码是否可以删除
|
List<Long> list = questionsIds.toList(Long.class);
|
||||||
// JSONObject parse = JSONObject.parse(chat);
|
|
||||||
// JSONArray questionsIds = parse.getJSONArray("questions");
|
|
||||||
// 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)
|
||||||
@@ -167,71 +157,25 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
|||||||
}
|
}
|
||||||
JSONObject jsonObject = new JSONObject();
|
JSONObject jsonObject = new JSONObject();
|
||||||
jsonObject.put("data", jsonArray);
|
jsonObject.put("data", jsonArray);
|
||||||
|
return String.format("""
|
||||||
|
你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。
|
||||||
|
|
||||||
return PromptTemplate.builder()
|
【去重规则】
|
||||||
.renderer(
|
1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。
|
||||||
StTemplateRenderer.builder()
|
2. **合并同类项**:将表达同一主题或问题的文本归为一组。
|
||||||
.startDelimiterToken('<')
|
3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。
|
||||||
.endDelimiterToken('>')
|
4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。
|
||||||
.build()
|
请按照下述格式返回,已被剔除掉的数据无需返回
|
||||||
)
|
{
|
||||||
.template("""
|
"questions": [1, 2, 3, .....]
|
||||||
请对以下题库JSON数据进行智能去重处理。
|
}
|
||||||
|
分类规则:
|
||||||
## 任务说明
|
|
||||||
识别并移除语义相似或表达意思基本相同的重复题目,只保留每个独特题目的一个版本。
|
|
||||||
|
|
||||||
## 语义相似度判断标准
|
|
||||||
1. 核心意思相同:即使表述不同,但考察的知识点和答案逻辑一致
|
|
||||||
2. 同义替换:使用同义词、近义词但意思不变的题目
|
|
||||||
3. 句式变换:主动被动语态转换、疑问词替换等句式变化
|
|
||||||
4. 冗余表述:增加了无关修饰词但核心内容相同的题目
|
|
||||||
|
|
||||||
## 处理规则
|
|
||||||
- 对语义相似的题目组,只保留其中一条数据
|
|
||||||
- 保留原则:选择表述最清晰、最完整的那条
|
|
||||||
- 如果难以判断,保留ID较小或创建时间较早的那条
|
|
||||||
|
|
||||||
## 输出要求
|
|
||||||
1. 只返回JSON,不要其他解释文字
|
1. 只返回JSON,不要其他解释文字
|
||||||
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
||||||
3. 请严格按照网络接口的形式返回JSON数据!!!
|
3. 请严格按照网络接口的形式返回JSON数据!!!
|
||||||
{
|
【请处理以下数据列表】:
|
||||||
"questionIds": "1, 2, 3" # 请返回保留数据的id
|
%s
|
||||||
}
|
""", jsonObject.toJSONString());
|
||||||
|
|
||||||
## 特殊说明
|
|
||||||
- 注意区分真正重复和只是题型相似的题目
|
|
||||||
- 对于选择题,要同时考虑题干和选项的语义相似度
|
|
||||||
- 保留题目版本的完整性
|
|
||||||
|
|
||||||
请处理以下JSON数据:
|
|
||||||
<data>
|
|
||||||
""")
|
|
||||||
.build()
|
|
||||||
.render(Map.of("data", jsonObject.toJSONString()))
|
|
||||||
;
|
|
||||||
|
|
||||||
// TODO: 检查这些注释代码是否可以删除 - 这是旧的prompt模板实现
|
|
||||||
// return String.format("""
|
|
||||||
// 你是一个数据清洗与去重专家。你的任务是从以下文本列表中,识别出语义相同或高度相似的条目,并为每一组相似条目筛选出一个最规范、最简洁的代表性版本。
|
|
||||||
//
|
|
||||||
// 【去重规则】
|
|
||||||
// 1. **语义核心优先**:忽略无关的修饰词、标点符号、后缀(如“:2”)、大小写和空格。
|
|
||||||
// 2. **合并同类项**:将表达同一主题或问题的文本归为一组。
|
|
||||||
// 3. **选择标准**:从每一组中,选出那个最完整、最简洁、且没有多余符号(如序号、特殊后缀)的版本作为代表。如果两个版本质量相当,优先选择更短的那个。
|
|
||||||
// 4. **保留原意**:确保选出的代表版本没有改变原文本的核心含义。
|
|
||||||
// 请返回数据的id,已被剔除掉的数据无需返回,格式如下:
|
|
||||||
// {
|
|
||||||
// "questions": [1, 2, 3, .....]
|
|
||||||
// }
|
|
||||||
// 分类规则:
|
|
||||||
// 1. 只返回JSON,不要其他解释文字
|
|
||||||
// 2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
|
||||||
// 3. 请严格按照网络接口的形式返回JSON数据!!!
|
|
||||||
// 【请处理以下数据列表】:
|
|
||||||
// %s
|
|
||||||
// """, jsonObject.toJSONString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -267,7 +211,7 @@ public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> i
|
|||||||
if (CollectionUtil.isNotEmpty(dto.getCategoryIds())) {
|
if (CollectionUtil.isNotEmpty(dto.getCategoryIds())) {
|
||||||
questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds());
|
questionCategories = questionCategoryService.batchFindByAncestorIdsUnion(dto.getCategoryIds());
|
||||||
if (CollectionUtil.isNotEmpty(questionCategories)) {
|
if (CollectionUtil.isNotEmpty(questionCategories)) {
|
||||||
treeList = TreeUtils.buildTree(
|
treeList = TreeUtil.buildTree(
|
||||||
questionCategories,
|
questionCategories,
|
||||||
QuestionCategory::getId,
|
QuestionCategory::getId,
|
||||||
QuestionCategory::getParentId,
|
QuestionCategory::getParentId,
|
||||||
|
|||||||
35
src/main/java/com/qingqiu/interview/service/llm/LlmService.java
Executable file
35
src/main/java/com/qingqiu/interview/service/llm/LlmService.java
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
191
src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
Executable file
191
src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
Executable file
@@ -0,0 +1,191 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,43 +1,14 @@
|
|||||||
|
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
|
|
||||||
|
|
||||||
# TODO: 考虑删除已注释的配置
|
|
||||||
# ai:
|
# ai:
|
||||||
# openai:
|
# openai:
|
||||||
# api-key: sk-faaa2a1b485442ccbf115ff1271a3480
|
# api-key: sk-faaa2a1b485442ccbf115ff1271a3480
|
||||||
@@ -45,14 +16,6 @@ spring:
|
|||||||
# chat:
|
# chat:
|
||||||
# options:
|
# options:
|
||||||
# model: deepseek-chat
|
# model: deepseek-chat
|
||||||
logging:
|
|
||||||
level:
|
|
||||||
org:
|
|
||||||
springframework:
|
|
||||||
ai:
|
|
||||||
chat:
|
|
||||||
client:
|
|
||||||
advisor: debug
|
|
||||||
mybatis-plus:
|
mybatis-plus:
|
||||||
configuration:
|
configuration:
|
||||||
map-underscore-to-camel-case: true
|
map-underscore-to-camel-case: true
|
||||||
|
|||||||
Reference in New Issue
Block a user