序:90% 的 AI 应用,死在了”上线”这一步

2026 年上半年,一个残酷的现实正在浮现:Gartner 报告指出,超过 85% 的 AI 项目从未真正进入生产环境。 不是因为模型不够好,不是因为功能没实现,而是因为——它们无法稳定地、安全地、经济地在真实流量下持续运行。

在过去四篇文章中,我们从概念走到代码,构建了完整的 AI Agent 开发能力链:

  • 第一篇:理解了 AI Agent 的范式跃迁,用 Vercel AI SDK 跑通了第一个 Agent
  • 第二篇:设计了 Agent 的工具系统,掌握了 Function Calling 与 MCP
  • 第三篇:编排了多 Agent 协作,构建了覆盖前后端的 AI 开发团队
  • 第四篇:实现了 Agent 的记忆与状态管理,让它跨会话”记住”用户

但在本地的 npm run dev 里跑得再漂亮,也只是温室里的花朵。一旦推向真实的生产环境,你将面对一系列全新的问题:

API 在凌晨 3 点超时了,为什么?Token 消耗突然飙升了 300%,是谁在用?一个恶意用户注入了 Prompt,把系统指令泄露了,怎么办?大模型厂商限流了,你的服务怎么办?灰度发布时,如何对比两个模型版本的输出质量?

这些问题,没有一篇论文会教你,没有一个 SDK 能帮你解决。它们属于工程化的领域——一个前端开发者可能不太熟悉,但在 AI 时代必须掌握的领域。

从原型到生产,AI Agent 应用需要的不是”更强的模型”,而是”更硬的工程”。

本篇是《前端 + AI Agent 开发实战》系列的收官之作。我们将从部署架构、安全防护、可观测性、成本优化、弹性伸缩到 CI/CD,系统性地拆解 AI Agent 应用的生产化路径。


一、生产环境:一场完全不同的游戏

1.1 为什么”能跑”不等于”能上线”

在开发阶段,你的 Agent 面对的环境是理想化的:

1
2
3
4
5
6
7
8
开发环境(理想化)              生产环境(真实世界)
───────────────── ──────────────────
单用户,低并发 → 多用户,高并发
LLM API 响应稳定 → 限流、超时、降级
无恶意输入 → Prompt 注入、滥用
不计成本 → Token 费用按月计费
无状态崩溃 → 需要优雅降级
手动测试验证 → 需要自动化评估

这意味着,你在本地用 50 个测试用例验证通过的 Agent,到了生产环境可能在第 51 个真实请求上就崩溃了。

1.2 AI Agent 应用的特殊运维挑战

与传统 Web 应用相比,AI Agent 应用在生产环境中有五个独特挑战:

挑战一:非确定性输出。 传统 API 对于相同输入,永远返回相同输出。但 LLM 是概率模型——同一个 Prompt,两次调用可能返回不同结果。这让传统的断言式测试和监控几乎失效。

挑战二:长时延迟。 一个普通 API 的响应时间在 50-200ms 之间。而一次 Agent 推理(含多轮工具调用)可能需要 10-60 秒。这彻底改变了超时策略、连接管理、用户体验设计。

挑战三:Token 经济学。 传统应用的成本是固定的(服务器+带宽)。AI 应用的成本是变量——每次调用都消耗 Token,而 Token 价格随模型和输入长度变化。一个用户的一天高频使用,可能花掉你一整个月的预算。

挑战四:安全新维度。 传统安全关注的是 SQL 注入、XSS 攻击。AI Agent 还要面对 Prompt 注入——攻击者通过用户输入劫持 Agent 的指令,让它执行未授权操作。更棘手的是,Agent 拥有工具调用能力,一个被劫持的 Agent 可能直接修改数据库。

挑战五:质量评估。 传统应用的质量是二元的——功能正确或错误。AI 应用的输出质量是连续的——“足够好”到”完全不能用”之间有广阔的灰色地带,而且没有标准答案。

1
2
3
4
5
6
7
8
9
10
// 一个看似简单的问题,暴露了所有挑战
async function handleUserMessage(userId: string, message: string) {
// 挑战1:如何限制单用户并发?
// 挑战2:如果 LLM 响应超过 30 秒怎么办?
// 挑战3:这条消息会消耗多少 Token?预算够吗?
// 挑战4:message 里是否包含恶意 Prompt?
// 挑战5:如何评估 Agent 的回复质量?
const response = await agent.generate(message);
return response;
}

理解了这些挑战,我们就能有针对性地设计生产架构。


二、部署架构:从单机到分布式

2.1 容器化:一切的基础

AI Agent 应用的容器化与普通 Web 应用类似,但有几个关键差异需要处理。

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
26
27
28
29
30
31
32
33
34
35
36
37
# Dockerfile - AI Agent 应用
FROM node:22-alpine AS builder

WORKDIR /app

# 利用 Docker 层缓存:先复制 package.json
COPY package*.json ./
RUN npm ci --production=false

# 复制源码并构建
COPY . .
RUN npm run build

# ---- 运行阶段 ----
FROM node:22-alpine AS runner

WORKDIR /app

# 安全:使用非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001 -G nodejs

# 只复制生产依赖和构建产物
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

USER nextjs

# 健康检查 - 注意 AI 应用需要特殊的健康端点
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

EXPOSE 3000

# 启动命令:使用生产模式
CMD ["node", "--max-old-space-size=1024", "dist/server.js"]

关键点解释:

  1. 内存限制。 AI Agent 应用在处理长上下文时内存消耗大。--max-old-space-size=1024 限制 Node.js 堆内存,防止单个请求 OOM。
  2. 多阶段构建。 构建产物不含 devDependencies,镜像体积从 1.2GB 降到 180MB。
  3. 非 root 用户。 安全最佳实践——如果 Agent 被劫持,攻击者拿到的不是 root 权限。

2.2 健康检查:AI 应用的特殊设计

普通应用的健康检查是”HTTP 200”。AI Agent 应用的健康检查需要分层:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/health.ts - 分层健康检查
import { Router } from 'express';

const healthRouter = Router();

// Liveness Probe - 进程是否活着
healthRouter.get('/health', (_req, res) => {
res.status(200).json({ status: 'alive' });
});

// Readiness Probe - 是否准备好接收流量
healthRouter.get('/ready', async (_req, res) => {
const checks = await Promise.allSettled([
checkLLMConnection(), // LLM API 是否可达
checkDatabaseConnection(), // 数据库是否连通
checkRedisConnection(), // Redis(缓存/队列)是否连通
checkVectorDBConnection(), // 向量数据库是否连通
]);

const results = checks.map((c, i) => ({
service: ['llm', 'database', 'redis', 'vectorDB'][i],
status: c.status === 'fulfilled' ? 'healthy' : 'unhealthy',
latency: c.status === 'fulfilled' ? c.value : null,
}));

const allHealthy = results.every(r => r.status === 'healthy');

res.status(allHealthy ? 200 : 503).json({
status: allHealthy ? 'ready' : 'degraded',
checks: results,
timestamp: new Date().toISOString(),
});
});

