在敏感环境( coworkingspace、热aptop、有 staging 秘钥在 prompt 里)跑本地 AI Agent 有风险。作者提出分类-决策-生成的顺序 gate:先判断环境是否安全,再决定走本地还是云端模型,最后才生成。
一位支付工程师坐在联合办公空间里,让一个本地编码 AI 智能体检查一个 webhook 重试循环。提示词里包含一个从终端回滚缓冲区中粘贴过来的预发布密钥。彼时笔记本电脑的风扇因为一个 Docker 构建已经嗡嗡作响,咖啡馆的 Wi-Fi 每隔几分钟就会丢包。工程师想要本地推理以保护隐私,但那台机器即将因为过热而触发降频,并超出延迟预算。
本文把那个联合办公空间的场景视为一个路由问题,而非模型质量问题。许多本地优先的 AI 智能体栈假设宿主机会保持凉爽、密钥安全、并具备完整的离线能力。当笔记本电脑发热、提示词包含敏感信息、网络仅有部分可用时,这些假设会同时崩塌。一个小的预检门控可以在四个信号都同意某一条路由之前拒绝生成。
大多数 AI 智能体 CLI 在用户敲下回车时就立即调用模型,然后事后才应用过滤器。事后 redact 无法撤回已经离开开发者电脑的预发布密钥。事后延迟日志也无法恢复已经在过热笔记本电脑上卡住的编辑器交互。更安全的操作顺序是:先分类、再决策、最后生成,并打印出原因码。
本地优先仍然是此类读者的默认选择,因为源代码、密钥和未完成的 diff 不应该习惯性地外传。云端溢出是矩阵在提示词安全且宿主机确实超出预算后才授予的特权。当运行时已宕机且任务涉及公开文档时,离线模式不是一种纯洁的标志。有趣的设计是一个能说出"本地"、"远程"或"拒绝"的门控,然后停下来。
信号一是密钥类别,而这个标签与用户意图不是同一回事。公开的文档字符串重写可以离开机器,而粘贴的环境片段绝对不行。跳过这个标签的团队往往按任务类型路由,然后在一次本无害的重构中泄露凭证。因此,当分类器不确定时,预检失败于关闭状态,而不是寄希望于远程日志会忘记这条记录。
信号二是连接性,包括那些仍然能返回一个成功 ping 包的降级咖啡馆网络。单个 ICMP 响应并不意味着长生成流能够撑过 NAT 空闲超时。信号三是本地延迟预算,与最近的 p95 相比,而非峰值 token 速率。信号四是远程的备用容量和允许的路径,这只在密钥和连接性都允许溢出之后才起作用。
下面这个矩阵是给笔记本电脑 AI 智能体的提案,不是经过衡量的生产 SLO。运营商在信任远程分支之前,应该用自己的运行时探测样本替换示例 p95。密钥类别仍然是策略输入,而非模型输出,因为在压力下分类器会发明标签。连接性使用对目标溢出主机的短 HTTPS 探测,而非通用的 ping。
这张表是团队可以复制到设计评审中的产物,无需采用任何供应商。Remote 仅出现在两行中,且两行都需要在剥离后保留非密钥部分。降级网络不溢出,因为一个挂起的流比一个缓慢的本地答案更糟糕。Refuse 是一个一等路由,不是一个埋在 HTTP 客户端代码里的异常处理器。
以下 Python 模块是一个未执行的示例,团队可以粘贴到临时文件中。在后方连接了远程路由的适配器之前,它不会调用任何供应商。把它放在 AI 智能体 CLI 旁边,这样在任何 tokenizer 运行之前就打印出原因码。保持密钥扫描器故意简单直白,因为精巧的 NLP 分类器正是内部 token 泄露的方式。
# preflight.py — proposal / unexecuted example
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
from typing import Optional
import re
import time
import urllib.request
class SecretClass(str, Enum):
PUBLIC = "PUBLIC"
INTERNAL = "INTERNAL"
SECRET = "SECRET"
UNKNOWN = "UNKNOWN"
class Connectivity(str, Enum):
OFFLINE = "OFFLINE"
DEGRADED = "DEGRADED"
ONLINE = "ONLINE"
class Route(str, Enum):
LOCAL = "local"
REMOTE = "remote"
REMOTE_SANITIZED = "remote_sanitized"
REFUSE = "refuse"
@dataclass(frozen=True)
class Signals:
secret_class: SecretClass
connectivity: Connectivity
local_runtime_up: bool
local_p95_ms: int
latency_budget_ms: int
@dataclass(frozen=True)
class Decision:
route: Route
reason_code: str
sanitized_prompt: Optional[str]
这些类型使得后面的矩阵在日志中可读,这在 refuse 决策看起来像生产故障时非常重要。Frozen dataclasses 防止后续钩子在打印后修改密钥类别。Enum 干净地序列化为 JSON 追踪,用于事后复盘而不会泄露提示词主体。sanitized prompt 是可选的,因为 refuse 和 local-secret 路由不能留下一个剥离副本悬停。
SECRET_PATTERNS = (
re.compile(r"sk-[A-Za-z0-9]{8,}"), # placeholder shape, not a live credential
re.compile(r"-----BEGIN (?:RSA |OPENSSH )?PRIVATE KEY-----"),
re.compile(r"(?i)(api[_-]?key|secret|password|token)\s*[:=]\s*\S+"),
re.compile(r"AKIA[0-9A-Z]{16}"),
)
INTERNAL_MARKERS = (
re.compile(r"(?i)staging\."),
re.compile(r"(?i)internal-only"),
re.compile(r"(?i)\b10\.\d+\.\d+\.\d+\b"),
)
def classify_prompt(text: str) -> SecretClass:
if any(p.search(text) for p in SECRET_PATTERNS):
return SecretClass.SECRET
if any(p.search(text) for p in INTERNAL_MARKERS):
return SecretClass.INTERNAL
if not text.strip():
return SecretClass.UNKNOWN
return SecretClass.PUBLIC
def strip_internal(text: str) -> str:
redacted = text
for p in SECRET_PATTERNS:
redacted = p.sub("[REDACTED_SECRET]", redacted)
redacted = re.sub(r"(?i)\b10\.\d+\.\d+\.\d+\b", "[REDACTED_IP]", redacted)
return redacted
扫描器特意使用基于模式的方式,这样审查者可以在 pull request 中列出每一条匹配。它会漏掉新型密钥格式,这就是为什么 UNKNOWN 和 SECRET 在矩阵中都失败关闭的原因。INTERNAL 是一个较弱的类别,适用于不应该成为公开训练数据的hostname和 RFC1918 地址。剥离在内存中进行,应该记录为替换次数,而非被替换的值。
def probe_connectivity(url: str, timeout_s: float = 2.0) -> Connectivity:
started = time.monotonic()
try:
req = urllib.request.Request(url, method="HEAD")
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
elapsed_ms = (time.monotonic() - started) * 1000
if resp.status >= 500:
return Connectivity.DEGRADED
if elapsed_ms > 800:
return Connectivity.DEGRADED
return Connectivity.ONLINE
except Exception:
return Connectivity.OFFLINE
def read_local_p95(path: str = "/tmp/local-agent-p95.ms") -> int:
try:
return int(open(path).read().strip())
except (OSError, ValueError):
return 10**9 # fail closed: missing probe counts as infinitely slow
该探测对目标溢出主机使用 HEAD,这样 DNS 和 TLS 失败都计为离线。跳过 ping,因为 ICMP 在仍然允许 HTTPS 的咖啡馆网络上往往被阻止。本地 p95 从一个辅助文件读取,运行时心跳应该已经在写入该文件。缺失的指标失败关闭,这样冷的笔记本电脑就不会声称自己很快。
一个微小的心跳写入器可以在团队已经在笔记本电脑上运行的本地运行时旁边运行。下一个代码片段是关于那个辅助文件的提案,而非关于任何特定模型二进制的声明。虚拟样本保持文章的诚实,因为这里没有报告来自本账户的真实 p95。在为公开提示词启用任何远程溢出之前,用真实的计时数据替换那个列表。
# proposal: write a dummy local p95 from ten placeholder samples
python - <<'PY'
from pathlib import Path
samples = [1800, 2100, 1950, 2400, 2600, 1900, 2050, 2300, 2500, 2200]
samples.sort()
p95 = samples[int(0.95 * (len(samples) - 1))]
Path("/tmp/local-agent-p95.ms").write_text(str(p95))
print(p95)
PY
def decide(signals: Signals, prompt: str) -> Decision:
secret = signals.secret_class
online = signals.connectivity
up = signals.local_runtime_up
over = signals.local_p95_ms > signals.latency_budget_ms
if secret in (SecretClass.SECRET, SecretClass.UNKNOWN):
if up:
return Decision(Route.LOCAL, "LOCAL_SECRET_PIN", None)
return Decision(Route.REFUSE, "REFUSE_SECRET_NO_RUNTIME", None)
if online is Connectivity.OFFLINE: if up: return Decision(Route.LOCAL, "LOCAL_OFFLINE_OK", None) code = ( "REFUSE_INTERNAL_OFFLINE_DEAD" if secret is SecretClass.INTERNAL else "REFUSE_OFFLINE_DEAD" ) return Decision(Route.REFUSE, code, None)
if online is Connectivity.DEGRADED: if up: return Decision(Route.LOCAL, "LOCAL_DEGRADED_HOLD", None) return Decision(Route.REFUSE, "REFUSE_DEGRADED_DEAD", None)
if up and not over:
return Decision(Route.LOCAL, "LOCAL_BUDGET_OK", None)
if secret is SecretClass.PUBLIC: if up and over: return Decision(Route.REMOTE, "REMOTE_LATENCY_SPILL", prompt) if not up: return Decision(Route.REMOTE, "REMOTE_RUNTIME_DOWN", prompt)
if secret is SecretClass.INTERNAL and (over or not up): return Decision( Route.REMOTE_SANITIZED, "REMOTE_STRIP_THEN_SPILL", strip_internal(prompt), )
return Decision(Route.REFUSE, "REFUSE_UNMAPPED", None)
每个分支都有返回值,这正是笔记本电脑上预检闸门的核心逻辑。公开工作在笔记本超出预算或本地运行时宕机时可能溢出。内部工作只有在剥离后才能溢出,而机密工作永远不会溢出。未映射的组合直接拒绝,而不是发明一个无法审计的巧妙默认行为。
5. 在模型适配器运行前打印决策
def preflight_or_raise(prompt: str, spill_url: str, budget_ms: int = 2000) -> Decision: signals = Signals( secret_class=classify_prompt(prompt), connectivity=probe_connectivity(spill_url), local_runtime_up=_runtime_up(), local_p95_ms=read_local_p95(), latency_budget_ms=budget_ms, ) decision = decide(signals, prompt) print( f"route={decision.route.value} reason={decision.reason_code} " f"secret={signals.secret_class.value} net={signals.connectivity.value}" ) if decision.route is Route.REFUSE: raise SystemExit(f"preflight refused: {decision.reason_code}") return decision
def _runtime_up(host: str = "127.0.0.1", port: int = 11434) -> bool: import socket try: with socket.create_connection((host, port), timeout=0.3): return True except OSError: return False
python -c "from preflight import preflight_or_raise; preflight_or_raise('rewrite this public README section', 'https://example.invalid/health')"
打印出的原因行是与坐在笔记本前的人类之间的契约。如果原因码是 LOCAL_SECRET_PIN,prompt 永远不会穿过网卡,即使风扇正在狂转。如果原因码是 REMOTE_LATENCY_SPILL,团队已经接受了文本是公开的且传输较慢。如果原因码是拒绝,CLI 会在咖啡馆 Wi-Fi 上开始重试风暴之前退出。
这些测试被标记为提案,不报告实际部署的结果。它们锁定了设计评审真正关心的 AI 智能体 CLI 故障关闭行为。在将模块复制到笔记本上的一个一次性虚拟环境后,用 pytest 运行它们。扩展决策表,而不是在 decide 函数内部添加隐藏分支。
from preflight import ( Connectivity, Route, SecretClass, Signals, decide, classify_prompt )
def test_secret_never_spills_when_runtime_is_up(): s = Signals(SecretClass.SECRET, Connectivity.ONLINE, True, 9000, 2000) d = decide(s, "token=sk-EXAMPLEONLY") assert d.route is Route.LOCAL assert d.reason_code == "LOCAL_SECRET_PIN" assert d.sanitized_prompt is None
def test_secret_refuses_when_runtime_is_down(): s = Signals(SecretClass.SECRET, Connectivity.ONLINE, False, 9000, 2000) d = decide(s, "-----BEGIN RSA PRIVATE KEY-----") assert d.route is Route.REFUSE
def test_public_spills_on_latency(): s = Signals(SecretClass.PUBLIC, Connectivity.ONLINE, True, 4000, 2000) d = decide(s, "Rewrite a public README badge section.") assert d.route is Route.REMOTE assert d.reason_code == "REMOTE_LATENCY_SPILL"
def test_degraded_does_not_spill(): s = Signals(SecretClass.PUBLIC, Connectivity.DEGRADED, True, 4000, 2000) d = decide(s, "Rewrite a public README badge section.") assert d.route is Route.LOCAL assert d.reason_code == "LOCAL_DEGRADED_HOLD"
def test_classify_unknown_empty(): assert classify_prompt(" ") is SecretClass.UNKNOWN
这些单元测试锁定的是路由策略,而非生成质量或任何供应商吞吐量声明。测试全绿意味着笔记本不会因为 p95 文件看起来不美观而静默泄露私钥。测试通过不代表生成质量达标,不代表工具调用技能完备,也不代表法律审查已完成。团队应在将测试套件称为控制措施之前,为自己的机密形态添加测试 fixture。
何时免费服务器该赢,何时绝不能赢
延迟、机密和离线行为往不同方向拉扯,这就是为什么单一默认配置无法支撑笔记本智能体。当 prompt 是机密的、网络处于降级状态、或近期 p95 仍符合编辑器预算时,本地推理胜出。当运行时在线时,离线是公开工作的特性;当运行时宕机时,离线是一个陷阱。只有在提示为公开或经 sanitized 处理的内部内容、且路径健康、主机已耗尽预算后,远程计算才获胜。
当矩阵选中远程计算时,实现需要一个不需要签订新供应商合同就能进行短期试用的端点。披露:本文作为 MonkeyCode 产品推广的一部分而准备。MonkeyCode 提供免费模型访问和一个可作为预检批准的公开工作溢出主机的免费服务器选项。该适配器应位于 REMOTE 原因码之后,它不是本地 pin 规则的替代品。
后续适配器在原因码打印到终端后,将 Route.REMOTE 映射到该免费服务器。Route.REMOTE_SANITIZED 必须只发送 decision.sanitized_prompt,绝不能发送原始内部文本。Route.LOCAL 应继续使用桌上的运行时,即使远程探测看起来空闲且健康。这些映射都不应绕过 classify_prompt,或在打印后重写 SECRET 类。
正则表达式机密检测是不完整的,会漏掉使用不熟悉前缀的云密钥。执意绕过检测的用户仍可能粘贴不匹配示例模式的口令。p95 文件在心跳死亡而模型进程完全正常时可能已过时。HEAD 探测可能被强制门户拦截,而浏览器会话仍可通过。
该矩阵还忽略了真实笔记本上的电池百分比、热区和磁盘加密状态。添加这些信号是有用的,但每个新轴都增加了拒绝路径的审查成本。此设计面向开发者工作站上的交互式凭感觉编程智能体,而非无人值守的批处理作业。护栏不是合规项目,也不能单独满足审计要求。
谁不应该使用这种方法
没有合法远程溢出出口的气隙团队应删除远程行,而非探测免费服务器。无法定义 INTERNAL 与 SECRET 区别的组织不应将 sanitizer 当作保险库发货。需要人工介入的生产事故响应者不应在分页笔记本上自动溢出。尚未运行本地运行时的用户会遇到拒绝码,应从纯本地 CLI 开始。
如何处理原因码
将原因码视为智能体本身的产品分析,仅存储原因码而不存储 prompt 正文。一周的 LOCAL_SECRET_PIN 事件意味着扫描器正在捕获意外事故,远程路径保持未使用。一周的 REFUSE_OFFLINE_DEAD 事件意味着本地运行时需要一个看门狗而非新模型。一周的 REMOTE_LATENCY_SPILL 事件意味着溢出计算正在赢得一席之地,而不会成为默认选项。
已经在使用本地 pin 规则的读者可以将 preflight.py 放到 CLI 旁边,在日常工作中观察原因码。如果公开溢出只在风扇已经很大声时才出现,那么四信号护栏的路由比模型切换器更好。干净码运行一天后,将仅允许的公开溢出指向上述免费服务器选项。让机密数据留在桌上,将拒绝码视为预检的成功结果。
如需进一步行动,你可以考虑屏蔽此人并/或举报滥用