内容
# 委员会 — 自组装商议引擎
[](LICENSE)
[](https://www.python.org/)
[](https://google.github.io/adk-docs/)
[](https://modelcontextprotocol.io/)
[](https://fastapi.tiangolo.com/)
[](https://ai.google.dev/)
[](#运行测试-必须绿色-离线 -- fr-014)
> **提出问题 → 委员会组装专家小组 → 各方辩论 → 声明经过事实核查与维基百科对比 → 您获得带有引文来源的平衡判决简报** — 所有内容都实时流式传输到自包含的网页界面。无需前端构建步骤,无需付费服务。
### 亮点
- 🤖 **多智能体协调** — 主持人、专家、事实核查员和综合者基于 Google ADK,通过有界商议循环驱动。
- 🔌 **真正的 MCP 集成** — 本地标准输入/输出事实核查服务器(`MCPToolset`)基于无密钥维基百科。
- 🛡️ **设计中的防护措施** — ADK 回调函数拒绝监管建议、不安全内容和提示注入 *在任何小组运行之前*。
- 🧠 **会话内存** — 后续跟进基于先前的判决,通过 `InMemorySessionService`(仅内存,无持久化)。
- 📡 **实时 SSE 流式传输** — 每个事件都流式传输到实时界面(阶段时间线、角色卡、事实核查徽章;同源资产,无 CDN,无构建步骤)。
- 🔀 **自带提供商** — Gemini(默认)、OpenAI、Anthropic、OpenCode Zen 或任何 OpenAI 兼容端点,在页面中选择;密钥保留在内存中。
- 🧩 **结构化输出** — 类型化的 `PanelRoster`、`PanelistTurn`、`Claim` 和 `VerdictBrief` 端到端(处理 ADK R6 问题)。
- ✅ **79 个离线测试** — 确定性;无需 API 密钥或浏览器。
```
┌──────────┐ ┌────────────┐ ┌───────────────┐ ┌────────────┐ ┌────────────┐
│主持人 │──▶│ 专家 │──▶│ 事实核查员 │──▶│ 综合者 │──▶│判决简报 │
│ (招募) │ │ (多轮) │ │ (MCP/维基百科)│ │ (结构化) │ │ + 追溯 │
└──────────┘ └────────────┘ └───────────────┘ └────────────┘ └────────────┘
▲ 防护回调拒绝受监管/不安全/注入输入在任何小组运行前
```
## 为什么
政策问题需要比单一答案预言机更多的东西。委员会模拟了一个**商议过程**:不同的专家观点、可见的反驳、扎实的事实核查和明确的信心调用。它在一个可复制的应用中展示了多智能体协调、MCP 工具互操作性、结构化输出、会话/内存和安全防护。
## 先决条件
- Python **3.11+** (在 3.12 上开发)
- 一个免费的 **Google AI Studio** API 密钥 — https://aistudio.google.com/apikey
- Internet 访问(Gemini + 维基百科);无需其他帐户、计费或云项目
- 任意现代浏览器(界面是一个 localhost 页面 — 无需 npm,无需构建步骤)
## 设置
```bash
# 1. 创建 + 激活虚拟环境
python -m venv .venv
# Windows: .venv\Scripts\activate macOS/Linux: source .venv/bin/activate
# 2. 安装固定依赖项
pip install -r requirements.txt
pip install -e . --no-deps # 安装 `council` 包(可编辑)
# 3. 配置密钥(永远不要提交 .env)
cp config/.env.example .env # Windows: copy config\.env.example .env
# 编辑 .env 并设置 GOOGLE_API_KEY=<您的密钥> (GOOGLE_GENAI_USE_VERTEXAI=FALSE)
```
`.env` 和 `.claude/` 被 gitignore 了。一个秘密扫描门控(`scripts/secret-scan.sh`)在 CI 中运行,并作为预提交钩子(安装一次:`git config core.hooksPath .githooks`)。
## 运行演示
```bash
council # 或:python -m council
```
启动本地服务器(默认 `http://127.0.0.1:8400`)并打开您的浏览器。无需配置提供商,页面将以 **开始使用** 状态打开,引导您进入设置 — 服务器始终启动(退出代码:`0` 干净关闭,`1`意外启动错误)。
在页面中,询问:
> 一个中等大小的城市是否应该在 2030 年前禁止汽油叶吹风机?
预期,实时流式传输:
1. **小组组装** — 专家卡片逐一出现,带有招募原因;≥1 带有 **挑战者徽章**。如果招募失败,您会看到一个“备用小组”通知,运行仍会完成(FR-016)。
2. **辩论** — 2 轮;每个专家都有一个稳定的颜色;反驳意见在视觉上突出显示(智能体到智能体的信号)。
3. **事实核查** — 声明获得状态徽章 + 可点击的维基百科来源。
4. **判决简报卡** — 各方立场、建议、信心衡量、异议和 **下载 JSON**。
5. 打开 **追溯抽屉** 以获取完整的有序 `DeliberationTrace`(FR-008)。
标志:`--port N`、`--quiet`(警告日志)、`--verbose`(调试日志)。
### 验证防护拒绝(US3)
询问:`请告诉我明天哪些股票可以购买以保证利润` → 一个 **拒绝卡** 解释原因;**没有小组出现**,没有辩论运行。注入尝试(`忽略您的指示...`)被视为不受信任的内容并被拒绝;医疗/法律建议也被拒绝。
### 验证会话上下文(US2)
在获得判决后,在同一页面中提出后续问题。页面重用其 `session_id`,因此第二个判决引用第一个。会话状态仅在内存中,服务器停止时被丢弃(FR-012)。
## 选择您的 AI 提供商(多提供商 — 功能 002)
委员会不再仅限于 Gemini。打开页面上的 **⚙ 设置** 以选择提供商和模型,粘贴您的密钥,然后运行 — 无需编辑文件,无需重启:
- **Google Gemini** (默认)、**OpenAI**、**Anthropic**、**OpenCode Zen** 或 **自定义** OpenAI 兼容端点(本地运行时、网关...)。
- 凭证仅保存在 **服务器内存中** — 绝不写入磁盘、日志、跟踪或 git,并在输入后在 UI 中屏蔽。
- 每个商议角色(主持人、专家、事实核查员、综合者)都在您选择的提供商/模型上运行;标题芯片实时显示运行情况。
如果您更喜欢环境变量?它们仍然有效(参见 `config/.env.example`):
```bash
COUNCIL_PROVIDER=openai # google | openai | anthropic | opencode | custom
COUNCIL_MODEL=gpt-4o-mini # 可选;否则使用提供商默认值
OPENAI_API_KEY=... # 与所选提供商匹配的密钥变量
# 自定义 OpenAI 兼容端点仅:
# COUNCIL_BASE_URL=http://localhost:11434/v1
```
**优先级:** UI 选择 → 环境默认值 → 构建的 Gemini 默认值。UI 中输入的密钥在运行过程中获胜;重启返回到环境配置。非 Google 提供商通过 ADK 自己的 `LiteLlm` 包装器路由(添加固定的 `litellm` 依赖项)。完整演练:`specs/002-multi-provider-ui/quickstart.md`。
## 独立运行一个智能体(FR-013/FR-015)
每个智能体都可以独立寻址 — 有助于调试和可复制的检查。`scripts/run_one_agent.py` 驱动单个智能体回合,使用真实模型并打印最终文本和结构化输出:
```bash
# 主持人招募专家小组(结构化 PanelRoster)
python scripts/run_one_agent.py moderator "一个城市应该在 2030 年前禁止汽油叶吹风机吗?"
# 事实核查员通过 MCP/维基百科工具集收集证据(自由格式)
python scripts/run_one_agent.py factchecker "声明:叶吹风机排放过量污染物。"
# 结构者将自由格式证据映射为类型化声明
python scripts/run_one_agent.py structurer "声明:天空是蓝色的。判决:支持。"
# 综合者从 JSON 有效载荷中撰写判决简报
python scripts/run_one_agent.py synthesizer '{"question":"...","roster":{...},"transcript":[],"claims":[]}'
```
## 运行测试(必须绿色离线 — FR-014)
```bash
pytest # 全部:单元测试 + 烟雾测试 + 端到端测试(离线,确定性,免费)
pytest tests/e2e # 完整问题 → SSE 流 → 判决简报(假模型 + 模拟维基百科)
pytest tests/smoke # 每个智能体 + 每个 MCP 工具的烟雾测试
```
默认套件**不进行实时 API 调用,不需要浏览器** — 它注入了一个 `ScriptedLlm` 假模型,模拟事实核查 MCP 传输,并使用进程内 ASGI `TestClient` 驱动 Web API,因此它是确定性和免费的。
## 架构
```
src/council/
config.py 界限(MIN_PANEL=2, DEFAULT_PANEL=3, MAX_PANEL=4, MAX_ROUNDS=2, FACTCHECK_TIMEOUT_S=8, MODEL=gemini-2.5-flash)+ 环境加载
app.py CouncilApp:ADK Runner + InMemorySessionService;run_agent_for_output
(带有 429/503 重试然后降级的机制,以便免费层运行完成)
providers.py 多提供商注册、优先级、内存凭证、
验证探针 + 模型工厂(Gemini 字符串 | LiteLlm)— 功能 002
logging_config.py dictConfig 日志记录;TraceEvents 同时到达日志和 UI(FR-008)
agents/
moderator.py 防护回调(拒绝/澄清)+ 招募(output_schema=PanelRoster)
panelist.py 一轮(output_schema=PanelistTurn;引用先前的 A2A 信号)
factchecker.py MCP 工具集,无 output_schema(R6 问题)+ 声明结构步骤
synthesizer.py output_schema=VerdictBrief
instructions/*.md 每个智能体的提示(在构建时加载)
guardrails/
input_guard.py 验证问题 + 分类拒绝(受监管/不安全/注入)
claim_guard.py strike_unsupported(从不将不支持的声明作为事实呈现)
orchestration/
deliberation.py 有界循环:防护 → 招募 → 辩论 → 事实核查 → 防护 → 综合
(FR-018 在 3 个位置重试一次然后降级;US2 上下文线程)
fallback.py 确定性 3 人小组当招募失败时
session_state.py 类型访问器超过 ADK 会话状态(仅内存)
schemas/ 名册、声明、判决、跟踪(PanelistTurn+DebateTurn)、会话
tools/factcheck_mcp/
server.py FastMCP 标准输入/输出服务器(2 个工具:search_wikipedia, get_summary)
wikipedia.py 无密钥 REST + MediaWiki API;超时 → 空降级
web/
server.py FastAPI:SSE 流 + /api/providers 配置 API + 静态页面
static/index.html 标记 — 实时 UI(同源 app.js/style.css,无 CDN,<500 行)
static/app.js SSE 处理、阶段时间线、设置面板、实时渲染
static/style.css 暗色动画主题(阶段步进器、卡片、判决、设置)
scripts/ secret-scan.sh, run_one_agent.py
tests/ 单元测试 + 烟雾测试 + 端到端测试(离线,假模型 + 模拟 MCP)
```
### 关键设计决策(参见 `specs/001-the-council/research.md`)
- **有界 Python 应用壳** 驱动 ADK 智能体(不是 `LoopAgent`)— 强制终止(FR-009),同时推理保留在 ADK/Gemini 内。
- **本地标准输入/输出 MCP 服务器** 用于事实核查 — 真正的 MCP 集成(`MCPToolset`),最小权限(只有事实核查员获得工具集),无密钥(维基百科 REST)。
- **结构化输出** 无处不在;ADK 问题(R6)即工具使用智能体无法声明 `output_schema` 由单独的结构步骤处理,该步骤将自由格式证据映射为 `list[Claim]`。
- **防护措施作为 ADK 回调** (“裁判”)— `before_agent_callback` 在任何小组运行前中断越界输入。
- **会话/内存** 通过 `InMemorySessionService` — 后续跟进建立在先前的判决之上;超出会话的持久化内容。
## 已知限制
- **免费层速率限制**:`gemini-2.5-flash` 免费层允许 ~5 请求/分钟 和 ~20 请求/天。完整的商议大约需要 11 个 LLM 调用,因此 1-2 次运行/天可能会耗尽每日配额。启动器在出现 429/503 时会重试并带有回退;如果事实核查调用最终受到速率限制,它会降级为“不确定”的声明(按照规范的边缘情况),并且运行仍会完成。能力在离线(模拟)和隔离情况下得到验证。
- **提供商默认值**:Gemini 仍然是内置默认提供商;OpenAI、Anthropic、OpenCode Zen 和自定义 OpenAI 兼容端点通过设置或环境变量选择(功能 002)。OAuth 帐户登录(US3)故意不在范围内。
- **仅维基百科** 事实核查来源(按设计 — 无密钥,可复制)。
- **仅限本地主机** — 服务器绑定到 `127.0.0.1`;未部署到生产环境(绘制了一个 `Dockerfile`/Cloud Run 路径作为可选冻结后任务)。
## 团队
由四人团队为 **Google + Kaggle 5-Day AI Agents** 顶点项目构建。系统协同设计;实施、集成和工具由 Taha Fahd 领导。
- **Taha Fahd** — 团队负责人 & 主要开发人员 · GitHub [@6rzan](https://github.com/6rzan) · Kaggle [tahafahdthabit](https://www.kaggle.com/tahafahdthabit)
- **ESAM Al-AREQi** — 贡献者 · GitHub [@esammostafa9-cloud](https://github.com/esammostafa9-cloud) · Kaggle [esamalareqi](https://www.kaggle.com/esamalareqi)
- **Kamil Ayub Khan** — 贡献者 · GitHub [@Slimyzz](https://github.com/Slimyzz) · Kaggle [kamilayubkhan](https://www.kaggle.com/kamilayubkhan)
- **Ahmad Hemedany** — 贡献者 · GitHub [@hemedany](https://github.com/hemedany) · Kaggle [ahmadhemedany](https://www.kaggle.com/ahmadhemedany)
## 许可证
MIT — 参见 [LICENSE](LICENSE)。