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

先说结论

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

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

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

质检的定位

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

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

质检报告:QualityReport

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

ini 复制代码
# 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="详细信息")

四个字段,统一接口

字段 类型 说明
passed bool 这维度过没过
violations liststr 具体违规了什么
score float 0.0-1.0 的分数
details dict 附加信息(如相似度数值)

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

二、四维质检架构

架构图

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

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

执行顺序

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

为什么是这个顺序?

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

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

每个维度的统一模式

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

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

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

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

四类检查

python 复制代码
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%有效"、"绝对"、"根治"
低俗内容 正则模式 "擦边"、"低俗"、"色情"、"裸露"

夸大宣传模式

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

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

人设配置控制

ini 复制代码
# 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 默认开启,但允许关闭(比如某些赛道需要"史上最强"这种夸张表达吸引眼球)。

自动修复:替换为 ***

csharp 复制代码
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

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

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

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

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

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

n-gram 余弦相似度

python 复制代码
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字符的片段,用向量夹角衡量两篇文章的相似度。

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

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

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

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

检查逻辑

ini 复制代码
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,就不通过。

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

修复:LLM 改写

python 复制代码
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 接口

python 复制代码
class DedupChecker:
    """基于文本相似度的去同质化检测

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

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

existing_contents 从哪来?

ini 复制代码
# 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 个书面语→口语化替换

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

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

检测与修复

python 复制代码
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 改写虽然更灵活,但可能"改过头" --- 把不该改的也改了。规则替换只动确定的书面语,其他一律不碰。

扣分比合规轻

ini 复制代码
# 合规检测:每处违规扣 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 驱动的检查器,检查文章的逻辑问题。

三个检查维度

python 复制代码
class LogicChecker:
    """LLM 驱动的逻辑校验

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

LLM 审查

swift 复制代码
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 过长。大多数逻辑问题在前半部分就能发现。

为什么默认关闭?

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

两个原因

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

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

降级:LLM 异常时跳过

python 复制代码
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:全链路质检

ini 复制代码
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

流程图

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

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

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

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

开关控制:按需启用

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

三个开关,灵活组合

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

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

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

八、质检分数与阈值

分数计算

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

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

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

总分计算

ini 复制代码
# 汇总
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:所有维度分数的平均值
ini 复制代码
示例:
  合规检测:passed=True,  score=0.9  (1处违规修复后通过)
  去同质化:passed=True,  score=0.85 (相似度 0.3,score=1.0-0.3*0.5=0.85)
  口语化优化:passed=False, score=0.85 (3处书面语,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 中

ini 复制代码
# 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

ini 复制代码
# 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              # 逻辑校验开关(默认关)

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

yaml 复制代码
# 美妆赛道 --- 严格模式
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    # ← 关闭逻辑校验,追求速度

九、质检与生成的闭环

当前:检完就输出

ini 复制代码
# 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)

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

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

未来:不过就改

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

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

ini 复制代码
# 未来的伪代码
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、前端状态管理。

相关推荐
Aloudata1 小时前
企业级 AI 问数安全指南:如何兼顾可用性、权限与审计?
大数据·人工智能·数据分析·agent·语义编织
zzz海羊1 小时前
2026全平台移动办公远控助手横测:从鸿蒙到工作站,ToDesk、向日葵、TeamViewer、AnyDesk谁更适配?
人工智能·华为·agent·harmonyos·teamviewer
leeyi2 小时前
Eino Callbacks 回调机制:在 Agent 执行每个环节注入自定义逻辑(第95篇-E81)
aigc·agent·ai编程
YakProject2 小时前
如何让 AI Agent 跑得更快???
人工智能·测试工具·网络安全·agent·yakit·yaklang
怕浪猫2 小时前
深入 Cordis 核心:这个插件框架比 Webpack Tapable 还好用
aigc·agent·ai编程
sg_knight3 小时前
openCode 安装与初始化配置(Windows / macOS / Linux)
linux·windows·macos·llm·agent·ai编程·opencode
Shawn_Shawn3 小时前
本体论的基本核心概念
后端·llm·领域驱动设计
七牛云行业应用5 小时前
Cursor 如何接入自定义模型:Override Base URL 完整配置与四类踩坑速查
人工智能·agent·ai编程