从“一句话”到“一篇文”:我用DeepSeek API实现结构化文章生成的踩坑实录

2 阅读1分钟

背景

上个月,我在重构自己的个人知识库工具。这个工具的核心功能之一,就是能把我在碎片时间记录的零散想法(有时就是几个关键词或一两句话),自动整理扩展成结构清晰、内容完整的文章草稿。之前我试过一些现成的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)

跑出来的结果……怎么说呢,内容本身还行,但问题一大堆:

  1. 结构随机:有时有标题,有时没有;分段完全看模型心情。
  2. 长度失控:我想要800-1000字的草稿,它可能给我300字,也可能给我2000字。
  3. 格式混乱:说好要Markdown,但标题的#号时有时无,代码块经常不用反引号包裹。
  4. 成本问题:一次生成太长的文章,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后,能在保持结构合理的前提下,获得更有新意的角度。

第三步:基于大纲的流式内容生成

有了认可的大纲后,我就可以分段生成内容了。这里我用了两个技巧:

  1. 流式输出:让用户能看到生成进度,体验更好
  2. 上下文管理:每生成一节,都把前面的大纲和已生成内容带上,保持连贯性
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}")

踩坑记录

  1. 坑一:模型忘记格式要求

    • 现象:明明在系统提示词里要求用Markdown,但生成的内容经常不用#号做标题,或者代码块不用反引号。
    • 解决:在用户提示词里再次强调格式,并且在后处理阶段自动修正。我发现把格式要求放在系统提示词和用户提示词里各说一遍,效果更好。
  2. 坑二:文章结构“虎头蛇尾”

    • 现象:前面的章节写得很详细,越到后面越敷衍,有时最后一章只有一句话。
    • 解决:在大纲阶段就明确每个章节的要点数量,在生成每个章节时,提示词里明确要求“本节长度控制在200-400字”。另外,分节生成时保持温度较低(0.3),让输出更稳定。
  3. 坑三:token超限导致生成中断

    • 现象:生成长文章时,有时会收到token超限的错误。
    • 解决:分步生成是关键。我设置max_tokens=800限制单次生成长度,并且把长文章拆成多个请求。这样即使某节生成失败,也只需要重试这一节,而不是整篇文章。
  4. 坑四:代码示例不完整

    • 现象:模型说“下面是一个示例”,然后只给两三行代码,缺少必要的导入或上下文。
    • 解决:在大纲里用特殊标记【代码】标明需要代码的地方,在生成该章节时特别强调“请提供完整、可运行的代码示例”。有时我还会在提示词里举例说明什么样的代码算“完整”。
  5. 意外发现:流式输出能改善内容质量

    • 这不是坑,而是一个意外收获。我开始用流式输出只是为了用户体验(能看到生成过程),后来发现当模型“边想边写”时,生成的内容反而更连贯、更自然。可能因为流式生成迫使模型更注重局部的连贯性。

小结

通过这次实践,我最大的收获是:让AI生成可用内容的关键不是找到一个“完美提示词”,而是设计一个合理的生成流程。把“写文章”这个大任务拆解成“定大纲→分节写→后处理”几个可控的步骤,每个步骤都有明确的输入输出和质量标准,最终结果的稳定性和可用性才上得去。

这个方案还有很多可以优化的地方,比如加入更多领域知识库、实现更智能的大纲调整、或者加入人工审核环节。但至少现在,它已经能稳定地帮我从零散的灵感生成可用的文章草稿了,这让我在内容创作上效率提升了至少三倍。