EdgeOne Makers Agents 可观测性
Agent 上线后,你得知道它跑得怎么样、哪里出了问题、花了多少钱。这篇讲怎么监控和调试 Agent。
为什么需要可观测性
Agent 不像普通接口,调用链路长、成本高、问题难定位。
| 问题 | 没有监控的后果 |
|---|---|
| 响应慢 | 用户流失,不知道卡在哪 |
| 报错 | 不知道哪一步挂了 |
| 成本失控 | Token 用超了才发现 |
| 效果差 | 不知道模型回复质量如何 |
日志记录
最基础的监控,记录每次请求的关键信息。
async function logRequest(request, response, duration, env) {
const logEntry = {
timestamp: Date.now(),
path: new URL(request.url).pathname,
method: request.method,
statusCode: response.status,
duration,
userAgent: request.headers.get('user-agent'),
// 不要记录完整的请求体,可能包含敏感信息
contentLength: request.headers.get('content-length')
};
// 写入 KV,设置 7 天过期
const logKey = `log:${Date.now()}:${crypto.randomUUID()}`;
await env.KV.put(logKey, JSON.stringify(logEntry), {
expirationTtl: 7 * 24 * 60 * 60
});
}
export default {
async fetch(request, env, ctx) {
const start = Date.now();
// 处理请求
const response = await handleRequest(request, env);
// 记录日志
const duration = Date.now() - start;
ctx.waitUntil(logRequest(request, response, duration, env));
return response;
}
};
指标统计
统计关键指标,方便看趋势。
async function recordMetrics(type, value, env) {
const now = Date.now();
const hourKey = `metrics:${type}:${Math.floor(now / 3600000)}`;
// 简单计数器
if (type === 'request_count') {
const count = parseInt(await env.KV.get(hourKey) || '0');
await env.KV.put(hourKey, String(count + 1));
}
// 耗时统计(存平均值)
if (type === 'response_time') {
const data = await env.KV.get(hourKey, 'json') || { sum: 0, count: 0 };
data.sum += value;
data.count += 1;
await env.KV.put(hourKey, JSON.stringify(data));
}
// Token 消耗
if (type === 'token_usage') {
const tokens = parseInt(await env.KV.get(hourKey) || '0');
await env.KV.put(hourKey, String(tokens + value));
}
}
// 在请求处理中使用
export default {
async fetch(request, env, ctx) {
const start = Date.now();
// 记录请求数
ctx.waitUntil(recordMetrics('request_count', 1, env));
const response = await handleRequest(request, env);
// 记录耗时
const duration = Date.now() - start;
ctx.waitUntil(recordMetrics('response_time', duration, env));
return response;
}
};
链路追踪
Agent 调用链路长,需要追踪每一步的耗时。
class TraceContext {
constructor() {
this.traceId = crypto.randomUUID();
this.spans = [];
this.startTime = Date.now();
}
startSpan(name) {
const span = {
name,
startTime: Date.now(),
endTime: null,
duration: null
};
this.spans.push(span);
return span;
}
endSpan(span) {
span.endTime = Date.now();
span.duration = span.endTime - span.startTime;
}
toJSON() {
return {
traceId: this.traceId,
totalDuration: Date.now() - this.startTime,
spans: this.spans
};
}
}
export default {
async fetch(request, env, ctx) {
const trace = new TraceContext();
// 追踪:读取对话历史
let span = trace.startSpan('load_history');
const history = await loadHistory(request, env);
trace.endSpan(span);
// 追踪:调用大模型
span = trace.startSpan('call_model');
const modelResponse = await callModel(history, env);
trace.endSpan(span);
// 追踪:保存对话
span = trace.startSpan('save_history');
await saveHistory(request, modelResponse, env);
trace.endSpan(span);
// 把追踪信息写入响应头
const response = Response.json({ reply: modelResponse });
response.headers.set('X-Trace-Id', trace.traceId);
// 异步保存追踪数据
ctx.waitUntil(saveTrace(trace, env));
return response;
}
};
async function saveTrace(trace, env) {
const key = `trace:${trace.traceId}`;
await env.KV.put(key, JSON.stringify(trace), {
expirationTtl: 24 * 60 * 60 // 1天
});
}
错误追踪
记录错误详情,方便排查。
async function logError(error, request, env) {
const errorLog = {
timestamp: Date.now(),
error: error.message,
stack: error.stack,
path: new URL(request.url).pathname,
method: request.method,
userAgent: request.headers.get('user-agent')
};
const key = `error:${Date.now()}:${crypto.randomUUID()}`;
await env.KV.put(key, JSON.stringify(errorLog), {
expirationTtl: 30 * 24 * 60 * 60 // 30天
});
}
// 全局错误处理
export default {
async fetch(request, env, ctx) {
try {
return await handleRequest(request, env);
} catch (error) {
// 记录错误
ctx.waitUntil(logError(error, request, env));
// 返回友好错误
return Response.json(
{ error: '服务暂时不可用,请稍后重试' },
{ status: 500 }
);
}
}
};
成本监控
大模型调用有成本,需要跟踪 Token 消耗。
async function trackTokenUsage(model, tokens, env) {
const now = Date.now();
const dayKey = `cost:${model}:${Math.floor(now / 86400000)}`;
const usage = await env.KV.get(dayKey, 'json') || { tokens: 0, cost: 0 };
usage.tokens += tokens;
// 简单成本计算(实际价格看各模型定价)
const pricePerToken = {
'gpt-4o': 0.00003,
'gpt-4o-mini': 0.0000015,
'claude-3-5-sonnet': 0.00003,
'deepseek-v3': 0.000001
};
usage.cost += tokens * (pricePerToken[model] || 0);
await env.KV.put(dayKey, JSON.stringify(usage));
}
// 在调用模型后记录
async function callModelWithTracking(messages, model, env) {
const response = await env.AI.chat(model, { messages });
// 记录 Token 使用
if (response.usage) {
const totalTokens = response.usage.prompt_tokens + response.usage.completion_tokens;
await trackTokenUsage(model, totalTokens, env);
}
return response;
}
查询监控数据
提供接口查询监控数据。
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/metrics/summary') {
const now = Date.now();
const hours = 24;
const summary = {
requests: 0,
avgResponseTime: 0,
totalTokens: 0,
totalCost: 0,
errors: 0
};
// 统计最近 24 小时
for (let i = 0; i < hours; i++) {
const hourKey = Math.floor((now - i * 3600000) / 3600000);
const reqCount = parseInt(
await env.KV.get(`metrics:request_count:${hourKey}`) || '0'
);
summary.requests += reqCount;
const timeData = await env.KV.get(
`metrics:response_time:${hourKey}`, 'json'
);
if (timeData) {
summary.avgResponseTime += timeData.sum;
}
}
if (summary.requests > 0) {
summary.avgResponseTime /= summary.requests;
}
return Response.json(summary);
}
if (url.pathname === '/metrics/errors') {
// 获取最近的错误
const errors = [];
const list = await env.KV.list({ prefix: 'error:' });
for (const key of list.keys.slice(-10)) {
const error = await env.KV.get(key.name, 'json');
errors.push(error);
}
return Response.json({ errors });
}
}
};
可观测性方案对比
| 方案 | 优点 | 缺点 | 适合 |
|---|---|---|---|
| KV 日志 | 简单,免费 | 查询慢,不适合大量数据 | 小项目 |
| 外部服务 | 功能强,查询快 | 有成本 | 生产环境 |
| 自建数据库 | 灵活,可控 | 需要维护 | 大项目 |
速查卡片
| 要点 | 说明 |
|---|---|
| 日志记录 | 记录请求信息、耗时、状态码 |
| 指标统计 | 请求数、响应时间、Token 消耗 |
| 链路追踪 | 追踪每步耗时,定位瓶颈 |
| 错误追踪 | 记录错误详情,方便排查 |
| 成本监控 | 跟踪 Token 使用和成本 |
| 查询接口 | 提供 API 查询监控数据 |
| 推荐方案 | 小项目用 KV,生产环境用外部服务 |