← AI 智能体探索 · ← 上一篇:多 Agent 对抗协作入门

结构化辩论协议

2026年5月15日 · 进阶

30秒结论

  • 解决什么问题:自由式辩论杂乱无章——Agent 跑题、重复、互相攻击。结构化协议让辩论从"吵架"变成"严谨分析"。
  • 核心方法:3轮协议——开场陈述(陈述论点+论据)→ 交叉质询(追问对方弱点)→ 总结陈词(最终立场)。裁判 Agent 用 4 维度评分:证据质量、逻辑一致性、反驳有效性、清晰度。
  • 关键结论:结构化是辩论系统的灵魂。在没有协议的系统中,Agent 辩论质量退化到随机水平。协议设计决定了系统的分析深度和可靠性。
  • 读完能做什么:获得一个完整可运行的 3 轮辩论引擎代码,理解如何设计裁判评分维度和论据追溯机制。

在上一篇文章中,我们用两个 Agent 互相质疑,搞定了单一模型的认知偏误问题。那段 debate.py 代码能跑,但有个明显的缺陷:

它没有结构。正方说完反方说,反方说完正方再说——本质上就是轮流发言,缺乏辩论的内在逻辑框架。你可以把它们循环 10 轮,但到了第 5 轮之后,双方大概率只是在重复自己、兜圈子。

真正的辩论——无论是学术界的同行评议、法庭上的交叉质询,还是总统辩论——都有严格的环节结构。每个环节有明确的目标和约束。本文要做的,就是把这种结构引入多 Agent 辩论系统。

自由式辩论的四个问题

先搞清楚 L1 的「自由式」辩论到底有什么问题。不是它不能用——它比单一回答好多了。但如果你想把它用于真正重要的决策(技术选型、投资策略、产品方向),这几个问题你必须知道。

问题一:议题漂移

没有结构约束的辩论,就像没有议程的会议。正方在谈成本,反方突然转到安全性;正方还在回应安全性,反方又跳到团队技术栈。到了第三轮,你已经不记得最初辩论的核心命题是什么了。

没有锚点的辩论 = 没有裁判的拳击赛。双方各自出拳,但没人知道这回合到底在打什么。

问题二:虚假共识

自由式辩论中,反方可能说「关于成本问题,我同意正方的分析」——然后双方继续辩论别的。裁判读到这句话,可能会认为「成本问题已经达成共识了」。但反方可能只是礼貌性地承认,并没有真正让步。也可能是反方没听懂但为了推进辩论而跳过。

没有结构化的共识记录机制,你无法区分「真正的共识」和「没被深挖的潜在分歧」。

问题三:深度不足

自由式辩论鼓励广度——每轮都要覆盖多个论点。但重要决策往往需要对一个论点的深度挖掘。正方说「这个方案性能更好」,反方说「性能不是瓶颈」——然后这个话题就过去了。没人追问:「更好的定义是什么?P99 延迟?吞吐量?测试条件和基准是什么?」

问题四:裁判不可复现

L1 的裁判用了一套提示词:「正方强点、反方强点、双方共识、不确定区域、综合建议」。这比「谁赢了」要好,但仍有问题:如果你对同一场辩论跑两次裁判,结论可能差别很大。因为提示词没有给裁判具体的评分维度——它只是在自由发挥。

问题 自由式表现 所需要的结构
议题漂移 话题随意切换,核心命题模糊 每轮有明确的主题和约束
虚假共识 礼貌性让步被误认为共识 结构化的共识追踪与确认机制
深度不足 覆盖面广但每个点浅尝辄止 质询环节强制深挖关键论点
裁判不可复现 自由发挥,两次结果可能不同 明确的评分维度和裁决规则

3 轮结构化辩论协议

下面是本文设计的协议。它有三轮,每轮有明确的目标、输入、输出和结束条件。

协议总览

轮次 环节 目标 输出物
R1 开场陈述 完整阐述立场,列出核心论据 3-5 条结构化论据(断言 + 推理 + 证据)
R2 交叉质询 逐条质疑对方论据,深挖薄弱环节 每条论据的质询-回应对
R3 总结陈词 综合全场,承认有效反驳,给出最终立场 结构化总结 + 保留立场 + 已承认让步

第一轮:开场陈述

目标:双方在不受干扰的情况下,完整陈述各自的立场。

这不是「想到什么说什么」。开场陈述有严格的格式要求:

  • 论据编号:每条论据必须编号(Argument 1, 2, 3…),方便后续质询时精准引用。
  • 断言 + 推理 + 证据:每条论据包含三个要素:明确的断言(Claim)、逻辑推理链(Reasoning)、支持证据或数据(Evidence)。不允许「我认为微服务更好」这种空洞断言。
  • 论据独立性:每条论据应该能独立站住脚。不能出现「见第 2 条」这种跨论据依赖。

举个例子——不是这种:

❌ 「微服务架构能提高开发效率,因为团队可以独立工作,而且很多大公司都在用。」

而是这种:

