const sampleResume = `张同学 | 计算机科学与技术 | 2026届硕士 求职方向:大模型应用算法 / 推荐算法实习生,期望城市深圳或北京。 技能:Python、PyTorch、Transformer、RAG、Agent、Embedding、FAISS、LangChain、推荐系统、召回排序、NDCG、A/B Test、SQL。 项目经历: 1. GenAdRec 生成式广告推荐项目:基于 Transformer 建模用户行为序列,将广告候选集转化为生成式 likelihood rerank 问题;构建 Semantic ID 表示广告 item,结合多兴趣召回提升 NDCG@10。 2. LLM 求职助手 Demo:使用 DeepSeek API 和 bge embedding 实现 JD 检索、简历关键词诊断、Prompt 模板优化,支持输出岗位匹配解释。 3. MIND 多兴趣推荐复现:复现 capsule routing 用户多兴趣建模,在公开数据集上对比召回 HitRate 与 NDCG。 实习经历: 曾参与推荐系统离线评估脚本开发,负责样本构造、特征清洗和模型结果分析。 补充:希望找能结合 LLM、Agent、RAG 和推荐排序的算法岗位。`; const jobs = [ { id: "llm-agent", title: "大模型应用算法实习生", company: "腾讯云智能", city: "深圳", direction: "大模型应用算法", stage: "实习", skills: ["LLM", "RAG", "Agent", "Embedding", "Python", "Prompt", "LangChain", "FAISS"], projectSignals: ["RAG", "Agent", "Embedding", "Prompt", "检索", "简历", "JD"], jd: "负责企业知识库问答、RAG 检索增强、Agent 工具调用、多轮对话效果优化,要求熟悉 Python、Embedding、Prompt Engineering 和 LLM 应用评估。", interviewThemes: ["RAG 召回与重排", "Agent 工具调用失败兜底", "Prompt 结构化输出", "Embedding 评估指标"] }, { id: "rec-llm", title: "LLM 推荐算法实习生", company: "内容平台事业群", city: "北京", direction: "推荐算法", stage: "实习", skills: ["推荐系统", "Transformer", "召回", "排序", "Embedding", "NDCG", "PyTorch", "A/B Test"], projectSignals: ["推荐", "Transformer", "Semantic ID", "rerank", "NDCG", "召回"], jd: "负责内容推荐召回排序模型优化,探索 LLM 与推荐系统结合,包括语义表征、用户兴趣建模、候选集重排和离线指标评估。", interviewThemes: ["召回排序链路", "多兴趣建模", "Semantic ID", "NDCG 与线上指标"] }, { id: "search-rag", title: "智能搜索算法实习生", company: "微信事业群", city: "广州", direction: "大模型应用算法", stage: "实习", skills: ["搜索", "RAG", "向量检索", "Embedding", "重排", "Python", "评估"], projectSignals: ["检索", "RAG", "Embedding", "FAISS", "重排", "TopK"], jd: "建设面向业务知识的智能搜索系统,优化 query 理解、向量召回、cross-encoder 重排和答案生成质量。", interviewThemes: ["向量召回 TopK", "重排模型", "搜索相关性评估", "幻觉控制"] }, { id: "backend-ai", title: "AI 平台后端研发实习生", company: "混元平台", city: "深圳", direction: "后端研发", stage: "实习", skills: ["Python", "FastAPI", "服务部署", "SQL", "Redis", "API", "Docker"], projectSignals: ["API", "后端", "部署", "服务", "SQL"], jd: "负责大模型平台后端服务开发、API 编排、任务队列、日志监控和模型服务接入。", interviewThemes: ["API 设计", "异步任务", "缓存策略", "服务稳定性"] }, { id: "data-analyst", title: "校招数据分析实习生", company: "HR Tech", city: "上海", direction: "数据分析", stage: "实习", skills: ["SQL", "Python", "可视化", "指标体系", "漏斗分析", "A/B Test"], projectSignals: ["SQL", "指标", "分析", "A/B", "可视化"], jd: "围绕校招转化漏斗、渠道质量和简历初筛效果进行数据分析,建设可视化看板并输出业务洞察。", interviewThemes: ["漏斗指标", "归因分析", "SQL 窗口函数", "实验设计"] }, { id: "product-ai", title: "AI 产品经理实习生", company: "企服产品部", city: "深圳", direction: "产品经理", stage: "实习", skills: ["用户研究", "Prompt", "产品设计", "数据分析", "原型", "LLM"], projectSignals: ["用户", "产品", "Prompt", "Demo", "需求"], jd: "负责 AI 助手类产品需求分析、Prompt 配置、原型设计、效果验收和用户反馈闭环。", interviewThemes: ["需求拆解", "Prompt 评估", "用户体验", "产品指标"] }, { id: "campus-rec", title: "校招人岗匹配算法实习生", company: "招聘技术中心", city: "北京", direction: "大模型应用算法", stage: "实习", skills: ["人岗匹配", "推荐系统", "Embedding", "LLM", "排序", "可解释性", "Python"], projectSignals: ["岗位", "简历", "匹配", "推荐", "排序", "Embedding"], jd: "基于简历和 JD 构建人岗匹配模型,优化候选人召回、岗位推荐排序、匹配解释和简历优化建议。", interviewThemes: ["人岗匹配建模", "冷启动", "特征交叉", "可解释排序"] }, { id: "game-ai", title: "游戏 AI 算法实习生", company: "互动娱乐事业群", city: "上海", direction: "大模型应用算法", stage: "校招", skills: ["LLM", "强化学习", "Agent", "Python", "多模态", "评估"], projectSignals: ["Agent", "多模态", "评估", "LLM"], jd: "探索游戏 NPC Agent、剧情生成、行为决策和多模态内容理解,要求有 LLM Agent 或强化学习项目经验。", interviewThemes: ["Agent 记忆机制", "行为决策", "多模态理解", "自动评估"] } ]; const agents = [ ["Resume Parser", "抽取教育、技能、项目、经历"], ["JD Understanding", "解析岗位技能与业务要求"], ["Matching Ranker", "召回 TopK 并多维排序"], ["Gap Analyzer", "定位能力缺口与简历风险"], ["Resume Coach", "生成改写建议与面试计划"] ]; const skillCatalog = [ "LLM", "RAG", "Agent", "Embedding", "FAISS", "LangChain", "Prompt", "Python", "PyTorch", "Transformer", "推荐系统", "召回", "排序", "NDCG", "A/B Test", "SQL", "FastAPI", "Docker", "Redis", "搜索", "重排", "向量检索", "多模态", "强化学习", "产品设计", "可视化", "指标体系" ]; const state = { results: [], selectedIndex: 0, activeTab: "strengths" }; const el = { resumeInput: document.querySelector("#resumeInput"), targetRole: document.querySelector("#targetRole"), targetCity: document.querySelector("#targetCity"), careerStage: document.querySelector("#careerStage"), runBtn: document.querySelector("#runBtn"), clearBtn: document.querySelector("#clearBtn"), sampleBtn: document.querySelector("#sampleBtn"), agentStrip: document.querySelector("#agentStrip"), jobList: document.querySelector("#jobList"), bestTitle: document.querySelector("#bestTitle"), bestCompany: document.querySelector("#bestCompany"), bestScore: document.querySelector("#bestScore"), skillCount: document.querySelector("#skillCount"), skillPreview: document.querySelector("#skillPreview"), detailTitle: document.querySelector("#detailTitle"), detailMeta: document.querySelector("#detailMeta"), scoreBreakdown: document.querySelector("#scoreBreakdown"), radarCanvas: document.querySelector("#radarCanvas"), tabContent: document.querySelector("#tabContent"), tabs: Array.from(document.querySelectorAll(".tab")) }; function normalize(text) { return (text || "").toLowerCase(); } function includesTerm(text, term) { return normalize(text).includes(term.toLowerCase()); } function unique(items) { return Array.from(new Set(items)).filter(Boolean); } function clampScore(score) { return Math.max(0, Math.min(100, Math.round(score))); } function extractProfile(resume) { const foundSkills = skillCatalog.filter((skill) => includesTerm(resume, skill)); const projectSignals = unique([ ...foundSkills, ...["Semantic ID", "rerank", "MIND", "简历", "JD", "岗位", "检索", "评估", "Demo"].filter((term) => includesTerm(resume, term) ) ]); return { foundSkills, projectSignals, hasMetric: /ndcg|hitrate|auc|准确率|召回率|提升|%|topk/i.test(resume), hasLLMProject: /llm|rag|agent|prompt|deepseek|openai|通义|混元/i.test(resume), hasRecProject: /推荐|召回|排序|mind|semantic id|rerank|用户兴趣/i.test(resume) }; } function overlapCount(required, actual, resume) { return required.filter((term) => actual.includes(term) || includesTerm(resume, term)).length; } function scoreJob(job, profile, resume, preferences) { const skillHits = overlapCount(job.skills, profile.foundSkills, resume); const projectHits = overlapCount(job.projectSignals, profile.projectSignals, resume); const skillScore = clampScore((skillHits / job.skills.length) * 100); const projectBase = job.projectSignals.length ? (projectHits / job.projectSignals.length) * 100 : 60; const projectBonus = profile.hasMetric ? 8 : 0; const projectScore = clampScore(projectBase + projectBonus); const experienceScore = clampScore(job.stage === preferences.stage ? 86 : 68); const directionScore = clampScore( job.direction === preferences.role ? 92 : includesTerm(job.direction, preferences.role) ? 80 : 55 ); const cityScore = clampScore( preferences.city === "不限" ? 88 : job.city === preferences.city ? 96 : 62 ); const preferenceScore = clampScore(directionScore * 0.66 + cityScore * 0.34); const growthScore = clampScore( 48 + Math.min(profile.foundSkills.length, 10) * 3 + (profile.hasLLMProject ? 12 : 0) + (profile.hasRecProject ? 8 : 0) ); const total = clampScore( 0.32 * skillScore + 0.24 * projectScore + 0.18 * experienceScore + 0.16 * preferenceScore + 0.1 * growthScore ); const matchedSkills = job.skills.filter((term) => includesTerm(resume, term)); const missingSkills = job.skills.filter((term) => !includesTerm(resume, term)); return { ...job, total, matchedSkills, missingSkills, dimensions: { "技能匹配": skillScore, "项目相关": projectScore, "经历阶段": experienceScore, "偏好一致": preferenceScore, "成长潜力": growthScore }, strengths: buildStrengths(job, matchedSkills, profile), gaps: buildGaps(job, missingSkills, profile), rewrites: buildRewrites(job, matchedSkills, missingSkills, profile), plan: buildPlan(job, missingSkills) }; } function buildStrengths(job, matchedSkills, profile) { const strengths = []; if (matchedSkills.length) { strengths.push(`命中岗位核心关键词:${matchedSkills.slice(0, 6).join("、")},具备进入精排池的基础。`); } if (profile.hasLLMProject && job.direction.includes("大模型")) { strengths.push("简历中已经出现 LLM/RAG/Agent 相关项目,可直接对齐大模型应用算法岗位。"); } if (profile.hasRecProject && (job.direction.includes("推荐") || job.jd.includes("匹配"))) { strengths.push("推荐系统、召回排序和语义表征经历能自然迁移到人岗匹配或内容推荐场景。"); } if (profile.hasMetric) { strengths.push("项目描述中包含 NDCG、HitRate、TopK 等指标,便于证明算法效果。"); } if (!strengths.length) { strengths.push("简历具备基础技术栈,但需要补充更明确的项目证据和岗位关键词。"); } return strengths; } function buildGaps(job, missingSkills, profile) { const gaps = []; if (missingSkills.length) { gaps.push(`岗位还要求 ${missingSkills.slice(0, 5).join("、")},建议在项目或技能栏补充真实经历。`); } if (!profile.hasMetric) { gaps.push("项目缺少可量化指标,容易被认为只是功能实现,建议补充离线评估或效果提升。"); } if (job.jd.includes("Agent") && !includesTerm(profile.projectSignals.join(" "), "Agent")) { gaps.push("Agent 工作流表达不够突出,需要写清任务拆解、工具调用、失败兜底和评估方式。"); } if (job.jd.includes("评估") && !/评估|指标|NDCG|HitRate|A\/B/i.test(profile.projectSignals.join(" "))) { gaps.push("岗位强调效果评估,简历需补充准确率、相关性、召回率或人工验收指标。"); } return gaps; } function buildRewrites(job, matchedSkills, missingSkills, profile) { const mainSkill = matchedSkills[0] || job.skills[0]; const missing = missingSkills[0] || job.skills[job.skills.length - 1]; return [ { before: "负责推荐系统建模和模型结果分析。", after: `围绕 ${job.direction} 场景,使用 ${mainSkill} 构建候选召回与精排链路,并通过 NDCG@10 / TopK 命中率评估模型收益。` }, { before: "做过一个 LLM 求职助手 Demo。", after: `设计多 Agent 求职匹配流程,将简历解析、JD 理解、岗位排序、能力缺口诊断和简历优化拆成可解释节点,提升输出稳定性。` }, { before: "熟悉大模型相关技术。", after: `熟悉 ${job.skills.slice(0, 4).join("、")};可独立完成 Prompt 模板、结构化输出、检索增强和结果评估配置。` }, { before: "技能栏可以继续补充。", after: `若确有实践,建议补充 ${missing},并用一次实验、接口调用或离线评估证明不是仅停留在概念层。` } ]; } function buildPlan(job, missingSkills) { const focus = missingSkills.slice(0, 3); return [ `第 1 天:复盘 ${job.direction} 岗位 JD,整理岗位关键词和自己项目的对应证据。`, `第 2 天:准备 ${job.interviewThemes[0]},用一个项目讲清输入、模型、输出、指标。`, `第 3 天:补齐 ${focus[0] || job.skills[0]} 的实践案例,写成可追问的简历 bullet。`, `第 4 天:准备 ${job.interviewThemes[1]},重点讲失败 case 和优化方案。`, `第 5 天:针对 ${job.company} 场景模拟 3 个业务问题,练习算法方案设计。`, "第 6 天:做一次 20 分钟项目深挖 mock,覆盖数据、模型、评估、工程落地。", "第 7 天:压缩成 1 分钟自我介绍、3 分钟项目介绍和 5 个高频追问答案。" ]; } function runMatch() { const resume = el.resumeInput.value.trim(); if (!resume) { renderEmpty("请先粘贴简历文本,或点击“填充示例”。"); return; } renderAgents(true); const preferences = { role: el.targetRole.value, city: el.targetCity.value, stage: el.careerStage.value }; const profile = extractProfile(resume); state.results = jobs .map((job) => scoreJob(job, profile, resume, preferences)) .sort((a, b) => b.total - a.total); state.selectedIndex = 0; state.activeTab = "strengths"; renderAll(profile); } function renderAll(profile) { renderAgents(false); renderSummary(profile); renderJobList(); renderDetail(); } function renderAgents(active) { el.agentStrip.innerHTML = agents .map( ([name, desc], index) => `
${index + 1}. ${name} ${desc}
` ) .join(""); } function renderSummary(profile) { const best = state.results[0]; el.bestTitle.textContent = best.title; el.bestCompany.textContent = `${best.company} · ${best.city} · ${best.stage}`; el.bestScore.textContent = `${best.total}/100`; el.skillCount.textContent = `${profile.foundSkills.length} 个`; el.skillPreview.textContent = profile.foundSkills.slice(0, 5).join("、") || "未命中明显技能词"; } function renderJobList() { el.jobList.innerHTML = state.results .map((job, index) => { const tags = job.skills .slice(0, 5) .map((skill) => `${skill}`) .join(""); return `
${job.title}
${job.total}
${job.company} · ${job.city} · ${job.direction}
${tags}
`; }) .join(""); document.querySelectorAll(".job-card").forEach((card) => { card.addEventListener("click", () => { state.selectedIndex = Number(card.dataset.index); state.activeTab = "strengths"; renderJobList(); renderDetail(); }); }); } function renderDetail() { const job = state.results[state.selectedIndex]; if (!job) { renderEmpty("运行匹配后查看岗位诊断。"); return; } el.detailTitle.textContent = job.title; el.detailMeta.textContent = `${job.company} · ${job.city} · ${job.stage}`; renderBreakdown(job); drawRadar(job.dimensions); updateTabs(); renderTabContent(job); } function renderBreakdown(job) { el.scoreBreakdown.innerHTML = Object.entries(job.dimensions) .map( ([name, value]) => `
${name}
${value}
` ) .join(""); } function drawRadar(dimensions) { const canvas = el.radarCanvas; const ctx = canvas.getContext("2d"); const width = canvas.width; const height = canvas.height; const cx = width / 2; const cy = height / 2 + 4; const radius = 82; const entries = Object.entries(dimensions); ctx.clearRect(0, 0, width, height); ctx.strokeStyle = "#d9e0e8"; ctx.fillStyle = "#667085"; ctx.font = "12px Microsoft YaHei, sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; for (let level = 1; level <= 4; level += 1) { drawPolygon(ctx, entries.length, cx, cy, (radius * level) / 4, false); } entries.forEach(([label], index) => { const angle = (Math.PI * 2 * index) / entries.length - Math.PI / 2; const x = cx + Math.cos(angle) * (radius + 24); const y = cy + Math.sin(angle) * (radius + 18); ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius); ctx.stroke(); ctx.fillText(label, x, y); }); const points = entries.map(([, value], index) => { const angle = (Math.PI * 2 * index) / entries.length - Math.PI / 2; const pointRadius = (radius * value) / 100; return [cx + Math.cos(angle) * pointRadius, cy + Math.sin(angle) * pointRadius]; }); ctx.beginPath(); points.forEach(([x, y], index) => { if (index === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.closePath(); ctx.fillStyle = "rgba(15, 143, 114, 0.18)"; ctx.strokeStyle = "#0f8f72"; ctx.lineWidth = 2; ctx.fill(); ctx.stroke(); } function drawPolygon(ctx, sides, cx, cy, radius, fill) { ctx.beginPath(); for (let i = 0; i < sides; i += 1) { const angle = (Math.PI * 2 * i) / sides - Math.PI / 2; const x = cx + Math.cos(angle) * radius; const y = cy + Math.sin(angle) * radius; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.closePath(); if (fill) ctx.fill(); ctx.stroke(); } function updateTabs() { el.tabs.forEach((tab) => { tab.classList.toggle("active", tab.dataset.tab === state.activeTab); }); } function renderTabContent(job) { if (state.activeTab === "strengths") { el.tabContent.innerHTML = listHtml(job.strengths); return; } if (state.activeTab === "gaps") { el.tabContent.innerHTML = listHtml(job.gaps, "warning"); return; } if (state.activeTab === "rewrite") { el.tabContent.innerHTML = `
${job.rewrites .map( (item) => `
原表达

${item.before}

建议改写

${item.after}

` ) .join("")}
`; return; } el.tabContent.innerHTML = listHtml(job.plan); } function listHtml(items, className = "") { return ` `; } function renderEmpty(message) { el.jobList.innerHTML = `
${message}
`; el.tabContent.innerHTML = `
${message}
`; } el.runBtn.addEventListener("click", runMatch); el.sampleBtn.addEventListener("click", () => { el.resumeInput.value = sampleResume; runMatch(); }); el.clearBtn.addEventListener("click", () => { el.resumeInput.value = ""; state.results = []; el.bestTitle.textContent = "等待分析"; el.bestCompany.textContent = "填写简历后运行"; el.bestScore.textContent = "--"; el.skillCount.textContent = "--"; el.skillPreview.textContent = "待解析"; renderAgents(false); renderEmpty("请先粘贴简历文本,或点击“填充示例”。"); drawRadar({ "技能匹配": 0, "项目相关": 0, "经历阶段": 0, "偏好一致": 0, "成长潜力": 0 }); }); el.tabs.forEach((tab) => { tab.addEventListener("click", () => { state.activeTab = tab.dataset.tab; updateTabs(); const job = state.results[state.selectedIndex]; if (job) renderTabContent(job); }); }); el.resumeInput.value = sampleResume; renderAgents(false); runMatch();