第 17 期 | 日志系统与可观测性实践

更新于 2026/4/15

好的,作为技术教育专家,我将为你撰写 Hermes Agent 教程的第 17 期文章。


日志系统与可观测性实践

副标题:揭秘暗箱——对话轨迹回放、技能追踪和性能分析。集成专业可观测平台提升可控度。


学习目标

在本期课程中,你将深入 Hermes Agent 的内部运作,学习如何从一个“黑箱”使用者转变为一个能洞察其内部状态的“白箱”开发者。完成本章后,你将能够:

  1. 掌握 Hermes Agent 的内置日志系统:理解其日志结构、级别和配置,并利用日志进行基础的故障排查。
  2. 实现对话轨迹回放与技能追踪:通过精细化日志分析,精确复现 Agent 的每一次决策路径,诊断问题根源。
  3. 进行基础性能分析:从日志中提取关键性能指标(Metrics),如响应延迟、Token 消耗等,量化 Agent 的表现。
  4. 集成专业可观测性平台:学习如何将 Hermes Agent 与 OpenTelemetry (OTel) 生态集成,实现分布式追踪 (Tracing),将可观测性提升到工业级水平。

核心概念讲解

在复杂的 AI Agent 应用中,当用户报告“它没有按我说的做”或“它反应很慢”时,我们面临的最大挑战是不可见性。Agent 的决策链条(接收信息 -> 检索记忆 -> 思考 -> 选择技能 -> 调用 LLM -> 执行技能 -> 生成回复)隐藏在代码执行的背后。可观测性 (Observability) 正是解决这一问题的关键。它不仅仅是“监控 (Monitoring)”,监控告诉我们系统是否在工作,而可观测性则让我们能提出任意问题,并从系统产生的数据中找到为什么的答案。

可观测性通常建立在三大支柱之上:

  1. 日志 (Logging)

    • 是什么:记录离散的、带有时间戳的事件。每一行日志都是一个事实的快照,例如“用户输入了消息”、“Skill get_weather 被调用”、“LLM API 返回错误”。
    • 在 Hermes 中的体现:Hermes Agent 使用 Loguru 库提供强大且易于配置的日志功能。默认情况下,它会记录核心组件的运行状态、收到的消息、执行的技能等关键信息。通过配置,我们可以输出结构化日志 (Structured Logging),通常是 JSON 格式,这使得日志可以被机器轻松解析、查询和分析。
  2. 追踪 (Tracing)

    • 是什么:记录单个请求(在 Agent 场景下,即一次完整的对话交互)在系统中流经不同服务或组件的完整路径。一个 Trace 由多个 Span 组成,每个 Span 代表一个工作单元(如一次函数调用、一次 API 请求)。
    • 为什么重要:对于 Agent 而言,一次对话交互可能涉及多个内部环节:消息解析、记忆查询、LLM 推理、工具(技能)调用等。分布式追踪能将这些环节串联起来,形成一个完整的调用链。这让我们能清晰地看到:
      • 对话轨迹回放:请求的完整生命周期是怎样的?
      • 技能追踪:哪个 Skill 被调用了?传入了什么参数?执行了多久?
      • 性能瓶颈定位:整个交互过程中,是 LLM 调用慢,还是某个 Skill 的外部 API 调用慢?
  3. 指标 (Metrics)

    • 是什么:在一段时间内聚合的、可量化的数据。例如:每分钟处理的消息数、平均响应延迟、LLM API 调用成功率、Token 消耗总数等。
    • 为什么重要:指标为我们提供了系统健康状况的宏观视图。通过观察指标的变化趋势,我们可以设置告警(例如,当延迟超过 2 秒时),进行容量规划,或评估模型/技能变更带来的性能影响。

本期教程将从最基础的日志实践开始,逐步引导你进入使用 OpenTelemetry 实现高级追踪的工业级实践。

💻 实战演示

Part 1: 玩转 Hermes Agent 内置日志系统

Hermes Agent 的日志配置位于项目根目录的 config.yml 文件中。让我们从这里开始。

1.1 日志级别与格式调整

默认配置可能如下:

