文稿起草系统,离线学习写作人的“思维 + 风格“,按学到的特征起草新文稿,再用反馈闭环持续提升

writer-offline-perspective

目录

  1. 项目特性
  2. 技术栈
  3. 项目结构
  4. 快速开始
  5. 核心功能
  6. 踩坑记录
  7. 扩展方向
  8. 部署

项目特性

  • 双 profile 结构:把"思维立场"(认知框架/价值排序/逻辑结构/情感基调/边界意识)和"表达风格"(句长/词频/四字格/标点分布)拆成两个文件分别管理,避免混在一起难维护
  • 关键词聚类(不用 TF-IDF/KMeans):用可解释、可调的关键词规则把范文按主题域分组,每个主题域自动选 top-5 代表作生成 best-practices.md
  • 起草强制三版本:每次起草输出 A 稳健 / B 进取 / C 平衡 三版,用户挑选后再走反馈闭环
  • --materials 必填:起草命令必须传素材文件,防止"凭空起草"产生幻觉数据
  • 大纲预演:起草前先输出推荐大纲让用户确认,骨架错了就别浪费时间写正文
  • 多作者隔离:每位作者一个独立 profile 目录,互不影响

技术栈

技术 用途
Python 3.8+ 主语言
python-docx 读写 docx 文件
jieba 中文分词 + 关键词提取
pyyaml YAML frontmatter 解析
argparse CLI 子命令

项目结构

复制代码
writer-offline-perspective/
├── writer.py                # CLI 入口(learn / list / draft / draft-package)
├── requirements.txt
├── SKILL.md                 # 工作流 skill(供 Claude 调用)
├── CLAUDE.md                # 项目规则
├── core/                    # 核心模块
│   ├── article_splitter.py      # docx → 单篇 .md
│   ├── structure_extractor.py   # 提取每篇标题层级
│   ├── expression_profiler.py   # 句长/词频/四字格统计
│   ├── topic_cluster.py         # 关键词聚类
│   ├── profile_manager.py       # profile 目录 CRUD + _index.json
│   ├── draft_preparer.py        # 起草上下文打包
│   └── draft_packager.py        # 3 版本 → docx + comparison
├── templates/               # 文稿模板(speech/report/summary)
├── docs/superpowers/specs/  # 设计文档
└── .claude/                 # 项目自进化结构(规则/记忆/skill)

运行时产生:

复制代码
profiles/<author>/
├── style-profile.md         # 数据驱动生成
├── stance-profile.md        # 5 段式思维立场
├── samples/original/        # N 篇 .md 范文
├── topics/<主题域>/         # 主题聚类 + best-practices.md
└── _index.json

drafts/<author>/draft_<ts>_<topic>/
├── context.md               # 起草上下文(给 Claude)
├── materials.md             # 素材原文
├── draft_A/B/C.md           # 三版本起草
├── draft_A/B/C.docx         # 打包后 docx
└── comparison.md            # 三版本对比摘要

模块数据流(学习路径)

