质检系统:Agent的自我审查 — 不自检的Agent就像没有编辑的报社

0 阅读20分钟

先说结论

LLM 能写出流畅的文章,但不代表文章没问题 — 夸大宣传、敏感词、AI味书面语、内容重复,这些坑 LLM 自己看不见。

没有质检的 Agent,就像一家没有编辑的报社 — 记者写完直接付印,什么内容都敢发。在 self-media-agent 项目里,quality/ 模块实现了四维质检:

生成文章 → 合规检测 → 去同质化 → 口语化优化 → 逻辑校验 → 输出

四个维度,一个编排器,每个维度 check → fix → 下一个。

维度检查什么怎么修对应代码
合规检测敏感词、夸大宣传、低俗内容替换为 ***quality/sensitive.py
去同质化与已有内容相似度LLM 改写quality/dedup.py
口语化优化AI 生硬书面语规则替换quality/colloquial.py
逻辑校验前后矛盾、逻辑断层LLM 修复quality/logic_check.py

编排器 quality/orchestrator.py 依次执行四个维度,汇总分数,决定通过与否。

不自检的 Agent 不是好 Agent — 质检是 Agent 的"良知"。

一、为什么需要质检?

没有质检的 Agent

LLM 生成:「这款面膜是世界第一好用的,100%有效,绝对能根治你的皮肤问题」
           → 直接发布
           → 平台审核不通过(夸大宣传)
           → 账号被限流
LLM 生成:「综上所述,值得注意的是,毋庸置疑,夏季防晒至关重要」
           → 直接发布
           → 用户一眼看出是 AI 写的
           → 取关
LLM 生成:和昨天生成的文章 90% 重复
           → 直接发布
           → 用户觉得"这号怎么天天发一样的内容"
           → 掉粉

三个致命问题:合规风险、AI 味暴露、内容同质化。这些都不是 LLM 能自己发现的 — LLM 只管"写得流畅",不管"写得对不对"。

质检的定位

生成:LLM 负责"写出来"(追求流畅、有内容)
质检:质检器负责"把关"(追求合规、口语化、不重复、有逻辑)

生成和质检是两个独立的环节,各管一段。 生成追求"能写",质检追求"能发" — 一篇文章要同时过两关才能输出。

质检报告:QualityReport

所有质检维度都返回同一个数据结构 — QualityReport

# content/schema.py
class QualityReport(BaseModel):
    passed: bool = Field(default=True, description="是否通过")
    violations: list[str] = Field(default_factory=list, description="违规项")
    score: float = Field(1.0, ge=0.0, le=1.0, description="质检分数")
    details: dict = Field(default_factory=dict, description="详细信息")

四个字段,统一接口

字段类型说明
passedbool这维度过没过
violationslist[str]具体违规了什么
scorefloat0.0-1.0 的分数
detailsdict附加信息(如相似度数值)

每个检查器都返回 QualityReport — 统一接口,编排器才能统一处理。

二、四维质检架构

架构图

QualityOrchestrator(编排器)
├── SensitiveFilter        ← 维度1:合规检测
├── DedupChecker           ← 维度2:去同质化
├── ColloquialOptimizer    ← 维度3:口语化优化
└── LogicChecker           ← 维度4:逻辑校验

编排器持有四个检查器的引用,依次调用,汇总结果。

执行顺序

# quality/orchestrator.py
class QualityOrchestrator:
    """质检编排:依次执行四个质检维度
​
    执行顺序:
    1. 合规检测(敏感词过滤)
    2. 去同质化(相似度检测)
    3. 口语化优化
    4. 逻辑校验
​
    每个维度 check → 可选 fix → 下一个维度
    """

为什么是这个顺序?

① 合规检测  → 最优先,不合规直接不能发
② 去同质化  → 早检测早改写,改完再优化口语化
③ 口语化优化 → 在合规、去重的基础上优化表达
④ 逻辑校验  → 最后做,因为需要 LLM,开销最大

从硬到软,从快到慢 — 合规检测是正则匹配(毫秒级),逻辑校验是 LLM 调用(秒级)。先做快的拦截明显问题,再做慢的深度检查。

每个维度的统一模式

check(检测)→ passed? → 是:进入下一维度
                       → 否:auto_fix? → 是:fix(修复)→ recheck → 进入下一维度
                                     → 否:记录违规 → 进入下一维度

