mcpField
M
Model Council

Model Council

本地服务
github.com/Totti0135AI 与智能体210 周前更新

关于 Model Council

Model Council 是一个 MCP 服务,让助手在同一对话中向多个兼容 OpenAI 或 Anthropic 的模型提问、传递彼此答案并汇总结论。它支持多轮交叉评议、文件或图片材料,以及可选的反方论证席位。

主要功能

  1. 01向单个或多个模型提问
  2. 02并行收集并比较回答
  3. 03多轮传递回答进行评议
  4. 04向模型提供文件、文本和图片
  5. 05设置反方席位参与讨论
  6. 06查看成员配置并探测模型

适用场景

  1. 01比较多个模型对方案的意见
  2. 02让模型交叉检查彼此的回答
  3. 03请模型审阅规格、日志或差异文件
  4. 04围绕计划收集并检验反对意见

MCP 相关

作为 MCP 服务提供 ask、ask_all、revise 等工具,使宿主助手可调用配置的模型端点,并将回答作为工具结果纳入对话。服务本身不访问助手的会话记录;每次调用都需传入所需上下文。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"totti0135-model-council": {
"command": "uvx",
"args": [
"model-council-mcp"
],
"env": {
"COUNCIL_MODELS": "<COUNCIL_MODELS>",
"COUNCIL_CONFIG": "<COUNCIL_CONFIG>"
}
}
}
}
启动命令uvx model-council-mcp
需要配置的环境变量
COUNCIL_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+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypimodel-council-mcp0.14.0

工具

6

收录的工具列表 · 更新于 2 周前

参数类型必填说明
modelenum是chatgpt | glm
promptstring是
systemany否
materialsany否

详细介绍

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 指定谁可以访问之前,服务拒绝在非回环地址上启动。回环地址无需标志,始终允许访问。

标志
--allowCIDR、裸地址,或名称 private(RFC1918)、loopback、any
--trust-proxy可信任其 X-Forwarded-For 的反向代理
--allow-origin允许浏览器 Origin;可重复指定
--materials-rootmaterials 路径可指向的唯一目录;未设置时,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_urlprovider,或没有 provider 的成员路由所依附的根路径——openai 使用 /chat/completions,anthropic 使用 /v1/messages。与 OpenAI 兼容的主机通常以 /v1 结尾
api_keyprovider,或没有 provider 的成员
formatprovider,或没有 provider 的成员openai(默认)或 anthropic
model成员发送到端点的模型 ID
label成员答案中显示的名称;默认为 ID
weight成员该成员的意见所占分量。默认值为 1,最大值为 10,0 表示仅供参考。请参见权重
max_tokens成员仅适用于 Anthropic 格式,此时为必填项。默认值为 8192
temperature成员仅在设置时发送
headersprovider,成员额外的 HTTP 标头
timeoutprovider,成员每次尝试的秒数。默认值为 180
retriesprovider,成员瞬时故障时的额外尝试次数。默认值为 2,最大值为 5,0 表示禁用
retry_backoffprovider,成员首次重试前等待的秒数,之后逐次翻倍。默认值为 1
proxy顶层、provider、成员出站路由。省略时遵循 HTTP_PROXY/HTTPS_PROXY;设为 false 时直接连接;设为 URL 时使用该代理;设为 "env" 时恢复使用环境变量中的代理。请参见代理
vision成员对无法查看图像的模型设为 false。遇到包含图像的调用时,该模型会跳过,而不是只针对文本作答。默认值为 true
cacheprovider,成员设为 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

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。