// LLM 连通性检查
async function checkLLMConnection(): Promise<number> {
const start = Date.now();
const response = await fetch(`${process.env.LLM_BASE_URL}/v1/models`, {
headers: { 'Authorization': `Bearer ${process.env.LLM_API_KEY}` },
signal: AbortSignal.timeout(5000), // 5秒超时
});
if (!response.ok) throw new Error(`LLM health check failed: ${response.status}`);
return Date.now() - start;
}

export { healthRouter };

为什么分层? Kubernetes 的 Liveness Probe 失败会重启 Pod,Readiness Probe 失败只会从负载均衡中摘除。如果 LLM API 临时不可用,你应该停止接收新请求(Readiness 失败),但不应该重启应用(Liveness 通过),因为重启并不能解决 LLM 的问题。

2.3 整体架构图

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
26
27
28
29
30
31
          ┌──────────────────────────────────────────┐
│ 用户(浏览器 / API) │
└──────────────────┬───────────────────────┘

┌────────▼────────┐
│ CDN / WAF │ ← DDoS 防护、静态资源缓存
└────────┬────────┘

┌────────▼────────┐
│ API Gateway │ ← 认证、限流、路由
│ (Kong/Traefik) │
└────────┬────────┘

┌──────────────────┼──────────────────┐
│ │ │
┌────────▼──────┐ ┌────────▼───────┐ ┌───────▼───────┐
│ Agent Service │ │ Agent Service │ │ Agent Service │ ← 水平伸缩
│ (Pod #1) │ │ (Pod #2) │ │ (Pod #N) │
└───────┬───────┘ └───────┬────────┘ └───────┬───────┘
│ │ │
┌────────┴─────────────────┴──────────────────┴────────┐
│ 服务层 │
├──────────┬──────────┬──────────┬──────────┬──────────┤
│ LLM │ Database │ Redis │ VectorDB │ Queue │
│ Proxy │PostgreSQL│ (Cache) │ Qdrant │ BullMQ │
└──────────┴──────────┴──────────┴──────────┴──────────┘

┌────────▼────────┐
│ LLM Provider │ ← OpenAI / Anthropic / 自托管
│ (外部 API) │
└─────────────────┘

2.4 LLM 代理层:隔离外部依赖

LLM API 是外部依赖,可能限流、超时、价格变动。在应用和 LLM 之间加一层代理是生产环境的标配:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// src/llm/llm-proxy.ts - LLM 代理层
import { OpenAI } from 'openai';

// 多 Provider 故障转移配置
const providers = [
{
name: 'primary',
client: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
model: 'gpt-4o',
weight: 80, // 80% 流量
},
{
name: 'fallback',
client: new OpenAI({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: 'https://api.anthropic.com/v1/',
}),
model: 'claude-sonnet-4-20250514',
weight: 20, // 20% 流量
},
];

export class LLMProxy {
private circuitBreaker = new Map<string, { failures: number; lastFailure: number }>();

async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
// 1. 按权重选择 Provider
const provider = this.selectProvider();

// 2. 检查熔断器
if (this.isCircuitOpen(provider.name)) {
// 跳到备用 Provider
const fallback = providers.find(p => p.name !== provider.name)!;
return this.callWithRetry(fallback, messages, options);
}

// 3. 带超时和重试的调用
return this.callWithRetry(provider, messages, options);
}

private async callWithRetry(
provider: typeof providers[0],
messages: ChatMessage[],
options?: ChatOptions,
maxRetries = 3,
): Promise<ChatResponse> {
let lastError: Error;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await provider.client.chat.completions.create({
model: options?.model || provider.model,
messages,
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 4096,
}, {
// 请求级超时:第一次 30s,后续递增
timeout: 30000 + attempt * 10000,
maxRetries: 0, // 我们自己管理重试
});

// 成功则重置熔断器
this.resetCircuit(provider.name);

return {
content: response.choices[0].message.content,
usage: {
promptTokens: response.usage!.prompt_tokens,
completionTokens: response.usage!.completion_tokens,
},
provider: provider.name,
};
} catch (error) {
lastError = error as Error;
this.recordFailure(provider.name);

// 指数退避
const delay = Math.min(1000 * Math.pow(2, attempt), 8000);
await this.sleep(delay);
}
}

throw new LLMProxyError(`All retries exhausted: ${lastError.message}`, {
provider: provider.name,
attempts: maxRetries,
});
}

private selectProvider() {
const totalWeight = providers.reduce((s, p) => s + p.weight, 0);
let random = Math.random() * totalWeight;
for (const p of providers) {
random -= p.weight;
if (random <= 0) return p;
}
return providers[0];
}

private isCircuitOpen(name: string): boolean {
const state = this.circuitBreaker.get(name);
if (!state) return false;
// 连续失败 5 次则熔断 60 秒
if (state.failures >= 5) {
const elapsed = Date.now() - state.lastFailure;
if (elapsed < 60000) return true; // 熔断中
// 半开状态:允许试探性请求
this.circuitBreaker.set(name, { failures: 0, lastFailure: 0 });
}
return false;
}

private recordFailure(name: string) {
const state = this.circuitBreaker.get(name) || { failures: 0, lastFailure: 0 };
state.failures++;
state.lastFailure = Date.now();
this.circuitBreaker.set(name, state);
}

private resetCircuit(name: string) {
this.circuitBreaker.set(name, { failures: 0, lastFailure: 0 });
}

private sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}

这层代理解决了三个核心问题:

  1. 故障转移:主 Provider 挂了,自动切换到备用
  2. 熔断保护:连续失败时停止重试,避免雪崩
  3. 超时控制:指数退避重试,防止请求堆积

三、流量入口与安全防护

3.1 API Gateway:第一道防线

在生产环境中,每一个进入 Agent 的请求都必须经过 API Gateway。它负责认证、限流、路由和日志记录。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// src/middleware/api-gateway.ts
import { Request, Response, NextFunction } from 'express';
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';

// ---- 认证中间件 ----
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
const apiKey = req.headers['x-api-key'] as string;

if (!apiKey) {
return res.status(401).json({ error: 'Missing API key' });
}

// 验证 API Key 并加载用户信息
const user = validateApiKey(apiKey);
if (!user) {
return res.status(401).json({ error: 'Invalid API key' });
}

req.user = user;
next();
}

// ---- 多维度限流策略 ----
export function createRateLimiters(redisClient: any) {
const redisStore = new RedisStore({
sendCommand: (...args: string[]) => redisClient.call(...args),
});

return {
// 维度1:全局 QPS 限制(保护整体服务)
global: rateLimit({
store: redisStore,
windowMs: 60 * 1000, // 1 分钟窗口
max: 1000, // 全局 1000 QPM
message: { error: 'Server is busy, please try later' },
}),

// 维度2:单用户限制(防滥用)
perUser: rateLimit({
store: redisStore,
windowMs: 60 * 1000,
max: 30, // 每用户 30 请求/分钟
keyGenerator: (req) => req.user.id,
message: { error: 'Rate limit exceeded for your account' },
}),

// 维度3:Token 消耗限制(控成本)
tokenBudget: async (req: Request, res: Response, next: NextFunction) => {
const userId = req.user.id;
const today = new Date().toISOString().split('T')[0];
const key = `token_budget:${userId}:${today}`;

// 从 Redis 获取今日已消耗 Token 数
const consumed = parseInt(await redisClient.get(key) || '0', 10);
const dailyLimit = req.user.plan === 'pro' ? 500000 : 50000;

if (consumed >= dailyLimit) {
return res.status(429).json({
error: 'Daily token budget exceeded',
consumed,
limit: dailyLimit,
resetAt: 'tomorrow 00:00 UTC',
});
}

next();
},
};
}