#mermaid-svg-VkX6sScYK5S5Gl0v{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VkX6sScYK5S5Gl0v .error-icon{fill:#552222;}#mermaid-svg-VkX6sScYK5S5Gl0v .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VkX6sScYK5S5Gl0v .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VkX6sScYK5S5Gl0v .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VkX6sScYK5S5Gl0v .marker.cross{stroke:#333333;}#mermaid-svg-VkX6sScYK5S5Gl0v svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VkX6sScYK5S5Gl0v p{margin:0;}#mermaid-svg-VkX6sScYK5S5Gl0v .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v .cluster-label text{fill:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v .cluster-label span{color:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v .cluster-label span p{background-color:transparent;}#mermaid-svg-VkX6sScYK5S5Gl0v .label text,#mermaid-svg-VkX6sScYK5S5Gl0v span{fill:#333;color:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v .node rect,#mermaid-svg-VkX6sScYK5S5Gl0v .node circle,#mermaid-svg-VkX6sScYK5S5Gl0v .node ellipse,#mermaid-svg-VkX6sScYK5S5Gl0v .node polygon,#mermaid-svg-VkX6sScYK5S5Gl0v .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VkX6sScYK5S5Gl0v .rough-node .label text,#mermaid-svg-VkX6sScYK5S5Gl0v .node .label text,#mermaid-svg-VkX6sScYK5S5Gl0v .image-shape .label,#mermaid-svg-VkX6sScYK5S5Gl0v .icon-shape .label{text-anchor:middle;}#mermaid-svg-VkX6sScYK5S5Gl0v .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-VkX6sScYK5S5Gl0v .rough-node .label,#mermaid-svg-VkX6sScYK5S5Gl0v .node .label,#mermaid-svg-VkX6sScYK5S5Gl0v .image-shape .label,#mermaid-svg-VkX6sScYK5S5Gl0v .icon-shape .label{text-align:center;}#mermaid-svg-VkX6sScYK5S5Gl0v .node.clickable{cursor:pointer;}#mermaid-svg-VkX6sScYK5S5Gl0v .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-VkX6sScYK5S5Gl0v .arrowheadPath{fill:#333333;}#mermaid-svg-VkX6sScYK5S5Gl0v .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-VkX6sScYK5S5Gl0v .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-VkX6sScYK5S5Gl0v .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VkX6sScYK5S5Gl0v .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VkX6sScYK5S5Gl0v .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VkX6sScYK5S5Gl0v .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-VkX6sScYK5S5Gl0v .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-VkX6sScYK5S5Gl0v .cluster text{fill:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v .cluster span{color:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-VkX6sScYK5S5Gl0v .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VkX6sScYK5S5Gl0v rect.text{fill:none;stroke-width:0;}#mermaid-svg-VkX6sScYK5S5Gl0v .icon-shape,#mermaid-svg-VkX6sScYK5S5Gl0v .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VkX6sScYK5S5Gl0v .icon-shape p,#mermaid-svg-VkX6sScYK5S5Gl0v .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-VkX6sScYK5S5Gl0v .icon-shape .label rect,#mermaid-svg-VkX6sScYK5S5Gl0v .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VkX6sScYK5S5Gl0v .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-VkX6sScYK5S5Gl0v .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-VkX6sScYK5S5Gl0v :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 作者汇编 docx
ArticleSplitter

居中标题 + 元信息过滤
N 篇 .md

含 frontmatter
StructureExtractor

标题层级
ExpressionProfiler

句长/词频/四字格
TopicCluster

关键词聚类
structure-summary.md
style-profile.md
expression-stats.json
topics/<主题域>/

best-practices.md
profiles/<author>/
stance-profile.md

由 Claude 用 SKILL 补全

起草流程(用户视角)

复制代码
┌─────────┐                ┌──────────────────┐
│  用户   │                │   writer.py      │
└────┬────┘                └────────┬─────────┘
     │                              │
     │  --topic + --materials       │
     ├──────────────────────────────►
     │                              │
     │           prepare 阶段       │
     │  ┌───────────────────────────┤
     │  │ • 读 profile (style+stance)│
     │  │ • 读 materials             │
     │  │ • 匹配最相关主题域 top-2   │
     │  │ • 写 context.md            │
     │  └───────────────────────────►
     │                              │
     │       Claude 读 context.md   │
     │  ┌────────────────────────────┤
     │  │ 起草 draft_A.md (稳健)    │
     │  │ 起草 draft_B.md (进取)    │
     │  │ 起草 draft_C.md (平衡)    │
     │  └────────────────────────────►
     │                              │
     │           package 阶段       │
     │  ┌────────────────────────────┤
     │  │ • 统计每版本指标           │
     │  │ • 生成 3 个 docx           │
     │  │ • 写 comparison.md         │
     │  └────────────────────────────►
     │                              │
     │  挑选最优版本                │
     ◄──────────────────────────────┤
     │                              │
     ▼                              ▼

快速开始

bash 复制代码
# 安装依赖
pip install -r requirements.txt

# 1. 学习一位作者(从作者汇编 docx 提取风格)
python writer.py learn 李世泽 --source /path/to/李世泽文章汇编.docx

# 2. 列出已学习的作者
python writer.py list

# 3. 起草新文稿(必须传素材)
python writer.py draft 李世泽 \
  --topic "做大做强平陆运河文旅产业  打造广西向海经济新名片" \
  --materials /path/to/materials.md
# 输出:drafts/李世泽/draft_<ts>_<topic>/context.md
# 然后 Claude 按 context.md 写 draft_A/B/C.md 到该目录

# 4. 打包成 docx + 对比摘要
python writer.py draft-package drafts/李世泽/draft_<ts>_<topic>

list 子命令查看已学到的作者:

draft-package 打包后输出三版本对比:

核心功能

1. ArticleSplitter(docx 拆分)

从作者汇编 docx 按"居中标题 + 日期行 + 正文段"的模式拆分单篇 .md。关键是头部元信息("XX文章汇编"、"(共95篇...)"、"汇编生成: ...")也是居中的,必须用 META_PATTERNS 内容匹配过滤:

