基于 agno 的 Inter-Annotator Agreement 实践:用纯标准库实现评估 LLM 标注与陪审团投票的一致性
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
本文基于 agno 数据标注系列 cookbook 中的_19_inter_annotator_agreement模块(含 TEST_LOG.md 的实测记录、README.md 的用法说明及 basic.py、jury_votes.py 两个可运行示例),讲解如何衡量多个标注者(模型、指令框架或人类)对同一批样本产出标签的一致程度。读完本文,你将掌握 raw agreement、Cohen's kappa、Fleiss' kappa、Krippendorff's alpha 四种一致性指标的纯标准库实现与公式细节,理解如何在 agno 中以同一基础模型的三种指令框架模拟独立标注者、构建 item × rater 矩阵、处理缺失单元格,以及如何在标签分布倾斜时识别"被原始一致性掩盖"的陪审团投票信号。
为什么需要一致性指标:原始一致性会"吹捧"流水线
任何标注流水线的标准可靠性检查,都是检验独立标注者能否复现彼此的标签。在本模块中,"标注者"并非多个不同模型,而是同一个判断模型在不同指令框架(instruction framings)下以 temperature=0 运行。这一设计非常关键:既然采样噪声被 temperature=0 消除,那么每一次分歧都只能追溯到指南措辞(guideline wording)本身,而不是随机性。正如 README.md 所指出的,仅看 raw agreement 会美化一条标注流水线,只有经过机会校正(chance-corrected)的指标才能告诉你指南是否真正可复现。
该模块的适用场景(来自 README 的 "When to use"):
- 审计一次指南重写是否真的改变了标签结果;
- 决定单一模型的标注器是否足够可靠、能否独立运行;
- 审查陪审团投票过滤器(jury-vote filters):当标签分布倾斜时,高 raw agreement 阈值可能放行的只是大量偶然一致;而一个永远投多数标签的陪审员,即使与其他人有很高的原始一致性,其机会校正后的一致性也可能为零。
四种一致性指标的纯标准库实现与公式
basic.py与jury_votes.py都以Matrix = list[list[Optional[str]]]表示item(行)× rater(列)矩阵,单元格None表示缺失评分。四个指标全部用纯标准库实现(仅依赖itertools、collections、typing),并把公式以注释形式写在函数内,便于核对与教学。以下实现细节均可在 basic.py 中直接阅读。
raw_agreement —— 原始一致性
对每个 item,统计评了同一标签的 rater 对占所有 rater 对的比例,再对所有 item 取平均:
P_o = (1/N) * sum_i [ agreeing_pairs_i / total_pairs_i ]少于两条评分的 item 被跳过。它不包含任何机会校正,因此在标签倾斜场景下会给出虚高的数值。
Cohen's kappa —— 两两比较的机会校正
适用于两两 rater 比较,仅统计两者都评过的 item:
p_o = 标签完全一致的 item 占比 p_e = sum_k p_a(k) * p_b(k) (两个 rater 各自边际分布的乘积和) kappa = (p_o - p_e) / (1 - p_e)在basic.py主流程中,三个 rater 两两组合,产出cohen_kappa_terse_vs_rubric、cohen_kappa_terse_vs_persona、cohen_kappa_rubric_vs_persona三个成对指标。
Fleiss' kappa —— 多 rater 的机会校正
适用于每个 item 的 rater 数相等且无缺失单元格的矩阵:
n_ik = 类别 k 被分配给 item i 的 rater 数,n = 每 item 的 rater 数,N = item 数 P_i = (sum_k n_ik^2 - n) / (n * (n - 1)) 逐 item 一致性 P_bar = (1/N) * sum_i P_i 观测一致性 p_k = sum_i n_ik / (N * n) 类别占比 P_e = sum_k p_k^2 机会一致性 kappa = (P_bar - P_e) / (1 - P_e)实现中若检测到任何缺失单元格,会直接抛出ValueError("fleiss_kappa requires complete rows (no missing cells)")。
Krippendorff's alpha(nominal)—— 原生支持缺失单元格
这是四个指标中唯一为缺失单元格提供原生处理的多 rater 机会校正指标,对 nomin 类数据(delta = 1 when c != k, else 0)按以下步骤计算:
- 只保留含 ≥ 2 个可配对(非缺失)值的 item;
- 构建共现矩阵:单元内有 m_u 个值,来自不同 rater 的每个有序值对 (c, k) 贡献
1/(m_u - 1)到 o_ck; - 边际:n_c = sum_k o_ck,n = sum_c n_c;
- 名义不一致:D_o = sum_{c != k} o_ck,期望不一致 D_e = sum_{c != k} n_c * n_k / (n - 1),最终 alpha = 1 - D_o / D_e。
缺失单元格不贡献任何配对,因此天然被正确处理;而 Fleiss' kappa 没有这种机制。
实验一:basic.py —— 三种情感指南框架标注 12 段文本
实验设计
basic.py用同一个Gemini(id="gemini-3.5-flash", temperature=0)基础模型,以三种真实不同的指南措辞(代码见 basic.py)标注 12 段商品评论文本:
| 框架 | 指令要点 |
|---|---|
| terse | 一句话:Label the sentiment of the text: positive, negative, or neutral. |
| rubric | 详细规则:讽刺/反语按真实意图标注;混合情绪因正负相抵标 neutral;微弱或高度修饰的表扬(如 "fine, I guess")标 neutral;纯事实陈述标 neutral |
| persona | 购物者视角:会推动购买标 positive,让消费者犹豫标 negative,无购买信号(如纯事实)标 neutral |
所有 Agent 均使用output_schema=SentimentLabel输出Literal["positive", "negative", "neutral"]的 Pydantic 结构化结果,并在label_text中最多重试 3 次解析 schema 失败,绝不强转(basic.py)。
12 条 item 中,8 条为清晰的正/负/中性样本,4 条被特意设计为真正模糊(basic.py):讽刺(字面正向、意图负向的 "Oh great, another update...")、均衡混合情绪("Gorgeous screen and superb speakers, but...")、微弱表扬("better than I expected. I would not buy it again...")以及作者本人摇摆不定的矛盾文本("Some days I love it, some days I want to throw it out the window.")。
自检:先于任何模型调用验证指标实现
self_check()在任何模型调用之前用两组手算案例断言四个实现(basic.py):
- 完全一致矩阵(mixed categories):四个指标全部为 1.0;
- 2 rater × 4 item 手工推导矩阵:rater A 为
pos pos neg neg,rater B 为pos neg neg pos。raw agreement 2/4 = 0.5;Cohen 因双方边际均为 0.5/0.5,kappa = 0.0;Fleiss 同样为 0.0;Krippendorff 共现矩阵推导得 alpha = 1 - 4/(32/7) = 0.125。
TEST_LOG.md 记录:self_check passed,即实现与手算值全部吻合,通过后才发起模型请求。
实测结果(TEST_LOG.md 记录,2026-07-18,agno 2.7.4)
本次运行指标:
raw_agreement 0.833 fleiss_kappa 0.742 krippendorff_alpha 0.749 cohen_kappa terse_vs_rubric 0.874 terse_vs_persona 0.739 rubric_vs_persona 0.629关键观察:
- 恰好 3 个被设计为模糊的 item 产生分歧并被路由到 review 列表,其余 9 条一致:
- 混合情绪项:terse=negative / rubric=neutral / persona=negative;
- 微弱表扬与矛盾文本项:terse=neutral / rubric=neutral / persona=negative;
- 讽刺项:三个框架一致判 negative(说明 rubric 的"按真实意图标注"规则与 persona 的购物者底线判断一致)。
- 最终汇总为
12 items x 3 raters: 9 unanimous, 3 routed to review。 - 两次运行(temperature=0)输出完全一致;但 TEST_LOG.md 同时提醒,标签原则上仍可能随模型更新而漂移。
实验二:jury_votes.py —— 对 DPO 偏好对做陪审团投票一致性分析
实验设计
jury_votes.py将同一套指标应用到dpo_jury 形状的偏好投票上:三个 juror 框架(terse、反冗长 rubric、助教 persona)对 8 对答案投a/b/tie(jury_votes.py)。8 对答案被刻意设计为倾斜:其中 6 对明显是 "a" 更好(如 fib、sum35、round、tcp、reset、sky),2 对设计为"接近"的简明 vs 教学型对比(http404、float),此时 rubric 的"永远选更短"规则与 persona 的"永远选更有教学价值"规则指向相反方向(jury_votes.py)。
此外,persona juror 在 fib 对上确定性弃权(recusal)——这是对 dpo_jury 中"自偏好弃权"(模型家族不评自己族生成的答案)的模拟(dpo_jury.py 中真实实现为if family == ex["source_family"]: recuse)。三个框架共享同一基础模型,因此这里通过RECUSALS: set = {("persona", "fib")}硬编码一次弃权,在投票矩阵中留下一个缺失单元格。
自检:含缺失单元格与低于机会一致性案例
self_check()除完全一致矩阵外,还手算断言了两个关键案例(jury_votes.py):
- 含缺失单元格的 3 juror × 3 item 矩阵:
[a, a, None] / [a, b, b] / [b, b, b]。raw agreement = 7/9;Krippendorff's alpha = 8/15(缺失单元格不贡献配对);Fleiss 仅在完整行子集(item 2、3)上计算,得 kappa = -0.2,即低于机会一致性的案例。 - 这直接验证了 "alpha 原生处理缺失、Fleiss 只算完整行" 的差异。
实测结果(TEST_LOG.md 记录)
本次运行:投票分布{'a': 19, 'b': 4},即83% 的投票是 'a'(倾斜正是实验目的)。核心结果:
- 6 对倾斜对上 raw agreement = 1.000;
- 2 对接近对上 terse 与 persona 投 b、rubric 投 a,使整体 raw agreement = 0.833;
- 但机会校正后急剧回落:krippendorff alpha = 0.421,fleiss kappa = 0.382(在 7/8 完整行上计算)——输出行醒目地指出 "0.833 collapsing to 0.421 under 83% skew";
- 成对 Cohen:terse_vs_persona = 1.000,terse_vs_rubric = 0.000,rubric_vs_persona = 0.000。rubric juror 在全部 8 对上都投 'a',其退化的一致性边际(全多数标签)使得 kappa 在与 terse 有 75% raw agreement 的情况下恰好为 0;
- 最终汇总:
8 pairs x 3 jurors: 23 votes cast, 1 recused, 6 pairs unanimous among sitting jurors;两次运行输出一致。
这正是 README 强调的教训:在 83% 的标签倾斜下,raw agreement 0.833 一旦移除多数标签上的机会一致性,就坍缩为 alpha 0.421;一个永远投多数标签的 juror 可以同时拥有高 raw agreement 与零机会校正一致性。因此 jury_votes.py 结尾建议:通过agreement >= 0.75过滤的陪审团,其超机会信号可能远低于原始数值所暗示的水平,应把 alpha 与 raw agreement 并列报告(jury_votes.py)。
与相邻 cookbook 的关系:从投票生成到裁决闭环
该模块不是孤立存在的,它位于 agno 数据标注体系(cookbook/data_labeling)中,与上下游用例衔接:
- 偏好投票的产生:本模块评测的 dpo_jury 形状投票,来自 cookbook/data_labeling/_05_text_pairwise_preference 的
dpo_jury.py模式——5 个模型家族(OpenAI、Anthropic、Google、Groq、Mistral)构成陪审团,带置信度、双向交换消解顺序敏感、自偏好弃权与 gold pair 校准; - 分歧的处理:被路由到 review 的 item,可按 cookbook/data_labeling/_18_quality_review 的 labeler → reviewer → adjudicator 工作流进行裁决——两个不同 provider 的 labeler 并行独立抽取、reviewer 逐字段 diff、分歧时由 Condition 步触发 adjudicator;
- 单一评判模型的打分:这些指标所压力测试的 single-judge 打分范式,见 cookbook/data_labeling/_17_llm_as_judge。
运行方式与前置条件
python cookbook/data_labeling/_19_inter_annotator_agreement/basic.py python cookbook/data_labeling/_19_inter_annotator_agreement/jury_votes.py需要环境变量GOOGLE_API_KEY(两例均使用agno.models.google.Gemini的gemini-3.5-flash;TEST_LOG.md 记录于 2026-07-18 在 agno 2.7.4 下验证)。两个脚本的运行顺序一致:先执行self_check()打印self_check passed,再发起模型调用构建矩阵、打印指标(basic.py 用rich.pretty.pprint输出指标字典)、路由分歧 item,最后给出汇总行。若更换其他 Gemini 模型版本,temperature=0虽能消除采样噪声,但标签仍可能随模型更新或服务端非确定性发生漂移,这已在两个文件的模块 docstring 中明确标注。
小结:何时使用一致性指标
只要超过一个标注者——无论来自不同模型、不同指令框架还是不同人类——触及同一批 item,就需要用一致性指标回答"他们的一致中有多少是真正的信号"。本模块给出的最小可靠实践是:以 temperature=0 消除采样噪声、以多种指令框架制造可追踪的分歧、以纯标准库实现并先自检机会校正指标、在倾斜标签分布下同时报告 raw agreement 与 Krippendorff's alpha、对非一致 item 路由人工复审。这套流程可以直接复用到产品评论情感标注、DPO 偏好数据构建、指南改写审计等任何多标注者场景。
【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考