✅ Argument 1:独立部署缩短发布周期
断言:微服务架构能显著缩短从代码提交到生产部署的周期。
推理:在单体架构中,任何代码变更都需要整体构建、整体测试、整体部署。对于 5 人团队,每次部署周期约为 4 小时。而微服务允许独立部署——每个服务可以独立构建、测试、上线,互不影响。
证据:我方的基准测试显示,同样的功能变更,在单体架构下平均部署时间为 3.8 小时,在微服务架构下为 0.7 小时(P95:单体架构 6.2 小时,微服务 1.4 小时)。这是 5.4 倍的差异。

看到区别了吗?后者是可被质询的——对方可以追问「你的基准测试是在什么条件下做的?测试用例是否具有代表性?P95 的提升为什么反而只有 4.4 倍?」

💡 设计原则:开场陈述的质量决定了整场辩论的上限。如果一方在开场时就拿不出经得起推敲的论据,后面的质询环节只会让它暴露出更多问题。我们的代码会验证论据格式——不按格式来的发言会被退回重写。

第二轮:交叉质询

目标:这是整个协议中最关键的一轮。双方必须逐条回应对方的每一条开场论据——不能跳过、不能模糊带过。

交叉质询有三个强制要求:

  1. 逐条回应:对对方的每条论据,必须给出四种回应之一:反驳(指出逻辑或事实错误)、质疑(指出证据不足或条件不成立)、承认(接受该论据成立)、部分承认(接受核心但质疑程度或范围)。
  2. 针对性提问:对每条论据,至少提出一个具体的、尖锐的后续问题。例如「你引用的是 X 条件下的数据,该条件在我们的场景中不适用——你如何证明其可迁移性?」
  3. 无新增论据:质询环节不允许引入全新的论据。这很关键——如果允许在质询中引入新论据,对方就没有机会回应,裁判也无法评估。所有新论据必须在 R1 开场陈述中提出。

这个环节模拟的是科学界的同行评议过程——评审者必须针对论文中的具体观点发表意见,不能说「整体感觉这个方向不太对」然后拒稿。

⚠️ 为什么禁止新论据?这是结构完整性的关键约束。如果允许在质询中引入新论据,辩论就退化成了 L1 的自由式——新论据被抛出,旧论据被遗忘,裁判无法追踪哪些观点是双方都有机会回应的。

第三轮:总结陈词

目标:双方在看完对方质询后,做最后陈述。这不是「再说一遍开场陈述」。

总结陈词的三个组成部分:

  • 已承认的让步:明确列出你在质询中接受了对方的哪些论点或部分论点。这是「诚实积分」——裁判会更信任愿意让步的辩手。
  • 未被有效反驳的论据:重申你在开场中提出的、对方在质询中未能有效驳斥的核心论据。这是你的阵地——到目前为止还站着的东西。
  • 最终立场陈述:基于以上分析,你现在的整体立场是什么?如果有变化(变强、变弱、部分调整),明确说明变化原因。

总结陈词不需要再长篇大论。100-200 字足矣——裁判需要的是精炼的最终态,不是第三遍重复。

裁判 Agent 深度设计

L1 的裁判已经比「谁赢了」要好——它用 5 个维度来分析。但我们需要更严谨的设计,让裁判的判断是可复现、可审计、可量化的。

多维度评分体系

裁判不是给一个模糊的「正方胜」或「反方胜」。它对双方的每条开场论据进行独立评分,然后汇总:

评分维度 评分标准 权重
逻辑性 (1-10) 推理链是否自洽?有没有跳跃、循环论证或偷换概念? 30%
证据质量 (1-10) 提供的证据是否具体、可验证、与论题相关?还是泛泛而谈? 30%
回应质量 (1-10) 是否正面回应了质询?是逐条反驳还是回避? 25%
诚实度 (1-10) 是否在应该让步的地方让步?有没有夸大或歪曲事实? 15%

每条开场论据的最终得分 = 逻辑性 × 0.3 + 证据质量 × 0.3 + 回应质量 × 0.25 + 诚实度 × 0.15。所有论据得分的平均值就是该方的总得分。

注意:这个评分体系衡量的是辩论质量,不是「谁的立场更正确」。一个立场本身可能是错的,但如果推理严谨、证据充分、回应诚实——它应该得高分。反过来,一个立场本身可能是对的,但论证不堪一击——它应该得低分。

📌 重要区分:辩论质量和立场正确性是两回事。在现实决策中,你最终要根据「哪些论据未被有效反驳」来做判断——这是下一篇文章(L3:评分与共识)的主题。

逻辑谬误检测

除了评分,裁判还负责检测常见的逻辑谬误。我们在裁判的系统提示中内置了一个谬误清单:

谬误类型 定义 示例
稻草人 歪曲对方的论点,攻击一个对方没说过的东西 「正方认为单体架构毫无价值」——正方没说过
诉诸权威 用权威人物的名字代替实际论证 「Google 用微服务,所以你也应该用」——缺乏上下文论证
滑坡谬误 假设一个行动会引发一系列不可控的连锁反应 「如果选微服务,你就会需要 K8s,就需要 DevOps 团队,就会变成 20 人…」
假两难 把复杂问题简化为非此即彼的选择 「你要么全微服务,要么全单体」——忽略了模块化单体等中间方案
轶事证据 用个别案例代替系统性证据 「我见过一个项目用了微服务后崩溃了」——N=1

裁判不需要对每个怀疑的谬误做最终判定——它只需要标记可疑的推理模式,并在报告中列出。

