临床 Python 进阶路线图
⌕ /
路线图 › 第六阶段 · Agent 工程化

第 22 章 · 多 Agent 协作

本章目标:判断什么时候真的需要多个 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 角色定义:契约比人设重要

角色不是"你是一个资深统计师"这种人设,而是一份契约:

Python
# 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 —— 最小权限

Python
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 —— 把"不能做"写明

Python
REVIEWER = RoleCard(
    forbidden=(
        "不得修改被审查的代码或数据",
        "不得与实现者交流(独立性要求)",
        "不得基于推测给出结论,必须引用工具返回的数据",
    ),
)

③ outputs —— 输出必须结构化

Python
IMPLEMENTER.outputs = ("derived_adsl", "derivation_log", "known_limitations")

⚠️ 最常见的多 Agent 故障是"角色重叠": 两个 Agent 都能写文件 → 互相覆盖; 两个 Agent 都能下结论 → 结论矛盾后无人负责。 tools 的最小权限划分,就是防止这个的第一道闸。


22.5 通信:结构化消息,不要自由文本

如果 Agent 之间传自由文本,会发生两件事:信息丢失和互相客气。

Python
# ❌ 自由文本消息
agent_a → agent_b: "我把 ADSL 派生好了,你帮我看看有没有问题,谢谢!"

# agent_b 收到这条,不知道:
#   - "派生好了"是哪个文件?
#   - "看看"要看什么?结构?数值?逻辑?
#   - 期望我输出什么?
Python
# ✅ 结构化消息
@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 的结构是关键 —— 它强制质疑方说清"我期望什么、实际是什么":

Python
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 结果不一致,就急着"让它们达成一致"。

在临床统计里,差异恰恰是你最想看到的东西:

文本
两个独立实现,结果一致  → 大概率都对(但可能是同一个前提都错了)
两个独立实现,结果不同  → ★ 定位到具体变量 → 人工审查 → 这是验证的核心价值

差异分级与处理

Python
@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, 附上两种口径的受试者差异清单,交由人工决策。

仲裁的三种手段(按优先级)

  1. 回到证据 —— 两个 Agent 一起看工具返回的原始数据。多数分歧在这一步就解决了
  2. 确定性裁决 —— 用普通代码判断(比如"按 SAP 第 6.2 节口径"是明确规则,不需要 LLM 投票)
  3. 交人工 —— 前两步解决不了,就是需要人类判断的地方(这正是你想要的信号)

⚠️ 不要让 LLM 投票决定分歧。"少数服从多数"在这里毫无意义 —— 3 个模型一致认为 2+2=5 也不会让它变成 5。 分歧要么回到证据,要么交给人。


22.7 双编程 Agent 对:最有价值的模式

这个模式直接对应临床程序员最熟悉的工作方式。

文本
┌──────────────────────────────────────────────────────────────┐
│  共享输入(受试者级原始数据 + 派生规格)                       │
│  ⚠️ 注意:只共享【输入】,不共享【上下文】                     │
└───────────────┬──────────────────────────┬───────────────────┘
                ▼                          ▼
    ┌────────────────────┐      ┌────────────────────┐
    │  实现者 A           │      │  实现者 B           │
    │  · 独立设计算法     │      │  · 独立设计算法      │
    │  · 只看到输入规格   │      │  · 只能看到输入规格  │
    │  · 禁止看 A 的代码  │      │  · 禁止看 B 的代码   │
    └─────────┬──────────┘      └──────────┬─────────┘
              │ 派生结果 A                 │ 派生结果 B
              └────────────┬───────────────┘
                           ▼
                 ┌──────────────────┐
                 │  compare_datasets│  ← 确定性代码(clinic.qc)
                 │  逐变量比对       │
                 └────────┬─────────┘
                          ▼
              ┌───────────────────────┐
              │  差异分级 + 根因分析   │
              │  critical→人工审查     │
              │  explainable→记录口径  │
              └───────────────────────┘

实现上的三个关键约束

Python
PAIR_POLICY = {
    "share_inputs": True,        # 共享输入数据
    "share_context": False,      # ★ 绝不共享对话历史
    "share_artifacts": False,    # ★ 交付前互不可见产出
    "communication": "none",     # ★ 实现阶段零交流
}

communication: "none" 是双编程的生命线。 一旦允许交流, 两个实现就会趋同,验证价值归零。

与人类双编程的对照

维度 人类双编程 Agent 双编程
独立性 靠纪律(不看对方代码) 靠架构强制(上下文物理隔离)
速度 两人×数天 分钟级
出错模式 都理解错同一处规格 同样会都理解错(所以规格本身也要审)
差异处理 坐下来讨论 结构化 challenge + 确定性比对
人的角色 两个实现者 审计者 + 仲裁者

🔥 注意最后一行:Agent 双编程并没有让人退出,而是把人的位置从 "实现"移到了"审计"。省下的是打字和调试,没有省下的是判断责任。 在临床场景里,这个责任本来就不该被省下。


22.8 成本与延迟

Python
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 本章小结

  1. 能用加工具解决的,不要加 Agent
  2. 多 Agent 的价值来自独立性,不是来自数量
  3. 角色是契约不是人设 —— tools/outputs/forbidden
  4. 通信必须结构化 —— 尤其是 challenge 要带 expected/actual
  5. 差异是信号 —— 分级处理,explainable 的写进口径说明
  6. 仲裁靠证据和规则,不靠投票
  7. 双编程的生命线是"零交流"

下一章:Agent 要和 EDC、统计平台、外部 API 打交道时,怎么不出事。


22.11 动手练习

  1. 跑 cases/case11_双编程Agent对.py,观察两个实现者的差异清单。 然后故意让两个 Agent 共享上下文(改 PAIR_POLICY), 看差异是否消失 —— 这能直观理解"独立性"的价值。
  2. 给 Challenge 消息加一个 severity 字段,让仲裁器按级别处理。
  3. 设计一个"三方拓扑":实现者 A + 实现者 B + 审查者 C。 注意 C 应该先看谁的结果?为什么?(提示:想想会不会引入偏见)
  4. 用 clinic.qc.compare_frames 给双编程结果做逐变量比对, 输出一张"变量 × 一致性"表,并统计 explainable 占比。
  5. 思考题:如果两个 Agent 对同一条规则的理解都错了(规格本身有歧义), 双编程能发现吗?如果不能,需要补什么机制?