每个维度都是"检测→修复→继续"的三步 — 修复后内容会变,所以修复后传给下一个维度的是修改后的内容。

三、维度1:合规检测(敏感词过滤)

quality/sensitive.pySensitiveFilter — 最硬的检查,不合规的内容绝对不能发。

四类检查

class SensitiveFilter:
    def check(self, persona: PersonaConfig, content: str) -> QualityReport:
        violations = []
​
        # 1. 检查禁用词(人设配置的 banned_words)
        violations += self._check_banned_words(content, persona.banned_words)
​
        # 2. 检查额外敏感词(从文件加载)
        violations += self._check_banned_words(content, self.extra_words)
​
        # 3. 检查夸大宣传
        if persona.no_exaggeration:
            violations += self._check_exaggeration(content)
​
        # 4. 检查低俗内容
        if persona.no_vulgar:
            violations += self._check_vulgar(content)
​
        score = max(0.0, 1.0 - len(violations) * 0.1)
        return QualityReport(passed=len(violations) == 0, violations=violations, score=score)

四类检查,逐个扫描

检查类来源示例
禁用词人设的 banned_words人设配了 ["某品牌名"] → 文章出现就违规
额外敏感词敏感词文件从文件加载行业敏感词
夸大宣传正则模式"世界第一"、"100%有效"、"绝对"、"根治"
低俗内容正则模式"擦边"、"低俗"、"色情"、"裸露"

夸大宣传模式

_EXAGGERATION_PATTERNS = [
    r"世界第一", r"全球第一", r"100%有效", r"绝对", r"一定",
    r"包治", r"根治", r"永不", r"史上最强", r"无人能及",
]

10 个正则模式,覆盖自媒体常见夸大宣传 — 这些词在广告法里是明确禁止的,平台审核会直接拦截。

人设配置控制

# persona/schema.py
banned_words: list[str] = Field(default_factory=list, description="禁用词列表")
no_exaggeration: bool = Field(default=True, description="禁止夸大宣传")
no_vulgar: bool = Field(default=True, description="禁止低俗")
no_fake_data: bool = Field(default=True, description="禁止虚假数据")

质检规则跟着人设走 — 不同人设可以配不同的 banned_words,美妆赛道禁某些成分词,职场赛道禁某些敏感话题词。no_exaggerationno_vulgar 默认开启,但允许关闭(比如某些赛道需要"史上最强"这种夸张表达吸引眼球)。

自动修复:替换为 ***

async def auto_fix(self, persona, content, report) -> str:
    fixed = content

    # 替换禁用词为 ***
    for word in persona.banned_words:
        if word in fixed:
            fixed = fixed.replace(word, "***")

    # 替换额外敏感词
    for word in self.extra_words:
        if word in fixed:
            fixed = fixed.replace(word, "***")

    # 替换夸大宣传词
    if persona.no_exaggeration:
        for pattern in _EXAGGERATION_PATTERNS:
            fixed = re.sub(pattern, "***", fixed)

    return fixed

简单粗暴:直接替换为 ***

修复前:这款面膜是世界第一好用的,100%有效
修复后:这款面膜是***好用的,***

为什么不用 LLM 改写? — 合规问题不能"改写",只能"消除"。把"世界第一"改写成"名列前茅"虽然合规了,但可能改变了用户的原意。直接替换为 *** 是最安全的 — 消除了违规词,保留了句子结构,用户看到后可以手动调整。

四、维度2:去同质化(相似度检测)

quality/dedup.pyDedupChecker — 防止 Agent 生成的文章和之前的内容太像,用户觉得"天天发一样的东西"。

n-gram 余弦相似度

def _ngrams(text: str, n: int = 3) -> Counter:
    """生成文本的 n-gram 频率表"""
    text = text.replace("\n", " ").strip()
    if len(text) < n:
        return Counter([text])
    return Counter(text[i : i + n] for i in range(len(text) - n + 1))

def _cosine_similarity(a: Counter, b: Counter) -> float:
    """计算两个 Counter 的余弦相似度"""
    intersection = set(a.keys()) & set(b.keys())
    numerator = sum(a[k] * b[k] for k in intersection)
    denom_a = sum(v * v for v in a.values()) ** 0.5
    denom_b = sum(v * v for v in b.values()) ** 0.5
    if denom_a == 0 or denom_b == 0:
        return 0.0
    return numerator / (denom_a * denom_b)