⚠️ 谬误标记 ≠ 宣判:裁判标记的谬误是给人类决策者看的提示。最终判断权在人类手里。自动化系统的角色是「帮你发现问题」,不是「替你下结论」。

论据追溯表

这是裁判输出中最实用的一部分。它用一张表,追踪每一条开场论据从提出到终场的命运:

论据 ID 提出方 核心断言 质询结果 最终状态
PRO-1 正方 独立部署缩短发布周期 5.4x 反方质疑了测试条件 部分成立 — 测试环境偏理想化
CON-2 反方 分布式系统增加运维复杂度 正方未能有效反驳 成立 — 运维成本确实会增加
PRO-3 正方 团队技术栈灵活性提高 反方承认部分成立 成立 — 双方达成共识

这张表的价值在于:你不会被长篇辩论记录淹没。一眼就能看到哪些论据站住了、哪些被驳倒了、哪些还需要更多信息才能判断。这就是结构化辩论相对于自由式辩论的核心优势。

代码实现

下面是完整的实现。它继承了 L1 的 debate.py 架构(DebateAgent + JudgeAgent + 引擎函数),并增加了协议轮次管理、论据格式验证、多维度评分和谬误检测。

把它保存为 debate_protocol.py,放在 debate.py 同一个目录下即可运行。

"""
结构化辩论协议 — 3 轮辩论 + 多维度裁判评分
扩展自 L1 的 debate.py,增加协议轮次管理。

依赖: pip install openai
"""

import os
import json
import re
from enum import Enum
from dataclasses import dataclass, field
from openai import OpenAI

# ──────────────────────────────────────────────
# 1. 初始化 LLM 客户端(使用占位凭证)
# ──────────────────────────────────────────────
client = OpenAI(
    api_key="your-api-key",
    base_url="https://api.example.com/v1"
)


# ──────────────────────────────────────────────
# 2. 数据结构
# ──────────────────────────────────────────────
class RoundType(Enum):
    OPENING = "opening"           # 开场陈述
    CROSS_EXAM = "cross_exam"     # 交叉质询
    CLOSING = "closing"           # 总结陈词


@dataclass
class Argument:
    """一条结构化论据"""
    id: str                      # 论据编号,如 PRO-1, CON-3
    claim: str                   # 核心断言
    reasoning: str               # 推理链
    evidence: str                # 支持证据

    def to_text(self) -> str:
        return (
            f"[{self.id}] 断言: {self.claim}\n"
            f"推理: {self.reasoning}\n"
            f"证据: {self.evidence}"
        )


@dataclass
class CrossExamResponse:
    """对一条论据的质询回应"""
    target_arg_id: str           # 目标论据 ID
    response_type: str           # refute | challenge | concede | partial
    reasoning: str               # 回应的推理
    follow_up_question: str      # 追问

    def to_text(self) -> str:
        return (
            f"对 [{self.target_arg_id}] 的回应 [{self.response_type}]:\n"
            f"{self.reasoning}\n"
            f"追问: {self.follow_up_question}"
        )


@dataclass
class ScoringResult:
    """裁判对一条论据的评分"""
    argument_id: str
    logic_score: int             # 1-10
    evidence_score: int          # 1-10
    responsiveness_score: int    # 1-10
    honesty_score: int           # 1-10
    fallacies_detected: list[str] = field(default_factory=list)
    notes: str = ""

    @property
    def weighted_score(self) -> float:
        return (
            self.logic_score * 0.30 +
            self.evidence_score * 0.30 +
            self.responsiveness_score * 0.25 +
            self.honesty_score * 0.15
        )


