2026 年 6 月,一个 AI Agent 在扫描去中心化网络 DN42 时,因无限循环调用外部 API 导致运营商账单飙升至数千美元——这条登上 Hacker News 头条的帖子获得了超过 1000 个 upvote,引发了开发者社区对 AI Agent 安全工程 的深度反思。当 Agent 从「聊天助手」进化为「自主操作真实系统的执行者」时,传统的 prompt 安全已经远远不够——你需要一套完整的资源管控、成本防护和故障隔离工程体系。 本文将从这个真实案例出发,深入讲解如何构建与外部网络系统安全交互的 AI Agent。
🔐 一、为什么 Agent 安全需要重新定义?
1.1 从「幻觉」到「物理伤害」
传统 LLM 安全关注的是 输出质量——幻觉(Hallucination)、提示注入(Prompt Injection)、有害内容生成。但当 Agent 获得了调用外部系统的工具权限后,安全模型发生了根本性变化:
- ❌ 传统 LLM 安全:模型输出了错误的代码片段 → 影响:开发者浪费时间
- ✅ Agent 系统安全:模型发起了 10,000 次无限制的网络扫描 → 影响:真实金钱损失
DN42 事件的核心问题不是 AI 模型「犯了错」,而是 系统没有对 Agent 的行为施加任何资源约束。Agent 本质上是一个具有工具调用能力的自治循环(Autonomous Loop),如果没有外部限制,它会以机器的速度无限制地消耗资源。
⚠️ **警告:**永远不要将 Agent 的工具调用权限与资源消耗限制分开设计。给 Agent 一个没有速率限制的 API Key,等同于给一个实习生无限额度的公司信用卡。
1.2 Agent 安全的三层模型
构建安全的 Agent 系统需要在三个层面建立防线:
| 层面 | 关注点 | 传统做法 | Agent 时代做法 |
|---|---|---|---|
| 输入层 | Prompt 注入、越狱 | 输入过滤、System Prompt | 工具参数校验、意图白名单 |
| 执行层 | 工具调用安全 | 无(LLM 无执行能力) | 资源预算、熔断器、沙箱 |
| 输出层 | 幻觉、有害内容 | 输出过滤、RLHF | 结果审计、回滚机制 |
大多数团队只关注输入层和输出层,却忽略了执行层——这恰恰是 Agent 系统最关键的安全边界。
1.3 真实世界的 Agent 失败模式
根据 2026 年多个公开事故的分析,Agent 与外部系统交互时最常见的失败模式:
| 失败模式 | 发生频率 | 平均损失 | 根因 |
|---|---|---|---|
| 无限循环调用 | 高 | $500-$50,000 | 缺少终止条件 |
| 资源耗尽(内存/连接) | 中 | $100-$5,000 | 无资源配额 |
| 级联故障 | 中 | $1,000-$100,000 | 无熔断机制 |
| 权限提升 | 低 | 不可估量 | 过度授权 |
| 数据泄露 | 低 | 不可估量 | 无输出审计 |
🚀 二、资源预算控制:从 Token 到真实成本
2.1 多维度预算体系
Agent 的「成本」不仅仅是 LLM 的 Token 消耗。与外部网络系统交互时,你需要追踪多个维度的资源消耗:
// 资源预算定义 — 每个维度独立限制
interface ResourceBudget {
tokenBudget: number; // LLM Token 上限
apiCallBudget: number; // 外部 API 调用次数上限
networkBudget: number; // 网络请求字节数上限
timeBudget: number; // 总执行时间上限(毫秒)
costBudget: number; // 真实货币成本上限(美元)
}
// 预算追踪器 — 实时监控多维度资源消耗
class BudgetTracker {
private usage: Record<keyof ResourceBudget, number> = {
tokenBudget: 0,
apiCallBudget: 0,
networkBudget: 0,
timeBudget: 0,
costBudget: 0,
};
constructor(private budget: ResourceBudget) {}
// 检查是否超出任何维度的预算
checkBudget(dimension: keyof ResourceBudget, amount: number): boolean {
const projected = this.usage[dimension] + amount;
if (projected > this.budget[dimension]) {
console.warn(
`⚠️ 预算告警: ${dimension} 即将超限 ` +
`(${this.usage[dimension]} + ${amount} > ${this.budget[dimension]})`
);
return false;
}
return true;
}
// 记录资源消耗
record(dimension: keyof ResourceBudget, amount: number): void {
this.usage[dimension] += amount;
}
// 获取预算使用率
getUsageReport(): Record<string, { used: number; limit: number; pct: string }> {
const report: any = {};
for (const [key, value] of Object.entries(this.usage)) {
const limit = this.budget[key as keyof ResourceBudget];
report[key] = {
used: value,
limit,
pct: `${((value / limit) * 100).toFixed(1)}%`,
};
}
return report;
}
}
2.2 真实成本计算模型
LLM Token 只是成本的一部分。与网络系统交互时,真实成本包括:
// 真实成本计算器 — 综合考虑所有资源消耗
class CostCalculator {
// 2026 年主流模型定价(每百万 Token)
private static MODEL_PRICING: Record<string, { input: number; output: number }> = {
'claude-sonnet-4': { input: 3.0, output: 15.0 },
'gpt-4.1': { input: 2.0, output: 8.0 },
'deepseek-v4': { input: 0.27, output: 1.1 },
'gemini-2.5-pro': { input: 1.25, output: 10.0 },
};
// 外部 API 调用定价(按次计费示例)
private static API_PRICING: Record<string, number> = {
'cloud-dns-lookup': 0.0004, // $0.0004/次
'whois-query': 0.001, // $0.001/次
'port-scan': 0.0001, // $0.0001/端口
'ssl-cert-check': 0.0002, // $0.0002/次
};
static calculateLLMCost(
model: string,
inputTokens: number,
outputTokens: number
): number {
const pricing = this.MODEL_PRICING[model];
if (!pricing) throw new Error(`未知模型: ${model}`);
return (inputTokens * pricing.input + outputTokens * pricing.output) / 1_000_000;
}
static calculateTotalCost(params: {
model: string;
inputTokens: number;
outputTokens: number;
apiCalls: Array<{ type: string; count: number }>;
networkBytes: number;
}): number {
const llmCost = this.calculateLLMCost(params.model, params.inputTokens, params.outputTokens);
const apiCost = params.apiCalls.reduce((sum, call) => {
const unitPrice = this.API_PRICING[call.type] ?? 0.001;
return sum + unitPrice * call.count;
}, 0);
// 网络带宽成本:$0.09/GB(典型云厂商出站流量价格)
const networkCost = (params.networkBytes / (1024 * 1024 * 1024)) * 0.09;
return llmCost + apiCost + networkCost;
}
}
💡 **提示:**在 DN42 事件中,Agent 的主要成本来自外部 API 调用(数千次 DNS 查询和网络扫描),而非 LLM Token 消耗。如果你只监控 Token 预算,就像只看油表不看里程表一样危险。
2.3 渐进式预算收紧
聪明的做法不是一开始就设定严格的预算,而是根据任务复杂度动态调整:
// 渐进式预算策略 — 根据任务阶段收紧预算
function createProgressiveBudget(taskComplexity: 'low' | 'medium' | 'high'): ResourceBudget {
const baseBudgets: Record<string, ResourceBudget> = {
low: { tokenBudget: 50_000, apiCallBudget: 50, networkBudget: 10_000_000, timeBudget: 30_000, costBudget: 0.50 },
medium: { tokenBudget: 200_000, apiCallBudget: 200, networkBudget: 50_000_000, timeBudget: 120_000, costBudget: 2.00 },
high: { tokenBudget: 500_000, apiCallBudget: 500, networkBudget: 200_000_000, timeBudget: 300_000, costBudget: 5.00 },
};
return baseBudgets[taskComplexity];
}
⚡ 三、熔断器与安全执行模式
3.1 熔断器(Circuit Breaker)模式
当 Agent 与外部系统交互时,熔断器是最重要的安全机制。它在检测到异常行为时自动「断开电路」,阻止 Agent 继续消耗资源:
// 熔断器实现 — 保护 Agent 不会失控
class CircuitBreaker {
private state: 'closed' | 'open' | 'half-open' = 'closed';
private failureCount = 0;
private lastFailureTime = 0;
constructor(
private readonly failureThreshold: number = 5, // 连续失败次数阈值
private readonly recoveryTimeout: number = 60_000, // 恢复等待时间(毫秒)
private readonly anomalyThreshold: number = 3, // 异常调用频率倍数
) {}
// 检查是否允许执行
canExecute(): boolean {
if (this.state === 'closed') return true;
if (this.state === 'open') {
const elapsed = Date.now() - this.lastFailureTime;
if (elapsed > this.recoveryTimeout) {
this.state = 'half-open';
return true; // 允许一次试探性调用
}
return false;
}
return true; // half-open 状态允许一次调用
}
// 记录调用结果
recordResult(success: boolean): void {
if (success) {
if (this.state === 'half-open') {
this.state = 'closed';
this.failureCount = 0;
}
} else {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
this.state = 'open';
console.error(`🔴 熔断器触发!连续 ${this.failureCount} 次失败,暂停执行 ${this.recoveryTimeout / 1000}s`);
}
}
}
getState(): string {
return this.state;
}
}
3.2 调用频率异常检测
DN42 事件的核心问题是 Agent 以远超正常人类操作的频率调用外部 API。正常开发者一天扫描 50 个 IP,Agent 在 10 分钟内扫描了 5000 个。 你需要一个频率异常检测器:
// 滑动窗口频率检测器 — 捕捉异常调用模式
class RateAnomalyDetector {
private windows: Map<string, number[]> = new Map();
constructor(
private readonly windowSize: number = 60_000, // 窗口大小 1 分钟
private readonly maxPerWindow: number = 30, // 每窗口最大调用次数
) {}
// 检查调用是否异常
check(operation: string): { allowed: boolean; reason?: string } {
const now = Date.now();
const window = this.windows.get(operation) ?? [];
// 清理过期记录
const validCalls = window.filter(t => now - t < this.windowSize);
this.windows.set(operation, validCalls);
if (validCalls.length >= this.maxPerWindow) {
return {
allowed: false,
reason: `⚠️ 频率异常: ${operation} 在 ${this.windowSize / 1000}s 内已调用 ${validCalls.length} 次(阈值: ${this.maxPerWindow})`,
};
}
validCalls.push(now);
return { allowed: true };
}
}
// 使用示例
const detector = new RateAnomalyDetector(60_000, 30);
const result = detector.check('dns-lookup');
if (!result.allowed) {
throw new Error(result.reason); // 阻止调用并报警
}
3.3 安全执行沙箱
对于高风险操作(如网络扫描、批量 API 调用),应该在隔离的沙箱环境中执行:
// Agent 执行沙箱 — 限制资源和权限
class AgentSandbox {
private budgetTracker: BudgetTracker;
private circuitBreaker: CircuitBreaker;
private rateDetector: RateAnomalyDetector;
private executionLog: Array<{ timestamp: number; operation: string; result: string }> = [];
constructor(budget: ResourceBudget) {
this.budgetTracker = new BudgetTracker(budget);
this.circuitBreaker = new CircuitBreaker(5, 60_000);
this.rateDetector = new RateAnomalyDetector(60_000, 30);
}
// 安全执行工具调用
async executeToolCall<T>(
operation: string,
costEstimate: number,
executor: () => Promise<T>,
): Promise<T> {
// 1. 检查熔断器
if (!this.circuitBreaker.canExecute()) {
throw new Error(`🔴 熔断器开启,拒绝执行: ${operation}`);
}
// 2. 检查频率限制
const rateCheck = this.rateDetector.check(operation);
if (!rateCheck.allowed) {
throw new Error(rateCheck.reason!);
}
// 3. 检查预算
if (!this.budgetTracker.checkBudget('costBudget', costEstimate)) {
throw new Error(`💰 成本预算超限,拒绝执行: ${operation}(预估 $${costEstimate.toFixed(4)})`);
}
if (!this.budgetTracker.checkBudget('apiCallBudget', 1)) {
throw new Error(`📞 API 调用次数超限,拒绝执行: ${operation}`);
}
// 4. 执行并记录
const startTime = Date.now();
try {
const result = await executor();
this.circuitBreaker.recordResult(true);
this.budgetTracker.record('costBudget', costEstimate);
this.budgetTracker.record('apiCallBudget', 1);
this.executionLog.push({
timestamp: startTime,
operation,
result: `✅ 成功 (耗时 ${Date.now() - startTime}ms, 成本 $${costEstimate.toFixed(4)})`,
});
return result;
} catch (error) {
this.circuitBreaker.recordResult(false);
this.executionLog.push({
timestamp: startTime,
operation,
result: `❌ 失败: ${(error as Error).message}`,
});
throw error;
}
}
// 获取执行报告
getReport() {
return {
budgetUsage: this.budgetTracker.getUsageReport(),
circuitBreakerState: this.circuitBreaker.getState(),
executionLog: this.executionLog.slice(-20), // 最近 20 条
};
}
}
📌 **记住:**沙箱的核心设计原则是「默认拒绝」(Default Deny)。任何超出预定义白名单的操作都应该被自动阻止,而不是依赖 AI 模型自己判断「这个操作是否安全」。
💡 四、成本可观测性与告警
4.1 实时成本仪表盘
构建一个实时成本追踪系统,让你在 Agent 运行时就能看到资源消耗:
// 成本追踪中间件 — 包装每个工具调用
function createCostTrackedTool(
name: string,
tool: Function,
costEstimator: (args: any) => number,
sandbox: AgentSandbox,
) {
return async function trackedTool(args: any) {
const estimatedCost = costEstimator(args);
return sandbox.executeToolCall(
name,
estimatedCost,
() => tool(args),
);
};
}
// 使用示例:包装 DNS 查询工具
const safeDnsLookup = createCostTrackedTool(
'dns-lookup',
async (domain: string) => {
const response = await fetch(`https://dns.google/resolve?name=${domain}`);
return response.json();
},
() => 0.0004, // 每次查询成本 $0.0004
sandbox,
);
4.2 分级告警策略
不是所有超限都需要立即停止执行。设置分级告警:
| 级别 | 触发条件 | 动作 |
|---|---|---|
| 🟢 信息 | 任一维度使用率 > 50% | 记录日志 |
| 🟡 警告 | 任一维度使用率 > 75% | 发送通知,继续执行 |
| 🟠 严重 | 任一维度使用率 > 90% | 降级为只读模式 |
| 🔴 紧急 | 任一维度使用率 > 100% | 立即停止,触发熔断 |
4.3 执行后审计
每次 Agent 执行完成后,生成一份完整的审计报告:
// 审计报告生成器
interface AuditReport {
taskId: string;
startTime: string;
endTime: string;
totalCost: number;
breakdown: {
llmTokens: { input: number; output: number; cost: number };
apiCalls: Array<{ type: string; count: number; cost: number }>;
networkBytes: number;
networkCost: number;
};
toolCallSequence: Array<{
timestamp: number;
tool: string;
input: string;
output: string;
cost: number;
latencyMs: number;
}>;
riskEvents: Array<{
severity: 'warning' | 'critical';
description: string;
timestamp: number;
}>;
}
💡 **提示:**审计报告不仅是安全工具,也是优化依据。通过分析历史审计数据,你可以发现哪些工具调用模式是低效的,从而优化 Agent 的 Prompt 或工具设计。
⚠️ 五、避坑指南:来自真实事故的教训
坑 1:只监控 Token,不监控外部调用
很多团队只追踪 LLM Token 消耗,却忽略了 Agent 的外部 API 调用成本。在 DN42 事件中,Token 成本不到 $5,但外部 API 调用成本超过了 $3,000。
✅ **正确做法:**建立多维度预算体系,每个维度独立追踪和限制。
坑 2:信任 Agent 的「自省能力」
不要指望 AI 模型自己判断「我已经调用太多次了」。LLM 没有内置的资源感知能力,它的上下文窗口可能已经包含了数百次工具调用记录,但它不会主动停止。
✅ **正确做法:**所有资源限制必须在 Agent 循环的外部实现,不能依赖模型的自省。
坑 3:没有设置全局超时
Agent 可能陷入推理循环——每次工具调用都成功,但 Agent 永远不认为任务「完成」了。没有全局超时,Agent 可以运行数小时甚至数天。
✅ **正确做法:**设置硬性时间上限(如 5 分钟),超时后强制终止并保存中间结果。
坑 4:过度授权
给 Agent 一个具有 admin 权限的 API Key 是最常见的安全错误。Agent 只需要完成特定任务所需的最小权限。
✅ **正确做法:**遵循最小权限原则(Principle of Least Privilege),为每个 Agent 任务创建专用的受限凭证。
坑 5:缺少回滚机制
当 Agent 执行了错误操作(如删除了 DNS 记录、修改了防火墙规则),如果没有回滚机制,修复成本可能远超执行成本。
✅ **正确做法:**对所有写操作记录审计日志,支持一键回滚。优先使用幂等操作(Idempotent Operations)。
✅ 总结:Agent 安全工程清单
构建与外部系统安全交互的 AI Agent,核心要点:
- ✅ 多维度预算控制 — Token、API 调用、网络流量、时间、真实成本,缺一不可
- ✅ 熔断器模式 — 检测到异常行为时自动停止,不依赖模型自省
- ✅ 频率异常检测 — Agent 的调用频率应该与人类操作频率对齐
- ✅ 沙箱执行 — 默认拒绝,白名单放行,最小权限
- ✅ 实时可观测性 — 成本仪表盘 + 分级告警
- ✅ 执行后审计 — 完整的操作日志,支持回滚
- ✅ 全局超时 — 硬性时间上限,防止无限运行
⚡ **关键结论:**Agent 安全的核心不是让 AI 模型「更聪明」,而是在系统层面建立不可逾越的资源边界。模型会犯错,但系统约束不会。DN42 事件给所有构建 Agent 系统的开发者敲响了警钟:给 Agent 能力的同时,必须给它边界。
相关工具推荐:
- 🔧 LangSmith — Agent 执行追踪与成本监控
- 🔧 Helicone — LLM API 成本分析与优化
- 🔧 OpenTelemetry — 分布式追踪,监控 Agent 与外部系统的交互链路
- 🔧 jsjson.com JSON 格式化工具 — 处理 Agent 审计日志中的 JSON 数据