3-gram 余弦相似度 — 把文章拆成连续3字符的片段,用向量夹角衡量两篇文章的相似度。

文章A:"夏季防晒很重要" → 3-grams: {夏季防, 季防晒, 防晒很, 晒很重, 很重要}
文章B:"夏天防晒很关键" → 3-grams: {夏天防, 天防晒, 防晒很, 晒很关, 很关键}

共同 3-gram:{防晒很} → 相似度低
文章A:"夏季防晒很重要" → 3-grams: {夏季防, 季防晒, 防晒很, 晒很重, 很重要}
文章C:"夏季防晒很关键" → 3-grams: {夏季防, 季防晒, 防晒很, 晒很关, 很关键}

共同 3-gram:{夏季防, 季防晒, 防晒很} → 相似度高

3-gram 比词级匹配更细粒度 — 即使换了几个字,只要大量 3-gram 重合,相似度就会高。

检查逻辑

class DedupChecker:
    def __init__(self, similarity_threshold: float = 0.85):
        self.threshold = similarity_threshold

    async def check(self, persona, content, existing_contents=None) -> QualityReport:
        if not existing_contents:
            return QualityReport(passed=True, score=1.0)

        content_ngrams = _ngrams(content)
        max_sim = 0.0
        most_similar_idx = -1

        for i, existing in enumerate(existing_contents):
            sim = _cosine_similarity(content_ngrams, _ngrams(existing))
            if sim > max_sim:
                max_sim = sim
                most_similar_idx = i

        if max_sim > self.threshold:
            violation = f"与已有内容(#{most_similar_idx + 1})相似度 {max_sim:.2%},超过阈值 {self.threshold:.0%}"
            return QualityReport(passed=False, violations=[violation], score=max(0.0, 1.0 - max_sim), ...)

        return QualityReport(passed=True, score=1.0 - max_sim * 0.5, ...)

与所有已有内容逐一比较,取最大相似度 — 只要和任何一篇已有内容相似度超过 0.85,就不通过。

新文章 vs 已有文章[0] → 相似度 0.3
新文章 vs 已有文章[1] → 相似度 0.7
新文章 vs 已有文章[2] → 相似度 0.88 ← 超过阈值 0.85!
→ 不通过,违规:"与已有内容(#3)相似度 88%,超过阈值 85%"

修复:LLM 改写

async def fix(self, persona, content, report) -> str:
    prompt = (
        "以下内容与已有作品过于相似,请改写以增加原创性和独特观点,"
        "保持原文核心信息不变,但改变表述方式和结构:\n\n"
        f"{content}"
    )
    result = await llm.generate(
        system_prompt="你是一位自媒体内容改写专家,擅长在保持核心信息的前提下增加原创性。",
        user_prompt=prompt,
    )
    return result

和合规检测不同,去同质化的修复用 LLM 改写 — 因为相似不是"错",只是"太像了",改写一下就行,不需要删除。

预留 FAISS 接口

class DedupChecker:
    """基于文本相似度的去同质化检测

    V2 使用 n-gram 余弦相似度,未来可替换为 FAISS 向量检索。
    """

当前是 n-gram 余弦相似度(O(n) 逐一比较),未来文章多了可以换 FAISS 向量检索(O(log n) 近似最近邻)。 接口不变,实现可替换。

existing_contents 从哪来?

# pipeline/runner.py
existing_contents = [
    c.body for c in self.repo.store.list_contents()
    if c.persona_id == persona.id
]
quality_result = await self.quality_orchestrator.check_and_fix(
    persona=persona,
    content=formatted,
    existing_contents=existing_contents,
    ...
)

取同一个人设下所有已有文章的正文 — 只和自己比,不和别人比。美妆人设的文章不会和职场人设的文章比相似度。

五、维度3:口语化优化

quality/colloquial.pyColloquialOptimizer — 消除 AI 生成的"书面语味",让文章读起来像人写的。

15 个书面语→口语化替换