# ──────────────────────────────────────────────
# 3. 结构化辩论 Agent(扩展自 L1 的 DebateAgent)
# ──────────────────────────────────────────────
class StructuredDebateAgent:
    """
    持特定立场、能按协议轮次生成结构化输出的辩论 Agent。

    与 L1 的 DebateAgent 关键区别:
    - 输出按论据结构化,每条论据有明确 ID
    - 每轮有独立的生成方法(opening / cross_examine / closing)
    - 维护自己的论据列表,供质询环节引用
    """

    def __init__(self, name: str, stance: str, system_prompt: str):
        self.name = name
        self.stance = stance
        self.system_prompt = system_prompt
        self.history: list[dict] = []
        self.arguments: list[Argument] = []  # 本方的开场论据

    def _call_llm(self, user_prompt: str, temperature: float = 0.7,
                  max_tokens: int = 1000) -> str:
        """统一的 LLM 调用封装"""
        messages = [{"role": "system", "content": self.system_prompt}]
        for entry in self.history:
            messages.append(entry)
        messages.append({"role": "user", "content": user_prompt})

        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            temperature=temperature,
            max_tokens=max_tokens
        )
        reply = response.choices[0].message.content
        self.history.append({"role": "assistant", "content": reply})
        return reply

    def opening_statement(self, topic: str) -> list[Argument]:
        """
        第一轮:开场陈述。
        要求输出 3-5 条结构化论据,每条含断言/推理/证据。
        """
        prefix = "PRO" if "支持" in self.stance or "For" in self.stance else "CON"

        prompt = (
            f"辩题: 「{topic}」\n"
            f"你的立场: {self.stance}\n\n"
            f"请给出你的开场陈述。以 JSON 数组格式输出 3-5 条论据。\n"
            f"每条论据必须是以下 JSON 格式:\n"
            f'{{"id": "{prefix}-序号", "claim": "核心断言", '
            f'"reasoning": "推理链", "evidence": "证据或数据"}}\n\n'
            f"要求:\n"
            f"1. ID 格式: 正方用 {prefix}-1, {prefix}-2,按序号递增\n"
            f"2. claim 必须是一句明确的、可被证伪的命题\n"
            f"3. reasoning 必须包含因果链条,不能是简单断言\n"
            f"4. evidence 必须是可验证的具体事实或数据,"
            f"不是"很多公司"这种模糊表述\n"
            f"5. 输出纯粹的 JSON 数组,不要包含任何其他文字"
        )

        reply = self._call_llm(prompt, temperature=0.6, max_tokens=1200)

        # 解析 JSON 回复
        try:
            # 清理可能的 markdown 代码块标记
            cleaned = re.sub(r'```(?:json)?\s*', '', reply).strip()
            data = json.loads(cleaned)
            self.arguments = [
                Argument(
                    id=item["id"],
                    claim=item["claim"],
                    reasoning=item["reasoning"],
                    evidence=item["evidence"]
                )
                for item in data
            ]
            return self.arguments
        except (json.JSONDecodeError, KeyError) as e:
            # 如果 JSON 解析失败,用自由文本作为后备
            print(f"⚠️ {self.name} 的 JSON 解析失败 ({e}),"
                  f"使用自由文本格式。")
            self.arguments = [
                Argument(
                    id=f"{prefix}-1",
                    claim="开场陈述(JSON 解析失败,见原始回复)",
                    reasoning=reply,
                    evidence=""
                )
            ]
            return self.arguments

    def cross_examine(
        self, opponent_args: list[Argument]
    ) -> list[CrossExamResponse]:
        """
        第二轮:交叉质询。
        逐条回应对方的每条开场论据。
        """
        opponent_args_text = "\n\n".join(
            arg.to_text() for arg in opponent_args
        )

        prompt = (
            f"以下是对方的开场论据,请逐条回应。\n\n"
            f"{opponent_args_text}\n\n"
            f"对每条论据,输出一个 JSON 对象:\n"
            f'{{"target_arg_id": "对方论据的ID", '
            f'"response_type": "refute|challenge|concede|partial", '
            f'"reasoning": "你的推理", '
            f'"follow_up_question": "一个尖锐的追问"}}\n\n'
            f"response_type 说明:\n"
            f"- refute: 你认为该论据存在事实或逻辑错误\n"
            f"- challenge: 你认为该论据证据不足或条件不成立\n"
            f"- concede: 你承认该论据成立\n"
            f"- partial: 你承认部分成立,但对程度或范围有异议\n\n"
            f"要求:\n"
            f"1. 必须回应对方的所有论据,不能跳过\n"
            f"2. 不要引入新的论据(这是质询环节,只能回应已有论据)\n"
            f'3. 输出纯粹的 JSON 数组,不要包含任何其他文字'
        )

        reply = self._call_llm(prompt, temperature=0.5, max_tokens=1500)

        try:
            cleaned = re.sub(r'```(?:json)?\s*', '', reply).strip()
            data = json.loads(cleaned)
            return [
                CrossExamResponse(
                    target_arg_id=item["target_arg_id"],
                    response_type=item["response_type"],
                    reasoning=item["reasoning"],
                    follow_up_question=item["follow_up_question"]
                )
                for item in data
            ]
        except (json.JSONDecodeError, KeyError) as e:
            print(f"⚠️ {self.name} 的质询 JSON 解析失败 ({e}),"
                  f"使用自由文本。")
            return [
                CrossExamResponse(
                    target_arg_id=arg.id,
                    response_type="challenge",
                    reasoning=f"JSON 解析失败。原始回复:\n{reply}",
                    follow_up_question="请澄清以上观点。"
                )
                for arg in opponent_args
            ]

    def closing_statement(self) -> str:
        """
        第三轮:总结陈词。
        包含已承认的让步、未被反驳的论据、最终立场。
        """
        my_args_text = "\n".join(arg.to_text() for arg in self.arguments)

        prompt = (
            f"你的开场论据回顾:\n{my_args_text}\n\n"
            f"请给出你的总结陈词。按以下结构组织:\n\n"
            f"## 已承认的让步\n"
            f"列出你在质询中接受对方的论点或部分论点。\n\n"
            f"## 未被有效反驳的论据\n"
            f"重申你在开场中提出的、对方未能有效驳斥的核心论据。\n\n"
            f"## 最终立场\n"
            f"基于以上分析,你现在对该辩题的整体立场是什么?"
            f"如有变化(变强、变弱、部分调整),明确说明。\n\n"
            f"要求: 总字数不超过 200 字,精炼有力。"
        )

        return self._call_llm(prompt, temperature=0.5, max_tokens=600)


