最近一段时间,AI 写作工具几乎成了内容团队的标配。大到产品文案、技术博客,小到周报、会议纪要,都能交给大模型草拟一版。但很多人在拿到 AI 草稿后,会遇到同一个尴尬问题:内容看起来对,读起来却总觉得“不像人写的”——句式结构高度雷同,形容词堆叠过度,专业术语的使用忽轻忽重,前后语气甚至会出现明显漂移。
人工逐句改当然能解决,但成本太高。如果团队每天要产出几十篇内容,编辑和工程师都会在“洗 AI 味”这件事上消耗大量时间。更麻烦的是,这类问题不像语法错误那样有标准答案,它属于风格问题,过去只能靠人凭感觉判断。
Writing-eval 这类工具的价值,正好落在这一点上。它把“AI 草稿有没有明显机器痕迹”“风格是否符合项目规范”这些模糊问题,变成一组确定性的、可以在本地执行的风格检查规则。本文会讲清楚它的设计思路、适用场景、工程实现方式,以及真正容易踩坑的地方。
1. 这篇文章真正要解决的问题
先说结论:Writing-eval 解决的不是“AI 写得好不好”的问题,而是“AI 写出来的稿子能不能稳定通过团队验收”的问题。
这两者有本质区别。模型输出质量评估属于模型评测范畴,通常要交给更强的模型或者人工评分;而风格检查更像代码里的 Lint 工具,它的核心任务是把一套可描述的写作规范固化成规则,让机器在几分钟内扫完一篇几千字的文档,然后明确指出哪些句子可能存在问题。
回到实际工作流。假设你是某个团队的技术负责人,团队最近开始用 AI 辅助写技术文档。你会发现几个典型痛点:
- AI 写出来的句子经常超过 60 个单词,信息密度低,读者需要来回读两遍才明白。
- 同一篇文档里,某些段落用被动语态,某些段落又突然切换成第一人称,风格不统一。
- 高频词大量重复,比如“深入”“全面”“赋能”“支持”,每隔几段就出现一次,阅读体验很差。
- 数字、产品名、API 名称经常被模型替换成看似合理但实际错误的内容。
这些问题的共性是:它们不是逻辑错误,不是事实错误,而是风格层面不符合团队规范。人工逐条排查效率太低,而且标准不统一。不同编辑对“这句话是否有 AI 味”的判断可能完全不同。
Writing-eval 的思路,就是把这套判断逻辑显式地写成规则。你定义“什么算问题”,工具在本地执行,输出一份可读的报告。它不判断内容好坏,只判断内容是否符合你的规范。
2. 核心概念:本地确定性风格检查
要理解 Writing-eval,先要拆开它的三个关键词:本地、确定性、风格检查。
2.1 什么是确定性检查
确定性检查,是指同样的输入永远得到同样的输出。它不依赖概率模型,不涉及随机采样,也没有“不同模型版本导致结果不同”的问题。对一份文档执行风格检查,结果只有两种:通过或者不通过,并且每条问题都能追踪到触发它的规则。
这和基于 LLM 的自动评估有本质区别。用大模型评价文章风格,你的结论可能是“这段可以”“这段有点怪”,但模型无法给你一个精确的定位级别判断:它说“有点怪”,你很难直接定位是哪一句话导致了这个结果。确定性规则则不同,它能明确告诉你:第 3 段第 2 句使用了被动语态,或者“深入”这个高频词在第 4 段重复了 3 次。
2.2 风格检查检查什么
我整理了常见可以规则化的检查维度:
| 检查维度 | 示例规则 | 输出结果示例 |
|---|---|---|
| 句式复杂度 | 句子超过 40 个单词时提示拆分 | sentence_too_long: 第 12 段第 1 句 |
| 词汇重复 | 同一非停用词在 300 字内出现超过 4 次 | repeated_word: “深入”出现 5 次 |
| 语气一致性 | 检测全文是否混用“你/您/我们” | tone_mixed: 第 3 段使用“您”,第 5 段使用“你” |
| 被动语态 | 检测 “be + 过去分词” 结构 | passive_voice: 第 7 段第 2 句 |
| 术语规范 | 检测非标准写法 | term_invalid: 应使用 “API”,而不是 “api” |
| 结构规范 | 检测标题层级跳跃 | heading_skip: H2 直接跳到 H4 |
| 事实标记 | 检测无依据的数字断言 | unsupported_number: “提升 50%” 未给出数据来源 |
这些规则本质上都是文本分析任务,很多可以通过正则、词性标注、缩进解析等手段实现。Writing-eval 的核心工作就是把它们统一到一套可配置的检查框架里。
2.3 与在线 AI 检测工具的对比
| 对比维度 | Writing-eval 类工具 | 在线 AI 检测服务 |
|---|---|---|
| 部署方式 | 本地运行,无需上传文档 | 云端服务,需要上传文本 |
| 确定性 | 高,规则可复现 | 低,模型版本和参数影响结果 |
| 隐私性 | 好,文档不出本地 | 有数据外泄风险 |
| 可解释性 | 每条问题都能定位到规则 | 通常只能给出概率或分数 |
| 自定义程度 | 高,规则可自由编写 | 受服务功能限制 |
| 使用成本 | 一次性搭建成本,后续几乎为零 | 按调用量计费或订阅 |
从对比可以看出,本地确定性风格检查并不试图替代所有 AI 检测手段。它的优势在于稳定、可解释、可自定义,适合作为内容生产流程里的第一道把关。
3. 环境准备与前置条件
Writing-eval 适合以命令行工具或 Python 包的形式运行,也可以作为一个模块集成进现有的 CI、Git Hook 或内容发布流程。
以下环境要求是通用参考,具体版本请以实际项目为准,这里重点演示通用思路。
3.1 基本环境
- 操作系统:macOS / Linux / Windows(Windows 建议使用 WSL)
- Python 版本:3.9 及以上
- 包管理器:pip 或 poetry
- 文本处理依赖:建议使用
spaCy或jieba做分词和词性标注;如果只是基础正则检查,标准库即可
3.2 安装依赖
# 创建虚拟环境 python -m venv venv source venv/bin/activate # 安装核心依赖(示例,以实际项目为准) pip install spacy # 安装中文模型 python -m spacy download zh_core_web_sm如果只是处理英文文本,可以使用en_core_web_sm。中文场景还可以配合jieba处理分词问题,但要注意spaCy的中文模型已经覆盖了大部分基础需求。
3.3 项目文件结构
一个典型的 Writing-eval 风格项目可以这样组织:
writing_eval/ ├── rules/ │ ├── __init__.py │ ├── sentence_length.py │ ├── repeated_words.py │ ├── passive_voice.py │ └── terminology.py ├── reports/ │ └── report.html ├── examples/ │ ├── good_article.md │ └── bad_article.md ├── eval.py ├── config.yaml └── README.mdrules 目录下放不同的检查规则,eval.py 作为入口,config.yaml 用来配置规则开关和参数。这种结构便于后续添加新规则,也方便团队协作。
4. 核心流程拆解
把 Writing-eval 落地到真实项目,核心流程分为五步。这里不涉及具体 API,而是讲清楚每一步要做什么、为什么需要、以及可能遇到什么问题。
4.1 准备待检查文本
输入可以是 Markdown 文件、纯文本、HTML 或者从 Notion / 飞书导出的文档。建议先统一转成 Markdown 或纯文本,因为这两种格式最容易处理,而且不会丢失段落结构信息。
实现时要注意:如果输入是 Markdown,需要先剥离代码块、链接、图片等不影响文风的元素。否则,内部的 URL 或者代码标识符会被误判为重复词,产生大量误报。
4.2 定义风格规范
这是最核心的一步。风格规范不是从工具里直接生成的,而是由团队自己定义。你需要先问自己几个问题:
- 我们允许的最大句子长度是多少?
- 哪些词语属于高频禁用词?
- 文档应该使用“你”还是“您”?
- API 名称、产品名称有没有标准写法?
把这些问题的答案整理成规则清单,再映射到具体的检查函数。规则粒度越小越好。例如,与其写“检查语气是否统一”,不如拆成“检测第二人称视角的无法混用”。
4.3 执行检查
执行阶段需要做三件事:读取配置、加载规则、逐条运行并聚合结果。
执行时要关注性能。一篇几千字的文章,基础正则检查通常在亚秒级别完成;如果引入词性标注,几百个句子的处理时间可能需要几秒到十几秒。建议先跑基础规则,再跑词性标注类规则,这样在终端里能看到逐步输出,也方便定位卡在哪一步。
4.4 输出报告
报告建议同时输出两种格式:
- 终端可读的摘要,方便开发者快速定位问题
- JSON / Markdown 报告,方便 CI 系统消费
理想报告应该包含:文件路径、段落位置、规则名称、问题等级、具体文本片段、以及建议修改方式。可读性比数量更重要。
4.5 接入 CI 和提交流程
风格检查和代码检查一样,最怕“只在本地跑一次”。更好的做法是接入 CI,让每次变更都能触发检查,并且设置失败阈值。比如超过 10 条严重问题就显示红灯,低于阈值则警告。
5. 完整示例与代码实现
下面给出一个可运行的参考实现。这个示例不是一个特定仓库的完整代码,而是展示 Writing-eval 类工具的核心思路,你可以在这个基础上替换成自己的规则。
5.1 示例一:检查器骨架
# 文件路径:eval.py import re import sys import json from pathlib import Path class WritingEval: def __init__(self, max_sentence_len=40, banned_words=None): self.max_sentence_len = max_sentence_len self.banned_words = banned_words or ["深入", "全面", "赋能", "闭环"] self.issues = [] def run(self, text): self.issues = [] self._check_sentence_length(text) self._check_banned_words(text) self._check_repeated_words(text) return self.issues def _check_sentence_length(self, text): sentences = re.split(r'[。!؟.!?]', text) for idx, sent in enumerate(sentences, 1): word_count = len(sent.replace(" ", "")) if word_count > self.max_sentence_len: self.issues.append({ "rule": "sentence_too_long", "level": "warning", "position": f"第{idx}句", "message": f"句子长度 {word_count} 超过阈值 {self.max_sentence_len}" }) def _check_banned_words(self, content): for word in self.banned_words: if word in content: count = content.count(word) self.issues.append({ "rule": "banned_word", "level": "error", "position": "全文", "message": f"检测到禁用词「{word}」,出现 {count} 次" }) def _check_repeated_words(self, content, window=300): cleaned = re.sub(r'[^\w\u4e00-\u9fff]', ' ', content) words = [w for w in cleaned.split() if w not in self.banned_words] seen = {} for idx, word in enumerate(words): if len(word) < 2 or word in self.banned_words: continue seen.setdefault(word, []).append(idx) for word, positions in seen.items(): if len(positions) >= 4: self.issues.append({ "rule": "repeated_word", "level": "warning", "position": f"第{positions[0] + 1}词附近", "message": f"「{word}」出现 {len(positions)} 次,建议替换或精简" }) if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python eval.py <file>") sys.exit(1) filepath = Path(sys.argv[1]) content = filepath.read_text(encoding="utf-8") evaluator = WritingEval() issues = evaluator.run(content) print(json.dumps(issues, ensure_ascii=False, indent=2)) if any(item["level"] == "error" for item in issues): sys.exit(1)这份代码用三个核心函数演示了规则框架:句子长度、禁用词、高频重复词。每个函数都把识别出的问题追加到issues列表,每个问题包含规则名、等级、位置和消息。入口处支持传入文件路径,同时输出 JSON 供 CI 消费。
5.2 示例二:YAML 配置文件
为了让规则参数不写死在代码里,建议用 YAML 管理规则配置。
# 文件路径:config.yaml rules: sentence_length: enabled: true max_length: 40 level: warning banned_words: enabled: true words: - "深入" - "全面" - "赋能" - "闭环" level: error repeated_words: enabled: true window: 300 min_repeat: 4 level: warning passive_voice: enabled: false level: info terminology: enabled: true terms: - wrong: "api" correct: "API" - wrong: "AI写作" correct: "AI 写作"配置化之后,团队改规则不需要动代码,只需要修改 YAML 文件。这对非工程师参与规范制定尤其友好。
5.3 示例三:命令行入口和 CI 集成
#!/usr/bin/env bash # 文件路径:scripts/check_writing.sh set -e DOCS_DIR=${1:-docs} echo "Running writing style checks on ${DOCS_DIR}..." python eval.py "${DOCS_DIR}/README.md" # 如果包含其他文档,可以循环遍历 for file in $(find "${DOCS_DIR}" -name "*.md"); do echo "Checking ${file}" python eval.py "$file" done接入 CI 的时候,只需要让工作流执行这个脚本。一旦有 error 级别的问题,脚本会通过非零退出码让任务失败。这个行为模式与 ESLint、Checkstyle 等工具完全一致,团队学习成本很低。
5.4 示例四:输出 Markdown 报告
除了 JSON,还可以输出便于阅读的 Markdown 报告,方便直接贴在评审评论里。
# 文件路径:report.py from pathlib import Path from eval import WritingEval content = Path("examples/bad_article.md").read_text(encoding="utf-8") evaluator = WritingEval() issues = evaluator.run(content) lines = ["## Writing-Eval Report", ""] for issue in issues: lines.append(f"- **[{issue['level'].upper()}]** {issue['rule']} @ {issue['position']}") lines.append(f" - {issue['message']}") lines.append("") Path("reports/report.md").write_text("\n".join(lines), encoding="utf-8") print("Report generated: reports/report.md")执行后生成的 report.md 可以直接作为 GitHub PR 评论或 GitLab Note 的内容,也可以作为知识库中的评审记录。
6. 运行结果与效果验证
假设待检查的文档examples/bad_article.md内容如下:
# 产品介绍 我们的产品能够深入提升团队协作效率,全面赋能数字化转型,助力企业在激烈的市场竞争中实现业务的闭环管理。 通过深度分析和智能预测,我们可以全面覆盖客户的需求,并且能够深入理解用户的每一个痛点,进一步提升服务质量,全面优化用户体验。运行:
python eval.py examples/bad_article.md预期会看到类似输出:
[ { "rule": "sentence_too_long", "level": "warning", "position": "第1句", "message": "句子长度 42 超过阈值 40" }, { "rule": "banned_word", "level": "error", "position": "全文", "message": "检测到禁用词「深入」,出现 2 次" }, { "rule": "banned_word", "level": "error", "position": "全文", "message": "检测到禁用词「全面」,出现 2 次" }, { "rule": "banned_word", "level": "error", "position": "全文", "message": "检测到禁用词「赋能」,出现 1 次" }, { "rule": "banned_word", "level": "error", "position": "全文", "message": "检测到禁用词「闭环」,出现 1 次" } ]如何判断运行成功:
- 终端输出合法 JSON,并且
issues数组包含预期的规则命中结果。 - 当存在 error 级别问题时,命令退出码为 1;如果全部通过,退出码为 0。
- 生成的报告文件内容完整,路径、规则名、消息都与终端输出一致。
如果运行失败,先看两个地方:第一,文件路径是否正确,建议在脚本开头打印绝对路径;第二,文本编码是否为 UTF-8,Windows 环境容易出现编码问题。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 误报大量句子过长 | 标点符号切分不完整,中文分句未考虑顿号、冒号 | 打印切分后的句子列表,检查句子边界 | 增加分句符号,如[。!?.!?;;] |
| 中文重复词检测不准确 | 简单空格切分不适合中文,词语粘连 | 使用 jieba 或 spaCy 分词后检测 | 引入jieba.lcut()做分词 |
| 规则改动后不生效 | 新规则没有注册到 eval.py 的 run 方法 | 检查规则函数是否被调用 | 统一用装饰器或配置映射注册规则 |
| 报告里缺少具体位置 | 规则里没有计算句子序号或章节信息 | 检查规则实现,增加位置计算 | 基于 Markdown 标题和段落序号生成位置 |
| CI 执行超时 | 输入包含超大文件或全文正则性能差 | 使用time命令定位耗时阶段 | 对超大文件分段检查,或缓存分词结果 |
| JSON 输出乱码 | 控制台编码问题 | 检查终端和文件编码 | 输出时指定ensure_ascii=False,保存文件时用 UTF-8 |
这里最需要提醒的是“误报治理”。风格检查工具和 Lint 工具一样,真正难的不是写出规则,而是让规则在真实语料上保持合理精确率。如果规则误报率太高,团队用两周就会放弃。因此,建议为每条规则准备一个“测试集”,包含正例和反例,任何规则改动都要通过测试集验证。
8. 最佳实践与工程建议
Writing-eval 类工具看起来简单,真正落地成团队基础设施时,还是有一些值得注意的点。
8.1 规则从“问题现象”反推,而不是从“技术方案”出发
很多人实现第一个规则时,会想“我可以用正则写一个句子长度检查”。但更好做法是先从团队的实际文本样本里找出 10 个最常见的风格问题,再决定哪些适合规则化。常见可规则化的问题是:词频、句式、语气、术语、标点、结构规范。这些在 Python 或 Node.js 里都能高效实现,不依赖模型。
8.2 分等级处理,不要一刀切
建议把所有规则分为三个等级:
- error:一旦触发,必须修复,比如禁用词、错误术语。
- warning:建议修复,如句子过长、重复词。
- info:仅提示,不进入失败判断,如被动语态数量。
这样设计的好处是:CI 的失败门槛可以被量化。你可以规定“超过 5 个 warning 才失败”,而不是“出现任何一个 warning 就失败”。门槛太高容易漏问题,门槛太低会整天报警。
8.3 把“测试集”当成一等公民
每条规则都应该自带一组测试样例。拿“句子过长”举例:
# 文件路径:tests/test_sentence_length.py import pytest from eval import WritingEval evaluator = WritingEval() def test_short_sentence_pass(): content = "今天天气不错。" issues = evaluator.run(content) assert not any(i["rule"] == "sentence_too_long" for i in issues) def test_long_sentence_fail(): content = "这个产品能够帮助企业在数字化转型过程中实现完整的解决方案。" issues = evaluator.run(content) assert any(i["rule"] == "sentence_too_long" for i in issues)这套测试集的价值会随着规则数量增加而放大。没有测试集的规则,本质上只是临时脚本。有了测试集,规则才能被团队安全地修改和扩展。
8.4 注意安全与权限边界
如果 Writing-eval 接入 CI 或内容发布系统,请确保它遵循最小权限原则。例如:
- 不需要以管理员身份运行。
- 不要读取超出待检查目录以外的文件。
- 如果工具需要访问模型服务或其他远程接口,建议先向安全团队确认数据出口和权限范围。
- 涉及生产环境发布时,先在本机和测试环境验证检查结果,再进行全量发布。
8.5 先本地跑,再进 CI,最后进插件
很多团队第一次做这类工具时,直接跳到 CI 环节,结果 CI 红了一大片,所有人都在处理误报。更稳妥的顺序是:
- 本地跑通最小示例。
- 收集一周真实内容样本,记录规则命中率和误报数量。
- 调整规则阈值,直到误报可接受。
- 接入 CI,设置在 Pull Request 上运行。
- 再考虑接入编辑器插件,让作者在写作时就实时看到问题。
8.6 与手工审查配合
确定性风格检查能解决“明显不符合规范”的问题,但它不能替代编辑判断,更不能判断文章逻辑是否通顺、观点是否有洞见。更合理的分工是:
- 机器负责:格式、术语、高频词、句子长度、语气一致性。
- 人工负责:逻辑结构、事实准确性、观点表达、受众契合度。
把这个分工写进团队协作流程里,能避免两个极端:一是完全依赖机器审查,二是完全靠人工逐字逐句读。
9. 总结与后续学习方向
Writing-eval 这类本地确定性风格检查工具,解决的是 AI 写作流程里一个非常具体、又容易被忽略的问题:如何用低成本、可解释、可复现的方式,给 AI 草稿做一次风格层面的大扫除。
它不适合用来判断“这篇文章写得好不好”,它适合用来判断“这篇文章是否符合团队的写作规范”。这两件事在工程上是完全可以分开的。
如果你想继续深入,可以从这几个方向扩展:
- 把规则引擎从正则升级到基于语法树的分析,比如用
spaCy的依存分析检查被动语态、句子成分完整度。 - 做规则版本管理,把团队的写作规范变成一个 Git 仓库里的版本化配置。
- 把检查结果和错误样例送入一个小型数据集,作为后续训练本地检测模型的素材。
- 尝试把 Writing-eval 做成 Git Hook,让每次提交文档时自动检查,把问题暴露在最早阶段。
最后建议收藏备用。下一次你的 AI 草稿再出现“赋能”“闭环”“深入提升”这样的高频词时,至少可以有一个不依赖人工肉眼的检查手段,让「AI 写作」和「AI 写作规范」这两件事在工程上互相约束起来。