python 复制代码
META_PATTERNS = [
    re.compile(r'^.{2,8}文章汇编$'),
    re.compile(r'^(共\d+篇.*)$'),
    re.compile(r'^汇编生成[::]'),
    re.compile(r'^目\s*录$'),
]

2. TopicCluster(关键词聚类)

按"先命中者优先"的顺序把范文归入主题域。每个主题的关键词表设计原则:

  • 避免宽泛词("广西/发展/开放"),用专属词("一带一路/RCEP/平陆运河")
  • 新增细分主题域放 topic_order 末位作为兜底,不会抢错已正确归类的篇目
  • 匹配范围 = 标题 + 首段前 200 字,避免正文里引述其他主题时误命中

3. ExpressionProfiler(表达统计)

用 jieba 分词,统计:

  • 句长分布(短 < 15 / 中 15-30 / 长 30-50 / 超长 50+)
  • 段长分布(同上四档)
  • Top 30 高频词(去停用词、单字)
  • Top 20 四字格 + 密度
  • 引号/问号/感叹号/逗号/分号密度

输出双文件:expression-stats.json(原始数据)+ style-profile.md(人类可读表格)。

4. DraftPreparer(起草上下文打包)

接收 --topic--materials,输出 context.md 给 Claude 看,含:

  • 风格 DNA 摘要(平均句长目标、四字格密度目标、Top 词、Top 四字格)
  • stance-profile 摘要(5 段思维立场约束)
  • 主题最相关的 best-practices 摘录(按关键词匹配 top 2 主题域)
  • 三版本起草指引(A 稳健 / B 进取 / C 平衡)
  • 素材原文(强制约束:"所有事实/数据/引文必须出自此处")

5. DraftPackager(三版本打包)

