每个第一次尝试用 AI 做 HTML 演示文稿的人,几乎都会犯同一个错误。
他们打开对话框,输入一段雄心勃勃的 Prompt:
"请帮我生成一个完整的 HTML 演示文稿,共 100 页,主题是XX,要求有封面、目录、图文分栏、代码示例、数据图表,风格简洁现代,支持键盘翻页……"
然后按下回车,满怀期待地等待。
AI 开始输出。前几页看起来不错,HTML 结构清晰,样式也挺好看。但写到第 15 页左右,代码开始出现 <!-- 此处省略,样式同上 -->。写到第 30 页,翻页逻辑消失了。写到第 50 页,输出直接被截断。最终你得到的,是一个残缺的、样式混乱的、根本无法运行的代码堆。
这不是 AI 不够聪明,也不是你的 Prompt 写得不够好。这是一个物理限制问题,任何大语言模型都无法绕过。
真正的问题出在哪里?
我们习惯把"做一个 100 页 PPT"理解为一项写作任务——就像让 AI 写一篇长文章一样,只要给够指令,AI 应该能一气呵成输出全部内容。
但 100 页 HTML PPT 本质上不是一篇文章,它是一个软件工程项目。
一篇文章可以从第一个字写到最后一个字,中间的每一句话都依赖前面的语境。但一个软件项目有架构、有模块、有数据、有渲染逻辑——这些东西不能被"顺序生成",它们必须被设计,然后被组装。
把软件工程项目当成写作任务交给 AI,就像拿着一张食材清单,让厨师直接端出一桌满汉全席——不是厨师的问题,是你给错了任务单。
本文要解决的核心问题,只有一个:
如何把"生成 100 页 PPT"这件事,从一个写作任务重新定义为一个工程任务?
一旦完成这个认知转变,AI 就不再是那个需要一口气输出所有内容的"全知作者",而是变成你流水线上的专项工程师——每次只做一件明确的事,每次的输出都能直接运行,最终由你把所有模块组装成完整的产品。
这就是 Vibe Coding 真正应该被使用的方式。
和 AI 对话,感觉像是在和一个无所不知的人聊天。但这个"人"有一个非常具体的生理限制:它的大脑在任意时刻只能容纳有限数量的文字。
这个限制的单位叫做 Token。
Token 不完全等于字符,也不完全等于单词。粗略地理解,一个英文单词大约是 1-2 个 Token,一个中文字大约是 1-2 个 Token。对于大多数主流模型来说,单次对话能处理的 Token 总量是有上限的——包括你输入的所有内容,加上 AI 需要输出的所有内容,加上系统的背景设定,全部加在一起,不能超过这个上限。
这带来两个直接后果。
第一个后果:输出被截断。
当你要求 AI 生成 100 页 HTML,AI 会在某个时刻触碰到输出长度的天花板,然后戛然而止。你看到的不是一个完整的文件,而是一个被硬生生切断的代码片段——有开头,没结尾,浏览器根本无法解析。
第二个后果:AI 主动压缩内容。
更隐蔽的问题是,AI 在生成过程中会"感知"到自己剩余的输出空间不多了,于是开始自动省略。第 20 页之后,你会看到这样的代码:
<!-- 第21页 - 结构同第20页,内容替换即可 -->
<!-- 第22页 - 同上 -->
<!-- ... 此处省略至第100页 -->
这不是 AI 在偷懒,这是它在用它认为"合理"的方式,在有限的输出空间内尽量覆盖你的需求。问题在于,这种"合理"对你毫无价值——你得到的是一堆注释,而不是可以运行的代码。
Token 限制是一个硬限制,碰到了就停。但还有另一个问题更加隐蔽,很多人遇到了却不知道原因——注意力稀释。
要理解这个问题,先理解 AI 是怎么"记住"上下文的。
AI 在生成每一个新的字符时,都会回头看整个对话历史,计算"哪些之前说过的内容,和我现在要写的东西最相关"。这个计算过程叫做注意力机制。它不是简单地从头读到尾,而是对历史内容做加权——越相关的内容权重越高,越不相关的权重越低。
这套机制在短对话里表现完美。但当上下文变得非常长,问题就出现了。
权重被稀释了。
想象你在开头定义了一套完整的 CSS 设计规范:主色调、字体大小、间距系统、组件样式。这套规范写了大约 200 行。在生成第 1 页到第 10 页时,AI 能清楚地"看见"这套规范,输出的样式高度一致。
但当对话进行到第 80 页时,历史上下文已经积累了几万个 Token。开头那 200 行 CSS 规范,相对于整个上下文的比重变得极小。AI 的注意力被后面大量的内容分散,对最初规范的"记忆"开始模糊。
于是你会看到这样的现象:
48px,第 80 页悄悄变成了 36px#1a365d,后面某页突然用了默认的黑色border-radius: 12px,第 90 页的卡片变成了直角没有任何报错,没有任何警告。代码完全可以运行。但视觉上,你的第 1 页和第 80 页看起来像是两个完全不同的人做的。
Token 限制和注意力稀释,本质上是同一件事的两面。
Token 限制说的是:AI 能处理的信息总量有上限。注意力稀释说的是:即使在上限之内,AI 对早期信息的把握也会随着上下文增长而衰减。
两者叠加的结果是:你越想一次性让 AI 做更多,AI 能做好的比例就越低。
这不是通过"写更好的 Prompt"能解决的问题。这是架构问题,需要架构层面的解法。
而这个解法,就是接下来整篇文章要讲的核心思路:不要让 AI 记住所有东西,而是把"需要记住的东西"从对话中剥离出去,变成代码和数据。
上一章说清楚了问题的根源:AI 的上下文是有限的,注意力是会稀释的。那么解法的第一步,就是把"内容"从"结构"里彻底剥离出去。
什么叫"只写壳子"?
想象你在建一座剧院。剧院建好之后,今晚演莎士比亚,明晚演现代舞,后天办音乐会——舞台本身不需要为此改变任何结构。灯光、座位、音响系统,全部不动。变的只是上面演的内容。
我们要做的 HTML PPT,结构上应该是同一件事:一个稳定的舞台,加上可以随时替换的内容。
这意味着,第二章要让 AI 写的这个"壳子",完成之后应该长这样:
这个壳子一旦写好,就不再需要 AI 碰它了。
后续所有的对话,AI 只需要关心一件事:生成内容。翻页逻辑、全屏适配、键盘事件——这些东西只写一次,永久稳定。
这是你和 AI 的第一次实质性对话。Prompt 应该非常具体,边界非常清晰:
"请写一个零外部依赖的单页 HTML 文件,实现以下功能:
全屏自适应容器,始终占满视口,背景色 #0f0f0f页面中央有一个 id="slide-mount"的 div,作为幻灯片的挂载点,暂时为空监听键盘左右方向键事件,分别触发 prevSlide()和nextSlide()函数维护一个 state对象,包含current(当前页索引)和total(总页数)两个字段不要写任何幻灯片内容,不要写任何样式组件,只写框架本身 完整输出所有代码,禁止使用任何注释替代真实代码"
注意最后两条指令。第 5 条划定了边界——这次对话只做这一件事。第 6 条是防压缩指令——明确告诉 AI 不能省略。
AI 应该输出类似这样的结构:
<!DOCTYPE html>
<htmllang="zh">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>Presentation</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 100vw;
height: 100vh;
background: #0f0f0f;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
#slide-mount {
width: 100%;
height: 100%;
position: relative;
}
</style>
</head>
<body>
<divid="slide-mount"></div>
<script>
const state = {
current: 0,
total: 0
};
functionprevSlide() {
if (state.current > 0) {
state.current--;
renderCurrent();
}
}
functionnextSlide() {
if (state.current < state.total - 1) {
state.current++;
renderCurrent();
}
}
functionrenderCurrent() {
// 由 engine.js 实现,此处为占位
}
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') prevSlide();
if (e.key === 'ArrowRight') nextSlide();
});
</script>
</body>
</html>
这段代码此刻什么都不显示。但它已经具备了一个完整 PPT 框架所需要的全部骨骼:状态管理、事件监听、挂载点。
这就够了。现在关掉这个对话窗口。
下一个对话,我们去做完全不同的一件事。
现在你有了一个稳定的舞台。下一步,很多人会直接开始写内容——打开新对话,让 AI 开始生成第 1 页、第 2 页、第 3 页……
这是第二个常见陷阱。
100 页 PPT,看起来是 100 个独立的页面。但仔细观察任何一套专业的演示文稿,你会发现一个规律:页面的种类其实非常少。
一套典型的技术分享 PPT,大概率只有这几种页面:
100 页内容,本质上是这 4-5 种模板的不同数据填充。
这个认知非常重要。它意味着你不需要让 AI 写 100 段 HTML——你只需要让 AI 写 5 个模板函数,然后用数据驱动这 5 个函数生成 100 页。
这就是组件化思路的核心:把"页面"变成"模板 + 数据"。
在写任何组件之前,先用一次独立的对话,让 AI 输出一套全局 CSS 规范。
为什么要单独做这一步?
因为这套规范是所有组件的共同语言。字体大小、颜色系统、间距单位——如果这些东西在每个组件里各自定义,最终拼在一起必然视觉混乱。把它们提取成 CSS 变量,写在一个地方,所有组件只引用变量名,就从根本上消灭了这个风险。
给 AI 的 Prompt:
"请输出一套用于全屏 HTML 演示文稿的 CSS 全局规范,要求:
使用 CSS 自定义属性(变量)定义所有设计 token 包含:主色、辅助色、背景色、文字色(主次两级) 包含:字体大小系统(标题大、标题中、正文、注释四级) 包含:间距系统(4px 基准单位,定义 sm / md / lg / xl 四个档位) 包含:针对幻灯片的基础布局类(全屏居中、左右分栏、上下分栏) 不要写任何组件样式,只写设计 token 和布局工具类 完整输出,禁止省略"
AI 应该输出类似这样的内容:
:root {
/* 颜色系统 */
--color-primary: #4F6EF7;
--color-accent: #00D4AA;
--color-bg: #0f0f0f;
--color-surface: #1a1a1a;
--color-text: #F0F0F0;
--color-text-muted: #888888;
/* 字体大小系统 */
--text-display: 64px;
--text-heading: 40px;
--text-body: 20px;
--text-caption: 14px;
/* 间距系统 */
--space-sm: 8px;
--space-md: 16px;
--space-lg: 32px;
--space-xl: 64px;
}
/* 布局工具类 */
.slide-center {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--space-xl);
}
.slide-split {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-lg);
padding: var(--space-xl);
}
.slide-stack {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
padding: var(--space-xl);
gap: var(--space-md);
}
这个文件同样只写一次,之后不再修改。
所有组件的样式,都从这套变量里取值。改颜色?改一个变量,100 页同时生效。调间距?同理。这就是设计 token 的价值——让全局一致性变成一个系统保证,而不是靠人工检查。
全局规范确立之后,现在可以开始写组件了。
每个组件用一次独立的对话完成。不要在同一个对话里让 AI 连续写多个组件——每次只做一件事,边界越清晰,输出质量越高。
第一个组件:Hero(封面页)
给 AI 的 Prompt:
"请写一个 JavaScript 函数
renderHero(data),用于渲染演示文稿的封面页。 要求:
接收一个 data对象,结构为{ title, subtitle, eyebrow? }返回一个 HTMLElement,不要直接操作 DOM,不要使用 innerHTML 使用已有的 CSS 工具类 slide-center,以及:root中定义的 CSS 变量eyebrow 是可选字段,有则显示在 title 上方,无则不渲染 完整输出函数体,禁止省略任何代码"
AI 应该输出:
functionrenderHero(data) {
const { title, subtitle, eyebrow } = data;
const slide = document.createElement('div');
slide.className = 'slide-center';
slide.style.background = 'var(--color-bg)';
slide.style.textAlign = 'center';
if (eyebrow) {
const eye = document.createElement('p');
eye.textContent = eyebrow;
eye.style.cssText = `
font-size: var(--text-caption);
color: var(--color-accent);
letter-spacing: 0.15em;
text-transform: uppercase;
margin-bottom: var(--space-md);
`;
slide.appendChild(eye);
}
const h1 = document.createElement('h1');
h1.textContent = title;
h1.style.cssText = `
font-size: var(--text-display);
color: var(--color-text);
font-weight: 700;
line-height: 1.1;
margin-bottom: var(--space-md);
`;
const sub = document.createElement('p');
sub.textContent = subtitle;
sub.style.cssText = `
font-size: var(--text-body);
color: var(--color-text-muted);
max-width: 600px;
line-height: 1.6;
`;
slide.appendChild(h1);
slide.appendChild(sub);
return slide;
}
拿到这个函数之后,做一件事:在浏览器里测试它。
// 在控制台里粘贴这两行
const el = renderHero({ title: "测试标题", subtitle: "这是副标题", eyebrow: "Chapter 01" });
document.getElementById('slide-mount').appendChild(el);
如果封面页出现了,这个组件就完成了。关掉对话,开下一个。
第二个组件:Split(图文分栏页)
给 AI 的 Prompt:
"请写一个 JavaScript 函数
renderSplit(data),用于渲染图文分栏页。 要求:
接收 data对象,结构为{ heading, body, image?, side }side字段值为"left"或"right",决定图片在哪一侧image是可选字段,有则显示图片,无则该侧显示一个占位色块返回 HTMLElement,使用 CSS 工具类 slide-split完整输出,禁止省略"
AI 应该输出:
functionrenderSplit(data) {
const { heading, body, image, side } = data;
const slide = document.createElement('div');
slide.className = 'slide-split';
slide.style.background = 'var(--color-bg)';
const textCol = document.createElement('div');
textCol.style.cssText = `
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--space-md);
`;
const h2 = document.createElement('h2');
h2.textContent = heading;
h2.style.cssText = `
font-size: var(--text-heading);
color: var(--color-text);
font-weight: 600;
line-height: 1.2;
`;
const p = document.createElement('p');
p.textContent = body;
p.style.cssText = `
font-size: var(--text-body);
color: var(--color-text-muted);
line-height: 1.7;
`;
textCol.appendChild(h2);
textCol.appendChild(p);
const mediaCol = document.createElement('div');
mediaCol.style.cssText = `
display: flex;
align-items: center;
justify-content: center;
border-radius: 12px;
overflow: hidden;
background: var(--color-surface);
`;
if (image) {
const img = document.createElement('img');
img.src = image;
img.style.cssText = `
width: 100%;
height: 100%;
object-fit: cover;
`;
mediaCol.appendChild(img);
}
if (side === 'left') {
slide.appendChild(mediaCol);
slide.appendChild(textCol);
} else {
slide.appendChild(textCol);
slide.appendChild(mediaCol);
}
return slide;
}
每个组件写完之后,立刻测试,立刻收工。
不要在同一个对话里追问"能不能加个动画"、"能不能改改颜色"。这些都是后续的事。现在的目标只有一个:让这个函数能跑通,返回正确的 DOM 结构。
组件库不需要一次写完。先写 Hero 和 Split,够用了就去做第四章。等到真正遇到需要 Code 组件或 Chart 组件的内容时,再回来补写。
不要提前造你现在不需要的轮子。
先回想一下,如果不用数据驱动的方式,100 页 PPT 的内容是怎么存在于代码里的。
最直觉的做法,是让 AI 直接生成 100 个 <section> 标签:
<sectionid="slide-1">
<h1>第一页标题</h1>
<p>第一页内容……</p>
</section>
<sectionid="slide-2">
<h1>第二页标题</h1>
<p>第二页内容……</p>
</section>
<!-- 以此类推,直到第100页 -->
这个方案有三个致命问题。
第一,它根本无法生成。 100 个 <section>,加上每页的样式和内容,轻松超过任何模型的单次输出上限。你会在某一页被截断,然后不知道从哪里续写。
第二,修改成本极高。 第 73 页有个错别字,你需要在几千行 HTML 里找到那个位置,改完还要确保没有误碰到周围的代码。
第三,内容和表现完全耦合。 想把第 40 页从"要点列表"改成"图文分栏"?你需要重写整个 <section> 的 HTML 结构,而不是改一个字段。
JSON 数据驱动的方式,从根本上解决了这三个问题。
内容只是数据。代码只是模板。两者完全分离。
整个 PPT 的内容,存储为一个 JSON 数组。每个元素代表一页幻灯片,结构如下:
[
{
"id": 1,
"type": "hero",
"theme": "dark",
"data": {
"eyebrow": "Vibe Coding 实战",
"title": "用 AI 做 100 页 HTML PPT",
"subtitle": "一套可复用的工程化方法论"
}
},
{
"id": 2,
"type": "list",
"theme": "dark",
"data": {
"heading": "今天要解决的三个问题",
"items": [
"为什么 AI 无法一次生成 100 页",
"如何用工程化思维拆解这个任务",
"数据驱动渲染的完整实现方式"
]
}
},
{
"id": 3,
"type": "split",
"theme": "dark",
"data": {
"heading": "Token 限制的本质",
"body": "大语言模型在任意时刻能处理的信息总量是有上限的。超过这个上限,内容会被截断,或者被模型主动压缩省略。",
"side": "right",
"image": null
}
}
]
这个结构有三个关键设计决策,每一个都有明确的理由。
决策一:用 type 字段决定渲染逻辑。
type 的值直接对应第三章写的组件函数名。"hero" 对应 renderHero(),"split" 对应 renderSplit()。渲染引擎只需要一个简单的映射表,就能知道每一页该用哪个组件来渲染。
决策二:data 字段的结构由 type 决定。
不同类型的页面需要不同的数据字段。Hero 页需要 title 和 subtitle,Split 页需要 heading、body 和 side。把这些字段全部收进 data 对象,而不是平铺在顶层,是为了让顶层结构保持统一——无论什么类型的页面,顶层永远只有 id、type、theme、data 四个字段。
决策三:内容修改只发生在 JSON 里。
第 73 页有错别字?打开 slides.json,找到 id: 73 的对象,改掉那个字符串,保存,刷新浏览器。整个过程不需要碰任何 JavaScript 或 HTML 文件。这是数据驱动最直接的价值体现。
这个 JSON 文件本身,也可以由 AI 来生成。
当你的 PPT 内容已经在脑子里,或者已经有一份大纲、一份 Word 文档时,可以直接把内容交给 AI,让它帮你转换成这个格式:
"请将以下 PPT 大纲转换为 JSON 数组,每页包含 id、type、theme、data 四个字段。type 只能是 hero、split、list、code、chart 五种之一。data 的字段结构根据 type 决定,规则如下:[粘贴字段规范]。只输出 JSON,不要输出任何解释。"
这是一个边界极清晰的任务——输入是大纲,输出是结构化数据,没有歧义,没有发挥空间。AI 在这类任务上的表现非常稳定,几乎不会出现幻觉。
有了 JSON 数据,有了组件函数,现在需要一个"调度中心"把它们连接起来。这就是渲染管道的职责。
渲染管道由两个函数组成,各自做一件事。
第一个函数:renderSlide()
它的职责只有一个:接收一个幻灯片的数据对象,判断它的类型,调用对应的组件函数,返回渲染好的 DOM 元素。
给 AI 的 Prompt:
"请写一个函数
renderSlide(slideData),作为组件库的调度入口。 要求:
接收单个幻灯片数据对象,结构为 { id, type, theme, data }根据 type字段,调用对应的组件函数:hero → renderHero,split → renderSplit,list → renderList,code → renderCode,chart → renderChart如果 type未知,返回一个显示错误信息的占位 div返回 HTMLElement 完整输出,禁止省略"
AI 应该输出:
functionrenderSlide(slideData) {
const { type, data } = slideData;
const renderers = {
hero: renderHero,
split: renderSplit,
list: renderList,
code: renderCode,
chart: renderChart,
};
const renderer = renderers[type];
if (!renderer) {
const fallback = document.createElement('div');
fallback.className = 'slide-center';
fallback.style.cssText = `
background: var(--color-bg);
color: var(--color-text-muted);
font-size: var(--text-body);
`;
fallback.textContent = `未知组件类型:${type}`;
return fallback;
}
return renderer(data);
}
注意 renderers 这个映射表的设计。它把 type 字符串和函数引用绑定在一起,避免了冗长的 if-else 或 switch 语句。以后要新增一种组件类型,只需要在这个对象里加一行,其他代码完全不需要动。
第二个函数:mountSlides()
它的职责是加载 JSON 数据,初始化状态,渲染第一页,并把整个系统启动起来。
给 AI 的 Prompt:
"请写一个异步函数
mountSlides(),作为整个渲染系统的启动入口。 要求:
使用 fetch 加载 slides.json文件将数据存入 state.slides,将state.total设为数组长度将 state.current设为 0调用 renderCurrent()渲染第一页renderCurrent()函数负责清空挂载点,渲染state.slides[state.current],并将结果插入id="slide-mount"的容器在 DOMContentLoaded事件里调用mountSlides()完整输出,禁止省略"
AI 应该输出:
asyncfunctionmountSlides() {
const response = await fetch('slides.json');
const slides = await response.json();
state.slides = slides;
state.total = slides.length;
state.current = 0;
renderCurrent();
}
functionrenderCurrent() {
const mount = document.getElementById('slide-mount');
mount.innerHTML = '';
const slideData = state.slides[state.current];
const el = renderSlide(slideData);
mount.appendChild(el);
}
document.addEventListener('DOMContentLoaded', mountSlides);
现在把所有部分连起来,验证整个管道是否跑通。
在 slides.json 里放三条测试数据:
[
{
"id": 1,
"type": "hero",
"theme": "dark",
"data": {
"eyebrow": "测试",
"title": "管道跑通了",
"subtitle": "如果你能看到这行字,说明一切正常"
}
},
{
"id": 2,
"type": "split",
"theme": "dark",
"data": {
"heading": "第二页",
"body": "按左右方向键可以翻页",
"side": "right",
"image": null
}
},
{
"id": 3,
"type": "list",
"theme": "dark",
"data": {
"heading": "第三页",
"items": ["条目一", "条目二", "条目三"]
}
}
]
打开浏览器,按左右方向键。如果三页能正常切换,样式统一,没有报错——整个工程的核心管道就验证完毕了。
此刻你拥有的,是一个可以承载任意数量页面的渲染引擎。把 slides.json 从 3 条数据扩展到 100 条,系统不需要做任何改变。
这就是数据驱动架构的核心价值:规模增长的成本趋近于零。
前四章解决的是架构问题。但在实际操作过程中,你还会遇到另一类问题——不是代码跑不通,而是AI 在对话过程中悄悄偏离了你的要求。
这种偏离有一个规律:它不会突然发生,而是缓慢累积的。
第一轮对话,AI 完全按你的要求输出。第三轮,它开始在函数里加入你没要求的"优化"。第五轮,它已经把你第一轮定义的接口改掉了,用了一套它自己觉得"更好"的结构。第八轮,你发现新输出的代码和之前的完全无法兼容。
这个现象叫做上下文漂移。原因很简单:随着对话轮次增加,早期的约束在注意力权重里越来越弱,AI 越来越倾向于根据最近几轮的对话来决定输出方向。
解决这个问题,不需要任何黑魔法。只需要一个习惯:每次对话开始时,主动把关键约束重新带入上下文。
上下文锚点,就是在每次新对话的开头,用几句话重新声明当前的技术边界。
模板如下:
【当前项目约束】
- 技术栈:Vanilla JS,零外部依赖,4个文件(index.html / engine.js / components.js / slides.json)
- 组件接口:每个 render 函数接收 data 对象,返回 HTMLElement,不直接操作 DOM
- CSS:所有样式值使用 :root 中定义的 CSS 变量,不允许硬编码颜色或字号
- 状态管理:只通过 state 对象维护,不引入任何外部状态
【当前进度】
已完成:index.html 框架、全局 CSS 规范、renderHero()、renderSplit()、renderSlide()、mountSlides()
本次任务:写 renderList() 函数
【本次要求】
接收 data 对象,结构为 { heading, items: string[], note? }
返回 HTMLElement,使用 slide-stack 布局类
完整输出,禁止省略
这个模板做了三件事:
第一,重申约束。 把整个项目最核心的技术边界写在最前面,确保 AI 在生成任何内容之前,先读到这些约束。
第二,同步进度。 告诉 AI 已经存在哪些函数,避免它重复定义或者和已有代码产生冲突。
第三,限定任务。 本次对话只做一件事,边界清晰,没有歧义。
这三个部分缺一不可。只有任务没有约束,AI 会自由发挥。只有约束没有进度,AI 不知道哪些东西已经存在。只有约束和进度没有任务,AI 不知道这次要输出什么。
上下文锚点解决的是"方向跑偏"的问题。防压缩指令解决的是另一个问题:AI 输出了正确的结构,但省略了关键细节。
最常见的省略形式有三种:
// 第一种:用注释替代代码
functionrenderChart(data) {
// 此处实现图表渲染逻辑
// 使用 Canvas API 绘制柱状图
}
// 第二种:用 TODO 占位
functionrenderCode(data) {
const el = document.createElement('div');
// TODO: 添加语法高亮
return el;
}
// 第三种:声称"同上"
functionrenderList(data) {
// 结构与 renderSplit 类似,此处省略,请参考上文
}
这三种省略,表面上看是 AI 在节省 token,实际上是把工作甩给了你。你需要自己去补全这些空洞,而补全的过程往往会引入新的不一致。
防压缩指令的核心,是在 Prompt 里明确声明"省略"是不可接受的输出:
"请完整输出
renderList函数的全部代码。 以下行为视为输出无效,需要重新生成:
使用注释替代任何真实代码 出现 TODO、省略号、'同上'、'参考上文'等字样 函数体未完整实现,无法直接运行"
把"无效输出"的定义写清楚,AI 就有了明确的约束边界。大多数情况下,这类指令能有效减少省略行为。
有时候问题不是 AI 省略了代码,而是它把某个组件写错了——逻辑有 bug,或者样式和其他组件不一致。
这时候很多人会在原来的对话里继续追问,让 AI 修复。这是一个容易让问题变得更复杂的选择。
原因在于:原来的对话上下文里已经积累了大量的"错误信息"——AI 之前输出的错误代码、你的报错反馈、AI 的解释。这些内容会干扰 AI 对问题的判断,它在修复时很容易引入新的错误,或者把之前正确的部分也改掉。
更好的做法是局部隔离:
第一步,开一个全新的对话窗口。
新对话没有任何历史包袱,AI 的注意力完全集中在你即将给出的信息上。
第二步,只带入必要的上下文。
【背景】
我有一个 renderList(data) 函数需要重写。
【接口约束】
接收:{ heading, items: string[], note? }
返回:HTMLElement
使用 CSS 变量:--color-text、--color-text-muted、--text-heading、--text-body、--space-md、--space-lg
使用布局类:slide-stack
【当前问题】
items 超过 5 条时,内容溢出容器底部,没有做截断或滚动处理。
【要求】
重新实现这个函数,完整输出,禁止省略。
第三步,拿到新代码后,直接替换旧代码。
不需要合并,不需要对比差异,直接用新函数覆盖旧函数。因为接口没有变(输入输出一致),替换之后整个系统依然能正常运行。
这就是局部隔离的价值:把一个组件的修复工作,完全限定在一次干净的对话里,不污染其他任何东西。
经过前五章,你手里已经有了所有的零件:
index.html:主舞台框架,带挂载点和键盘事件components.js:五个组件渲染函数engine.js:renderSlide()、renderCurrent()、mountSlides()slides.json:所有页面的内容数据现在要做的,是把它们组装成一个可以运行的整体。
在 index.html 的 <head> 里,按顺序引入 CSS 变量文件和 JS 文件:
<!DOCTYPE html>
<htmllang="zh">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>Presentation</title>
<linkrel="stylesheet"href="styles.css">
</head>
<body>
<divid="slide-mount"></div>
<scriptsrc="components.js"></script>
<scriptsrc="engine.js"></script>
</body>
</html>
加载顺序很关键。components.js 必须在 engine.js 之前加载,因为 engine.js 里的 renderSlide() 依赖 components.js 里定义的五个渲染函数。如果顺序反了,浏览器会报"函数未定义"的错误。
slides.json 不需要在 HTML 里引入,它由 engine.js 里的 fetch() 在运行时动态加载。
组装完成后,用本地服务器打开 index.html。注意:不能直接双击打开文件,因为 fetch() 在本地文件协议下会被浏览器安全策略拦截。最简单的启动方式:
# 如果安装了 Python
python3 -m http.server 8080
# 如果安装了 Node.js
npx serve .
打开 http://localhost:8080,如果第一页正常显示,方向键能够翻页,说明组装成功。
真正让 Vibe Coding 效率倍增的,不只是 AI,还有浏览器的开发者工具。两者配合使用,能让调试速度大幅提升。
场景一:样式微调。
第 12 页的标题字号看起来太大了。不要立刻去找 AI,先打开 DevTools 的 Elements 面板,直接在 CSS 变量或行内样式上改数值。DevTools 里的改动实时生效,你能立刻看到效果。
确认满意之后,把改动同步回代码。这个过程可能只需要 30 秒,而如果去找 AI,来回一两轮对话可能要 5 分钟。
场景二:定位 Bug。
某一页渲染出来是空白的。打开 DevTools 的 Console 面板,看有没有报错信息。大多数情况下,错误信息会直接告诉你问题所在:
Uncaught TypeError: Cannot read properties of undefined (reading 'heading')
at renderList (components.js:47)
这条报错告诉你:renderList 函数在第 47 行试图读取 heading 属性,但传入的 data 对象是 undefined。
回去检查 slides.json,找到对应页面的数据,很可能是 type 写成了 "lists" 而不是 "list",导致 renderSlide() 找不到对应的渲染函数,传入了 undefined。
改掉 JSON 里的一个字母,刷新,问题消失。
场景三:把 DevTools 的发现交给 AI 处理。
遇到复杂的 Bug,自己看不出原因,这时候再去找 AI。但不要只说"第 12 页显示不对",而是把 DevTools 里的完整报错信息一起带过去:
"renderList 函数报错如下:[粘贴完整报错堆栈] 当前传入的 data 对象是:[粘贴 JSON] 函数代码如下:[粘贴函数代码] 请找出问题并给出修复后的完整函数,禁止省略。"
信息越完整,AI 定位问题越准确,输出的修复代码越可靠。
验证管道跑通之后,正式开始填充内容。
这个阶段的工作几乎完全在 slides.json 里进行,和 AI 的交互模式也变得非常简单:
"请将以下内容转换为 slides.json 格式的 JSON 数组。每个对象包含 id、type、theme、data 四个字段,type 只能是 hero、split、list、code、chart 五种之一。只输出 JSON,不要输出任何解释文字。
[粘贴你的 PPT 大纲或内容文稿]"
这是一个纯粹的格式转换任务,没有逻辑推理,没有代码生成,AI 的输出非常稳定。即使某几条数据格式有误,也只影响那几页的渲染,不会波及整个系统。
逐段转换,逐段验证,每次处理 10-20 页,保持每次任务的粒度可控。全部转换完成后,slides.json 就是你的 100 页内容,整个 PPT 自动渲染完毕。
回顾整篇文章走过的路:
我们没有试图让 AI 一次性生成所有东西。我们做的恰恰相反——把一个看起来庞大的任务,拆解成一系列有明确边界的小任务,每次只让 AI 做一件事,每次的输出都可以独立验证。
这套方法背后有一个核心洞察:
AI 的能力上限,取决于你给它的任务有多清晰。
任务越模糊,AI 需要做的假设就越多,出错的概率就越高。任务越具体,AI 的输出就越稳定,越可预期。
Vibe Coding 这个词,容易让人联想到一种随性的、直觉驱动的创作方式——感觉对了就行,不需要想太多。但本文展示的实践告诉我们,真正高效的 Vibe Coding 恰恰相反:
它需要你在动手之前,想清楚整个系统的架构。它需要你知道哪些东西只需要写一次,哪些东西需要反复复用。它需要你理解 AI 的局限,并在任务设计层面绕开这些局限,而不是在出了问题之后再去修补。
换句话说:越强大的 AI 工具,越需要使用者具备清晰的工程化思维。 AI 负责执行,你负责设计。这个分工不会因为 AI 变得更强而改变——它只会变得更加重要。
100 页 PPT 只是一个具体的例子。这套思路可以迁移到任何需要用 AI 完成的复杂任务:长篇文档、自动化脚本、数据处理管道……只要你能把任务拆解清楚,AI 就能成为你真正意义上的协作者。
工具从来不是瓶颈。思维方式才是。
全文完。