第1853篇:AI工程师面试的高频考点——2025年实际遇到的题目与答题思路
第1853篇:AI工程师面试的高频考点——2025年实际遇到的题目与答题思路
去年帮一家公司做了几轮AI工程师候选人的技术面试,也亲自参加过几次面试。积累了一些真实的题目和答题思路,今天整理出来分享。
说明一下:这里说的AI工程师,不是研究型的ML科学家,而是能把AI能力落地到产品里的工程师。这两类岗位的面试方向差异很大,本文聚焦后者。
面试前的认知校准
先说一个重要的认知:2025年的AI工程师面试,和2023年已经很不一样了。
2023年,很多公司刚开始做AI项目,面试官自己也不一定完全清楚要考什么。那时候只要会调API、能搭RAG,基本就能通过。
2025年,大部分公司已经有了一定的AI项目积累,踩过坑,也遇到过各种工程问题。面试官更关心的是:候选人有没有真实的项目经验,有没有系统性的工程思维,遇到问题时会不会独立解决。
这意味着:背诵知识点的价值在降低,真实项目经验和工程化能力的价值在提升。
高频题目分类整理
第一类:RAG系统相关
这是目前AI工程类岗位出现频率最高的话题。大多数落地AI项目都包含某种形式的RAG,所以面试官特别关注候选人对RAG的理解深度。
题目一:描述一下你设计的RAG系统架构
这道题看似基础,实际上是个开放题,考的是你的系统化思维。
差的回答:把RAG的基本流程背一遍——切文档、向量化、存储、检索、生成。
好的回答:从业务需求出发,说明为什么要用RAG,然后描述你的具体选型决策(为什么选这个embedding模型、为什么选这个向量数据库、chunk size怎么定的),最后说说上线后遇到的问题和优化措施。
题目二:RAG效果不好,你会从哪些维度排查?
这道题考的是系统性的问题定位能力。
答题框架:把RAG拆成"检索"和"生成"两个模块,分别评估。
检索模块的问题通常有:
- 命中率低:知识库缺文档、embedding模型对专业领域理解不够、chunk size太大导致语义稀释
- 噪声多:相似度阈值太低、chunk划分不合理导致同类内容散落在多个chunk里
生成模块的问题通常有:
- 幻觉:模型在上下文信息不足时"脑补"了内容
- 答案不聚焦:prompt没有明确限制输出范围
- 格式错乱:输出格式要求没有在prompt里说清楚
还有一类问题是"检索是对的,但生成时没有充分利用检索结果",这通常是prompt设计的问题。
题目三:如何评估RAG系统的质量?
标准答案包括三个维度:
- 忠实度(Faithfulness):答案中有多少内容能在检索到的文档中找到依据
- 答案相关性(Answer Relevance):答案是否真正回答了问题
- 上下文精确率/召回率(Context Precision/Recall):检索到的文档中有多少是相关的,以及相关文档有多少被检索到
有实际项目经验的加分项:说说你用什么工具做评估(RAGAS、自己写的评估框架等),以及如何建立评估数据集。
第二类:LLM应用工程化
题目四:如何处理LLM的幻觉问题?
这是个很大的话题,面试官通常想听的是有层次的回答:
预防层面:
- 提供高质量的上下文(RAG的基本作用)
- 在prompt里明确要求模型在不确定时说"不知道"
- 避免让模型做超出其能力范围的任务(比如让它做精确计算)
检测层面:
- 输出一致性检测:对同一问题多次采样,看答案是否一致
- 基于检索内容的事实核查:让第二个LLM检查答案是否与提供的上下文一致
- 置信度估计:某些框架支持获取token级别的概率,低概率token多的答案可信度低
处理层面:
- 低置信度答案降级处理(转人工、提示用户核实)
- 关键事实让模型标注来源,便于用户验证
题目五:Token限制下如何处理长文档?
这道题考的是工程实践能力,有几种常见方案:
方案一:Map-Reduce
/**
* 长文档摘要生成 - Map-Reduce方式
*/
@Service
public class LongDocumentSummaryService {
private final LlmClient llmClient;
private static final int MAX_CHUNK_TOKENS = 3000;
private static final int MAX_PARALLEL_REQUESTS = 5;
public String summarizeLongDocument(String document, String task) {
// 1. 将长文档切分成块
List<String> chunks = splitIntoChunks(document, MAX_CHUNK_TOKENS);
if (chunks.size() == 1) {
// 文档足够短,直接处理
return processChunk(chunks.get(0), task);
}
// 2. Map阶段:并行处理每个块
List<CompletableFuture<String>> futures = chunks.stream()
.map(chunk -> CompletableFuture.supplyAsync(
() -> processChunk(chunk, task),
Executors.newFixedThreadPool(MAX_PARALLEL_REQUESTS)
))
.collect(Collectors.toList());
List<String> chunkResults = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
// 3. Reduce阶段:合并中间结果
return reduceResults(chunkResults, task);
}
private String processChunk(String chunk, String task) {
String prompt = String.format("""
请针对以下文本内容,执行任务:%s
文本内容:
%s
请给出简洁的结果:
""", task, chunk);
return llmClient.call(prompt);
}
private String reduceResults(List<String> chunkResults, String originalTask) {
String combinedResults = IntStream.range(0, chunkResults.size())
.mapToObj(i -> "片段" + (i+1) + "的结果:\n" + chunkResults.get(i))
.collect(Collectors.joining("\n\n"));
String reducePrompt = String.format("""
以下是对一篇长文档各部分的处理结果,请综合这些结果,给出完整的%s。
各部分结果:
%s
综合结果:
""", originalTask, combinedResults);
return llmClient.call(reducePrompt);
}
private List<String> splitIntoChunks(String text, int maxTokens) {
// 按段落分割,估算token数(中文约1token/字)
String[] paragraphs = text.split("\n\n");
List<String> chunks = new ArrayList<>();
StringBuilder currentChunk = new StringBuilder();
for (String para : paragraphs) {
if (currentChunk.length() + para.length() > maxTokens &&
currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
}
currentChunk.append(para).append("\n\n");
}
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks;
}
}方案二:分层摘要(Hierarchical Summarization) 先对每个chunk生成摘要,再对摘要生成摘要,层层压缩。适合需要全文理解的场景。
方案三:关键段落选取 对于问答类任务,不需要处理全文,用RAG检索相关段落就够了。
第三类:AI Agent相关
2025年Agent已经从概念变成了实际落地的技术方向,面试中出现频率明显增加。
题目六:描述一下你对AI Agent的理解,以及你做过的Agent项目
面试官想知道的:你理解Agent的本质是什么,而不只是会用LangChain的AgentExecutor。
我的理解:Agent本质上是一个"思考-行动-观察"的循环,LLM负责决策,工具负责执行,结果反馈回LLM继续决策,直到任务完成。
关键点:
- LLM的规划能力是Agent效果的上限
- 工具设计要简单清晰,工具越多越复杂,LLM越容易选错工具
- 必须有失败处理机制,Agent不会总按预期执行
/**
* 简化的Agent执行框架
*/
@Service
public class SimpleAgentService {
private final LlmClient llmClient;
private final Map<String, AgentTool> tools;
private static final int MAX_ITERATIONS = 10;
public AgentResult execute(String task, List<AgentTool> availableTools) {
// 初始化工具注册表
Map<String, AgentTool> toolMap = availableTools.stream()
.collect(Collectors.toMap(AgentTool::getName, t -> t));
List<AgentStep> steps = new ArrayList<>();
String currentContext = buildInitialContext(task, availableTools);
for (int i = 0; i < MAX_ITERATIONS; i++) {
// 让LLM决定下一步行动
AgentDecision decision = parseDecision(llmClient.call(currentContext));
if (decision.isFinished()) {
return AgentResult.success(decision.getFinalAnswer(), steps);
}
if (!toolMap.containsKey(decision.getToolName())) {
// LLM选了不存在的工具,提示它重新选择
currentContext += "\n\n系统提示:工具'" + decision.getToolName() +
"'不存在,请重新选择可用工具。";
continue;
}
// 执行工具调用
AgentTool tool = toolMap.get(decision.getToolName());
ToolResult toolResult;
try {
toolResult = tool.execute(decision.getToolInput());
} catch (Exception e) {
toolResult = ToolResult.error("工具执行失败:" + e.getMessage());
}
// 记录步骤
steps.add(AgentStep.builder()
.thought(decision.getThought())
.toolName(decision.getToolName())
.toolInput(decision.getToolInput())
.toolResult(toolResult.getContent())
.build());
// 更新上下文
currentContext += String.format("""
\n思考:%s
行动:%s
行动输入:%s
观察:%s
""",
decision.getThought(),
decision.getToolName(),
decision.getToolInput(),
toolResult.getContent()
);
}
return AgentResult.failed("超过最大迭代次数,任务未完成", steps);
}
private String buildInitialContext(String task, List<AgentTool> tools) {
String toolDescriptions = tools.stream()
.map(t -> String.format("- %s: %s", t.getName(), t.getDescription()))
.collect(Collectors.joining("\n"));
return String.format("""
你是一个任务执行助手,可以使用以下工具来完成任务:
可用工具:
%s
执行格式(严格按照此格式输出):
思考:[你对当前情况的分析]
行动:[工具名称]
行动输入:[工具的输入内容]
或者当任务完成时:
思考:[总结]
最终答案:[答案内容]
任务:%s
开始执行:
""", toolDescriptions, task);
}
private AgentDecision parseDecision(String llmOutput) {
// 解析LLM输出,提取思考、行动、输入
AgentDecision decision = new AgentDecision();
String[] lines = llmOutput.split("\n");
for (String line : lines) {
if (line.startsWith("思考:")) {
decision.setThought(line.substring(3).trim());
} else if (line.startsWith("行动:")) {
decision.setToolName(line.substring(3).trim());
} else if (line.startsWith("行动输入:")) {
decision.setToolInput(line.substring(5).trim());
} else if (line.startsWith("最终答案:")) {
decision.setFinished(true);
decision.setFinalAnswer(line.substring(5).trim());
}
}
return decision;
}
}题目七:如何保证Agent执行的可靠性?
这是2025年的高频问题,因为很多公司已经踩过Agent可靠性的坑。
答题要点:
- 工具调用失败的重试和降级机制
- 执行历史记录和可回溯性(出了问题能追溯)
- 人工干预钩子(关键步骤需要人工确认)
- 最大迭代次数限制(防止死循环)
- 工具权限控制(不同的任务只能用特定的工具)
第四类:系统设计题
题目八:设计一个每天处理百万条数据的AI分析系统
这道题考的是工程综合能力。好的回答包括:
明确需求和约束:
- 延迟要求(实时还是批量)
- 准确率要求(多少幻觉可以接受)
- 成本预算(影响模型选型)
架构设计:
成本估算:百万条数据,如果每条平均1000 token,GPT-4的价格大概是多少,GPT-3.5大概是多少,哪些可以用本地模型处理。
监控和异常处理:批量处理中任何一条失败不能影响整体流程,需要死信队列处理失败数据,需要监控每批次的质量指标。
第五类:编程题
面试中也会有编程题,但AI工程师面试的编程题通常不是算法题,而是设计类编程题。
典型题目:实现一个支持流式输出的对话系统
@RestController
@RequestMapping("/api/chat")
public class StreamingChatController {
private final StreamingLlmClient llmClient;
private final ConversationHistoryService historyService;
/**
* 流式对话接口
*/
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> streamChat(
@RequestBody ChatRequest request,
@RequestHeader(value = "X-Session-Id") String sessionId) {
// 获取历史对话
List<Message> history = historyService.getHistory(sessionId);
// 构建包含历史的消息列表
List<Message> messages = new ArrayList<>(history);
messages.add(Message.user(request.getMessage()));
// 记录用户消息
historyService.saveMessage(sessionId, Message.user(request.getMessage()));
// 收集完整回答(用于保存到历史)
StringBuilder fullResponse = new StringBuilder();
return llmClient.streamChat(messages)
.map(token -> {
fullResponse.append(token);
return ServerSentEvent.<String>builder()
.data(token)
.build();
})
.doOnComplete(() -> {
// 流式输出完成后,保存完整回答到历史
historyService.saveMessage(
sessionId,
Message.assistant(fullResponse.toString())
);
})
.doOnError(error -> {
log.error("Stream chat error for session: {}", sessionId, error);
historyService.saveMessage(
sessionId,
Message.system("系统错误,请重试")
);
})
.onErrorReturn(
ServerSentEvent.<String>builder()
.data("[ERROR]")
.build()
);
}
}这道题考的不只是会写代码,还考验你对SSE的理解、流式处理的异常处理、对话历史管理等工程细节。
答题时的加分项
1. 说踩过的坑
面试官听到最多的是背书一样的答案。如果你能说"我们用了X方案,但发现Y问题,后来改成了Z方案,效果是这样的",这会让你显得非常真实,区分度很高。
2. 主动提到权衡取舍
不要说"最好的方案是XXX",要说"在我们的场景下,考虑到延迟要求和成本预算,选择了XXX,代价是YYY,我们通过ZZZ来弥补"。好的工程师理解没有完美方案,只有合适方案。
3. 数字化你的成果
"优化了RAG效果"不如"通过混合检索和重排序,把答案准确率从68%提升到了84%"。有数字的成果让人印象深刻。
4. 展示系统化思考
遇到开放性问题,先说"我会从以下几个维度来分析",然后有条理地展开。这比想到哪说到哪要好很多。
容易失分的地方
1. 只知道概念,没有实践
说得头头是道,但追问"你在项目里怎么做的"就卡壳了。这是最致命的失分项。
2. 拿来主义,不懂原理
"我用LangChain搭了一个RAG"没问题,但如果面试官问"LangChain的RetrievalQAChain内部是怎么工作的"就说不出来,会让人对你的技术深度产生怀疑。
3. 对非技术因素一无所知
成本、延迟、可维护性、团队协作,这些工程化因素同样重要。只关注技术实现、不考虑工程化问题,说明工程成熟度不够。
4. 答案太完美
真实项目都是有缺陷的。如果你的项目听起来完全没有任何问题,面试官会觉得你在说谎,或者你的项目太简单。
准备面试的最好方式还是多做真实项目,把每个项目的踩坑过程整理成清晰的故事。好的面试官不会被你的简历打动,他们会被你的真实经历打动。