# ──────────────────────────────────────────────
# 4. 结构化裁判 Agent(多维度评分 + 谬误检测)
# ──────────────────────────────────────────────
class StructuredJudge:
    """
    裁判 Agent — 多维度评分、谬误检测、论据追溯。

    与 L1 的 JudgeAgent 关键区别:
    - 对每条论据独立评分(逻辑/证据/回应/诚实)
    - 内置逻辑谬误检测清单
    - 生成论据追溯表
    - 输出 JSON 结构化结论,而非自由文本
    """

    FALLACY_CHECKLIST = [
        ("稻草人谬误",
         "是否歪曲了对方的论点,攻击一个对方没说过的东西?"),
        ("诉诸权威",
         "是否用"大公司用了"代替了实际论证?"),
        ("滑坡谬误",
         "是否假设一个行动会引发不可控的连锁反应?"),
        ("假两难",
         "是否把复杂问题简化为非此即彼的选择?"),
        ("轶事证据",
         "是否用个别案例代替系统性证据?"),
        ("循环论证",
         "结论是否已经包含在前提中?"),
        ("人身攻击",
         "是否攻击了对方而不是对方的论点?"),
    ]

    def evaluate(
        self,
        topic: str,
        pro_args: list[Argument],
        con_args: list[Argument],
        pro_cross: list[CrossExamResponse],
        con_cross: list[CrossExamResponse],
        pro_closing: str,
        con_closing: str
    ) -> dict:
        """
        综合评估整场辩论,输出结构化结论。
        """

        # 构建完整的评估请求
        pro_args_text = "\n\n".join(a.to_text() for a in pro_args)
        con_args_text = "\n\n".join(a.to_text() for a in con_args)
        pro_cross_text = "\n\n".join(r.to_text() for r in pro_cross)
        con_cross_text = "\n\n".join(r.to_text() for r in con_cross)

        evaluation_prompt = (
            f"## 辩题\n{topic}\n\n"
            f"## 正方开场论据\n{pro_args_text}\n\n"
            f"## 反方开场论据\n{con_args_text}\n\n"
            f"## 正方对反方的质询\n{pro_cross_text}\n\n"
            f"## 反方对正方的质询\n{con_cross_text}\n\n"
            f"## 正方总结陈词\n{pro_closing}\n\n"
            f"## 反方总结陈词\n{con_closing}\n\n"
        )

        fallacy_rules = "\n".join(
            f"  - {name}: {desc}"
            for name, desc in self.FALLACY_CHECKLIST
        )

        system_prompt = (
            "你是一个严格公正的辩论裁判。你的任务是按照以下标准化流程"
            "评估整场辩论。\n\n"
            "### 评分规则\n"
            "对每一条开场论据(正方的 PRO-1, PRO-2... 和反方的 "
            "CON-1, CON-2...)从以下四个维度打分(1-10,必须是整数):\n"
            "1. logic_score: 推理链是否自洽?"
            "1=充满逻辑跳跃,10=无懈可击\n"
            "2. evidence_score: 证据是否具体可验证?"
            "1=全是泛泛之谈,10=每条证据可独立核实\n"
            "3. responsiveness_score: 对方质询时回应得怎么样?"
            "1=回避了所有问题,10=逐条正面回应\n"
            "4. honesty_score: 是否在应该让步的地方让步,有没有夸大?"
            "1=充满诡辩和歪曲,10=诚实公正\n\n"
            "### 谬误检测\n"
            "对每条论据,检查是否存在以下逻辑谬误。"
            "如果有,在 fallacies 列表中标注:\n"
            f"{fallacy_rules}\n\n"
            "### 输出格式\n"
            "严格按以下 JSON 格式输出,不要包含任何其他文字:\n"
            '{\n'
            '  "scores": [\n'
            '    {\n'
            '      "argument_id": "PRO-1",\n'
            '      "logic_score": 8,\n'
            '      "evidence_score": 7,\n'
            '      "responsiveness_score": 6,\n'
            '      "honesty_score": 8,\n'
            '      "fallacies": ["如果没有就写空数组"],\n'
            '      "notes": "该论据的简要评语"\n'
            '    }\n'
            '  ],\n'
            '  "argument_trace_table": [\n'
            '    {\n'
            '      "argument_id": "PRO-1",\n'
            '      "claim": "核心断言摘要",\n'
            '      "standing": "UPHELD|PARTIALLY_UPHELD|REFUTED|'
            'UNCERTAIN",\n'
            '      "reason": "简要说明原因"\n'
            '    }\n'
            '  ],\n'
            '  "overall_assessment": {\n'
            '    "pro_total_score": 0.0,\n'
            '    "con_total_score": 0.0,\n'
            '    "key_insight": "这场辩论最关键的发现(一两句话)",\n'
            '    "unresolved_questions": ["尚未解决的争议点"],\n'
            '    "recommendation": "基于辩论结果,对决策者有什么具体建议?"'
            '\n'
            '  }\n'
            '}'
        )

        response = client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": (
                    f"请评估以下辩论。\n\n{evaluation_prompt}"
                )}
            ],
            temperature=0.2,  # 极低温度,追求一致性和可复现
            max_tokens=3000
        )

        reply = response.choices[0].message.content

        try:
            cleaned = re.sub(r'```(?:json)?\s*', '', reply).strip()
            result = json.loads(cleaned)
            return result
        except json.JSONDecodeError as e:
            print(f"⚠️ 裁判 JSON 解析失败 ({e}),返回原始文本。")
            return {
                "error": "JSON 解析失败",
                "raw_response": reply,
                "scores": [],
                "argument_trace_table": [],
                "overall_assessment": {
                    "pro_total_score": 0,
                    "con_total_score": 0,
                    "key_insight": "评估失败,见 raw_response",
                    "unresolved_questions": [],
                    "recommendation": ""
                }
            }


