结构化辩论协议
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 倍?」
💡 设计原则:开场陈述的质量决定了整场辩论的上限。如果一方在开场时就拿不出经得起推敲的论据,后面的质询环节只会让它暴露出更多问题。我们的代码会验证论据格式——不按格式来的发言会被退回重写。
第二轮:交叉质询
目标:这是整个协议中最关键的一轮。双方必须逐条回应对方的每一条开场论据——不能跳过、不能模糊带过。
交叉质询有三个强制要求:
- 逐条回应:对对方的每条论据,必须给出四种回应之一:反驳(指出逻辑或事实错误)、质疑(指出证据不足或条件不成立)、承认(接受该论据成立)、部分承认(接受核心但质疑程度或范围)。
- 针对性提问:对每条论据,至少提出一个具体的、尖锐的后续问题。例如「你引用的是 X 条件下的数据,该条件在我们的场景中不适用——你如何证明其可迁移性?」
- 无新增论据:质询环节不允许引入全新的论据。这很关键——如果允许在质询中引入新论据,对方就没有机会回应,裁判也无法评估。所有新论据必须在 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 轮框架的已知局限:
- 对模型能力敏感:当双方 Agent 使用同一个模型时,它们共享相同的知识边界和推理模式。两个 GPT-4o 互辩,仍然看不到 GPT-4o 不知道的东西。解决方案是使用不同的模型作为双方 Agent(比如 GPT-4o vs Claude),但这在本文代码中尚未实现——留作后续文章。
- JSON 解析脆弱:LLM 的 JSON 输出偶尔会出错(多了个逗号、少了引号)。我们在代码中做了容错处理(后备为自由文本),但在生产环境中,你可能需要更鲁棒的解析策略(如 Schema-constrained 生成或多次重试)。
- 质询环节可能「点到为止」:对方提出了质疑,正方回应了——但裁判可能不判断回应是否真的有效。裁判只能评估回应的表面质量(是否正面回答、是否逻辑一致),无法验证事实准确性。
- 缺乏外部验证:辩论全程在 LLM 的「颅内」进行。如果双方都引用了一个不存在的研究,裁判无法发现。后续文章将引入 RAG 和工具调用来解决这个问题。
关键收获
- 结构 = 可靠性:自由式辩论容易陷入议题漂移、虚假共识和深度不足。3 轮协议(开场→质询→总结)用结构约束解决了这些问题。
- 质询是辩论的核心:第二轮的交叉质询是整个协议中最关键的环节——它强制双方深挖对方的推理链,暴露逻辑漏洞和证据缺陷。
- 裁判需要「尺子」,不是「感觉」:多维度评分体系(逻辑/证据/回应/诚实)比模糊的「谁赢了」更可靠、更可复现。
- 论据追溯表是决策者的地图:它把长篇辩论压缩成「哪些论据站住了、哪些被驳倒了」——这是从辩论到决策的关键桥梁。
- AI 辩论是辅助工具,不是最终决策者:裁判的分数和追溯表是给人类决策者的输入,不是替代品。
📎 系列说明:本文是多 Agent 辩论系列的第 2 篇。上一篇 L1:为什么辩论比单一回答更可靠 介绍了认知偏误和对抗协作的基本原理。建议按顺序阅读。
📖 下一篇:辩论的评分与共识 — 评分尺度校准、多裁判体系、加权投票、共识度量
常见问题
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 个百分点)。更强的模型不只是"更聪明"——它们更擅长理解协议规则、遵循结构化输出格式、识别对方论据中的细微逻辑漏洞。建议关键决策场景使用你能力范围内最好的模型。