最近后台收到不少留言,大意都是:明明已经能熟练打开对话框问问题了,可真到写论文提纲、整理复习资料、做实验报告的时候,还是一堆复制粘贴、格式错乱、章节层级对不上的活。对话框里排版好好的内容,粘到 Word 里全变成一坨纯文本,标题不是标题、列表不是列表,改格式的时间比写内容还长。
这篇文章就围绕这个问题,给出一次完整的实操整理。核心思路不是教你怎么“问 AI”,而是把 AI 的输出真正变成可以直接交上去、可以直接打印、可以直接编辑的文件——Word 提纲、Excel 知识卡片、PPT 汇报稿。文中会给出完整的提示词模板、Python 调用封装、文件生成代码,以及我实际跑下来踩到的坑。零基础的同学可以跟着一步步跑通,有一点编程基础的同学可以直接拿代码改成自己的小工具。
需要提前说明一点:大模型版本更新节奏很快,本文代码里的模型名称统一通过环境变量注入,你在自己账号里能用哪个就填哪个,不要照抄本文示例里的字符串。功能层面本文只讲公开、通用的文本生成与文件写入能力,不涉及任何来源不明的接口。
1. 先把能力边界搞清楚
1.1 大模型在研究学习场景里真正擅长什么
很多人对这类工具的期待是“我丢一句话,它给我一篇论文”。实际用下来,它在结构化整理和语言润色这两件事上表现最稳:把一堆零散笔记归纳成三级提纲、把一段口语化的实验描述改写成规范表述、把一篇英文摘要翻成中文并保留术语、把错题按知识点归类。
它不擅长的也很明确:凭空给你一个真实实验数据、替你做文献检索并保证引用真实存在、替你做数学推导而且保证每一步都对。这三点必须自己把关。我在实际使用中养成了一个习惯——凡是涉及具体数字、具体文献、具体人名出处的内容,一律当作“待核验草稿”,绝不直接采信。
理解这个边界很重要,因为它直接决定了你应该把哪些任务交给它、哪些任务必须自己动手。把边界搞反了,轻则返工,重则出问题。
1.2 学生党四类高频需求拆解
把需求拆细,才能设计出可复用的流程。我把它归成四类:
| 需求类型 | 典型场景 | 输出形态 | 适合自动化的程度 |
|---|---|---|---|
| 结构生成 | 论文提纲、报告框架、读书笔记目录 | 多层级的标题树 | 高 |
| 内容整理 | 课堂笔记归纳、错题分类、术语对照 | 表格 / 列表 | 高 |
| 语言改写 | 摘要润色、口语转书面、中英互译 | 段落文本 | 中 |
| 事实核验 | 文献出处、数据来源、公式推导 | 需要人工确认 | 低,不建议自动化 |
前两类是最值得做成工具的。因为它们的输出天然是结构化的,而结构化数据正好可以被程序直接写进 docx、xlsx、pptx 里,省掉手工排版。
第三类可以用,但一定要自己读一遍,尤其是专业术语。
第四类我的建议很明确:不要让模型替你做,它给你的“看起来很像”的引用,很可能根本不存在。
1.3 为什么“聊天框复制粘贴”效率低
先说结论:卡点不在模型,在格式在传输过程中丢失了。
你在对话框里看到的三级标题、加粗、编号列表,本质上是 Markdown 渲染后的视觉效果。当你 Ctrl+C / Ctrl+V 到 Word 里,纯文本粘贴会把这些标记全部抹平;带格式粘贴又经常把字体、行距、颜色一起带进来,整篇文档风格不统一。
更麻烦的是批量场景。假设你有二十个知识点要整理成卡片,在聊天框里就是二十轮对话,然后二十次复制粘贴。这个过程中任何一次粘贴出错,你都得回头找是哪一条。
所以真正省时间的方案是:让模型输出结构化数据(JSON),让程序负责排版落到文件里。这样格式是代码控制的,永远一致;内容是模型生成的,批量也不累。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与版本说明
2.1 运行环境与依赖清单
本文示例在 Windows 11 和 macOS 上都跑过,Linux 理论上没有差异。Python 建议用 3.9 及以上版本,太老的版本在类型标注和部分库兼容上会出问题。
依赖只有五个,都是长期维护、文档齐全的库:
bash复制pip install openai python-docx openpyxl python-pptx python-dotenv
逐个说明它们的职责:
openai:调用兼容接口的 SDK。注意这个库在 1.x 版本之后 API 有过较大调整,如果你在网上看到openai.ChatCompletion.create(...)这种写法,那是 0.x 时代的老代码,直接抄会报错。本文统一用 1.x 的客户端写法。python-docx:生成和修改.docx文件,支持标题层级、段落、表格、页边距。openpyxl:生成.xlsx,支持样式、列宽、冻结窗格、筛选。python-pptx:生成.pptx,支持版式、占位符、文本框。python-dotenv:从.env文件读取密钥,避免把 Key 写死在代码里。
关于 Python 版本和库版本:本文不写死具体的版本号,因为这几个库的小版本迭代比较频繁,而且向后兼容做得不错。你可以先用上面的命令装最新版跑通,如果遇到某个方法不存在,用 pip show 库名 看一下版本,再对照官方文档调整。这是比抄版本号更靠谱的做法。
2.2 密钥管理:不要写死在代码里
这是我最想强调的一点。很多同学图省事,直接把密钥写进 main.py,然后代码往群里一发、往仓库一推,密钥就泄露了。密钥泄露的后果是别人可以消耗你的额度,甚至滥用你的账号。
正确做法是三层隔离:
第一层,.env 文件存密钥,这个文件永远不提交到 Git:
properties复制# 文件路径:.env(不要提交到代码仓库)
API_KEY=sk-your-key-here
BASE_URL=https://your-endpoint.example.com/v1
MODEL_NAME=your-available-model
第二层,.gitignore 把 .env 排除掉:
properties复制# 文件路径:.gitignore
.env
__pycache__/
*.pyc
output/
第三层,代码里通过环境变量读取,读取不到就给出明确提示:
python复制# 文件路径:config.py
import os
from dotenv import load_dotenv
load_dotenv()
API_KEY = os.getenv("API_KEY")
BASE_URL = os.getenv("BASE_URL")
MODEL_NAME = os.getenv("MODEL_NAME")
if not API_KEY:
raise RuntimeError("未读取到 API_KEY,请检查 .env 文件是否存在于项目根目录")
if not MODEL_NAME:
raise RuntimeError("未读取到 MODEL_NAME,请填入你账号中实际可用的模型名称")
BASE_URL 这一项取决于你使用的服务入口,如果你的 SDK 默认地址就能用,把这一行删掉即可。MODEL_NAME 一定要填你自己账号里实际可选的名称,本文所有示例都不写死这个值。
2.3 项目目录结构
code复制study-helper/
├── .env # 密钥配置,不提交
├── .gitignore
├── config.py # 配置读取与校验
├── llm_client.py # 模型调用封装
├── prompts.py # 提示词模板集中管理
├── generators/
│ ├── __init__.py
│ ├── docx_gen.py # 生成 Word
│ ├── xlsx_gen.py # 生成 Excel
│ └── pptx_gen.py # 生成 PPT
├── main.py # 入口,按需调用
└── output/ # 生成的文件输出目录
这个结构看起来比“一个文件搞定”麻烦,但它解决了后面两个真实痛点:一是提示词改了要能快速回滚,二是三种文件生成逻辑互相独立,改一个不会影响另外两个。
3. 提示词模板:从“随口一问”到可复用
3.1 一个够用的提示词骨架
我试过很多写法,最后稳定下来的是四段式:角色 + 任务 + 约束 + 输出格式。
python复制# 文件路径:prompts.py
OUTLINE_SYSTEM = """你是一名严谨的学术写作助手,擅长把零散的研究想法整理成层次清晰的写作提纲。
工作原则:
1. 只输出提纲结构,不要输出正文段落。
2. 每一级标题控制在 20 字以内,避免长句。
3. 如果用户给的信息不足以支撑某一章节,标注为「待补充」,不要虚构内容。
"""
OUTLINE_USER_TEMPLATE = """请为下面的主题生成一份 {level} 级提纲。
主题:{topic}
专业方向:{major}
字数规模:{scale}
必须覆盖的要点:{must_cover}
输出要求:
- 严格输出 JSON,不要有任何解释性文字,不要用 Markdown 代码块包裹。
- JSON 结构如下:
{{
"title": "文章标题",
"sections": [
{{
"heading": "一级标题",
"subsections": [
{{"heading": "二级标题", "points": ["要点1", "要点2"]}}
]
}}
]
}}
"""
这里有三个细节值得展开。
第一,只输出 JSON,不要用代码块包裹 这句话很重要。如果你不说,模型很可能给你一段 ```json 包起来的文本,json.loads() 直接抛异常。说了之后成功率会高很多,但仍然不是 100%,所以后面代码里还要做容错。
第二,不要虚构内容 这条约束要显式写。不写的话,遇到信息不足的章节,它会自己编一段看起来很像回事的内容填进去,你事后排查非常费劲。
第三,模板里用 {{ 转义大括号,是因为这段字符串要过 str.format()。这是一个很容易踩的坑,第一次写的人经常被 KeyError 搞懵。
3.2 论文提纲场景的具体化
上面是通用骨架,实际用的时候要往里面填业务约束。以本科毕业论文为例:
python复制# 文件路径:main.py(片段)
from prompts import OUTLINE_SYSTEM, OUTLINE_USER_TEMPLATE
user_prompt = OUTLINE_USER_TEMPLATE.format(
level="三",
topic="基于协同过滤的校园二手书推荐系统设计与实现",
major="计算机科学与技术",
scale="正文约 1.2 万字",
must_cover="需求分析、系统设计、算法选型对比、数据库设计、测试与评估",
)
注意 must_cover 这一项。它是控制提纲质量的关键杠杆。你不填,模型给你的提纲往往非常“通用”——绪论、相关技术、系统设计、系统实现、总结与展望,五章套模板。你填了具体要点,它才会把这些要点分配到合理的章节位置,并且在你没提到的部分做补充。
3.3 复习资料整理场景的具体化
这个场景和论文提纲不一样,它的输出更适合表格而不是树形结构。因为复习资料的核心是“知识点 - 解释 - 易错点”这种三元组关系,表格天然适合。
python复制# 文件路径:prompts.py(片段)
CARD_SYSTEM = """你是一名擅长教学的学习助手。你的任务是把教材内容拆解成便于记忆的知识卡片。
工作原则:
1. 一张卡片只讲一个知识点,不要合并。
2. 解释控制在 80 字以内,用学生能听懂的话。
3. 易错点必须是真实存在的常见错误,不要为了凑数编造。
4. 信息不足时,字段填「待补充」。
"""
CARD_USER_TEMPLATE = """请把下面的内容拆解成知识卡片。
科目:{subject}
章节:{chapter}
原文内容:
{content}
输出要求:
- 严格输出 JSON 数组,不要任何解释文字。
- 每个元素包含四个字段:term(知识点名称)、explain(通俗解释)、pitfall(常见易错点)、tag(所属子类)。
"""
3.4 为什么要落到 JSON,而不是直接要 Markdown
有人会问:我直接让它输出 Markdown,然后转成 docx 不行吗?
可以,但麻烦。Markdown 转 docx 需要额外的解析步骤,而且模型输出的 Markdown 格式经常不规范——有时候用 #,有时候用 一、,有时候列表符号混着 - 和 *。你得写一堆正则去兼容各种变形。
JSON 的好处是结构是契约。你在提示词里定义了字段,解析出来的就是固定字段,代码里直接 item["term"] 就能拿到值,不需要猜。代价是要处理解析失败的情况,这个代价比写正则小得多。
4. 从模型输出到真实文件:中间发生了什么
4.1 完整链路拆解
整个流程可以拆成五步,用文字描述比画图更清楚:
第一步,构造提示词。把模板和业务参数拼成最终的 user message。
第二步,发起请求。带上 system message 和 user message,指定模型名称。
第三步,拿到响应文本,做 JSON 解析。这里必须做容错,因为模型偶尔会加解释、加代码块标记、或者输出被截断。
第四步,把解析好的字典/列表交给对应的生成器,生成文件对象。
第五步,保存到 output/ 目录,并打印文件路径方便核对。
每一步都可能出问题,所以要分开封装,出错时能快速定位是哪一步。
4.2 调用封装:把重试和容错写进去
下面这个封装我用了很久,主要解决三件事:统一超时、JSON 解析容错、失败重试。
python复制# 文件路径:llm_client.py
import json
import re
import time
from openai import OpenAI
from config import API_KEY, BASE_URL, MODEL_NAME
_client_kwargs = {"api_key": API_KEY}
if BASE_URL:
_client_kwargs["base_url"] = BASE_URL
client = OpenAI(**_client_kwargs)
def _strip_code_fence(text: str) -> str:
"""去掉模型可能加上的 ```json 包裹"""
text = text.strip()
fence = re.match(r"^```(?:json)?\s*(.*?)\s*```$", text, re.S)
if fence:
return fence.group(1).strip()
return text
def chat_json(system_prompt: str, user_prompt: str, retries: int = 3) -> dict | list:
"""调用模型并返回解析后的 JSON,失败自动重试"""
last_error = None
for attempt in range(1, retries + 1):
try:
resp = client.chat.completions.create(
model=MODEL_NAME,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0.3,
)
raw = resp.choices[0].message.content or ""
cleaned = _strip_code_fence(raw)
return json.loads(cleaned)
except json.JSONDecodeError as e:
last_error = e
print(f"[第 {attempt} 次] JSON 解析失败,准备重试:{e}")
time.sleep(1.5 * attempt)
except Exception as e:
last_error = e
print(f"[第 {attempt} 次] 请求异常,准备重试:{e}")
time.sleep(1.5 * attempt)
raise RuntimeError(f"连续 {retries} 次调用失败,最后一次错误:{last_error}")
几个参数值得解释。
temperature=0.3:这个值控制输出的随机性。范围一般是 0 到 1,越低越确定。做结构化抽取这种任务,0.2 到 0.4 之间比较合适。调太高,同样的问题每次给你的字段名都可能不一样,解析容易失败。
retries=3:不是所有失败都值得重试,但 JSON 解析失败是典型值得重试的——模型下一次很可能就输出对了。用 time.sleep(1.5 * attempt) 做退避,避免短时间内连续打请求。
dict | list 这个类型标注需要 Python 3.10 以上。如果你用的是 3.9,改成 -> dict 或者去掉标注,不影响运行。
4.3 长文本为什么要分批
如果你要处理的原文超过几千字,一次性塞进去有两个问题:一是可能超出上下文长度限制,二是模型容易“注意力分散”,后半段的质量明显下降。
我的经验做法是按章节切分。比如复习资料整理,按教材的小节切,每节单独发一次请求,最后把结果 merge 起来。这样既能保证质量,又能在某一段失败时只重跑那一段,不用整体重来。
5. 实战一:一键生成论文提纲 Word 文档
5.1 安装依赖并验证
先确认依赖装好了:
bash复制pip install openai python-docx openpyxl python-pptx python-dotenv
python -c "import docx, openpyxl, pptx; print('依赖 OK')"
如果最后一行能打印出 依赖 OK,说明环境没问题。报 ModuleNotFoundError 的话,多半是 pip 装到了另一个 Python 环境,用 python -m pip install 重新装一次。
5.2 编写 Word 生成器
python复制# 文件路径:generators/docx_gen.py
import os
from docx import Document
from docx.shared import Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
def _set_default_font(doc: Document, font_name: str = "宋体", size: int = 12):
"""统一设置正文默认字体,中文必须同时设置 eastAsia"""
style = doc.styles["Normal"]
style.font.name = font_name
style.font.size = Pt(size)
style.element.rPr.rFonts.set(qn("w:eastAsia"), font_name)
def build_outline_docx(data: dict, output_path: str) -> str:
doc = Document()
_set_default_font(doc)
# 页边距:上下 2.54cm,左右 3.17cm,这是 Word 的默认值
for section in doc.sections:
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(3.17)
# 文档标题
title = data.get("title", "未命名文档")
heading = doc.add_heading(level=0)
run = heading.add_run(title)
run.font.size = Pt(18)
run.font.name = "黑体"
run.element.rPr.rFonts.set(qn("w:eastAsia"), "黑体")
heading.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 逐级写入
for idx, section in enumerate(data.get("sections", []), start=1):
sec_heading = f"{idx}. {section.get('heading', '未命名章节')}"
doc.add_heading(sec_heading, level=1)
for sub_idx, sub in enumerate(section.get("subsections", []), start=1):
sub_heading = f"{idx}.{sub_idx} {sub.get('heading', '未命名小节')}"
doc.add_heading(sub_heading, level=2)
for point in sub.get("points", []):
para = doc.add_paragraph(style="List Bullet")
para.add_run(point)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
doc.save(output_path)
return output_path
这里有几个地方值得单独说明,都是实际调试中碰出来的。
中文字体必须设置 w:eastAsia。只设 style.font.name 是不够的,那只对英文字符生效。中文字符会走 w:eastAsia 这个属性,不设的话你在 Word 里看到的就是默认宋体,怎么改都改不动。这也是很多人用 python-docx 时最常见的困惑。
add_heading(level=0) 是文档标题,对应 Word 里的“标题”样式,level=1 对应“标题 1”,以此类推。用内置样式而不是手动设字号,好处是生成后还能用 Word 的导航窗格、自动生成目录。
style="List Bullet" 是内置的项目符号样式。如果你自己 add_paragraph("• " + text) 手动加符号,看起来一样,但实际上不是真正的列表,复制出去格式会乱。
5.3 组装主流程
python复制# 文件路径:main.py
import json
from datetime import datetime
from llm_client import chat_json
from prompts import OUTLINE_SYSTEM, OUTLINE_USER_TEMPLATE
from generators.docx_gen import build_outline_docx
def make_outline_docx(topic: str, major: str, scale: str, must_cover: str) -> str:
user_prompt = OUTLINE_USER_TEMPLATE.format(
level="三",
topic=topic,
major=major,
scale=scale,
must_cover=must_cover,
)
data = chat_json(OUTLINE_SYSTEM, user_prompt)
# 打印出来先看一眼结构对不对
print(json.dumps(data, ensure_ascii=False, indent=2)[:800])
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"output/outline_{stamp}.docx"
return build_outline_docx(data, output_path)
if __name__ == "__main__":
path = make_outline_docx(
topic="基于协同过滤的校园二手书推荐系统设计与实现",
major="计算机科学与技术",
scale="正文约 1.2 万字",
must_cover="需求分析、系统设计、算法选型对比、数据库设计、测试与评估",
)
print(f"已生成:{path}")
5.4 运行与结果核对
bash复制python main.py
预期输出两段内容:第一段是解析后的 JSON 结构,用来快速检查章节数量和层级是否正确;第二段是生成的文件路径。
打开生成的 docx,你应该看到:居中的文档标题、带编号的一级标题(自动套用“标题 1”样式)、带 1.1 这种编号的二级标题、以及下面的项目符号列表。在 Word 里按 Ctrl+F5 或者“引用 - 目录”就能自动生成带页码的目录页。
如果生成的文档里中文变成了方块或者字体很怪,回去检查 _set_default_font 里 w:eastAsia 那一行有没有漏。
6. 实战二:把复习资料批量生成为 Excel 知识卡片
6.1 数据结构设计
Excel 版本的价值在于可筛选、可排序、可打印。比如期末复习时,你想只看某一章、只看标记为“高频易错”的条目,直接筛选就行,比在 Word 里翻页快得多。
表格设计四列:知识点、通俗解释、常见易错点、所属子类。列宽和行高要提前规划好,否则生成出来一打开就是一堆挤在一起的长文本。
6.2 生成器实现
python复制# 文件路径:generators/xlsx_gen.py
import os
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from openpyxl.utils import get_column_letter
def build_cards_xlsx(cards: list, output_path: str) -> str:
wb = Workbook()
ws = wb.active
ws.title = "知识卡片"
headers = ["知识点", "通俗解释", "常见易错点", "所属子类"]
ws.append(headers)
# 表头样式
header_font = Font(name="微软雅黑", size=11, bold=True, color="FFFFFF")
header_fill = PatternFill("solid", fgColor="4F81BD")
thin = Side(style="thin", color="BFBFBF")
border = Border(left=thin, right=thin, top=thin, bottom=thin)
for col_idx in range(1, len(headers) + 1):
cell = ws.cell(row=1, column=col_idx)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center", vertical="center")
cell.border = border
# 写入数据
for row_idx, item in enumerate(cards, start=2):
ws.cell(row=row_idx, column=1, value=item.get("term", ""))
ws.cell(row=row_idx, column=2, value=item.get("explain", ""))
ws.cell(row=row_idx, column=3, value=item.get("pitfall", ""))
ws.cell(row=row_idx, column=4, value=item.get("tag", ""))
for col_idx in range(1, len(headers) + 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.alignment = Alignment(
vertical="top", wrap_text=True,
horizontal="center" if col_idx in (1, 4) else "left",
)
cell.border = border
# 交替行底色,方便横向阅读时对齐
if row_idx % 2 == 0:
for col_idx in range(1, len(headers) + 1):
ws.cell(row=row_idx, column=col_idx).fill = PatternFill("solid", fgColor="F2F2F2")
# 列宽
widths = [22, 50, 45, 18]
for idx, width in enumerate(widths, start=1):
ws.column_dimensions[get_column_letter(idx)].width = width
ws.row_dimensions[1].height = 24
for r in range(2, len(cards) + 2):
ws.row_dimensions[r].height = 60
# 冻结首行 + 自动筛选
ws.freeze_panes = "A2"
ws.auto_filter.ref = f"A1:{get_column_letter(len(headers))}{len(cards) + 1}"
os.makedirs(os.path.dirname(output_path), exist_ok=True)
wb.save(output_path)
return output_path
三个关键点解释一下。
wrap_text=True 必须开,否则长解释会溢出到相邻单元格,看起来像错行。配合你手动设的 row_dimensions[r].height,才能保证每条内容都能完整显示。
freeze_panes = "A2" 冻结首行,往下滚动时表头一直可见。auto_filter.ref 开启自动筛选,这个在复习时特别实用——点一下“所属子类”列的下拉箭头,就能只看某一类。
交替行底色不是必须的,但表格行数多的时候,人眼横向对齐很累,加了底色阅读成本明显下降。这是纯体验层面的优化,代码只有三行。
6.3 主流程与验证
python复制# 文件路径:main.py(追加)
from prompts import CARD_SYSTEM, CARD_USER_TEMPLATE
from generators.xlsx_gen import build_cards_xlsx
def make_cards_xlsx(subject: str, chapter: str, content: str) -> str:
user_prompt = CARD_USER_TEMPLATE.format(
subject=subject, chapter=chapter, content=content,
)
cards = chat_json(CARD_SYSTEM, user_prompt)
if not isinstance(cards, list):
raise ValueError(f"预期返回列表,实际返回 {type(cards)},请检查提示词")
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
return build_cards_xlsx(cards, f"output/cards_{stamp}.xlsx")
那个 isinstance(cards, list) 检查是有必要的。模型有时候会把数组包在一个对象里返回,比如 {"cards": [...]},这时候你直接当列表用就会报 TypeError。提前检查能省不少调试时间。
跑完打开 xlsx,检查三件事:表头颜色和字体是否生效、长解释是否自动换行、筛选按钮是否出现在第一行。
7. 实战三:生成汇报用的 PPT
7.1 版式规划
PPT 和 Word、Excel 的生成逻辑差别最大,因为 PPT 的核心是版式,不是内容流。python-pptx 提供了十一种内置版式,常用的就三种:
- 索引 0:标题幻灯片(一个大标题 + 一个副标题)
- 索引 1:标题和内容(标题 + 正文占位符,适合放要点)
- 索引 5:仅标题(只有标题,正文自己加文本框,适合做对比页)
我的做法是:封面用 0,内容页统一用 1,最后一页用 5 放总结。
7.2 代码实现
python复制# 文件路径:generators/pptx_gen.py
import os
from pptx import Presentation
from pptx.util import Pt, Inches
from pptx.dml.color import RGBColor
def build_report_pptx(plan: dict, output_path: str) -> str:
prs = Presentation()
prs.slide_width = Inches(13.333) # 16:9
prs.slide_height = Inches(7.5)
# 封面
cover = prs.slides.add_slide(prs.slide_layouts[0])
cover.shapes.title.text = plan.get("title", "汇报")
cover.placeholders[1].text = plan.get("subtitle", "")
# 内容页
for item in plan.get("slides", []):
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = item.get("heading", "")
body = slide.placeholders[1]
tf = body.text_frame
tf.clear()
points = item.get("points", [])
for idx, point in enumerate(points):
para = tf.paragraphs[0] if idx == 0 else tf.add_paragraph()
para.text = point
para.level = 0
for run in para.runs:
run.font.size = Pt(20)
run.font.color.rgb = RGBColor(0x33, 0x33, 0x33)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
prs.save(output_path)
return output_path
prs.slide_width = Inches(13.333) 这一行是把默认的 4:3 改成 16:9。这一步必须在添加任何幻灯片之前做,否则改的是新建幻灯片的尺寸,已添加的不受影响,最后会出现页面宽高不一致的情况——这是 python-pptx 里一个很隐蔽的坑。
tf.clear() 是为了清掉占位符里的默认空段落,不清的话你设置的第一个要点前面会多一个空行。
字号 Pt(20) 是经验值。内容页正文一般控制在 18 到 24 之间,太小了投影看不清,太大了要点放不下。如果某一页要点特别多,建议在提示词阶段就限制“每页不超过 5 个要点”,而不是在代码里缩字号——后者治标不治本。
7.3 对应的提示词
python复制# 文件路径:prompts.py(片段)
PPT_USER_TEMPLATE = """请为下面的主题设计一份汇报 PPT 大纲。
主题:{topic}
汇报场景:{scene}
汇报时长:{duration}
输出要求:
- 严格输出 JSON,字段为 title、subtitle、slides。
- slides 是一个数组,每个元素包含 heading 和 points。
- points 是字符串数组,每页控制在 3 到 5 条,每条不超过 25 字。
- 总页数控制在 8 到 12 页。
"""
PPT_SYSTEM = """你是一名擅长学术汇报的助手。输出要简洁,要点式表达,避免大段文字。"""
8. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
json.JSONDecodeError |
模型加了 ```json 包裹或附带了说明文字 | 用正则剥掉代码块标记;提示词里强调“只输出 JSON” |
| 生成的 docx 中文字体不对 | 只设了 font.name,没设 w:eastAsia |
通过 style.element.rPr.rFonts.set(qn("w:eastAsia"), "宋体") 单独设置 |
| Excel 长文本显示不全 | 没开 wrap_text,或行高不够 |
设置 Alignment(wrap_text=True) 并手动指定 row_dimensions 高度 |
| PPT 页面宽高不一致 | 在添加幻灯片之后才改 slide_width |
把尺寸设置挪到 add_slide 之前 |
| 大纲章节编号错乱 | 模型自己加了“第一章”这类中文编号 | 提示词里明确“不要输出编号”,由代码统一生成 |
| 请求报 401 | 密钥无效或未正确加载 | 检查 .env 是否在项目根目录、变量名是否拼写一致 |
| 请求报 429 | 触发频率限制 | 加大重试间隔,或降低并发,不要短时间内连续打请求 |
| 返回内容被截断 | 输出超过最大 token 限制 | 拆分任务,控制单次输出量;或调大 max_[token](https://taotoken.net?utm_source=hardware)s |
| 生成的列表不是真列表 | 手动用 "• " + text 拼接 |
改用 doc.add_paragraph(style="List Bullet") |
再补充一份排查顺序,遇到问题时按这个顺序走,能覆盖八成场景:
第一步,看原始响应。在 chat_json 里把 raw 打印出来,80% 的解析问题看一眼就能定位。
第二步,看提示词。字段名、层级、数量限制有没有写清楚。提示词写得含糊,输出一定含糊。
第三步,看数据类型。打印 type(data),确认是 dict 还是 list,跟代码里的预期是否一致。
第四步,看文件是否保存成功。路径里有没有不存在的目录,os.makedirs 有没有加。
9. 最佳实践与工程建议
9.1 提示词与代码分离
把所有提示词放在 prompts.py 里,不要散落在各个函数内部。这个习惯的价值在调优时会非常明显——你要改一句约束,只需要动一个文件,改完跑一次就能对比效果。如果提示词混在业务代码里,改起来畏手畏脚,最后就懒得优化了。
更进一步的做法是把提示词版本化。比如 OUTLINE_SYSTEM_V1、OUTLINE_SYSTEM_V2,跑同一批输入对比输出质量,留下效果好的那个。这个做法在大模型应用开发里很常见,叫“提示词迭代”。
9.2 成本与速率控制
三个实用手段。
一是缓存。同样的输入不要重复请求。最简单的做法是把 md5(system + user) 作为文件名,把结果存到 cache/ 目录,下次先查缓存。对于反复调试同一份提纲的场景,能省掉大量重复开销。
二是批量。如果一次要处理二十个章节,不要开二十个线程同时打。用 for 循环串行处理,中间 time.sleep(1),稳定性远高于并发。
三是限制输出。提示词里明确“每页 3 到 5 条,每条不超过 25 字”,这是最直接的降本手段——输出短了,token 消耗自然就下来了,而且质量往往更好。
9.3 学术诚信边界
这一条必须单独说,因为学生场景绕不开。
工具可以用来整理结构、润色语言、梳理思路、生成练习题,这些是学习辅助。不能用来代替自己完成需要独立完成的作业和考核内容,更不能把生成的内容当成自己的原创成果提交。
我的建议是:把生成结果当作“初稿”或者“对照材料”。比如提纲生成之后,你自己逐条判断哪些章节该保留、哪些该合并、哪些该换成你自己的思路。这个过程本身就是学习和思考,跳过它,工具就变成了负担。
另外,如果课程或学校对工具有明确使用规定,以规定为准。
9.4 数据与隐私
不要往请求里塞真实的个人信息、他人的隐私数据、未公开的实验数据。如果确实需要处理包含敏感信息的文档,先做脱敏——把姓名、学号、联系方式替换成占位符。
密钥管理前面已经说过,这里再强调一次:.env 不进版本库,不在截图里露出来,不在公开场合贴出来。如果不小心泄露了,第一时间去平台重置。
9.5 代码可维护性
几个小习惯,长期收益很大。
函数职责单一:build_outline_docx 只负责写文件,不负责调模型。这样你想换个文件格式,只需要重写生成器,调用逻辑不用动。
样式参数集中:字号、颜色、列宽这些魔法数字,统一放在文件顶部的常量区,不要散在函数里。下次老师要求“正文用仿宋三号”,你改一个常量就完事。
输出目录固定:所有生成的文件都落到 output/,带时间戳命名。别问为什么,等你生成到第二十个文件、分不清哪个是最终版的时候就懂了。
10. 继续往下走的方向
把三条链路跑通之后,你会发现这套东西的复用性比想象中强。同样的“提示词 + 结构化输出 + 文件生成”模式,可以扩展到很多场景:把实习周报生成成规范的 Word 文档、把项目需求整理成 Excel 任务清单、把课程设计答辩内容做成 PPT。
如果要继续深入,我建议按这个顺序:
先把提示词模板打磨到自己满意。这是投入产出比最高的一块,模板好了,后面所有环节都省事。可以试试同一个主题用三种不同写法跑一遍,对比输出质量。
再考虑把命令行参数化。用 argparse 加几个参数,python main.py --type docx --topic "xxx",比每次改代码方便得多。
最后再考虑做成图形界面。tkinter 或者 gradio 都能做,但建议等你完全摸清命令行版本之后再说,否则调试成本会翻倍。
实际项目里最需要警惕的两件事:一是不要相信模型给的任何具体数字和文献出处,全部自己核验;二是不要在没搞清楚提示词效果的情况下就批量处理几百条数据,先拿五条试跑,确认输出结构稳定再放大。
代码都在上面了,直接复制到一个空目录里,配好 .env 就能跑。跑通第一条链路之后,剩下的就是改提示词和改样式,没有新的技术门槛。