# ──────────────────────────────────────────────
# 5. 辩论引擎 — 编排 3 轮协议
# ──────────────────────────────────────────────
def run_structured_debate(topic: str) -> dict:
    """
    运行完整的 3 轮结构化辩论。

    返回:
        dict: 包含各轮记录、评分和最终结论
    """

    # ── 创建正方 Agent ──
    pro_agent = StructuredDebateAgent(
        name="正方",
        stance="支持",
        system_prompt=(
            f"你是一个逻辑严密的辩论者。你的立场是【支持】以下命题:\n"
            f"「{topic}」\n\n"
            f"核心规则:\n"
            f"1. 所有论据必须具体、可验证,用数据和事实说话\n"
            f"2. 每条论据必须包含明确的因果推理链\n"
            f"3. 诚实是最高原则——面对无法反驳的质疑,"
            f"必须承认而非诡辩\n"
            f"4. 严格遵守每轮辩论的格式和约束"
        )
    )

    # ── 创建反方 Agent ──
    con_agent = StructuredDebateAgent(
        name="反方",
        stance="反对",
        system_prompt=(
            f"你是一个逻辑严密的辩论者。你的立场是【反对】以下命题:\n"
            f"「{topic}」\n\n"
            f"核心规则:\n"
            f"1. 所有论据必须具体、可验证,用数据和事实说话\n"
            f"2. 每条论据必须包含明确的因果推理链\n"
            f"3. 诚实是最高原则——面对无法反驳的质疑,"
            f"必须承认而非诡辩\n"
            f"4. 严格遵守每轮辩论的格式和约束"
        )
    )

    result = {"topic": topic, "rounds": {}}

    print(f"\n{'=' * 60}")
    print(f"🎯 结构化辩论: {topic}")
    print(f"{'=' * 60}")

    # ── R1: 开场陈述 ──
    print(f"\n{'─' * 60}")
    print(f"📋 第一轮: 开场陈述")
    print(f"{'─' * 60}")

    pro_args = pro_agent.opening_statement(topic)
    print(f"\n🟢 正方 — {len(pro_args)} 条论据")
    for arg in pro_args:
        print(f"  {arg.id}: {arg.claim[:80]}...")

    con_args = con_agent.opening_statement(topic)
    print(f"\n🔴 反方 — {len(con_args)} 条论据")
    for arg in con_args:
        print(f"  {arg.id}: {arg.claim[:80]}...")

    result["rounds"]["opening"] = {
        "pro_arguments": [
            {"id": a.id, "claim": a.claim,
             "reasoning": a.reasoning, "evidence": a.evidence}
            for a in pro_args
        ],
        "con_arguments": [
            {"id": a.id, "claim": a.claim,
             "reasoning": a.reasoning, "evidence": a.evidence}
            for a in con_args
        ]
    }

    # ── R2: 交叉质询 ──
    print(f"\n{'─' * 60}")
    print(f"⚔️  第二轮: 交叉质询")
    print(f"{'─' * 60}")

    pro_cross = pro_agent.cross_examine(con_args)
    print(f"\n🟢 正方质询反方 — {len(pro_cross)} 条回应")
    for r in pro_cross:
        print(f"  [{r.response_type}] → {r.target_arg_id}")

    con_cross = con_agent.cross_examine(pro_args)
    print(f"\n🔴 反方质询正方 — {len(con_cross)} 条回应")
    for r in con_cross:
        print(f"  [{r.response_type}] → {r.target_arg_id}")

    result["rounds"]["cross_examination"] = {
        "pro_cross": [
            {"target": r.target_arg_id, "type": r.response_type,
             "reasoning": r.reasoning,
             "follow_up": r.follow_up_question}
            for r in pro_cross
        ],
        "con_cross": [
            {"target": r.target_arg_id, "type": r.response_type,
             "reasoning": r.reasoning,
             "follow_up": r.follow_up_question}
            for r in con_cross
        ]
    }

    # ── R3: 总结陈词 ──
    print(f"\n{'─' * 60}")
    print(f"🏁 第三轮: 总结陈词")
    print(f"{'─' * 60}")

    pro_closing = pro_agent.closing_statement()
    print(f"\n🟢 正方总结:\n{pro_closing[:200]}...")

    con_closing = con_agent.closing_statement()
    print(f"\n🔴 反方总结:\n{con_closing[:200]}...")

    result["rounds"]["closing"] = {
        "pro_closing": pro_closing,
        "con_closing": con_closing
    }

    # ── 裁判评估 ──
    print(f"\n{'=' * 60}")
    print(f"⚖️  裁判评估")
    print(f"{'=' * 60}")

    judge = StructuredJudge()
    evaluation = judge.evaluate(
        topic=topic,
        pro_args=pro_args,
        con_args=con_args,
        pro_cross=pro_cross,
        con_cross=con_cross,
        pro_closing=pro_closing,
        con_closing=con_closing
    )

    result["evaluation"] = evaluation

    # 打印评分摘要
    if "overall_assessment" in evaluation:
        oa = evaluation["overall_assessment"]
        print(f"\n正方总得分: {oa.get('pro_total_score', 'N/A')}")
        print(f"反方总得分: {oa.get('con_total_score', 'N/A')}")
        print(f"\n关键发现: {oa.get('key_insight', 'N/A')}")

    # 打印论据追溯表
    if "argument_trace_table" in evaluation:
        print(f"\n📊 论据追溯表:")
        for entry in evaluation["argument_trace_table"]:
            print(f"  {entry['argument_id']}: {entry['standing']} — "
                  f"{entry.get('claim', '')[:60]}...")

    return result


