Appearance
课 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)降级(如果重试都失败,返回"当前服务繁忙,请稍后重试",不让错误透传给用户)。同时记录失败日志,监控失败率,超过阈值发告警。