# config.yml
log:
  level: "INFO"
  path: "hermes.log"
  rotation: "10 MB"
  retention: "7 days"
  • level: 日志的详细程度。常见的级别有 DEBUG, INFO, WARNING, ERROR, CRITICAL
    • INFO: 默认级别,记录常规操作信息。
    • DEBUG: 最详细的级别,用于开发和调试。它会打印出 LLM 的完整 Prompt、模型的思考过程(Thought Process)等海量信息。

实战操作:开启 DEBUG 模式

为了深入观察 Agent 的“思考过程”,我们将日志级别调整为 DEBUG

# config.yml
log:
  level: "DEBUG"
  # ... 其他配置保持不变

现在,启动 Hermes Agent 并与之交互:

# 确保你在 Hermes Agent 项目的根目录
hermes run

在另一个终端或通过消息网关(如 Telegram)向 Agent 发送一个需要调用 Skill 的指令,例如:“今天北京天气怎么样?”

然后,查看日志文件 hermes.log

tail -f hermes.log

你会看到比 INFO 级别详细得多的输出,其中包含关键的调试信息:

...
DEBUG    | hermes.core.agent:process_message:123 - Received new message from user: "今天北京天气怎么样?"
DEBUG    | hermes.core.brain:think:45 - Generating thought process for the user query...
DEBUG    | hermes.core.brain:_construct_prompt:67 - Constructed prompt for LLM:
"""
... (这里会显示完整的、发送给 LLM 的 Prompt) ...
"""
DEBUG    | hermes.providers.openai:chat_completion:89 - Calling OpenAI API with model gpt-4-turbo...
INFO     | hermes.core.agent:execute_skill:234 - Agent decided to use skill: get_weather
DEBUG    | hermes.core.agent:execute_skill:235 - Skill arguments: {'city': '北京'}
INFO     | hermes.skills.weather:get_weather:56 - Executing get_weather skill for city: 北京
... (技能执行的内部日志) ...
DEBUG    | hermes.core.agent:execute_skill:250 - Skill `get_weather` returned: "北京今天晴,气温25度..."
DEBUG    | hermes.core.brain:think:90 - LLM call took 1.234 seconds.
INFO     | hermes.core.agent:process_message:150 - Sending final response to user.
...

通过 DEBUG 日志,我们清晰地完成了:

  • 对话轨迹回放:从收到消息到最终回复的每一步都记录在案。
  • 技能追踪:我们看到了 get_weather 被调用,以及传入的参数 {'city': '北京'}
  • 性能分析:我们看到了 LLM 调用的耗时 1.234 seconds
1.2 启用结构化日志 (JSON)

对于自动化分析,纯文本日志难以处理。结构化日志是更好的选择。

实战操作:切换到 JSON 格式

修改 config.yml

# config.yml
log:
  level: "DEBUG"
  path: "hermes.log"
  rotation: "10 MB"
  retention: "7 days"
  json: true  # 新增此行

重启 Hermes Agent 并再次交互。现在查看 hermes.log,内容会变成这样:

{"text": "...", "record": {"elapsed": {"repr": "...", "seconds": ...}, "exception": null, "extra": {}, "file": {"name": "agent.py", "path": "..."}, "function": "process_message", "level": {"icon": "...", "name": "DEBUG", "no": 10}, "line": 123, "message": "Received new message from user: \"今天北京天气怎么样?\"", "module": "agent", "name": "hermes.core.agent", "process": {"id": ..., "name": "..."}, "thread": {"id": ..., "name": "..."}, "time": {"repr": "...", "timestamp": ...}}}

虽然可读性变差,但现在我们可以用 jq 等工具轻松地查询和分析日志。

实战操作:用 jq 查询所有技能调用

# 筛选出记录技能执行的日志,并提取技能名称和参数
cat hermes.log | jq 'select(.record.message | test("Agent decided to use skill:")) | .record.extra'

这将输出类似这样的结果,非常适合脚本处理:

{
  "skill_name": "get_weather",
  "skill_args": {
    "city": "北京"
  }
}

Part 2: 集成 OpenTelemetry 实现高级追踪

日志分析虽然强大,但在复杂、高并发的场景下,手动关联日志来构建一个完整的请求链路非常困难。这正是 OpenTelemetry (OTel) 的用武之地。我们将使用 OTel 将 Hermes Agent 的内部调用转换为可视化的分布式追踪数据。

