小说自动生成系统(二)

目录

一.导出模块

二.pipeline流程

三.程序入口

一.导出模块

本节编写将大模型写好的小说导出为markdown文件或者word文件的功能模块。

uv add python-docx

在核心代码文件夹src下创建exporter.py文件,仍让用cline编写,提示词如下:

@/src/exporter.py 创建导出模块:把生成好的整本小说写成文件。
输入是一本 " 已经填好各章正文 " 的大纲对象。需要两种导出:

  • Markdown :书名做大标题,下面是梗概和各章(每章一个 标题 + 正文),拼成一个 .md 文件。
  • Word ( .docx ):书名、梗概,每章一个标题并按章分页。
    Word 生成库要等到真正用它的函数里才导入 ------ 这样没装它的 时候,Markdown 导出照样能跑。
    输出目录不存在要自动建。

这时候可以新建一个对话,因为这个模块和之前的模块直接联系性不大,所以新建一个模块还省token。

python 复制代码
from pathlib import Path

from src.schema import Outline


def export_markdown(outline: Outline, output_dir: str | Path = ".") -> Path:
    """将整本小说导出为 Markdown 文件。

    Args:
        outline: 已填好各章正文的大纲对象。
        output_dir: 输出目录,不存在时自动创建。

    Returns:
        生成的 .md 文件路径。
    """
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    lines: list[str] = []

    # 书名(大标题)
    lines.append(f"# {outline.title}")
    lines.append("")

    # 梗概
    if outline.summary:
        lines.append(outline.summary)
        lines.append("")

    # 各章
    for chapter in outline.chapters:
        lines.append(f"## 第{chapter.index}章 {chapter.title}")
        lines.append("")
        if chapter.content:
            lines.append(chapter.content)
        lines.append("")

    text = "\n".join(lines)
    file_path = output_path / f"{outline.title}.md"
    file_path.write_text(text, encoding="utf-8")
    return file_path


def export_docx(outline: Outline, output_dir: str | Path = ".") -> Path:
    """将整本小说导出为 Word (.docx) 文件。

    注意:此函数依赖 python-docx 库,仅在调用时导入。

    Args:
        outline: 已填好各章正文的大纲对象。
        output_dir: 输出目录,不存在时自动创建。

    Returns:
        生成的 .docx 文件路径。
    """
    from docx import Document
    from docx.enum.text import WD_ALIGN_PARAGRAPH

    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    doc = Document()

    # --- 封面:书名 ---
    title_para = doc.add_heading(outline.title, level=0)
    title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # --- 梗概 ---
    if outline.summary:
        doc.add_paragraph(outline.summary)

    # --- 各章 ---
    for i, chapter in enumerate(outline.chapters):
        # 每章另起一页(跳过第一章,因为已在封面页之后)
        if i > 0:
            doc.add_page_break()

        doc.add_heading(f"第{chapter.index}章 {chapter.title}", level=1)
        doc.add_paragraph(chapter.content)

    file_path = output_path / f"{outline.title}.docx"
    doc.save(str(file_path))
    return file_path

二.pipeline流程

本节负责将之前写的几个功能模块串联起来。
所有零件 (schema/config/prompts/llm_client/exporter )就绪,组装主线。
和之前同样的流程,创建pipeline.py文件,输入提示词。

@/src/pipeline.py 创建编排模块,把整个流程串起来:先规划出大纲,再逐章扩写成正文,最后导出成书。
这是对外的主入口,接收主题和章节数。
三件事:

  • 生成大纲:调 LLM 拿结构化大纲,校验它合不合法;不 合法就降温再试一次。
  • 扩写单章:调 LLM ,把 " 全书摘要、本章大纲、上一章的 结尾" 一起带进去, 好让这章和前后连贯。
  • 主流程:生成大纲 → 循环每一章扩写(每写完一章,记下它的结尾,留给下一章衔接)
  • 导出。某一章写失败不要让整本书崩掉,记下来最后报 告。过程用 print 提示进度。
    规划和大纲扩写都写成普通函数就行,不要搞成类。
python 复制代码
"""编排模块------对外主入口,把整个流程串起来。

流程:
    生成大纲 → 逐章扩写 → 导出成书。
某一章写失败不会让整本书崩掉,记下来最后报告。
"""

from __future__ import annotations

import json
from typing import Any

from src import config
from src.exporter import export_markdown
from src.llm_client import (
    LLMConnectionError,
    LLMJSONError,
    chat,
    chat_json,
)
from src.prompts import CHAPTER_SYSTEM, OUTLINE_SYSTEM, chapter_user, outline_user
from src.schema import Chapter, Outline


# ============================================================
# 1. 生成大纲
# ============================================================


