import type { CheckStatus, OptimizationFactCard, ImageInput, OptimizedArticle, PublishPlatform, QaCheck, QaReport, QualityRuleId, } from "../domain/types"; import { qaCheckSchema, qaReportSchema } from "../domain/validation"; import { generateValidatedJson, type GenerateInput } from "../llm/client"; import { QUALITY_INSPECTOR_SYSTEM_PROMPT, buildQualityInspectorPrompt, } from "../llm/prompts"; import { z } from "zod"; const REQUIRED_RULES: QualityRuleId[] = [ "industry_alignment", "image_text_match", "voice_consistency", "platform_fit", "company_name_integrity", "title_quality", "body_quality", "hallucination_risk", "claim_consistency", "context_sensitive_terms", ]; const HARD_FAILURE_RULES = new Set([ "company_name_integrity", "claim_consistency", ]); const llmQaPatchSchema = z.object({ checks: z.array(qaCheckSchema).default([]), }); export interface InspectQualityInput { article: OptimizedArticle; factCard: OptimizationFactCard; platform: PublishPlatform; sourceImages: ImageInput[]; rewriteRound?: number; onTraceEvent?: GenerateInput["onTraceEvent"]; onAuditSummary?: GenerateInput["onAuditSummary"]; } export function inspectQuality(input: InspectQualityInput): QaReport { const checks = REQUIRED_RULES.map((ruleId) => inspectRule(ruleId, input)); const overall_status: CheckStatus = checks.some((check) => check.status === "fail") ? "fail" : checks.some((check) => check.status === "warn") ? "warn" : "pass"; return qaReportSchema.parse({ overall_status, checks }); } export async function inspectQualityWithLlm( input: InspectQualityInput, ): Promise { const deterministicReport = inspectQuality(input); const llmPatch = await generateValidatedJson({ schema: llmQaPatchSchema, system: QUALITY_INSPECTOR_SYSTEM_PROMPT, prompt: buildQualityInspectorPrompt({ article: input.article, factCard: input.factCard, platform: input.platform, deterministicChecks: deterministicReport.checks, }), temperature: 0.1, task: "quality_inspector", schemaName: "llmQaPatchSchema", traceStage: "qa", rewriteRound: input.rewriteRound, onTraceEvent: input.onTraceEvent, onAuditSummary: input.onAuditSummary, }); const patchedChecks = deterministicReport.checks.map((deterministicCheck) => { const llmCheck = llmPatch.checks.find( (check) => check.rule_id === deterministicCheck.rule_id, ); if (!llmCheck) { return deterministicCheck; } if (deterministicCheck.status === "fail") { return deterministicCheck; } const status = llmCheck.status === "fail" && !HARD_FAILURE_RULES.has(deterministicCheck.rule_id) ? "warn" : llmCheck.status; return { ...deterministicCheck, status, evidence: llmCheck.evidence, reason: llmCheck.reason, suggested_fix: llmCheck.suggested_fix, target_agent: llmCheck.target_agent, }; }); const overall_status: CheckStatus = patchedChecks.some( (check) => check.status === "fail", ) ? "fail" : patchedChecks.some((check) => check.status === "warn") ? "warn" : "pass"; return qaReportSchema.parse({ overall_status, checks: patchedChecks }); } function inspectRule( ruleId: QualityRuleId, { article, factCard, platform, sourceImages }: InspectQualityInput, ): QaCheck { const combined = `${article.title}\n${article.summary}\n${article.body_markdown}`; const lower = combined.toLowerCase(); if (ruleId === "industry_alignment") { const targetIndustry = factCard.target_industry.trim().toLowerCase(); const aligned = targetIndustry.length === 0 || lower.includes(targetIndustry); return check( ruleId, aligned ? "pass" : "warn", aligned ? factCard.target_industry : article.summary, aligned ? "文章内容与事实卡确认的目标行业一致。" : "文章可能没有充分体现事实卡确认的目标行业。", "围绕事实卡确认的目标行业重写相关段落。", aligned ? null : "body", ); } if (ruleId === "image_text_match") { return check( ruleId, "pass", sourceImages.length > 0 ? "当前版本暂不评估图片内容。" : "当前版本未启用图片分析。", "当前版本仅优化文本,图片匹配检查暂不参与质量门禁。", "后续启用图片工作流后再补充图文匹配检查。", null, ); } if (ruleId === "voice_consistency") { const thirdPartyOfficial = platform === "official_site" && /\bthey\b|\btheir\b/i.test(combined); return check( ruleId, thirdPartyOfficial ? "warn" : "pass", thirdPartyOfficial ? "发现第三方代词。" : "表达视角符合平台要求。", thirdPartyOfficial ? "官网文章应避免疏离的第三方叙述口吻。" : "未发现明显的平台口吻不一致问题。", "改写为更适合官网的品牌表达。", thirdPartyOfficial ? "body" : null, ); } if (ruleId === "platform_fit") { return check( ruleId, article.summary.toLowerCase().includes(platform.replace(/_/g, " ")) ? "pass" : "warn", article.summary, "平台适配基于生成摘要与文章结构判断。", "复核文章是否符合目标平台的表达方式和结构。", null, ); } if (ruleId === "company_name_integrity") { const companyFullName = factCard.company_full_name.trim(); if (companyFullName.length === 0) { return check( ruleId, "warn", "事实卡尚未提供公司全称。", "无法执行公司全称一致性硬性检查,因为事实卡中的公司全称仍待确认。", "补充公司全称,或确认当前文案可以使用简称。", "fact_card", ); } const hasFullName = combined.includes(companyFullName); return check( ruleId, hasFullName ? "pass" : "fail", hasFullName ? companyFullName : article.body_markdown, hasFullName ? "文章中包含事实卡确认的公司全称。" : "文章缺少事实卡确认的公司全称,或使用了不完整简称。", "首次出现公司时使用事实卡确认的公司全称。", hasFullName ? null : "body", ); } if (ruleId === "title_quality") { const badTitle = /!!!|\?\?|keyword keyword/i.test(article.title); return check( ruleId, badTitle ? "fail" : "pass", article.title, badTitle ? "标题存在语义不顺或标点堆砌问题。" : "标题表达自然清晰。", "重写标题,提升语义清晰度和语法质量。", badTitle ? "title" : null, ); } if (ruleId === "body_quality") { const longSentence = combined.split(/[.!?。]/).some((part) => part.length > 220); return check( ruleId, longSentence ? "warn" : "pass", longSentence ? "发现过长句子。" : "正文结构可读。", longSentence ? "部分句子过长,影响阅读和理解。" : "未发现严重正文语法问题。", "拆分长句,并明确指代关系。", longSentence ? "body" : null, ); } if (ruleId === "hallucination_risk") { const unsupportedNumber = findUnsupportedNumbers(combined, factCard).length > 0; return check( ruleId, unsupportedNumber || article.requires_user_confirmation.length > 0 ? "warn" : "pass", unsupportedNumber ? findUnsupportedNumbers(combined, factCard).join(", ") : "未发现未确认的数字类事实主张。", unsupportedNumber ? "发现需要人工复核的数字类事实主张。" : "事实主张可以追溯到已确认事实卡。", "建议人工复核,必要时删除未确认主张,或补充到事实卡后再确认。", unsupportedNumber ? "body" : null, ); } if (ruleId === "claim_consistency") { const years = [...combined.matchAll(/\b(\d{1,3})\s*(?:years?|年)\b/gi)].map( (match) => Number(match[1]), ); const conflicts = years.filter((year) => year !== factCard.experience_years); return check( ruleId, conflicts.length > 0 ? "fail" : "pass", conflicts.length > 0 ? conflicts.join(", ") : "未发现冲突事实。", conflicts.length > 0 ? "经验年限与事实卡确认内容冲突。" : "重复出现的事实主张保持一致。", "将年限、产品和服务表述统一为事实卡确认内容。", conflicts.length > 0 ? "body" : null, ); } return check( ruleId, /\b(?:sensitive|forbidden)\b/i.test(combined) ? "warn" : "pass", "已完成语境敏感词检查。", "涉及敏感或禁用表达时,需要结合上下文判断。", "人工复核相关表述,避免机械删除导致语义错误。", null, ); } function check( rule_id: QualityRuleId, status: CheckStatus, evidence: string, reason: string, suggested_fix: string, target_agent: string | null, ): QaCheck { return { rule_id, status, evidence, reason, suggested_fix, target_agent }; } function findUnsupportedNumbers(text: string, factCard: OptimizationFactCard) { const allowed = new Set( [ factCard.experience_years, ...extractNumbersFromFactCard(factCard), ] .filter((value): value is number | string => typeof value === "number" || typeof value === "string", ) .map(String), ); return [...text.matchAll(/\b\d{1,4}\b/g)] .map((match) => match[0]) .filter((number) => !allowed.has(number)); } function extractNumbersFromFactCard(factCard: OptimizationFactCard) { return [ factCard.company_full_name, ...factCard.company_short_names, ...factCard.brand_names, ...factCard.product_names, factCard.target_industry, factCard.target_audience, ...factCard.core_claims, ...factCard.forbidden_claims, ...factCard.image_topics, ].flatMap((value) => [...value.matchAll(/\b\d{1,4}\b/g)].map((match) => match[0])); }