多模型研讨 Agent 实战:让 GPT、Claude、Qwen 交叉验证(LangGraph + FastAPI)
摘要:让多个异构大模型先独立作答、再互相挑错、最后由首席模型总结。本文讲三阶段怎么落地,以及开发过程中踩到的三个坑------深度思考模型超时、SSE 乱序顶掉内容、研讨记录丢气泡。
一、为什么要让模型互相挑错
单个模型回答问题时,「自我检查」几乎是无效的。
原因很简单:同一个模型、同一套权重、同一批训练数据,复查十遍也还是同一个盲区。你让它「再看看有没有问题」,它只会换个说法把原答案重复一遍。
换一个思路:让不同厂商的模型同时回答,然后互相挑错。
GPT、Claude、Qwen、DeepSeek 的训练数据、对齐策略、知识截止时间都不一样。它们的分歧本身就是一个信号------分歧出现的地方,往往就是原答案最不可靠的地方。
这就是多模型研讨(Multi-Model Debate)的基本假设。我把它做成了一个完整可跑的项目。
二、整体架构
三个阶段是线性推进的:
- 第一轮 · 独立作答:所有副模型并行回答,彼此看不到对方,保证观点独立
- 第二轮 · 交叉研讨:每个模型读到其他人的答案,补充或反驳
- 第三轮 · 首席总结:主模型综合所有观点,剔除错误信息,输出一份结论
编排用 LangGraph 的 StateGraph,三个阶段是三个节点;模型调用是 IO 密集型,所以并发用线程池;进程内的请求上下文用 contextvars 隔离(Trace ID、LLM 客户端、模型名)。
整个流程通过 SSE 推给前端,三个阶段的产出按轮次分块渲染。
三、第一轮:并行独立作答
关键点是互不可见。每个专家拿到的 prompt 里只有原始问题,没有别人的答案:
python
def run_first_round(model_name):
client = get_client_for_model(model_name)
prompt = f"你是一名专家,请针对以下问题,给出你独立、专业的见解。\n问题:{query}"
resp = client.chat.completions.create(
model=resolve_model_id(model_name),
messages=[{"role": "user", "content": prompt}],
max_tokens=2048
)
return model_name, resp.choices[0].message.content
调度用 asyncio.as_completed,谁先返回谁先推给前端,不用等最慢的那个:
python
tasks = [run_first_round(m) for m in staff_models]
for future in asyncio.as_completed(tasks):
name, content = await future
yield sse({'event': 'speak', 'stage': 1, 'model': name, 'content': content})
四、第二轮:交叉研讨
每个模型拿到其他人的答案(不含自己的),要求指出漏洞或补充:
python
context_str = ""
for m, ans in first_round.items():
if m != model_name:
context_str += f"【{m} 的观点】:\n{ans}\n\n"
prompt = (f"针对问题:{query}\n\n其他专家的第一轮观点如下:\n{context_str}\n\n"
f"请你基于以上信息,发表你的补充意见,或者指出他们可能存在的漏洞。")
实际跑下来的观察:大约 1/5 的发言是「我同意前面那位」,但只要有一个人指出问题,往往就是真问题。
五、第三轮:首席总结
把所有人的观点拼起来交给主模型,要求它去伪存真,并且不要提及专家名字,直接给结论------否则输出会变成「A 说了什么、B 说了什么」的会议纪要,用户要的是答案。
六、踩坑一:深度思考模型超时
接入豆包 Seed 2.1 之后,简单问一句「现在几点」都要 20~60 秒,直接顶穿 LLM_TIMEOUT(默认 60 秒),前端只能看到一个错误。
原因是这一代模型默认开启深度思考,会在正式回答前先生成一大段推理内容。关掉它:
json
{"thinking": {"type": "disabled"}}
效果对比(同样是问「回复两个字:你好」):
| 默认 | 关闭思考 | |
|---|---|---|
| 耗时 | 20~60s | 1.65s |
| completion_tokens | 99 | 2 |
我把这个参数做成了注册表的一个可选字段,加模型的时候顺手写进去,不用改任何代码:
python
def get_model_extra_body(model_name: str) -> dict:
config = get_model_registry().get(model_name) or {}
extra = config.get("extra_body")
return extra if isinstance(extra, dict) else {}
调用时透传:
python
extra = get_model_extra_body(model_name)
kwargs = {"extra_body": extra} if extra else {}
resp = client.chat.completions.create(..., **kwargs)
七、踩坑二:SSE 乱序把内容顶掉
现象:研讨到第二轮,第一轮的内容会突然消失一部分。
原因是前端靠一个 currentStage 变量记录「现在是第几轮」,收到内容就往对应的容器里写。但 SSE 帧到达顺序不保证,一旦第二轮的分隔帧先到,后续第一轮的内容就被写进了第二轮的容器------看起来就是「被顶掉」。
解法:不要依赖到达顺序,让每一帧自己带上下标。
python
# 后端:每个 speak 帧都带上 stage
yield sse({'event': 'speak', 'stage': 1, 'model': name, 'content': content})
javascript
// 前端:用帧里的 stage 定位轮次
const round = payload.stage || currentRound;
const target = ensureBubble(round, payload.model);
typewriterEffect(target, payload.content);
这个坑的教训是:流式场景下,任何「靠全局状态变量推断位置」的写法都是脆的,状态必须跟着数据走。
八、踩坑三:研讨记录丢气泡
研讨结束后,整场讨论被拼成一大段 Markdown,作为一条消息存库:
python
full_history = "### --- 第一轮 ---\n\n**gpt-6**:\n...\n\n### 👑 首席总结\n"
save_message(db, session_id, "assistant", full_history)
问题:实时渲染时有专家头像、模型名标签、轮次分隔线,一旦切到别的会话再切回来,全部消失,只剩一大段纯文本。
改成每位专家单独存一条,元信息塞进已有的 thought_process 字段(不用改表结构):
python
for m, ans in state["first_round"].items():
save_message(db, session_id, "assistant", ans, f"GROUP_SPEAK:1:{m}")
for m, ans in state["discussion"].items():
save_message(db, session_id, "assistant", ans, f"GROUP_SPEAK:2:{m}")
save_message(db, session_id, "assistant", summary, f"GROUP_SUMMARY:{chief_model}")
前端读历史时按标记还原成同样的 DOM:
javascript
if (tp.startsWith('GROUP_SPEAK:')) {
const [, stage, model] = tp.split(':');
histSpeak(stage, model, msg.content);
} else if (tp.startsWith('GROUP_SUMMARY:')) {
histSummary(model, msg.content);
} else {
addMessage(msg.content, msg.role);
}
九、实测数据
本地跑的单次调用耗时(不含前端渲染):
| 场景 | 耗时 |
|---|---|
| 查时间(走工具) | 1.52s |
| 算数 123*456(走工具) | 1.09s |
| 一场完整研讨(3 模型 / 4 次发言 / 含总结) | 19.3s |
研讨的耗时主要取决于最慢的那个专家,因为每一轮都要等所有人说完才能进下一轮。
十、还没做的
- 量化评测:目前只有主观验证,没有跑基准。下一步想用 HotpotQA + 事实核查集,比较「单模型」 vs 「研讨模式」在相同 token 预算下的准确率。
- 研讨过程流式:现在是每个阶段结束才推送整段内容,理想状态是边生成边推。
- 动态选人:根据问题类型自动决定用几个专家、用谁。
项目地址:github.com/3038816077/...(MIT,Docker 一条命令能跑)
如果这个思路对你有用,欢迎 star 支持一下。