跳到主要内容

AI Agent 工程化实践:构建可观测、可管控的生产级智能体系统

· 阅读需 10 分钟

本文是 Agent 工程系列的综合篇。前文分别探讨了 Agent 推理与执行分离 和 Agent 安全防护(ToolGuardian),本篇从可观测性、安全管控与错误恢复三个维度,系统梳理生产级 Agent 系统的工程实践。

2025-2026 年,AI Agent 从「Demo 阶段」迈入「生产阶段」。Gartner 的 2026 年 Hype Cycle 报告将 Agentic AI 定位在「泡沫破裂前的过热期」——这意味着大量系统正在实际生产环境中运行,相关的工程教训也在加速积累。

本文不讨论 Agent 的理论架构,而是聚焦于三个工程问题:怎么知道 Agent 在做什么?怎么确保它不会做不该做的事?当它失败时怎么恢复?

一、可观测性:Agent 的 「黑盒」 困境​

传统后端服务的可观测性(Logging → Metrics → Tracing)在 Agent 场景下面临根本性挑战:

  1. 非确定性:同一 prompt 可能产生不同的工具调用序列,传统 trace ID → span 的静态拓扑不适用
  2. 高 token 成本:全量日志(包含完整 prompt/response)在 128K 上下文下成本惊人
  3. 循环检测:Agent 可能在工具调用循环中震荡数百轮

1.1 Agent 专用 Tracing 结构​

我们推荐在标准 OpenTelemetry 基础上增加 Agent 语义层:

from opentelemetry import trace
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional

class AgentSpanKind(Enum):
THOUGHT = "thought" # LLM 推理步骤
TOOL_CALL = "tool_call" # 工具调用
TOOL_RESULT = "tool_result" # 工具返回
OBSERVATION = "observation" # 环境观察
FINAL_ANSWER = "final_answer" # 最终输出

@dataclass
class AgentSpanAttributes:
agent_id: str
session_id: str
span_kind: AgentSpanKind
execution_time_ms: float
token_usage: Optional[dict] = None
tool_name: Optional[str] = None
error_type: Optional[str] = None
# 以下字段仅在 debug 模式下填充
prompt_preview: Optional[str] = None # 截断到 200 tokens
response_preview: Optional[str] = None

class AgentTracer:
def __init__(self, service_name: str):
self.tracer = trace.get_tracer(service_name)
self._max_log_tokens = 200 # 截断阈值

def _safe_truncate(self, text: Optional[str]) -> Optional[str]:
if text is None:
return None
# 粗略估算 token 数(4 chars ≈ 1 token)
max_chars = self._max_log_tokens * 4
return text[:max_chars] + ("..." if len(text) > max_chars else "")

def trace_tool_call(self, agent_id: str, session_id: str,
tool_name: str, input_args: dict,
result: str, duration_ms: float,
tokens: dict, debug: bool = False):
with self.tracer.start_as_current_span("agent.tool_call") as span:
span.set_attribute("agent.id", agent_id)
span.set_attribute("agent.session_id", session_id)
span.set_attribute("agent.span_kind", AgentSpanKind.TOOL_CALL.value)
span.set_attribute("agent.tool_name", tool_name)
span.set_attribute("agent.duration_ms", duration_ms)
if tokens:
span.set_attribute("agent.prompt_tokens", tokens.get("prompt", 0))
span.set_attribute("agent.completion_tokens", tokens.get("completion", 0))
if debug:
span.set_attribute("agent.input_preview",
self._safe_truncate(str(input_args)))
span.set_attribute("agent.result_preview",
self._safe_truncate(str(result)))

关键的设计决策是 分层采样(layered sampling):

  • 基础层(100%):只记录 span kind、agent_id、tool_name、duration、token count(不包含 payload)
  • 详细层(5-10%):额外记录截断的 input/output(上文的 debug=True)
  • 全量层(0.1%):记录完整的 prompt 和 response,用于离线分析

这样可以将日志存储成本降低 90% 以上,同时保留可追溯性。

1.2 循环检测与自动终止​

Agent 循环是生产环境中最常见的问题。一个工具调用链可能在 3 个工具间无限震荡。

import hashlib
from collections import deque
from typing import Any
import time

class LoopDetector:
"""基于状态哈希的循环检测器"""

def __init__(self, max_history: int = 50, threshold: int = 3):
self.history: deque = deque(maxlen=max_history)
self.threshold = threshold
self._state_count: dict = {}

def _compute_state_hash(self, tool_name: str,
args: dict, result_preview: str) -> str:
"""对 (工具名, 参数, 返回摘要) 计算 hash"""
state_key = f"{tool_name}::{hashlib.md5(
str(sorted(args.items())).encode()
).hexdigest()}::{result_preview[:100]}"
return hashlib.sha256(state_key.encode()).hexdigest()

