背景
上个月,我在重构自己的个人知识库工具。这个工具的核心功能之一,就是能把我在碎片时间记录的零散想法(有时就是几个关键词或一两句话),自动整理扩展成结构清晰、内容完整的文章草稿。之前我试过一些现成的AI写作工具,但要么无法集成到我的工作流里,要么生成的内容风格和我想要的相差甚远。
我的需求很明确:输入一个核心主题(比如“Python装饰器的三种用法”),输出一篇包含引言、分点论述和总结的Markdown格式文章。一开始我觉得这很简单,不就是调用一下DeepSeek的Chat Completion API,写个提示词让它“写篇文章”嘛。结果真正动手才发现,从“能生成文字”到“能生成我想要的、可用的文章”,中间隔着好几个大坑。
问题分析
我的第一版代码简单到有点天真:
import openai
client = openai.OpenAI(
api_key="your_deepseek_api_key",
base_url="https://api.deepseek.com"
)
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "user", "content": "写一篇关于Python装饰器用法的文章"}
]
)
print(response.choices[0].message.content)
跑出来的结果……怎么说呢,内容本身还行,但问题一大堆:
- 结构随机:有时有标题,有时没有;分段完全看模型心情。
- 长度失控:我想要800-1000字的草稿,它可能给我300字,也可能给我2000字。
- 格式混乱:说好要Markdown,但标题的
#号时有时无,代码块经常不用反引号包裹。 - 成本问题:一次生成太长的文章,token消耗大,而且中间出错就要全部重来。
我意识到,不能把“写一篇结构完整的文章”这么复杂的任务,一股脑丢给模型自由发挥。我需要拆解任务,给模型更明确的指引,并且把生成过程控制起来。
核心实现
第一步:设计系统提示词,锁定文章框架
我首先解决的是结构问题。我的思路是,在系统提示词里明确约定文章必须包含的章节和格式要求。这里有个关键点:不仅要告诉模型“你要写Markdown”,还要告诉它每个章节应该写什么、怎么写。
def get_system_prompt(topic):
return f"""你是一位资深的{get_topic_field(topic)}领域技术作者,擅长撰写结构清晰、实用性强的教程类文章。
请根据用户提供的主题,生成一篇完整的Markdown格式文章。
# 文章结构要求(必须严格遵循):
1. 主标题:用一级标题(# )表示,直接点明文章核心
2. 引言:用普通段落,简要说明文章要解决的问题和受众
3. 核心内容:分3-5个小节,每个小节用二级标题(## )开头
4. 每个小节下应有2-4个要点,用段落或列表形式展开
5. 代码示例:如果涉及技术概念,必须提供可运行的代码示例,用```python包裹
6. 总结:用“## 总结”开头,回顾全文要点
7. 注意事项:用“## 注意事项”开头,列出常见的坑或最佳实践
# 格式要求:
- 严格使用Markdown语法
- 代码块必须指定语言类型
- 文章总长度控制在800-1200字之间
- 语言风格:口语化、亲切,像经验分享而不是教科书
当前文章主题:{topic}
"""
注意这个细节:我通过get_topic_field(topic)函数简单判断主题领域(比如包含“Python”就返回“Python开发”),这样能让模型更好地把握技术细节的准确性。系统提示词写得越具体,模型“跑偏”的概率就越小。
第二步:分步生成,先大纲后内容
直接生成完整文章,一旦不满意就要全部重来,token浪费严重。我改成了两步走策略:先让模型生成详细大纲,我审核调整后,再根据大纲填充内容。
def generate_outline(topic):
"""第一步:生成文章大纲"""
client = get_deepseek_client()
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": "你是一位专业的文章架构师,请根据主题生成详细的内容大纲,只返回大纲,不要生成具体内容。"},
{"role": "user", "content": f"""请为《{topic}》生成文章大纲,要求:
1. 列出所有二级标题(## 标题)
2. 在每个二级标题下,用短句列出3-5个要点
3. 标明哪里需要插入代码示例
4. 总章节数控制在4-6个
请用清晰的层级格式返回,例如:
## 引言
- 要点1
- 要点2
## 第一部分标题
- 要点1(此处需要Python代码示例)
- 要点2
"""}
],
temperature=0.7 # 稍高的温度让大纲更有创意
)
outline = response.choices[0].message.content
print("生成的大纲:")
print(outline)
print("\n" + "="*50)
# 这里可以加入人工审核或自动修正逻辑
# 比如检查章节数量、是否有代码标记等
return outline
这里有个坑:最初我把temperature设得太低(0.2),结果每次生成的大纲都差不多,缺乏多样性。调到0.7后,能在保持结构合理的前提下,获得更有新意的角度。
第三步:基于大纲的流式内容生成
有了认可的大纲后,我就可以分段生成内容了。这里我用了两个技巧:
- 流式输出:让用户能看到生成进度,体验更好
- 上下文管理:每生成一节,都把前面的大纲和已生成内容带上,保持连贯性
def generate_content_by_section(topic, outline):
"""第二步:根据大纲分节生成内容"""
client = get_deepseek_client()
# 解析大纲,拆分成各个章节
sections = parse_outline(outline) # 这是一个自定义函数,按##分割大纲
full_article = f"# {topic}\n\n"
for i, section in enumerate(sections):
print(f"正在生成第 {i+1}/{len(sections)} 节:{section['title']}")
# 构建当前节的上下文
messages = [
{"role": "system", "content": get_system_prompt(topic)},
{"role": "user", "content": f"""这是文章《{topic}》的完整大纲:
{outline}
以下是已经写好的前文:
{full_article}
现在请你接着写大纲中的这一部分:
{section['content']}
要求:
1. 只写这一部分的内容,不要写其他部分
2. 保持与上文连贯
3. 如果大纲中标注了“需要代码示例”,请务必提供
4. 长度控制在200-300字左右
"""}
]
# 流式生成
stream = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
stream=True,
temperature=0.3 # 内容生成时温度低一些,更稳定
)
section_content = f"## {section['title']}\n\n"
for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
print(content, end="", flush=True)
section_content += content
full_article += section_content + "\n\n"
print("\n" + "-"*40)
return full_article
这里有个重要的优化:我最初是让模型一次性生成所有内容,但发现后面的章节经常会忘记前面的设定,或者出现重复。改成逐节生成,并把“完整大纲”和“已生成前文”都放在上下文里,显著提升了文章的一致性和连贯性。
第四步:后处理与格式校验
模型生成的内容,有时候会在格式细节上出问题。我增加了一个后处理环节:
def post_process_article(article):
"""后处理:修正常见的格式问题"""
# 1. 确保标题格式正确
lines = article.split('\n')
processed_lines = []
for line in lines:
# 修复:如果行末有#号(模型有时会这样)
if line.strip().endswith('#') and not line.strip().startswith('#'):
line = line.rstrip('#').rstrip()
# 修复:代码块语言标记
if '```' in line and 'python' not in line and 'bash' not in line and 'json' not in line:
# 检查内容是否像代码
if any(keyword in line.lower() for keyword in ['def ', 'import ', 'print(', 'return ']):
line = '```python'
elif line.strip() == '```':
line = '```'
processed_lines.append(line)
# 2. 移除过多的空行(超过2个连续空行)
processed_article = '\n'.join(processed_lines)
import re
processed_article = re.sub(r'\n\s*\n\s*\n+', '\n\n', processed_article)
# 3. 确保文章以总结或注意事项结尾
if '## 总结' not in processed_article and '## 注意事项' not in processed_article:
processed_article += "\n\n## 总结\n\n本文介绍了相关概念和用法,建议在实际项目中多加练习。"
return processed_article
这个后处理模块是从实际错误中积累起来的。比如我发现模型有时会在段落末尾误加#号,或者代码块忘记指定语言。这些自动修正让最终输出的文章质量更稳定。
完整代码
"""
article_generator.py
使用DeepSeek API实现结构化文章生成
需要安装:pip install openai
"""
import re
from typing import List, Dict
import openai
class ArticleGenerator:
def __init__(self, api_key: str):
"""初始化DeepSeek客户端"""
self.client = openai.OpenAI(
api_key=api_key,
base_url="https://api.deepseek.com"
)
self.model = "deepseek-chat"
def get_system_prompt(self, topic: str) -> str:
"""生成系统提示词"""
field = self._detect_field(topic)
return f"""你是一位资深的{field}领域技术作者,擅长撰写结构清晰、实用性强的教程类文章。
请根据用户提供的主题,生成一篇完整的Markdown格式文章。
# 文章结构要求(必须严格遵循):
1. 主标题:用一级标题(# )表示,直接点明文章核心
2. 引言:用普通段落,简要说明文章要解决的问题和受众
3. 核心内容:分3-5个小节,每个小节用二级标题(## )开头
4. 每个小节下应有2-4个要点,用段落或列表形式展开
5. 代码示例:如果涉及技术概念,必须提供可运行的代码示例,用```python包裹
6. 总结:用“## 总结”开头,回顾全文要点
7. 注意事项:用“## 注意事项”开头,列出常见的坑或最佳实践
# 格式要求:
- 严格使用Markdown语法
- 代码块必须指定语言类型
- 文章总长度控制在800-1200字之间
- 语言风格:口语化、亲切,像经验分享而不是教科书
当前文章主题:{topic}
"""
def _detect_field(self, topic: str) -> str:
"""简单判断文章领域"""
topic_lower = topic.lower()
if 'python' in topic_lower:
return 'Python开发'
elif 'javascript' in topic_lower or 'js' in topic_lower:
return '前端开发'
elif '数据库' in topic_lower or 'sql' in topic_lower:
return '数据库'
elif 'docker' in topic_lower or 'k8s' in topic_lower:
return '运维部署'
else:
return '技术'
def generate_outline(self, topic: str) -> str:
"""第一步:生成文章详细大纲"""
print(f"正在为《{topic}》生成大纲...")
response = self.client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": "你是一位专业的文章架构师,请根据主题生成详细的内容大纲,只返回大纲,不要生成具体内容。"
},
{
"role": "user",
"content": f"""请为《{topic}》生成文章大纲,要求:
1. 列出所有二级标题(## 标题)
2. 在每个二级标题下,用短句列出3-5个要点
3. 标明哪里需要插入代码示例(用【代码】标记)
4. 总章节数控制在4-6个
请用清晰的层级格式返回,例如:
## 引言
- 要点1
- 要点2
## 第一部分标题
- 要点1【代码】
- 要点2
"""
}
],
temperature=0.7,
max_tokens=800
)
outline = response.choices[0].message.content
print("✅ 大纲生成完成")
print("=" * 60)
print(outline)
print("=" * 60)
return outline
def parse_outline(self, outline: str) -> List[Dict]:
"""解析大纲文本,拆分成章节列表"""
sections = []
current_section = None
for line in outline.strip().split('\n'):
line = line.rstrip()
# 检测二级标题
if line.startswith('## ') and not line.startswith('###'):
if current_section:
sections.append(current_section)
title = line[3:].strip() # 去掉'## '
current_section = {
'title': title,
'content': line + '\n',
'needs_code': False
}
elif current_section:
current_section['content'] += line + '\n'
# 检查是否需要代码
if '【代码】' in line or '[代码]' in line:
current_section['needs_code'] = True
# 添加最后一个章节
if current_section:
sections.append(current_section)
# 如果没有解析出章节,则创建默认章节
if not sections:
sections = [
{'title': '引言', 'content': '## 引言\n\n', 'needs_code': False},
{'title': '核心概念', 'content': '## 核心概念\n\n', 'needs_code': True},
{'title': '实战应用', 'content': '## 实战应用\n\n', 'needs_code': True},
{'title': '总结', 'content': '## 总结\n\n', 'needs_code': False}
]
return sections
def generate_section_content(self, topic: str, outline: str,
previous_content: str, section: Dict) -> str:
"""生成单个章节的内容"""
# 构建提示词
code_hint = "(请提供完整的代码示例)" if section['needs_code'] else ""
messages = [
{
"role": "system",
"content": self.get_system_prompt(topic)
},
{
"role": "user",
"content": f"""这是文章《{topic}》的完整大纲:
{outline}
以下是已经写好的前文:
{previous_content}
现在请你接着写大纲中的这一部分:
{section['content']}
具体要求:
1. 只写【{section['title']}】这一部分的内容,不要写其他部分
2. 保持与上文风格和内容的连贯性
3. 如果大纲中标注了需要代码示例{code_hint},请务必提供完整、可运行的代码
4. 本节长度控制在200-400字
5. 直接开始本节内容,不要重复章节标题(标题我会自己加)
"""
}
]
# 流式生成
print(f"📝 正在生成:{section['title']}{code_hint}")
stream = self.client.chat.completions.create(
model=self.model,
messages=messages,
stream=True,
temperature=0.3,
max_tokens=800
)
section_content = ""
for chunk in stream:
if chunk.choices[0].delta.content is not None:
content = chunk.choices[0].delta.content
print(content, end="", flush=True)
section_content += content
print("\n" + "-" * 40)
return section_content.strip()
def generate_article(self, topic: str) -> str:
"""主函数:生成完整文章"""
# 1. 生成大纲
outline = self.generate_outline(topic)
# 2. 解析大纲
sections = self.parse_outline(outline)
# 3. 生成文章开头
full_article = f"# {topic}\n\n"
# 4. 逐节生成内容
for i, section in enumerate(sections):
section_content = self.generate_section_content(
topic, outline, full_article, section
)
# 拼接章节
full_article += f"## {section['title']}\n\n{section_content}\n\n"
# 5. 后处理
full_article = self._post_process(full_article)
return full_article
def _post_process(self, article: str) -> str:
"""后处理:修正格式问题"""
# 修复代码块语言标记
lines = article.split('\n')
processed_lines = []
in_code_block = False
for i, line in enumerate(lines):
# 检测代码块开始
if line.strip().startswith('```') and len(line.strip()) > 3:
# 已经有语言标记
processed_lines.append(line)
in_code_block = True
elif line.strip() == '```':
processed_lines.append(line)
in_code_block = False
elif line.strip().startswith('```'):
# 没有语言标记的代码块开始
# 检查接下来的几行是否像代码
is_likely_code = False
for j in range(i+1, min(i+5, len(lines))):
next_line = lines[j]
if '```' in next_line:
break
if any(pattern in next_line for pattern in
['def ', 'class ', 'import ', 'from ', 'print(', 'return ', 'if ', 'for ', 'while ']):
is_likely_code = True
break
if is_likely_code and 'python' not in line:
processed_lines.append('```python')
else:
processed_lines.append(line)
in_code_block = True
else:
processed_lines.append(line)
# 合并连续空行
processed_text = '\n'.join(processed_lines)
processed_text = re.sub(r'\n\s*\n\s*\n+', '\n\n', processed_text)
return processed_text
# 使用示例
if __name__ == "__main__":
# 替换为你的DeepSeek API Key
API_KEY = "your_deepseek_api_key_here"
generator = ArticleGenerator(API_KEY)
# 生成文章
topic = "Python装饰器的三种实战用法"
print(f"开始生成文章:《{topic}》")
print("=" * 60)
try:
article = generator.generate_article(topic)
print("\n" + "=" * 60)
print("✅ 文章生成完成!")
print("=" * 60)
# 保存到文件
filename = f"{topic.replace(' ', '_')}.md"
with open(filename, 'w', encoding='utf-8') as f:
f.write(article)
print(f"文章已保存到:{filename}")
print(f"文章长度:{len(article)} 字符")
except Exception as e:
print(f"生成失败:{e}")
踩坑记录
-
坑一:模型忘记格式要求
- 现象:明明在系统提示词里要求用Markdown,但生成的内容经常不用
#号做标题,或者代码块不用反引号。 - 解决:在用户提示词里再次强调格式,并且在后处理阶段自动修正。我发现把格式要求放在系统提示词和用户提示词里各说一遍,效果更好。
- 现象:明明在系统提示词里要求用Markdown,但生成的内容经常不用
-
坑二:文章结构“虎头蛇尾”
- 现象:前面的章节写得很详细,越到后面越敷衍,有时最后一章只有一句话。
- 解决:在大纲阶段就明确每个章节的要点数量,在生成每个章节时,提示词里明确要求“本节长度控制在200-400字”。另外,分节生成时保持温度较低(0.3),让输出更稳定。
-
坑三:token超限导致生成中断
- 现象:生成长文章时,有时会收到token超限的错误。
- 解决:分步生成是关键。我设置
max_tokens=800限制单次生成长度,并且把长文章拆成多个请求。这样即使某节生成失败,也只需要重试这一节,而不是整篇文章。
-
坑四:代码示例不完整
- 现象:模型说“下面是一个示例”,然后只给两三行代码,缺少必要的导入或上下文。
- 解决:在大纲里用特殊标记【代码】标明需要代码的地方,在生成该章节时特别强调“请提供完整、可运行的代码示例”。有时我还会在提示词里举例说明什么样的代码算“完整”。
-
意外发现:流式输出能改善内容质量
- 这不是坑,而是一个意外收获。我开始用流式输出只是为了用户体验(能看到生成过程),后来发现当模型“边想边写”时,生成的内容反而更连贯、更自然。可能因为流式生成迫使模型更注重局部的连贯性。
小结
通过这次实践,我最大的收获是:让AI生成可用内容的关键不是找到一个“完美提示词”,而是设计一个合理的生成流程。把“写文章”这个大任务拆解成“定大纲→分节写→后处理”几个可控的步骤,每个步骤都有明确的输入输出和质量标准,最终结果的稳定性和可用性才上得去。
这个方案还有很多可以优化的地方,比如加入更多领域知识库、实现更智能的大纲调整、或者加入人工审核环节。但至少现在,它已经能稳定地帮我从零散的灵感生成可用的文章草稿了,这让我在内容创作上效率提升了至少三倍。