Vibe Coding 实战复盘一个人 AI从零打造会聊天的个人主页从技术选型、页面设计、功能打磨到部署上线这是一次用 AI 辅助完成个人主页的完整实践记录。项目地址github-yueyq-homepage网页地址yueyq-homepage成果展示 起因为什么要做这个项目这段时间我一直在学习 AI Agent 和 Vibe Coding但学着学着总会有一个想法如果只是看教程、记笔记而不真正做一个项目很难把这些东西变成自己的能力。于是我决定做一个个人主页。这个主页不只是简单展示头像、简介和联系方式而是希望它能带一点“智能感”访客不仅可以浏览我的信息还能和一个“数字分身”对话询问关于我的研究方向、兴趣、项目经历等内容。最初的需求很简单有一个简洁的个人信息展示区让别人快速知道我是谁、在做什么有一个可以对话的「数字分身」访客可以问关于我的问题页面风格简约清爽带一点科技感同时适配移动端整个开发过程尽量使用 AI 辅助完成真正体验一次 Vibe Coding。这篇文章就是这次实践的完整复盘。 技术选型为什么选择 Astro TypeScript Tailwind CSS MDX最开始我其实是用最原始的 HTML CSS JavaScript 写了一个原型主打一个“能跑就行”。但很快就遇到几个问题没有类型检查后续改聊天逻辑时很容易出错CSS 写得比较散想统一调整颜色和间距时需要到处找没有组件化头像区、信息卡片、聊天区都挤在一起后续如果要接入 API、写博客内容维护成本会越来越高。所以我决定把它重构成一个更工程化的项目最终选型如下Astro TypeScript Tailwind CSS MDXAstro适合个人主页这种“静态展示 局部交互”的场景个人主页的大部分内容其实都是静态的比如个人介绍、研究方向、时间线、项目展示等。真正需要交互的部分只有聊天窗口、作品抽屉、点击特效这些局部功能。Astro 的 Islands Architecture 很适合这种场景页面大部分内容可以作为静态 HTML 输出只有需要交互的组件才加载 JavaScript。这样既能保证页面加载速度又不会牺牲交互体验。TypeScript让聊天逻辑和 API 调用更稳接入大模型 API 之后前端和服务端之间会传递消息数组、角色字段、回复内容等数据。如果全靠 JavaScript很容易因为字段名写错、结构不一致导致 bug。TypeScript 的好处就是当代码量开始增加时它能提前帮你发现很多问题。尤其是聊天消息这种有明确结构的数据非常适合用类型约束。Tailwind CSS适合快速迭代 UIVibe Coding 的开发节奏通常是描述想法 → AI 生成代码 → 看效果 → 继续调整Tailwind CSS 很适合这种模式。它可以直接在组件里写样式不需要在 HTML 和 CSS 文件之间来回切换。更重要的是Tailwind 可以配合设计令牌统一颜色、字号、间距。后续如果想调整整体风格只需要改一处配置而不是全局搜索替换。MDX让长内容更容易维护个人主页里有一些较长的介绍内容如果全部写成 HTML会比较臃肿。MDX 既保留了 Markdown 的易写性又可以在需要的时候嵌入组件适合后续扩展成博客或项目介绍页。这次选型给我的一个体会是技术栈不是越新越好而是要看它能不能解决当前项目的核心问题。对于个人主页来说Astro 的静态能力、Tailwind 的快速样式开发、TypeScript 的类型安全刚好形成了一个比较平衡的组合。️ 架构设计先拆结构再写代码确定技术栈之后我没有马上开始写页面而是先把页面结构拆了一遍。整体结构大概是这样┌──────────────────────────────┐ │ BaseLayout │ │ ┌────────────────────────┐ │ │ │ ProfileHeader │ │ 头像、名字、一句话介绍 │ │ InfoCard │ │ 个人简介、方向标签、联系方式 │ │ Timeline │ │ 成长时间线 │ │ Footer │ │ 页脚信息 │ └────────────────────────┘ │ │ │ │ ┌────── 浮动交互层 ───────┐ │ │ │ PortfolioDrawer │ │ 左侧作品展示抽屉 │ │ ChatSection │ │ 右下角数字分身聊天窗口 │ └─────────────────────────┘ │ └──────────────────────────────┘这里主要做了两个设计决策。1. 页面内容和浮动交互分离个人介绍、时间线、联系方式这些信息走正常文档流保证阅读体验清晰稳定。聊天窗口和作品展示抽屉则采用浮动层设计聊天入口固定在右下角作品展示按钮固定在左侧。这样它们不会打断页面阅读但访客随时可以打开。2. 组件职责尽量单一我把页面拆成了几个独立组件组件职责ProfileHeader.astro展示头像、名字和一句话介绍InfoCard.astro展示 Focus / Interests / ContactTimeline.astro展示成长时间线桌面端左右交替移动端单列ChatSection.astro实现聊天按钮、聊天面板和客户端交互PortfolioDrawer.astro实现左侧作品展示抽屉Footer.astro展示动态年份和页脚标语这样拆分之后后续让 AI 修改某个功能时可以直接指定组件。例如只修改 ChatSection.astro把聊天窗口从居中弹窗改成右下角浮窗。这种指令比“帮我改一下页面”要清晰得多也更符合 Vibe Coding 的工作方式人负责拆解问题和做决策AI 负责具体实现。 设计迭代从“能用”到“有质感”这个项目里我花时间最多的不是功能而是设计细节。整体设计大概经历了三轮迭代。第一轮先做出能跑的原型第一版非常简单头像用渐变色块代替信息区用几个普通卡片展示聊天区用关键词匹配返回固定回答。这个阶段的目标不是好看而是验证功能是否成立页面能否正常展示手机端能否浏览聊天窗口能否打开和关闭消息能否发送和渲染页面结构是否方便后续继续扩展原型阶段最重要的是快不要一开始就陷入颜色、阴影、圆角这些细节。第二轮去掉“模板感”第一版虽然能用但有一个明显问题看起来太像模板站。具体表现包括标题前面到处是、、这类 emoji多个白色卡片从上到下平铺缺少层次聊天区有一个绿色“在线”标签看起来像客服插件信息区和功能区没有明显主次。于是我做了几处调整把 emoji 标题换成英文 section label例如Focus、Interests、Contact标签使用uppercase、小字号、浅色文字让它更像设计元素个人信息区去卡片化用细线分隔让页面更透气只有聊天窗口保留明显卡片样式突出它的功能属性聊天头部改成简洁的一行圆点 「数字分身」「问我任何问题」。这一步让我意识到页面是否有质感很多时候不是靠加东西而是靠删东西。删掉过度装饰统一视觉语言页面会立刻干净很多。第三轮增加层次感和动态细节 ✨“能用”和“好看”之间差的往往是一些不显眼的细节。配色调整最开始背景颜色接近白色卡片也是白色所以页面层次不明显。后来我把背景稍微加深卡片立刻就浮了出来。同时我统一调整了文字色阶主文字颜色更深保证可读性次级文字不要太浅避免小字号看不清分隔线从半透明改成更明确的浅灰蓝品牌蓝只用于强调不大面积滥用。这类调整看起来很细但对整体观感影响很大。动态粒子背景为了让页面有一点科技感我加了一个粒子网络背景。实现方式是用 Canvas 在页面底部绘制 30 个半透明粒子。粒子会缓慢移动距离较近时用细线连接。核心结构大概是这样interfaceParticle{x:number;y:number;vx:number;vy:number;r:number;opacity:number;}constCOUNT30;constMAX_DIST130;functiondraw():void{ctx.clearRect(0,0,width,height);for(leti0;iparticles.length;i){constpparticles[i];p.xp.vx;p.yp.vy;// 边界反弹、速度限制、微扰等逻辑// ...for(letji1;jparticles.length;j){constqparticles[j];constdistMath.hypot(p.x-q.x,p.y-q.y);if(distMAX_DIST){constalpha(1-dist/MAX_DIST)*0.16;ctx.strokeStylergba(91,158,207,${alpha});ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(q.x,q.y);ctx.stroke();}}ctx.fillStylergba(91,158,207,${p.opacity});ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.fill();}requestAnimationFrame(draw);}这里最关键的是克制粒子不能太多颜色不能太亮动画不能太快。它应该只是背景氛围而不是抢走页面主体的注意力。另外Canvas 层设置了pointer-events:none;z-index:0;这样它不会影响用户点击、滚动和输入。点击特效我还加了一个轻量级点击特效用户点击页面空白区域时鼠标位置会随机出现一个小图案比如♥、✦、◆、●然后向上漂浮并渐隐。核心逻辑如下constICONS[♥,✦,◆,●];functionspawn(x:number,y:number):void{consticonICONS[Math.floor(Math.random()*ICONS.length)];consteldocument.createElement(span);el.textContenticon;el.style.cssTextposition: fixed; left:${x}px; top:${y}px; pointer-events: none; z-index: 9999; font-size: 18px; color: rgba(91,158,207,0.7); transform: translate(-50%, -50%) scale(0.3); transition: transform 750ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 750ms ease-out;;document.body.appendChild(el);requestAnimationFrame((){el.style.transformtranslate(-50%, -50%) translateY(-32px) scale(1);el.style.opacity0;});setTimeout(()el.remove(),800);}这个功能也有一个小细节在按钮、输入框、链接等交互元素上点击时不触发特效避免干扰正常操作。作品展示抽屉作品展示没有做成普通列表而是放在左侧抽屉里。左侧中间有一个竖式按钮点击后抽屉从左侧滑入展示几个方向的产出。关闭方式支持按钮、遮罩和 ESC 键。这种设计的好处是作品集不会挤占主页主要空间但又足够容易被发现。成长时间线时间线部分也做了排版优化。移动端采用左侧竖线 单列内容保证阅读顺畅桌面端则采用左右交替布局中间一条线贯穿节点标记每个阶段。这种结构比普通列表更有节奏感也更适合展示个人经历。div classtimeline relative div classtimeline-line absolute md:left-1/2 md:-translate-x-px/div {items.map((item, i) { const isLeft i % 2 0; return ( div class{flex gap-5 ${isLeft ? md:flex-row : md:flex-row-reverse}} div classtimeline-dot/div div classhidden md:block md:flex-1 / div classtimeline-card !-- 时间线内容 -- /div /div ); })} /div整个设计迭代过程其实就是不断重复这个循环描述想要的效果 → AI 生成实现 → 看页面效果 → 说明哪里不满意 → AI 继续修改Vibe Coding 不是一次性生成完美项目而是通过高频、小步、可见的反馈把结果逐渐逼近你想要的样子。 数字分身从关键词匹配到真实 AI 对话这个项目最有意思的功能是右下角的「数字分身」。一开始我只是用关键词匹配实现了一个简易版本if(input.includes(研究方向)){return我的研究方向是伪装图像生成。;}if(input.includes(兴趣)){return我平时比较关注 AI Agent、深度学习和前端开发。;}这种方式可以跑但缺点非常明显可扩展性差回答生硬只能覆盖预设问题稍微换一种问法就匹配不到。所以后面我决定接入大模型 API让数字分身真正具备对话能力。接入 DeepSeek API接入 API 时我重点考虑了三个问题安全、身份和边界。1. 安全API Key 不能出现在浏览器端 API Key 绝对不能写在前端代码里也不能用PUBLIC_这类会暴露到浏览器端的环境变量。最终采用的结构是浏览器 ↓ POST /api/chat ↓ Astro 服务端 API Route ↓ DeepSeek API ↓ 返回模型回复也就是说浏览器只请求我自己的/api/chat真正的 DeepSeek API Key 只存在服务端环境变量里。服务端通过import.meta.env.DEEPSEEK_API_KEY读取密钥前端完全拿不到。2. 身份它不是通用机器人而是“我”这个功能的重点不是做一个通用聊天助手而是做一个“数字分身”。所以系统提示词非常关键。最初我写的是“你是我的数字分身”结果模型经常用第三人称描述我例如“yueyq 是一个……”。后来我把提示词改成你就是 yueyq 本人。访客正在和你对话你要用第一人称“我”回答所有问题。这样模型才稳定地用第一人称回复。3. 边界不知道就说不知道另一个坑是模型会“友好地编造”。比如访客问它年龄、具体经历、某些没有提供的信息时如果提示词没有明确限制它就可能为了保持对话自然生成一些看似合理但实际不存在的内容。所以我在系统提示词里加了严格边界你只能根据我提供的信息回答问题。 如果访客问到未提供的信息必须直接说 “这个我不太清楚建议发邮件进一步交流~” 不要为了显得友好而编造比喻、经历或细节。 不确定的事一概说不清楚。这个边界非常重要。数字分身的目标不是“什么都能答”而是“在已知信息范围内可靠地答”。服务端 API 端点核心代码如下// src/pages/api/chat.tsexportconstPOST:APIRouteasync({request}){constapiKeyimport.meta.env.DEEPSEEK_API_KEY;constbodyawaitrequest.json();constresawaitfetch(https://api.deepseek.com/v1/chat/completions,{method:POST,headers:{Content-Type:application/json,Authorization:Bearer${apiKey},},body:JSON.stringify({model:deepseek-chat,messages:[{role:system,content:SYSTEM_PROMPT},...body.messages,],temperature:0.7,max_tokens:600,}),});constdataawaitres.json();returnnewResponse(JSON.stringify({reply:data.choices[0].message.content,}),{headers:{Content-Type:application/json,},},);};客户端则保留最近几轮对话让模型具备基本上下文consthistory:ChatMessage[][];asyncfunctionfetchReply(userMessage:string):Promisestring{constmessages[...history.slice(-10),{role:user,content:userMessage,},];constresawaitfetch(/api/chat,{method:POST,headers:{Content-Type:application/json,},body:JSON.stringify({messages}),});constdataawaitres.json();returndata.reply;}这里我只保留最近 10 轮消息避免上下文过长也避免无意义地增加 token 消耗。 部署上线从静态站到 SSR最开始这个项目只是一个普通静态站Astro 默认构建就可以直接部署。但接入/api/chat后项目就不再只是纯静态页面了因为它需要服务端 API Route 来代理请求大模型。这时就需要切换到 SSR 模式。一开始我用了astrojs/node适配器本地运行没问题但部署到 Vercel 后出现了 404。后来才意识到部署到 Vercel 时应该使用 Vercel 对应的适配器。最终配置如下// astro.config.mjsimport{defineConfig}fromastro/config;importtailwindfromastrojs/tailwind;importmdxfromastrojs/mdx;importvercelfromastrojs/vercel;exportdefaultdefineConfig({integrations:[tailwind(),mdx()],output:server,adapter:vercel(),});环境变量则在 Vercel 后台配置DEEPSEEK_API_KEY你的 API Key这样 API Key 不会和代码一起提交也不会暴露到浏览器端。⚠️ 一次 API Key 泄露事故最重要的一课这个项目里最惊险的一次踩坑是 API Key 泄露。早期提交时我没有把.vercel/output/这类构建产物目录加入.gitignore。结果构建产物被提交到了 GitHub而其中包含了和环境变量相关的内容最终触发了 GitGuardian 的泄露提醒。这个提醒非常有价值因为它说明问题不一定出现在你当前的源码文件里也可能出现在历史提交、构建产物、预览输出或被忽略掉的某个目录里。发现问题后我第一时间做了三件事。1. 立刻撤销旧 Key这一步比清理 Git 历史更重要。一旦 API Key 出现在公开仓库里就应该默认它已经不安全。即使后来删掉文件只要旧 Key 没有撤销它依然可能被别人使用。所以第一步永远是撤销旧 Key → 新建 Key → 重新配置环境变量2. 清理 Git 历史仅仅删除当前文件是不够的因为 Git 历史里仍然保留着旧提交。我当时使用git filter-branch清理了.vercel/目录gitfilter-branch--force--index-filter\git rm --cached --ignore-unmatch -r .vercel\--prune-empty ----allgitupdate-ref-drefs/original/refs/heads/maingitreflog expire--expirenow--allgitgc--prunenow--aggressivegitpush--force--all不过现在回头看个人项目里更稳妥的方案其实是撤销旧 Key 后直接重建一个干净仓库只保留当前安全代码不带旧 Git 历史。3. 补全.gitignore最后我补全了.gitignore.env .env.* !.env.example dist/ .astro/ .vercel/ node_modules/.env.example只保留占位符DEEPSEEK_API_KEYyour_deepseek_api_key_here这次事故给我的教训非常直接.env必须在项目最开始就加入.gitignore构建产物目录也必须忽略不要把任何真实 Key 写进 README、注释、示例文件只要出现泄露提醒就先撤销旧 Key不要先争论是不是误报清理历史是补救撤销 Key 才是止血。 Vibe Coding 复盘AI 到底帮我做了什么回顾整个项目AI 并不是简单地“替我写代码”而是在不同环节扮演了不同角色。AI 主导、我审核的部分这些任务相对明确交给 AI 效率非常高Tailwind 样式编写响应式布局调整粒子背景动画点击特效抽屉面板交互重复性重构比如替换硬编码色值、统一 class 命名。这类工作如果手写会很耗时间但本质上是“明确目标下的实现细节”非常适合交给 AI。我主导、AI 辅助的部分这些部分需要先做判断再让 AI 执行技术选型页面信息架构组件拆分方式数字分身的系统提示词聊天 API 的安全设计部署方案选择。例如技术选型时AI 可以给出 Astro、Next.js、Vue、纯 HTML 等方案的对比但最终要不要用 Astro还是要结合项目目标来判断。必须由我自己判断的部分有些事情 AI 可以辅助但不能替代人的判断。比如当前设计是不是太像模板站信息层次是否清晰某个动效是不是太抢眼一个功能到底值不值得做数字分身的回答是否符合“我”的表达方式。这些判断依赖审美、语境和目标感。AI 可以执行修改但很难独立判断“什么才是刚刚好”。 几个关键认知1. 精准描述比“让 AI 自由发挥”更重要当我只说“帮我改好看一点”时AI 给出的结果通常比较泛甚至会继续堆装饰。但如果我明确描述把 emoji 标题换成英文 uppercase 小标签 字号 12px 颜色使用次级文字色 个人信息区去掉卡片阴影 只保留细线分隔。AI 的输出就会非常接近预期。Vibe Coding 不是“你随便说一句AI 自动猜出完美结果”而是“你越清楚自己要什么AI 越能快速帮你实现”。2. 小步迭代比一次性生成更可靠这个项目我做了很多次小提交每次只改一个组件、一个样式点或一个功能点。我的迭代节奏大概是提出一个明确改动 → AI 生成代码 → 本地看效果 → 继续描述不满意的地方 → AI 修改 → 满意后提交每一轮都很小所以即使 AI 改错了也能很快定位问题。相比一次性让 AI 生成整个项目小步迭代更稳也更容易保持项目结构清晰。3. 人负责价值判断AI 负责实现细节这是我对 Vibe Coding 最核心的理解。AI 很擅长写样式、补类型、改结构、实现动画、接 API但它不一定知道这个功能是否值得做也不一定知道这个页面应该呈现什么气质。真正决定项目质量的仍然是人的判断这个页面应该给人什么感觉访客最先看到什么哪些内容应该弱化哪些信息应该突出数字分身应该回答到什么边界AI 提高的是实现速度而不是替代产品判断。 结语从最开始的 HTML CSS JavaScript 三文件原型到后来的 Astro TypeScript Tailwind 工程化项目再到接入 DeepSeek API、部署到 Vercel、处理 API Key 泄露问题这个个人主页项目给了我一次非常完整的 Vibe Coding 实践体验。如果完全手写这个项目可能要多花两到三倍时间。但这次实践让我觉得Vibe Coding 最大的价值不只是“快”而是它让开发者可以把更多注意力放在真正重要的事情上信息怎么组织页面应该是什么风格交互是否自然AI 的身份边界如何设计安全问题如何处理。而 CSS 细节、动画代码、响应式适配、API 调用这些实现工作则可以更多交给 AI 来完成。如果你也在学习 AI Agent 或 Vibe Coding我非常建议动手做一个小项目。哪怕只是一个个人主页也足够让你经历完整的需求拆解、技术选型、设计迭代、接口接入和部署上线流程。看十篇教程不如亲手踩一次坑。
Vibe Coding 实战复盘:一个人 + AI,从零打造会聊天的个人主页
发布时间:2026/6/30 2:17:51
Vibe Coding 实战复盘一个人 AI从零打造会聊天的个人主页从技术选型、页面设计、功能打磨到部署上线这是一次用 AI 辅助完成个人主页的完整实践记录。项目地址github-yueyq-homepage网页地址yueyq-homepage成果展示 起因为什么要做这个项目这段时间我一直在学习 AI Agent 和 Vibe Coding但学着学着总会有一个想法如果只是看教程、记笔记而不真正做一个项目很难把这些东西变成自己的能力。于是我决定做一个个人主页。这个主页不只是简单展示头像、简介和联系方式而是希望它能带一点“智能感”访客不仅可以浏览我的信息还能和一个“数字分身”对话询问关于我的研究方向、兴趣、项目经历等内容。最初的需求很简单有一个简洁的个人信息展示区让别人快速知道我是谁、在做什么有一个可以对话的「数字分身」访客可以问关于我的问题页面风格简约清爽带一点科技感同时适配移动端整个开发过程尽量使用 AI 辅助完成真正体验一次 Vibe Coding。这篇文章就是这次实践的完整复盘。 技术选型为什么选择 Astro TypeScript Tailwind CSS MDX最开始我其实是用最原始的 HTML CSS JavaScript 写了一个原型主打一个“能跑就行”。但很快就遇到几个问题没有类型检查后续改聊天逻辑时很容易出错CSS 写得比较散想统一调整颜色和间距时需要到处找没有组件化头像区、信息卡片、聊天区都挤在一起后续如果要接入 API、写博客内容维护成本会越来越高。所以我决定把它重构成一个更工程化的项目最终选型如下Astro TypeScript Tailwind CSS MDXAstro适合个人主页这种“静态展示 局部交互”的场景个人主页的大部分内容其实都是静态的比如个人介绍、研究方向、时间线、项目展示等。真正需要交互的部分只有聊天窗口、作品抽屉、点击特效这些局部功能。Astro 的 Islands Architecture 很适合这种场景页面大部分内容可以作为静态 HTML 输出只有需要交互的组件才加载 JavaScript。这样既能保证页面加载速度又不会牺牲交互体验。TypeScript让聊天逻辑和 API 调用更稳接入大模型 API 之后前端和服务端之间会传递消息数组、角色字段、回复内容等数据。如果全靠 JavaScript很容易因为字段名写错、结构不一致导致 bug。TypeScript 的好处就是当代码量开始增加时它能提前帮你发现很多问题。尤其是聊天消息这种有明确结构的数据非常适合用类型约束。Tailwind CSS适合快速迭代 UIVibe Coding 的开发节奏通常是描述想法 → AI 生成代码 → 看效果 → 继续调整Tailwind CSS 很适合这种模式。它可以直接在组件里写样式不需要在 HTML 和 CSS 文件之间来回切换。更重要的是Tailwind 可以配合设计令牌统一颜色、字号、间距。后续如果想调整整体风格只需要改一处配置而不是全局搜索替换。MDX让长内容更容易维护个人主页里有一些较长的介绍内容如果全部写成 HTML会比较臃肿。MDX 既保留了 Markdown 的易写性又可以在需要的时候嵌入组件适合后续扩展成博客或项目介绍页。这次选型给我的一个体会是技术栈不是越新越好而是要看它能不能解决当前项目的核心问题。对于个人主页来说Astro 的静态能力、Tailwind 的快速样式开发、TypeScript 的类型安全刚好形成了一个比较平衡的组合。️ 架构设计先拆结构再写代码确定技术栈之后我没有马上开始写页面而是先把页面结构拆了一遍。整体结构大概是这样┌──────────────────────────────┐ │ BaseLayout │ │ ┌────────────────────────┐ │ │ │ ProfileHeader │ │ 头像、名字、一句话介绍 │ │ InfoCard │ │ 个人简介、方向标签、联系方式 │ │ Timeline │ │ 成长时间线 │ │ Footer │ │ 页脚信息 │ └────────────────────────┘ │ │ │ │ ┌────── 浮动交互层 ───────┐ │ │ │ PortfolioDrawer │ │ 左侧作品展示抽屉 │ │ ChatSection │ │ 右下角数字分身聊天窗口 │ └─────────────────────────┘ │ └──────────────────────────────┘这里主要做了两个设计决策。1. 页面内容和浮动交互分离个人介绍、时间线、联系方式这些信息走正常文档流保证阅读体验清晰稳定。聊天窗口和作品展示抽屉则采用浮动层设计聊天入口固定在右下角作品展示按钮固定在左侧。这样它们不会打断页面阅读但访客随时可以打开。2. 组件职责尽量单一我把页面拆成了几个独立组件组件职责ProfileHeader.astro展示头像、名字和一句话介绍InfoCard.astro展示 Focus / Interests / ContactTimeline.astro展示成长时间线桌面端左右交替移动端单列ChatSection.astro实现聊天按钮、聊天面板和客户端交互PortfolioDrawer.astro实现左侧作品展示抽屉Footer.astro展示动态年份和页脚标语这样拆分之后后续让 AI 修改某个功能时可以直接指定组件。例如只修改 ChatSection.astro把聊天窗口从居中弹窗改成右下角浮窗。这种指令比“帮我改一下页面”要清晰得多也更符合 Vibe Coding 的工作方式人负责拆解问题和做决策AI 负责具体实现。 设计迭代从“能用”到“有质感”这个项目里我花时间最多的不是功能而是设计细节。整体设计大概经历了三轮迭代。第一轮先做出能跑的原型第一版非常简单头像用渐变色块代替信息区用几个普通卡片展示聊天区用关键词匹配返回固定回答。这个阶段的目标不是好看而是验证功能是否成立页面能否正常展示手机端能否浏览聊天窗口能否打开和关闭消息能否发送和渲染页面结构是否方便后续继续扩展原型阶段最重要的是快不要一开始就陷入颜色、阴影、圆角这些细节。第二轮去掉“模板感”第一版虽然能用但有一个明显问题看起来太像模板站。具体表现包括标题前面到处是、、这类 emoji多个白色卡片从上到下平铺缺少层次聊天区有一个绿色“在线”标签看起来像客服插件信息区和功能区没有明显主次。于是我做了几处调整把 emoji 标题换成英文 section label例如Focus、Interests、Contact标签使用uppercase、小字号、浅色文字让它更像设计元素个人信息区去卡片化用细线分隔让页面更透气只有聊天窗口保留明显卡片样式突出它的功能属性聊天头部改成简洁的一行圆点 「数字分身」「问我任何问题」。这一步让我意识到页面是否有质感很多时候不是靠加东西而是靠删东西。删掉过度装饰统一视觉语言页面会立刻干净很多。第三轮增加层次感和动态细节 ✨“能用”和“好看”之间差的往往是一些不显眼的细节。配色调整最开始背景颜色接近白色卡片也是白色所以页面层次不明显。后来我把背景稍微加深卡片立刻就浮了出来。同时我统一调整了文字色阶主文字颜色更深保证可读性次级文字不要太浅避免小字号看不清分隔线从半透明改成更明确的浅灰蓝品牌蓝只用于强调不大面积滥用。这类调整看起来很细但对整体观感影响很大。动态粒子背景为了让页面有一点科技感我加了一个粒子网络背景。实现方式是用 Canvas 在页面底部绘制 30 个半透明粒子。粒子会缓慢移动距离较近时用细线连接。核心结构大概是这样interfaceParticle{x:number;y:number;vx:number;vy:number;r:number;opacity:number;}constCOUNT30;constMAX_DIST130;functiondraw():void{ctx.clearRect(0,0,width,height);for(leti0;iparticles.length;i){constpparticles[i];p.xp.vx;p.yp.vy;// 边界反弹、速度限制、微扰等逻辑// ...for(letji1;jparticles.length;j){constqparticles[j];constdistMath.hypot(p.x-q.x,p.y-q.y);if(distMAX_DIST){constalpha(1-dist/MAX_DIST)*0.16;ctx.strokeStylergba(91,158,207,${alpha});ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.lineTo(q.x,q.y);ctx.stroke();}}ctx.fillStylergba(91,158,207,${p.opacity});ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);ctx.fill();}requestAnimationFrame(draw);}这里最关键的是克制粒子不能太多颜色不能太亮动画不能太快。它应该只是背景氛围而不是抢走页面主体的注意力。另外Canvas 层设置了pointer-events:none;z-index:0;这样它不会影响用户点击、滚动和输入。点击特效我还加了一个轻量级点击特效用户点击页面空白区域时鼠标位置会随机出现一个小图案比如♥、✦、◆、●然后向上漂浮并渐隐。核心逻辑如下constICONS[♥,✦,◆,●];functionspawn(x:number,y:number):void{consticonICONS[Math.floor(Math.random()*ICONS.length)];consteldocument.createElement(span);el.textContenticon;el.style.cssTextposition: fixed; left:${x}px; top:${y}px; pointer-events: none; z-index: 9999; font-size: 18px; color: rgba(91,158,207,0.7); transform: translate(-50%, -50%) scale(0.3); transition: transform 750ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 750ms ease-out;;document.body.appendChild(el);requestAnimationFrame((){el.style.transformtranslate(-50%, -50%) translateY(-32px) scale(1);el.style.opacity0;});setTimeout(()el.remove(),800);}这个功能也有一个小细节在按钮、输入框、链接等交互元素上点击时不触发特效避免干扰正常操作。作品展示抽屉作品展示没有做成普通列表而是放在左侧抽屉里。左侧中间有一个竖式按钮点击后抽屉从左侧滑入展示几个方向的产出。关闭方式支持按钮、遮罩和 ESC 键。这种设计的好处是作品集不会挤占主页主要空间但又足够容易被发现。成长时间线时间线部分也做了排版优化。移动端采用左侧竖线 单列内容保证阅读顺畅桌面端则采用左右交替布局中间一条线贯穿节点标记每个阶段。这种结构比普通列表更有节奏感也更适合展示个人经历。div classtimeline relative div classtimeline-line absolute md:left-1/2 md:-translate-x-px/div {items.map((item, i) { const isLeft i % 2 0; return ( div class{flex gap-5 ${isLeft ? md:flex-row : md:flex-row-reverse}} div classtimeline-dot/div div classhidden md:block md:flex-1 / div classtimeline-card !-- 时间线内容 -- /div /div ); })} /div整个设计迭代过程其实就是不断重复这个循环描述想要的效果 → AI 生成实现 → 看页面效果 → 说明哪里不满意 → AI 继续修改Vibe Coding 不是一次性生成完美项目而是通过高频、小步、可见的反馈把结果逐渐逼近你想要的样子。 数字分身从关键词匹配到真实 AI 对话这个项目最有意思的功能是右下角的「数字分身」。一开始我只是用关键词匹配实现了一个简易版本if(input.includes(研究方向)){return我的研究方向是伪装图像生成。;}if(input.includes(兴趣)){return我平时比较关注 AI Agent、深度学习和前端开发。;}这种方式可以跑但缺点非常明显可扩展性差回答生硬只能覆盖预设问题稍微换一种问法就匹配不到。所以后面我决定接入大模型 API让数字分身真正具备对话能力。接入 DeepSeek API接入 API 时我重点考虑了三个问题安全、身份和边界。1. 安全API Key 不能出现在浏览器端 API Key 绝对不能写在前端代码里也不能用PUBLIC_这类会暴露到浏览器端的环境变量。最终采用的结构是浏览器 ↓ POST /api/chat ↓ Astro 服务端 API Route ↓ DeepSeek API ↓ 返回模型回复也就是说浏览器只请求我自己的/api/chat真正的 DeepSeek API Key 只存在服务端环境变量里。服务端通过import.meta.env.DEEPSEEK_API_KEY读取密钥前端完全拿不到。2. 身份它不是通用机器人而是“我”这个功能的重点不是做一个通用聊天助手而是做一个“数字分身”。所以系统提示词非常关键。最初我写的是“你是我的数字分身”结果模型经常用第三人称描述我例如“yueyq 是一个……”。后来我把提示词改成你就是 yueyq 本人。访客正在和你对话你要用第一人称“我”回答所有问题。这样模型才稳定地用第一人称回复。3. 边界不知道就说不知道另一个坑是模型会“友好地编造”。比如访客问它年龄、具体经历、某些没有提供的信息时如果提示词没有明确限制它就可能为了保持对话自然生成一些看似合理但实际不存在的内容。所以我在系统提示词里加了严格边界你只能根据我提供的信息回答问题。 如果访客问到未提供的信息必须直接说 “这个我不太清楚建议发邮件进一步交流~” 不要为了显得友好而编造比喻、经历或细节。 不确定的事一概说不清楚。这个边界非常重要。数字分身的目标不是“什么都能答”而是“在已知信息范围内可靠地答”。服务端 API 端点核心代码如下// src/pages/api/chat.tsexportconstPOST:APIRouteasync({request}){constapiKeyimport.meta.env.DEEPSEEK_API_KEY;constbodyawaitrequest.json();constresawaitfetch(https://api.deepseek.com/v1/chat/completions,{method:POST,headers:{Content-Type:application/json,Authorization:Bearer${apiKey},},body:JSON.stringify({model:deepseek-chat,messages:[{role:system,content:SYSTEM_PROMPT},...body.messages,],temperature:0.7,max_tokens:600,}),});constdataawaitres.json();returnnewResponse(JSON.stringify({reply:data.choices[0].message.content,}),{headers:{Content-Type:application/json,},},);};客户端则保留最近几轮对话让模型具备基本上下文consthistory:ChatMessage[][];asyncfunctionfetchReply(userMessage:string):Promisestring{constmessages[...history.slice(-10),{role:user,content:userMessage,},];constresawaitfetch(/api/chat,{method:POST,headers:{Content-Type:application/json,},body:JSON.stringify({messages}),});constdataawaitres.json();returndata.reply;}这里我只保留最近 10 轮消息避免上下文过长也避免无意义地增加 token 消耗。 部署上线从静态站到 SSR最开始这个项目只是一个普通静态站Astro 默认构建就可以直接部署。但接入/api/chat后项目就不再只是纯静态页面了因为它需要服务端 API Route 来代理请求大模型。这时就需要切换到 SSR 模式。一开始我用了astrojs/node适配器本地运行没问题但部署到 Vercel 后出现了 404。后来才意识到部署到 Vercel 时应该使用 Vercel 对应的适配器。最终配置如下// astro.config.mjsimport{defineConfig}fromastro/config;importtailwindfromastrojs/tailwind;importmdxfromastrojs/mdx;importvercelfromastrojs/vercel;exportdefaultdefineConfig({integrations:[tailwind(),mdx()],output:server,adapter:vercel(),});环境变量则在 Vercel 后台配置DEEPSEEK_API_KEY你的 API Key这样 API Key 不会和代码一起提交也不会暴露到浏览器端。⚠️ 一次 API Key 泄露事故最重要的一课这个项目里最惊险的一次踩坑是 API Key 泄露。早期提交时我没有把.vercel/output/这类构建产物目录加入.gitignore。结果构建产物被提交到了 GitHub而其中包含了和环境变量相关的内容最终触发了 GitGuardian 的泄露提醒。这个提醒非常有价值因为它说明问题不一定出现在你当前的源码文件里也可能出现在历史提交、构建产物、预览输出或被忽略掉的某个目录里。发现问题后我第一时间做了三件事。1. 立刻撤销旧 Key这一步比清理 Git 历史更重要。一旦 API Key 出现在公开仓库里就应该默认它已经不安全。即使后来删掉文件只要旧 Key 没有撤销它依然可能被别人使用。所以第一步永远是撤销旧 Key → 新建 Key → 重新配置环境变量2. 清理 Git 历史仅仅删除当前文件是不够的因为 Git 历史里仍然保留着旧提交。我当时使用git filter-branch清理了.vercel/目录gitfilter-branch--force--index-filter\git rm --cached --ignore-unmatch -r .vercel\--prune-empty ----allgitupdate-ref-drefs/original/refs/heads/maingitreflog expire--expirenow--allgitgc--prunenow--aggressivegitpush--force--all不过现在回头看个人项目里更稳妥的方案其实是撤销旧 Key 后直接重建一个干净仓库只保留当前安全代码不带旧 Git 历史。3. 补全.gitignore最后我补全了.gitignore.env .env.* !.env.example dist/ .astro/ .vercel/ node_modules/.env.example只保留占位符DEEPSEEK_API_KEYyour_deepseek_api_key_here这次事故给我的教训非常直接.env必须在项目最开始就加入.gitignore构建产物目录也必须忽略不要把任何真实 Key 写进 README、注释、示例文件只要出现泄露提醒就先撤销旧 Key不要先争论是不是误报清理历史是补救撤销 Key 才是止血。 Vibe Coding 复盘AI 到底帮我做了什么回顾整个项目AI 并不是简单地“替我写代码”而是在不同环节扮演了不同角色。AI 主导、我审核的部分这些任务相对明确交给 AI 效率非常高Tailwind 样式编写响应式布局调整粒子背景动画点击特效抽屉面板交互重复性重构比如替换硬编码色值、统一 class 命名。这类工作如果手写会很耗时间但本质上是“明确目标下的实现细节”非常适合交给 AI。我主导、AI 辅助的部分这些部分需要先做判断再让 AI 执行技术选型页面信息架构组件拆分方式数字分身的系统提示词聊天 API 的安全设计部署方案选择。例如技术选型时AI 可以给出 Astro、Next.js、Vue、纯 HTML 等方案的对比但最终要不要用 Astro还是要结合项目目标来判断。必须由我自己判断的部分有些事情 AI 可以辅助但不能替代人的判断。比如当前设计是不是太像模板站信息层次是否清晰某个动效是不是太抢眼一个功能到底值不值得做数字分身的回答是否符合“我”的表达方式。这些判断依赖审美、语境和目标感。AI 可以执行修改但很难独立判断“什么才是刚刚好”。 几个关键认知1. 精准描述比“让 AI 自由发挥”更重要当我只说“帮我改好看一点”时AI 给出的结果通常比较泛甚至会继续堆装饰。但如果我明确描述把 emoji 标题换成英文 uppercase 小标签 字号 12px 颜色使用次级文字色 个人信息区去掉卡片阴影 只保留细线分隔。AI 的输出就会非常接近预期。Vibe Coding 不是“你随便说一句AI 自动猜出完美结果”而是“你越清楚自己要什么AI 越能快速帮你实现”。2. 小步迭代比一次性生成更可靠这个项目我做了很多次小提交每次只改一个组件、一个样式点或一个功能点。我的迭代节奏大概是提出一个明确改动 → AI 生成代码 → 本地看效果 → 继续描述不满意的地方 → AI 修改 → 满意后提交每一轮都很小所以即使 AI 改错了也能很快定位问题。相比一次性让 AI 生成整个项目小步迭代更稳也更容易保持项目结构清晰。3. 人负责价值判断AI 负责实现细节这是我对 Vibe Coding 最核心的理解。AI 很擅长写样式、补类型、改结构、实现动画、接 API但它不一定知道这个功能是否值得做也不一定知道这个页面应该呈现什么气质。真正决定项目质量的仍然是人的判断这个页面应该给人什么感觉访客最先看到什么哪些内容应该弱化哪些信息应该突出数字分身应该回答到什么边界AI 提高的是实现速度而不是替代产品判断。 结语从最开始的 HTML CSS JavaScript 三文件原型到后来的 Astro TypeScript Tailwind 工程化项目再到接入 DeepSeek API、部署到 Vercel、处理 API Key 泄露问题这个个人主页项目给了我一次非常完整的 Vibe Coding 实践体验。如果完全手写这个项目可能要多花两到三倍时间。但这次实践让我觉得Vibe Coding 最大的价值不只是“快”而是它让开发者可以把更多注意力放在真正重要的事情上信息怎么组织页面应该是什么风格交互是否自然AI 的身份边界如何设计安全问题如何处理。而 CSS 细节、动画代码、响应式适配、API 调用这些实现工作则可以更多交给 AI 来完成。如果你也在学习 AI Agent 或 Vibe Coding我非常建议动手做一个小项目。哪怕只是一个个人主页也足够让你经历完整的需求拆解、技术选型、设计迭代、接口接入和部署上线流程。看十篇教程不如亲手踩一次坑。