创意评估的困境

评估代码生成的正确性很简单——跑测试就行。评估事实问答的准确性也不难——对比标准答案。但当你让 LLM 写一首诗、创作一个短篇故事、或生成一个创意广告时,“好不好” suddenly becomes a very hard question。

创意评估面临三个核心挑战:

  1. 主观性:同一篇作品,不同读者评价可能天差地别
  2. 多维性:创意好不好涉及语言、结构、新颖度、情感共鸣等多个维度
  3. 缺乏标准答案:创意任务没有唯一正确答案,甚至没有"参考答案"

评估方法分类

创意评估方法
├── 人工评估
│   ├── 整体评分(Holistic Scoring)
│   ├── 多维 Rubric 评分
│   └── 排序比较(Pairwise Ranking)
├── 自动化指标
│   ├── 多样性指标(Distinct-n, Self-BLEU)
│   ├── 新颖度指标(Semantic Novelty)
│   └── 连贯性指标(Coherence Score)
└── LLM-as-Judge
    ├── 单维度评分
    ├── 多维度 Rubric
    └── 对比较(Pairwise Comparison)

一、人工评估体系

整体评分法

最直接的方式:让评分者对作品给出一个综合分数(1-5 分或 1-10 分)。

class HolisticScoring:
    """整体评分体系"""
    
    SCALE_5 = {
        5: "杰出 — 创意独特,语言精炼,情感打动人心",
        4: "优秀 — 有明确创意点,语言流畅,有一定感染力",
        3: "合格 — 表达清晰,但创意普通,缺乏亮点",
        2: "较差 — 表达生硬,创意陈旧,难以读完",
        1: "不合格 — 逻辑混乱,语言不通,无法理解"
    }

    def __init__(self, num_raters: int = 3):
        self.num_raters = num_raters

    def aggregate(self, scores: list[int]) -> dict:
        import numpy as np
        scores = sorted(scores)
        return {
            "mean": np.mean(scores),
            "median": np.median(scores),
            "std": np.std(scores),
            "min": min(scores),
            "max": max(scores),
            "agreement": self._inter_rater_agreement(scores),
        }

    def _inter_rater_agreement(self, scores: list[int]) -> float:
        """计算评分者间一致性"""
        if len(scores) < 2:
            return 1.0
        mean = sum(scores) / len(scores)
        variance = sum((s - mean) ** 2 for s in scores) / len(scores)
        max_variance = (max(scores) - min(scores)) ** 2 / 4
        return 1 - (variance / max_variance) if max_variance > 0 else 1.0

多维 Rubric 评分

整体评分太粗,更精细的方法是定义多个评分维度:

维度权重评分标准(5分制)
创意性30%5=前所未见的创意;3=有一定新意;1=完全老套
语言质量20%5=语言优美精准;3=表达清晰;1=语病频出
结构组织20%5=结构精巧;3=结构合理;1=结构混乱
情感共鸣15%5=强烈感染力;3=有一定感受;1=无感
主题契合15%5=完美切题;3=基本相关;1=偏题
RUBRIC = {
    "creativity": {
        "weight": 0.30,
        "descriptors": {
            5: "创意独特,角度新颖,令人惊喜",
            4: "有明显创意点,但非全新角度",
            3: "有一定新意,但整体较常见",
            2: "创意薄弱,多为套路化表达",
            1: "完全老套,毫无新意",
        }
    },
    "language": {
        "weight": 0.20,
        "descriptors": {
            5: "语言精准优美,用词生动,节奏感强",
            4: "语言流畅,用词得当",
            3: "表达清晰,但语言平淡",
            2: "偶有语病,表达不够准确",
            1: "语病频出,难以理解",
        }
    },
    "structure": {
        "weight": 0.20,
        "descriptors": {
            5: "结构精巧,层次分明,起承转合自然",
            4: "结构合理,层次清晰",
            3: "有基本结构,但部分松散",
            2: "结构不够清晰,逻辑跳跃",
            1: "结构混乱,无法跟随",
        }
    },
    "emotion": {
        "weight": 0.15,
        "descriptors": {
            5: "情感真挚,强烈感染力,引发共鸣",
            4: "有情感表达,有一定感染力",
            3: "情感表达一般,偶有触动",
            2: "情感淡薄,难以引起共鸣",
            1: "毫无情感,或情感虚假",
        }
    },
    "relevance": {
        "weight": 0.15,
        "descriptors": {
            5: "完美切题,深度回应主题",
            4: "紧扣主题,有适度延伸",
            3: "基本相关,但略有偏移",
            2: "与主题关联较弱",
            1: "严重偏题",
        }
    }
}