def check(self, tool_name: str, args: dict,
result: str) -> tuple[bool, str]:
"""
返回 (is_looping, reason)
如果检测到循环模式,is_looping=True
"""
state_hash = self._compute_state_hash(tool_name, args, result)
self.history.append(state_hash)

# 简单模式:相同状态出现 threshold 次
self._state_count[state_hash] = \
self._state_count.get(state_hash, 0) + 1
if self._state_count[state_hash] >= self.threshold:
return True, f"State repeated {self.threshold} times: {tool_name}"

# 复杂模式:检测交替循环 A→B→A→B
if len(self.history) >= 4:
recent = list(self.history)[-4:]
if len(set(recent)) == 2 and recent[0] == recent[2] and recent[1] == recent[3]:
return True, "Alternating loop detected: A→B→A→B"

return False, ""

def reset(self):
self.history.clear()
self._state_count.clear()


class AgentExecutor:
"""带循环检测的生产级 Agent 执行器"""

def __init__(self, max_steps: int = 30):
self.loop_detector = LoopDetector()
self.max_steps = max_steps
self.tracer = AgentTracer("production-agent")

def execute(self, task: str, debug: bool = False) -> dict:
session_id = f"session_{int(time.time())}"
step = 0
state = {"task": task, "result": None, "history": []}

while step < self.max_steps:
step += 1

# 1. LLM 推理获取下一步动作
action = self._llm_reason(state)
if action["type"] == "final_answer":
state["result"] = action["content"]
break

# 2. 执行工具调用
tool_name = action["tool"]
tool_args = action["args"]
start = time.perf_counter()
try:
tool_result = self._call_tool(tool_name, tool_args)
duration = (time.perf_counter() - start) * 1000
except Exception as e:
# 3. 工具异常处理
tool_result = f"TOOL_ERROR: {e}"
duration = 0

# 4. 循环检测
is_looping, reason = self.loop_detector.check(
tool_name, tool_args, str(tool_result)[:200]
)
if is_looping:
return {
"status": "loop_terminated",
"reason": reason,
"steps": step,
"partial_result": state.get("history", [])[-3:]
}

# 5. 记录 trace(debug 模式下记录更多)
self.tracer.trace_tool_call(
agent_id="agent-v1",
session_id=session_id,
tool_name=tool_name,
input_args=tool_args,
result=str(tool_result),
duration_ms=duration,
tokens={"prompt": 0, "completion": 0}, # 实际应从 LLM 返回获取
debug=debug,
)

state["history"].append({
"step": step,
"tool": tool_name,
"result": str(tool_result)[:500],
})

return {"status": "completed" if step < self.max_steps else "max_steps",
"steps": step, "result": state.get("result")}

二、安全管控:ToolGuardian 模式的生产实践​

我们在 Agent 安全防护 一文中介绍了 ToolGuardian 的基本架构。本节补充生产部署的关键实践。

2.1 策略决策点分布​

Agent 安全策略不能仅在「调用前」做一次检查,而应分布在调用链的每一层:

用户输入
│
▼
┌─────────────────────────┐
│ L1: Input Guard │ ← 敏感词过滤、 prompt 注入检测
│ 正则匹配 + 分类器 │ 延迟 < 5ms
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ L2: Intent Classifier │ ← 识别任务语义(「读文件」vs「删文件」)
│ Lightweight LLM │ 延迟 < 100ms(使用 1B 模型)
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ L3: Tool Schema Guard │ ← 参数验证、权限检查
│ JWT/OPA/POLO │ 延迟 < 1ms
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ L4: Output Guard │ ← 输出内容安全审查
│ 正则 + 分类器 │ 延迟 < 5ms
└─────────────────────────┘
│
▼
用户输出

关键设计:L2 的 Intent Classifier 使用小模型(如 Qwen2.5-1.5B)而非主 Agent 模型,避免 Agent 绕过自己的安全检查。

2.2 权限模型的工程实现​

结合 OPA(Open Policy Agent)实现细粒度权限控制:

# tools_policy.rego
package agent.tools

# 默认拒绝
default allow = false

# 允许:文件读取工具,且路径在白名单内
allow {
input.tool == "read_file"
startswith(input.args.path, "/home/user/allowed/")
not contains(input.args.path, "..")
}

# 允许:数据库查询,只读模式
allow {
input.tool == "db_query"
input.args.read_only == true
lower(input.args.sql) == "select"
}

# 禁止:写入系统关键路径
deny["writing to system path"] {
input.tool in ["write_file", "delete_file"]
startswith(input.args.path, "/etc/")
}

# 速率限制:单个 API 工具每秒不超过 10 次
deny["rate limit exceeded"] {
input.tool == "api_call"
count_tool_calls(input.agent_id, "api_call", time.now_ns() - 1e9) > 10
}

三、错误恢复:容错与回退策略​

生产环境中 Agent 会以各种方式失败。以下是按频率排序的故障模式及对策:

故障模式出现频率典型症状恢复策略
LLM 输出格式错误15-25%JSON 解析失败、缺少必填字段重试 + 修正 prompt
工具调用超时10-15%外部 API 无响应超过 30s超时后重试(1-3 次)
上下文溢出5-10%Token 超过模型限制截断历史 + 摘要替代
权限拒绝3-5%工具返回 403终止 + 友好提示
循环震荡2-5%工具 A→B→A→B终止 + 重启(带指令)
幻觉事实1-3%返回不存在的数据验证步骤 + 事实校验

3.1 指数退避重试​

import asyncio
import random
from functools import wraps

def agent_retry(max_retries: int = 3, base_delay: float = 1.0):
"""带指数退避的 Agent 调用重试装饰器"""
def decorator(func):
@wraps(func)
async def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_retries):
try:
return await func(*args, **kwargs)
except ToolTimeoutError as e:
last_exception = e
delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
print(f"Attempt {attempt+1} failed: timeout. "
f"Retrying in {delay:.1f}s...")
await asyncio.sleep(delay)
except ToolPermissionError:
# 权限错误不重试,直接上报
raise
except ToolRateLimitError as e:
# 限流错误可能需要更长的等待
retry_after = getattr(e, 'retry_after', 5)
await asyncio.sleep(retry_after)
raise last_exception
return wrapper
return decorator

@agent_retry(max_retries=2, base_delay=0.5)
async def call_external_api(agent_ctx, endpoint: str, payload: dict):
# 假设这是一个外部 API 调用
async with aiohttp.ClientSession() as session:
async with session.post(
endpoint,
json=payload,
timeout=aiohttp.ClientTimeout(total=30)
) as resp:
if resp.status == 429:
raise ToolRateLimitError(
retry_after=int(resp.headers.get("Retry-After", 5))
)
if resp.status == 403:
raise ToolPermissionError("API denied access")
resp.raise_for_status()
return await resp.json()

3.2 上下文压缩(Context Summarization)​

长对话 Agent 面临的核心矛盾:保留足够历史以维持连贯性 vs 控制 token 消耗。

class ContextCompressor:
"""分层上下文压缩策略"""

def __init__(self, max_tokens: int = 32000,
summarizer_model: str = "qwen2.5-1.5b"):
self.max_tokens = max_tokens
self.summarizer = summarizer_model # 小模型用于摘要

def compress(self, history: list, current_tokens: int) -> list:
if current_tokens <= self.max_tokens:
return history

# 策略:保留最近的 N 轮完整对话,压缩更早的部分
recent_rounds = 3
recent = history[-recent_rounds:]

# 对历史进行分段摘要
old_history = history[:-recent_rounds]
summaries = []
current_summary = []
summary_token_count = 0

for turn in old_history:
current_summary.append(turn)
summary_token_count += self._estimate_tokens(str(turn))
if summary_token_count > 5000: # 每 5K tokens 生成一个摘要
summary_text = self._summarize(current_summary)
summaries.append(summary_text)
current_summary = []
summary_token_count = 0

if current_summary:
summaries.append(self._summarize(current_summary))

# 拼接:摘要 + 最近的完整对话
compressed = [
{"role": "system",
"content": "[压缩摘要] " + " | ".join(summaries)}
] + recent

return compressed

def _summarize(self, turns: list) -> str:
"""调用小模型生成摘要(实际需集成 LLM 接口)"""
# 伪代码:实际调用 self.summarizer
return f"Previous {len(turns)} turns summarized: ..."

def _estimate_tokens(self, text: str) -> int:
return len(text) // 4 # 粗略估计

四、生产运维清单​

以下 checklist 建议在 Agent 系统上线前逐项检查:

  • 可观测性:是否实现分层采样 tracing?是否能在 5 秒内定位到失败的 tool call?
  • 超时控制:每个工具调用是否有独立超时(不是全局超时)?超时后的行为是否定义清楚?
  • 循环检测:是否有循环检测器和最大步数限制?循环触发后是否有替代路径?
  • 权限最小化:Agent 的每个工具权限是否是必要的最小集?是否使用 OPA 做策略决策?
  • 限流保护:对每个外部 API 是否有按 agent_id 的限流?限流上限是 API 提供方的 70% 以下?
  • 成本预算:每个 session 是否有 token 预算上限?超限后是优雅降级还是硬终止?
  • 审计日志:所有工具调用是否记录到不可篡改的审计日志?重放事件的流程是否定义?
  • 灰度发布:新工具上线是否有灰度机制?回滚流程是否经过演练?
  • 监控告警:Agent 的 P50/P95/P99 延迟是否监控?工具调用失败率 > 5% 是否告警?
  • 回滚方案:是否能在 10 分钟内将 Agent 回滚到上一个稳定版本?包括模型版本和工具版本的回滚?

相关阅读​