读 Claude 写好的 draft_A/B/C.md,对每版本:

  • 用 ExpressionProfiler.analyze_text 计算句长/四字格/引文密度/估算阅读时长
  • 生成对应 .docx(# 标题 → Heading,其它 → 普通段落)
  • 输出一页 comparison.md(指标对比 + 挑选建议)

踩坑记录

1. docx 头部元信息恰好是居中段,过滤算法没拦截

现象:拆分李世泽 95 篇文章时多出 3 篇假文章------"李世泽文章汇编"、"(共95篇 | 当代广西网·当代论坛)"、"汇编生成: 2026年06月08日"。

原因 :原始 _segment 函数只用"非居中"判定跳过元信息,但这 3 段恰好是居中的、字数也在标题范围内("李世泽文章汇编" 6 个字),完全混进去。

解决 :加 META_PATTERNS 内容匹配,在"居中段 + 长度合规"基础上叠加正则模式检查。

2. _extract_meta 收到 strip 后的正文,title 永远是空

现象 :聚类时打印调试发现 title: 全是空字符串,导致关键词匹配范围只剩首段,36 篇误归"其他"。

原因_extract_meta(md_file, text) 的契约是"从 frontmatter 提取 title/date",但调用方传的是 _strip_frontmatter(text) 后的正文,里面已经没有 --- 了,所有判断都失效,return 空。

解决 :让 _extract_meta 自包含------自己 split frontmatter 不依赖外部,把"已经处理过的状态"这种隐式契约去掉:

python 复制代码
def _extract_meta(self, md_file, text_with_frontmatter):
    """text_with_frontmatter 必须是原始全文。函数内自己 split。"""
    if text_with_frontmatter.startswith("---"):
        parts = text_with_frontmatter.split("---", 2)
        fm, body = parts[1], parts[2]
        ...

3. 关键词表只为一个作者优化,跨作者迁移就崩

现象:李世泽跑出 "其他" 0% 很漂亮,换林柏成 26 篇就变成 27%。

原因 :初版 DEFAULT_KEYWORDS 是看着李世泽数据反推的关键词组合("一带一路"/"RCEP"/"平陆运河"),林柏成主题域是"家风家教/文化自信/全过程人民民主",完全不命中。

解决 :短期接受作者特征差异;长期 TopicCluster 需要支持 per-author keywords overlay 而非全局单表。

4. 派了 8 个 agent 并行填 best-practices.md,6 个清掉标题里的占位 2 个没清

现象 :让 8 个 general-purpose agent 各填一个主题的 best-practices.md,最后 grep 发现还有 2 个文件残留 ## 主题特征(待 Claude 补全) 标题。

原因:prompt 没明确"H2 标题里的'(待 Claude 补全)'后缀也要清掉",6 个 agent 自己理解到了,2 个没有。多 agent 输出有细微差异是常态。

解决:派 N 个 agent 后必须主线程 grep 验证完成状态,不能假设 agent 都做了同样的事。

5. nuwa-offline-perspective 包名跟自己的 core 冲突

现象 :想复用兄弟项目 nuwa 的 outline_extractor.py,常规 from nuwa.core import outline_extractor 走不通------nuwa 不是 Python 包(没有 __init__.py),且本项目自己有 core 包名,sys.path 加进去会冲突。

解决 :用 importlib.util.spec_from_file_location 直接按文件路径加载,绕开包名机制:

python 复制代码
spec = importlib.util.spec_from_file_location(
    "nuwa_outline_extractor",
    "/path/to/nuwa-offline-perspective/core/outline_extractor.py",
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)

6. learned-rules.mdcore-invariants.md 重复了 6 条规则,几个月没人发现

现象 :/evolve 时清点学到的规则,发现前 6 条(list[-1]nohup 重定向、service ready 验证、WPS TOC、LLM both constraints、hybrid mode)跟 ~/.claude/rules/core-invariants.md 完全重复。

原因:当时把规则加进 learned-rules.md 时没检查 core-invariants 是否已有;每次 /evolve 也没做重复检查。两套规则平行存在,浪费 30+ 行不说,还有"一份更新另一份没更新"的腐烂风险。

解决 :给 /evolve 命令加 "Step 0: Duplicate Check" 强制重复检查,并在 Current State 节自动报告 learned-rules.md 行数 + rules/ 目录现状。

扩展方向

  • Phase 5 反馈闭环writer.py feedback <draft_dir> --selected A --polished polished.md 计算 polished vs draft 的段落+句子双粒度 diff,写入 iterations/v<n>-feedback.md,反向更新 style/stance
  • 8 维度主观打分(来自 SKILL.md Phase 5.3):观点鲜明度/结构清晰度/语言精准度/情感共鸣/逻辑严密/广西特色/创新性/可读性,每维 1-10 分 + 客观指标对比
  • per-author keywords overlay :让 TopicCluster 支持每位作者自定义关键词表叠加在 DEFAULT_KEYWORDS 之上,解决跨作者"其他"占比上升问题
  • draft 风格违规扫描:在 draft-package 阶段加"风格违规检查"------如果起草版本含感叹号/语气助词且 stance-profile 明确禁止,输出警告(不阻塞)
  • 跨作者风格借鉴:从作者 A 学到的"开篇模板"应用到作者 B 的起草,做风格 A/B 测试

部署

无服务端、纯 CLI,无需部署。本地有 Python 3.8+ 即可:

bash 复制代码
git clone <repo>
cd writer-offline-perspective
pip install -r requirements.txt
python writer.py --help

如果要发布给团队用,可以把核心模块打包成 PyPI wheel:

bash 复制代码
# 加 setup.py / pyproject.toml 后
python -m build
twine upload dist/*
相关推荐
踏着七彩祥云的小丑1 小时前
Go学习第5天:变量作用域 + 数组 + 指针
开发语言·学习·golang·go
至此流年莫相忘2 小时前
Windows 环境下 RocketMQ 安装与 NSSM 后台服务化部署指南
windows·rocketmq
MartinYeung52 小时前
[论文学习]针对 LLM 的间接提示注入攻击用于高效隐私洩露之深度分析
人工智能·学习
AI棒棒牛2 小时前
YOLO26 全网独家改进创新: MIT 2025 振荡状态空间模型:引入可学习的阻尼机制,独家创新!
人工智能·学习·目标检测·计算机视觉·yolo26
留白_3 小时前
pandas进阶学习
学习·pandas
AI行业学习3 小时前
CC‑Switch v3.16.1 免费下载(Windows+macOS+Linux)、使用方法【2026.6.11】
linux·开发语言·windows·python·macos·前端框架·html
_Evan_Yao3 小时前
递归函数入门:以阶乘和斐波那契数列为例
python·学习·算法
啦啦啦~~~3303 小时前
【装机工具】电脑重装系统!office安装管理软件!一键自动化下载、安装、部署Office的办公增强工具
运维·c语言·windows·自动化·电脑
一个人旅程~3 小时前
如何进行win11右键菜单优化(poweshell命令行与bat自动脚本方式)
windows·经验分享·macos·电脑