三维度限流的意义: 全局限流保护服务、用户限流防滥用、Token 限流控成本。三者缺一不可——光有 QPS 限制,一个用户可以慢慢消耗你所有的 Token 预算;光有 Token 限制,一个用户的突发请求可能拖垮整个服务。

3.2 Prompt 注入防护

这是 AI Agent 安全面临的最独特威胁。攻击者通过用户输入,试图覆盖 Agent 的系统指令。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// src/security/prompt-guard.ts

// 输入净化:检测并过滤潜在的 Prompt 注入
export function detectPromptInjection(input: string): { safe: boolean; threats: string[] } {
const threats: string[] = [];
const lower = input.toLowerCase();

// 模式1:试图覆盖系统指令
const systemOverridePatterns = [
/ignore\s+(all\s+)?(previous|above|prior)\s+instructions?/i,
/disregard\s+(all\s+)?(previous|your)\s+instructions?/i,
/you\s+are\s+now\s+(a|an)\s+/i, // "you are now a..."
/new\s+instructions?\s*:/i,
/system\s*:\s*/i,
/\[system\]/i,
];

// 模式2:试图获取系统提示
const extractionPatterns = [
/(repeat|show|print|reveal|output)\s+(your|the)\s+(system\s+)?(prompt|instructions?|rules?)/i,
/what\s+(are|is)\s+your\s+(system\s+)?(prompt|instructions?)/i,
];

// 模式3:角色扮演绕过
const jailbreakPatterns = [
/let'?s\s+play\s+a\s+game/i,
/pretend\s+(you\s+are|to\s+be)/i,
/roleplay\s+as/i,
/hypothetical\s+scenario/i,
];

// 模式4:分隔符逃逸
const delimiterEscapePatterns = [
/```+/, // Markdown 代码块逃逸
/<\|system\|>/i, // 特殊标记
/<\|im_start\|>/i,
];

const allPatterns = [
...systemOverridePatterns,
...extractionPatterns,
...jailbreakPatterns,
...delimiterEscapePatterns,
];

for (const pattern of allPatterns) {
if (pattern.test(input)) {
threats.push(pattern.source);
}
}

return {
safe: threats.length === 0,
threats,
};
}

// 输出过滤:防止 Agent 泄露系统提示
export function sanitizeOutput(output: string, systemPrompt: string): string {
// 检查输出是否包含系统提示的片段
const systemChunks = systemPrompt.split('\n').filter(c => c.length > 20);

let sanitized = output;
for (const chunk of systemChunks) {
if (sanitized.includes(chunk)) {
// 替换泄露的系统提示片段
sanitized = sanitized.replace(chunk, '[REDACTED]');
}
}

return sanitized;
}

// 综合安全中间件
export function promptSecurityMiddleware(req: Request, res: Response, next: NextFunction) {
const { message } = req.body;

if (message) {
const check = detectPromptInjection(message);
if (!check.safe) {
// 记录安全事件
logger.warn('Prompt injection detected', {
userId: req.user.id,
threats: check.threats,
input: message.substring(0, 200),
});

return res.status(400).json({
error: 'Input rejected by security policy',
code: 'PROMPT_INJECTION_DETECTED',
});
}
}

next();
}

3.3 数据安全:最小权限原则

Agent 拥有工具调用能力——它可以读写数据库、执行代码、访问 API。这要求你以最小权限原则设计每一个工具:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// src/security/permission-system.ts

// 定义权限等级
enum Permission {
READ_ONLY = 'read',
WRITE_OWN = 'write_own', // 只能操作自己资源
WRITE_ALL = 'write_all', // 管理员级
EXECUTE = 'execute', // 执行代码
}

// 工具权限声明
const toolPermissions: Record<string, {
requiredPermission: Permission;
rateLimit?: { calls: number; windowMs: number };
auditLog: boolean;
}> = {
'search_documents': {
requiredPermission: Permission.READ_ONLY,
rateLimit: { calls: 20, windowMs: 60_000 },
auditLog: false,
},
'create_task': {
requiredPermission: Permission.WRITE_OWN,
rateLimit: { calls: 10, windowMs: 60_000 },
auditLog: true,
},
'delete_task': {
requiredPermission: Permission.WRITE_OWN,
rateLimit: { calls: 5, windowMs: 60_000 },
auditLog: true,
},
'execute_code': {
requiredPermission: Permission.EXECUTE,
rateLimit: { calls: 3, windowMs: 60_000 },
auditLog: true,
// 沙箱限制:只允许在隔离环境中执行
sandbox: true,
},
};

// 权限检查中间件
export function checkToolPermission(
toolName: string,
userId: string,
userPermissions: Permission[],
) {
const config = toolPermissions[toolName];
if (!config) {
throw new Error(`Unknown tool: ${toolName}`);
}

if (!userPermissions.includes(config.requiredPermission)) {
throw new PermissionDeniedError(
`Tool "${toolName}" requires ${config.requiredPermission} permission`,
);
}

return config;
}

核心原则: Agent 不应该拥有比用户更大的权限。如果一个用户没有删除数据的权限,那么 Agent 代用户操作时也不应该能删除数据。


四、可观测性:让 Agent 的行为可见

4.1 三大支柱:日志、指标、链路追踪

传统应用的可观测性关注”请求是否成功”。AI Agent 应用的可观测性还需要回答”Agent 做了什么决策、用了多少 Token、质量如何”。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// src/observability/logger.ts - 结构化日志
import pino from 'pino';

export const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
// 生产环境使用 JSON 格式,便于日志聚合
...(process.env.NODE_ENV === 'production'
? {}
: { transport: { target: 'pino-pretty' } }),
});

// AI Agent 专用日志结构
export interface AgentLogEntry {
// 标准字段
timestamp: string;
level: 'debug' | 'info' | 'warn' | 'error';
traceId: string; // 链路追踪 ID
spanId: string; // 当前 Span ID

// Agent 专属字段
agentId: string; // 哪个 Agent
userId: string; // 哪个用户
sessionId: string; // 哪个会话

// 事件类型
event: 'tool_call' | 'llm_request' | 'llm_response' | 'error' | 'decision';

// 载荷(根据 event 类型不同)
data: {
// tool_call: { toolName, args, result, durationMs }
// llm_request: { model, messages, temperature, maxTokens }
// llm_response: { content, usage, latencyMs, finishReason }
// error: { type, message, stack, recoverable }
// decision: { reasoning, action, confidence }
[key: string]: unknown;
};

// Token 追踪
tokenUsage?: {
prompt: number;
completion: number;
total: number;
estimatedCost: number;
};
}

4.2 链路追踪:看见 Agent 的每一步

一次 Agent 请求可能涉及多轮 LLM 调用、多个工具执行、数据库查询。链路追踪让你看见完整的执行路径:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// src/observability/tracer.ts
import { trace, context, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('ai-agent-app');

// 包装 Agent 执行流程,自动生成追踪 Span
export async function tracedAgentExecution(
userId: string,
sessionId: string,
message: string,
) {
return tracer.startActiveSpan('agent.execute', async (span) => {
try {
span.setAttributes({
'agent.user_id': userId,
'agent.session_id': sessionId,
'agent.message_length': message.length,
'agent.message_hash': hashMessage(message), // 不存原文,隐私保护
});

// 阶段1:意图理解
const intent = await tracer.startActiveSpan('agent.understand_intent', async (s) => {
const result = await understandIntent(message);
s.setAttribute('agent.intent', result.type);
s.setAttribute('agent.confidence', result.confidence);
s.end();
return result;
});

// 阶段2:工具选择与执行
const toolResults = await tracer.startActiveSpan('agent.execute_tools', async (s) => {
const tools = selectTools(intent);
s.setAttribute('agent.tools_selected', tools.map(t => t.name).join(','));

const results = await Promise.all(
tools.map(tool => executeToolTraced(tool, intent)),
);
s.end();
return results;
});

// 阶段3:生成回复
const response = await tracer.startActiveSpan('agent.generate_response', async (s) => {
const result = await generateResponse(message, toolResults);
s.setAttribute('llm.model', result.model);
s.setAttribute('llm.tokens_prompt', result.usage.promptTokens);
s.setAttribute('llm.tokens_completion', result.usage.completionTokens);
s.setAttribute('llm.latency_ms', result.latencyMs);
s.end();
return result;
});

span.setAttributes({
'agent.total_tokens': response.usage.totalTokens,
'agent.total_duration_ms': Date.now() - span.startTime?.[0]! * 1000,
});

span.setStatus({ code: SpanStatusCode.OK });
return response;
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: (error as Error).message,
});
throw error;
} finally {
span.end();
}
});
}

// 包装单个工具执行
async function executeToolTraced(tool: Tool, intent: Intent) {
return tracer.startActiveSpan(`tool.${tool.name}`, async (span) => {
span.setAttributes({
'tool.name': tool.name,
'tool.args': JSON.stringify(intent.params),
});

const start = Date.now();
try {
const result = await tool.execute(intent.params);
span.setAttribute('tool.duration_ms', Date.now() - start);
span.setAttribute('tool.success', true);
return result;
} catch (error) {
span.setAttribute('tool.success', false);
span.setAttribute('tool.error', (error as Error).message);
throw error;
} finally {
span.end();
}
});
}

追踪数据的可视化效果: 在 Jaeger 或 Grafana Tempo 中,你能看到一条完整的调用链:

1
2
3
4
5
6
7
8
9
10
agent.execute (3.2s)
├── agent.understand_intent (450ms)
│ └── llm.request → llm.response (430ms, 120 tokens)
├── agent.execute_tools (2.1s)
│ ├── tool.search_documents (800ms)
│ ├── tool.query_database (150ms)
│ └── tool.create_summary (1.1s)
│ └── llm.request → llm.response (1.0s, 850 tokens)
└── agent.generate_response (650ms)
└── llm.request → llm.response (630ms, 400 tokens)

哪个环节慢、哪个环节消耗 Token 最多,一目了然。

4.3 质量监控:不只是”有没有报错”

传统监控只关注技术指标(CPU、内存、延迟)。AI Agent 应用还需要质量指标——回答好不好、准不准、有没有幻觉。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// src/observability/quality-monitor.ts

// 定义质量评估维度
interface QualityMetrics {
// 客观指标(自动计算)
responseLatencyMs: number;
tokenEfficiency: number; // 信息量 / Token 数
toolCallSuccessRate: number; // 工具调用成功率
hallucinationScore?: number; // 幻觉检测分数(0-1,越低越好)

// 主观指标(来自用户反馈)
userFeedbackScore?: number; // 用户评分(1-5)
userFeedbackText?: string; // 用户文字反馈
isMarkedHelpful?: boolean; // 是否被标记为"有帮助"
}

// 质量监控服务
export class QualityMonitor {
// 采样评估:不是每个请求都做质量分析(太贵)
private readonly sampleRate = 0.1; // 10% 采样

async evaluate(
requestId: string,
response: AgentResponse,
userMessage: string,
): Promise<QualityMetrics | null> {
// 只有被采样到的请求才做深度评估
if (Math.random() > this.sampleRate) {
return null;
}

// 使用小模型做质量评估(成本可控)
const evaluation = await this.callEvaluationModel({
userQuery: userMessage,
agentResponse: response.content,
toolsUsed: response.tools.map(t => t.name),
context: response.contextSummary,
});

const metrics: QualityMetrics = {
responseLatencyMs: response.latencyMs,
tokenEfficiency: this.calculateTokenEfficiency(response),
toolCallSuccessRate: this.calculateToolSuccessRate(response),
hallucinationScore: evaluation.hallucinationRisk,
};

// 写入时间序列数据库
await this.metricsClient.write('agent_quality', metrics, {
tags: { agentId: response.agentId, requestId },
});

return metrics;
}

// 基于规则的幻觉检测(快速、低成本)
detectHallucination(response: string, context: string[]): number {
// 检查回复中的事实声明是否有上下文支撑
const claims = extractClaims(response);
let unsupported = 0;

for (const claim of claims) {
const hasSupport = context.some(c =>
computeSimilarity(claim, c) > 0.7 // 相似度阈值
);
if (!hasSupport) unsupported++;
}

return claims.length > 0 ? unsupported / claims.length : 0;
}
}

4.4 实战监控面板

将以上指标整合到 Grafana 面板中,你的运维团队就能像监控传统服务一样监控 AI Agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌─────────────────────────────────────────────────────────────────┐
│ AI Agent 运维仪表盘 │
├───────────────────┬───────────────────┬───────────────────────┤
│ 请求量 (RPM) │ 平均延迟 (P50) │ Token 消耗 (今天) │
│ ▲ 1,234 │ ▲ 2.3s │ ▲ 4.2M / 10M │
│ ─────── │ ─────── │ ─────── │
│ 周环比 +12% │ 周环比 +0.1s │ 预算使用 42% │
├───────────────────┼───────────────────┼───────────────────────┤
│ 工具调用成功率 │ 质量评分 (平均) │ 安全事件 (24h) │
│ ▲ 98.5% │ ▲ 4.2 / 5 │ ▲ 3 (已拦截) │
│ ─────── │ ─────── │ ─────── │
│ ↓ 0.3% 需关注 │ 用户反馈 +15% │ 0 起成功注入 │
├───────────────────┴───────────────────┴───────────────────────┤
│ 关键告警 │
│ ⚠ Token 消耗速率异常 - 当前消耗速度将在 3 天内耗尽月度预算 │
│ ⚠ LLM API 延迟 P99 超过 15s - 已自动切换到备用 Provider │
│ ⚠ 工具 "execute_code" 调用失败率 > 5% - 检查沙箱环境 │
└─────────────────────────────────────────────────────────────────┘

五、成本优化:让每一分钱花在刀刃上

5.1 Token 经济学

AI Agent 应用的运营成本与传统应用有本质区别:成本与使用量正相关,而非固定的。

一个简单的计算:假设使用 GPT-4o,输入 $2.5/M tokens,输出 $10/M tokens。一个日均 1000 次对话的应用,每次平均消耗 2000 输入 + 500 输出 Token:

1
2
3
4
日成本 = 1000 × (2000 × $2.5/1M + 500 × $10/1M)
= 1000 × ($0.005 + $0.005)
= $10/天
= $300/月

如果优化到 1500 输入 + 300 输出 Token(减少冗余上下文):

1
2
3
4
日成本 = 1000 × (1500 × $2.5/1M + 300 × $10/1M)
= 1000 × ($0.00375 + $0.003)
= $6.75/天
= $202.50/月

月度节省 33%。 这就是 Token 优化的价值。

5.2 语义缓存:最高 ROI 的优化

很多用户的提问是相似的。如果每次都调用 LLM,就是白花钱。语义缓存通过向量相似度匹配,让相似问题复用之前的回答。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
// src/cache/semantic-cache.ts
import { QdrantClient } from '@qdrant/js-client';

export class SemanticCache {
private qdrant: QdrantClient;
private readonly collection = 'agent_cache';
private readonly similarityThreshold = 0.95; // 高阈值,避免错误缓存

constructor() {
this.qdrant = new QdrantClient({
url: process.env.QDRANT_URL,
apiKey: process.env.QDRANT_API_KEY,
});
}

async get(query: string, context: CacheContext): Promise<CacheHit | null> {
// 1. 将查询转为 Embedding
const embedding = await this.embed(query);

// 2. 在向量数据库中搜索相似缓存
const results = await this.qdrant.search(this.collection, {
vector: embedding,
filter: {
must: [
{ key: 'agentId', match: { value: context.agentId } },
{ key: 'modelVersion', match: { value: context.modelVersion } },
],
},
limit: 1,
score_threshold: this.similarityThreshold,
});

if (results.length === 0) return null;

const cached = results[0].payload as CachedResponse;

// 3. 检查缓存是否过期
const age = Date.now() - cached.timestamp;
if (age > this.getMaxAge(cached)) {
return null; // 缓存过期
}

// 4. 记录缓存命中(用于计算节省成本)
await this.recordCacheHit(cached, results[0].score);

return {
response: cached.response,
similarity: results[0].score,
cachedAt: cached.timestamp,
savedTokens: cached.tokenUsage,
};
}

async set(
query: string,
response: string,
context: CacheContext,
tokenUsage: TokenUsage,
): Promise<void> {
const embedding = await this.embed(query);

await this.qdrant.upsert(this.collection, {
points: [{
id: crypto.randomUUID(),
vector: embedding,
payload: {
query,
response,
agentId: context.agentId,
modelVersion: context.modelVersion,
tokenUsage,
timestamp: Date.now(),
},
}],
});
}

// 判断哪些查询适合缓存
shouldCache(query: string, context: CacheContext): boolean {
// 不缓存:包含时间戳、随机数、用户特定ID的查询
if (/今天|现在|刚刚|刚才/i.test(query)) return false;
if (/\b(my|our|this)\s+\w+\s+(id|name|file|project)\b/i.test(query)) return false;

// 缓存:通用知识性问题
if (/什么是|解释一下|how\s+to|what\s+is|explain/i.test(query)) return true;

return false; // 默认不缓存,避免过度缓存
}

private async embed(text: string): Promise<number[]> {
// 使用小而快的 Embedding 模型
const response = await fetch('https://api.openai.com/v1/embeddings', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'text-embedding-3-small', // $0.02/M tokens,极便宜
input: text.substring(0, 8000), // 截断,控制成本
}),
});
const data = await response.json();
return data.data[0].embedding;
}
}

实际效果: 在客服场景中,语义缓存通常能命中 20-40% 的请求,直接节省相应比例的 LLM 调用成本。Embedding 的成本极低(约为主模型成本的 1/100),ROI 非常高。

5.3 模型路由:用对的模型做对的事

不是所有任务都需要最贵的模型。模型路由策略根据任务复杂度动态选择模型:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// src/llm/model-router.ts

type TaskComplexity = 'trivial' | 'simple' | 'moderate' | 'complex';

const modelMapping: Record<TaskComplexity, { model: string; maxTokens: number }> = {
trivial: { model: 'gpt-4o-mini', maxTokens: 500 }, // $0.15/$0.60 per M
simple: { model: 'gpt-4o-mini', maxTokens: 1000 }, // 同上
moderate: { model: 'gpt-4o', maxTokens: 2000 }, // $2.5/$10 per M
complex: { model: 'gpt-4o', maxTokens: 4096 }, // 同上,但更长
};

export class ModelRouter {
// 评估任务复杂度
assessComplexity(message: string, context: AgentContext): TaskComplexity {
const len = message.length;
const hasCode = /```|function|class|const|import/.test(message);
const hasReasoning = /为什么|分析|比较|设计|架构|why|analyze|compare|design/i.test(message);
const toolCount = context.availableTools.length;

// 复杂任务:需要推理 + 工具调用 + 代码生成
if (hasReasoning && hasCode && toolCount > 3) return 'complex';

// 中等任务:需要推理或代码
if (hasReasoning || (hasCode && toolCount > 1)) return 'moderate';

// 简单任务:有上下文依赖
if (len > 100 || toolCount > 0) return 'simple';

// 琐碎任务:简单问答
return 'trivial';
}

route(message: string, context: AgentContext) {
const complexity = this.assessComplexity(message, context);
const config = modelMapping[complexity];

logger.info('Model routing decision', {
complexity,
model: config.model,
messageLength: message.length,
});

return config;
}
}