我们将集成一个开源的 OTel 后端 SigNoz(你也可以使用 Jaeger、Zipkin 等)。SigNoz 的优势是它将日志、追踪和指标整合在一个平台。

2.1 部署 SigNoz

我们将使用 Docker Compose 在本地快速部署一个 SigNoz 实例。

创建一个 docker-compose.yml 文件:

# docker-compose.yml for SigNoz
version: '3'
services:
  signoz-otel-collector:
    image: signoz/otel-collector:0.93.0
    command: ["--config=/etc/otel-collector-config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
    ports:
      - "4317:4317"  # OTLP gRPC receiver
      - "4318:4318"  # OTLP HTTP receiver
    depends_on:
      - signoz-clickhouse

  signoz-query-service:
    image: signoz/query-service:0.41.0
    ports:
      - "8080:8080"
    command: [ "-config=/etc/query-service/config.yaml" ]
    volumes:
      - ./query-service-config.yaml:/etc/query-service/config.yaml
    depends_on:
      - signoz-clickhouse

  signoz-clickhouse:
    image: clickhouse/clickhouse-server:23.10.2-alpine
    ports:
      - "8123:8123"
      - "9000:9000"
    volumes:
      - signoz-clickhouse-data:/var/lib/clickhouse
    healthcheck:
      test: ["CMD", "clickhouse-client", "-q", "SELECT 1"]
      interval: 30s
      timeout: 10s
      retries: 5

  signoz-frontend:
    image: signoz/frontend:0.41.0
    ports:
      - "3301:3000"
    depends_on:
      - signoz-query-service

volumes:
  signoz-clickhouse-data:

同时,你还需要 otel-collector-config.yamlquery-service-config.yaml。为简化起见,你可以从 SigNoz 官方文档 获取这些配置文件。

下载配置文件后,启动 SigNoz:

docker-compose up -d

等待几分钟,然后访问 http://localhost:3301,你应该能看到 SigNoz 的 UI。

2.2 为 Hermes Agent 安装 OTel 依赖

在你的 Hermes Agent Python 环境中,安装必要的 OTel 库。

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp
2.3 在 Hermes Agent 中植入追踪代码

现在,我们需要修改 Hermes Agent 的代码,让它在关键路径上创建和管理 Span,并将数据发送到 SigNoz。

创建一个新的 Python 文件,例如 hermes/utils/telemetry.py

# hermes/utils/telemetry.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource

def setup_tracer(service_name="hermes-agent"):
    """初始化 OpenTelemetry Tracer"""
    resource = Resource(attributes={
        "service.name": service_name
    })

    # 配置 OTLP Exporter,将数据发送到 SigNoz Collector
    # 默认地址是 localhost:4317,与我们的 docker-compose 配置匹配
    otlp_exporter = OTLPSpanExporter(endpoint="localhost:4317", insecure=True)

    # 设置 Tracer Provider 和 Span Processor
    provider = TracerProvider(resource=resource)
    provider.add_span_processor(BatchSpanProcessor(otlp_exporter))

    # 设置全局 Tracer Provider
    trace.set_tracer_provider(provider)

    # 返回一个 tracer 实例
    return trace.get_tracer(__name__)

# 在模块级别初始化一次
tracer = setup_tracer()

现在,我们可以在 Agent 的核心逻辑中“植入”追踪代码。一个理想的植入点是处理用户消息的主函数 Agent.process_message

修改 hermes/core/agent.py

# hermes/core/agent.py
# ... 其他 import ...
from hermes.utils.telemetry import tracer  # 导入我们创建的 tracer

class Agent:
    # ... 其他方法 ...

    async def process_message(self, message: str, user_id: str):
        # 创建一个顶层 Span,代表整个对话交互
        with tracer.start_as_current_span("process_user_message") as parent_span:
            # 为 Span 添加有用的属性 (Attributes),便于查询
            parent_span.set_attribute("user.id", user_id)
            parent_span.set_attribute("user.message", message)

            try:
                # ... 原有的 process_message 逻辑 ...

                # 1. 记忆检索
                with tracer.start_as_current_span("memory_retrieval") as memory_span:
                    relevant_memory = self.memory.retrieve(user_id, message)
                    memory_span.set_attribute("memory.found_count", len(relevant_memory))
                
                # 2. 思考与技能选择
                with tracer.start_as_current_span("brain_think") as brain_span:
                    thought_process, skill_name, skill_args = await self.brain.think(message, user_id)
                    brain_span.set_attribute("llm.thought", thought_process)
                    brain_span.set_attribute("skill.selected", skill_name or "None")

                # 3. 技能执行
                if skill_name:
                    with tracer.start_as_current_span("skill_execution") as skill_span:
                        skill_span.set_attribute("skill.name", skill_name)
                        skill_span.set_attribute("skill.args", str(skill_args))
                        
                        skill_result = await self.execute_skill(skill_name, **skill_args)
                        
                        skill_span.set_attribute("skill.result_length", len(str(skill_result)))
                        # 如果技能执行出错,可以记录异常
                        # skill_span.record_exception(e)
                        # skill_span.set_status(trace.Status(trace.StatusCode.ERROR, "Skill execution failed"))
                
                # ... 后续生成最终回复的逻辑 ...

                final_response = "..." # 假设这是最终回复
                parent_span.set_attribute("agent.response", final_response)
                
                return final_response

            except Exception as e:
                # 如果整个过程发生异常,在顶层 Span 中记录
                parent_span.record_exception(e)
                parent_span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
                raise

最后,在 Hermes Agent 的主入口 main.py__main__.py 中,确保 OTel 初始化代码被调用。

# 在应用启动时调用
from hermes.utils.telemetry import setup_tracer
setup_tracer()

# ... 后续的启动代码 ...
2.4 分析追踪数据

重启 Hermes Agent,并再次与之交互。然后打开 SigNoz UI (http://localhost:3301)。

  1. 点击左侧的 "Traces" 标签页。
  2. 你应该能看到名为 hermes-agent 的服务。
  3. 在列表中,你会看到名为 process_user_message 的 Trace。点击其中一个。

你将看到一个火焰图 (Flame Graph)甘特图 (Gantt Chart),它完美地可视化了整个对话处理流程:

process_user_message -------------------------------------------------- [2500ms]
  |
  |- memory_retrieval --- [50ms]
  |
  |- brain_think --------------------------------------- [1500ms]
  |
  |- skill_execution (get_weather) -------------------- [800ms]
  |
  |- ... (response_generation) ... --- [150ms]

通过这个视图,你可以:

  • 一目了然地回放轨迹:清晰地看到代码执行的顺序和父子关系。
  • 精确定位性能瓶颈:上图中,brain_think (LLM 调用) 和 skill_execution 是最耗时的部分。如果 skill_execution 耗时异常,你可以进一步下钻,查看是哪个外部 API 调用慢了。
  • 深入分析细节:点击任何一个 Span(如 skill_execution),在右侧的详情面板中,你可以看到我们设置的所有属性,如 skill.nameskill.args 等,这对于调试至关重要。

至此,你已经将 Hermes Agent 的可观测性从“文本日志”时代带入了“分布式追踪”时代,极大地提升了对这个智能体的理解和控制能力。

涉及命令

# 查看实时日志
tail -f hermes.log

# 使用 jq 查询结构化日志
cat hermes.log | jq '...'

# 启动 SigNoz (或其他 OTel 后端)
docker-compose up -d

# 安装 OpenTelemetry Python 库
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp

# 启动 Hermes Agent
hermes run

要点回顾

  1. 可观测性三支柱:日志 (Logging)、追踪 (Tracing) 和指标 (Metrics) 是理解和调试复杂系统的基石。
  2. Hermes 内置日志:通过调整 config.yml 中的 log.level (尤其是 DEBUG) 和 log.json,可以获得丰富的诊断信息,并为自动化分析做好准备。
  3. 日志是基础DEBUG 级别的日志可以让你手动完成对话轨迹回放和技能追踪,是排查问题的首选工具。
  4. 追踪是进阶:当系统变得复杂或需要精确定位性能瓶颈时,集成 OpenTelemetry 进行分布式追踪是最佳实践。
  5. Span 与 Attributes:OTel 的核心是创建代表工作单元的 Span,并为其附加丰富的上下文信息 (Attributes),这使得追踪数据具有极高的分析价值。
  6. 从黑箱到白箱:善用可观测性工具,能将 Agent 的内部决策过程透明化,让你从一个被动的用户,转变为一个主动的、能够深度掌控和优化 Agent 的开发者。

参考资料