很长时间以来,AI 写作辅助工具和内容生成模型已经深入开发者的日常工作。我们既享受大模型带来的效率提升,也面对一个现实问题:AI 生成的文字往往带着明显的“机器味”,包括句式重复、连接词滥用、逻辑跳跃以及高频套话。更麻烦的是,很多团队想在校稿阶段统一风格,却依赖人工逐条核对,效率很低。
本文围绕一个很有意思的项目理念展开:Writing-eval,即针对 AI 生成稿件做本地、确定性(deterministic)风格检查。不是说教模型怎么写得更好,而是用一套可复现、可测试、不依赖云端大模型的规则和指标,在代码工程层面自动发现风格问题。
如果你是内容平台的工程师、AI 应用开发者、技术写作者,或者正在做 AI 稿件审核工具,这篇文章会比较实用。文中的思路可以脱离具体仓库落地到自己的项目里,整套方案适合本地执行,也适合集成进 CI 流水线。
1. 背景与核心概念
1.1 什么是 Writing-eval 和确定性风格检查
先拆开来看这个项目名。
Writing-eval 可以理解为一套“写作评估器”。它接收一段文本(尤其是 AI 生成的初稿),输出一系列质量指标、样式警告和可读性评分。
最关键的限定词是local deterministic,也就是“本地确定性”。这在当前 AI 写作工具泛滥的背景下非常重要:
- local(本地):所有检查逻辑在本地机器或自建服务器上执行,不需要把文本发送给第三方大模型 API。这对于处理内部文档、未发布产品文案、包含业务敏感信息的稿件尤其重要。
- deterministic(确定性):同样的输入文本,永远产生同样的输出结果。它不像大模型生成那样带有随机性,也不依赖模型版本波动。检查规则是固定的,指标算法是明确的,阈值是可配置的。
换句话说,Writing-eval 这一类工具更像传统代码风格检查器(例如 ESLint、Pylint),而不是 AI 评审系统。它不“感受”文章好坏,而是用明确规则判断你是否使用了太多被动语态、是否反复出现同一个开头词、是否每个句子都超过 40 个单词。
1.2 它解决什么问题
在日常工作里,我们经常遇到三类场景:
产品团队批量生成营销文案。AI 初稿往往有固定套路,例如每段都以 “首先”、“其次”、“总的来说” 开头。人工修改没有统一标准,团队不同成员对“什么是好风格”理解不一样。
技术博客和文档平台。AI 生成的技术文章会有很重的“AI 腔”,例如频繁使用 “值得注意的是”、“综上所述” 等连接词,句子长度分布失衡,并列结构过度重复。平台需要在上线前自动拦截这类内容。
AI Agent 应用的后处理环节。Agent 生成的内容需要经过风格规则检查,如果不符合预设风格标准,就进入重写流程,而不是直接返回给用户。这个检查环节必须快速、稳定、可批量执行,本地确定性规则比再次调用大模型便宜得多。
Writing-eval 的核心价值就是把这套检查从人工经验变成程序逻辑:规则容易理解、结果可以追溯、行为永远一致。
1.3 和基于 LLM 的评估器有什么区别
这是理解和运用 Writing-eval 的关键。
业界常见的 AI 内容评估方式是用另一个大模型来打分,例如让 GPT 系列模型做“论文评审助手”。它确实能理解语义和上下文,但存在几个问题:
| 对比维度 | 基于 LLM 的评估 | 本地确定性风格检查 |
|---|---|---|
| 结果稳定性 | 不稳定,随机采样影响大 | 完全可复现 |
| 延迟 | 高,需要网络请求 | 极低,毫秒级 |
| 成本 | 按 Token 计费 | 零边际成本 |
| 隐私 | 文本出域 | 本地处理 |
| 规则透明度 | 不透明,难以解释 | 规则即代码,可阅读 |
| 语义理解 | 强 | 弱,主要靠统计和模式 |
因此,实际项目中更合理的组合是:先用确定性规则快速过滤明显问题,再按需调用大模型做深层的逻辑连贯性评估。Writing-eval 解决的是第一层,也是最频繁、最基础的一层。
1.4 适合谁使用
- AI 应用开发工程师:需要在 Agent 或内容生成管线中加入后处理质检。
- 平台内容治理工程师:需要批量检测 AI 生成内容的风格统一度。
- 技术文档工程师:希望用脚本统一博客、API 文档、教程的语言风格。
- 独立开发者:想做一个轻量的本地写作辅助工具,不依赖大模型 API。
2. 环境准备与版本说明
Writing-eval 的实现不限定某种特定语言。你可以用 Python、Node.js、Go 甚至 Shell 脚本完成一部分检查。不过考虑到文本处理生态成熟度,本文以 Python 为例。
2.1 基础环境
- 操作系统:Windows 10/11、macOS、Linux 均可。
- Python 版本:建议 3.9 及以上。输入材料没有指定具体依赖版本,下面给出的标准库用法无需额外安装第三方包,重点演示思路。如果你的项目已经有虚拟环境,直接使用即可。
检查 Python 版本:
python --version建议创建独立虚拟环境,避免污染全局环境:
python -m venv .venv # Windows .venv\Scripts\activate # macOS / Linux source .venv/bin/activate2.2 项目结构设计
为了让教程清晰,我们规划一个最小可扩展的项目结构:
writing_eval/ ├── checks/ │ ├── __init__.py │ ├── sentence_length.py │ ├── repetition.py │ ├── transitional_words.py │ ├── passive_voice.py │ └── readability.py ├── readers/ │ ├── __init__.py │ ├── text_reader.py │ └── markdown_reader.py ├── reporters/ │ ├── __init__.py │ ├── json_reporter.py │ └── console_reporter.py ├── config/ │ └── default_rules.yaml ├── main.py └── requirements.txt其中:
checks目录存放各类检查规则。readers目录负责读取不同格式的文本,例如纯文本、Markdown、HTML。reporters目录负责输出检查结果。config目录存放规则配置。
2.3 为什么要用目录模块化
风格检查规则会越来越多,如果全部写在单个脚本里,最后会变成一座难以维护的大泥球。模块化之后,每个规则是一个独立函数或类,输入永远是文本块,输出永远是RuleResult对象。这样新增一条规则只需要新增一个文件,注册一下即可。
3. 核心原理拆解:规则、指标与模式
在写代码之前,先理解 Writing-eval 背后的核心机制。它主要由三部分组成:文本预处理、规则引擎、指标聚合。
3.1 文本预处理
原始文本不能直接进入规则判断。需要先做:
- 按段落拆分。
- 段落按句子拆分。
- 句子进行单词化(Tokenization)。
- 去除代码块、Markdown 标记、HTML 标记等噪声。
句子拆分看起来简单,实际有很多边界情况:
- 缩写中的句号,例如 “Dr.”、“e.g.”。
- 小数点,例如 “3.14”。
- 引号和括号内的句子结束符。
- 换行符导致的错误拆分。
一个保守的做法是先处理缩写表,再做规则拆分。下面是简化版示例:
# readers/text_reader.py import re ABBREVIATIONS = { "dr.", "mr.", "mrs.", "ms.", "prof.", "sr.", "jr.", "e.g.", "i.e.", "etc.", "vs.", "inc.", "ltd.", "co.", } SENTENCE_END = re.compile(r'[.!?。!?]') def split_sentences(text: str) -> list[str]: """按句号拆分为句子,保留缩写词场景。""" # 先保护缩写词中的句号,避免误拆 protected = text for abbr in ABBREVIATIONS: protected = protected.replace(abbr, abbr.replace(".", "@@")) # 再按句子结束符拆分 candidates = SENTENCE_END.split(protected) # 恢复缩写词句号 result = [] for sentence in candidates: sentence = sentence.replace("@@", ".") sentence = sentence.strip() if sentence: result.append(sentence) return result这里把缩写里的句号临时替换成@@,拆分完成后再换回来。思路很朴素,但能处理多数英文文本。
3.2 规则引擎设计
每一条规则都遵循统一接口:
# checks/base.py from dataclasses import dataclass, field @dataclass class RuleResult: rule_name: str severity: str # "info", "warning", "error" message: str line: int = 0 start_char: int = 0 end_char: int = 0 suggestions: list[str] = field(default_factory=list) class BaseRule: name = "base" description = "Base rule" def check(self, text: str) -> list[RuleResult]: raise NotImplementedError所有检查规则都继承BaseRule,实现check方法。返回结果是RuleResult列表。这样规则可组合、可排序、可控制误报。
3.3 几类核心检查指标
Writing-eval 的风格检查可以归为以下几种模式:
模式 1:统计阈值型
例如句子平均长度、单词长度分布、段落长度。这类检查只做统计,超过阈值则告警。
模式 2:正则匹配型
例如检测“In conclusion”、“Moreover”这类过渡词在相邻句子中是否高频出现,检测是否连续三句都以“The”开头。
模式 3:语言模型/语法规则型
例如启发式被动语态检测,用词性标注判断 “be + 过去分词” 结构。这一层需要依赖 NLP 库,比如spaCy或nltk。
模式 4:上下文一致性型
例如同一个术语在文章中出现多种写法:“AI”、“A.I.”、“Artificial Intelligence” 混用。这可以基于简单字符串匹配实现。
3.4 为什么选择确定性规则
确定性意味着你可以在 CI/CD 管道里断言:
如果 AI 生成的文章违反了超过 5 条 warning 级别规则,构建失败。这个断言是可靠且可测试的。你不会遇到“上次跑通过了,这次没通过,但是我不知道为什么”的尴尬情况。对于工程化交付,确定性是硬要求。
4. 完整实战案例:构建一个本地风格检查工具
接下来我们实现一个精简但能运行的本地风格检查器。它包含:
- 句子长度检查
- 过渡词频率检查
- 重复开头词检查
- 被动语态基础检测(使用正则近似,不引入重型 NLP 依赖)
- 可读性指标计算(Flesch Reading Ease 简化版)
4.1 创建项目结构与虚拟环境
mkdir writing_eval cd writing_eval python -m venv .venv source .venv/bin/activate # Windows 用户运行 .venv\Scripts\activate创建上述目录结构,然后在项目根目录创建main.py和配置文件。
4.2 实现句子与段落读取器
# readers/markdown_reader.py import re from .text_reader import split_sentences class MarkdownReader: """从 Markdown 文本中提取段落和句子,忽略代码块。""" def __init__(self): self.code_block_pattern = re.compile(r"```.*?```", re.DOTALL) def clean_markdown(self, text: str) -> str: # 去掉代码块 text = self.code_block_pattern.sub(" ", text) # 去掉行内代码 text = re.sub(r"`[^`]*`", "code", text) # 去掉图片和链接语法,保留链接文字 text = re.sub(r"!\[([^\]]*)\]\([^)]*\)", r"\1", text) text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text) # 去掉 Markdown 标题符号 text = re.sub(r"^#+\s*", "", text, flags=re.MULTILINE) # 去掉列表符号 text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.MULTILINE) return text def read(self, text: str) -> dict: cleaned = self.clean_markdown(text) paragraphs = [p.strip() for p in cleaned.split("\n\n") if p.strip()] return { "paragraphs": paragraphs, "sentences": [s for p in paragraphs for s in split_sentences(p)], }这里需要注意,clean_markdown会尽量保留正文文字,去掉标记符号,因为风格检查主要针对自然语言内容。
4.3 实现句子长度检查规则
句子长度是 AI 写作风格失衡的最直观指标。大模型生成内容经常出现两种极端:要么全是短句,读起来碎片化;要么全是 50 词以上的长句,信息密度过高但难读。
# checks/sentence_length.py from .base import BaseRule, RuleResult class SentenceLengthRule(BaseRule): name = "sentence_length" description = "检查是否存在过长或过短的句子" def __init__(self, max_words=40, min_words=3): self.max_words = max_words self.min_words = min_words def check(self, text: str) -> list[RuleResult]: # 这里简化处理,直接用空白分割计算单词数 # 实际项目中可复用 readers 模块的句子拆分结果 import re sentences = re.split(r'(?<=[.!?。!?])\s+', text) results = [] for idx, sentence in enumerate(sentences, start=1): words = sentence.split() if not words: continue word_count = len(words) if word_count > self.max_words: results.append(RuleResult( rule_name=self.name, severity="warning", message=f"第 {idx} 个句子过长({word_count} 词,建议不超过 {self.max_words} 词)", line=idx, suggestions=["拆分长句为 2-3 个逻辑短句", "检查是否有冗余修饰语"], )) elif word_count < self.min_words and len(sentence.strip()) > 2: results.append(RuleResult( rule_name=self.name, severity="info", message=f"第 {idx} 个句子过短({word_count} 词)", line=idx, )) return results4.4 实现重复开头词检查
AI 内容容易出现连续多句使用相同主语或开头词的现象。例如:
- The system provides...
- The system supports...
- The system enables...
这种重复读起来非常机械。我们可以通过统计开头词的出现次数来发现。
# checks/repetition.py from collections import Counter from .base import BaseRule, RuleResult class RepetitiveOpeningRule(BaseRule): name = "repetitive_opening" description = "检查相邻句子是否重复使用相同开头词" def __init__(self, min_repetition=3): self.min_repetition = min_repetition def check(self, text: str) -> list[RuleResult]: import re sentences = re.split(r'(?<=[.!?。!?])\s+', text) first_words = [] for sentence in sentences: words = sentence.split() if not words: first_words.append("") else: # 取第一个实际单词,并转为小写 first_words.append(words[0].strip("'\"").lower()) # 滑动窗口,检查最近 min_repetition 个句子中是否存在大量重复开头 results = [] counter = Counter() for i, word in enumerate(first_words): if not word: continue counter[word] += 1 # 控制窗口 if i >= self.min_repetition: old_word = first_words[i - self.min_repetition] if old_word: counter[old_word] -= 1 if counter[word] >= self.min_repetition: results.append(RuleResult( rule_name=self.name, severity="warning", message=f"最近句子重复使用开头词 “{word}” {counter[word]} 次", line=i + 1, suggestions=[f"尝试替换部分 “{word}” 为同义表达", "调整句式,避免千篇一律的主谓宾结构"], )) return results4.5 实现过渡词检测
过渡词不是不能用,而是 AI 生成的文本特别喜欢在每段开头堆砌固定过渡词。常见有:
- However
- Therefore
- Moreover
- Furthermore
- In conclusion
- Additionally
- Finally
连续段落使用同一类过渡词,会显得模板化。
# checks/transitional_words.py import re from .base import BaseRule, RuleResult class TransitionalWordRule(BaseRule): name = "transitional_words" description = "检查过渡词是否滥用" DEFAULT_WORDS = [ "however", "therefore", "moreover", "furthermore", "in conclusion", "additionally", "finally", "consequently", "nevertheless", "meanwhile", ] def __init__(self, max_per_1000_words=3, words=None): self.words = words or self.DEFAULT_WORDS self.max_per_1000_words = max_per_1000_words def check(self, text: str) -> list[RuleResult]: lower_text = text.lower() total_words = len(text.split()) if total_words == 0: return [] # 统计每个过渡词出现次数 counts = {} for phrase in self.words: # 使用正则匹配完整词组,避免匹配到单词中间 pattern = r'\b' + re.escape(phrase) + r'\b' counts[phrase] = len(re.findall(pattern, lower_text)) total_transitions = sum(counts.values()) # 归一化到每 1000 词 normalized = total_transitions / total_words * 1000 results = [] if normalized > self.max_per_1000_words: top = sorted(counts.items(), key=lambda x: x[1], reverse=True)[:3] detail = ", ".join([f"{word}({count} 次)" for word, count in top if count > 0]) results.append(RuleResult( rule_name=self.name, severity="warning", message=f"过渡词使用频率偏高(每 1000 词 {normalized:.1f} 次,阈值 {self.max_per_1000_words}),高频项:{detail}", suggestions=["删除冗余过渡词", "用具体承接句代替模板化连接词"], )) return results4.6 实现基础被动语态检测
被动语态不一定是错误,但过度使用会让文章冗长、权威感弱。完整的被动语态检测需要词性标注,这里我们用正则近似实现一个轻量版本,识别be + 过去分词结构。
# checks/passive_voice.py import re from .base import BaseRule, RuleResult class PassiveVoiceRule(BaseRule): name = "passive_voice" description = "检测过度使用的被动语态" # 简化版:匹配 be 动词 + 过去分词 BE_VERBS = r'\b(am|is|are|was|were|been|be|being)\b' PAST_PARTICIPLE = r'\b(\w+ed|taken|given|written|built|designed|developed|created|used|implemented)\b' PATTERN = re.compile(BE_VERBS + r'\s+' + PAST_PARTICIPLE, re.IGNORECASE) def __init__(self, max_passive_ratio=0.15): self.max_passive_ratio = max_passive_ratio def check(self, text: str) -> list[RuleResult]: matches = self.PATTERN.findall(text) total_sentences = len(re.findall(r'[.!?。!?]+', text)) if total_sentences == 0: return [] passive_count = len(matches) ratio = passive_count / total_sentences results = [] if ratio > self.max_passive_ratio: examples = [f"{m[0]} {m[1]}" for m in matches[:5]] results.append(RuleResult( rule_name=self.name, severity="warning", message=f"被动语态比例偏高({ratio:.0%},阈值 {self.max_passive_ratio:.0%}),例:{', '.join(examples)}", suggestions=["尽量使用主动语态,让动作发出者更清晰", "如果被动语态无法避免,保留并确认其必要性"], )) return results4.7 实现可读性指标
可读性度量最有名的是 Flesch Reading Ease。它考虑句子的平均长度和单词的平均音节数。虽然这个指标诞生于英语文本,但它对检测“是否又把文章写得很拗口”依然有效。
简化版算法:
Flesch Reading Ease = 206.835 - 1.015 * (总单词数 / 总句子数) - 84.6 * (总音节数 / 总单词数)分数越高,文本越容易读。常见分段:
- 90-100:非常容易读
- 60-70:标准英语
- 30-50:较难
- 0-30:极难
# checks/readability.py import re from .base import BaseRule, RuleResult class FleschReadingEaseRule(BaseRule): name = "flesch_reading_ease" description = "计算 Flesch Reading Ease 可读性得分" def __init__(self, min_score=50, max_score=80): self.min_score = min_score self.max_score = max_score def _count_syllables(self, word: str) -> int: """简化音节计数:按元音组数估算""" word = word.lower() # 去掉末尾不发音 e if word.endswith("e") and len(word) > 3: word = word[:-1] # 计算元音组 vowel_groups = re.findall(r'[aeiouy]+', word) count = len(vowel_groups) return max(1, count) def check(self, text: str) -> list[RuleResult]: sentences = re.split(r'[.!?。!?]+', text) sentences = [s for s in sentences if s.strip()] words = re.findall(r'\b\w+\b', text.lower()) if not sentences or not words: return [] total_syllables = sum(self._count_syllables(w) for w in words) score = 206.835 - 1.015 * (len(words) / len(sentences)) - 84.6 * (total_syllables / len(words)) score = round(max(0, min(100, score)), 2) results = [] if score < self.min_score: results.append(RuleResult( rule_name=self.name, severity="warning", message=f"可读性得分偏低({score} / 100,阈值 {self.min_score}),内容可能过于晦涩", suggestions=["拆分复合长句", "替换生僻词为常用词", "补充解释性文字"], )) elif score > self.max_score: results.append(RuleResult( rule_name=self.name, severity="info", message=f"可读性得分较高({score} / 100),内容容易理解,但注意不要过于口语化", )) else: results.append(RuleResult( rule_name=self.name, severity="info", message=f"可读性得分 {score} / 100,处于合理区间", )) return results4.8 组装规则引擎
现在我们把所有规则放入一个检查器,统一执行。
# main.py import json import sys from readers.markdown_reader import MarkdownReader from checks.sentence_length import SentenceLengthRule from checks.repetition import RepetitiveOpeningRule from checks.transitional_words import TransitionalWordRule from checks.passive_voice import PassiveVoiceRule from checks.readability import FleschReadingEaseRule def run_all_checks(text: str) -> list[dict]: reader = MarkdownReader() parsed = reader.read(text) content = " ".join(parsed["sentences"]) rules = [ SentenceLengthRule(max_words=35, min_words=3), RepetitiveOpeningRule(min_repetition=3), TransitionalWordRule(max_per_1000_words=3), PassiveVoiceRule(max_passive_ratio=0.15), FleschReadingEaseRule(min_score=50, max_score=80), ] all_results = [] for rule in rules: results = rule.check(content) for r in results: all_results.append({ "rule": r.rule_name, "severity": r.severity, "message": r.message, "line": r.line, "suggestions": r.suggestions, }) return all_results def report_console(results: list[dict]) -> None: if not results: print("✅ 未发现风格问题") return warning_count = len([r for r in results if r["severity"] == "warning"]) info_count = len(results) - warning_count print(f"共发现 {len(results)} 条提示,其中 {warning_count} 条警告,{info_count} 条提示。\n") for r in results: level = r["severity"].upper() print(f"[{level}] {r['rule']}: {r['message']}") if r["suggestions"]: for s in r["suggestions"]: print(f" 建议:{s}") print() def main(): if len(sys.argv) < 2: print("用法:python main.py <文本文件路径>") sys.exit(1) filepath = sys.argv[1] with open(filepath, "r", encoding="utf-8") as f: text = f.read() results = run_all_checks(text) report_console(results) if __name__ == "__main__": main()代码中run_all_checks函数接收原始文本,经过 MarkdownReader 清洗后,送给每个规则。最后在控制台输出报告。
4.9 准备测试文本并运行
创建一份示例 AI 风格的 Markdown 文件:sample_draft.md。
# Introduction AI technology is used in many industries. It is believed that AI can improve efficiency. However, the implementation of AI systems is complicated. Therefore, organizations need to adopt a careful strategy. # Main Body The system provides a user-friendly interface. The system supports multiple data sources. The system enables real-time collaboration. Moreover, the system is designed with security in mind. The data is processed by the backend server. The results are displayed in a dashboard. In conclusion, the proposed solution is effective. Furthermore, it is expected that the system will be adopted by many teams. Finally, the users are satisfied with the new features.运行:
python main.py sample_draft.md预期会看到类似输出:
共发现 4 条提示,其中 4 条警告,0 条提示。 [WARNING] sentence_length: 第 2 个句子过长(43 词,建议不超过 35 词) 建议:拆分长句为 2-3 个逻辑短句 建议:检查是否有冗余修饰语 ...你可能注意到,上面的输入文本是我演示用的,实际检查器会因为分词、句子拆分方式不同而略有差异,这很正常。重点是流程能跑通。
4.10 集成 JSON 报告输出
为了让结果能被 CI 或其他工具消费,添加 JSON 导出能力:
# reporters/json_reporter.py import json def export_json(results: list[dict], output_path: str) -> None: with open(output_path, "w", encoding="utf-8") as f: json.dump(results, f, ensure_ascii=False, indent=2)在main.py中增加参数:
def main(): if len(sys.argv) < 2: print("用法:python main.py <文本文件路径> [--json output.json]") sys.exit(1) filepath = sys.argv[1] with open(filepath, "r", encoding="utf-8") as f: text = f.read() results = run_all_checks(text) report_console(results) if "--json" in sys.argv: idx = sys.argv.index("--json") if idx + 1 < len(sys.argv): export_json(results, sys.argv[idx + 1])这样就完成了从“检查规则”到“结果输出”的闭环。
5. 常见问题与排查思路
无论工具多简单,使用过程中总会遇到问题。下面整理几个高频场景。
5.1 检查结果不稳定,同一个文件每次运行结果不同
如果你的检查器引入了词典、分词模型的随机性,建议先做排查。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 每次运行警告数不同 | 使用了在线 API 或未固定随机种子 | 移除在线依赖,固定所有随机种子 |
| 规则结果依赖环境 | 分词、正则库版本不一致 | 锁定依赖版本,使用 lock 文件 |
| 文件编码导致乱码 | 读取文件时未指定 utf-8 | 统一使用encoding="utf-8",做好编码降级 |
为了验证确定性,建议写一个回归测试:
python main.py sample_draft.md > output1.txt python main.py sample_draft.md > output2.txt diff output1.txt output2.txt如果diff没有输出,说明结果是确定的。
5.2 句子拆分把缩写词的句号当作结尾
这是一个很经典的坑。例如 “Dr. Smith is here. He works at MIT.” 如果用简单正则split('.'),会把 “Dr” 前面的部分切错。
解决思路:
- 维护一个常见缩写词表。
- 在拆分前保护缩写词中的句号。
- 也可以引入
nltk.sent_tokenize或spaCy的句子边界检测,但这会增加依赖。
5.3 Markdown 代码块里的内容被误判
代码块中经常包含大段英文注释、字符串、甚至文档字符串。如果风格检查器不过滤代码块,会被计算进句子长度和可读性指标,导致结果严重失真。
解决思路:
- 在 MarkdownReader 中优先剥离代码块。
- 只对正文自然语言段落执行规则。
- 保留行号映射,方便定位问题位置。
5.4 阈值太严导致误报太多
刚开始设置规则时,很容易把阈值调得很严格,结果真实有用的警告被淹没在大量误报里。
建议:
- 先收集一批人工确认过“没有问题”的文本,用工具跑一遍,把阈值调高到不误报为止。
- 再收集一批人工确认“有问题”的文本,验证工具能否发现。
- 规则上线初期设置为
info级别,观察一段时间再提升为warning。
5.5 中文文本处理适配不足
Writing-eval 原本针对英文文本设计,直接套用到中文会出现分词、句子边界、可读性算法不适用的问题。
| 中文适配点 | 处理思路 |
|---|---|
| 句子拆分 | 增加。!?等中文句末标点 |
| 分词 | 使用 jieba 分词代替空格分割 |
| 被动语态 | 检测中文的“被、由、给”等词 |
| 可读性 | 改用基于汉字统计、句子长度的简化指标 |
不要迷信单一指标,中文和英文的写作规范差异很大。
6. 最佳实践与工程建议
6.1 规则配置外部化
不要把阈值写死在代码里。实际项目中,不同内容线(技术博客、产品文案、客服话术)要求完全不同。建议把配置抽到 YAML 或 JSON 文件:
# config/default_rules.yaml sentence_length: enabled: true max_words: 35 min_words: 3 transitional_words: enabled: true max_per_1000_words: 3 words: - however - therefore - moreover passive_voice: enabled: true max_ratio: 0.15 repetitive_opening: enabled: true min_repetition: 3 readability: enabled: true min_score: 50 max_score: 80Python 侧解析:
import yaml def load_config(path: str) -> dict: with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f)这样的好处是,编辑或内容运营同学也能自己调参数,不需要改代码。
6.2 规则可组合、可插拔
随着业务发展,你可能会积累几十条规则。建议实现一个注册表机制:
# checks/registry.py RULES = {} def register(cls): RULES[cls.name] = cls return cls @register class SentenceLengthRule(BaseRule): name = "sentence_length" ...这样新增规则不需要修改main.py的核心逻辑,只需要导入新模块即可。
6.3 与 CI/CD 流水线集成
本地确定性检查工具最适合放进 CI。例如在 GitHub Actions 中:
name: style-check on: [pull_request] jobs: writing-eval: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: "3.11" - run: pip install -r requirements.txt - run: python main.py docs/draft.md --json report.json - run: python check_report.py report.json --max-warnings 5这里check_report.py可以读 JSON,判断错误数是否超过阈值,超过则让 CI 失败。因为是确定性的,CI 的通过与否可以预期。
6.4 控制规则误报,建立基线
风格检查最怕的是“狼来了”。如果每条规则每天都报 20 条警告,但大家知道其中 18 条不用管,那么剩下 2 条真问题也会被忽略。
工程建议:
- 为不同内容类型建不同基线。
- 每周统计各规则触发频率,对高频无用规则做降级或下线。
- 在规则配置中为
info级别的结果提供豁免列表(suppress list),只对确实需要整改的文本输出 warning。
6.5 安全与隐私边界
确定性本地检查的最大卖点之一就是隐私。但要注意:
- 不要把文本发送给任何第三方服务,除非你检查过其协议。
- 日志中不要记录完整原文,只记录文件名、规则名、行号、触发片段摘要。
- 在多租户系统中,不同团队的风格库和规则配置需要做权限隔离,避免通过自定义规则读取其他团队数据。
6.6 与 AI 生成流水线的配合方式
在实际 AI 应用开发中,Writing-eval 逻辑通常放在生成模型之后、用户展示之前:
用户请求 -> Prompt 组装 -> 大模型生成初稿 -> 本地确定性风格检查 -> 通过则返回 -> 不通过则触发特定规则重写(局部改写) -> 再次检查 -> 返回这样做的好处是:
- 每一次重写都有明确依据,不是无脑重新生成。
- 可控性更强,可以只对“过渡词过度使用”这一个问题做定点修复,而不是全部重写。
- 成本更低,不需要为了修正一个标点问题再调用一次大模型。
6.7 日志与可观测性
给每条规则加上 ID、版本号和触发器:
{ "rule": "sentence_length", "rule_version": "v1.2.0", "severity": "warning", "trigger": "sentence_length > 40", "message": "..." }这样出了问题,可以快速判断是规则配置变化导致,还是文本变化导致。对于确定性系统,可复现性不仅体现在结果上,还体现在规则版本的可追溯性上。
7. 总结与进一步学习建议
Writing-eval 这类本地确定性风格检查工具,看起来不如大模型评估器“聪明”,但它胜在稳定、便宜、透明、可测试。在 AI 内容生产越来越普及的今天,它更像一道路线固定的质检闸门,拦截掉那些一眼就能看出的风格问题,让真正有价值的深度评审可以聚焦在高层次逻辑和创造性内容上。
从一个简单想法到一个可用的工程系统,核心路径并不复杂:
- 先用统计阈值和正则规则解决最常见的风格痛点,例如句子过长、重复开头、过渡词滥用。
- 把规则做成模块化、可配置的插件,让不同业务线能调整参数。
- 接入 JSON 输出和 CI 流程,保证每次检查都有记录、可追踪。
- 再逐步引入更复杂的 NLP 能力,例如词性标注、依赖句法分析,提升被动语态和句式多样性的识别准确度。
下一步你可以继续探索的方向包括:
- 基于词性分布计算句子结构多样性指标。
- 使用 TF-IDF 或关键词密度分析检测主题漂移。
- 把确定性规则的结果作为弱标签,训练一个小型的风格分类模型。
- 将同一套检查逻辑封装为 HTTP 服务,供多个业务系统调用。
如果你正在做 AI 内容平台、Agent 后处理或者团队内部写作规范落地,不妨从这套思路开始,先跑通最小闭环,再逐步丰富规则库。每个规则都是可解释的,每次改动都是可测试的,这本身就是工程化的最大优势。