成本影响: 在实际项目中,约 60% 的请求可以用 mini 模型处理。综合下来,模型路由可以将平均成本降低 50-70%。

5.4 成本仪表盘

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
26
27
28
29
30
31
32
33
// src/observability/cost-tracker.ts

export class CostTracker {
private readonly pricing = {
'gpt-4o': { input: 2.5e-6, output: 10e-6 }, // per token
'gpt-4o-mini': { input: 0.15e-6, output: 0.6e-6 },
'text-embedding-3-small': { input: 0.02e-6, output: 0 },
};

// 按用户/天维度追踪成本
async recordUsage(userId: string, model: string, usage: TokenUsage) {
const cost = this.calculateCost(model, usage);
const today = new Date().toISOString().split('T')[0];
const key = `cost:${userId}:${today}`;

await redis.hincrbyfloat(key, 'total', cost);
await redis.hincrbyfloat(key, `model:${model}`, cost);
await redis.hincrby(key, 'requests', 1);
await redis.expireat(key, nextMidnightTimestamp());

// 实时成本告警
const dailyTotal = await redis.hget(key, 'total');
if (parseFloat(dailyTotal) > this.getAlertThreshold(userId)) {
await this.triggerCostAlert(userId, parseFloat(dailyTotal));
}
}

private calculateCost(model: string, usage: TokenUsage): number {
const price = this.pricing[model];
if (!price) return 0;
return usage.promptTokens * price.input + usage.completionTokens * price.output;
}
}

