本章目标:判断什么时候真的需要多个 Agent(多数时候不需要), 以及需要时怎么让它们不打架。 配套模块:
clinic/agent_role.py配套案例:cases/case11_双编程Agent对.py
22.1 先泼一盆冷水
"多 Agent 协作"是这几年被包装得最过头的概念。真实的工程账是这样的:
| 成本 | 单 Agent | 3 个 Agent |
|---|---|---|
| token | 1× | 3×(每个 Agent 都要带完整上下文) |
| 延迟 | 串行 N 步 | 串行 N 步 + 通信开销,可能更长 |
| 调试难度 | 一条轨迹 | 三条轨迹 + 它们之间的消息 |
| 出错模式 | 做错 | 做错 + 互相甩锅 + 理解偏差 + 死锁 |
🔥 一条经验法则: 如果你能用"给同一个 Agent 加一个工具"解决的问题,就不要加一个 Agent。
加 Agent 的理由必须强到能抵掉上面那些成本。
22.2 四个"真的需要"的理由
只有这四种情况,多 Agent 的收益才大于成本:
理由 1:上下文隔离
某个角色的知识量大到装不下(比如"法务合规审查员"需要一整套法规文本, "统计师"需要 SAP 全文)。这时候拆开是为了让每个 Agent 的上下文干净, 不是为了"人多力量大"。
理由 2:视角独立性 ★ 最重要
有些任务的价值全部来自独立性。最典型的就是双编程:
如果两个实现者能互相商量: 如果两个实现者完全独立:
→ 会趋同,验证失效 → 差异才是信号
→ 一个想错,另一个也被带偏 → 两边同时错成一样的概率极低
共享上下文的两个 Agent 做验证,等于自己检查自己 —— 这是本章最重要的一句话。
理由 3:提示词差异巨大
"数据管理员"关注数据质量与合规,"统计师"关注分析口径, "医学写作"关注表述规范。让一个 Agent 同时扮演三种角色, 提示词会互相稀释,哪种都做不好。
理由 4:任务真正可并行
各自独立、互不依赖、最后汇总。比如同时检查 SDTM 的 5 个域。
反例:什么情况不该用
| 场景 | 为什么不该 | 该怎么做 |
|---|---|---|
| "让一个 Agent 检查另一个的输出" | 共享上下文时没有独立性 | 要么真独立,要么做确定性校验 |
| "三个人从不同角度看同一个数据" | 结果趋同,成本 3× | 一个 Agent + 多个工具 |
| "多一步总比少一步好" | 通信开销 + 理解偏差 | 单 Agent + Reflection |
22.3 三种协作拓扑
① 监督者(Supervisor) ② 流水线(Pipeline)
┌────────────┐ ┌──────┐
│ Supervisor │ │ 输入 │
│ 分配/汇总 │ └──┬───┘
└──┬──┬──┬───┘ ▼
│ │ │ ┌────────┐
┌────▼┐ │ ┌▼────┐ │ Agent A│
│ A │ │ │ B │ └───┬────┘
└─────┘ │ └──────┘ ▼
┌───▼───┐ ┌────────┐
│ C │ │ Agent B│ ← 接手 A 的产物
└───────┘ └───┬────┘
▼
适合:任务可拆、需汇总 ┌────────┐
风险:Supervisor 成瓶颈 │ Agent C│
└───┬────┘
▼
┌──────┐
│ 输出 │
└──────┘
适合:流程固定、有交接
风险:错误会沿链条放大
③ 对等辩论 / 双编程(Peer)
┌──────────┐ ┌──────────┐
│ 实现者 A │ │ 实现者 B │
│(独立) │ ✗✗✗ │(独立) │ ← 禁止交流
└────┬─────┘ └─────┬────┘
│ 产出1 产出2 │
└────────┬────────────┘
▼
┌────────────┐
│ 差异比对 │ ← 确定性代码,不是 LLM
└─────┬──────┘
▼
┌────────────┐
│ 仲裁者 │ ← 只处理差异项
└────────────┘
适合:需要验证、需要第二意见
风险:成本 2×;若共享上下文则完全失效
| 拓扑 | 独立性 | 成本 | 适用场景 |
|---|---|---|---|
| 监督者 | 中 | N+1 | 任务可清晰拆分 |
| 流水线 | 低 | N | 流程固定(数据准备→分析→报告) |
| 对等/双编程 | 高 | 2 | 需要验证 |
在临床统计里,对等拓扑的适用性远高于另外两种 —— 因为"验证" 本身就是这个行业的核心动作。
22.4 角色定义:契约比人设重要
角色不是"你是一个资深统计师"这种人设,而是一份契约:
# clinic/agent_role.py(节选)
@dataclass
class RoleCard:
"""一个 Agent 角色的完整定义。"""
name: str
goal: str # 一句话目标
system_prompt: str # 行为规范
tools: tuple[str, ...] # ★ 只能看到这些工具(最小权限)
inputs: tuple[str, ...] # 需要哪些输入(key)
outputs: tuple[str, ...] # 必须产出哪些 key
forbidden: tuple[str, ...] = () # ★ 明确不能做什么
max_steps: int = 12
三个字段最容易被忽略,但最重要:
① tools —— 最小权限
IMPLEMENTER = RoleCard(
tools=("describe_dataset", "read_dataset", "summarize_by_group"),
# 注意:没有 write_report、没有 write_xpt
...
)
REVIEWER = RoleCard(
tools=("describe_dataset", "compare_datasets", "run_qc_checks"),
# 注意:没有 read_dataset 的原始读取权?不 —— 审查者需要读,但不能写
...
)
② forbidden —— 把"不能做"写明
REVIEWER = RoleCard(
forbidden=(
"不得修改被审查的代码或数据",
"不得与实现者交流(独立性要求)",
"不得基于推测给出结论,必须引用工具返回的数据",
),
)
③ outputs —— 输出必须结构化
IMPLEMENTER.outputs = ("derived_adsl", "derivation_log", "known_limitations")
⚠️ 最常见的多 Agent 故障是"角色重叠": 两个 Agent 都能写文件 → 互相覆盖; 两个 Agent 都能下结论 → 结论矛盾后无人负责。
tools的最小权限划分,就是防止这个的第一道闸。
22.5 通信:结构化消息,不要自由文本
如果 Agent 之间传自由文本,会发生两件事:信息丢失和互相客气。
# ❌ 自由文本消息
agent_a → agent_b: "我把 ADSL 派生好了,你帮我看看有没有问题,谢谢!"
# agent_b 收到这条,不知道:
# - "派生好了"是哪个文件?
# - "看看"要看什么?结构?数值?逻辑?
# - 期望我输出什么?
# ✅ 结构化消息
@dataclass
class Message:
kind: str # request / result / challenge / verdict
from_role: str
to_role: str
payload: dict
refs: tuple[str, ...] = () # 引用的产物 key,便于追溯
四类消息就够了:
| kind | 用途 | 必需的 payload 字段 |
|---|---|---|
request |
请求做某事 | task、expected_output、deadline |
result |
交付产物 | outputs(key→值)、evidence(工具调用记录) |
challenge |
提出质疑 | target(针对哪一项)、reason、expected、actual |
verdict |
裁决 | decision、rationale、confidence |
challenge 的结构是关键 —— 它强制质疑方说清"我期望什么、实际是什么":
Message(
kind="challenge",
from_role="reviewer", to_role="implementer",
payload={
"target": "TRTEDT",
"reason": "第 810 行的填充逻辑与本实现的假设不一致",
"expected": "缺失 EXENDTC 时用下一条 EXSTDTC - 1 天补全",
"actual": "当前实现直接取 max(EXENDTC),未做补全",
"affected_rows": ["01-701-1180", "01-701-1234"],
},
refs=("derivation_log",),
)
这样一条质疑是完全可核查的 —— 有具体变量、具体行、具体受影响的受试者。
🔥 有了结构化消息,"两个 Agent 互相客气"这种退化就消失了:
challenge必须带expected和actual,编不出来。
22.6 差异是信号,不是错误
多 Agent 最容易被误用的地方:看到两个 Agent 结果不一致,就急着"让它们达成一致"。
在临床统计里,差异恰恰是你最想看到的东西:
两个独立实现,结果一致 → 大概率都对(但可能是同一个前提都错了)
两个独立实现,结果不同 → ★ 定位到具体变量 → 人工审查 → 这是验证的核心价值
差异分级与处理
@dataclass
class Discrepancy:
key: str
a: Any
b: Any
level: str # critical / major / minor / explainable
root_cause: str | None = None
| 级别 | 含义 | 处理 |
|---|---|---|
critical |
影响主要结论 | 必须查清 |
major |
影响部分输出 | 查清或记录理由 |
minor |
格式/精度差异 | 记录 |
explainable |
口径不同但都成立 | 记录口径差异,写进 ADRG |
🔥 最后一条值得展开:第 13 章 case05 里有个真实例子 ——
TRT01A(实际治疗组)按DM.ACTARM还是按"最高剂量"派生, 两种口径都说得通,差 12 个人。 这类差异不需要"修好",需要的是"记录下来并说明选择了哪一个"。Agent 仲裁器在这里的正确行为是:标注为
explainable, 附上两种口径的受试者差异清单,交由人工决策。
仲裁的三种手段(按优先级)
- 回到证据 —— 两个 Agent 一起看工具返回的原始数据。多数分歧在这一步就解决了
- 确定性裁决 —— 用普通代码判断(比如"按 SAP 第 6.2 节口径"是明确规则,不需要 LLM 投票)
- 交人工 —— 前两步解决不了,就是需要人类判断的地方(这正是你想要的信号)
⚠️ 不要让 LLM 投票决定分歧。"少数服从多数"在这里毫无意义 —— 3 个模型一致认为 2+2=5 也不会让它变成 5。 分歧要么回到证据,要么交给人。
22.7 双编程 Agent 对:最有价值的模式
这个模式直接对应临床程序员最熟悉的工作方式。
┌──────────────────────────────────────────────────────────────┐
│ 共享输入(受试者级原始数据 + 派生规格) │
│ ⚠️ 注意:只共享【输入】,不共享【上下文】 │
└───────────────┬──────────────────────────┬───────────────────┘
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ 实现者 A │ │ 实现者 B │
│ · 独立设计算法 │ │ · 独立设计算法 │
│ · 只看到输入规格 │ │ · 只能看到输入规格 │
│ · 禁止看 A 的代码 │ │ · 禁止看 B 的代码 │
└─────────┬──────────┘ └──────────┬─────────┘
│ 派生结果 A │ 派生结果 B
└────────────┬───────────────┘
▼
┌──────────────────┐
│ compare_datasets│ ← 确定性代码(clinic.qc)
│ 逐变量比对 │
└────────┬─────────┘
▼
┌───────────────────────┐
│ 差异分级 + 根因分析 │
│ critical→人工审查 │
│ explainable→记录口径 │
└───────────────────────┘
实现上的三个关键约束
PAIR_POLICY = {
"share_inputs": True, # 共享输入数据
"share_context": False, # ★ 绝不共享对话历史
"share_artifacts": False, # ★ 交付前互不可见产出
"communication": "none", # ★ 实现阶段零交流
}
communication: "none" 是双编程的生命线。 一旦允许交流,
两个实现就会趋同,验证价值归零。
与人类双编程的对照
| 维度 | 人类双编程 | Agent 双编程 |
|---|---|---|
| 独立性 | 靠纪律(不看对方代码) | 靠架构强制(上下文物理隔离) |
| 速度 | 两人×数天 | 分钟级 |
| 出错模式 | 都理解错同一处规格 | 同样会都理解错(所以规格本身也要审) |
| 差异处理 | 坐下来讨论 | 结构化 challenge + 确定性比对 |
| 人的角色 | 两个实现者 | 审计者 + 仲裁者 |
🔥 注意最后一行:Agent 双编程并没有让人退出,而是把人的位置从 "实现"移到了"审计"。省下的是打字和调试,没有省下的是判断责任。 在临床场景里,这个责任本来就不该被省下。
22.8 成本与延迟
MULTI_AGENT_BUDGET = {
"2 agents": {"token": "2.2×", "latency": "1.1×"}, # 并行时延迟接近 1×
"3 agents": {"token": "3.4×", "latency": "1.3×"},
"5 agents": {"token": "5.8×", "latency": "1.6×"},
}
(token 系数略大于 N,因为要加上通信消息与汇总的开销)
所以只在关键路径上使用多 Agent:
✅ 值得用 2 个 Agent 的地方
· 递交物的最终验证(ADaM 数据集、关键 TLF)
· 高风险判断(人群定义、缺失值处理规则)
❌ 不值得的地方
· 生成 20 张例行表格(脚本就好)
· 数据体检(单 Agent 足够)
· 文档格式化
一句话:把多 Agent 的预算花在"错了会出大事"的地方。
22.9 常见误区
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 共享上下文做验证 | 验证完全失效 | 上下文物理隔离 |
| 用"人设"定义角色 | 职责不清,互相甩锅 | 用 tools/outputs/forbidden 定义契约 |
| 自由文本通信 | 信息丢失、互相客气 | 结构化消息(4 种 kind) |
| 让 LLM 投票裁决分歧 | 三个都错也变不成对 | 回到证据,或交人工 |
| 强求两个 Agent 结果一致 | 丢掉了最宝贵的差异信号 | 差异分级,explainable 记录口径 |
| 所有环节都上多 Agent | 成本 3×,调试地狱 | 只用在关键路径 |
| 让 Agent 互审代码找 bug | 共享上下文时收益递减 | 用确定性测试 + 差异比对 |
22.10 本章小结
- 能用加工具解决的,不要加 Agent
- 多 Agent 的价值来自独立性,不是来自数量
- 角色是契约不是人设 ——
tools/outputs/forbidden - 通信必须结构化 —— 尤其是
challenge要带 expected/actual - 差异是信号 —— 分级处理,
explainable的写进口径说明 - 仲裁靠证据和规则,不靠投票
- 双编程的生命线是"零交流"
下一章:Agent 要和 EDC、统计平台、外部 API 打交道时,怎么不出事。
22.11 动手练习
- 跑
cases/case11_双编程Agent对.py,观察两个实现者的差异清单。 然后故意让两个 Agent 共享上下文(改PAIR_POLICY), 看差异是否消失 —— 这能直观理解"独立性"的价值。 - 给
Challenge消息加一个severity字段,让仲裁器按级别处理。 - 设计一个"三方拓扑":实现者 A + 实现者 B + 审查者 C。 注意 C 应该先看谁的结果?为什么?(提示:想想会不会引入偏见)
- 用
clinic.qc.compare_frames给双编程结果做逐变量比对, 输出一张"变量 × 一致性"表,并统计explainable占比。 - 思考题:如果两个 Agent 对同一条规则的理解都错了(规格本身有歧义), 双编程能发现吗?如果不能,需要补什么机制?