把流式输出接入终端或界面#

想观察一个请求的运行过程,有两种方式:为你关心的事件注册分发器,或者在需要取消或转发时自己消费这个流。

用回调做展示#

继承 AgentEventDispatcher,只覆写你想渲染的事件。通过 create_agent(..., event_dispatcher=...) 绑定它;client.run() 仍然返回最终答案。

class ConsoleEvents(AgentEventDispatcher):
    async def on_reasoning_delta_event(self, event: AgentEvent) -> None:
        print(f"Reasoning: {event.delta}")

    async def on_tool_started_event(self, event: AgentEvent) -> None:
        call = event.tool_calls[0]
        print(f"Tool: {call.name} {call.arguments}")

    async def on_tool_completed_event(self, event: AgentEvent) -> None:
        assert event.message is not None
        print(f"Result: {event.message.content}")

    async def on_text_delta_event(self, event: AgentEvent) -> None:
        print(event.delta)

有两条规则能让展示保持正确:

  • delta 是增量,而完成事件上的 message 是完整内容。把两者追加到同一个缓冲区会重复答案。

  • 工具参数可能以不完整的片段到达。只有完整的工具调用才会被执行,而且由运行时来执行,界面从不运行它们。

TOOL_STARTED 可能描述一批调用:请遍历 event.tool_calls,而不是假设只有一个调用。完成和失败事件标识那个已经结束的调用;由于并行调用可能乱序完成,请按 ID 关联它们。

自己消费流以获得控制力#

from contextlib import aclosing

async with aclosing(client.stream("Inspect the project")) as events:
    async for event in events:
        await send_to_ui(event)

对于一个简单的终端,只匹配每种事件相关的字段:

from zett_agent.events import AgentEventType

async with aclosing(client.stream("What is 20 plus 22?")) as events:
    async for event in events:
        if event.type == AgentEventType.TEXT_DELTA:
            print(event.delta, end="", flush=True)
        elif event.type == AgentEventType.TOOL_STARTED:
            for call in event.tool_calls:
                print(f"\nCalling {call.name} ({call.id})")
        elif event.type == AgentEventType.RUN_COMPLETED:
            final_message = event.message

client 必须已经配好模型和它需要的所有工具。最终消息才是权威答案,增量只用于展示。模型提供商可能只在最终响应中给出答案,因此界面应该以最终内容为准,而不是假设文本增量一定存在。如果绑定了分发器,那么在你收到每个事件之前,它已经处理过该事件。

即使提前跳出循环,也要用 aclosing:它会停止请求并等待清理。回调按顺序等待执行,因此慢回调会产生背压;请把工作交给有界的应用队列,而不是无限制地创建任务。

你会看到什么#

事件

含义

TEXT_DELTA

答案文本的一个片段。

REASONING_DELTA

模型推理内容的一个片段,前提是模型会公开这部分内容。

TOOL_STARTED / TOOL_COMPLETED / TOOL_FAILED

一次被请求的工具调用以及它的结束方式。

TOOL_CALL_DELTA

部分工具参数,仅用于展示进度。

COMPACTION_*

在下一步模型处理之前,较旧的上下文正在被摘要。

MODEL_COMPLETED

一次完整的模型响应,包括用量。

MODEL_STOPPED

模型没有正常结束:被拒绝、被截断、被中断,或最终停在暂停状态。它紧跟在 MODEL_COMPLETED 之后并携带 stop;参见 了解模型为什么停下。

RUN_COMPLETED

请求成功结束。

CUSTOM

你自己的扩展发出的事件。

提供商托管的工具会添加 SERVER_TOOL_* 事件;steering 和 internal continuation 会添加各自的事件对。完整列表见 AgentEventType。

跟踪模型用量#

在 MODEL_COMPLETED 上从 event.response.usage 读取用量。它描述的是一次模型处理,而不是整个用户请求。一次工具往返通常会产生两个或更多这类事件,因此要把它们的计数相加来衡量主循环:

tokens = 0
async with aclosing(client.stream("Explain the result.")) as events:
    async for event in events:
        if event.type == AgentEventType.MODEL_COMPLETED and event.response is not None:
            tokens += event.response.usage.total_tokens

total_tokens 是输入加输出;推理和缓存计数已经包含在内,不能再加一次。缺少用量并不能证明成本为零。独立的压缩过程或被委派的模型可能在这些主模型事件之外产生额外用量。记录的时间信息见 观察用量与耗时,缓存上报见 连接模型提供商。

失败就是异常#

RUN_COMPLETED 表示请求运行到了结尾,这与模型完成回答不是一回事:拒绝也会结束并完成一次请求。要判断这一点,请检查 MODEL_STOPPED 或 message.stop;参见 了解模型为什么停下。提供方错误、扩展错误和取消都会以异常传播,不会被转换成合成的完成事件。当 UI 需要展示失败时,请包装这个流,并参阅 取消并复用智能体 了解清理与复用。

下一步:把对话保存在 保存并恢复对话 中,或者阅读 三类事件,了解界面事件、内部通知和外部输入之间的区别。