六、弹性伸缩与容错

6.1 伸缩策略

AI Agent 应用的伸缩策略与传统应用不同——因为单个请求耗时长、资源消耗大。

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# k8s/hpa-agent.yaml - Kubernetes HPA 配置
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: agent-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: agent-service
minReplicas: 3 # 最少 3 个副本(高可用基线)
maxReplicas: 30 # 最多 30 个副本
metrics:
# 指标1:CPU 使用率(传统指标)
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60

# 指标2:内存使用率
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70

# 指标3:自定义指标 - 活跃请求数(更适合 AI 应用)
- type: Pods
pods:
metric:
name: active_agent_requests
target:
type: AverageValue
averageValue: "10" # 每个 Pod 平均 10 个活跃请求时扩容

# 伸缩行为:快速扩容,缓慢缩容
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # 30秒内快速扩容
policies:
- type: Percent
value: 100 # 每次最多翻倍
periodSeconds: 30
scaleDown:
stabilizationWindowSeconds: 300 # 5分钟稳定期才缩容
policies:
- type: Percent
value: 10 # 每次最多缩 10%
periodSeconds: 60

为什么”快扩慢缩”? AI 请求是长时任务——一个请求可能需要 30 秒。如果缩容太快,正在处理的请求会被中断。快速扩容应对突发流量,缓慢缩容等待正在进行的请求完成。

