在Agent执行写操作前,先用免费模型配额做只读探针,根据token预算(30Mtoken约9700次调用)计算任务覆盖量,避免免费额度的write side effects。
当前的 AI 功能周期已从「哪个模型回答得最好」转移到「哪个工具被允许改变状态」。最近的 agent-tooling 讨论不断落到同一个失败边界:写访问权限。一个模型在沙盒中生成一个糟糕的摘要,你什么也不会损失。但同一个模型如果更新工单状态、删除一行数据或发布一条回复,就会产生清理工作,其代价可能远超 token 成本。
先做预算算术,因为它会改变 canary 的设计。假设一次接地 RAG 调用使用 2,400 个 prompt token 和 700 个 completion token。每次调用 3,100 个 token,30M token 的配额大约支持 9,700 次调用。如果一个 agent 路径在每个任务中平均重试或重新规划 5 次调用,同样的预算只能覆盖不到 2,000 个任务。在这种情况下,免费的服务器槽位不是稀缺资源;token 消耗和副作用才是。
对于下面的沙盒,我使用了 MonkeyCode 的免费模型访问和免费服务器选项,运营商描述为包含 30M token 配额和一个免费服务器槽位。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。我将这两个可用性声明视为运营商提供的信息,而非永久配额或硬件保证。
第一个关卡不是 prompt,而是一个 HTTP 方法白名单。大多数写操作以 POST、PATCH、PUT 或 DELETE 形式到达。只读 canary 应该在默认模式下拒绝所有这些动词,同时仍然允许模型支持的 GET 路由,如摘要、分类或实体提取。这使得模型在不允许改变应用状态的情况下仍然有用。
表格迫使决策变得明确。如果一个路由用 GET 调用不安全,它就不应该在默认 canary 部署中运行。如果它是安全的,仍然需要记录 token 使用量,以免免费配额无声消失。
这个示例很小:一个 FastAPI 应用、一个 SQLite 台账,和一个除非设置了环境变量否则禁止写方法的 guard。
import os
import sqlite3
import time
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
import httpx
app = FastAPI()
DB = os.getenv("LEDGER_DB", "canary.db")
ALLOW_WRITE = os.getenv("ALLOW_WRITE") == "1"
def init_db():
with sqlite3.connect(DB) as conn:
conn.execute(
"""
CREATE TABLE IF NOT EXISTS token_events (
id INTEGER PRIMARY KEY,
route TEXT NOT NULL,
prompt_tokens INTEGER NOT NULL,
completion_tokens INTEGER NOT NULL,
created_at INTEGER NOT NULL
)
"""
)
@app.middleware("http")
async def read_only_guard(request: Request, call_next):
if not ALLOW_WRITE and request.method not in ("GET", "HEAD", "OPTIONS"):
return JSONResponse(
{"error": "write_path_blocked", "mode": "read_only_canary"},
status_code=403,
)
return await call_next(request)
async def call_model(route: str, messages: list[dict]) -> dict:
base_url = os.getenv("MODEL_BASE_URL")
if not base_url:
raise RuntimeError("MODEL_BASE_URL is not set")
resp = httpx.post(
f"{base_url}/chat/completions",
json={"messages": messages, "temperature": 0},
timeout=30,
)
resp.raise_for_status()
data = resp.json()
usage = data.get("usage") or {}
prompt_tokens = int(usage.get("prompt_tokens") or 0)
completion_tokens = int(usage.get("completion_tokens") or 0)
with sqlite3.connect(DB) as conn:
conn.execute(
"INSERT INTO token_events(route, prompt_tokens, completion_tokens, created_at) "
"VALUES (?, ?, ?, ?)",
(route, prompt_tokens, completion_tokens, int(time.time())),
)
return data
@app.get("/read/ticket-summary/{ticket_id}")
async def ticket_summary(ticket_id: str):
response = await call_model(
"summary",
[
{"role": "system", "content": "Summarize the ticket without taking an action."},
{"role": "user", "content": f"Read ticket {ticket_id} and summarize only."},
],
)
return {"ticket_id": ticket_id, "summary": response["choices"][0]["message"]["content"]}
该示例假设提供商的端点接受 OpenAI 风格的 messages body 并返回一个 usage 对象。如果你的提供商使用不同的 HTTP 形式,替换 call_model 即可,不需要改变台账。台账才是重要的部分。
几次调用后,检查配额用在了哪里:
SELECT route,
COUNT(*) AS calls,
SUM(prompt_tokens) AS prompt_tokens,
SUM(completion_tokens) AS completion_tokens,
SUM(prompt_tokens + completion_tokens) AS total_tokens
FROM token_events
GROUP BY route
ORDER BY total_tokens DESC;
对于预算检查,从运营商提供的配额中减去记录的总量,而不是将数字硬编码为永久保证。
def remaining_budget(budget=30_000_000) -> int:
with sqlite3.connect(DB) as conn:
row = conn.execute(
"SELECT COALESCE(SUM(prompt_tokens + completion_tokens), 0) FROM token_events"
).fetchone()
return budget - row[0]
一个有用的 canary 运行应该在 ALLOW_WRITE=1 被设置之前证明三件事。
默认模式拒绝写操作。POST、PATCH、PUT 和 DELETE 应该返回 403 和 write_path_blocked。如果它们没有返回,说明方法 guard 不是第一个关卡。
台账与提供商报告的使用量一致。运行相同的只读请求三次,比较 prompt_tokens 和 completion_tokens。大的差异意味着该路由是非确定性的、提供商在静默重试,或者 prompt 被以你没有预料到的方式扩展了。
剩余预算按预期量减少。在一个已知请求前后检查 remaining_budget。变化量应该等于报告的使用量减去任何提供商的取整。
写路径应该是一个独立的部署,而不是在 canary 环境中翻转一个标志。将 canary 保持在 ALLOW_WRITE=0,然后创建一个具有明确写授权和不同数据库或租户的第二版发布。
只读 guard 只检查 HTTP 动词。它无法捕获 GET 路由在应用代码中的隐藏副作用,所以保持只读路由的纯粹性。台账也依赖于提供商报告的 token 计数;本地 tokenizer 可能不同意,而这种差异是评估的一部分。
这也不是针对 prompt 注入的安全边界。一个能够读取私有数据并将其返回给恶意 prompt 的模型仍然存在数据泄露风险,即使该路由不能写。把 canary 视为发布关卡,而不是访问控制的替代品。
如果你已经运行了带有预算警报和审计日志的 LLM 网关,或者每个写操作在执行前已经有人审查,则跳过这种方法。但是,如果我要构建一个新的评估沙盒,我会使用一个可支配的免费服务器槽位来完成这个只读阶段,然后再申请写权限。