引言
Agent系统的日志不仅是排障工具,更是质量改进和安全审计的数据基础。一次Agent对话可能涉及路由决策、记忆检索、工具调用、LLM推理等多个步骤,跨越多个微服务。如何在分布式环境中建立完整的日志链路,是Agent系统可观测性的核心挑战。
日志架构全景
┌──────────────────────────────────────────────────────┐
│ 日志数据流 │
│ │
│ Agent Services │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Router│ │Tool │ │LLM │ │Memory│ │
│ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────┐ │
│ │ Fluent Bit (采集) │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌───────┼───────┐ │
│ ▼ ▼ │
│ ┌──────┐ ┌──────────┐ │
│ │ Loki │ │Elasticsearch│ │
│ │(日志) │ │ (全文搜索) │ │
│ └──────┘ └──────────┘ │
│ │ │ │
│ └───────┬───────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Grafana │ │
│ │ (可视化) │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────┘
结构化日志标准
import structlog
from datetime import datetime
import uuid
# 结构化日志配置
structlog.configure(
processors=[
structlog.stdlib.add_log_level,
structlog.processors.TimeStamper(fmt="iso"),
structlog.processors.JSONRenderer()
],
wrapper_class=structlog.stdlib.BoundLogger,
logger_factory=structlog.stdlib.LoggerFactory(),
)
logger = structlog.get_logger()
class AgentLogger:
"""Agent专用日志器"""
@staticmethod
def log_request(
session_id: str,
request_id: str,
user_input: str,
route_decision: dict
):
"""记录请求日志"""
logger.info(
"agent_request_received",
session_id=session_id,
request_id=request_id,
user_input_length=len(user_input),
user_input_preview=user_input[:100],
route_model=route_decision.get("model"),
route_tools=route_decision.get("tools"),
timestamp=datetime.now().isoformat()
)
@staticmethod
def log_tool_call(
session_id: str,
request_id: str,
tool_name: str,
params: dict,
result: dict,
latency_ms: float,
success: bool
):
"""记录工具调用日志"""
logger.info(
"tool_call_completed",
session_id=session_id,
request_id=request_id,
tool_name=tool_name,
params_hash=hash(str(sorted(params.items()))),
result_size=len(str(result)),
latency_ms=latency_ms,
success=success,
error=result.get("error") if not success else None
)
@staticmethod
def log_llm_call(
session_id: str,
request_id: str,
model: str,
prompt_tokens: int,
completion_tokens: int,
latency_ms: float,
quality_score: float = None
):
"""记录LLM调用日志"""
logger.info(
"llm_call_completed",
session_id=session_id,
request_id=request_id,
model=model,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=prompt_tokens + completion_tokens,
latency_ms=latency_ms,
quality_score=quality_score
)
@staticmethod
def log_agent_decision(
session_id: str,
decision_type: str,
reasoning: str,
action: str,
confidence: float
):
"""记录Agent决策日志——用于审计和改进"""
logger.info(
"agent_decision",
session_id=session_id,
decision_type=decision_type, # route, tool_select, terminate
reasoning=reasoning[:500], # 截断推理过程
action=action,
confidence=confidence
)
Trace ID传播
from opentelemetry import trace
from opentelemetry.propagate import inject, extract
class TracingMiddleware:
"""分布式追踪中间件"""
async def __call__(self, request, call_next):
# 提取或生成trace context
context = extract(request.headers)
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
"agent_request",
context=context,
attributes={
"session_id": request.session_id,
"user_id": request.user_id,
}
) as span:
# 在请求上下文中注入trace信息
headers = {}
inject(headers)
request.trace_context = headers
try:
response = await call_next(request)
span.set_attribute("response.status", "success")
span.set_attribute(
"response.latency_ms",
response.latency_ms
)
return response
except Exception as e:
span.record_exception(e)
span.set_status(trace.Status(trace.StatusCode.ERROR))
raise
# 在服务间调用时传播Trace ID
class ServiceClient:
"""带Trace传播的服务客户端"""
async def call_service(
self,
service: str,
method: str,
data: dict,
trace_context: dict = None
) -> dict:
"""调用其他微服务"""
headers = {
"Content-Type": "application/json",
}
# 注入trace context
if trace_context:
headers.update(trace_context)
else:
inject(headers) # 从当前context注入
# 记录出站调用
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span(
f"call_{service}",
attributes={
"peer.service": service,
"http.method": method,
}
) as span:
response = await self.http_client.post(
f"http://{service}/{method}",
json=data,
headers=headers
)
span.set_attribute("http.status_code", response.status_code)
return response.json()
日志关联查询
class LogCorrelator:
"""日志关联查询器"""
async def get_session_timeline(
self,
session_id: str
) -> list:
"""获取会话的完整事件时间线"""
# 从多个数据源查询
logs = await self.loki.query(
f'{{session_id="{session_id}"}} | json'
)
traces = await self.jaeger.get_traces(
tags={"session_id": session_id}
)
metrics = await self.prometheus.query_range(
query=f'agent_session_metrics{{session_id="{session_id}"}}',
start=..., end=...
)
# 合并并按时间排序
events = []
for log in logs:
events.append({
"type": "log",
"timestamp": log["timestamp"],
"level": log["level"],
"message": log["message"],
**log
})
for trace in traces:
for span in trace.spans:
events.append({
"type": "trace",
"timestamp": span.start_time,
"span_name": span.name,
"duration_ms": span.duration_ms,
"service": span.service,
**span.tags
})
events.sort(key=lambda e: e["timestamp"])
return events
日志采样策略
class LogSampler:
"""日志采样器——在保证可观测性的前提下控制日志量"""
SAMPLING_RULES = {
# 正常请求:10%采样
"normal": {"rate": 0.1, "level": "INFO"},
# 错误请求:100%记录
"error": {"rate": 1.0, "level": "ERROR"},
# 慢请求(>5s):100%记录
"slow": {"rate": 1.0, "level": "INFO", "min_latency_ms": 5000},
# 工具调用失败:100%记录
"tool_failure": {"rate": 1.0, "level": "WARN"},
# 安全相关:100%记录
"security": {"rate": 1.0, "level": "INFO"},
# 高价值用户:50%采样
"enterprise": {"rate": 0.5, "level": "INFO"},
}
def should_log(
self,
log_type: str,
request: dict,
response: dict = None
) -> tuple:
"""判断是否需要记录日志"""
# 优先级判断
if response and response.get("error"):
rule = self.SAMPLING_RULES["error"]
elif response and response.get("latency_ms", 0) > 5000:
rule = self.SAMPLING_RULES["slow"]
elif request.get("user_tier") == "enterprise":
rule = self.SAMPLING_RULES["enterprise"]
else:
rule = self.SAMPLING_RULES["normal"]
import random
if random.random() < rule["rate"]:
return True, rule["level"]
return False, None
日志分析
class LogAnalyzer:
"""日志分析器"""
async def analyze_session(self, session_id: str) -> dict:
"""分析单个会话日志"""
events = await self.log_store.get_session_events(session_id)
analysis = {
"session_id": session_id,
"total_steps": len(events),
"tool_calls": [],
"llm_calls": [],
"errors": [],
"total_tokens": 0,
"total_latency_ms": 0,
"quality_indicators": {},
}
for event in events:
if event["type"] == "tool_call":
analysis["tool_calls"].append({
"tool": event["tool_name"],
"latency_ms": event["latency_ms"],
"success": event["success"]
})
if not event["success"]:
analysis["errors"].append(event)
elif event["type"] == "llm_call":
analysis["llm_calls"].append({
"model": event["model"],
"tokens": event["total_tokens"],
"latency_ms": event["latency_ms"]
})
analysis["total_tokens"] += event["total_tokens"]
analysis["total_latency_ms"] += event.get("latency_ms", 0)
return analysis
async def detect_anomalies(
self,
time_window_hours: int = 1
) -> list:
"""检测日志异常模式"""
anomalies = []
# 1. 突发错误聚集
error_clusters = await self._find_error_clusters(time_window_hours)
for cluster in error_clusters:
anomalies.append({
"type": "error_cluster",
"service": cluster["service"],
"error_count": cluster["count"],
"time_range": cluster["range"]
})
# 2. 异常Token消耗
token_outliers = await self._find_token_outliers(time_window_hours)
for outlier in token_outliers:
anomalies.append({
"type": "token_anomaly",
"session_id": outlier["session_id"],
"tokens": outlier["tokens"],
"expected": outlier["expected"]
})
# 3. 工具调用模式异常
tool_anomalies = await self._find_tool_anomalies(time_window_hours)
anomalies.extend(tool_anomalies)
return anomalies
日志保留策略
| 日志类型 | 热存储(SSD) | 温存储(HDD) | 冷存储(S3) |
|---|---|---|---|
| 错误日志 | 7天 | 30天 | 180天 |
| 安全审计 | 30天 | 180天 | 2年 |
| 正常请求 | 3天 | 14天 | 90天 |
| Trace数据 | 3天 | 7天 | 30天 |
| 指标数据 | 7天 | 90天 | 365天 |
总结
Agent系统的日志架构需要在"详细度"和"成本"之间取得平衡。结构化日志是一切的基础——没有结构化,就无法进行有效的查询和分析。Trace ID的跨服务传播让分布式追踪成为可能。智能采样策略确保在控制成本的同时不丢失关键信息。
核心原则:日志的价值不在于"记录了什么",而在于"能找到什么"。好的日志架构让排障时间从小时级降到分钟级,让系统改进从"凭感觉"变成"看数据"。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。