6.2 降级策略

当系统压力过大或 LLM 不可用时,需要有优雅的降级方案:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// src/resilience/degradation.ts

export class DegradationManager {
private currentLevel: 'normal' | 'degraded' | 'emergency' = 'normal';

// 根据系统状态调整行为
async handleRequest(
message: string,
context: AgentContext,
): Promise<AgentResponse> {
switch (this.currentLevel) {
case 'normal':
// 正常模式:完整 Agent 流程
return this.fullAgentFlow(message, context);

case 'degraded':
// 降级模式:跳过多轮工具调用,直接用 LLM 回答
return this.simplifiedFlow(message, context);

case 'emergency':
// 紧急模式:使用缓存或预设回复
return this.emergencyResponse(message, context);
}
}

// 系统状态监控,自动切换降级等级
async monitor() {
setInterval(async () => {
const metrics = await this.getSystemMetrics();

if (metrics.llmErrorRate > 0.3 || metrics.queueDepth > 1000) {
this.currentLevel = 'emergency';
logger.error('System entered emergency mode', metrics);
} else if (metrics.llmLatencyP99 > 30000 || metrics.cpuUsage > 0.85) {
this.currentLevel = 'degraded';
logger.warn('System entered degraded mode', metrics);
} else {
this.currentLevel = 'normal';
}
}, 10000); // 每10秒检查一次
}

private async simplifiedFlow(message: string, context: AgentContext) {
// 跳过工具选择和多轮循环,直接单轮 LLM 调用
const response = await this.llmProxy.chat([
{ role: 'system', content: context.systemPrompt },
{ role: 'user', content: message },
], { maxTokens: 1000 }); // 限制输出长度

return {
content: response.content + '\n\n*(系统当前处于降级模式,部分功能可能不可用)*',
degraded: true,
toolsUsed: [],
};
}

private async emergencyResponse(message: string, context: AgentContext) {
// 尝试语义缓存
const cached = await this.cache.get(message, context);
if (cached) {
return {
content: cached.response,
cached: true,
toolsUsed: [],
};
}

// 缓存也没有,返回预设回复
return {
content: '抱歉,系统当前负载过高,暂时无法处理您的请求。请稍后再试。',
emergency: true,
toolsUsed: [],
};
}
}

6.3 异步任务队列

对于耗时特别长的 Agent 任务(如代码生成、深度分析),同步等待不现实。使用消息队列将任务异步化:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// src/queue/task-queue.ts
import { Queue, Worker } from 'bullmq';

// 定义任务队列
export const agentTaskQueue = new Queue('agent-tasks', {
connection: { host: 'redis', port: 6379 },
defaultJobOptions: {
attempts: 3, // 最多重试 3 次
backoff: {
type: 'exponential',
delay: 5000, // 指数退避,起始 5 秒
},
removeOnComplete: 100, // 保留最近 100 条完成记录
removeOnFail: 50, // 保留最近 50 条失败记录
timeout: 120000, // 2 分钟超时
},
});

// API 端点:提交异步任务
app.post('/api/agent/async', authMiddleware, async (req, res) => {
const { message } = req.body;

// 优先级队列:付费用户优先
const priority = req.user.plan === 'pro' ? 1 : 10;

const job = await agentTaskQueue.add(
'process',
{
userId: req.user.id,
sessionId: req.session.id,
message,
},
{ priority },
);

res.status(202).json({
taskId: job.id,
status: 'queued',
estimatedWaitTime: await estimateWaitTime(job.id),
// 客户端通过 WebSocket 或轮询获取结果
resultUrl: `/api/agent/result/${job.id}`,
});
});

// Worker:处理任务
const worker = new Worker('agent-tasks', async (job) => {
const { userId, sessionId, message } = job.data;

try {
// 进度上报
await job.updateProgress(10);

const context = await buildAgentContext(userId, sessionId);
await job.updateProgress(30);

const response = await agent.generate(message, { context });
await job.updateProgress(80);

// 存储结果
await storeResult(job.id, response);
await job.updateProgress(100);

// 通过 WebSocket 通知前端
await notifyClient(userId, { taskId: job.id, status: 'completed' });

return response;
} catch (error) {
logger.error('Agent task failed', { jobId: job.id, error });
throw error; // BullMQ 会根据配置自动重试
}
}, {
connection: { host: 'redis', port: 6379 },
concurrency: 5, // 每个 Worker 并发处理 5 个任务
});

七、CI/CD:AI 应用的持续交付

7.1 测试策略

AI 应用的测试不能照搬传统方式。你需要三层测试体系:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// tests/agent.test.ts

// 第一层:单元测试 - 工具函数和纯逻辑
describe('Tool: search_documents', () => {
it('should return results for valid query', async () => {
const result = await searchDocumentsTool.execute({ query: 'React hooks' });
expect(result.results).toBeDefined();
expect(result.results.length).toBeGreaterThan(0);
});

it('should handle empty query gracefully', async () => {
const result = await searchDocumentsTool.execute({ query: '' });
expect(result.results).toEqual([]);
});
});

