让AI不仅能当面试官,还能输出专业的多维度评估报告。本文详解Prompt设计、JSON解析容错、异步生成架构的完整实现。
一、面试报告要解决什么问题?
模拟面试最大的价值不在面试过程本身,而在于**面试后的反馈**。练完不知道哪里好、哪里差,等于白练。
我们需要AI在面试结束后自动生成一份结构化的评估报告:
{
"totalScore": 75,
"dimensions": {
"技术深度": 78,
"表达清晰度": 85,
"逻辑思维": 72,
"知识广度": 68,
"实战经验": 80
},
"summary": "候选人在Java基础方面表现扎实...",
"suggestions": "1. 建议加强JVM调优方面的学习..."
}
核心挑战:让大模型输出可解析的结构化JSON,而不是自由散文。
二、数据模型
sql
CREATE TABLE interview_report (
id BIGSERIAL PRIMARY KEY,
session_id BIGINT UNIQUE NOT NULL,
total_score INT,
dimensions JSONB, -- 多维度评分,结构灵活
summary TEXT, -- AI总结
suggestions TEXT, -- AI建议
created_at TIMESTAMP DEFAULT NOW()
);
`dimensions`用JSONB存储------不同面试方向可以有不同维度(Java面试评"JVM理解",前端面试评"CSS布局"),无需改表。
三、Prompt设计:让AI输出结构化数据
3.1 报告生成Prompt(完整版)
你是一位资深的技术面试评估专家。请根据以下面试记录生成详细评估报告。
方向:Java后端
难度:3/5
题目数量:5
Q1: 请介绍HashMap的底层实现原理。
A1: HashMap底层是数组加链表,当链表长度超过8会转为红黑树...
Q2: 为什么阈值是8?
A2: 因为泊松分布,链表长度达到8的概率极低,大约是千万分之六...
Q3: ConcurrentHashMap如何保证线程安全?
A3: 1.7用分段锁Segment,1.8改用CAS+synchronized锁单个Node...
请严格按照以下JSON格式输出,不要包含任何JSON之外的内容:
{
"totalScore": <0-100的整数>,
"dimensions": {
"技术深度": <0-100>,
"表达清晰度": <0-100>,
"逻辑思维": <0-100>,
"知识广度": <0-100>,
"实战经验": <0-100>
},
"summary": "<150字以内的整体评价>",
"suggestions": "<3-5条具体改进建议,每条一行>"
}
90-100:优秀,回答深入全面,有独到见解和实战经验
75-89:良好,回答正确但深度不足
60-74:一般,基础知识有欠缺
60以下:较差,核心概念理解不清
总分应与各维度加权平均大致一致
至少一个维度分数应低于总分10分以上(体现区分度)
回答"不知道"或明显编造的题目,该维度最高40分
回答字数少于50字且无实质内容的,该维度最高50分
3.2 Prompt设计的6个技巧
技巧1:给完整JSON示例而不是只说"输出JSON"
AI看到具体格式后遵循率远高于抽象描述。
技巧2:评分锚点校准
"90-100分是优秀"这样的说明让AI有统一的评分尺度,避免所有人都得70-80分。
技巧3:负面约束
"不要包含JSON之外的内容"------防止AI在JSON前加"好的,以下是评估报告:"导致解析失败。
技巧4:分数逻辑约束
"总分应与各维度加权平均一致"------防止AI给出逻辑矛盾的评分(维度都是60但总分85)。
技巧5:区分度要求
"至少一个维度低于总分10分"------防止所有维度分数都差不多,没有参考价值。
技巧6:惩罚空回答
明确说"回答不知道最高40分"------让AI对差的回答敢打低分,而不是"政治正确"地都给及格。
四、后端实现
4.1 异步生成架构
报告生成需要调用大模型(非流式),通常耗时5-15秒,不能让用户同步等待。
用户点击"结束面试"
↓ POST /session/{id}/end
Controller立即返回 "面试已结束,报告生成中"
↓ 异步
CompletableFuture.runAsync → InterviewService.generateReport()
↓ 调用AI(非流式,等完整响应)
↓ 解析JSON → 保存到interview_report表
↓ 完成
前端轮询 GET /session/{id}/report(每3秒,最多10次)
↓ report存在?
├── 否 → 返回404,继续轮询
└── 是 → 返回报告数据,展示页面
4.2 报告生成Service
java
public InterviewReport generateReport(Long sessionId) {
InterviewSession session = sessionMapper.selectById(sessionId);
// 1. 获取所有面试消息
List<InterviewMessage> messages = messageMapper.selectList(
new LambdaQueryWrapper<InterviewMessage>()
.eq(InterviewMessage::getSessionId, sessionId)
.orderByAsc(InterviewMessage::getCreatedAt));
// 2. 拼装问答记录
StringBuilder qaText = new StringBuilder();
int qNum = 1;
for (InterviewMessage msg : messages) {
if ("INTERVIEWER".equals(msg.getRole())) {
qaText.append("Q").append(qNum).append(": ")
.append(msg.getContent()).append("\n");
} else {
qaText.append("A").append(qNum).append(": ")
.append(msg.getContent()).append("\n\n");
qNum++;
}
}
// 3. 组装完整prompt
String prompt = buildReportPrompt(
session.getDirection(),
session.getDifficulty(),
session.getQuestionCount(),
qaText.toString());
// 4. 非流式调用AI(需要完整JSON)
String aiResponse = aiService.chat(prompt);
// 5. 解析并保存
return parseAndSaveReport(sessionId, aiResponse);
}
4.3 JSON解析与容错(重点)
AI输出的JSON不总是完美的,需要层层容错:
java
private InterviewReport parseAndSaveReport(Long sessionId, String response) {
InterviewReport report = new InterviewReport();
report.setSessionId(sessionId);
try {
// 第1层:从AI响应中提取JSON
String json = extractJson(response);
// 第2层:解析JSON
JSONObject obj = JSON.parseObject(json);
report.setTotalScore(
clamp(obj.getIntValue("totalScore"), 0, 100)); // 限制0-100
report.setDimensions(obj.getJSONObject("dimensions").toJSONString());
report.setSummary(obj.getString("summary"));
report.setSuggestions(obj.getString("suggestions"));
} catch (Exception e) {
log.error("解析AI报告失败, sessionId={}", sessionId, e);
// 第3层:降级方案------保存原文,给默认分数
report.setTotalScore(70);
report.setDimensions("{\"综合评估\": 70}");
report.setSummary(response); // 把原始回复当summary
report.setSuggestions("AI评估结果解析异常,以上为原始评价内容。");
}
reportMapper.insert(report);
return report;
}
/**
* 从AI响应中提取JSON
* 处理3种常见情况:
* 1. 纯JSON
* 2. ```json ... ``` 包裹
* 3. JSON前后有说明文字
*/
private String extractJson(String response) {
// 情况2:```json块
Matcher matcher = Pattern.compile("```json\\s*(.+?)\\s*```",
Pattern.DOTALL).matcher(response);
if (matcher.find()) {
return matcher.group(1).trim();
}
// 情况3:提取第一个完整的{...}块
int start = response.indexOf('{');
int end = response.lastIndexOf('}');
if (start >= 0 && end > start) {
return response.substring(start, end + 1);
}
// 情况1:假设整个响应就是JSON
return response.trim();
}
private int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
4.4 Controller层
java
// 结束面试 + 异步生成报告
@PostMapping("/session/{id}/end")
public R<?> endSession(@PathVariable Long id) {
Long userId = StpUtil.getLoginIdAsLong();
interviewService.endSession(id, userId);
// 异步生成报告
CompletableFuture.runAsync(() -> {
try {
interviewService.generateReport(id);
} catch (Exception e) {
log.error("报告生成失败, sessionId={}", id, e);
}
});
return R.ok("面试已结束,报告生成中");
}
// 查询报告(前端轮询调用)
@GetMapping("/session/{id}/report")
public R<?> getReport(@PathVariable Long id) {
InterviewReport report = reportMapper.selectOne(
new LambdaQueryWrapper<InterviewReport>()
.eq(InterviewReport::getSessionId, id));
if (report == null) {
return R.fail(404, "报告生成中,请稍候...");
}
return R.ok(report);
}
五、前端轮询与展示
5.1 轮询获取报告
TypeScript
const Report: React.FC = () => {
const { sessionId } = useParams();
const [report, setReport] = useState<ReportData | null>(null);
const [loading, setLoading] = useState(true);
const [retryCount, setRetryCount] = useState(0);
useEffect(() => {
let timer: number;
const fetchReport = async () => {
try {
const res = await api.get(`/interview/session/${sessionId}/report`);
setReport(res.data);
setLoading(false);
} catch (e: any) {
if (e.response?.status === 404 && retryCount < 10) {
// 报告还在生成,3秒后重试
timer = setTimeout(() => {
setRetryCount(prev => prev + 1);
}, 3000);
} else {
message.error('报告获取失败');
setLoading(false);
}
}
};
fetchReport();
return () => clearTimeout(timer);
}, [sessionId, retryCount]);
if (loading) {
return (
<div style={{ textAlign: 'center', padding: 80 }}>
<Spin size="large" />
<p style={{ marginTop: 16, color: '#999' }}>
AI正在生成评估报告... ({retryCount}/10)
</p>
</div>
);
}
return <ReportContent report={report!} />;
};
5.2 多维度进度条展示
TypeScript
const DimensionScores: React.FC<{ dimensions: Record<string, number> }> =
({ dimensions }) => (
<Card title="维度评分">
{Object.entries(dimensions).map(([name, score]) => (
<div key={name} style={{ marginBottom: 20 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 500 }}>{name}</span>
<span style={{
fontWeight: 'bold',
color: score >= 80 ? '#52c41a' : score >= 60 ? '#faad14' : '#f5222d'
}}>
{score}分
</span>
</div>
<Progress
percent={score}
strokeColor={
score >= 80 ? '#52c41a' : score >= 60 ? '#faad14' : '#f5222d'
}
showInfo={false}
/>
</div>
))}
</Card>
);
5.3 总分大盘展示
TypeScript
const ScoreOverview: React.FC<{ score: number }> = ({ score }) => {
const getLevel = (s: number) => {
if (s >= 90) return { text: '优秀', color: '#52c41a' };
if (s >= 75) return { text: '良好', color: '#1890ff' };
if (s >= 60) return { text: '一般', color: '#faad14' };
return { text: '待提升', color: '#f5222d' };
};
const level = getLevel(score);
return (
<Card style={{ textAlign: 'center' }}>
<div style={{ fontSize: 64, fontWeight: 'bold', color: level.color }}>
{score}
</div>
<Tag color={level.color} style={{ fontSize: 16, padding: '4px 16px' }}>
{level.text}
</Tag>
</Card>
);
};
六、Prompt迭代优化实录
6.1 v1版本的问题
初版Prompt只说"请打分并给建议",结果:
-
所有人都是72-78分,区分度极差
-
AI不敢打低分,"政治正确"
-
输出格式不稳定,经常JSON前面带一段废话
6.2 逐步优化
|--------|-----------------------|-----------|---------------|
| 版本 | 改进内容 | 分数标准差 | JSON解析成功率 |
| v1 | 基础prompt | 5.2 | 73% |
| v2 | 增加评分锚点(90/75/60) | 8.7 | 73% |
| v3 | 增加"不要输出JSON外内容" | 8.7 | 95% |
| v4 | 增加负面约束和区分度要求 | 14.1 | 95% |
| v5 | 增加```json提取 + 降级方案 | 14.1 | 99.5% |
v5的99.5%解析成功率中,剩余0.5%由降级方案兜底(保存原文 + 默认分数),实际上线零报告丢失。
6.3 关键教训
-
别指望AI一次就对 ------必须有容错和降级
-
给AI明确的约束比给自由更重要------越自由越不可控
-
用数据验证Prompt效果 ------"感觉更好了"不算,要看标准差和解析成功率
-
Prompt是迭代出来的------没有人能一次写对
七、总结
AI面试报告生成的技术要点:
-
Prompt工程 :明确格式 + 评分锚点 + 负面约束 + 区分度要求
-
JSON容错解析:正则提取 → JSON.parse → 降级方案,三层保障
-
异步架构 :CompletableFuture异步生成 + 前端轮询,解耦用户等待
-
JSONB存储 :维度字段灵活可扩展,不同方向不同维度
-
持续迭代:用解析成功率和分数标准差量化Prompt效果
在线体验地址:[http://106.12.14.47:8090/\]
测试账号:
-
手机号:18088889999
-
密码:test123#$qaz
也可以自行注册,新用户自动赠送3次免费面试机会。完成一次面试后即可查看AI生成的评估报告。