关于 Model Council
Model Council 是一个 MCP 服务,让助手在同一对话中向多个兼容 OpenAI 或 Anthropic 的模型提问、传递彼此答案并汇总结论。它支持多轮交叉评议、文件或图片材料,以及可选的反方论证席位。
主要功能
- 01向单个或多个模型提问
- 02并行收集并比较回答
- 03多轮传递回答进行评议
- 04向模型提供文件、文本和图片
- 05设置反方席位参与讨论
- 06查看成员配置并探测模型
适用场景
- 01比较多个模型对方案的意见
- 02让模型交叉检查彼此的回答
- 03请模型审阅规格、日志或差异文件
- 04围绕计划收集并检验反对意见
MCP 相关
作为 MCP 服务提供 ask、ask_all、revise 等工具,使宿主助手可调用配置的模型端点,并将回答作为工具结果纳入对话。服务本身不访问助手的会话记录;每次调用都需传入所需上下文。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
uvx model-council-mcpCOUNCIL_MODELSComma-separated short ids for the models to seat, e.g. 'gpt5,glm'. Each id then takes its own <ID>_BASE_URL, <ID>_API_KEY, <ID>_MODEL and optional <ID>_FORMAT (openai or anthropic). Omit to use a config file instead.COUNCIL_CONFIGPath to a JSON config file, for rosters large enough that several models share one endpoint. Defaults to ~/.config/model-council/config.json when that exists.本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
工具
6收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| model | enum | 是 | chatgpt | glm |
| prompt | string | 是 | |
| system | any | 否 | |
| materials | any | 否 |
详细介绍
Model Council
English | 简体中文
一个让其他大模型加入你讨论的 MCP 服务。你的助手会向它们提问, 将它们的回答作为工具结果读取,再把这些回答来回传递给它们进行 评议,最后给你一份汇总后的结论——这一切都在一次普通对话中完成, 无需复制粘贴。
你的助手主持评议会。成员数量不限,可以混用兼容 OpenAI 和兼容 Anthropic 的端点——托管 API、自行运行的网关、本地服务,或上述类型的任意组合。
工具
| 工具 | 功能 |
|---|---|
ask(model, prompt, materials?) | 按 ID 向一位成员提问 |
ask_all(prompt, models?, rounds?, guests?, materials?, steelman?) | 向所有成员(或指定的成员子集)并行提出同一个提示词,并排展示回答。rounds=2 会将其变成一场讨论;steelman 会加入一位固定反对者,每轮都进行反驳;guests 会加入你已有的回答;materials 会提供一个文件供它们阅读 |
revise(prompt, answers, round?, materials?) | 自行再进行一轮:向成员展示上一轮的所有发言——包括只有你能提供的回答——并让它们返回修订后的回答 |
revision_prompt(prompt, answers, seat, materials?) | 提供给你自己的席位用于下一轮的提示词,与成员收到的内容逐字相同。不进行网络调用 |
list_council() | 成员列表:ID、端点、权重、可向每位成员展示的内容、每位成员使用的出站路由、调用额度,以及是否已就绪。不进行网络调用 |
probe_models(model?) | 查询提供商的 /models 路由,了解其实际提供的 ID |
成员是无状态的,无法看到你的对话,因此主持人会在每次调用时传递它们所需的全部内容。这正是交叉评审能够奏效的原因:
它会将一位成员的回答放入另一位成员的 prompt 中。
给评议会提供可阅读的材料
问题通常与某些内容有关——规范、日志、差异、截图。显而易见的做法是把它粘贴到 prompt 中,但这恰恰是代价最高的做法,而且代价藏在没人留意的地方:prompt 是主席编写的参数,因此每次调用时,一份长文档都会在生成的 token 中完整复制一遍;而成员收到的内容,则取决于主席能复述出什么。对于 40 页的文档,这绝不可能可靠地还原原文。评议会审查的是转述内容,而不是原文,记录中也不会显示这一区别。
materials 可以直接指定材料:
ask_all(
prompt="Where would this break under load?",
materials=[
{"label": "The design", "path": "/abs/path/design.md"},
{"label": "Latency graph", "path": "/abs/path/p99.png"},
{"label": "My summary", "text": "…something with no file behind it…"},
],
rounds=2,
)
服务端会读取每份材料,并将其置于问题之前,每位成员、每一轮的位置都相同。这个位置不只是为了展示:
- 字节完全相同。 每位成员讨论的都是同一份副本,这样他们的分歧才是围绕文档本身,而不是因为各自收到的版本不同。
- 可缓存的前缀。 从开头到材料末尾的所有内容,在同一场讨论的每次调用中都相同。Anthropic 格式会通过一个位于边界处的
cache_control断点明确标记这一点;无论如何,按前缀自行缓存的端点都能获得所需的结构。如果某位成员使用的网关不接受此字段,请为其设置"cache": false。 - 主席只付出一次代价。 指定路径只需主席写一行,而无需复制整份文档;
ask_all会自动将材料带入第 2 轮和第 3 轮。
图像最能体现这一点的重要性。 没有图像时,主席只能用文字描述截图——之后每位成员读到的都是同一份描述,因此主席误读的任何内容都会被整个评议会一并误读,交叉审查也无法补救。这是唯一一种传递不当就会悄然削弱评议会独立性的输入。Png、jpeg、gif 和 webp 会发送给所有能够查看图像的成员;无法查看图像的成员应设置为 "vision": false,这样在包含图像的调用中,它们会选择跳过,而不会假装看过图像,只根据文本作答。记录会注明哪些成员跳过了。
如何判断哪些成员无法查看图像: 无法查看图像的成员很少会主动说明。构建过程中测试的一个 Anthropic 兼容网关接受了图像块,却将其丢弃,随后模型还说出了一个颜色——而不是按提示中对此情况的明确要求回答“我没有收到图像”。同一端点传递的文本材料则完好无损,因此调用看起来没有任何问题。给成员看一张内容明确的图片,并问它图里有什么;自信却错误的回答就是症状,解决办法是设置 "vision": false。
有两点限制值得了解。材料无法读取时,调用会停止——如果评议会讨论的是一份它根本没收到的文档,它仍然会流畅作答,读起来和收到文档时毫无区别。而 revision_prompt 会在成员收到文件的位置点出你的文件名,而不是把文件内容重新粘贴出来;因此,在让它处理其余内容之前,请先在你自己的席位上打开这些文件。
此服务端是否会读取路径,取决于它的启动方式;通过 HTTP 时则不会,除非运营者指定了一个目录。list_council 会在表格上方的一行中说明具体情况。
轮次
ask_all(prompt, rounds=2) 会为你执行交叉评审。第 1 轮是常规的并行提问。第 2 轮会把问题以及第 1 轮的每个回答——包括自己的和其他成员的,逐字不改——一并发回给每位成员,请它们修订:采纳正确之处,纠正错误之处,并说明仍有分歧的地方及原因。记录会按轮次返回,因此你可以看到哪些成员改变了看法,哪些坚持己见。
把上一轮的内容带回来,就是整个机制的关键。成员不会记住不同调用之间的内容,所以没有这一步,第二轮就只是把同一个问题问两遍。最多 3 轮;每一轮都会为每位成员增加一次调用,而且提示词会比上一轮更长。因此,调查意见时选 1 轮就好;如果分歧本身才是重点,则选 2 轮。
rounds 必须在提出任何问题之前选定,这是唯一的问题:你还没读过第一轮,就得决定是否进行第二轮;结果一致的评审团,花费却和仍在争论的评审团完全相同。如果你想先看看结果,就用 rounds=1 提问,然后通过 revise 购买下一轮——它每次恰好再运行一轮,你认为值得就可以运行任意多次,而且它本身没有上限。先读再决定通常更省:再来一轮会为每位成员增加一次完整调用,而 revise 只会把你写回去的回答作为成本。ask_all 中的 3 限制的是单次调用的花费,而不是讨论可进行的轮数;达到上限时,记录会明确说明这一点。
成员还可以采用不同的权重,适用于评审团并非人人平等的常见情况。
持续提出异议
议会大多达成一致,而达成一致这件事恰恰是它所产生的信息量最少的部分。以上一切都在阻碍趋同——成员彼此匿名,从未被告知权重,而且结束时的指示还告诉他们不要让步——但这些都没有造成任何分歧压力。认为计划没问题的成员,不会主动提出对计划最有力的反对意见。
steelman 安排一席:
ask_all(prompt, rounds=3, steelman={})
每轮由一名成员针对议会达成一致的方案,写出最有力的反对理由,然后像普通的匿名回答一样发回给所有人。下一轮,他们就必须对此作出回应。
不会要求任何成员为自己并不认同的立场辩护。 这就是界限所在,也正是它与辩论模式的区别。成员说的仍然是他们自己的想法;这个任务只多出一次他们并不知情的调用。如果给模型安排一个标注着“反对方”的席位,它会对论点打折看待,而不是认真回应——所以,论点的来源会在记录末尾的备注中告诉你,而论点本身则会交给他们。
它每轮都会发言,而不是只说一次。 这一点看似只是细节,实则不然。无法回应自身被驳斥的异议,只是被引用,而非真正呈现:它无法纠正别人对自己的误读,因此到了第三轮,议会争论的已经是它的转述,却把这当作回应。tenure 会让轮数少于默认值;如果席位提前退场,记录会注明这是由配置所致——因为没有解释的沉默,看起来恰好就像放弃了立场。
与此相对的一面也同样重要:异议最后说的话会成为记录中的最后一句,因此没有人被要求回应它,记录也会注明这一点。无人回应的异议,并不代表论点站得住脚——而是它从未经过审视。 设置 rounds=2 时,它说过的所有内容都属于这种情况,因此备注会指出能解决问题的设置,而不是让你付出代价后才自行发现。
请将它的产出视为这个议会能提出的最有力反对意见,而不要将其视为任何人认同该意见的证据。某个观点经回应后仍然成立,确实有一定分量;但它刚出现的那一轮里,同一个观点还不能说明什么。
默认由第一位成员撰写异议,同时也以自己的身份作答——据它所知,这两次调用彼此无关。使用 steelman={"model": "glm"} 指定另一位成员。
为已有答案安排席位
你的助手不只是主持者——它也可以回答问题;在 Claude Code 或 Codex 中,它还能启动子 Agent 作答。这些答案过去一直是放在议会回答的旁边,最后再由你手动比较。guests 会把它们放进议会之中:
ask_all(
prompt="What are the traps in this plan?",
guests=[{"label": "Subagent", "text": "<what your subagent answered>"}],
rounds=2,
)
第 1 轮会将它与成员的回答并列显示。从第 2 轮起,每位成员都会收到原文,并被要求针对它进行论证:
--- ANSWER C (does not revise between rounds) ---
the migration has no rollback path
区别仅此而已。不启用这个功能,模型就不会知道你的子 Agent 有自己的看法,而你只能自己比较——而这正是轮次机制已经做得更好的工作,因为它能让模型彼此回应,而不是对着虚空作答。
值得注意的事项:
- 原样传入文本,不要传摘要。 成员看到的内容与你传入的完全一致,而摘要并不是你想让他们批评的内容。
- 访客只发言一次。 没有人可以要求它修改,因此它不会在第 1 轮后再次出现。记录会注明这一点,因为席位无故消失会让人觉得它放弃了立场。
- 访客也算一个声音。 一位成员加一位访客,就足以进行真正的第二轮讨论——你的子 Agent 与一个模型之间的讨论仍然算讨论。
- 访客也适用
weight,权重范围与成员相同。 - 访客按调用单独设置。不会写入任何配置,也不会保留任何状态。
子 Agent 作为完整成员
访客只发言一次。若要让你自行生成的声音也能像其他成员一样持续参与——回答、阅读其他成员的发言、修改观点,一轮接一轮——你必须驱动各轮,因为此服务无法启动你的子 Agent。
revise 会按需运行一轮:
ask_all(prompt) → members answer round 1
...you spawn your subagent on the same prompt → its round-1 answer
revise(prompt, round=1, answers=[
{"model": "glm", "text": "<glm's round-1 answer>"},
{"model": "sol", "text": "<sol's round-1 answer>"},
{"label": "Subagent", "text": "<subagent's round-1 answer>"},
]) → members come back revised
...you re-run your subagent on the same material
revise(prompt, round=2, answers=[...round 2...]) → and so on
用 model 指定成员,才能进行修改:该成员会收到自己的上一条回答,并将其视为自己的回答,然后基于它继续,而不是重新作答。使用 label 的条目则代表名册以外的声音。
成员无法分辨其中的区别。 传给 revise 的外部声音会被呈现得和其他成员完全一样——只有一个字母,以及 --- ROUND-1 ANSWER C ---——因为它下一轮还会再次作答。这也是 ask_all 中的访客会被标记为已结束的原因。明明一个声音马上又要发言,却将其描述为已经结束,会误导正在辩论的模型。
用 revision_prompt 提示你的子 Agent,而不是用原始问题。
这是唯一会让循环出错的地方,而且失败时不会有任何提示:子 Agent 再次收到问题,只会简单复述自己之前的回答;记录看起来仍像是在讨论,却没有任何标记指出该席位从哪一轮起不再参与。
revision_prompt(prompt, answers=[...same as revise...], seat="Subagent", round=1)
它会返回成员们收到的完整内容——标记为它自己的上一条回答、其他所有人的回答,以及相同的结束指示,其中包括提醒它不要仅仅因为自己处于少数就放弃仍然相信的立场。正是这句话最大程度地避免了议事会沦为一团和气;若提示某个席位时没有这句话,就等于没有让它回答与其他成员相同的问题。此调用在本地运行,不会发出网络请求,因此可以与 revise 同时运行,而不必等它结束后再运行。
revise 没有轮数上限——ask_all 的 3 轮上限,是因为该调用会消耗自己的预算;而在这里,每一轮都由你决定是否付费运行。
ask_all(guests=…) | revise(answers=…) | |
|---|---|---|
| 调用次数 | 一次 | 每轮一次 |
| 你的声音 | 发言一次 | 每轮修改 |
| 由谁驱动各轮 | 服务 | 你 |
成员匿名辩论
在修改轮中,其他回答会以字母而非姓名呈现:
--- ROUND-1 ANSWER B ---
concurrent writes race
模型名称和权重是同一类信号,而且前者更强:模型对彼此的开发者往往抱有强烈的先验看法。只隐藏数值却显示品牌,不过是关闭较小的信号通道,同时保留较大的通道。成员看到的是论点,而这正是我们想让他们权衡的内容。
字母根据席位确定,因此始终稳定:对每位成员、每一轮而言,B 都是同一个席位,这样其中一位说“B 对索引的看法错了”,下一轮大家也能理解。
你仍然能看到姓名。 每份包含修改轮的记录末尾都会附上键:
[the members saw each other as letters, not names: A = GLM-5.3, B = GPT-5.6-sol,
C = Subagent. ...]
你是例外,因为只有你需要区分他们——查看谁改变了观点、谁坚持了立场,以及谁在谈论谁。
有一点需要如实说明:这只适用于服务写入的标签,不适用于模型在自己的回答中提到的姓名。由于另一种做法意味着要编辑其他成员准备批评的文本,这些内容会原样传入下一轮。这里的匿名机制会移除身份信号,但不会移除所有提及。
重试
调用发生暂时性故障时,会在报告故障前重试:HTTP 429 和 5xx,以及连接断开或超时。退避时间从 1s 开始按指数增长,并加入随机抖动;Retry-After 标头优先于该退避曲线——但如果其指定的时间超过 30s,调用就会停止并说明原因,而不是继续等待。对于不会因再次尝试而改变的故障——401、404、响应正文格式错误——会立即报告;重试只会消耗相同的配额,却得到相同的结果。默认重试 2 次;设置 retries: 0 即可恢复旧版的单次尝试行为。
成员用尽重试次数后,会将其错误作为该成员的回答返回,因此理事会的其他成员仍会回答——除非该成员还有其他去处,此时它会转而使用备用项。参见备用项。
安装
该服务已列入官方 MCP 注册表,名称为 io.github.Totti0135/model-council,因此可以浏览注册表的客户端能够在那里找到并添加它。若要改为手动配置,请继续阅读。
无需克隆仓库或创建虚拟环境,即可从 PyPI 运行。你需要安装 uv:
curl -LsSf https://astral.sh/uv/install.sh | sh
Claude Desktop
最简单的方法是使用桌面扩展。从最新版本
下载 model-council-<version>.mcpb,然后将其拖到“设置” → “扩展”中。应用会通过表单询问端点和密钥,并将密钥保存在操作系统的钥匙串中,因此磁盘上不会有任何文件存储这些密钥。表单支持配置两个模型;如需规模更大的理事会,请将其“Config file”字段指向 JSON 配置文件(见下文)。
若要改为手动配置,请编辑 claude_desktop_config.json
(“设置” → “开发者” → “编辑配置”),添加以下代码块,然后完全退出并重新打开应用——按 Cmd-Q,而不只是关闭窗口。工具菜单中出现 model-council 时,即表示配置成功。
{
"mcpServers": {
"model-council": {
"command": "uvx",
"args": ["model-council-mcp"],
"env": {
"COUNCIL_MODELS": "gpt5,glm",
"GPT5_BASE_URL": "https://your-openai-compatible-host/v1",
"GPT5_API_KEY": "sk-xxxxxxxx",
"GPT5_MODEL": "gpt-5",
"GLM_BASE_URL": "https://open.bigmodel.cn/api/anthropic",
"GLM_API_KEY": "xxxxxxxx",
"GLM_MODEL": "glm-4.6",
"GLM_FORMAT": "anthropic"
}
}
}
}
Claude Code
claude mcp add model-council -e GPT5_BASE_URL=... -e GPT5_API_KEY=... -- uvx model-council-mcp
其他 MCP 客户端
任何能够启动 stdio 服务的客户端都可以使用:运行 uvx model-council-mcp,并传入相同的环境变量。
通过 HTTP 为团队提供服务
以上所有方式都由每个人自己的 MCP 客户端启动一份服务实例,并使用各自的密钥进行配置。--http 则是另一种方式:在一处部署中保存一组密钥,为整个团队提供服务;团队成员只需配置 URL,无需提供任何密钥。
model-council-mcp --http --host 0.0.0.0 --allow 10.20.0.0/16
同事随后可以将其添加为远程服务,配置中无需包含任何敏感信息:
claude mcp add --transport http model-council http://council.internal:8000/mcp
// Claude Desktop and other clients
{ "mcpServers": { "model-council": { "type": "http", "url": "http://council.internal:8000/mcp" } } }
deploy/ 包含一个 Dockerfile、一个
compose 文件和一个 systemd unit。
谁可以调用
通过 stdio 时,操作系统会处理这个问题:启动进程的人已经持有密钥。HTTP 则打破了这一保障——现在,一个端口前面站着共享的提供商配额——因此,在 --allow 指定谁可以访问之前,服务拒绝在非回环地址上启动。回环地址无需标志,始终允许访问。
| 标志 | |
|---|---|
--allow | CIDR、裸地址,或名称 private(RFC1918)、loopback、any |
--trust-proxy | 可信任其 X-Forwarded-For 的反向代理 |
--allow-origin | 允许浏览器 Origin;可重复指定 |
--materials-root | materials 路径可指向的唯一目录;未设置时,HTTP 会拒绝路径 |
--trust-proxy 是最值得仔细阅读的选项。未设置时,X-Forwarded-For 会被完全忽略,由对端地址决定——在 nginx 后面时,对端就是 nginx,因此允许列表要么匹配所有人,要么一个人也匹配不到。设置后,客户端地址是链中最右侧的、不属于受信任代理的跳点,这样就能阻止调用者写入 X-Forwarded-For: 10.0.0.1 后直接绕过限制。
默认情况下,携带 Origin 标头的请求会被拒绝。MCP 客户端不是浏览器,不会发送该标头;网页则总会发送。允许列表会放行办公室网络中的每台机器,而这些机器上运行的浏览器都会代表当前打开的任意网页发出请求——Origin 用来区分这两者。
每个标志都有对应的环境变量(COUNCIL_ALLOW、COUNCIL_TRUST_PROXY、COUNCIL_HTTP_HOST,等等);--help 会列出它们。
通过 HTTP 传递材料
通过 stdio 时,materials 可以读取进程有权访问的任何文件,无需设置标志:调用者启动了此进程,因此本来就拥有这些访问权限——这与回环绑定可以跳过 --allow 的道理相同。通过 HTTP 时,这一理由不再成立。调用者变成了任何能够访问该端口的人;路径会让共享的 council 变成读取主机文件的途径,而读出的内容会被发送给外部提供商。因此,HTTP 部署会直接拒绝路径,调用者必须将内容作为 text 传递。
--materials-root /srv/council/material 只开放一个目录,并会在比较之前解析该目录,因此符号链接会根据其最终指向的位置进行判断。
此功能不做什么
允许列表是网络边界,不是身份验证。边界内的任何人都无需凭据即可调用,因此无法将用量归属到具体人员、按人员限流,或撤销某个人的访问权限。这是有意为之的取舍——它让客户端配置只需填写一个裸 URL——但这意味着网络必须是一个真正值得信任的边界,而且如果 VPN 允许承包商接入,或 CI 运行器位于同一子网,这种信任就不再成立。
如果需要按人员归属用量或配额,请在此服务后面部署一个大模型网关(LiteLLM、one-api,或组织已在使用的其他网关),并为每个调用者分配各自的虚拟密钥;也可以在支持 SSO 的反向代理后运行此服务。
在公布 URL 之前,还有一点值得了解:此服务会将收到的所有文本转发给外部提供商。没有凭据的共享端点,会成为所有可访问它的人向外传输数据的通道。
位于反向代理之后
ask_all 搭配 rounds=2 是一个耗时较长的请求——涉及多个模型,每个模型还要尝试多次,每次调用最长可达 COUNCIL_TIMEOUT(180s)。默认的代理超时设置会在服务完成之前很久就中断请求:
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_buffering off; # the transport streams; buffering defeats it
proxy_read_timeout 900s;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
然后启动服务时,将 --trust-proxy 设置为代理的地址,否则允许列表看到的永远只有代理。
配置 council
分为两层,这样多个模型可以共用一个端点,而不必重复填写凭据:
- 提供方 — 一个端点:
base_url+api_key+ 它使用的线缆格式 - 成员 — 某个提供方上的一个模型,通过短 ID 寻址
配置按明确程度优先采用以下来源:COUNCIL_CONFIG 指向的文件;否则,COUNCIL_MODELS 指定的成员列表;否则,~/.config/model-council/config.json 中的配置文件;最后才是内置的默认成员列表。
明确指定的配置会优先于自动发现的配置,这是有意为之——客户端刚传给服务端的设置,不应被你遗留在某处的配置文件悄悄覆盖。
list_council() 总会报告最终采用了哪个来源。
环境变量
COUNCIL_MODELS 列出各 ID;每个 ID 对应的变量名由该 ID 转为大写,并将非字母数字字符替换为下划线(my-model → MY_MODEL_BASE_URL)。
COUNCIL_MODELS=gpt5,glm
GPT5_BASE_URL=https://your-openai-compatible-host/v1
GPT5_API_KEY=sk-xxxxxxxx
GPT5_MODEL=gpt-5
GLM_BASE_URL=https://open.bigmodel.cn/api/anthropic
GLM_API_KEY=xxxxxxxx
GLM_MODEL=glm-4.6
GLM_FORMAT=anthropic
每个成员可配置:_BASE_URL、_API_KEY、_MODEL、_FORMAT、_LABEL、_WEIGHT、_MAX_TOKENS、_TEMPERATURE、_TIMEOUT、_RETRIES、_RETRY_BACKOFF、_HEADERS(一个 JSON 对象)、_PROXY、_VISION、_CACHE、_ENABLED。全局变量:
COUNCIL_TIMEOUT、COUNCIL_RETRIES、COUNCIL_RETRY_BACKOFF、COUNCIL_PROXY、COUNCIL_CONFIG、COUNCIL_ENV_FILE、COUNCIL_MATERIALS_ROOT。
省略 COUNCIL_MODELS 时,默认成员列表为 chatgpt,glm,并读取 CHATGPT_* 和 GLM_*。
配置文件
当成员数量超过几个,或多个成员共用一个端点时,使用配置文件会更方便。设置 COUNCIL_CONFIG=/path/to/config.json,或者将文件放在 ~/.config/model-council/config.json,服务端会自动在该位置查找。
{
"providers": {
"my-relay": {
"base_url": "https://your-openai-compatible-host/v1",
"api_key": "${MY_RELAY_KEY}",
"format": "openai"
},
"zhipu": {
"base_url": "https://open.bigmodel.cn/api/anthropic",
"api_key": "${GLM_KEY}",
"format": "anthropic"
}
},
"members": [
{ "id": "gpt5", "provider": "my-relay", "model": "gpt-5", "label": "GPT-5",
"weight": 2 },
{ "id": "codex", "provider": "my-relay", "model": "gpt-5-codex", "temperature": 0.2 },
{ "id": "glm", "provider": "zhipu", "model": "glm-4.6" },
{ "id": "kimi", "base_url": "https://api.moonshot.cn/v1",
"api_key": "${KIMI_KEY}", "model": "kimi-k2" }
]
}
${ENV_VAR} 会从环境变量中展开,因此文件不包含机密信息,可以共享或提交到版本库。完整注释版请参阅 examples/config.json。
成员连接的配置方式有且只有两种,不能混用:指定 provider,并完整使用该提供方的端点;或者省略 provider,自行提供 base_url + api_key + format(如上面的 kimi)。如果指定了提供方,又覆盖这三个字段中的任意一个,系统就会拒绝该配置——该成员会被禁用,而 list_council 会说明原因。原因在于,部分覆盖可能会将一个端点的凭据与另一个端点的 URL 配对,从而悄悄将你的密钥发送到未获授权的主机。每个成员的 headers、timeout、temperature、max_tokens 和 label 不属于该身份信息,仍可覆盖。
字段
前三项作为一个整体一起使用——请参见上面的规则。
| 字段 | 适用对象 | 说明 |
|---|---|---|
base_url | provider,或没有 provider 的成员 | 路由所依附的根路径——openai 使用 /chat/completions,anthropic 使用 /v1/messages。与 OpenAI 兼容的主机通常以 /v1 结尾 |
api_key | provider,或没有 provider 的成员 | |
format | provider,或没有 provider 的成员 | openai(默认)或 anthropic |
model | 成员 | 发送到端点的模型 ID |
label | 成员 | 答案中显示的名称;默认为 ID |
weight | 成员 | 该成员的意见所占分量。默认值为 1,最大值为 10,0 表示仅供参考。请参见权重 |
max_tokens | 成员 | 仅适用于 Anthropic 格式,此时为必填项。默认值为 8192 |
temperature | 成员 | 仅在设置时发送 |
headers | provider,成员 | 额外的 HTTP 标头 |
timeout | provider,成员 | 每次尝试的秒数。默认值为 180 |
retries | provider,成员 | 瞬时故障时的额外尝试次数。默认值为 2,最大值为 5,0 表示禁用 |
retry_backoff | provider,成员 | 首次重试前等待的秒数,之后逐次翻倍。默认值为 1 |
proxy | 顶层、provider、成员 | 出站路由。省略时遵循 HTTP_PROXY/HTTPS_PROXY;设为 false 时直接连接;设为 URL 时使用该代理;设为 "env" 时恢复使用环境变量中的代理。请参见代理 |
vision | 成员 | 对无法查看图像的模型设为 false。遇到包含图像的调用时,该模型会跳过,而不是只针对文本作答。默认值为 true |
cache | provider,成员 | 设为 false 可停止随内容发送 cache_control 断点。仅适用于 Anthropic 格式;若网关拒绝此字段,请将其关闭。默认值为 true |
enabled | 成员 | 设为 false 可停用成员,而不删除其配置 |
backups | 成员 | 同一席位的备用端点;当前端点无响应时,按顺序尝试。最多 4 个。请参见备用端点 |
timeout、retries、retry_backoff 和 proxy 也可以在配置文件的顶层设置,
作为所有成员继承的默认值。
权重
评议团很少由地位相同的成员组成。weight 表示成员意见所占的分量——
除非另行设置,否则每个成员的值都是 1。只有比例有意义,因此 2 和 1
组成的评议团与 10 和 5 组成的相同。
{ "id": "gpt5", "provider": "my-relay", "model": "gpt-5", "weight": 2 }
它不会改变调用本身。权重不同时,ask_all 会在每个答案上标注其权重,
并在记录末尾给出排名:
===== GPT-5 (gpt-5) · weight 2 =====
...
===== Local (qwen3-8b) · weight 0.5 =====
...
[WEIGHTS — GPT-5 2, GLM 1, Local 0.5]
These are this council's standing priors on its members, not votes. ...
权重属于席位,而非端点,因此 provider 无法设置权重:同一中继上的两个
成员可以分别是前沿模型和小型快速模型。0 表示仅供参考——该成员仍会作答,
其回答也会被阅读,但它的赞同不计入结果。无法使用的值(负数、单词)
会回退为 1,而不会悄然破坏排名;list_council 会显示实际生效的值。
成员永远不会被告知彼此的权重。 如果模型得知自己的权重低于其他成员, 它就会停止争辩并开始附和,这恰恰会损害评议团旨在促成的独立异议——因此第二轮 会带上其他答案,但不会带上它们的排名。权重是给阅读记录的人看的,它代表先验, 而不是投票:权重用于打破平局,并决定谁承担举证责任。即使权重最低的成员给出了 具体、可核查的理由,也胜过权重最高的成员空口断言。
备用端点
同一个模型通常可以通过多个渠道访问——公司网关、公共中继、备用密钥。把每个渠道都列为单独的成员并不合适:这样会让同一个模型在场多次,于是它会回答多次、被计权多次,并且在第二轮中和自己争论。
backups 是同一席位的备用端点;当前一个端点没有响应时,会按顺序尝试:
{
"id": "sol", "provider": "internal", "model": "gpt-5-codex", "label": "Codex",
"backups": [
{ "provider": "my-relay" },
{ "base_url": "https://another-relay.example/v1", "api_key": "${SPARE_KEY}",
"model": "gpt-5-codex-latest" }
]
}
一个 ID、一个标签、一个权重、一票。你永远不必直接指定备用端点——ask(model="sol") 会访问三个端点中可用的那个。
备用端点会继承席位为模型设定的配置——model、max_tokens、temperature、vision、timeout、retries——因此,常见情况(同一模型位于第二个中继)就是上面的一行。只有当另一个渠道对模型使用不同名称时,才需要指定 model。有三项永远不会继承:
- 连接配置(
base_url、api_key、format)——这正是设置备用端点的目的。它要么指定一个provider,并从中获取这三项,要么自行提供全部三项。不会接受只提供其中一部分,原因见配置文件。 headers——为某个端点编写的请求头通常是该端点的凭据。proxy——路由属于主机,而非席位。如果某个席位为内部网关设置了proxy: false,其公共备用端点也会绕过它们所需的代理。备用端点使用自身 provider 的路由,或理事会的默认路由。
席位无法继续尝试时。 不只是连接中断:密钥被撤销、中继不再提供某个模型 ID,或网关返回 200,但内容并非答案,都属于这种情况。从席位的角度看,这些都是同一种事件——当前入口暂时无法访问该模型,而配置中指定了其他入口。对于暂时性故障(见重试),仍会先在发生故障的连接上重试;因此,备用端点适用于已不可用的端点,而非只是迟迟不肯响应的端点。如果你宁愿切换而不是等待,请在主端点设置 retries: 0。
答案会显示实际响应的端点。 备用端点使用的模型 ID 通常不同,有时甚至是不同的模型。将理事会的输出视为模型比较时,不应在不知情的情况下比较其他内容:
===== Codex (gpt-5-codex-latest — backup 2, after the primary did not answer) =====
list_council 会在每个席位下缩进显示端点链:
id label model format route endpoint tries status
sol Codex gpt-5-codex openai direct https://gateway.internal/v1 3 × 180s ready
↳ backup 1 gpt-5-codex openai env https://your-relay/v1 3 × 180s standing by
↳ backup 2 gpt-5-c... openai env https://another-relay/v1 3 × 180s standing by
probe_models 会探测链中的每个端点,因此,如果备用端点已悄然停止提供相应模型,你可以在真正需要它之前发现这一点。如果某个席位的所有端点都没有响应,报告会列出尝试过的每个连接及其失败原因。
备用端点是配置文件功能;环境变量成员列表没有对应功能。每个席位最多可设 4 个备用端点——一个席位代表一个模型的意见,而不是高可用集群;ask_all 必须等到最慢的端点链全部失败。
代理
每个成员都会遵循本机的 HTTP_PROXY/HTTPS_PROXY,这是默认的正确选择,直到一个议会包含无法共用同一路由的主机——某个公共 API 只能通过代理访问,某个内部网关则无法被代理访问。一个设置无法同时适用于两者,因此要为每个席位单独选择路由。
proxy 有四种取值,可设在成员、提供方上,也可设在配置文件顶层,作为议会自己的默认值:
| 值 | 路由 |
|---|---|
| 省略 | 遵循议会的 proxy(如果已设置),否则遵循 HTTP_PROXY/HTTPS_PROXY |
false,或 "direct" | 直接连接,忽略两者 |
| 一个 URL | 通过该代理连接——http://、https://、socks5:// 或 socks5h:// |
"env" | 改回遵循 HTTP_PROXY/HTTPS_PROXY,用于路由到其他地方的议会中的一个席位 |
越具体的设置优先级越高:成员,然后是提供方,再然后是议会,最后是环境变量。因此,议会中的大多数成员都通过代理连接,但有两个席位不能这样连接时,只需设置一次,再为那两个席位分别设置:
{
"proxy": "http://127.0.0.1:7890",
"providers": {
"internal": { "base_url": "https://gateway.internal.example/v1",
"api_key": "${INTERNAL_KEY}", "proxy": false }
},
"members": [
{ "id": "gpt5", "provider": "my-relay", "model": "gpt-5" },
{ "id": "inhouse", "provider": "internal", "model": "some-internal-model" },
{ "id": "local", "base_url": "http://127.0.0.1:11434/v1", "api_key": "-",
"model": "qwen3-8b", "proxy": false }
]
}
也可以通过环境变量进行同样的三种设置——用 COUNCIL_PROXY 设置议会,用 <ID>_PROXY 设置单个成员:
COUNCIL_PROXY=http://127.0.0.1:7890
INHOUSE_PROXY=direct
LOCAL_PROXY=direct
list_council 会打印最终采用的路由,显示为 env、direct 或代理 URL;只有成员之间的路由可能不同时,才会显示这一列:
network: HTTPS_PROXY=http://127.0.0.1:7890 in this server's environment — the members whose route is 'env' go through it
id label model weight format sees route endpoint tries status
gpt5 GPT-5 gpt-5 1 openai text+img env https://your-host/v1 3 × 180s ready
inhouse Inhouse internal 1 openai text+img direct https://gateway.internal/v1 3 × 180s ready
kimi Kimi kimi-k2 1 openai text+img http://127.0.0.1:7890 https://api.moonshot.cn/v1 3 × 180s ready
代理 URL 中的密码在所有打印内容中都会被隐藏——包括表格、警告和连接错误文本。
无法使用的代理会在读取成员名单时被捕获,而不是等到第一次调用时才发现。省略方案的 127.0.0.1:7890 会被解析为 http://127.0.0.1:7890,并在警告中说明;无法建立连接的方案会让该成员停用,list_council 会在其旁边显示原因,而不是让它在每次调用时都因请求内部抛出的 ValueError 而失败。成员会停用,而不会悄悄改走其他路由:既然指定了代理,就说明有人希望流量通过该代理。
socks5:// 需要一个 httpx 默认不会安装的软件包。请安装附加依赖——pip install 'model-council-mcp[socks]',或
uvx --from 'model-council-mcp[socks]' model-council-mcp——否则该成员会停用,并显示说明这一情况的提示。
线路格式说明
- 不会根据 URL 推断
format。 如果将base_url指向 Anthropic 风格的端点,却没有同时设置format: "anthropic",该成员仍会使用 OpenAI 格式,并且每次调用都会失败。这是最常见的配置错误。 - **Anthropic 端点:**服务端会向
{base_url}/v1/messages发送请求,因此base_url不应包含/v1。 - **兼容 OpenAI 的端点:**服务端使用
/chat/completions,绝不使用/responses。有些网关两者都提供,但/responses可能会注入由提供方选择的系统角色设定,这不适合通用顾问。 - 默认遵循系统代理。 如果某个成员所在的网络无法通过你的代理访问(通常是内部网关),它会报出一个不提及代理的
ConnectError。为该成员或提供方设置"proxy": false,即可直接连接,同时其他成员仍继续使用代理;参见代理。连接错误会指出成员采用的路由,因此三种策略不会都以相同的方式失败。 - 模型 ID 变化很快。 运行
probe_models,查看端点当前实际提供的模型。
使用方法
可以对主持人输入这些内容:
- “请先自行回答,然后运行
ask_all,并用表格列出你们意见一致和不一致的地方。” - “针对这个问题询问 gpt5 和 glm,然后分别批评两个答案,并告诉我哪个更准确、为什么。”
- “针对这个问题运行两轮
ask_all,然后告诉我谁改变了想法,以及究竟是什么让大家达成共识。” - “只问 glm——我想针对这个文件听听第二种意见。”
本地开发
uv sync
将 .env.example 复制为 .env,填入实际值,然后:
uv run python tests/test_smoke.py
冒烟测试会在离线状态下检查两种配置路径;如果 .env 可用,测试最后会进行一次实时往返调用。若要让客户端指向你的工作副本,请在环境中使用 model-council-mcp 脚本,而不是 uvx。
故障排查
- 服务未出现 — 检查客户端的 MCP 日志(Claude Desktop:
~/Library/Logs/Claude/mcp*.log)。服务启动时会将配置警告写入 stderr。 - 工具返回
[... is not configured]— 该成员缺少base_url、api_key或model。运行list_council查看各成员的详细情况。 - HTTP 401 — 密钥错误,或提供方已停用该密钥。
- HTTP 404 —
base_url错误,或该端点使用了错误的format。 - 模型 ID 被拒绝 — 运行
probe_models。 - 成员提示
gave up after N attempts— 它每次都遇到了暂时性故障。错误文本是端点最后一次返回的错误。list_council会将每个成员的预算显示为尝试次数 × 超时。 - 调用耗时远超超时时间 — 重试会使耗时倍增:每次尝试 180 秒、共尝试三次,最坏情况下会耗时约 9 分钟,另加退避时间。对于你希望快速失败的成员,请调低
timeout或retries。 - 长时间运行的
ask_all超时失败,但服务仍在工作 — 这条提示来自客户端,而非本服务;COUNCIL_TIMEOUT不会改变它,因为它只决定成员多久后放弃。参考 MCP SDK 会在静默 60 秒后触发超时,而一个响应较慢的成员单独就可能超过这个时间。本服务每十秒报告一次进度,让会重置计时器的客户端重新开始计时;如果客户端不会这样做,则需要配置客户端自身的设置(Claude Code 可在 MCP 配置中为每个服务设置以毫秒为单位的timeout,或使用MCP_TOOL_TIMEOUT统一设置)。请注意限制适用于谁:它属于启动服务进程的客户端,因此,在桌面应用中注册并桥接到另一个工具的服务,受桌面应用的设置限制,而不是你当前输入位置的设置。 probe_models报告某个成员无法列出模型 — 阅读它在表格下方打印的说明。拒绝GET /models的端点通常是只提供聊天路由的中继,这并不代表该成员无法回答。运行ask才能确定。- 一个成员返回不带详细信息的
ConnectError,其他成员正常 — 通常问题出在路由,而不是端点。错误会指出该成员使用的路由;list_council的route列会同时显示所有路由,而 代理 介绍了如何修改其中一个。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。