// 第二层:集成测试 - Agent 端到端流程(使用 Mock LLM)
describe('Agent integration flow', () => {
it('should call correct tools for "search and summarize" intent', async () => {
const mockLLM = createMockLLM({
// 模拟 LLM 的工具调用决策
toolCalls: [
{ name: 'search_documents', args: { query: 'React performance' } },
],
finalResponse: 'React performance can be improved by...',
});

const agent = new Agent({ llm: mockLLM, tools: [searchDocumentsTool] });
const result = await agent.generate('搜索 React 性能优化相关文档并总结');

expect(mockLLM.callCount).toBe(2); // 一次决策 + 一次总结
expect(result.toolsUsed[0].name).toBe('search_documents');
});
});

// 第三层:评估测试 - 输出质量评估(非确定性,使用统计断言)
describe('Agent quality assessment', () => {
const testCases = [
{
input: '帮我写一个 React useEffect 的使用示例',
assertions: {
containsCode: true,
mentionsCleanup: true, // 应该提到清理函数
maxLatency: 10000,
maxTokens: 1000,
},
},
{
input: '什么是闭包?',
assertions: {
containsExplanation: true,
hasExample: true,
maxLatency: 5000,
maxTokens: 800,
},
},
];

// 使用统计断言:N 次运行中至少 M 次通过
for (const testCase of testCases) {
it(`quality: "${testCase.input.substring(0, 30)}..."`, async () => {
const results = await Promise.all(
Array.from({ length: 5 }, () => agent.generate(testCase.input)),
);

const passCount = results.filter(r =>
evaluateAssertions(r, testCase.assertions)
).length;

// 5 次中至少 4 次通过(80% 通过率)
expect(passCount).toBeGreaterThanOrEqual(4);
});
}
});

7.2 评估流水线

在 CI/CD 中加入 AI 评估步骤,确保模型或 Prompt 变更不会降低质量:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# .github/workflows/agent-ci.yml
name: AI Agent CI/CD

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
# 阶段1:传统测试
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
- run: npm run lint
- run: npm run typecheck

# 阶段2:AI 评估测试(只在新模型版本/Prompt 变更时运行)
evaluate:
needs: test
runs-on: ubuntu-latest
if: |
contains(github.event.head_commit.message, '[evaluate]') ||
github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm ci

- name: Run evaluation suite
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
EVAL_DATASET_PATH: ./eval/dataset.jsonl
run: npx tsx scripts/run-evaluation.ts

- name: Compare with baseline
run: npx tsx scripts/compare-eval-results.ts \
--current ./eval/results/latest.json \
--baseline ./eval/results/baseline.json \
--threshold 0.05 # 质量下降不超过 5%

- name: Upload evaluation report
if: always()
uses: actions/upload-artifact@v4
with:
name: evaluation-report
path: ./eval/results/

# 阶段3:构建并推送镜像
build:
needs: evaluate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build -t registry.example.com/agent-service:${{ github.sha }} .
docker tag registry.example.com/agent-service:${{ github.sha }} \
registry.example.com/agent-service:latest
- name: Push to registry
run: docker push registry.example.com/agent-service:${{ github.sha }}

# 阶段4:灰度部署
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to staging (canary)
run: |
# 灰度部署:先部署 10% 流量到新版本
kubectl set image deployment/agent-service \
agent-service=registry.example.com/agent-service:${{ github.sha }} \
-n staging

# 等待 5 分钟,观察指标
sleep 300

# 检查错误率和延迟
ERROR_RATE=$(curl -s prometheus/api/v1/query \
'rate(http_requests_total{status=~"5.."}[5m])' | jq '.data.result[0].value[1]')

if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "Error rate too high, rolling back"
kubectl rollout undo deployment/agent-service -n staging
exit 1
fi

# 指标正常,全量部署到生产
kubectl set image deployment/agent-service \
agent-service=registry.example.com/agent-service:${{ github.sha }} \
-n production

7.3 评估脚本

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// scripts/run-evaluation.ts
import { readFileSync, writeFileSync } from 'fs';

interface EvalCase {
id: string;
input: string;
expected: {
containsKeywords?: string[];
hasCode?: boolean;
minHelpfulScore?: number; // 0-1,由评估模型打分
maxToxicityScore?: number;
};
}

interface EvalResult {
caseId: string;
input: string;
output: string;
passed: boolean;
scores: {
helpfulness: number;
correctness: number;
safety: number;
};
metrics: {
latencyMs: number;
tokenUsage: { prompt: number; completion: number };
cost: number;
};
}

async function runEvaluation(): Promise<void> {
const dataset: EvalCase[] = readFileSync(process.env.EVAL_DATASET_PATH!, 'utf8')
.split('\n')
.filter(Boolean)
.map(JSON.parse);

const results: EvalResult[] = [];

for (const testCase of dataset) {
const start = Date.now();
const response = await agent.generate(testCase.input);
const latency = Date.now() - start;

// 使用评估模型打分
const scores = await evaluateResponse(
testCase.input,
response.content,
testCase.expected,
);

const passed = checkAssertions(scores, testCase.expected);

results.push({
caseId: testCase.id,
input: testCase.input,
output: response.content,
passed,
scores,
metrics: {
latencyMs: latency,
tokenUsage: response.usage,
cost: calculateCost(response),
},
});

// 进度输出
process.stdout.write(` [${results.length}/${dataset.length}] ${testCase.id}: ${passed ? '✓' : '✗'}\n`);
}

// 汇总报告
const summary = {
total: results.length,
passed: results.filter(r => r.passed).length,
passRate: results.filter(r => r.passed).length / results.length,
avgLatencyMs: avg(results.map(r => r.metrics.latencyMs)),
avgCost: avg(results.map(r => r.metrics.cost)),
avgHelpfulness: avg(results.map(r => r.scores.helpfulness)),
timestamp: new Date().toISOString(),
gitCommit: process.env.GITHUB_SHA,
};

writeFileSync('./eval/results/latest.json', JSON.stringify({ summary, results }, null, 2));

console.log('\n=== Evaluation Summary ===');
console.log(`Pass rate: ${(summary.passRate * 100).toFixed(1)}%`);
console.log(`Avg latency: ${summary.avgLatencyMs.toFixed(0)}ms`);
console.log(`Avg cost: $${summary.avgCost.toFixed(4)}`);
console.log(`Avg helpfulness: ${summary.avgHelpfulness.toFixed(2)}/1.0`);

// 如果通过率低于阈值,CI 失败
if (summary.passRate < 0.85) {
console.error('Evaluation pass rate below threshold (85%)');
process.exit(1);
}
}

runEvaluation().catch(console.error);

八、运维实战清单

8.1 上线前检查清单

将以下清单作为每次发布前的硬性检查项:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
## AI Agent 应用上线检查清单

### 基础设施
- [ ] Docker 镜像已通过安全扫描(Trivy / Snyk)
- [ ] Kubernetes 副本数 ≥ 3(跨可用区分布)
- [ ] 资源 Request/Limit 已合理配置
- [ ] HPA 配置已验证(压测确认伸缩正常)
- [ ] 数据库连接池大小已调优
- [ ] Redis 内存限制和淘汰策略已配置