class RubricScorer:
    def __init__(self, rubric: dict):
        self.rubric = rubric

    def compute_weighted_score(self, dimension_scores: dict[str, int]) -> dict:
        total = 0.0
        breakdown = {}
        for dim, score in dimension_scores.items():
            weight = self.rubric[dim]["weight"]
            weighted = score * weight
            total += weighted
            breakdown[dim] = {
                "raw_score": score,
                "weight": weight,
                "weighted_score": round(weighted, 2),
            }
        return {
            "total_score": round(total, 2),
            "breakdown": breakdown,
            "grade": self._score_to_grade(total),
        }

    def _score_to_grade(self, score: float) -> str:
        if score >= 4.5: return "S"
        elif score >= 4.0: return "A"
        elif score >= 3.5: return "B"
        elif score >= 3.0: return "C"
        elif score >= 2.0: return "D"
        else: return "F"

排序比较法

相比绝对评分,人类更擅长相对比较。排序比较法让评分者两两比较作品优劣:

from itertools import combinations
import numpy as np

class PairwiseRanker:
    """基于两两比较的排序系统"""
    
    def __init__(self):
        self.comparisons = []

    def add_comparison(self, winner_id: str, loser_id: str, confidence: float = 1.0):
        self.comparisons.append({
            "winner": winner_id,
            "loser": loser_id,
            "confidence": confidence,
        })

    def compute_rankings(self, item_ids: list[str]) -> list[dict]:
        """使用 Bradley-Terry 模型计算排名"""
        # 简化的 Elo 评分
        elo = {item_id: 1000 for item_id in item_ids}
        K = 32
        for comp in self.comparisons:
            w, l = comp["winner"], comp["loser"]
            expected_w = 1 / (1 + 10 ** ((elo[l] - elo[w]) / 400))
            elo[w] += K * (1 - expected_w) * comp["confidence"]
            elo[l] -= K * (1 - expected_w) * comp["confidence"]
        
        ranked = sorted(item_ids, key=lambda x: elo[x], reverse=True)
        return [
            {"rank": i + 1, "item_id": item, "elo": round(elo[item])}
            for i, item in enumerate(ranked)
        ]

二、自动化创意指标

多样性指标

多样性衡量生成文本的丰富程度,避免模型总是输出相似的套路。

from collections import Counter
from typing import List

class DiversityMetrics:
    """文本多样性指标集合"""
    
    @staticmethod
    def distinct_n(text: str, n: int = 2) -> float:
        """Distinct-N: 唯一 n-gram 占比"""
        tokens = text.split()
        if len(tokens) < n:
            return 0.0
        ngrams = [tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)]
        unique = len(set(ngrams))
        total = len(ngrams)
        return unique / total if total > 0 else 0.0

    @staticmethod
    def self_bleu(texts: List[str], n: int = 4) -> float:
        """Self-BLEU: 越低表示多样性越高"""
        from nltk.translate.bleu_score import sentence_bleu, SmoothingFunction
        smoothie = SmoothingFunction().method1
        scores = []
        for i, ref in enumerate(texts):
            references = [t.split() for j, t in enumerate(texts) if j != i]
            candidate = ref.split()
            if len(candidate) < n or any(len(r) < n for r in references):
                continue
            score = sentence_bleu(references, candidate,
                                  weights=tuple(1/n for _ in range(n)),
                                  smoothing_function=smoothie)
            scores.append(score)
        return sum(scores) / len(scores) if scores else 0.0

    @staticmethod
    def entropy_n(text: str, n: int = 2) -> float:
        """n-gram 熵:越高表示越多样"""
        import math
        tokens = text.split()
        if len(tokens) < n:
            return 0.0
        ngrams = [tuple(tokens[i:i+n]) for i in range(len(tokens) - n + 1)]
        counts = Counter(ngrams)
        total = len(ngrams)
        entropy = 0.0
        for count in counts.values():
            p = count / total
            entropy -= p * math.log2(p)
        return entropy

    @staticmethod
    def semantic_diversity(texts: List[str], model_name: str = "all-MiniLM-L6-v2") -> float:
        """语义多样性:基于嵌入的余弦距离矩阵均值"""
        from sentence_transformers import SentenceTransformer
        import numpy as np
        model = SentenceTransformer(model_name)
        embeddings = model.encode(texts)
        # 计算两两余弦距离
        normalized = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
        sim_matrix = normalized @ normalized.T
        n = len(texts)
        # 取上三角(不含对角线)
        upper_tri = sim_matrix[np.triu_indices(n, k=1)]
        avg_similarity = np.mean(upper_tri)
        return 1 - avg_similarity  # 多样性 = 1 - 相似性

