项目地址

Github库

在线体验

它是做什么的?

你可以把它当成普通的简历模板,直接编辑,非常方便。但是它特别的亮点是可以用 AI 润色你的语言表达——点击“AI 美化”按钮直接提取字段,自动润色。如:用户在“专业技能”部分写“git”,则 AI 会将其自动美化为类似“掌握基于 Git 的开发流程,能够规范地进行分支管理代码提交与合并”的语句。同样地,只要在相应区域简述你的工作经历、项目经历,AI 也会帮你润色。

技术栈

Next.js;React;TypeScript;Tailwind CSS

开发难点—— AI 输出结构化

为什么一定要结构化?AI 拿到的是字段,返回的也是字段。我们需要的是:姓名、邮箱、技能、项目列表……这类“字段”,所以必须要求 AI 输出 JSON,把它的回答强行变成严格的 JSON 表格——而且,如果它写坏了,我们要能自动修好,保证系统永远有一个可用结果。

结构化的步骤是什么?首先,在代码里定义严格的 schema(结构规则),即这张“表格”长什么样。ResumeSchema 规定简历的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const ResumeSchema = z.object({
basics: z.object({
name: z.string(),
title: z.string(),
email: z.string(),
phone: z.string(),
location: z.string(),
website: z.string(),
summary: z.string(),
links: z.array(
z.object({
label: z.string(),
url: z.string(),
})
),
}),
skills: z.array(z.string()),
experience: z.array(
z.object({
company: z.string(),
role: z.string(),
summary: z.string(),
startDate: z.string(),
endDate: z.string(),
location: z.string(),
highlights: z.array(z.string()),
})
),
education: z.array(
z.object({
school: z.string(),
degree: z.string(),
field: z.string(),
startDate: z.string(),
endDate: z.string(),
location: z.string(),
highlights: z.array(z.string()),
})
),
projects: z.array(
z.object({
name: z.string(),
description: z.string(),
tech: z.array(z.string()),
link: z.string(),
highlights: z.array(z.string()),
})
),
});

ChatResponseSchema 规定顶层必须有 assistant_message 和 resume。

1
2
3
4
const ChatResponseSchema = z.object({
assistant_message: z.string(),
resume: ResumeSchema,
});

同时,我们创建了一个空简历。emptyResume() 返回一个全部为空的结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const emptyResume = () => ({
basics: {
name: "",
title: "",
email: "",
phone: "",
location: "",
website: "",
summary: "",
links: [],
},
skills: [],
experience: [],
education: [],
projects: [],
});

这个空简历的作用就是备用,它保证了如果 AI 完全崩了,也能返回一个合法结果。

当我们调用模型时,用 zhipu.chat.completions.create 请求,拿到 content(字符串)。这一步只是得到 AI 的原始输出,不能直接用。为防止 AI 的输出结果不是纯 JSON,而是类似:

这是你的简历:{ …JSON… }

要先用 extractFirstJsonObject 找第一个 {…},把 JSON 切出来。接着开始解析 JSON:

1
2
3
4
5
6
7
8
9
10
try {
raw = JSON.parse(jsonText);
} catch (parseError) {
console.error("Failed to parse model output as JSON.", {
error: parseError,
content,
});
return Response.json(buildFallbackResponse(resume));
}
//如果这里失败,说明 AI 输出根本不是合法 JSON。直接返回 buildFallbackResponse(备用结果)

最后进行结构校验——用 ChatResponseSchema.safeParse(raw) 检查结构。

1
2
3
4
5
6
7
8
9
const validated = ChatResponseSchema.safeParse(raw);
if (!validated.success) {
console.error("Model output failed schema validation.", {
issues: validated.error.issues,
raw,
});

//...
}

如果失败了怎么办?这是容错修复的核心。失败时不会直接丢弃,而是走 normalizeChatResponse:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const normalizeChatResponse = (raw: unknown, baseResume: Resume): ChatResponse | null => {
if (!raw || typeof raw !== "object") return null;
const record = raw as Record<string, unknown>;
const assistantMessage =
typeof record.assistant_message === "string"
? record.assistant_message
: typeof record.message === "string"
? record.message
: "模型输出格式异常,请重试或补充信息。";

const resumeCandidate = record.resume ?? record;
const patchResult = ResumePatchSchema.safeParse(resumeCandidate);
return {
assistant_message: assistantMessage,
resume: patchResult.success ? mergeResume(baseResume, patchResult.data) : baseResume,
};
};
//先找 assistant_message,再拿到 resume 或者直接把整个对象当作 resume。用 ResumePatchSchema 解析补丁

最后,用 mergeResume 把补丁和原简历合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const mergeResume = (base: Resume, patch: ResumePatch): Resume => {
const basicsPatch = patch.basics;
const merged: Resume = {
basics: {
name: mergeString(base.basics.name, basicsPatch?.name),
title: mergeString(base.basics.title, basicsPatch?.title),
email: mergeString(base.basics.email, basicsPatch?.email),
phone: mergeString(base.basics.phone, basicsPatch?.phone),
location: mergeString(base.basics.location, basicsPatch?.location),
website: mergeString(base.basics.website, basicsPatch?.website),
summary: mergeString(base.basics.summary, basicsPatch?.summary),
links: normalizeLinks(basicsPatch?.links) ?? base.basics.links,
},
skills: normalizeStringArray(patch.skills) ?? base.skills,
experience: normalizeExperience(patch.experience) ?? base.experience,
education: normalizeEducation(patch.education) ?? base.education,
projects: normalizeProjects(patch.projects) ?? base.projects,
};
return merged;
};

最后的保底:如果 AI 输出完全坏掉,返回 buildFallbackResponse(resume),即输出给用户“模型输出格式异常”,简历保持原样或空值。