观察用量与耗时#

记录 token 用量、提示词缓存命中和推理/内容的耗时,同时不必改变应用运行智能体的方式。模型给出的计数是固定值;该示例并不度量模型提供商的价格。

运行#

在仓库根目录下执行:

uv run python docs/_examples/observability.py

输出包含:

Reasoning and content boundaries recorded

这个程序在需要时使用确定性的离线模型和临时存储。它不需要 API 密钥,并由文档测试套件执行。

记录完成后的消息#

如果你想同时拿到最终确定的消息、用量和耗时,而不是一个个显示增量,就使用扩展:

class UsageObserver(AgentExtension):
    """Retain immutable observations for a demo; use a bounded sink in production."""

    def __init__(self) -> None:
        self.records: list[tuple[str, MessageAppendedEvent]] = []

    async def on_event(self, context: AgentRunContext, event: ExtensionEvent) -> None:
        if isinstance(event, MessageAppendedEvent) and event.usage is not None:
            self.records.append((context.session_id, event))

该示例把记录保存在列表中,并检查 60% 的缓存命中率。对于长期运行的服务,应把记录发送到有界队列或遥测接收端,而不是在内存中保留一个无界列表。上报的缓存用量取决于模型提供商;缺少用量并不等于实测的零。

完整源码#

下载 observability.py。

 1"""Observe persisted-message timing and provider usage without changing the loop."""
 2
 3import asyncio
 4from collections.abc import AsyncIterator
 5
 6from zett_agent.agent import AgentRunContext
 7from zett_agent.client import create_agent
 8from zett_agent.extensions.base import AgentExtension
 9from zett_agent.extensions.events import (
10    ExtensionEvent,
11    MessageAppendedEvent,
12)
13from zett_agent.messages import AssistantMessage
14from zett_agent.model import (
15    ModelEvent,
16    ModelRequest,
17    ModelResponse,
18    ModelUsage,
19    RetryOptions,
20)
21
22
23class UsageObserver(AgentExtension):
24    """Retain immutable observations for a demo; use a bounded sink in production."""
25
26    def __init__(self) -> None:
27        self.records: list[tuple[str, MessageAppendedEvent]] = []
28
29    async def on_event(self, context: AgentRunContext, event: ExtensionEvent) -> None:
30        if isinstance(event, MessageAppendedEvent) and event.usage is not None:
31            self.records.append((context.session_id, event))
32
33
34class UsageModel:
35    retry = RetryOptions(max_retries=0)
36
37    async def stream(self, request: ModelRequest) -> AsyncIterator[ModelEvent]:
38        yield ModelEvent.reasoning("Inspect the request.")
39        await asyncio.sleep(0)
40        yield ModelEvent.text("Done")
41        yield ModelEvent.completed(
42            ModelResponse(
43                AssistantMessage(content="Done", reasoning="Inspect the request."),
44                usage=ModelUsage(input_tokens=100, output_tokens=20, cache_read_tokens=60, reasoning_tokens=5),
45            )
46        )
47
48
49async def main() -> None:
50    observer = UsageObserver()
51    client = await create_agent(UsageModel(), extensions=[observer])
52    await client.run("Inspect usage")
53    _, record = observer.records[0]
54    assert record.usage is not None
55    assert record.timing.duration_ns >= 0
56    assert record.timing.reasoning_duration_ns is not None
57    assert record.timing.content_duration_ns is not None
58    assert record.usage.total_tokens == 120
59    assert record.usage.cache_hit_rate == 0.6
60    print("Tokens: 120; cache hit: 60%")
61    print("Reasoning and content boundaries recorded")
62
63
64if __name__ == "__main__":
65    asyncio.run(main())

下一步:提示词缓存见连接模型提供商,在显示事件和生命周期事件之间做选择见三类事件。