新颖度指标

class NoveltyMetrics:
    """创意新颖度评估"""
    
    def __init__(self, reference_corpus: list[str]):
        """用参考语料库定义"已知创意空间" """
        self.reference_embeddings = None
        self.reference_corpus = reference_corpus

    def _ensure_embeddings(self, model_name="all-MiniLM-L6-v2"):
        if self.reference_embeddings is None:
            from sentence_transformers import SentenceTransformer
            import numpy as np
            model = SentenceTransformer(model_name)
            self.reference_embeddings = model.encode(self.reference_corpus)
            self.model = model

    def novelty_score(self, text: str) -> float:
        """
        新颖度 = 与参考语料库的最大相似度的补数
        1.0 = 完全新颖,0.0 = 与已有作品高度相似
        """
        import numpy as np
        self._ensure_embeddings()
        text_emb = self.model.encode([text])
        similarities = np.dot(self.reference_embeddings, text_emb.T).flatten()
        max_sim = np.max(similarities)
        return float(1 - max_sim)

    def novelty_percentile(self, text: str) -> float:
        """新颖度百分位:比多少比例的参考作品更新颖"""
        import numpy as np
        self._ensure_embeddings()
        text_emb = self.model.encode([text])
        ref_sims = np.dot(self.reference_embeddings, text_emb.T).flatten()
        text_novelty = 1 - np.max(ref_sims)
        ref_novelties = []
        for i in range(len(self.reference_embeddings)):
            others = np.delete(self.reference_embeddings, i, axis=0)
            sim = np.dot(others, self.reference_embeddings[i])
            ref_novelties.append(1 - np.max(sim))
        return float(np.mean(np.array(ref_novelties) < text_novelty))

三、LLM-as-Judge 方法

单维度 LLM 评分

用强力 LLM 作为评委,对特定维度打分:

class LLMCreativeJudge:
    """使用 LLM 进行创意评估"""
    
    def __init__(self, judge_model: str = "gpt-4o"):
        self.judge_model = judge_model

    def score_creativity(self, prompt: str, response: str) -> dict:
        judge_prompt = f"""你是一位创意写作评审专家。请对以下 AI 生成内容的【创意性】打分。

原始题目:{prompt}
生成内容:{response}

评分标准(0-10分):
- 9-10: 创意极为独特,角度前所未见,令人惊喜
- 7-8: 创意明显,有新颖角度
- 5-6: 有一定新意,但整体较常见
- 3-4: 创意薄弱,多为套路
- 0-2: 完全老套,毫无创意

请输出 JSON 格式:
{{"score": <0-10>, "reasoning": "<50字以内理由>", "highlights": ["<亮点1>", "<亮点2>"]}}"""

        result = self._call_judge(judge_prompt)
        return result

    def score_multi_dimension(self, prompt: str, response: str) -> dict:
        judge_prompt = f"""你是一位创意写作评审专家。请对以下 AI 生成内容进行多维度评分。

原始题目:{prompt}
生成内容:{response}

请对以下 5 个维度各打 0-10 分:
1. creativity(创意性):角度是否新颖独特
2. language(语言质量):用词是否精准优美
3. structure(结构组织):篇章布局是否精巧
4. emotion(情感共鸣):是否有感染力
5. relevance(主题契合):是否深度回应题目

输出 JSON:
{{
  "creativity": {{"score": <0-10>, "note": "<简评>"}},
  "language": {{"score": <0-10>, "note": "<简评>"}},
  "structure": {{"score": <0-10>, "note": "<简评>"}},
  "emotion": {{"score": <0-10>, "note": "<简评>"}},
  "relevance": {{"score": <0-10>, "note": "<简评>"}},
  "overall": {{"score": <0-10>, "summary": "<总体评价>"}}
}}"""

        return self._call_judge(judge_prompt)

    def pairwise_compare(self, prompt: str, response_a: str, response_b: str) -> dict:
        judge_prompt = f"""你是创意写作评审专家。请比较以下两个 AI 生成内容哪个更好。

题目:{prompt}

内容 A:{response_a}
内容 B:{response_b}

评判维度:创意性、语言、结构、情感、主题契合。

输出 JSON:
{{
  "winner": "A" | "B" | "tie",
  "confidence": <0-1>,
  "reasoning": "<100字以内理由>"
}}"""

        return self._call_judge(judge_prompt)

    def _call_judge(self, prompt: str) -> dict:
        # 实际实现调用 LLM API
        import json
        raw = call_llm(self.judge_model, prompt)
        try:
            return json.loads(raw)
        except json.JSONDecodeError:
            # 尝试提取 JSON
            import re
            match = re.search(r'\{.*\}', raw, re.DOTALL)
            if match:
                return json.loads(match.group())
            return {"error": "Failed to parse judge output", "raw": raw}