STIFF_PATTERNS: dict[str, str] = {
    "值得注意的是": "注意哈",
    "综上所述": "总的来说",
    "毋庸置疑": "真的",
    "由此可见": "所以",
    "需要指出的是": "要注意",
    "显而易见": "很明显",
    "不言而喻": "大家都懂",
    "至关重要": "特别重要",
    "换言之": "换句话说",
    "与此同时": "同时",
    "在一定程度上": "某种程度上",
    "从长远来看": "长远看",
    "基于以上分析": "综合来看",
    "事实上": "其实",
    "具体而言": "具体来说",
}

15 个 AI 最爱用的书面语,全部替换为口语化表达。 这些词是 LLM 的"口音" — 一看就知道是 AI 写的。

检测与修复

class ColloquialOptimizer:
    async def check(self, persona, content) -> QualityReport:
        violations = []
        for stiff, casual in self.patterns.items():
            if stiff in content:
                violations.append(f"书面语表达:「{stiff}」→ 建议改为「{casual}」")
        score = max(0.0, 1.0 - len(violations) * 0.05)
        return QualityReport(passed=len(violations) == 0, violations=violations, score=score)

    async def fix(self, persona, content, report) -> str:
        fixed = content
        for stiff, casual in self.patterns.items():
            if stiff in fixed:
                fixed = fixed.replace(stiff, casual)
        # 额外处理:首先/其次/最后 → 第一/第二/第三
        fixed = re.sub(r"首先[,,]", "第一,", fixed)
        fixed = re.sub(r"其次[,,]", "第二,", fixed)
        fixed = re.sub(r"最后[,,]", "第三,", fixed)
        return fixed

纯规则替换,不需要 LLM — 毫秒级完成,开销最小。

修复前:综上所述,值得注意的是,夏季防晒至关重要
修复后:总的来说,注意哈,夏季防晒特别重要

还有一个额外处理 — "首先/其次/最后"替换为"第一/第二/第三",因为"首先"是学术写作的口吻,"第一"更口语化。

为什么不用 LLM 做口语化?

规则替换有三个优势

  1. — 正则匹配,毫秒级,不需要调 LLM
  2. — 精确替换,不会改错
  3. 可控 — 替换规则明确,不会引入新问题

LLM 改写虽然更灵活,但可能"改过头" — 把不该改的也改了。规则替换只动确定的书面语,其他一律不碰。

扣分比合规轻

# 合规检测:每处违规扣 0.1
score = max(0.0, 1.0 - len(violations) * 0.1)

# 口语化:每处违规扣 0.05
score = max(0.0, 1.0 - len(violations) * 0.05)

口语化问题比合规问题轻 — 合规违规是"不能发",口语化问题是"不够好"。所以扣分更轻,不会因为几个书面语就直接判不通过。

六、维度4:逻辑校验

quality/logic_check.pyLogicChecker — 唯一一个 LLM 驱动的检查器,检查文章的逻辑问题。

三个检查维度

class LogicChecker:
    """LLM 驱动的逻辑校验

    检查维度:
    1. 前后矛盾
    2. 逻辑断层(观点无依据)
    3. 凑数内容(空洞废话)
    """
维度说明示例
前后矛盾同一内容中出现相互矛盾的观点或数据前面说"油皮要用控油面膜",后面说"油皮要用补水面膜"
逻辑断层观点缺乏依据,推理跳跃"所以一定要买这款面膜" — 为什么?没有理由
凑数内容空洞废话,无实质信息的填充内容"大家都知道防晒很重要,防晒真的很重要,重要的事情说三遍"

LLM 审查

async def check(self, persona, content) -> QualityReport:
    system_prompt = (
        "你是一位自媒体内容逻辑审查专家。请审查以下内容是否存在逻辑问题:\n"
        "1. 前后矛盾:同一内容中出现相互矛盾的观点或数据\n"
        "2. 逻辑断层:观点缺乏依据,推理跳跃\n"
        "3. 凑数内容:空洞废话,无实质信息的填充内容\n\n"
        "请以JSON格式返回审查结果:\n"
        '{"no_issues": true/false, "issues": ["问题描述1", "问题描述2"]}'
    )
    user_prompt = f"请审查以下内容的逻辑问题:\n\n{content[:2000]}"

    result = await llm.generate_json(system_prompt=system_prompt, user_prompt=user_prompt)
    no_issues = result.get("no_issues", True)
    issues = result.get("issues", [])

    if not no_issues and issues:
        violations = [f"逻辑问题: {issue}" for issue in issues]
        score = max(0.0, 1.0 - len(issues) * 0.15)
        return QualityReport(passed=False, violations=violations, score=score, ...)

    return QualityReport(passed=True, score=1.0)

