一、技术选型与架构设计
1.1 核心架构分层
系统采用 Spring Cloud 微服务架构,按业务域拆分为以下核心服务-4:
┌─────────────────────────────────────────────────────┐ │ 前端层 │ │ WebRTC语音 / WebSocket实时通信 │ └─────────────────┬───────────────────────────────────┘ │ ┌─────────────────▼───────────────────────────────────┐ │ API网关 (Spring Cloud Gateway) │ │ 路由 / 鉴权 / 限流 / JWT校验 │ └─────────────────┬───────────────────────────────────┘ │ ┌───────────────┼───────────────┬───────────────┐ ▼ ▼ ▼ ▼ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │用户服务 │ │面试服务 │ │题库服务 │ │推理服务 │ │用户/权限 │ │会话管理 │ │RAG检索 │ │LLM推理 │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ └───────────────┴───────┬───────┴───────────────┘ ▼ ┌─────────────────────────┐ │ 基础设施层 │ │ Nacos/Redis/Milvus/Kafka │ └─────────────────────────┘
1.2 技术栈清单
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 微服务框架 | Spring Cloud Alibaba + Nacos | 服务注册发现、配置管理 |
| 网关 | Spring Cloud Gateway | 路由、鉴权、限流 |
| 大模型 | Ollama + Qwen 本地部署 | 数据安全,避免敏感数据外传-2 |
| 向量数据库 | Milvus / RedisVector | 存储知识库Embedding,支持RAG检索-3 |
| 缓存 | Redis | 会话状态、热点数据缓存 |
| 消息队列 | Kafka | 异步处理面试记录、日志-5 |
| 容错 | Resilience4j | 熔断、限流、重试-10 |
二、本地大模型部署与集成
2.1 模型选型与部署
核心原则:数据安全第一。 面试涉及候选人简历、回答等敏感信息,不能调用公有云API。推荐采用 Ollama + Qwen 本地部署方案-2。
# 安装 Ollamacurl -fsSL https://ollama.com/install.sh | sh# 拉取并运行千问模型(根据硬件选择版本)ollama pull qwen2:7b ollama run qwen2:7b
2.2 Spring Boot 集成 Ollama
使用 Spring AI 框架封装模型调用,支持流式输出和同步推理-8-2:
<!-- pom.xml 依赖 --><dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId> <version>0.8.0</version></dependency>
配置类:
@Configurationpublic class OllamaConfig {
@Bean
public OllamaChatModel ollamaChatModel() {
return new OllamaChatModel(
OllamaApi.builder()
.baseUrl("http://localhost:11434")
.build(),
OllamaChatOptions.builder()
.model("qwen2:7b")
.temperature(0.7) // 控制输出随机性
.topP(0.9)
.build()
);
}}推理服务封装:
@Service@Slf4jpublic class InterviewInferenceService {
@Autowired
private OllamaChatModel chatModel;
/**
* 同步推理 - 用于评估回答
*/ public String evaluateAnswer(String question, String candidateAnswer) {
String prompt = String.format(
"你是一位专业的面试官,请评估候选人对以下问题的回答质量。\n" +
"问题:%s\n" +
"回答:%s\n" +
"请从表达能力、逻辑性、专业性三个维度评分(0-10分),并给出简短评语。",
question, candidateAnswer );
ChatResponse response = chatModel.call(
new Prompt(prompt, OllamaChatOptions.DEFAULT)
);
return response.getResult().getOutput().getContent();
}
/**
* 流式推理 - 用于面试对话实时生成
*/ public Flux<String> streamChat(String prompt) {
Prompt chatPrompt = new Prompt(prompt);
return chatModel.stream(chatPrompt)
.map(response -> response.getResult().getOutput().getContent());
}}三、微服务核心实现
3.1 服务注册与发现(Nacos)
# application.ymlspring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
enabled: true
service: ${spring.application.name}3.2 服务间通信(Feign Client)
AI推理服务需要调用题库服务和用户服务,通过 Feign 实现声明式调用,配合 Hystrix 熔断降级-4:
@FeignClient(
name = "question-bank-service",
fallback = QuestionBankFallback.class)public interface QuestionBankClient {
@GetMapping("/api/questions/random")
ResponseEntity<QuestionDTO> getRandomQuestion(
@RequestParam("skill") String skill,
@RequestParam("level") String level );}@Component@Slf4jpublic class QuestionBankFallback implements QuestionBankClient {
@Override
public ResponseEntity<QuestionDTO> getRandomQuestion(String skill, String level) {
log.warn("题库服务不可用,使用降级题库");
// 返回默认题目
return ResponseEntity.ok(
QuestionDTO.builder()
.content("请介绍一下你的技术栈和项目经验")
.difficulty("medium")
.build()
);
}}3.3 服务容错(Resilience4j)
大模型推理可能耗时较长或超时,必须配置熔断和限流-9-10:
resilience4j: circuitbreaker: instances: inferenceService: failureRateThreshold: 50 # 失败率阈值 slowCallRateThreshold: 50 slowCallDurationThreshold: 5s waitDurationInOpenState: 10s permittedNumberOfCallsInHalfOpenState: 3 slidingWindowType: TIME_BASED slidingWindowSize: 60
@Servicepublic class InferenceServiceWithResilience {
@CircuitBreaker(name = "inferenceService", fallbackMethod = "fallbackInference")
public String infer(String prompt) {
// 调用大模型推理
return ollamaChatModel.call(prompt);
}
public String fallbackInference(String prompt, Throwable t) {
log.error("推理服务熔断降级: {}", t.getMessage());
return "系统繁忙,请稍后重试。您的回答已保存,将由人工复核。";
}}四、核心难点攻克
4.1 问题一:AI 幻觉导致瞎编答案
场景:候选人问「请评价我的回答」,AI 可能凭空捏造评分。
解决方案:RAG 检索增强生成。将面试评估标准、岗位能力模型等知识文档向量化,推理时先检索相关内容作为上下文-2。
知识库构建:
@Componentpublic class KnowledgeBaseLoader {
@Autowired
private EmbeddingModel embeddingModel;
@Autowired
private VectorStore vectorStore; // Milvus
public void loadKnowledge() throws IOException {
// 1. 加载评估标准文档
String text = loadFile("interview-standards.pdf");
// 2. 文档分片(每500字符,重叠50字符)
Document document = new Document(text);
DocumentSplitter splitter = DocumentSplitters.recursive(500, 50);
List<TextSegment> segments = splitter.split(document);
// 3. 向量化并存入Milvus
List<Embedding> embeddings = embeddingModel.embedAll(segments);
vectorStore.add(segments, embeddings);
}}推理时检索:
public String evaluateWithRAG(String question, String answer) {
// 1. 构建查询文本
String query = String.format("问题:%s\n回答:%s", question, answer);
// 2. 向量检索 TOP-3 相关评估标准
List<EmbeddingMatch<TextSegment>> matches =
vectorStore.search(embeddingModel.embed(query), 3);
// 3. 拼接 Prompt(强制基于资料回答)
StringBuilder context = new StringBuilder();
context.append("请根据以下评估标准严格评分,不要编造标准之外的维度:\n");
for (EmbeddingMatch<TextSegment> match : matches) {
context.append(match.getEmbedded().text()).append("\n");
}
context.append("\n候选人回答:").append(answer);
// 4. 调用模型
return chatModel.call(context.toString());}关键细节:给 Prompt 加上「不要编造」约束,实际项目中能显著降低幻觉率-2。
4.2 问题二:面试对话上下文丢失
解决方案:Redis 存储会话状态 + Kafka 异步持久化-5-9
@Servicepublic class InterviewSessionService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
private static final String SESSION_PREFIX = "interview:session:";
private static final long SESSION_TTL = 3600; // 1小时超时
/**
* 保存对话上下文
*/ public void saveContext(String sessionId, String role, String content) {
String key = SESSION_PREFIX + sessionId;
List<Map<String, String>> history = getHistory(sessionId);
Map<String, String> turn = Map.of(
"role", role, // candidate / interviewer
"content", content,
"timestamp", String.valueOf(System.currentTimeMillis())
);
history.add(turn);
// 只保留最近20轮,避免上下文过长
if (history.size() > 20) {
history = history.subList(history.size() - 20, history.size());
}
redisTemplate.opsForValue().set(key, history, SESSION_TTL, TimeUnit.SECONDS);
// 异步发送到Kafka持久化
kafkaTemplate.send("interview-logs", sessionId,
JsonUtils.toJson(turn));
}
@SuppressWarnings("unchecked")
public List<Map<String, String>> getHistory(String sessionId) {
String key = SESSION_PREFIX + sessionId;
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
return new ArrayList<>();
}
return (List<Map<String, String>>) value;
}}4.3 问题三:向量库重启数据丢失
开发环境用 InMemoryEmbeddingStore 很方便,但生产环境重启后所有知识库全丢-2。
解决方案:生产环境使用 Milvus/Redis 持久化向量库:
@Configuration@ConditionalOnProperty(name = "vector.store.type", havingValue = "milvus")public class MilvusVectorStoreConfig {
@Bean
public VectorStore vectorStore() {
return new MilvusVectorStore(
MilvusVectorStoreConfig.builder()
.host("milvus-service")
.port(19530)
.collectionName("interview_knowledge")
.embeddingDimension(1536)
.build(),
embeddingModel()
);
}}五、面试流程核心逻辑
5.1 动态出题策略
根据岗位技能标签和候选人回答动态抽取题目,避免重复-3-7:
@Servicepublic class QuestionStrategy {
@Autowired
private QuestionBankClient questionClient;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 三级随机化出题
* 1. 按难度分层(初级40% / 中级40% / 高级20%)
* 2. 按技能标签匹配
* 3. 去重(Redis BloomFilter)
*/ public QuestionDTO nextQuestion(String sessionId, String skill, int level) {
// 计算题目难度分布
String difficulty = level <= 3 ? "junior" :
(level <= 7 ? "medium" : "senior");
// 调用题库服务获取题目
QuestionDTO question = questionClient.getQuestion(skill, difficulty);
// 去重检查:使用 Redis 记录已出题目
String askedKey = "asked:" + sessionId;
Boolean isDuplicate = redisTemplate.opsForSet().isMember(askedKey, question.getId());
if (Boolean.TRUE.equals(isDuplicate)) {
// 重新获取
return nextQuestion(sessionId, skill, level);
}
redisTemplate.opsForSet().add(askedKey, question.getId());
return question;
}}5.2 回答评估与评分
@Servicepublic class AnswerEvaluator {
@Autowired
private InferenceServiceWithResilience inferenceService;
public EvaluationResult evaluate(String sessionId, String question, String answer) {
String prompt = buildEvaluationPrompt(question, answer);
String result = inferenceService.infer(prompt);
return parseEvaluationResult(result);
}
private String buildEvaluationPrompt(String question, String answer) {
return String.format("""
你是一位资深技术面试官,请评估候选人的回答。
【面试问题】
%s
【候选人回答】
%s
【评估要求】
1. 从以下维度评分(每项0-10分):技术深度、逻辑清晰度、表达能力
2. 给出简短评语(50字以内)
3. 给出综合推荐意见:通过 / 待定 / 不通过
请按JSON格式输出。
""", question, answer);
}}六、部署与运维
6.1 Docker Compose 一键部署
# docker-compose.ymlversion: '3.8'services: nacos: image: nacos/nacos-server:v2.2.0 ports: - "8848:8848" environment: MODE: standalone redis: image: redis:7-alpine ports: - "6379:6379" milvus: image: milvusdb/milvus:v2.2.0 ports: - "19530:19530" environment: ETCD_ENDPOINTS: etcd:2379 ollama: image: ollama/ollama:latest ports: - "11434:11434" volumes: - ./models:/root/.ollama command: serve interview-service: build: ./interview-service ports: - "8080:8080" depends_on: - nacos - redis - milvus - ollama environment: SPRING_CLOUD_NACOS_DISCOVERY_SERVER_ADDR: nacos:8848
6.2 监控与告警
集成 Prometheus + Grafana 监控关键指标-3-4:
| 指标 | 告警阈值 | 说明 |
|---|---|---|
| 推理服务 P99 延迟 | > 5s | 模型响应过慢 |
| 推理服务错误率 | > 5% | 可能模型异常 |
| 向量检索耗时 | > 200ms | 向量库性能问题 |
| 会话超时率 | > 10% | 候选人体验下降 |
七、面试官高频追问
Q1:为什么要用本地大模型,而不是调用 OpenAI API?
数据安全是招聘系统的生命线——候选人简历、面试回答属于敏感个人信息,不得出网。本地部署虽然模型效果稍逊,但符合企业合规要求-2。
Q2:如何保证系统高可用?
三管齐下:Nacos 服务注册实现多实例部署、Resilience4j 熔断防止雪崩、Kafka 异步削峰应对突发流量-9。
Q3:AI 瞎编答案怎么办?
RAG 检索增强生成 + 规则兜底。关键数据(如候选人评分)通过代码逻辑计算,不让 AI 凭空生成-2。对于知识类回答,强制 AI「基于检索到的资料回答,不要编造」。
八、总结与演进方向
本文完整实现了一个 Spring Cloud 微服务 + 本地大模型 的 AI 面试系统,核心要点:
架构分层:网关 → 微服务 → 基础设施,清晰解耦
模型本地化:Ollama + Qwen 保障数据安全
RAG 防幻觉:向量检索 + 知识库约束 AI 输出
容错机制:熔断、降级、重试保障稳定性
下一步演进方向:
暂无评论