项目地址 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)); }
最后进行结构校验——用 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, }; };
最后,用 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),即输出给用户“模型输出格式异常”,简历保持原样或空值。