
内容发布前的质量评估十分矛盾它听起来很重要却常常被压缩到发布前最后一刻。很多团队的现状是编辑人工看一遍再打开在线工具查标题长度、关键词密度和可读性最终结论很大程度上取决于个人经验。人工审核当然有价值但标准不统一、执行不可复现、规模一大就很容易漏项。这里要讨论的是如何自建一个类似 ContentIQ 的内容质量评估工具把“内容质量好不好”拆解成结构、可读性、SEO、格式和一致性等可计算指标在文章发布前先得到一份客观、可复现的质量诊断报告并根据诊断结果逐步优化内容。这个参考实现按照一条可运行的路径推进先设计质量维度再实现核心模块最后接入发布流程。实现以 Python 为主代码用于说明思路落地时要结合自己的项目结构、依赖版本和内容场景调整。文章会覆盖 Markdown 解析、可读性计算、标题结构检查、关键词密度检查、评分聚合、JSON 与 Markdown 报告输出以及 FastAPI 接口最后补充常见问题排查和从规则引擎走向模型评估的生产化路径。1. 发布前内容质量评估到底在解决什么问题1.1 内容发布前的质量断层一段内容从构思到发布通常要经过选题、写作、编辑、排版、上线几个环节。每个环节关注的问题并不一样写作者关心表达是否流畅编辑关心事实是否准确运营关心标题是否有吸引力负责排版的同学关心格式是否统一。这些关注点放在一起就是内容质量但它们很难被一次性评估。更麻烦的是质量评估往往发生在发布前最后一刻。此时时间压力最大漏掉的问题也最多标题超过搜索引擎显示长度、核心关键词没有出现在前两段、文档里出现了五级标题却没有二级标题、中英文标点混用、段落长度超过屏幕却没有任何小标题。这些问题单独看都不严重但叠加在一起就会直接影响阅读体验和搜索收录。人工审核解决不了这类问题原因不是审核人员不够专业而是重复性规则检查不依赖灵感只依赖一致性。人反复看同一类问题时会出现疲劳和标准漂移程序不会。1.2 从人工审核到规则化评估规则化评估的思路是把适合用程序判断的检查项变成规则每条规则只回答一个明确的是否问题。例如文档是否缺失 H1 标题标题是否包含核心关键词平均句长是否超过 35 个字符是否存在超过 400 字且没有小标题的长段落是否使用了非全角中文标点这类规则的共同点是输入是文档输出是“通过、不通过、提示”和对应的修改建议。规则化评估不会替代编辑它只替代编辑工作中最重复的部分让人把时间花在事实核查、观点判断和表达优化上。规则化评估的另一个收益是可复现。同一篇文档在任何时间运行只要规则不变结果就不变。这意味着可以把它放进 CI在内容合并或发布前自动触发也可以把历史结果存下来做趋势分析。1.3 ContentIQ 的技术定位与设计目标这里要实现的 ContentIQ 参考版本最少需要具备五种能力输入能力可以读取 Markdown 或纯文本内容。解析能力把文档拆成标题、段落、句子、代码块等结构单元。指标能力计算平均句长、长词占比、关键词密度、段落长度等数值。规则能力把维度模型翻译成可执行的检查项并输出问题级别。报告能力汇总评分和问题列表提供机器可读和人工可读两种输出。设计目标也很明确让一条命令完成一次内容质量检查并给出可定位、可解释、可修复的结果。学习环境能跑通即可生产环境再考虑部署方式、并发策略和数据存储。这个定位决定了后面所有代码的组织方式。2. 设计评估框架先把质量拆成可计算的维度2.1 质量维度模型内容质量无法用一个数字直接衡量必须先拆维度。每个维度对应一系列可计算指标每个指标对应一条或多条规则。维度核心指标典型规则防止的问题结构与格式标题层级、段落长度、列表使用标题不能从 H3 开始段落不能过长文档结构混乱长段落堆叠可读性平均句长、长句占比、难词占比平均句长不超过 35 字长句绕口术语过多SEO 基础标题长度、描述长度、关键词密度标题包含核心关键词搜索展示信息缺失关键词堆砌风格一致性标点、术语、大小写统一全角标点中英文混排不规范完整性字数、结论段、外部引用文章有结论段外部链接有来源内容突然结束引用无出处不要把维度设计得太多。实际项目里 5 到 8 个维度足够超过 10 个维度后指标之间会互相矛盾维护和解释成本都会上升。先保证每个维度至少有两条明确规则再用真实文档调整权重。2.2 指标设计与评分口径指标设计的关键问题是统计口径。同一个指标解析方式不同结果可能差异很大。以平均句长为例。中文句子一般以句号、感叹号、问号结尾。实现时要先把全角问号、感叹号归一化再按这些字符切分句子最后去掉空字符串。英文句子还要考虑小数点、缩写词和引号在这个最小版本中不处理英文分句而是统一按标点切分得到一个近似值。可读性分数直接使用平均句长并不够。可以参考 Flesch Reading Ease 的思路结合句子长度和难词占比给出一个 0 到 100 的综合值。下面是一个简化的中文可读性公式def readability_score(average_sentence_length, long_word_ratio): # 分数越高表示越容易阅读具体系数需要根据语料校准 score 100 - 0.6 * average_sentence_length - 400 * long_word_ratio return max(0, min(100, score))这个公式不是标准公式属于演示口径。实际项目要用自己的历史文章做回归让分数分布尽量符合人工判断。2.3 权重体系和评分公式评分可以采用扣分制总分从 100 开始遇到 error 级别问题扣 5 分warning 扣 2 分info 只是提示不扣分。扣分制的好处是问题越严重越容易暴露且每条扣分可以直接指向具体规则。每个规则用统一结构管理{ id: SEO-0001, level: warning, message: 标题不包含核心关键词, suggestion: 在标题前部加入核心关键词, score_deduct: 2 }把规则配置与规则代码分开是为了后面可以按团队需求调整扣分。同一个团队面向搜索引擎的内容和面向内部文档的内容权重理应不同。3. 从零搭建一个可运行的 ContentIQ 核心3.1 环境准备与项目结构先准备 Python 环境。这个参考实现使用 Python 3.10依赖较少。mkdir contentiq cd contentiq python -m venv venv source venv/bin/activate pip install jieba fastapi uvicornjieba 用于中文分词fastapi 和 uvicorn 用于提供 HTTP 接口。如果只做命令行工具只需要 jieba。如果文本全部是英文可以替换为 nltk 或直接按空白分词不引入 jieba。项目结构这样组织contentiq/ ├── contentiq/ │ ├── __init__.py │ ├── models.py │ ├── parser.py │ ├── metrics.py │ ├── rules.py │ ├── scorer.py │ └── reporters.py ├── cli.py ├── app.py ├── sample.md └── requirements.txtmodels.py 放数据结构parser.py 负责解析文档metrics.py 计算指标rules.py 定义规则scorer.py 汇总评分reporters.py 输出报告。依赖关系保持单向models 被所有人依赖解析器被规则依赖规则被评分器依赖。3.2 数据模型从文档解析到指标采集在 models.py 中定义三类数据文档信息、指标结果、规则结果最后汇总成报告。from dataclasses import dataclass, field from typing import Dict, List dataclass class DocumentInfo: path: str content: str headings: List[str] field(default_factorylist) paragraphs: List[str] field(default_factorylist) sentences: List[str] field(default_factorylist) word_count: int 0 metrics: Dict[str, float] field(default_factorydict) dataclass class MetricResult: name: str value: float unit: str dataclass class RuleResult: id: str level: str message: str suggestion: str score_deduct: float 0 dataclass class QualityReport: total_score: float 100.0 grade: str metrics: Dict[str, float] field(default_factorydict) results: List[RuleResult] field(default_factorylist)dataclass 可以省去大量样板代码。注意 ScoreDeduct 放在 RuleResult 里可以让扣分逻辑通用化方便以后接入配置中心。3.3 实现可读性分析模块可读性分析先做文本清洗和句子切分。在 metrics.py 中实现import re from contentiq.models import DocumentInfo SENTENCE_END { 。: 。, : 。, : 。, !: 。, : 。, ?: 。, } def split_sentences(text: str) - list: normalized text for src, dst in SENTENCE_END.items(): normalized normalized.replace(src, dst) parts [p.strip() for p in normalized.split(。) if p.strip()] return parts def compute_metrics(doc: DocumentInfo) - dict: char_count len(re.sub(r\s, , doc.content)) sentences split_sentences(doc.content) avg_sentence_length char_count / max(1, len(sentences)) # 长词占比字符长度大于 6 的英文单词或超过 8 个字符的中文片段 tokens re.findall(r[A-Za-z]|[\u4e00-\u9fff], doc.content) long_tokens [t for t in tokens if len(t) 6] long_word_ratio len(long_tokens) / max(1, len(tokens)) return { char_count: char_count, sentence_count: len(sentences), avg_sentence_length: round(avg_sentence_length, 2), long_word_ratio: round(long_word_ratio, 4), }这个实现没有使用 jieba原因是可读性指标只需要近似统计。在实际项目中中文分词对关键词密度的影响更大对可读性的影响相对小。需要注意长词占比的口径在中文场景下并不完全合理这里只是示例。3.4 实现标题结构与 SEO 检查模块parser.py 中解析 Markdown 标题然后 rules.py 根据标题等结构执行规则。import re def parse_markdown(content: str) - list: 返回标题列表元素为 (level, text) headings [] for line in content.splitlines(): line line.strip() if line.startswith(#): match re.match(r^(#{1,6})\s(.*)$, line) if match: headings.append((len(match.group(1)), match.group(2).strip())) return headings规则定义使用统一接口from contentiq.models import DocumentInfo, RuleResult def check_heading_levels(doc: DocumentInfo) - list: results [] if not doc.headings: results.append(RuleResult( idSTRUCT-0001, levelwarning, message文档没有任何标题, suggestion至少添加一个 H1 标题, score_deduct2, )) return results max_level max(level for level, _ in doc.headings) if max_level 3: results.append(RuleResult( idSTRUCT-0002, levelwarning, message标题层级过深, suggestion将四级及以上标题拆分为正文或重新组织结构, score_deduct2, )) return results def check_title_keyword(doc: DocumentInfo, keyword: str) - list: if not doc.headings: return [RuleResult( idSEO-0001, levelwarning, message标题缺失无法检查关键词, suggestion补充 H1 标题, score_deduct2, )] h1_texts [text for level, text in doc.headings if level 1] title_text .join(h1_texts) if keyword and keyword not in title_text: return [RuleResult( idSEO-0002, levelwarning, messageH1 标题不包含核心关键词, suggestion将核心关键词放入 H1 标题, score_deduct2, )] return []这样的规则函数直接依赖 DocumentInfo 和 RuleResult好处是测试简单构造一个伪文档断言返回值。3.5 实现规则评分与诊断输出scorer.py 汇总规则结果计算总分和等级。from contentiq.models import QualityReport def apply_score(results: list) - QualityReport: report QualityReport() for result in results: report.total_score - result.score_deduct report.results.append(result) report.total_score max(0, report.total_score) if report.total_score 90: report.grade 优秀 elif report.total_score 75: report.grade 良好 elif report.total_score 60: report.grade 需优化 else: report.grade 建议重写 return report分级只是一个参考口径。真正发布时要不要拦截应该看 error 级别问题的数量而不是只看总分因为扣分制会把少量严重问题平均进总分里。4. 用命令行和 Web 接口跑通发布前检查流程4.1 用 Python 脚本完成一次检查cli.py 读取 Markdown 文件执行完整检查。为了复用逻辑这里把核心处理函数抽出来供 CLI 和 HTTP 接口共同调用。import argparse import json import sys from contentiq.metrics import compute_metrics from contentiq.models import DocumentInfo from contentiq.parser import parse_markdown from contentiq.rules import check_heading_levels, check_title_keyword from contentiq.scorer import apply_score def run_check_with_content(content: str, keyword: str ) - QualityReport: doc DocumentInfo(pathmemory, contentcontent) doc.headings parse_markdown(content) doc.metrics compute_metrics(doc) results [] results.extend(check_heading_levels(doc)) results.extend(check_title_keyword(doc, keyword)) report apply_score(results) report.metrics doc.metrics return report def main(): parser argparse.ArgumentParser(descriptionContentIQ CLI) parser.add_argument(file, helpMarkdown 文件路径) parser.add_argument(--keyword, default, help核心关键词) parser.add_argument(