大模型工具调用的安全与稳定性实战

在前面的文章中,我们让 AI 拥有了“双手”(Function Calling)并统一了“接口”(MCP 协议)。看着 AI Copilot 行云流水地帮我们查天气、搜文档,确实令人兴奋。

但是,给 AI 赋予行动能力,就像给一个聪明的五岁小孩一把上了膛的枪。大模型会产生幻觉,用户会进行 Prompt 注入攻击(Prompt Injection),下游的 API 也会因为并发过高而宕机。

在将 AI 助手推向生产环境之前,我们必须构建一套坚固的防御体系。结合我们的项目代码,今天我们就来盘点工具调用中不可或缺的安全与稳定性控制


一、 永远不要信任大模型的输出:输入验证与清洗

在传统的 Web 开发中,我们有一条铁律:“永远不要信任用户的输入”。在 AI 开发中,这条规则要加上半句:“也永远不要信任大模型的输出”。

大模型在提取参数时,可能会带上多余的 Markdown 标记、恶意的 SQL 注入代码,或者奇怪的系统路径。在我们的项目中,所有的工具输入除了要经过 Zod 的类型校验,还必须经过专门的 input-sanitizers.ts 进行清洗。

实战代码:地点与搜索词的净化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/app/api/scrape/security/input-sanitizers.ts

/**
* 清洗地点输入,防止命令注入或路径穿越
*/
export function sanitizeLocation(input: string): string {
if (!input) return "";
// 仅允许字母、数字、中文、空格和基础标点,剥离特殊控制字符
return input.replace(/[^\w\u4e00-\u9fa5\s,.-]/gi, "").trim();
}

/**
* 清洗向量数据库检索词
*/
export function sanitizeSearchQuery(input: string): string {
if (!input) return "";
// 限制最大长度,防止超长文本打爆 Embedding 模型的 Token 限制
const truncated = input.slice(0, 200);
// 移除可能干扰 Pinecone 检索的特殊符号
return truncated.replace(/[\$#{}\\]/g, "").trim();
}

防御逻辑:在 lookupWeather 和 searchReactDocs 工具的执行层,第一步永远是调用这些清洗函数。宁可让查询失败,也不能让脏数据进入下游。


二、 保护你的钱包与下游系统:调用频率限制 (Rate Limit)

大模型在处理复杂任务时,可能会陷入死循环(比如工具 A 报错,AI 不断重试调用工具 A),这不仅会迅速烧光你的 LLM Token 余额,还会对下游服务造成 DdoS 级别的打击。

因此,我们需要在请求上下文中引入限流机制(Rate Limiting)。

实战代码:给工具加一把“锁”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/app/api/scrape/resilience/rate-limit.ts

export class ToolRateLimiter {
private requests = new Map<string, number[]>();
private readonly WINDOW_MS = 60000; // 1分钟窗口
private readonly MAX_REQUESTS = 10; // 每个用户每分钟最多调用 10 次工具

public checkLimit(userId: string, toolName: string): boolean {
const key = `${userId}:${toolName}`;
const now = Date.now();
const timestamps = this.requests.get(key) || [];

// 过滤掉窗口期外的时间戳
const validTimestamps = timestamps.filter(ts => now - ts < this.WINDOW_MS);

if (validTimestamps.length >= this.MAX_REQUESTS) {
console.warn(`[RateLimit] 用户 ${userId} 频繁调用 ${toolName} 被拦截`);
return false; // 触发限流
}

validTimestamps.push(now);
this.requests.set(key, validTimestamps);
return true;
}
}

防御逻辑:当 AI 试图以每秒 5 次的速度疯狂调用 search_react_docs 时,限流器会直接返回类似 "当前工具调用频率过高,请稍后再试" 的文本结果,强行打断 AI 的幻觉循环。


三、 雁过留声,查“内鬼”必备:审计日志 (Audit Log)

当你的 AI Copilot 在生产环境运行了一周后,你可能会收到用户的反馈:“AI 给我的财报数据是错的!” 这时候如果你没有日志,根本无法排查是大模型胡说八道,还是你的 API 返回了脏数据

我们需要对每一次工具的调用、输入、输出和耗时进行无死角的记录。

实战代码:基于上下文的日志追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/app/api/scrape/observability/audit-log.ts

export interface AuditLogEntry {
traceId: string;
toolName: string;
rawInput: any;
sanitizedInput: any;
executionTimeMs: number;
status: "success" | "error";
resultPreview: string; // 截断后的结果预览
}

export const auditLogger = {
log: (entry: AuditLogEntry) => {
// 生产环境中,这里通常会发送到 Datadog、ELK 或 ClickHouse
console.log(`[AUDIT LOG] [${entry.traceId}] Tool: ${entry.toolName} | Status: ${entry.status} | Time: ${entry.executionTimeMs}ms`);

// 如果是错误状态,记录详细堆栈
if (entry.status === "error") {
console.error(`[AUDIT ERROR] Details:`, entry.rawInput);
}
}
}

防御逻辑:在 mcp/protect-tools.ts 或执行中间件中拦截调用,记录 Trace ID。一旦出现故障,通过 Trace ID 可以完整回溯用户的提问、大模型生成的 JSON,以及工具的实际输出。


四、 最小权限原则:工具的读写隔离

在我们的代码设计中,你可能会注意到 index.ts 里的工具注册包含了 annotations 属性:

1
2
3
4
5
6
7
8
9
10
// src/app/api/scrape/tools/index.ts
export const SCRAPE_TOOLS = {
lookup_weather: {
// ...
annotations: {
readOnlyHint: true, // 【关键】声明这是一个只读操作
destructiveHint: false, // 【关键】声明它不会破坏数据
},
},
}

这是一个极其优秀的实践。我们在给 AI 分配工具时,应该严格遵循最小权限原则 (Least Privilege)

  1. 只读操作 (Read-Only):如查天气、搜文档,可以自动放行。

  2. 破坏性操作 (Destructive):如删除数据库记录、发邮件、执行服务器脚本。这类工具在本地执行前,必须强制拦截,通过 UI 弹窗要求人类用户进行二次确认(Human-in-the-loop)。


结语:让 AI 成为助手,而不是隐患

构建一个 AI Copilot,写对 Prompt 只是第一步,调通 MCP 只是第二步。真正的挑战在于:如何在一个充满不确定性(幻觉、超时、脏数据)的 LLM 生态中,构建一个确定且稳定的工程系统

通过输入清洗、频率限制、审计日志和权限隔离这四道防线,我们就成功地为 AI 戴上了“紧箍咒”。它依然可以七十二变,但再也无法大闹天宫。

源码已托管至🫱 GitHub,期待你的 Star🌟。


大模型工具调用的安全与稳定性实战
http://example.com/2026/05/02/大模型工具调用的安全与稳定性实战/
作者
Lingkai Shi
发布于
2026年5月2日
许可协议