六维教程

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,生产环境用外部服务
上一篇
EdgeOne Makers Agents 沙箱工具
下一篇
EdgeOne Makers Agents 鉴权