def generate_outline(theme: str, chapter_count: int) -> Outline:
    """生成小说大纲,并在失败时降温重试一次。

    步骤:
        1. 用 `chat_json` 调 LLM 获取结构化大纲(含 JSON 自修正)。
        2. 用 Pydantic 校验字段合法性(章节数是否匹配、字段是否完整等)。
        3. 校验失败 → 降温(temperature * 0.8),改用 `chat` + 手动解析重试一次。
        4. 两次都失败则向外抛出异常。

    参数:
        theme: 小说主题
        chapter_count: 期望的章节数量

    返回:
        校验通过的 Outline 对象。

    异常:
        LLMConnectionError: 网络/服务端错误且重试耗尽。
        LLMJSONError:       JSON 解析失败(自修正后仍失败)。
        ValueError:         降温重试后仍校验失败(字段残缺/章节数不匹配等)。
    """
    # ── 第一次尝试:用 chat_json(自带一次 JSON 自修正) ────────────
    try:
        raw_dict: dict[str, Any] = chat_json(
            prompt=outline_user(theme, chapter_count),
            system=OUTLINE_SYSTEM,
            temperature=config.outline_temperature,
        )
        outline = Outline.model_validate(raw_dict)
        _validate_chapter_count(outline, chapter_count)
        return outline

    except (LLMConnectionError, LLMJSONError):
        # 调用/JSON 层面的失败不值得降温重试(已内部重试过),继续往外抛
        raise

    except Exception as first_err:
        # Pydantic 校验失败 或 章节数不匹配 → 降温重试一次
        pass

    # ── 第二次尝试:降温,手动解析 ────────────────────────────────────
    lower_temp = config.outline_temperature * 0.8
    raw_text = chat(
        prompt=outline_user(theme, chapter_count),
        system=OUTLINE_SYSTEM,
        temperature=lower_temp,
    )
    try:
        cleaned = _extract_json(raw_text)
        raw_dict = json.loads(cleaned)
        outline = Outline.model_validate(raw_dict)
        _validate_chapter_count(outline, chapter_count)
        return outline

    except Exception as second_err:
        raise ValueError(
            f"大纲生成失败(降温重试后仍校验不通过)\n"
            f"第一次错误:{first_err}\n"
            f"第二次错误:{second_err}"
        ) from second_err


def _validate_chapter_count(outline: Outline, expected: int) -> None:
    """校验章节数量是否匹配。"""
    actual = len(outline.chapters)
    if actual != expected:
        raise ValueError(
            f"章节数量不匹配:期望 {expected} 章,实际返回 {actual} 章"
        )


def _extract_json(raw: str) -> str:
    """从 LLM 返回文本中提取 JSON 部分(适配 chat 返回的纯文本)。

    处理 ```json 代码块标记、前后缀文字等。
    """
    import re

    text = raw.strip()
    # 尝试找 ```json ... ```
    match = re.search(r"```(?:json)?\s*([\s\S]*?)```", text)
    if match:
        return match.group(1).strip()
    # 没有代码块标记,直接找第一个 { 和最后一个 }
    start = text.find("{")
    end = text.rfind("}")
    if start != -1 and end != -1 and end > start:
        return text[start : end + 1]
    return text


# ============================================================
# 2. 扩写单章
# ============================================================


def expand_chapter(
    outline: Outline,
    chapter_index: int,
    prev_ending: str = "",
) -> str:
    """扩写指定章节的正文。

    参数:
        outline:       全书大纲(含各章 outline 信息)
        chapter_index: 章节序号(1-based,即第几章)
        prev_ending:   上一章结尾文本(用于衔接,首章传空字符串)

    返回:
        本章的完整正文(Markdown 格式文本)。

    异常:
        LLMConnectionError: 网络/服务端错误且重试耗尽。
        ValueError:          chapters 中找不到对应索引的章节。
    """
    # 找到对应章节(outline.chapters 是 0-based list)
    try:
        chapter: Chapter = outline.chapters[chapter_index - 1]
    except IndexError:
        raise ValueError(
            f"章节索引 {chapter_index} 超出范围(共 {len(outline.chapters)} 章)"
        )

    prompt = chapter_user(
        book_title=outline.title,
        book_summary=outline.summary,
        chapter_title=chapter.title,
        chapter_outline=chapter.outline,
        chapter_clues=chapter.clues,
        prev_ending=prev_ending,
    )

    content = chat(
        prompt=prompt,
        system=CHAPTER_SYSTEM,
        temperature=config.chapter_temperature,
    )
    return content


# ============================================================
# 3. 主流程
# ============================================================