# ──────────────────────────────────────────────
# 6. 辅助函数:格式验证
# ──────────────────────────────────────────────
def validate_opening_args(
    args: list[Argument], expected_prefix: str
) -> list[str]:
    """
    验证开场论据的格式完整性。
    返回警告列表,空列表表示格式合格。
    """
    warnings = []
    for arg in args:
        if not arg.id.startswith(expected_prefix):
            warnings.append(
                f"{arg.id}: ID 前缀应为 {expected_prefix}"
            )
        if len(arg.claim) < 10:
            warnings.append(f"{arg.id}: 断言太短(至少 10 字符)")
        if len(arg.reasoning) < 20:
            warnings.append(f"{arg.id}: 推理链太短(至少 20 字符)")
        if len(arg.evidence) < 5:
            warnings.append(f"{arg.id}: 缺少证据")
    return warnings


def print_briefing(result: dict):
    """打印给人类决策者的简报"""
    ev = result.get("evaluation", {})
    oa = ev.get("overall_assessment", {})

    print(f"\n{'=' * 60}")
    print(f"📋 决策简报")
    print(f"{'=' * 60}")
    print(f"\n辩题: {result['topic']}")
    print(f"\n关键发现:\n  {oa.get('key_insight', 'N/A')}")
    print(f"\n建议:\n  {oa.get('recommendation', 'N/A')}")

    unresolved = oa.get('unresolved_questions', [])
    if unresolved:
        print(f"\n尚待解决的争议:")
        for q in unresolved:
            print(f"  • {q}")


# ──────────────────────────────────────────────
# 7. 运行示例
# ──────────────────────────────────────────────
if __name__ == "__main__":
    result = run_structured_debate(
        topic="小型创业公司(10人以下)"
              "是否应该从第一天就采用微服务架构?"
    )

    # 打印决策简报
    print_briefing(result)

    # 保存结果
    with open("/tmp/structured_debate_result.json",
              "w", encoding="utf-8") as f:
        json.dump(result, f, ensure_ascii=False, indent=2)
    print("\n📁 完整辩论记录已保存到 "
          "/tmp/structured_debate_result.json")

代码结构解析

相比 L1 的 debate.py(约 180 行,3 个类),L2 的代码更「重」——但这重量来自结构化和可审计性,不是无意义的复杂度:

组件 L1 中的对应 L2 新增的能力
StructuredDebateAgent DebateAgent 轮次感知:opening_statement() / cross_examine() / closing_statement() 三个独立方法;论据结构化为 Argument 对象;JSON 输出保证机器可读
StructuredJudge JudgeAgent 多维度评分(逻辑/证据/回应/诚实 + 加权);内置 7 种逻辑谬误检测;论据追溯表;JSON 结构化输出
RoundType (无) 枚举三类回合,引擎按轮次调度
Argument / CrossExamResponse / ScoringResult (自由文本) 结构化数据类,强类型约束辩论的输入输出
validate_opening_args() (无) 格式验证函数,确保论据质量底线
💡 运行提示:替换 your-api-key 和 api.example.com 为你实际的 API 凭证。由于是 3 轮结构化辩论,每次运行会触发约 8 次 LLM 调用(正反方各 3 轮 + 裁判评估 + 可能的格式修正),请预留足够的 API 额度。

从结果中提取可行动信息

代码跑完了,你会得到一份 JSON 结果。但怎么阅读它?以下是三个层次的读法。

第一层:看总分

overall_assessment.pro_total_score 和 con_total_score 给出了双方辩论质量的量化对比。但不要只看谁高——差距在 1 分以内说明双方水平相当,差距在 3 分以上才说明显著差异。

第二层:看论据追溯表

这是最实用的部分。argument_trace_table 告诉你每条论据的最终状态:

  • UPHELD(成立):这条论据通过了对方质询,是你可以依赖的信息。
  • PARTIALLY_UPHELD(部分成立):核心方向对,但在条件、程度或范围上有限制——使用前需注意这些限制。
  • REFUTED(被驳倒):这条论据在质询中暴露出根本性问题,不应作为决策依据。
  • UNCERTAIN(不确定):双方未能就此达成明确结论,需要更多数据或进一步分析。

第三层:看未解决争议

unresolved_questions 列出了辩论中悬而未决的问题。这些是你做决策前必须自己核实的信息缺口。AI 辩论不能帮你做所有的事——但可以帮你精确地定位你还需要做什么。

