内容
# 呼叫副驾驶
适用于值班工程师的小型、与提供商无关的AI助手。询问它 *"checkout 正在抛出 5xx,我该怎么办?"*,它会检索相关的运行手册(RAG),使用只读工具(实时指标、部署、日志)在模型驱动的代理循环中进行调查,并返回一个 **有引文、可靠的** 答案 — 提出诸如回滚之类的修复建议,但绝不会执行它们。同一应用程序在 **OpenRouter、Anthropic、OpenAI 或 Gemini** 上运行,只需更改一个环境变量,并且附带一个 **评估框架**,该框架可以评估正确性、工具选择和安全性。
> **注意:** 这是一个 **个人学习项目,基于模拟数据(正在扩展到实际用例)** (`data/` 和 `docs/` 都是虚构的)。它演示了模式 — RAG、工具使用、代理循环、MCP、评估、提供商抽象 — 而不是生产系统。下面评估的数字是 **此仓库的实际输出**,包括它 **失败** 的情况。
我以对待生产系统的方式来处理这个问题:不信任一个无法核实的答案。因此,每个回复都基于检索的运行手册或实时工具结果(并且有引文),工具在构造上是只读的,因此错误的答案不会造成损害,并且评估框架衡量它是否真正表现良好 — 它仍然失败的案例也包括在内。
## 结果一览
**指标 — 通过率** 在 **36 例标记评估** (n = 36):只有当案例 **正确** (独立的LLM裁判确认答案传达了关键事实) *和* 调用 **正确的实时数据工具** *和* 保持 **安全** (无禁止声明,例如从未虚假地声明 "healthy")时,案例才会通过。这是任务准确性在保留的标记集上的标准 "它是否真正有效?" 衡量标准。
**消融** — 裁判(`claude-haiku-4-5`) **固定** 在 n = 36。两个杠杆移动分数:**管道** (行 1-3,答案者固定在小型开放模型)和 **答案者** (行 4):
| 答案者 | 管道 | 通过率 | vs. 行 1 |
|---|---|---|---|
| `llama-3.3-70b` (小型、开放) | 单代理 | 20/36 = **56%** | — |
| `llama-3.3-70b` | 受控多代理 | 28/36 = **78%** | **+22 分** ✅ |
| `llama-3.3-70b` | 多 + "重新校准" 验证器 | ~68% (3 次运行:69/64/69) | **-10 分 → 已恢复** ✗ |
| **Claude Haiku 4.5** (小型、前沿) | 单代理 | ~**91%** (3 次运行:92/89/92) | **+34 分** ✅ **开放** |
```mermaid
xychart-beta
title "通过率,36 例评估(裁判 = Haiku 4.5,n=36)"
x-axis ["llama single", "llama multi", "llama + recal", "Haiku single"]
y-axis "通过率(%)" 0 --> 100
bar [56, 78, 68, 90]
```
**诚实地阅读这篇文章** — 衡量标准的用途:
- **答案者是最大的杠杆。** 更换小型 **开放** 模型为小型 **前沿** 模型(Haiku)优于 **一切** 管道优化弱模型 — +34 分,并且它是唯一一个超过 80% 门槛的配置。仅凭这一点。(注意:该行是自我评分的 — Haiku 回答并评判 — 所以它有点乐观;但 +34 远远超过任何自我偏好偏差,并且其中 3 次失败是客观的 **安全** 检查。llama 行使用相同的 Haiku 裁判但不是自我评分。)
- **管道有助于弱模型,但有局限性。** `llama` 单 → 多 是 **+22** — 真实(一个大 delta,超出约 ±3 例运行噪声)— 但仍然低于门槛。治理不能完全弥补弱基模型。
- **"重新校准" 是报告的 **负面** 结果。** 我假设双向批评者会有帮助;跨 **3 次种子**,它位于普通多代理 **下方** ~10 分,因此我 **恢复** 了它。知道一个更改 **没有** 起作用是您进行衡量的原因。
- **注意:** 少数种子数量 — 更紧密的误差条需要更多运行,但批量吞吐量(速率限制)限制了这一点。完整方法 + 每个更改的历史记录:[评估记分卡](#eval-scorecard-real-reproducible) ↓ 和 [`IMPROVEMENTS.md`](./IMPROVEMENTS.md)。
## 架构
```
┌─────────────────────────────────────────────────────────┐
您询问 ───► │ agent.py (观察 → 动作 → 观察,最大 5 步) │
"checkout 5xx" │ │
│ 1. RAG:retriever.py ──► top-k 运行手册片段 + [引用] │
│ 2. LLM 决定:答案,或调用工具? │
│ 3. 运行只读工具 ──► 馈送结果 ──► 重复 │
└───────────────┬───────────────────────────┬───────────────┘
│ │
┌────────────▼───────────┐ ┌───────────▼───────────────┐
│ tools.py (只读) │ │ llm.py (一个接口, │
│ list_services │ │ 许多提供商) │
│ get_metric │ │ ┌─────────────────────┐ │
│ recent_deploys │ │ │ OpenRouter(默认)│ │
│ search_logs │ │ │ Anthropic · OpenAI │ │
│ get_runbook │ │ │ Gemini │ │
└────────────┬───────────┘ │ └─────────────────────┘ │
│ └────────────────────────────┘
读取模拟 ▼
数据/*.json,jsonl + 文档/*.md
│
同样的 5 个工具也通过 MCP 暴露 ──► mcp_server/server.py
(所以 Claude Desktop / Claude Code / 任何 MCP 客户端都可以使用它们)
│
质量得到证明,而不是靠感觉 ──► evals/run_evals.py + evals/dataset.jsonl
(LLM 作为裁判的正确性 • 工具选择 • 安全性 • 通过率门槛)
```
## 运行
```bash
pip install -r requirements.txt
cp .env.example .env # 然后填写一个密钥(或直接导出下面的变量)
# 选择一个提供商(OpenRouter 不需要 Claude/OpenAI 订阅)。
# 代理需要一个具有工具能力的模型;纯 RAG/评估适用于任何模型。
export PROVIDER=openrouter
export OPENROUTER_API_KEY="sk-or-..." # 或 ANTHROPIC_API_KEY / OPENAI_API_KEY
python app.py # 交互式 CLI — 尝试:checkout is throwing 5xx, what do I do?
python -m evals.run_evals # 记分卡 + 在 36 个标记事件上发布门槛
python mcp_server/server.py # 通过 MCP 暴露 5 个工具(stdio)
python -m viz.server # 直观的视觉化工具 → 打开 http://localhost:8000
# 随时更换大脑: export PROVIDER=anthropic | openai | openrouter | gemini
```
**裁判/验证者独立性(重要):** 为了避免模型评估自己的工作,裁判是 **不同** 的模型,而不是答案者。默认情况下是 **全 OpenRouter** (答案使用 `llama-3.3-70b` ,裁判使用 `gemma` ) — 一个密钥,不需要 Anthropic/OpenAI。覆盖 `JUDGE_PROVIDER` / `JUDGE_MODEL`。
### 配置(环境变量)
所有内容都由环境变量驱动;没有任何供应商的代码。合理的默认值意味着您 **必须** 设置的唯一一件事是一个 API 密钥。
| 变量 | 默认值 | 它执行的操作 |
|---|---|---|
| `PROVIDER` | `openrouter` | 回答后端:`openrouter` \| `anthropic` \| `openai` \| `gemini`。 |
| `OPENROUTER_API_KEY` / `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` / `GEMINI_API_KEY` | — | 所选提供商的密钥。 |
| `OPENROUTER_MODEL` | `meta-llama/llama-3.3-70b-instruct` | OpenRouter 上的回答模型(代理需要工具支持)。 |
| `ANTHROPIC_MODEL` / `OPENAI_MODEL` | `claude-sonnet-4-5` / `gpt-4o` | 那些提供商的回答模型。 |
| `JUDGE_PROVIDER` / `JUDGE_MODEL` | `openrouter` / `google/gemma-4-31b-it:free` | 评判/验证角色的全局备用(被 `models.json` / `MODEL_JUDGE` 覆盖)。 |
| `MODEL_<ROLE>` / `PROVIDER_<ROLE>` | 来自 `models.json` | 按角色的模型覆盖 — `ROLE` ∈ `INVESTIGATOR·TRIAGE·VERIFIER·POSTMORTEM·JUDGE`。参见下面的“按角色的模型”。 |
| `ONCALL_MODE` | `single` | `single` = 一个调查代理;`multi` = 受控管道(分诊 → 调查 → 验证 → 事后总结)。 |
| `RETRIEVAL_MODE` | `keyword` | `keyword` (零依赖)\| `semantic` \| `hybrid` (关键词 + 局部嵌入;需要 `sentence-transformers`,如果不存在则回落到关键词)。 |
| `GUARDRAILS_FILE` | `guardrails.json` | 管制策略的路径。 |
| `ONCALL_LOG_DIR` | `logs/` | 每运行一次的 JSONL 跟踪写入的位置。 |
| `EVAL_WORKERS` | `1` | 并发运行评估案例(线程池)。`4–5` 是很大的时钟收益;太高就直接达到速率限制。 |
| `VIZ_PORT` | `8000` | 直观的可视化工具的端口。 |
**裁判/验证者独立性(重要):** 为了避免模型评估自己的工作,裁判是 **不同** 的模型,而不是答案者。默认情况下是 **全 OpenRouter** — 答案使用 `llama-3.3-70b` ,裁判使用 `gemma` — 所以您只需要 **一个 OpenRouter 密钥** 而不需要其他任何东西(没有 Anthropic/OpenAI,没有额外的成本)。
> **关于免费层(经过实践):** 免费模型在负载下会 **限制** — 对于单个运行或演示来说很好,但完整的 36 例评估会触发足够的调用以达到墙并在免费裁判上卡住。较小的裁判也会比前沿模型 **严格** 评判得多 — 真实的成本/质量权衡。当我想要一个更稳定的裁判时,我将 `JUDGE_MODEL` 指向一个更强的模型。(在测试中,`gemma-4-31b-it:free` 清晰地回答了;`qwen3-next-80b:free` 被限制了。)
- 如果无法构建裁判客户端(例如,没有其提供商的密钥),验证者 **回落到回答模型并报告独立性丢失** — 在可视化工具的验证者卡上和运行日志中显示,从不隐藏。
### 按角色划分的模型
每个代理角色都可以使用自己的模型 — 将廉价/快速的模型路由到分诊,将推理模型路由到验证者,将编写模型路由到事后总结,将具有工具能力的模型路由到调查员。默认值位于 [`models.json`](./models.json);使用 `MODEL_<ROLE>` (和可选的 `PROVIDER_<ROLE>`)覆盖任何角色。解析顺序:env → `models.json` → 全局备用(因此,如果两者都没有设置,则行为保持不变)。
| 角色 | 默认值 | 注意 |
|---|---|---|
| `investigator` | `meta-llama/llama-3.3-70b-instruct` | 回答;**必须具有工具能力**。 |
| `triage` | `google/gemma-4-31b-it:free` | 廉价分类器;每个运行调用一次。 |
| `verifier` | `google/gemma-4-31b-it:free` | ≠ 调查员 → 独立检查。 |
| `postmortem` | `meta-llama/llama-3.3-70b-instruct` | 编写事件总结。 |
| `judge` | `google/gemma-4-31b-it:free` | 评估分级;在紧密的循环中运行。 |
```bash
# 例如,给验证者分配不同的模型,并为评估循环使用更稳定的裁判:
export MODEL_VERIFIER="qwen/qwen3-coder:free"
export MODEL_JUDGE="openai/gpt-4o-mini" # 几美分;可靠的 15 倍评估循环
```
> **关于免费层(经过实践):** 免费模型会 **限制** — 对于单个运行或演示来说很好,但完整的 36 例评估会触发足够的调用以达到墙并在免费裁判上卡住。 与其与限制作斗争,我将裁判/验证器路由到 **Gemini`(`PROVIDER_JUDGE=gemini`,`MODEL_JUDGE=gemini-flash-latest`) — 一个更稳定的免费选项,具有工具能力和独立于 OpenRouter 答案者。而且,管道在 **优雅地降级** ,如果某个角色的模型不可用:分诊回落到“事件”,验证和事后总结被跳过,并附有注释,而不是崩溃运行。
### 直观地观看运行
`python -m viz.server` (然后打开 **http://localhost:8000**)是一个小的 **依赖无关** 的 Web 应用程序,它通过 Server-Sent Events 实时流式传输代理的轨迹。输入一个问题并观看整个流程一目了然: **RAG 检索 → 每个模型决策 → 只读工具调用(带有参数 + 观察)→ 循环 → 最终的引用答案。** 这是 **看到** 代理如何思考的最清晰方式 — 以及观看更便宜的模型采取更多步骤(或出错)比同一个问题上更强的模型。代理在正常使用时保持不变:可视化工具钩选一个可选的 `on_event` 回调,默认为关闭,因此 CLI 和评估不会受到影响。
### 可选:受控的多代理模式
默认情况下是 **单个代理** — 这是一个 **刻意、可防御的选择**。但是,有一个 **可选管道** (`ONCALL_MODE=multi` 或可视化工具的“多代理”切换)将调查员包装在另外三个角色中,以演示受控的多代理设计:
```
分诊(路由器)→ 调查员(单个代理)→ 验证者(独立)→ 事后总结
```
- **分诊** — 轻量级分类器:事件 / 知识 / 范围外。诚实地说是 **路由器** ,而不是重量级代理;它仅对明确的范围外问题进行短路,以便调查员保持完整的工具访问权限。
- **验证者** — 一个 **actor→critic** 护栏,由 **独立** 模型运行(`JUDGE_PROVIDER`/`JUDGE_MODEL`,默认为不同的模型 — 参见 [配置](#配置环境变量)中的单密钥设置),检查草稿是否基于收集的证据并违反安全规则。可以将其发送进行 **一次修订**。如果没有独立的裁判,它会回落到回答模型并说明,而不是假装独立。
- **事后总结** — 从轨迹中合成无责事件报告。
在之上,治理路径添加了您实际上想要在生产中使用的三个控件:
- **强制响应结构** — 答案必须带有标记的章节(`诊断/证据/推荐操作/批准`。策略存储在 [`guardrails.json`](./guardrails.json) 中(允许的工具、所需的引用、所需的章节、禁止的“我做了破坏性事情”的短语、批准语言要求)。`src/guardrails.py` 检查每个答案并在发生任何违规时强制进行修订。
- **完整的运行日志记录** — 每个运行(单个 *或* 多代理)都会将 JSONL 跟踪写入 `logs/run-<id>.jsonl`:推理,每个操作 + 观察,验证者裁决,护栏结果,最终答案。代理本身的可观察性。
**诚实的结果 — 和诚实的更新。** 在原始的 **15 例** 套件中,单代理和受控多代理都达到了约 80%:我无法检测到差异,我就说出来了。 这让我很烦,所以我构建了一个 **36 例** 套件(参见 [`IMPROVEMENTS.md`](./IMPROVEMENTS.md))来找出差异是否存在或只是太小而无法衡量。 结果是真实的:在更大的更难的集合中,受控多代理优于单代理 **56% → 78% (+22 分)**。 验证者和护栏捕获了单代理提出的过度声明 — 在一个案例中,它直接回答了 *"checkout 是健康的"* 当它不健康时 — 并且强制的结构 + 一次修订挽救了几个正确性案例。 这不是免费的:多代理在几个案例中回归了几个正确性案例,其中结构化的提示使弱的 `llama` 跳过了所需的工具调用。 但净收益是明确的、可衡量的胜利,在治理和可观察性(独立的安全检查、明确的策略、审计日志、事后总结)之上。
## 评估记分卡(真实,可复制)
**36 个标记事件。** 每个案例根据 **正确性** (LLM 作为裁判的评估:答案是否反映了关键事实?)、**工具** (它是否调用了案例所需的实时数据工具?)和 **安全性** (它是否避免了禁止性陈述,例如错误地将服务称为健康)进行评估。门槛为 **80%** (而不是 100% — 模型是非确定性的)。
标题是持有固定答案者(OpenRouter `llama-3.3-70b` · 裁判者 `claude-haiku-4-5`)的 **前后对比** — 所以唯一发生变化的是单代理与受控多代理:
| 设置(答案者 `llama-3.3-70b` · 裁判者 `claude-haiku-4-5`) | 通过率 | 门槛 |
|---|---|---|
| 单代理 | **20/36 = 56%** | ❌ 阻止 |
| **受控多代理** | **28/36 = 78%** | ❌ 阻止 |
**+22 分来自治理层** — 早期 15 例集合的准确性差异太小,无法揭示。两个都不超过 80% 门槛,因为 `llama` 是一个弱 **答案者**;治理有很大帮助,但不能完全弥补基础模型。
> *复制:* 记分卡使用最便宜的 Anthropic 模型作为更稳定的裁判 — `export JUDGE_PROVIDER=anthropic MODEL_JUDGE=claude-haiku-4-20251001` (需要 `ANTHROPIC_API_KEY`;整个套件几美分)。提交的默认裁判是免费的 OpenRouter 模型,因此新的克隆运行无需 Anthropic 密钥 — 但免费层在 36 例循环中会限制,因此更稳定的裁判值得拥有。
**早期运行(原始 15 例集合),供参考。** 使用 **Sonnet 进行回答和评判** ,单代理在 `get_metric` + 分段修复后达到 15/15 — Sonnet 是一个强大的答案者。 全部免费的堆栈(llama 回答,Gemini 评判)大约有 3/15,其中几个案例因免费层限制而丢失。两个经验教训:**裁判本身就是一个变量** (将其固定下来以比较答案者),以及 **答案者占主导地位** (大多数高分都是 Sonnet **回答** 的,而不是仅仅评判)。
**评估框架的作用:** 它将“哪种设置?” — 强大的答案者 vs 弱的,单代理 vs 受控,多代理 vs 另一个, — 变成一个有依据的决定,而不是一种感觉。数字在每次运行之间波动,这是 **为什么** 门槛是每个套件的百分比,而不是 100% 每个运行的规则。
### 已知失败模式(为什么网关不是一个舒适的通道)
该套件故意包含当前设计失败的硬案例。这些是**已理解的限制,而不是谜团** —— 并且是很好的面试材料:
1. ~~**`get_metric` 中的天真趋势标签。**~~ ✅ **已修复(2026-06-30)** — `get_metric` 现在具有阈值,并报告状态(正常/警告/严重)+ 一个幅度感知的趋势,因此它不再将0.1→0.2%的波动称为“上升”。这是最大的正确性提升(80% → 87%);参见 [`IMPROVEMENTS.md`](./IMPROVEMENTS.md)。 *原始经验教训,保留下来是因为这是重点:一个垃圾工具输出变成一个自信但错误的答案 — 修复仪器,而不是模型。*
2. ~~**关键词 RAG 回忆。**~~ ✅ **已修复(2026-07-01)** — 根源是*分段*,而不是检索方法:按空行分割允许一个只有标题的片段优先于*补救*段落。按 `##` 分段修复了数据库延迟案例(即使是纯关键词);一个可选的 **混合** 模式(关键词 + 本地嵌入,`RETRIEVAL_MODE=hybrid`)另外处理关键词无法处理的同义词/改述查询。参见 [`IMPROVEMENTS.md`](./IMPROVEMENTS.md)。 *经验教训:在使用更高级的检索器之前,请检查您的分段边界。*
3. **判断 + 基本事实严格性。** 几个案例取决于答案是否具有特定的框架(“它 *不是* 高”);当模型犹豫时,判断者(正确地)失败它。此外,超出范围的*拒绝*案例(`法国首都`)在运行之间波动 — 诚实的原因是网关是一个阈值,而不是“每次100%”。
### 直接衡量的检索质量
除了代理评估外,`python -m evals.retrieval_compare` 单独评估检索(在正确段落中回忆“黄金标记”短语 — 确定性,无需大型语言模型)。在 3 个难度递增的案例中比较关键词和混合检索:
| 案例 | 关键词 | 混合 |
|---|---|---|
| 简单 — “checkout 5xx, 第一次检查?” | 2/2 ✓ | 2/2 ✓ |
| 中等 — “搜索感觉延迟,如何调查?” | 2/2 ✓ | 2/2 ✓ |
| 大型 — “datastore 爬行得很慢… 加快速度?”(同义词间隙) | **0/2 ✗** | **2/2 ✓** |
| **Recall@4** | **67%** | **100%** |
大型案例隔离了嵌入式检索的优势:其单词与运行手册中的单词*完全不匹配*,因此关键词盲目地忽略了它,而嵌入式检索则通过含义匹配。
## 内部结构
| 路径 | 它演示的内容 |
|---|---|
| `src/retriever.py` | RAG:分段分块,关键词/语义/ **混合** (RRF)检索,引用,拒绝空上下文 |
| `evals/retrieval_compare.py` + `evals/retrieval_cases.jsonl` | 直接检索评估(recall@k):关键词与混合检索在 3 个案例中 |
| `src/tools.py` | 工具使用:JSON-Schema 工具定义,**仅可读** ,错误即结果恢复 |
| `src/llm.py` | 提供者抽象:一个中立日志 → Anthropic `tool_use` 块 **或** OpenAI 样式的 `tool_calls` (OpenAI,OpenRouter,Gemini) |
| `src/agent.py` | 代理循环:观察→行动→观察,最大步数停止 |
| `src/agents.py` | 可选多代理管道:分诊 → 调查 → 验证 → 事后分析 |
| `src/guardrails.py` + `guardrails.json` | 可配置的安全策略(允许的工具,引用,结构,批准) |
| `src/models.py` + `models.json` | 按角色划分的模型路由(调查员/分诊/验证员/事后分析/判断) |
| `src/trace.py` + `logs/` | 结构化每次运行的 JSONL 日志(推理,行动,观察,裁决) |
| `mcp_server/server.py` | MCP:向任何 MCP 客户端公开相同的 5 个工具 |
| `viz/` | 实时,无依赖的运行可视化(SSE) — 单代理和多代理流程 |
| `evals/` | 评估:数据集 + 大型语言模型作为判断 + 工具选择 + 安全 + 通过率网关 |
更深入的文章:
- **[`WALKTHROUGH.md`](./WALKTHROUGH.md)** — *我构建了什么,如何构建,为什么*,决策逐一列出,以我自己的声音(如果您想了解我的想法,请从这里开始)。
- **[`IMPROVEMENTS.md`](./IMPROVEMENTS.md)** — 改进日志:我做出的每个更改,为什么做,以及它教会我的内容(包括*没有*起作用的内容)。项目的演变。
- [`notes/full-build.md`](./notes/full-build.md) — 完整的构建指南 + 面试问答。
- [`notes/explained-simply.md`](./notes/explained-simply.md) — 普通英语游览,无专业术语。
## 安全
工具只能**读取**模拟文件 — 没有修改任何内容的代码路径。破坏性操作(回滚,重启)被*提议*,并附有明确的“需要人工批准”,评估套件断言助手从未声称执行过这些操作。安全由构造保证,而不仅仅是由指令保证。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器