def run_pipeline(
    theme: str,
    chapter_count: int,
    output_dir: str = "output",
) -> None:
    """主编排入口:生成大纲 → 逐章扩写 → 导出成书。

    参数:
        theme:         小说主题
        chapter_count: 章节数量
        output_dir:    导出目录(默认为 "output")

    过程提示全部用 print 输出。
    """
    print(f"🎯 开始生成大纲......(主题:{theme},共 {chapter_count} 章)")

    # ── 1. 生成大纲 ───────────────────────────────────────────────
    outline = generate_outline(theme, chapter_count)
    print(f"✅ 大纲生成完成:{outline.title}(共 {len(outline.chapters)} 章)")

    # ── 2. 逐章扩写 ───────────────────────────────────────────────
    prev_ending: str = ""
    failed_chapters: list[int] = []

    for chapter in outline.chapters:
        print(f"\n✍️  正在扩写第 {chapter.index} 章:{chapter.title}......")
        try:
            content = expand_chapter(outline, chapter.index, prev_ending)
            chapter.content = content

            # 记录本章结尾(取末尾 200 字,用于下一章衔接)
            tail_len = 200
            prev_ending = content[-tail_len:] if len(content) >= tail_len else content

            print(
                f"✅ 第 {chapter.index} 章扩写完成({len(content)} 字符)"
            )
        except Exception as e:
            print(f"❌ 第 {chapter.index} 章扩写失败:{e}")
            failed_chapters.append(chapter.index)
            # 失败时不更新 prev_ending,下一章沿用上一章的结尾

    # ── 3. 汇总报告 ───────────────────────────────────────────────
    print()
    if failed_chapters:
        failed_str = ", ".join(str(i) for i in failed_chapters)
        print(f"⚠️  有 {len(failed_chapters)} 章扩写失败:第 {failed_str} 章")
    else:
        print(f"🎉 所有 {chapter_count} 章扩写完成!")

    # ── 4. 导出 ───────────────────────────────────────────────────
    print("📄 正在导出 Markdown......")
    export_path = export_markdown(outline, output_dir)
    print(f"✅ 导出完成:{export_path}")

行文至此,虽然整个小项目还没做完,但已经感受到时代猛然变迁了,这AI提升效率简直是从蒸汽时代跨越电气时代直接来到了信息时代,NB。

三.程序入口

在main文件里编写,先安装click包,uv add click

提示词如下:

创建命令行入口(放项目根目录)。用户通过它启动生成。
要接收:小说主题(必填)、要写几章、导出什么格式(Markdown / Word / 都要)、输出放哪。
把这些传给编排模块的主入口。出了错要给用户一句清楚的人话报错,而不是甩一栈 traceback 。
用 click 做这个 CLI 。

AI生成代码如下:

python 复制代码
"""小说生成器 CLI ------ 用户通过它启动整个生成流程。"""

import click

from src.pipeline import run_pipeline


@click.command()
@click.argument("theme", type=str)
@click.option(
    "--chapters",
    "-c",
    default=5,
    show_default=True,
    help="要写多少章",
)
@click.option(
    "--format",
    "-f",
    type=click.Choice(["markdown", "word", "both"], case_sensitive=False),
    default="markdown",
    show_default=True,
    help="导出格式",
)
@click.option(
    "--output",
    "-o",
    default="output",
    show_default=True,
    help="输出目录",
)
def main(theme: str, chapters: int, format: str, output: str) -> None:
    """用 AI 自动生成一本小说。

    THEME 是小说主题,必填。例如:\b

        novel "修仙:我在凡间开了家烧烤摊"

    详见:https://github.com/your-repo/novel
    """
    # 将用户选的格式转成内部使用的列表
    format_map = {
        "markdown": ["markdown"],
        "word": ["docx"],
        "both": ["markdown", "docx"],
    }
    formats = format_map[format]

    try:
        run_pipeline(
            theme=theme,
            chapter_count=chapters,
            output_dir=output,
            formats=formats,
        )
    except Exception as exc:
        raise click.ClickException(
            f"生成失败:{exc}"
        )


if __name__ == "__main__":
    main()

然后按照AI给出的执行命令进行程序测试即可。

例如:

python main.py -c 4 -f word "重生之董事长给我打工"

最后不得不感叹,现在AI发展如此之快,不知道再发展几年,互联网行业会不会开始一定程度上进行员工优化呢?

不是我不明白,是世界变化快~

相关推荐
小林ixn1 小时前
会议纪要自动化:用 Claude Skill 把重复工作“卷”成一行命令
agent·自动化运维
通问AI2 小时前
Apple Intelligence 国行备案深度技术解析:阿里千问如何被集成到苹果端侧AI架构
人工智能·架构
视***间2 小时前
算力赋能零售与创意新生态:视程空间Pandora,解锁线下场景智能化无限可能
人工智能·边缘计算·智慧零售·ai算力·视程空间·创意开发
冬奇Lab2 小时前
MCP 系列(08):企业治理——Registry、路由与可观测性
人工智能·llm·mcp
冬奇Lab2 小时前
每日一个开源项目(第161篇):Open Interpreter - 开源 AI 编程 Agent,用 Rust 重写,支持 Kimi、Qwen、DeepSeek
人工智能·开源·资讯
renhongxia12 小时前
世界模型,是“空中楼阁”还是AGI的“最后一块拼图”?
运维·服务器·数据库·人工智能·算法·agi
天上路人2 小时前
AU-48双麦多功能语音处理模组:100dB AEC + 波束定向拾音,会议与监控全场景覆盖
人工智能·ai降噪·语音处理·ai语音·麦克风·回音消除
甲维斯2 小时前
Codex定制皮肤,Windows一键切换已经搞定!
人工智能
逐米时代2 小时前
AI智能体与制造执行系统(MES)的数据打通
人工智能