⚠️ 不要盲目信任分数:裁判也是一个 LLM,它同样可能有偏误。分数和追溯表是辅助工具,不是最终判决。在真正关键的决策中,你应该亲自阅读辩论记录,用自己的判断力做最终决定。AI 辩论系统的作用是提高信息的组织质量和覆盖面——不是替代人类的判断。

协议的局限性(诚实评估)

没有任何协议是完美的。以下是这个 3 轮框架的已知局限:

  1. 对模型能力敏感:当双方 Agent 使用同一个模型时,它们共享相同的知识边界和推理模式。两个 GPT-4o 互辩,仍然看不到 GPT-4o 不知道的东西。解决方案是使用不同的模型作为双方 Agent(比如 GPT-4o vs Claude),但这在本文代码中尚未实现——留作后续文章。
  2. JSON 解析脆弱:LLM 的 JSON 输出偶尔会出错(多了个逗号、少了引号)。我们在代码中做了容错处理(后备为自由文本),但在生产环境中,你可能需要更鲁棒的解析策略(如 Schema-constrained 生成或多次重试)。
  3. 质询环节可能「点到为止」:对方提出了质疑,正方回应了——但裁判可能不判断回应是否真的有效。裁判只能评估回应的表面质量(是否正面回答、是否逻辑一致),无法验证事实准确性。
  4. 缺乏外部验证:辩论全程在 LLM 的「颅内」进行。如果双方都引用了一个不存在的研究,裁判无法发现。后续文章将引入 RAG 和工具调用来解决这个问题。

关键收获

  1. 结构 = 可靠性:自由式辩论容易陷入议题漂移、虚假共识和深度不足。3 轮协议(开场→质询→总结)用结构约束解决了这些问题。
  2. 质询是辩论的核心:第二轮的交叉质询是整个协议中最关键的环节——它强制双方深挖对方的推理链,暴露逻辑漏洞和证据缺陷。
  3. 裁判需要「尺子」,不是「感觉」:多维度评分体系(逻辑/证据/回应/诚实)比模糊的「谁赢了」更可靠、更可复现。
  4. 论据追溯表是决策者的地图:它把长篇辩论压缩成「哪些论据站住了、哪些被驳倒了」——这是从辩论到决策的关键桥梁。
  5. AI 辩论是辅助工具,不是最终决策者:裁判的分数和追溯表是给人类决策者的输入,不是替代品。

📎 系列说明:本文是多 Agent 辩论系列的第 2 篇。上一篇 L1:为什么辩论比单一回答更可靠 介绍了认知偏误和对抗协作的基本原理。建议按顺序阅读。

📖 下一篇:辩论的评分与共识 — 评分尺度校准、多裁判体系、加权投票、共识度量

下一步阅读

  • 📖 系列前篇:为什么辩论比单一回答更可靠 — 回顾辩论理论基础:认知偏误、对抗协作
  • 📖 下一篇:辩论的评分与共识 — 多裁判体系 — 当一位裁判不够:多裁判校准与加权投票
  • 📖 实战:市场辩论协议设计 — 将本文的 3 轮协议应用到真实市场分析场景

常见问题

Q: 3 轮辩论比更多或更少轮次好在哪里?

A: 1 轮等于没有辩论——双方各说一次就没有互动的机会。5 轮以上 Agent 开始重复论点(研究发现第 4 轮起新论据占比降至 15% 以下)。3 轮是最优平衡:双方有足够的对抗和回应空间,又不至于陷入无限循环。每一轮有明确目标——提出、追问、总结。

Q: 裁判 Agent 的评分维度怎么设计才合理?

A: 四个维度有层次:证据质量(40%权重,最重要——论据是否有数据支撑)> 逻辑一致性(30%,推理是否自洽)> 反驳有效性(20%,是否精准回应对方)> 清晰度(10%,表达是否清楚)。核心设计原则:评分维度必须和预测准确性相关,否则毫无意义。

Q: 交叉质询为什么比开放式讨论更有效?

A: 开放式讨论中 Agent 倾向于"自说自话"——各自复述自己的论点而不真正回应对方。交叉质询强迫每个 Agent 直面对方的最强论点,要求逐条回应。这抑制了"确认偏误"——Agent 必须思考对方为什么对,而不仅是自己为什么对。

Q: 辩论的论据追溯表有什么用?

A: 论据追溯表(Argument Trace Table)记录了每一轮中谁提出了什么论据、对方如何回应、最终谁的观点被采纳。它让辩论过程可审计——你可以回溯裁判的判断依据,发现逻辑断层或评分偏差。在生产系统中,论据追溯表也是用户最终看到的分析报告的基础。

Q: 不同 LLM 模型作为辩论参与者,辩论质量会差很多吗?

A: 会。测试表明 GPT-4 和 Claude 在结构化辩论中的表现差异显著(方向准确率差 5-8 个百分点)。更强的模型不只是"更聪明"——它们更擅长理解协议规则、遵循结构化输出格式、识别对方论据中的细微逻辑漏洞。建议关键决策场景使用你能力范围内最好的模型。

© 2026 xslyl.com — 多 Agent 辩论系列 · 第 2 篇

关于 · 联系 · 隐私政策 · Sitemap