让 LLM 当"逻辑审查员" — 用一个独立的 Prompt 审查内容,返回 JSON 格式的问题列表。

content[:2000] — 只传前 2000 字给 LLM,避免 token 过长。大多数逻辑问题在前半部分就能发现。

为什么默认关闭?

# config/models.py
class QualityConfig(BaseModel):
    logic_check_enabled: bool = False  # 逻辑校验(需要 LLM,默认关闭)

两个原因

  1. 开销大 — 每次质检多一次 LLM 调用,生成一篇多花 2-5 秒
  2. 误杀风险 — LLM 审查逻辑可能过于严格,把正常的创意表达判为"逻辑断层"

所以逻辑校验是可选的 — 对内容质量要求高的场景手动开启,追求速度的场景关闭。

降级:LLM 异常时跳过

except Exception as e:
    logger.error(f"逻辑校验失败: {e}")
    return QualityReport(passed=True, score=0.8, details={"error": str(e)})

LLM 调用失败时,不判不通过,给 0.8 分 — 逻辑校验是"锦上添花",不是"一票否决"。LLM 挂了不应该影响文章发布。

七、质检编排器:orchestrator 模式

quality/orchestrator.pyQualityOrchestrator — 把四个检查器串起来,统一调度。

check_and_fix:全链路质检

class QualityOrchestrator:
    def __init__(self, sensitive_filter=None, dedup_checker=None,
                 colloquial_optimizer=None, logic_checker=None):
        self.sensitive_filter = sensitive_filter or SensitiveFilter()
        self.dedup_checker = dedup_checker or DedupChecker()
        self.colloquial_optimizer = colloquial_optimizer or ColloquialOptimizer()
        self.logic_checker = logic_checker or LogicChecker()

    async def check_and_fix(self, persona, content, auto_fix=True,
                            existing_contents=None, enable_dedup=True,
                            enable_colloquial=True, enable_logic=False) -> QualityResult:
        result = QualityResult(original=content, final_content=content)
        current = content

        # 1. 合规检测
        report = self.sensitive_filter.check(persona, current)
        if not report.passed and auto_fix:
            current = await self.sensitive_filter.auto_fix(persona, current, report)
            report = self.sensitive_filter.check(persona, current)
        result.checks.append({"name": "合规检测", "passed": report.passed, ...})

        # 2. 去同质化
        if enable_dedup:
            dedup_report = await self.dedup_checker.check(persona, current, existing_contents)
            if not dedup_report.passed and auto_fix:
                current = await self.dedup_checker.fix(persona, current, dedup_report)
            result.checks.append({"name": "去同质化", ...})

        # 3. 口语化优化
        if enable_colloquial:
            colloquial_report = await self.colloquial_optimizer.check(persona, current)
            if not colloquial_report.passed and auto_fix:
                current = await self.colloquial_optimizer.fix(persona, current, colloquial_report)
            result.checks.append({"name": "口语化优化", ...})

        # 4. 逻辑校验
        if enable_logic:
            logic_report = await self.logic_checker.check(persona, current)
            if not logic_report.passed and auto_fix:
                current = await self.logic_checker.fix(persona, current, logic_report)
            result.checks.append({"name": "逻辑校验", ...})

        # 汇总
        result.final_content = current
        result.passed = all(c.get("passed", True) for c in result.checks)
        scores = [c.get("score", 1.0) for c in result.checks]
        result.total_score = sum(scores) / len(scores) if scores else 1.0

        return result

流程图

content 输入
    │
    ▼
① 合规检测 ──→ 不通过?──→ auto_fix ──→ 替换为 *** ──→ recheck
    │ (current 可能已修改)
    ▼
② 去同质化 ──→ 不通过?──→ auto_fix ──→ LLM 改写
    │ (current 可能已修改)
    ▼
③ 口语化优化 ──→ 不通过?──→ auto_fix ──→ 规则替换
    │ (current 可能已修改)
    ▼
④ 逻辑校验 ──→ 不通过?──→ auto_fix ──→ LLM 修复
    │ (current 可能已修改)
    ▼
汇总:final_content = current, total_score = 平均分

关键设计:current 在维度间流转 — 每个维度修复后的内容传给下一个维度,不是各检各的。

