先说结论
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.py 的 SensitiveFilter --- 最硬的检查,不合规的内容绝对不能发。
四类检查
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_exaggeration 和 no_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.py 的 DedupChecker --- 防止 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.py 的 ColloquialOptimizer --- 消除 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 做口语化?
规则替换有三个优势:
- 快 --- 正则匹配,毫秒级,不需要调 LLM
- 准 --- 精确替换,不会改错
- 可控 --- 替换规则明确,不会引入新问题
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.py 的 LogicChecker --- 唯一一个 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,默认关闭)
两个原因:
- 开销大 --- 每次质检多一次 LLM 调用,生成一篇多花 2-5 秒
- 误杀风险 --- 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.py 的 QualityOrchestrator --- 把四个检查器串起来,统一调度。
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
两个判断标准:
passed:所有维度都通过才算通过(all)--- 一票否决制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 可配置 |
| 修复后没重新检查 | 合规修复后直接进下一维度 | 修复后 recheck:report = self.sensitive_filter.check(persona, current) |
| 各维度检查原始内容 | current 没流转 | 每个维度修复后更新 current,下一维度看到的是最新版本 |
| 口语化替换不全 | 只覆盖 15 个书面语 | 实际使用中积累,可扩展 STIFF_PATTERNS |
| 去重 O(n) 太慢 | 逐一比较所有已有内容 | 当前 n-gram 够用,预留 FAISS 接口未来替换 |
| 质检不通过仍输出 | 没有拦截逻辑 | 当前设计:auto_fix 能修就修,修不了带问题输出,用户自决 |
| 逻辑校验 LLM 挂了 | LLM 调用失败 | 降级:返回 score=0.8,不判不通过 |
经验总结
- 质检是 Agent 的"良知" --- LLM 只管"写得流畅",质检管"写得对不对",两个独立环节各管一段
- 四维质检从硬到软 --- 合规检测(正则,毫秒级)→ 去同质化(n-gram,毫秒级)→ 口语化(规则替换,毫秒级)→ 逻辑校验(LLM,秒级),先快后慢
- orchestrator 模式是关键 --- 编排器依次调用四个检查器,
current在维度间流转,每个维度看到的是前面修复后的最新版本 - 修复策略因维度而异 --- 合规检测替换为
***(消除),去同质化 LLM 改写(增加原创性),口语化规则替换(精确),逻辑校验 LLM 修复(重写) - 扣分力度反映严重性 --- 逻辑问题 0.15 > 合规问题 0.1 > 口语化 0.05,总分 0.5 以上才算通过
- 开关+配置让质检可调 ---
enable_dedup/enable_colloquial/enable_logic三个开关,dedup_threshold可配置,不同赛道不同策略 - 当前质检是"检完就输出" --- auto_fix 能修就修,修不了带问题输出,未来应实现"不过就重新生成"的闭环
下篇预告
下一篇讲 FastAPI + SPA:给Agent装上交互界面 --- Agent 不能只有 CLI,Web 界面让非技术用户也能用。FastAPI 路由设计、AppState 单例、单文件 SPA、前端状态管理。