LLM-as-Judge 的偏差与缓解

偏差类型描述缓解策略
位置偏差倾向选择先出现的选项随机化 A/B 顺序,取平均
冗长偏差倾向更长的回答在 Prompt 中强调关注质量而非长度
自我偏好倾向同族模型使用不同厂商模型交叉评估
分数膨胀给分偏高加入锚定样本校准
class BiasMitigatedJudge:
    """带偏差缓解的 LLM Judge"""
    
    def __init__(self, judge_models: list[str]):
        self.judges = judge_models  # 多个不同模型交叉评估

    def evaluate(self, prompt: str, responses: list[str]) -> dict:
        results = []
        for judge in self.judges:
            # 每个 judge 看到不同顺序的 responses
            import random
            ordered = list(enumerate(responses))
            random.shuffle(ordered)
            
            judge_result = self._evaluate_with_judge(judge, prompt, ordered)
            results.append(judge_result)
        
        # 聚合多个 judge 的结果
        return self._aggregate(results)

    def _aggregate(self, results: list[dict]) -> dict:
        """多 judge 结果聚合"""
        from collections import defaultdict
        scores = defaultdict(list)
        for r in results:
            for resp_id, score in r["scores"].items():
                scores[resp_id].append(score)
        
        final = {}
        for resp_id, score_list in scores.items():
            final[resp_id] = {
                "mean_score": sum(score_list) / len(score_list),
                "std": (sum((s - sum(score_list)/len(score_list))**2 for s in score_list) / len(score_list)) ** 0.5,
                "individual_scores": score_list,
            }
        return final

四、综合评估框架

将以上方法组合成一个端到端的创意评估框架:

class CreativeEvalFramework:
    """端到端创意评估框架"""
    
    def __init__(self, config: dict):
        self.human_rubric = RubricScorer(RUBRIC)
        self.diversity = DiversityMetrics()
        self.novelty = NoveltyMetrics(config["reference_corpus"])
        self.llm_judge = LLMCreativeJudge(config["judge_model"])
        
    def evaluate(self, prompt: str, response: str, 
                 human_scores: dict = None) -> dict:
        results = {}
        
        # 1. 自动化指标
        results["diversity"] = {
            "distinct_1": self.diversity.distinct_n(response, 1),
            "distinct_2": self.diversity.distinct_n(response, 2),
            "entropy_2": self.diversity.entropy_n(response, 2),
        }
        results["novelty"] = {
            "score": self.novelty.novelty_score(response),
            "percentile": self.novelty.novelty_percentile(response),
        }
        
        # 2. LLM-as-Judge 多维评分
        results["llm_judge"] = self.llm_judge.score_multi_dimension(prompt, response)
        
        # 3. 人工评分(如有)
        if human_scores:
            results["human"] = self.human_rubric.compute_weighted_score(human_scores)
        
        # 4. 综合分数
        results["composite"] = self._compute_composite(results)
        return results

    def _compute_composite(self, results: dict) -> float:
        """计算综合创意分数"""
        weights = {
            "novelty": 0.25,
            "llm_creativity": 0.25,
            "llm_language": 0.15,
            "llm_structure": 0.10,
            "llm_emotion": 0.15,
            "llm_relevance": 0.10,
        }
        score = 0
        score += results["novelty"]["score"] * weights["novelty"] * 10  # 归一化到 0-10
        llm = results["llm_judge"]
        for dim, w in [("creativity", 0.25), ("language", 0.15), 
                       ("structure", 0.10), ("emotion", 0.15), ("relevance", 0.10)]:
            if dim in llm:
                score += llm[dim]["score"] * w
        return round(score, 2)

评估方法对比总表

方法客观性可扩展性成本相关性推荐场景
人工整体评分小规模最终验证
人工 Rubric很高深入分析
排序比较模型间对比
Distinct-N极低多样性监控
新颖度防止套路化
LLM 单维评分中高大规模初筛
LLM 多维评分日常评估主力
LLM 对比较中高模型选型
综合框架中高很高正式评估

结语

创意评估没有银弹。最佳实践是分层评估:用自动化指标做大规模初筛,用 LLM-as-Judge 做日常评估主力,用人工 Rubric 做小规模深度验证。随着评估方法的成熟,我们正在从"我觉得不错"走向"创意性 7.3 分,新颖度 82 百分位"的量化时代。但也要记住:量化是为了辅助判断,而不是替代判断。最终的创意价值,永远需要人类读者的真实感受来确认。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。