为什么 current 要流转?
  ① 合规检测把"世界第一"替换为"***"
  ② 去同质化检查的是替换后的内容(不是原始内容)
  ③ 口语化优化的是去重后的内容
  ④ 逻辑校验的是口语化后的内容

如果各检各的(都检查原始内容),修复会冲突 — 合规检测删了"世界第一",但去同质化还在和包含"世界第一"的原文比相似度。流转 current 确保每个维度看到的是"前面维度修复后的最新版本"。

开关控制:按需启用

async def check_and_fix(self, persona, content, auto_fix=True,
                        existing_contents=None,
                        enable_dedup=True,        # ← 去重开关
                        enable_colloquial=True,   # ← 口语化开关
                        enable_logic=False):      # ← 逻辑校验开关

三个开关,灵活组合

场景dedupcolloquiallogic说明
快速生成FalseFalseFalse只做合规检测,最快
标准模式TrueTrueFalse合规+去重+口语化,默认
精品模式TrueTrueTrue全开,最慢但质量最高

未启用的维度记一条 skipped: True — 结果里有记录,知道哪些维度跑了哪些没跑:

else:
    result.checks.append({"name": "逻辑校验", "passed": True, "violations": [], "score": 1.0, "skipped": True})

八、质检分数与阈值

分数计算

每个维度有自己的扣分规则:

维度扣分规则每处扣分
合规检测1.0 - len(violations) * 0.10.1
去同质化1.0 - max_sim(相似度越高分越低)取决于相似度
口语化优化1.0 - len(violations) * 0.050.05
逻辑校验1.0 - len(issues) * 0.150.15

扣分力度反映问题严重性 — 逻辑问题最重(0.15),合规问题次之(0.1),口语化最轻(0.05)。

总分计算

# 汇总
result.passed = all(c.get("passed", True) for c in result.checks)
scores = [c.get("score", 1.0) for c in result.checks]
result.total_score = sum(scores) / len(scores) if scores else 1.0

两个判断标准

  1. passed:所有维度都通过才算通过(all)— 一票否决制
  2. total_score:所有维度分数的平均值
示例:
  合规检测:passed=True,  score=0.91处违规修复后通过)
  去同质化:passed=True,  score=0.85 (相似度 0.3,score=1.0-0.3*0.5=0.85)
  口语化优化:passed=False, score=0.853处书面语,score=1.0-3*0.05=0.85)
  逻辑校验:skipped,     score=1.0

  passed = all([True, True, False, True]) = False
  total_score = (0.9 + 0.85 + 0.85 + 1.0) / 4 = 0.9

阈值判断:在 pipeline 中

# pipeline/runner.py
quality_result = await self.quality_orchestrator.check_and_fix(...)
formatted = quality_result.final_content
quality_score = quality_result.total_score

# 构造 QualityReport
report = QualityReport(
    passed=quality_score >= 0.5,    # ← 阈值 0.5
    score=quality_score,
    details={"checks_count": len(quality_result.checks)},
)

最终判断用 quality_score >= 0.5 — 总分 0.5 以上才算通过。

分数区间判断行为
≥ 0.8优秀直接输出
0.5-0.8合格输出(但有改进空间)
< 0.5不合格当前仍输出,未来应拦截

配置化:QualityConfig

# config/models.py
class QualityConfig(BaseModel):
    sensitive_words_file: Optional[str] = None    # 敏感词文件路径
    auto_fix: bool = True                          # 是否自动修复
    dedup_enabled: bool = True                     # 去同质化开关
    dedup_threshold: float = 0.85                  # 去同质化相似度阈值
    colloquial_enabled: bool = True                # 口语化优化开关
    logic_check_enabled: bool = False              # 逻辑校验开关(默认关)

所有质检参数都在配置里 — 不用改代码就能调整阈值和开关。不同赛道可以配不同配置:

# 美妆赛道 — 严格模式
quality:
  auto_fix: true
  dedup_enabled: true
  dedup_threshold: 0.80        # ← 阈值低一点,允许更多相似
  colloquial_enabled: true
  logic_check_enabled: true     # ← 开启逻辑校验

# 职场赛道 — 快速模式
quality:
  auto_fix: true
  dedup_enabled: true
  dedup_threshold: 0.85
  colloquial_enabled: true
  logic_check_enabled: false    # ← 关闭逻辑校验,追求速度

