简介:这份资源面向需要快速产出专业演示文稿的开发者、职场人士与学生,提供一套用大模型生成PPT的可运行源码方案。核心思路是先明确主题、内容与格式需求,再借助DeepSeek等大模型生成结构清晰的Markdown大纲,最后通过Kimi、通义千问或Coze等工具一键转换为PPT,并支持后续版式、图表与动画的个性化调整。压缩包共3个文件,以html页面、inscode工程配置与gitignore忽略规则为主,整体约6KB,体量轻巧,便于直接运行与二次修改。已有241人学习关注,读者可据此掌握从需求梳理、大纲生成到成品输出的完整链路,理解Markdown作为大模型与PPT之间桥梁的作用,并借助开源代码快速搭建自己的文档生成环境,降低PPT制作的技术门槛,提升内容策划与展示效率。
1. 大模型生成PPT这件事,到底卡在哪一步
用大模型生成PPT,很多人第一次试完的感受是:文字挺像样,排版一塌糊涂。你给它一段需求,它哗哗输出一堆大纲和要点,可一旦要落到真正的幻灯片文件上,要么是纯文本堆在页面上,要么是每页字数爆炸、层级混乱,最后还得手动返工两小时。问题不在模型不会写内容,而在于「内容生成」和「文件生成」之间缺了一层结构化的桥。
这个标题要解决的就是这层桥:让大模型输出的不是一段散文,而是一份可被程序解析的页面结构,再由代码把它渲染成真正的 .pptx 文件。适合两类人——一类是想给自己或团队做个自动化周报、方案初稿工具的开发者;另一类是想搞清楚「大模型 + 文档生成」这条链路怎么搭、参数怎么控的工程师。读完你应该能自己跑通一条最小可运行链路,并且知道哪里最容易翻车。
2. 先定结构再谈生成:为什么不能让模型直接吐PPT
2.1 直接让模型输出pptx字节流是条死路
最常见的错误思路,是让大模型直接生成一个 .pptx 文件的二进制内容或者一段 python-pptx 的完整脚本。前者模型根本做不到,它输出的是 token,不是二进制;后者看似可行,但模型写出的脚本经常引用不存在的 API、参数顺序错乱、循环边界写错,你调试脚本的时间比自己做 PPT 还长。
正确的分工是:模型只负责它擅长的事——把模糊需求拆成有层级的标题、要点、备注;程序负责它擅长的事——把这些结构化数据映射到版式、字体、占位符。中间用一份约定好的 JSON schema 做契约。这样模型换了、版式换了,两边互不影响。
我一般会把 schema 定成「幻灯片数组」,每张幻灯片有 layout 类型、title、bullets 数组、可选的 notes。bullets 支持一层嵌套就够了,再深用户也看不清。这个 schema 一旦定下来,后面所有 prompt 都围绕它写,模型输出稳定性会明显提升。
2.2 一份能落地的JSON结构长什么样
先看结构定义,这是整条链路的地基。字段不要贪多,够用就行,多一个字段就多一个模型填错的机会。
{ "deck_title": "季度技术复盘", "slides": [ { "layout": "title", "title": "季度技术复盘", "subtitle": "某研发小组 · 内部初稿" }, { "layout": "bullets", "title": "本季度三个主要进展", "bullets": [ "构建流程耗时从 12 分钟降到 4 分钟", "核心模块测试覆盖率提升到 78%", "线上告警数量环比下降 40%" ], "notes": "重点讲第一条,其余带过" } ] }逻辑说明:layout是渲染时的分发依据,title 页和内容页走不同分支;bullets用字符串数组而不是对象数组,是为了降低模型输出复杂度,嵌套层级靠字符串前缀或单独字段控制。notes是演讲者备注,很多人生成时忘了这块,其实备注才是大模型最能发挥的地方。
参数说明:layout建议只保留三到四种取值,比如 title、bullets、two_column、section,取值越多模型越容易选错。bullets单页建议限制在 3 到 5 条,超过就拆页,这个约束要写进 prompt,不能指望模型自觉。
2.3 用prompt把模型约束到schema上
光有 schema 不够,得让模型知道必须按这个格式输出。常见做法是在 system prompt 里给出 schema 示例,并明确要求「只输出 JSON,不要任何解释文字」。下面是我常用的 prompt 骨架。
SYSTEM_PROMPT = """你是一个PPT大纲生成助手。 根据用户需求,输出严格的JSON,不要输出任何多余文字、不要用markdown代码块包裹。 JSON结构如下: { "deck_title": "字符串", "slides": [ {"layout": "title", "title": "字符串", "subtitle": "字符串"}, {"layout": "bullets", "title": "字符串", "bullets": ["字符串"], "notes": "字符串"} ] } 约束: 1. 每页bullets不超过5条,每条不超过30字。 2. 第一页必须是layout为title的封面。 3. 总页数控制在8到15页。 """ def build_messages(user_need: str): return [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_need}, ]逻辑说明:把 schema 直接写进 system prompt,比事后用正则去修更省事。约束条目要具体到数字,模型对「不超过5条」的执行率远高于「简洁一些」。build_messages单独抽出来,是为了后面接不同模型时只改这一处。
参数说明:如果用的模型支持 JSON mode 或结构化输出,务必打开,能省掉大量解析失败的兜底逻辑。温度建议设低,0.2 到 0.4 之间,太高会让模型在字段名上自由发挥。max_tokens 要留够,一份 12 页的大纲通常需要 1500 到 2500 token。
3. 从JSON到pptx:渲染层的实现与参数
3.1 用python-pptx搭一个最小渲染器
拿到 JSON 之后,渲染这步用 python-pptx 就够了,不需要更重的方案。核心思路是遍历 slides,按 layout 分发到不同函数。下面是一个能跑的最小版本。
from pptx import Presentation from pptx.util import Pt def render_deck(data: dict, out_path: str): prs = Presentation() # 使用内置版式:0为标题页,1为标题+内容 layout_title = prs.slide_layouts[0] layout_bullets = prs.slide_layouts[1] for slide in data["slides"]: if slide["layout"] == "title": s = prs.slides.add_slide(layout_title) s.shapes.title.text = slide["title"] s.placeholders[1].text = slide.get("subtitle", "") elif slide["layout"] == "bullets": s = prs.slides.add_slide(layout_bullets) s.shapes.title.text = slide["title"] body = s.placeholders[1].text_frame body.clear() for i, item in enumerate(slide["bullets"]): p = body.paragraphs[0] if i == 0 else body.add_paragraph() p.text = item p.font.size = Pt(20) if slide.get("notes"): s.notes_slide.notes_text_frame.text = slide["notes"] prs.save(out_path)逻辑说明:slide_layouts[0]和[1]是默认模板里的封面页和内容页,直接复用能省掉手动摆文本框的麻烦。body.clear()很关键,占位符默认带一个空段落,不清掉会多出一行空白。备注写入notes_slide,导出后演讲者视图能看到。
参数说明:Pt(20)是正文字号,中文建议 18 到 22,英文可以小一点。如果要点文字偏长,字号要相应下调,否则会溢出文本框。out_path记得带 .pptx 后缀,python-pptx 不会自动补。
3.2 版式映射:内置layout不够用怎么办
内置版式只有那么几种,实际项目里往往需要自定义。常见做法是准备一个模板 .pptx 文件,在里面把母版和占位符设计好,代码里用Presentation("template.pptx")打开,再按占位符索引或名称填充。
prs = Presentation("template.pptx") # 按占位符名称定位,比索引更稳 def get_placeholder(slide, name): for ph in slide.placeholders: if ph.name == name: return ph return None逻辑说明:用名称定位比用索引稳,因为模板改版时索引会变、名称一般不动。模板里给每个占位符起个有意义的名字,比如 title、body、footer,代码可读性会好很多。
参数说明:模板文件要放在代码能找到的路径,部署时别漏了它。如果模板里用了特殊字体,目标机器没装会回退,重要场合建议把字体嵌入或用通用字体。
3.3 中文字体和溢出这两个坑要提前处理
中文场景下,字体和溢出是最常见的两个翻车点。默认模板的字体对中文支持一般,字号稍大就换行、稍长就溢出。我的做法是在渲染时统一设置字体,并对超长文本做截断或降字号。
from pptx.util import Pt from pptx.dml.color import RGBColor def style_run(run, size=20, bold=False): run.font.size = Pt(size) run.font.bold = bold run.font.name = "微软雅黑" # 换成目标环境确定存在的字体 run.font.color.rgb = RGBColor(0x33, 0x33, 0x33)逻辑说明:font.name只对西文生效,中文要额外设置run.font._rPr里的 east asian 字体,或者干脆在模板母版里设好。颜色用深灰比纯黑柔和,投影时更耐看。
参数说明:字号和文本框宽度要匹配,经验值是每行中文不超过 18 到 20 字。超过就拆成两条要点,别硬塞。截断是下策,拆条才是上策。
4. 避坑与排查:生成PPT最容易翻车的五个地方
4.1 模型输出带markdown代码块导致解析失败
现象:json.loads报错,提示第一个字符是反引号。原因:模型习惯性把 JSON 包在 ```json 里。解决:解析前先剥掉代码块标记,或者用正则提取第一个{到最后一个}之间的内容。更稳的做法是开 JSON mode,从源头杜绝。
4.2 字段名漂移,这次叫bullets下次叫points
现象:渲染时报 KeyError。原因:prompt 里没把字段名钉死,模型自由发挥。解决:在 system prompt 里明确「字段名必须完全一致」,并在解析后做一次字段校验,缺字段就补默认值或重试一次。别指望一次成功,重试逻辑是必需品。
4.3 单页要点过多,导出后文字溢出页面
现象:PPT 打开后文字跑到页面外。原因:模型没遵守条数和字数约束。解决:渲染前做一次校验,超过 5 条或单条超过 30 字就自动拆页或截断。校验放在渲染层,不要只依赖 prompt。
4.4 中文字体在别人机器上显示成方块
现象:自己电脑正常,发给同事全是方框。原因:目标机器没装你用的字体。解决:用系统通用字体,或者在模板里嵌入字体。重要交付前,找一台干净环境试开一次,这个后悔药没有。
4.5 备注丢失或串页
现象:演讲者备注对不上内容。原因:notes 字段在拆页或重排时没跟着走。解决:把 notes 和 slide 绑在同一个对象里,任何拆页操作都同步复制 notes。别用两个平行数组存,迟早对不上。
5. 进阶:让生成质量再上一个台阶的几个技巧
5.1 两段式生成:先出大纲再逐页扩写
一次性让模型生成整份带细节的 PPT,页数一多质量就下滑。更稳的做法是两段式:第一段只生成页面标题列表,第二段对每页单独扩写要点。这样每页的上下文更聚焦,要点质量明显更好。
def gen_outline(need: str): # 第一段:只要标题列表 prompt = f"根据需求生成PPT页面标题列表,每行一个,共8-12个:{need}" return call_llm(prompt).splitlines() def gen_slide_detail(title: str, need: str): # 第二段:单页扩写 prompt = f"为标题「{title}」生成3-5条要点,每条不超过30字,背景需求:{need}" return call_llm(prompt)逻辑说明:拆成两步后,每步的 prompt 更简单,模型出错概率下降。代价是调用次数变多,成本上升,但对质量敏感的场景值得。
参数说明:第一段温度可以稍高,0.5 左右,让标题更有想法;第二段温度调低,0.2 到 0.3,保证要点收敛。两段的模型可以不同,大纲用强模型,扩写用便宜模型。
5.2 用校验函数兜住模型的自由发挥
不管 prompt 写得多严,模型总有概率跑偏。在渲染前加一层校验,把不合规的输出挡在门外,比事后手动改省事得多。
def validate_deck(data: dict) -> list: errors = [] if "slides" not in data or not data["slides"]: errors.append("缺少slides字段或为空") return errors for idx, s in enumerate(data["slides"]): if "layout" not in s or "title" not in s: errors.append(f"第{idx+1}页缺少layout或title") if s.get("layout") == "bullets": bullets = s.get("bullets", []) if len(bullets) > 5: errors.append(f"第{idx+1}页要点超过5条") for b in bullets: if len(b) > 30: errors.append(f"第{idx+1}页存在超长要点") return errors逻辑说明:校验函数返回错误列表,为空才继续渲染,否则触发重试或人工介入。把规则写成代码,比记在脑子里可靠。
参数说明:条数和字数阈值按你的模板调整,模板宽就放宽一点。校验失败的重试次数建议设 2 次,再多就是模型能力问题,换模型比死磕划算。
5.3 一个我踩过的坑:别在渲染层做内容决策
早期我图省事,在渲染函数里判断「这条要点太长就自动截断」。结果截出来的句子语义断裂,读起来莫名其妙。后来改成:渲染层只负责画,内容层面的取舍全部交给生成层或校验层。职责一旦混在一起,出了问题你都不知道该改哪。
现在我的习惯是,任何一条链路先画清楚「谁产出、谁校验、谁渲染」三段,每段只干一件事。这个习惯帮我省下了大量排查时间。希望帮到你。
本文还有配套的精品资源,点击获取