Agent 的可观测性设计:日志、追踪与调试最佳实践
想象一下这个场景:你负责的AI Agent系统刚刚上线,一切似乎都运行得很完美。用户反馈积极,业务指标稳步上升。然而,三天后的一个深夜,你收到了一条告警:“Agent响应延迟超过5分钟,用户满意度骤降”。你立刻登录系统,检查了服务器负载——正常。查看了数据库状态——一切正常。检查了网络连接——没有问题。但Agent就是出现了间歇性的性能问题,有时甚至完全停止响应。你尝试复现问题,但在测试环境中一切
Agent 的可观测性设计:日志、追踪与调试最佳实践
1. 引入与连接:为什么我们需要Agent可观测性?
1.1 从一个"神秘"的故障开始
想象一下这个场景:你负责的AI Agent系统刚刚上线,一切似乎都运行得很完美。用户反馈积极,业务指标稳步上升。然而,三天后的一个深夜,你收到了一条告警:“Agent响应延迟超过5分钟,用户满意度骤降”。
你立刻登录系统,检查了服务器负载——正常。查看了数据库状态——一切正常。检查了网络连接——没有问题。但Agent就是出现了间歇性的性能问题,有时甚至完全停止响应。
你尝试复现问题,但在测试环境中一切正常。你开始查看日志,但日志文件庞杂且分散,很难找到有用的线索。你尝试添加更多日志,但问题似乎又消失了。这就像在黑暗中摸索,你感觉自己就像一个没有地图的探险家。
这不是虚构的场景,而是许多AI系统开发者和运维人员的真实经历。在传统软件系统中,我们已经有了相对成熟的监控和调试工具,但AI Agent系统有其独特的复杂性,传统的监控方法往往显得力不从心。
1.2 AI Agent系统的独特挑战
为什么AI Agent系统的可观测性如此具有挑战性?让我们来看看AI Agent与传统软件系统的几个关键区别:
-
非确定性行为:传统软件通常遵循"输入-处理-输出"的确定性模式,相同的输入总是产生相同的输出。但AI Agent,特别是基于大型语言模型(LLM)的Agent,其行为具有内在的非确定性。相同的提示词可能会产生不同的响应,这使得调试变得异常困难。
-
多步骤推理链:AI Agent通常通过多步推理来完成任务,每一步的决策都会影响后续步骤。一个问题可能出现在推理链的任何环节,但传统的监控方法很难追踪这种复杂的决策路径。
-
状态空间爆炸:Agent可能处于的状态数量可能是天文数字,我们无法预定义所有可能的状态和转换。这使得传统的基于状态机的监控方法几乎不可行。
-
工具调用的复杂性:现代Agent通常能够调用各种外部工具和API,每次工具调用都是一个潜在的故障点。我们需要监控这些调用的输入、输出和性能,但这增加了系统的复杂性。
-
上下文的动态变化:Agent的决策高度依赖于上下文,而上下文可能会随着时间和交互不断变化。理解"为什么Agent在那一刻做出了那个决定"需要完整的上下文视图。
正是这些独特的挑战,使得Agent的可观测性设计成为一个既重要又困难的课题。
1.3 可观测性不仅仅是"监控"
在深入讨论之前,让我们先澄清一个常见的误解:可观测性(Observability)不等同于监控(Monitoring)。
监控通常是指收集预定义指标的数据,并在这些指标超出阈值时发出告警。它回答的问题是:“系统是否出了问题?”
而可观测性是一个更广泛的概念,它指的是通过检查系统的输出来理解系统内部状态的能力。它回答的问题是:"系统为什么出了问题?“以及"系统内部正在发生什么?”
对于AI Agent系统,我们需要的不仅仅是监控,而是完整的可观测性解决方案。
1.4 可观测性的三大支柱
在软件系统中,可观测性通常被描述为由三大支柱组成:
- 日志(Logging):记录离散的事件,如错误消息、交易完成等。
- 指标(Metrics):在一段时间内测量的数据,如CPU使用率、请求延迟等。
- 追踪(Tracing):记录请求通过系统的完整路径,包括服务间的调用关系。
对于AI Agent系统,这三大支柱仍然适用,但我们需要对其进行扩展和定制,以满足Agent系统的特殊需求。
在本篇文章中,我们将深入探讨如何为AI Agent系统设计和实现完整的可观测性解决方案。我们将从基础概念开始,逐步深入到技术细节,最后提供实战指南和最佳实践。
无论你是AI系统的开发者、运维工程师,还是对AI可观测性感兴趣的技术爱好者,这篇文章都将为你提供有价值的见解和实用的工具。
2. 概念地图:Agent可观测性的完整框架
在深入探讨各个组件之前,让我们先建立一个整体的概念框架。理解Agent可观测性的全貌将帮助我们更好地组织和理解后续的内容。
2.1 核心概念与术语
首先,让我们定义一些在Agent可观测性领域中常用的关键术语:
| 术语 | 定义 |
|---|---|
| Agent可观测性 | 通过收集、分析和解释Agent系统的内部状态和外部行为数据,来理解、调试和优化Agent性能的能力 |
| 推理追踪(Reasoning Trace) | 记录Agent从输入到输出的完整推理过程,包括中间步骤、决策依据和思考过程 |
| 提示词日志(Prompt Logging) | 记录发送给LLM的完整提示词和接收到的响应 |
| 决策点(Decision Point) | Agent在执行过程中需要做出选择的关键点 |
| 工具调用追踪(Tool Call Trace) | 记录Agent调用外部工具的详细信息,包括输入参数、输出结果和执行时间 |
| 上下文快照(Context Snapshot) | 在特定时间点Agent的完整状态记录,包括内存、对话历史和环境信息 |
| 根因分析(Root Cause Analysis) | 确定Agent异常行为根本原因的过程 |
| 回放调试(Replay Debugging) | 使用记录的数据重新执行Agent的推理过程,以便于调试 |
| 可解释性(Explainability) | 解释Agent决策过程和结果的能力,使Agent的行为对人类来说是可理解的 |
| 性能剖析(Performance Profiling) | 测量和分析Agent在不同部分的性能表现,识别瓶颈 |
2.2 Agent可观测性的层次结构
Agent可观测性可以分为多个层次,每个层次关注系统的不同方面:
- 基础设施层:监控运行Agent的底层基础设施,如服务器、容器、网络等。
- 服务层:监控Agent系统中的各个服务组件,如LLM API、向量数据库、工具服务等。
- 执行层:监控Agent的执行过程,包括推理步骤、工具调用、决策过程等。
- 语义层:理解Agent的行为语义,如推理逻辑、目标达成情况、用户意图理解等。
- 业务层:将Agent的行为与业务指标关联起来,如用户满意度、任务完成率等。
2.3 关键组件与数据流
一个完整的Agent可观测性系统通常包括以下关键组件:
- 数据收集器:负责从Agent系统中收集各类数据。
- 数据存储:高效存储收集到的各类可观测性数据。
- 数据处理与分析引擎:处理和分析收集到的数据,提取有价值的信息。
- 可视化界面:以直观的方式展示可观测性数据。
- 告警系统:在检测到异常时及时通知相关人员。
- 调试工具:帮助开发者深入分析和解决问题。
数据在这些组件之间的流动形成了可观测性系统的核心数据流:
Agent系统 → 数据收集器 → 数据存储 → 数据处理与分析引擎 → 可视化界面/告警系统/调试工具
2.4 与传统软件可观测性的对比
虽然Agent可观测性建立在传统软件可观测性的基础上,但两者之间存在重要的区别:
| 方面 | 传统软件可观测性 | Agent可观测性 |
|---|---|---|
| 行为特性 | 确定性,可预测 | 非确定性,概率性 |
| 核心关注点 | 错误、性能、资源使用 | 推理过程、决策逻辑、目标达成 |
| 数据类型 | 日志、指标、追踪 | 提示词、推理链、上下文、决策点 |
| 调试方法 | 断点调试、日志分析 | 回放调试、推理链分析、对比实验 |
| 成功指标 | 可用性、延迟、错误率 | 任务完成率、用户满意度、决策质量 |
2.5 Agent可观测性的概念图谱
为了更直观地理解这些概念之间的关系,让我们来看一个概念图谱:
这张概念图谱展示了Agent可观测性的主要组成部分及其相互关系。在接下来的章节中,我们将深入探讨每个组件的细节。
3. 基础理解:Agent可观测性的核心概念
在本节中,我们将深入探讨Agent可观测性的核心概念,建立直观的理解基础。我们将从生活化的类比开始,逐步引入专业概念。
3.1 把Agent想象成一个"黑盒"侦探
为了帮助理解Agent可观测性的重要性,让我们使用一个生活化的类比:把AI Agent想象成一个侦探。
想象你雇佣了一个侦探来解决一个复杂的案件。这个侦探非常聪明,但有一个特点:他不会告诉你他是如何破案的,只会告诉你最终结果。
有一天,侦探告诉你案件已经解决,但结果似乎不太对。你问他是怎么得出这个结论的,他只是耸耸肩说:“我就是知道。”
在这种情况下,你很难判断侦探的结论是否可靠,也很难帮助他改进。你不知道他是否忽略了重要线索,是否做出了错误的假设,或者是否被误导了。
这就是没有可观测性的Agent系统的工作方式。它给我们结果,但我们不知道它是如何得出这些结果的。
现在,想象一下如果这个侦探记录了他的整个调查过程会怎样:
- 他记录了所有查看过的线索
- 他写下了每一个假设和推理步骤
- 他记录了与证人的每一次对话
- 他标注了决策点和选择理由
- 他记录了每个步骤花费的时间
有了这些记录,即使侦探得出了错误的结论,你也能够:
- 理解他的推理过程
- 找出错误发生的环节
- 提供针对性的改进建议
- 在类似案件中避免同样的错误
这就是可观测性为Agent系统带来的价值。
3.2 Agent可观测性的三大核心目标
基于这个类比,我们可以确定Agent可观测性的三大核心目标:
3.2.1 透明性:理解Agent的"思考过程"
透明性是指能够看到Agent从输入到输出的完整路径,包括所有中间步骤、决策和推理过程。
在传统软件中,我们可以通过查看代码来理解程序的逻辑,但在基于LLM的Agent系统中,"逻辑"是隐含在模型参数中的,不是显式编写的代码。因此,我们需要通过其他方式来获得透明性。
透明性的关键组成部分包括:
- 推理追踪:记录Agent的每一步推理过程
- 决策记录:记录Agent在每个决策点的选择和理由
- 上下文可见性:展示Agent在每个时刻的完整上下文
- 提示词/响应记录:保存与LLM的完整交互历史
3.2.2 可调试性:快速定位和修复问题
可调试性是指在Agent出现异常行为时,能够快速定位问题根源并进行修复的能力。
传统软件调试通常依赖于断点、变量检查和堆栈跟踪,但这些方法在Agent系统中往往效果不佳。Agent系统的可调试性需要特殊的工具和方法:
- 回放调试:能够重放Agent的执行过程
- 状态快照:在关键节点保存Agent的完整状态
- 对比分析:能够对比正常和异常执行路径的差异
- 假设检验:能够测试"如果在某一点改变决策会怎样"
3.2.3 可优化性:持续改进Agent的性能
可优化性是指基于可观测性数据,持续改进Agent性能和效果的能力。
优化不仅仅是修复bug,还包括提高Agent的效率、准确性和用户满意度。可观测性为优化提供了数据基础:
- 性能分析:识别Agent执行中的瓶颈
- 成功率追踪:监控Agent完成任务的成功率
- 用户反馈关联:将Agent行为与用户反馈关联起来
- A/B测试支持:为不同版本的Agent提供对比数据
3.3 Agent可观测性的数据类型
为了实现上述三大目标,我们需要收集多种类型的数据。让我们逐一了解这些数据类型:
3.3.1 提示词与响应日志
提示词与响应日志是最基本也是最重要的数据类型之一。它们记录了Agent与LLM之间的完整交互:
- 发送给LLM的完整提示词:包括系统提示、上下文、用户输入等
- LLM返回的完整响应:包括生成的文本、函数调用等
- 元数据:如时间戳、模型版本、温度参数等
为什么提示词日志如此重要?因为提示词是Agent"思考"的输入,理解提示词的内容和结构是理解Agent行为的第一步。
3.3.2 推理追踪数据
推理追踪数据记录了Agent完成任务的完整推理过程,包括:
- 思考步骤:Agent的中间思考过程
- 决策点:Agent需要做出选择的关键点
- 决策理由:Agent做出特定选择的原因
- 中间结果:每个推理步骤产生的结果
推理追踪数据就像侦探的调查笔记,让我们能够跟随Agent的"思路",理解它是如何一步步得出结论的。
3.3.3 工具调用数据
现代Agent通常能够调用各种外部工具和API,工具调用数据记录了这些交互:
- 调用的工具名称
- 输入参数
- 输出结果
- 执行时间
- 成功/失败状态
- 错误信息(如果有)
工具调用是Agent系统中常见的故障点,详细的工具调用数据对于定位和修复问题至关重要。
3.3.4 上下文快照
上下文快照记录了Agent在特定时间点的完整状态,包括:
- 对话历史:用户和Agent的交互历史
- 短期记忆:Agent当前任务相关的临时信息
- 长期记忆:Agent从过去交互中学习和存储的信息
- 环境状态:Agent感知到的外部环境信息
上下文是Agent决策的基础,理解上下文对于解释Agent的行为至关重要。
3.3.5 性能指标数据
性能指标数据帮助我们了解Agent系统的运行效率:
- 端到端延迟:从用户输入到Agent响应的总时间
- LLM响应时间:LLM生成响应的时间
- 工具执行时间:各个工具的执行时间
- 资源使用情况:CPU、内存、GPU等资源的使用情况
- 令牌使用量:LLM API的令牌消耗情况
性能指标数据对于优化Agent系统的效率和成本非常重要。
3.3.6 用户反馈数据
最后,用户反馈数据将Agent的行为与实际用户体验关联起来:
- 显式反馈:用户的评分、评论等
- 隐式反馈:用户的行为数据,如点击、停留时间、完成率等
- 会话级反馈:整个会话的用户满意度
- 步骤级反馈:对Agent特定响应的反馈
用户反馈数据是衡量Agent成功与否的最终标准,也是指导优化方向的重要依据。
3.4 常见误解澄清
在探讨Agent可观测性时,有几个常见的误解需要澄清:
误解1:“可观测性就是日志记录”
虽然日志记录是可观测性的重要组成部分,但可观测性远不止于此。可观测性包括日志、追踪、指标,以及将这些数据关联起来、进行分析和可视化的能力。
误解2:“LLM的内部工作原理是不可观测的,所以Agent可观测性是不可能的”
虽然我们确实无法直接观察LLM的内部工作原理(如每个神经元的激活状态),但我们可以观察LLM的输入和输出,以及Agent围绕LLM构建的推理过程。这足以为我们提供有价值的可观测性。
误解3:“可观测性会增加太多开销,影响性能”
诚然,可观测性会带来一定的开销,但这种开销通常是值得的。而且,通过合理的设计(如采样、异步处理、压缩等),我们可以将开销控制在可接受的范围内。
误解4:“只有生产环境需要可观测性”
可观测性在开发和测试环境中同样重要。在开发阶段,良好的可观测性可以帮助开发者更快地理解和调试Agent的行为;在测试阶段,它可以帮助发现更多边缘情况和潜在问题。
4. 层层深入:Agent可观测性的技术实现
现在我们已经建立了对Agent可观测性的基础理解,接下来让我们深入探讨技术实现的细节。我们将从基本原理开始,逐步增加复杂度。
4.1 第一层:数据收集的基本原理与机制
数据收集是可观测性系统的第一步,也是最基础的一步。在这一层,我们将探讨如何从Agent系统中收集各类可观测性数据。
4.1.1 数据收集的设计原则
在设计数据收集机制时,我们应该遵循以下原则:
-
完整性原则:收集的数据应该足够完整,能够回答我们可能提出的问题。但这并不意味着我们需要收集一切,而是要有选择地收集关键数据。
-
最小侵入性原则:数据收集机制应该尽可能少地干扰Agent系统的正常运行。这意味着低开销、异步处理、失败不影响主流程等。
-
一致性原则:收集的数据应该具有一致的格式和结构,便于后续的处理和分析。
-
上下文关联原则:收集的数据应该包含足够的上下文信息,使得不同类型的数据能够关联起来。
-
可配置性原则:数据收集的范围和详细程度应该是可配置的,以便在不同的环境和场景下进行调整。
4.1.2 提示词与响应收集
提示词与响应是Agent可观测性中最基本的数据类型。让我们看看如何高效地收集这些数据:
拦截器模式
收集提示词与响应的一种常见方法是使用拦截器模式。我们可以在Agent与LLM API之间插入一个拦截层,记录所有的请求和响应:
class LLMInterceptor:
def __init__(self, llm_client, observability_client):
self.llm_client = llm_client
self.observability_client = observability_client
def generate(self, prompt, **kwargs):
# 记录开始时间
start_time = time.time()
# 生成唯一的调用ID
call_id = str(uuid.uuid4())
# 记录请求
self.observability_client.log_prompt(
call_id=call_id,
prompt=prompt,
kwargs=kwargs,
timestamp=start_time
)
try:
# 调用实际的LLM客户端
response = self.llm_client.generate(prompt, **kwargs)
# 计算延迟
latency = time.time() - start_time
# 记录响应
self.observability_client.log_response(
call_id=call_id,
response=response,
latency=latency,
timestamp=time.time()
)
return response
except Exception as e:
# 记录错误
self.observability_client.log_error(
call_id=call_id,
error=str(e),
timestamp=time.time()
)
raise
这种方法的优点是对Agent代码的侵入性很小,只需要替换LLM客户端即可。
结构化提示词记录
当我们记录提示词时,不仅仅是记录原始文本,还应该记录提示词的结构和组件。现代提示词通常包含多个部分,如系统提示、对话历史、工具描述、用户输入等:
def log_structured_prompt(call_id, structured_prompt):
"""记录结构化的提示词"""
observability_client.log_prompt(
call_id=call_id,
raw_prompt=structured_prompt.to_string(),
structured={
"system_prompt": structured_prompt.system_prompt,
"conversation_history": [
{"role": msg.role, "content": msg.content}
for msg in structured_prompt.conversation_history
],
"tools": structured_prompt.tools,
"user_input": structured_prompt.user_input,
"other_components": structured_prompt.other_components
}
)
结构化的提示词记录使得后续的分析和调试更加容易。
4.1.3 推理追踪收集
推理追踪是Agent可观测性的核心,它记录了Agent的"思考过程"。收集推理追踪数据需要Agent框架的支持:
追踪装饰器
我们可以使用装饰器模式来自动记录Agent的推理步骤:
def trace_step(step_name):
"""装饰器用于记录推理步骤"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 生成步骤ID
step_id = str(uuid.uuid4())
# 获取追踪上下文
trace_context = get_current_trace_context()
# 记录步骤开始
observability_client.log_step_start(
trace_id=trace_context.trace_id,
step_id=step_id,
parent_step_id=trace_context.current_step_id,
step_name=step_name,
inputs={
"args": args,
"kwargs": kwargs
},
timestamp=time.time()
)
# 更新追踪上下文
new_context = trace_context.with_current_step(step_id)
set_current_trace_context(new_context)
try:
# 执行实际的步骤
result = func(*args, **kwargs)
# 记录步骤结束
observability_client.log_step_end(
trace_id=trace_context.trace_id,
step_id=step_id,
output=result,
status="success",
timestamp=time.time()
)
return result
except Exception as e:
# 记录步骤错误
observability_client.log_step_end(
trace_id=trace_context.trace_id,
step_id=step_id,
error=str(e),
status="error",
timestamp=time.time()
)
raise
finally:
# 恢复追踪上下文
set_current_trace_context(trace_context)
return wrapper
return decorator
然后我们可以在Agent的关键方法上使用这个装饰器:
class MyAgent:
@trace_step("plan")
def plan(self, goal):
# 规划步骤
...
@trace_step("execute")
def execute(self, plan):
# 执行步骤
...
@trace_step("reflect")
def reflect(self, result):
# 反思步骤
...
思考内容记录
除了记录步骤的输入输出,我们还应该记录Agent的"思考内容"。这通常是Agent生成的用于指导其行动的内部文本:
def record_thought(trace_id, step_id, thought, thought_type="reasoning"):
"""记录Agent的思考内容"""
observability_client.log_thought(
trace_id=trace_id,
step_id=step_id,
thought=thought,
thought_type=thought_type,
timestamp=time.time()
)
在Agent代码中,我们可以这样使用:
def plan(self, goal):
# 生成思考内容
thought = self._generate_plan_thought(goal)
# 记录思考内容
record_thought(
trace_id=get_current_trace_id(),
step_id=get_current_step_id(),
thought=thought,
thought_type="planning"
)
# 基于思考内容生成计划
plan = self._parse_thought_to_plan(thought)
return plan
4.1.4 上下文快照收集
上下文快照记录了Agent在特定时间点的完整状态。收集上下文快照的关键是确定何时记录快照以及记录哪些信息:
关键快照点
我们应该在以下关键时间点记录上下文快照:
- 任务开始时
- 每个主要步骤完成后
- 每个决策点之前和之后
- 工具调用之前和之后
- 检测到异常时
- 任务结束时
快照内容
上下文快照应该包含以下内容:
def capture_context_snapshot(trace_id, step_id, snapshot_type):
"""捕获上下文快照"""
agent = get_current_agent()
snapshot = {
"trace_id": trace_id,
"step_id": step_id,
"snapshot_type": snapshot_type,
"timestamp": time.time(),
# 对话历史
"conversation_history": [
{"role": msg.role, "content": msg.content}
for msg in agent.conversation_history
],
# 短期记忆
"short_term_memory": agent.short_term_memory.as_dict(),
# 长期记忆(可能只记录关键信息或摘要)
"long_term_memory_summary": agent.long_term_memory.get_summary(),
# 当前目标
"current_goal": agent.current_goal,
# 执行状态
"execution_state": {
"current_step": agent.current_step,
"step_status": agent.step_status,
"plan": agent.plan,
"executed_steps": agent.executed_steps
},
# 环境信息
"environment": agent.get_environment_state(),
# 配置信息
"config": {
"model": agent.model,
"temperature": agent.temperature,
"max_tokens": agent.max_tokens,
# 其他相关配置
}
}
observability_client.save_context_snapshot(snapshot)
增量快照
为了减少存储空间和处理开销,我们可以使用增量快照策略,只记录自上次快照以来发生变化的部分:
class IncrementalSnapshotManager:
def __init__(self):
self.last_snapshot = None
def capture_incremental_snapshot(self, trace_id, step_id, snapshot_type):
full_snapshot = capture_full_context_snapshot(trace_id, step_id, snapshot_type)
if self.last_snapshot is None:
# 第一次快照,保存完整快照
incremental_snapshot = full_snapshot
incremental_snapshot["is_full"] = True
else:
# 计算增量
incremental_snapshot = {
"trace_id": trace_id,
"step_id": step_id,
"snapshot_type": snapshot_type,
"timestamp": full_snapshot["timestamp"],
"is_full": False,
"changes": self._compute_changes(self.last_snapshot, full_snapshot)
}
# 保存增量快照
observability_client.save_context_snapshot(incremental_snapshot)
# 更新上次快照
self.last_snapshot = full_snapshot
return incremental_snapshot
def _compute_changes(self, old, new):
"""计算两个快照之间的变化"""
changes = {}
for key in new:
if key not in old:
changes[key] = {"type": "added", "value": new[key]}
elif old[key] != new[key]:
if isinstance(old[key], dict) and isinstance(new[key], dict):
nested_changes = self._compute_changes(old[key], new[key])
if nested_changes:
changes[key] = {"type": "modified", "changes": nested_changes}
else:
changes[key] = {"type": "modified", "old_value": old[key], "new_value": new[key]}
for key in old:
if key not in new:
changes[key] = {"type": "removed"}
return changes
4.2 第二层:数据存储与处理的细节
收集到数据后,我们需要有效地存储和处理这些数据。在这一层,我们将探讨数据存储的选择、数据处理的流程以及相关的技术细节。
4.2.1 数据存储的选择
Agent可观测性数据具有以下特点:
- 数据量大:Agent系统可能会产生大量的可观测性数据,特别是提示词和响应可能很长。
- 数据类型多样:包括结构化数据(如指标)、半结构化数据(如日志)和非结构化数据(如提示词文本)。
- 写入频繁:需要持续写入大量数据。
- 查询模式复杂:需要支持多种查询模式,如按时间范围查询、按追踪ID查询、全文搜索等。
基于这些特点,我们通常需要结合使用多种存储系统:
日志存储
对于日志类数据(如提示词、响应、推理步骤),我们可以使用专门的日志存储系统:
- Elasticsearch:强大的全文搜索能力,适合存储和查询文本数据。
- Loki:轻量级的日志聚合系统,与Grafana集成良好。
- ClickHouse:高性能的列式数据库,适合大规模日志数据的存储和分析。
追踪存储
对于追踪数据,我们可以使用:
- Jaeger:开源的分布式追踪系统,提供可视化界面。
- Zipkin:另一个流行的分布式追踪系统。
- 时序数据库:如InfluxDB、Prometheus,适合存储指标数据。
对象存储
对于大型数据(如完整的上下文快照),我们可以使用对象存储:
- S3:亚马逊的对象存储服务。
- MinIO:开源的对象存储系统,兼容S3 API。
混合存储架构
在实践中,我们通常会使用混合存储架构,根据数据的特点和访问模式选择合适的存储系统:
4.2.2 数据预处理
在存储数据之前,我们通常需要进行一些预处理:
数据清洗
数据清洗包括去除敏感信息、格式化数据、过滤噪声等:
def clean_sensitive_data(data):
"""清洗敏感数据"""
# 定义敏感信息模式
patterns = [
(r'password["\s:]+([^\s,}]+)', '[REDACTED]'),
(r'api[_-]?key["\s:]+([^\s,}]+)', '[REDACTED]'),
(r'email["\s:]+([^\s,}]+)', '[REDACTED]'),
# 更多模式...
]
# 转换为字符串(如果还不是)
data_str = json.dumps(data) if isinstance(data, (dict, list)) else str(data)
# 应用所有模式
for pattern, replacement in patterns:
data_str = re.sub(pattern, replacement, data_str)
# 转换回原始类型(如果可能)
try:
return json.loads(data_str)
except json.JSONDecodeError:
return data_str
数据丰富化
数据丰富化是指向数据添加额外的上下文信息,使其更有价值:
def enrich_data(data):
"""丰富数据"""
enriched = data.copy()
# 添加环境信息
enriched["environment"] = {
"service_name": os.environ.get("SERVICE_NAME", "unknown"),
"service_version": os.environ.get("SERVICE_VERSION", "unknown"),
"deployment": os.environ.get("DEPLOYMENT_ENV", "unknown"),
"region": os.environ.get("REGION", "unknown"),
}
# 添加计算字段
if "start_time" in enriched and "end_time" in enriched:
enriched["duration"] = enriched["end_time"] - enriched["start_time"]
# 添加标签(基于内容的自动分类)
enriched["tags"] = generate_tags(enriched)
return enriched
数据采样
对于高吞吐量的系统,我们可能需要对数据进行采样,以减少存储和处理开销:
class DataSampler:
def __init__(self, default_rate=1.0):
self.default_rate = default_rate
self.rules = []
def add_rule(self, condition, rate):
"""添加采样规则"""
self.rules.append((condition, rate))
def should_sample(self, data):
"""决定是否应该采样这条数据"""
# 检查规则
for condition, rate in self.rules:
if condition(data):
return random.random() < rate
# 使用默认采样率
return random.random() < self.default_rate
# 示例用法
sampler = DataSampler(default_rate=0.5) # 默认50%采样率
# 添加规则:错误数据100%采样
sampler.add_rule(
lambda d: d.get("status") == "error",
1.0
)
# 添加规则:特定用户的数据100%采样
sampler.add_rule(
lambda d: d.get("user_id") in IMPORTANT_USER_IDS,
1.0
)
# 添加规则:慢查询100%采样
sampler.add_rule(
lambda d: d.get("duration", 0) > 5.0, # 超过5秒
1.0
)
4.2.3 数据关联
Agent可观测性的一个关键挑战是将不同类型的数据关联起来,形成一个完整的视图。我们可以通过以下方式实现数据关联:
全局追踪ID
为每个Agent执行实例分配一个全局唯一的追踪ID,所有相关数据都使用这个ID进行标记:
class TraceContext:
_context = threading.local()
@classmethod
def start_trace(cls):
"""开始一个新的追踪"""
trace_id = str(uuid.uuid4())
cls._context.trace_id = trace_id
cls._context.step_id = None
return trace_id
@classmethod
def get_trace_id(cls):
"""获取当前追踪ID"""
return getattr(cls._context, "trace_id", None)
@classmethod
def set_step_id(cls, step_id):
"""设置当前步骤ID"""
cls._context.step_id = step_id
@classmethod
def get_step_id(cls):
"""获取当前步骤ID"""
return getattr(cls._context, "step_id", None)
@classmethod
def get_context(cls):
"""获取完整上下文"""
return {
"trace_id": cls.get_trace_id(),
"step_id": cls.get_step_id()
}
因果关系建模
除了使用追踪ID关联数据,我们还可以建模数据之间的因果关系:
我们可以在数据中明确记录这些因果关系:
def log_causal_relation(source_id, target_id, relation_type):
"""记录因果关系"""
observability_client.log_relation(
trace_id=TraceContext.get_trace_id(),
source_id=source_id,
target_id=target_id,
relation_type=relation_type,
timestamp=time.time()
)
4.3 第三层:分析与可视化的底层逻辑
有了收集和存储的数据,接下来我们需要对数据进行分析和可视化,以提取有价值的见解。在这一层,我们将探讨数据分析的方法和可视化的底层逻辑。
4.3.1 追踪分析
追踪分析是理解Agent执行过程的核心。让我们探讨几种常见的追踪分析方法:
执行流程分析
执行流程分析帮助我们理解Agent是如何一步步完成任务的:
def analyze_execution_flow(trace_id):
"""分析执行流程"""
# 获取追踪数据
trace = observability_client.get_trace(trace_id)
# 构建执行图
execution_graph = build_execution_graph(trace)
# 分析关键路径
critical_path = find_critical_path(execution_graph)
# 分析决策点
decision_points = find_decision_points(trace)
# 分析循环和重试
loops_and_retries = find_loops_and_retries(execution_graph)
return {
"execution_graph": execution_graph,
"critical_path": critical_path,
"decision_points": decision_points,
"loops_and_retries": loops_and_retries
}
def build_execution_graph(trace):
"""构建执行图"""
graph = nx.DiGraph()
# 添加节点
for step in trace.steps:
graph.add_node(
step.step_id,
name=step.name,
start_time=step.start_time,
end_time=step.end_time,
duration=step.duration,
status=step.status,
type=step.type
)
# 添加边(基于父步骤关系)
for step in trace.steps:
if step.parent_step_id:
graph.add_edge(step.parent_step_id, step.step_id)
# 添加因果关系边
for relation in trace.relations:
if relation.source_id in graph and relation.target_id in graph:
graph.add_edge(
relation.source_id,
relation.target_id,
relation_type=relation.type
)
return graph
性能瓶颈分析
性能瓶颈分析帮助我们识别Agent执行过程中的慢步骤:
def find_performance_bottlenecks(trace_id, threshold_percentile=95):
"""发现性能瓶颈"""
# 获取追踪数据
trace = observability_client.get_trace(trace_id)
# 获取所有步骤的持续时间
step_durations = [step.duration for step in trace.steps if step.duration]
if not step_durations:
return []
# 计算阈值
threshold = np.percentile(step_durations, threshold_percentile)
# 找出超过阈值的步骤
bottlenecks = []
for step in trace.steps:
if step.duration and step.duration > threshold:
# 分析该步骤的详情
step_analysis = analyze_step_performance(step)
bottlenecks.append({
"step_id": step.step_id,
"step_name": step.name,
"duration": step.duration,
"threshold": threshold,
"analysis": step_analysis
})
# 按持续时间降序排序
bottlenecks.sort(key=lambda x: x["duration"], reverse=True)
return bottlenecks
def analyze_step_performance(step):
"""分析单个步骤的性能"""
analysis = {}
# 如果是LLM调用步骤
if step.type == "llm_call":
# 分析提示词复杂度
prompt_complexity = analyze_prompt_complexity(step.inputs.get("prompt", ""))
analysis["prompt_complexity"] = prompt_complexity
# 分析令牌使用情况
if "response" in step.outputs:
token_usage = step.outputs.get("token_usage", {})
analysis["token_usage"] = token_usage
# 如果是工具调用步骤
elif step.type == "tool_call":
# 分析工具执行时间
tool_name = step.inputs.get("tool_name", "unknown")
analysis["tool_name"] = tool_name
# 分析输入参数大小
input_size = len(json.dumps(step.inputs.get("args", {})))
analysis["input_size_bytes"] = input_size
return analysis
4.3.2 错误分析
错误分析帮助我们理解Agent失败的原因:
def analyze_errors(trace_id):
"""分析追踪中的错误"""
trace = observability_client.get_trace(trace_id)
# 找出所有错误步骤
error_steps = [step for step in trace.steps if step.status == "error"]
if not error_steps:
return {"has_errors": False}
# 分类错误
error_categories = {}
for step in error_steps:
error_type = step.error.get("type", "unknown")
if error_type not in error_categories:
error_categories[error_type] = []
error_categories[error_type].append({
"step_id": step.step_id,
"step_name": step.name,
"error_message": step.error.get("message", ""),
"timestamp": step.end_time,
"context_before": get_context_before_step(trace, step.step_id),
"context_after": get_context_after_step(trace, step.step_id)
})
# 分析错误趋势
error_trends = analyze_error_trends(error_categories)
# 生成修复建议
suggestions = generate_fix_suggestions(error_categories)
return {
"has_errors": True,
"error_count": len(error_steps),
"error_categories": error_categories,
"error_trends": error_trends,
"suggestions": suggestions
}
def generate_fix_suggestions(error_categories):
"""生成修复建议"""
suggestions = []
for error_type, errors in error_categories.items():
if error_type == "llm_api_error":
suggestions.append({
"type": "llm_api_error",
"suggestion": "考虑添加重试机制和降级策略",
"details": "LLM API可能会因各种原因失败,实现指数退避重试和备用模型可以提高系统的弹性"
})
elif error_type == "tool_execution_error":
tool_names = set(e.get("tool_name", "unknown") for e in errors)
suggestions.append({
"type": "tool_execution_error",
"suggestion": f"检查以下工具的实现: {', '.join(tool_names)}",
"details": "工具执行错误可能是由于参数验证不足、错误处理不完善或外部依赖问题"
})
# 更多错误类型的建议...
return suggestions
4.3.3 可视化设计
可视化是将数据转化为见解的关键。让我们探讨几种针对Agent可观测性的可视化设计:
推理链可视化
推理链可视化展示Agent从输入到输出的完整推理过程:
"如何提高代码质量?"] --> B[目标 ----------------------^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'
更多推荐

所有评论(0)