九、质检与生成的闭环

当前:检完就输出

# pipeline/runner.py
# 2d: 质检
quality_result = await self.quality_orchestrator.check_and_fix(...)
formatted = quality_result.final_content
quality_score = quality_result.total_score

# 2e: 构建内容(无论质检是否通过,都存入)
content = GeneratedContent(
    ...
    quality_score=quality_score,
    quality_report=report,
)

# 2f: 存入内存 + 可选落盘
self.repo.store.save_content(content)

# 2g: 导出文件
await self._export(content, output_dir)

当前质检是"检完就输出" — 不管质检通不通过,文章都会保存和导出。质检分数只是记录,不影响输出。

当前流程:生成 → 质检 → 输出(质检结果只记录,不拦截)

未来:不过就改

理想流程:生成 → 质检 → 通过?→ 是:输出
                          → 否:根据违规项重新生成 → 质检 → ...(最多3轮)

未来的质检应该是闭环的 — 不通过就根据违规项重新生成,直到通过或达到最大重试次数。

# 未来的伪代码
for attempt in range(max_attempts):
    content = await generate(...)
    quality_result = await quality_orchestrator.check_and_fix(...)
    if quality_result.passed:
        break
    # 不通过,把违规项反馈给生成 Prompt
    feedback = format_violations(quality_result.checks)
    persona.extra_constraint = feedback  # 注入约束
else:
    logger.warning(f"质检未通过,已重试 {max_attempts} 次,强制输出")

但当前没做这个闭环 — 因为"不通过就重新生成"可能导致死循环(每次生成的都有问题),而且增加 LLM 调用开销。当前的设计是务实的:质检能修就修(auto_fix),修不了的就带着问题输出,用户自己决定要不要发。

踩坑总结

根因修复
敏感词库太小只内置了 10 个夸大宣传 + 4 个低俗模式支持从文件加载额外敏感词 sensitive_words_file
逻辑检查太严LLM 把正常创意判为"逻辑断层"逻辑校验默认关闭 logic_check_enabled=False,按需开启
阈值不适用所有赛道去重阈值 0.85 写死改为 QualityConfig.dedup_threshold 可配置
修复后没重新检查合规修复后直接进下一维度修复后 recheckreport = self.sensitive_filter.check(persona, current)
各维度检查原始内容current 没流转每个维度修复后更新 current,下一维度看到的是最新版本
口语化替换不全只覆盖 15 个书面语实际使用中积累,可扩展 STIFF_PATTERNS
去重 O(n) 太慢逐一比较所有已有内容当前 n-gram 够用,预留 FAISS 接口未来替换
质检不通过仍输出没有拦截逻辑当前设计:auto_fix 能修就修,修不了带问题输出,用户自决
逻辑校验 LLM 挂了LLM 调用失败降级:返回 score=0.8,不判不通过

经验总结

  1. 质检是 Agent 的"良知" — LLM 只管"写得流畅",质检管"写得对不对",两个独立环节各管一段
  2. 四维质检从硬到软 — 合规检测(正则,毫秒级)→ 去同质化(n-gram,毫秒级)→ 口语化(规则替换,毫秒级)→ 逻辑校验(LLM,秒级),先快后慢
  3. orchestrator 模式是关键 — 编排器依次调用四个检查器,current 在维度间流转,每个维度看到的是前面修复后的最新版本
  4. 修复策略因维度而异 — 合规检测替换为 ***(消除),去同质化 LLM 改写(增加原创性),口语化规则替换(精确),逻辑校验 LLM 修复(重写)
  5. 扣分力度反映严重性 — 逻辑问题 0.15 > 合规问题 0.1 > 口语化 0.05,总分 0.5 以上才算通过
  6. 开关+配置让质检可调enable_dedup/enable_colloquial/enable_logic 三个开关,dedup_threshold 可配置,不同赛道不同策略
  7. 当前质检是"检完就输出" — auto_fix 能修就修,修不了带问题输出,未来应实现"不过就重新生成"的闭环

下篇预告

下一篇讲 FastAPI + SPA:给Agent装上交互界面 — Agent 不能只有 CLI,Web 界面让非技术用户也能用。FastAPI 路由设计、AppState 单例、单文件 SPA、前端状态管理。