### 安全
- [ ] API Key 认证已启用
- [ ] 多维度限流已配置(QPS + 用户 + Token)
- [ ] Prompt 注入检测已启用
- [ ] 工具权限按最小化原则配置
- [ ] 敏感数据已加密(API Key、用户数据)
- [ ] 审计日志已开启(所有写操作记录)
- [ ] CORS 配置正确(非 * 通配)
- [ ] HTTPS 已强制启用

### 可观测性
- [ ] 结构化日志已配置(JSON 格式)
- [ ] 链路追踪已接入(OpenTelemetry)
- [ ] 关键指标已上报(QPS、延迟、错误率、Token 用量)
- [ ] Grafana 面板已创建
- [ ] 告警规则已配置(延迟、错误率、Token 预算)
- [ ] 错误聚合已接入(Sentry)

### LLM 相关
- [ ] 多 Provider 故障转移已配置
- [ ] 熔断器已启用
- [ ] 语义缓存已启用并验证命中率
- [ ] 模型路由策略已配置
- [ ] Token 预算告警已设置
- [ ] 降级策略已验证

### CI/CD
- [ ] 单元测试通过率 100%
- [ ] 集成测试通过
- [ ] 评估测试通过率 ≥ 85%
- [ ] 灰度发布配置已准备
- [ ] 回滚脚本已验证

### 应急
- [ ] 降级开关已准备(feature flag)
- [ ] 回滚流程已文档化
- [ ] 值班联系人和升级路径已明确
- [ ] 事故响应模板已准备

8.2 事故响应流程

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// src/ops/incident-response.ts

// 自动检测并响应常见事故
export class IncidentResponder {
private rules: IncidentRule[] = [
{
name: 'LLM API 全线超时',
detect: (m) => m.llmErrorRate > 0.5 && m.llmLatencyP99 > 60000,
actions: [
{ type: 'switch_provider', target: 'fallback' },
{ type: 'enable_degradation', level: 'degraded' },
{ type: 'notify', channel: 'pagerduty', severity: 'P1' },
],
},
{
name: 'Token 消耗异常飙升',
detect: (m) => m.tokenUsageRate > m.expectedRate * 3,
actions: [
{ type: 'enable_strict_rate_limit' },
{ type: 'notify', channel: 'slack', severity: 'P2' },
{ type: 'auto_block_abusive_users' },
],
},
{
name: 'Prompt 注入攻击',
detect: (m) => m.securityEvents > 10,
actions: [
{ type: 'block_attacking_ips' },
{ type: 'notify', channel: 'pagerduty', severity: 'P1' },
{ type: 'enable_strict_input_filter' },
],
},
];

async evaluate(metrics: SystemMetrics) {
for (const rule of this.rules) {
if (rule.detect(metrics)) {
await this.executeActions(rule);
await this.createIncident(rule, metrics);
}
}
}

private async executeActions(rule: IncidentRule) {
for (const action of rule.actions) {
try {
switch (action.type) {
case 'switch_provider':
await llmProxy.switchTo(action.target);
break;
case 'enable_degradation':
await degradationManager.setLevel(action.level);
break;
case 'enable_strict_rate_limit':
await rateLimitManager.switchToStrictMode();
break;
case 'block_attacking_ips':
await firewall.blockIPs(await getAttackSourceIPs());
break;
case 'notify':
await notifier.send(action.channel, {
title: `Incident: ${rule.name}`,
severity: action.severity,
timestamp: new Date().toISOString(),
});
break;
}
} catch (error) {
logger.error(`Failed to execute action: ${action.type}`, error);
}
}
}
}

九、成本与性能的平衡术

在结束之前,让我们用一组真实的数据来总结生产化优化的综合效果。

假设一个 AI Agent 应用日均 10,000 次请求,以下是优化前后的对比:

1
2
3
4
5
6
7
8
9
                    优化前(裸奔)      优化后(生产级)
──────────── ──────────────
平均延迟 (P50) 8.2s 2.1s (-74%)
延迟 (P99) 45s 6.5s (-86%)
日均 Token 消耗 45M 18M (-60%)
日均成本 $180 $45 (-75%)
错误率 2.1% 0.3% (-86%)
缓存命中率 0% 32% (+32%)
安全事件 3-5 次/周 0 次 (全部拦截)

优化手段组合:

  • 语义缓存 → 减少 32% 的 LLM 调用
  • 模型路由 → 60% 请求使用 mini 模型
  • Prompt 精简 → 平均输入 Token 减少 25%
  • 流式输出 → 用户感知延迟降低 50%
  • 连接池复用 → 网络开销减少 30%

关键洞察: 最大的性能提升不是来自更强的硬件,而是来自减少不必要的 LLM 调用。缓存命中一次,就是省掉了 2-30 秒的延迟和对应的 Token 费用。


结语:五篇之后,回到起点

在这个系列的五篇文章中,我们走过了一条完整的路径:

第一篇谈的是认知——AI Agent 不是聊天机器人,是能自主规划、调用工具、迭代执行的智能体。前端开发者身处这场变革的最佳位置:Agent 的交互界面在浏览器中,而那是我们的主场。

第二篇谈的是工具——工具是 Agent 的手和脚。好的工具设计让 Agent 从”会说话”变成”能干活”。从 Function Calling 到 MCP 协议,工具生态正在标准化。

第三篇谈的是协作——单个 Agent 的能力是有限的。多 Agent 编排让 PM、前端、后端、QA 各司其职,用分工解决上下文污染和能力冲突。

第四篇谈的是记忆——没有记忆的 Agent 是一次性的。从短期上下文窗口到长期向量存储,记忆系统让 Agent 从”工具”进化为”伙伴”。

而这第五篇,谈的是生产化——以上所有能力,只有在真实流量、真实用户、真实预算下稳定运行,才算真正完成了交付。

这五步,构成了一个前端工程师向 AI Agent 工程师跃迁的完整地图:

1
2
3
4
5
6
7
认知 → 工具 → 协作 → 记忆 → 生产化
│ │ │ │ │
│ │ │ │ └── 部署、监控、成本、弹性
│ │ │ └── 跨会话状态持久化
│ │ └── 多 Agent 编排架构
│ └── Function Calling / MCP / 工具设计模式
└── Agent 范式、Vercel AI SDK、第一个 ToolLoopAgent

但请记住,技术栈会更新,框架会迭代,模型会更强大。今天写的代码,可能明年就需要重构。真正不变的是三个底层能力:

  1. 系统性思维——把 AI Agent 当作一个完整系统来设计,而不是一个聊天功能来开发
  2. 工程化能力——从原型到生产,每一步都需要扎实的工程实践
  3. 持续学习的习惯——这个领域每三个月就有一次范式迭代

AI 不会取代前端开发者。但会用 AI 构建完整应用的前端开发者,会取代不会的人。

系列到此结束,但实践才刚刚开始。去构建你的 Agent,去部署它,去在真实流量中打磨它。代码是写出来的,Agent 是运营出来的。


本文是《前端 + AI Agent 开发实战》系列第五篇(完结篇)。

系列文章: