Skip to content

课 2 · Trace、日志与成本统计

本课目标

给进阶项目加入请求追踪、结构化日志和 LLM 调用成本统计,能在出问题时快速定位。

关键理解:可观测性不是"加了就放心了",而是"出问题时能在 10 分钟内定位到根因"。

三个层次

层次工具解决什么
Trace(追踪)请求 ID 贯穿全链路"这个请求经过了哪些步骤,哪步慢"
Log(日志)结构化 JSON 日志"某个时间点发生了什么"
Metric(指标)LLM 调用成本统计"花了多少钱,哪类请求最贵"

请求追踪(Trace ID)

最简单的追踪:给每个请求生成一个 Trace ID,所有相关日志都带上它:

typescript
// apps/api/src/middleware/trace.ts
import { randomUUID } from 'node:crypto'
import type { MiddlewareHandler } from 'hono'

declare module 'hono' {
  interface ContextVariableMap {
    traceId: string
  }
}

export const traceMiddleware: MiddlewareHandler = async (c, next) => {
  const traceId = c.req.header('x-trace-id') ?? randomUUID()
  c.set('traceId', traceId)
  c.header('x-trace-id', traceId)

  const start = Date.now()
  await next()
  const duration = Date.now() - start

  // 记录每个请求的汇总日志
  logger.info({
    type: 'request',
    traceId,
    method: c.req.method,
    path: c.req.path,
    status: c.res.status,
    durationMs: duration,
  })
}

结构化日志

JSON 格式日志方便后续用工具搜索和分析:

typescript
// packages/shared/src/lib/logger.ts
export const logger = {
  info: (data: Record<string, unknown>) =>
    console.log(JSON.stringify({ level: 'info', timestamp: new Date().toISOString(), ...data })),
  warn: (data: Record<string, unknown>) =>
    console.warn(JSON.stringify({ level: 'warn', timestamp: new Date().toISOString(), ...data })),
  error: (data: Record<string, unknown>) =>
    console.error(JSON.stringify({ level: 'error', timestamp: new Date().toISOString(), ...data })),
}

在 Agent 每个阶段记录日志:

typescript
// apps/api/src/services/agent.ts
async function ask({ message, sessionId, traceId }: AskParams) {
  logger.info({ type: 'agent_start', traceId, message })

  const start = Date.now()
  const chunks = await retrieve(message)
  logger.info({
    type: 'retrieval_done',
    traceId,
    chunkCount: chunks.length,
    durationMs: Date.now() - start,
  })

  const llmStart = Date.now()
  const { text, usage } = await generateText({ /* ... */ })
  logger.info({
    type: 'llm_done',
    traceId,
    promptTokens: usage.promptTokens,
    completionTokens: usage.completionTokens,
    durationMs: Date.now() - llmStart,
    model: process.env.MODEL_ID,
  })

  return { answer: text }
}

LLM 成本统计

建一张简单的成本记录表:

sql
CREATE TABLE llm_usage (
  id TEXT PRIMARY KEY,
  trace_id TEXT NOT NULL,
  model TEXT NOT NULL,
  prompt_tokens INTEGER NOT NULL,
  completion_tokens INTEGER NOT NULL,
  estimated_cost_usd REAL,  -- 估算费用(美元)
  created_at TEXT NOT NULL
);
typescript
// packages/shared/src/lib/cost-tracker.ts

// 各模型每 1000 token 的价格(美元,仅作参考)
const MODEL_PRICES: Record<string, { input: number; output: number }> = {
  'gpt-4o': { input: 0.0025, output: 0.010 },
  'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
  'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
}

export async function trackUsage(params: {
  traceId: string
  model: string
  promptTokens: number
  completionTokens: number
}) {
  const price = MODEL_PRICES[params.model]
  const estimatedCost = price
    ? (params.promptTokens / 1000) * price.input +
      (params.completionTokens / 1000) * price.output
    : null

  await db.run(
    `INSERT INTO llm_usage (id, trace_id, model, prompt_tokens, completion_tokens, estimated_cost_usd, created_at)
     VALUES (?, ?, ?, ?, ?, ?, ?)`,
    [
      randomUUID(),
      params.traceId,
      params.model,
      params.promptTokens,
      params.completionTokens,
      estimatedCost,
      new Date().toISOString(),
    ],
  )
}

成本统计 API

typescript
// apps/api/src/routes/admin.ts
adminRouter.get('/stats/cost', async (c) => {
  const { from, to } = c.req.query()

  const stats = await db.all(`
    SELECT
      model,
      COUNT(*) AS calls,
      SUM(prompt_tokens) AS total_prompt_tokens,
      SUM(completion_tokens) AS total_completion_tokens,
      SUM(estimated_cost_usd) AS total_cost_usd
    FROM llm_usage
    WHERE created_at BETWEEN ? AND ?
    GROUP BY model
    ORDER BY total_cost_usd DESC
  `, [from ?? '2000-01-01', to ?? '2099-12-31'])

  return c.json(stats)
})

慢请求排查

有了结构化日志,用命令行就能分析慢请求:

bash
# 找出超过 5 秒的请求
cat logs/app.log | grep '"type":"request"' | node -e "
const lines = require('fs').readFileSync('/dev/stdin','utf8').trim().split('\n')
lines.map(l => JSON.parse(l))
     .filter(l => l.durationMs > 5000)
     .forEach(l => console.log(l.traceId, l.path, l.durationMs+'ms'))
"

# 找出某个 Trace 的完整调用链
cat logs/app.log | grep '"traceId":"abc-123"'

本节产物

packages/shared/src/lib/
  logger.ts             # 结构化日志
  cost-tracker.ts       # LLM 成本记录
apps/api/src/middleware/
  trace.ts              # Trace ID 中间件
apps/api/src/routes/
  admin.ts              # 成本统计 API

面试追问

线上 Agent 出问题怎么排查?

四步走:1)用 Trace ID 找到出问题的请求的完整日志链,确认是哪个阶段出错(检索?LLM 生成?工具调用?);2)看该请求的输入(用户问题)、检索结果(哪些 chunk)、LLM 输出(原始文本),复现问题;3)对比评测集,看是个案还是系统性问题;4)如果是模型问题,检查最近是否有 prompt 变更或模型切换。

如果 LLM API 调用失败怎么处理?

两层处理:1)重试(带指数退避,最多 3 次)——临时性错误(429 限流、502 网关错误)通过重试可以恢复;2)降级(如果重试都失败,返回"当前服务繁忙,请稍后重试",不让错误透传给用户)。同时记录失败日志,监控失败率,超过阈值发告警。

面向前端工程师和独立开发者的 AI 应用工程课程