Agent 工程体系 · 第 82/98 篇。内容以 2026 年 9 月可验证的公开规范和稳定接口为基线;框架版本敏感能力会明确标注,不把实验行为写成通用保证。

Agent 可观测性:Trace、Span、模型回合、工具调用、Token 和关联 ID

Agent 的一次请求,通常不是一次模型调用,而是一条动态执行链:

  1. 接收用户请求;
  2. 选择或创建 Agent;
  3. 调用模型;
  4. 解析模型输出;
  5. 调用工具、检索系统或子 Agent;
  6. 将工具结果重新交给模型;
  7. 继续循环,直到得到终态、触发限制或失败。

如果系统只记录最终回答,工程师只能知道“结果是什么”,却不知道:

  • 模型为什么选择了这个工具;
  • 工具参数是模型生成的,还是服务端补全的;
  • 哪一次模型调用消耗了最多 Token;
  • 延迟发生在模型、工具、排队还是重试;
  • 工具失败后 Agent 是否正确恢复;
  • 多个并发任务、重试请求和异步回调是否属于同一业务请求。

Agent 可观测性的核心,不是“多打日志”,而是把一次执行表示成一棵带有时间、父子关系、输入输出和状态的轨迹树。OpenAI Agents SDK 将一次工作流表示为 Trace,将其中的具体操作表示为 Span;Trace 可以覆盖模型生成、工具调用、交接、Guardrail 和自定义事件。(openai.github.io)


一、先建立执行模型:Agent 不是函数,而是状态机

定义一次 Agent 运行:

R=(x0,S0,π,E,xf)R = (x_0, S_0, \pi, E, x_f)

其中:

  • x0x_0:用户输入;
  • S0S_0:初始状态,包括会话、权限、工具、上下文等;
  • π\pi:Agent 的决策策略;
  • EE:外部环境,包括模型服务、工具服务、数据库和网络;
  • xfx_f:最终输出或失败终态。

在第 kk 次循环中,Agent 根据当前状态 SkS_k 生成模型请求:

mk=ModelRequest(Sk)m_k = \text{ModelRequest}(S_k)

模型返回:

yk=Model(mk)y_k = \text{Model}(m_k)

如果 yky_k 是工具调用,则执行:

ok=Tool(yk)o_k = \text{Tool}(y_k)

并更新状态:

Sk+1=Update(Sk,yk,ok)S_{k+1} = \text{Update}(S_k, y_k, o_k)

如果 yky_k 是最终答案,则运行进入终态;如果发生异常,则进入错误、恢复、回滚或人工接管路径。

这个模型直接决定了可观测性应该记录什么:

  • Trace 记录整个 RR
  • Span 记录 ModelRequestModelToolUpdate 等可定位的操作;
  • 模型回合记录一次循环 kk
  • Token 记录模型请求和输出的资源消耗;
  • 关联 ID 记录这些事件如何属于同一棵执行树。

二、Trace:一次端到端工作流

2.1 Trace 的定义

Trace 是一次完整工作流的端到端记录。它通常从业务请求进入开始,在最终回答、失败、超时或取消时结束。

可以把 Trace 看成一个容器:

Trace
└── Span
    ├── Span
    ├── Span
    └── Span

在 OpenAI Agents SDK 中,Trace 具有以下关键属性:

  • workflow_name:逻辑工作流名称;
  • trace_id:Trace 的唯一标识;
  • group_id:用于把同一会话或流程中的多条 Trace 归组;
  • metadata:附加业务元数据;
  • disabled:是否禁用记录。

SDK 文档规定,自动生成的 trace_id 应符合 trace_<32_alphanumeric> 格式;group_id 可以用于关联同一对话中的多个 Trace。(openai.github.io)

需要区分:

  • 一次用户发送消息,通常对应一次 Trace;
  • 一个长期会话,可以包含多个 Trace;
  • 一个重试请求,可能是新的 Trace,但应通过业务 ID 或 group_id 关联;
  • 一个异步任务,可以有独立 Trace,但不能丢失其来源请求的关联信息。

例如:

conversation_id = conv_42
request_id       = req_20260901_001
trace_id         = trace_abc...

第一轮用户消息:
  group_id = conv_42
  trace_id = trace_abc...

第二轮用户消息:
  group_id = conv_42
  trace_id = trace_def...

这里两个 Trace 不应强行合并,否则单轮延迟、Token 和终态会混在一起;但它们应能通过 group_id 或业务字段被查询到同一会话下。

2.2 Trace 的开始和结束

Trace 必须有明确生命周期:

from agents import Agent, Runner, trace

agent = Agent(
    name="订单助手",
    instructions="帮助用户查询订单状态。",
)

async def handle_request(user_text: str):
    with trace(
        "order_support",
        group_id="conversation_42",
        metadata={
            "request_id": "req_20260901_001",
            "tenant_id": "tenant_demo",
        },
    ):
        result = await Runner.run(agent, user_text)
        return result.final_output

with trace(...) 的价值不只是语法简洁:它能保证 Trace 在代码块退出时结束,包括异常退出路径。SDK 也支持手动调用 start()finish(),但手动模式必须自行保证所有异常路径都执行结束逻辑。(openai.github.io)

错误处理应记录“失败发生在哪里”,而不是只在最外层打印异常:

async def handle_request(user_text: str):
    try:
        with trace(
            "order_support",
            group_id="conversation_42",
            metadata={"request_id": "req_20260901_001"},
        ):
            result = await Runner.run(agent, user_text)
            return result.final_output
    except TimeoutError:
        # Trace 已经因为上下文退出而结束
        # 这里负责业务层响应和告警
        raise

2.3 长时间运行任务的导出风险

Tracing 通常不是同步写入远端,而是先进入内存队列,再批量导出。Agents SDK 的默认 BatchTraceProcessor 会在后台批量发送,并在进程退出时尝试最终刷新;因此长时间运行的 Worker 中,任务已经结束但 Trace 尚未立即出现在后台是可能的。若一个任务结束就必须保证当前 Trace 被导出,应在 Trace 上下文退出后调用 flush_traces()。(openai.github.io)

from agents import Runner, flush_traces, trace

def process_job(prompt: str):
    try:
        with trace("background_job"):
            result = Runner.run_sync(agent, prompt)
            return result.final_output
    finally:
        # 必须放在 with trace(...) 之后
        flush_traces()

顺序不能反过来:

# 不推荐
flush_traces()
with trace("background_job"):
    ...

因为此时 Trace 还没有结束,部分 Span 可能仍处于打开状态,刷新动作无法表达完整的终态。


三、Span:Trace 中可定位的操作区间

3.1 Span 的定义

Span 是一个有开始时间和结束时间的操作区间。一个 Span 至少应能回答:

  • 操作是什么;
  • 何时开始;
  • 何时结束;
  • 属于哪个 Trace;
  • 父操作是什么;
  • 输入、输出和错误状态是什么。

可抽象为:

Span=(span_id,trace_id,parent_id,tstart,tend,data,status)\text{Span} = (\text{span\_id}, \text{trace\_id}, \text{parent\_id}, t_\text{start}, t_\text{end}, \text{data}, \text{status})

Span 耗时为:

Δt=tendtstart\Delta t = t_\text{end} - t_\text{start}

但要注意:父 Span 的耗时不能简单等于所有子 Span 耗时之和。并发工具调用可能重叠,网络排队和本地处理也可能不属于任何子 Span。

3.2 常见 Span 类型

Agent 系统中,至少应区分以下操作:

Span 类型 表示内容 典型问题
Agent Span 某个 Agent 的逻辑作用域 是否发生了错误交接
Task Span 一次顶层 Runner 调用 一次运行整体耗时和终态
Turn Span Agent 循环中的一次回合 哪个回合发生了重复或发散
Generation Span 一次模型生成 模型、配置、输入输出和 Token
Function Span 一次函数或工具调用 参数、结果、异常和耗时
Handoff Span 控制权从一个 Agent 转移到另一个 Agent 路由是否正确
Guardrail Span 输入或输出安全检查 哪个检查阻断了流程
Custom Span 业务自定义操作 数据库、缓存、回滚、人工审批

Agents SDK 当前文档明确区分了 task_spanturn_spanfunction_spangeneration_span 等 Span 创建接口。(openai.github.io)

3.3 父子关系不是装饰,而是诊断依据

一次典型轨迹可以表示为:

Trace: 客服请求
└── Task Span: Runner.run
    └── Agent Span: 客服 Agent
        ├── Turn Span: turn=1
        │   └── Generation Span: 识别意图
        ├── Turn Span: turn=2
        │   ├── Generation Span: 生成工具调用
        │   └── Function Span: query_order
        └── Turn Span: turn=3
            └── Generation Span: 生成最终回答

由此可以定位不同类别的故障:

  • Generation Span 成功、Function Span 参数错误:模型决策或 Schema 问题;
  • 工具返回成功、下一次 Generation Span 仍重复调用:状态更新或工具结果注入问题;
  • Function Span 很快开始但长时间不结束:工具超时或下游阻塞;
  • 多个工具 Span 并行且总耗时接近最慢者:并发执行有效;
  • 父 Span 结束但子 Span 未结束:生命周期管理或取消传播有缺陷。

四、模型回合:Turn 不等于模型调用

4.1 模型回合的定义

模型回合,这里指 Agent 循环的一次迭代,通常称为 turn

Turnk:Sk模型请求模型输出工具执行或终态判断\text{Turn}_k: S_k \rightarrow \text{模型请求} \rightarrow \text{模型输出} \rightarrow \text{工具执行或终态判断}

OpenAI Agents SDK 将 turn_span 定义为“一次 Agent loop turn”。(openai.github.io)

一个回合可能包含:

  1. 构造上下文;
  2. 一次或多次模型请求;
  3. 解析工具调用;
  4. 并行执行多个工具;
  5. 把工具结果写回上下文;
  6. 判断是否继续。

因此必须区分:

  • Turn:Agent 的一次循环;
  • Generation:一次具体模型生成;
  • Tool Call:一次具体工具执行。

4.2 完整算例

用户请求:

查询订单 1001 的物流状态,如果已签收,告诉我签收时间。

可能产生以下轨迹:

Turn 1
└── Generation 1
    输入:用户问题 + 工具定义
    输出:调用 query_order(order_id="1001")

    Function 1
    输入:{"order_id": "1001"}
    输出:{"status": "shipped", "tracking_no": "SF123"}

Turn 2
└── Generation 2
    输入:用户问题 + query_order 结果 + 工具定义
    输出:调用 query_logistics(tracking_no="SF123")

    Function 2
    输入:{"tracking_no": "SF123"}
    输出:{"status": "delivered", "delivered_at": "2026-09-01T10:20:00+08:00"}

Turn 3
└── Generation 3
    输入:上述全部上下文
    输出:最终回答

这里有三个 Turn、三个 Generation 和两个工具调用。若监控只统计“模型请求次数”,会漏掉 Agent 实际经过了几个决策回合;若只统计“回合数”,又无法判断某个回合内部是否进行了多次模型请求。

4.3 不能把回合数当作质量指标

较少的 Turn 不一定更好:

方案 A:
Turn 1 -> 错误工具 -> Turn 2 -> 重试 -> Turn 3 -> 终态
方案 B:
Turn 1 -> 正确工具 -> Turn 2 -> 终态

方案 B 通常更高效,但如果方案 A 在第一次工具失败后正确恢复,而方案 B 直接返回错误,单纯比较 Turn 数会得出错误结论。

因此应同时记录:

  • turn_index
  • 每个 Turn 的模型调用数;
  • 每个 Turn 的工具调用数;
  • Turn 是否产生有效状态变化;
  • Turn 结束原因;
  • 累计 Token;
  • 是否发生重试、回滚或人工接管。

一个有用的效率指标是:

E=有效状态变化数Turn 数E = \frac{\text{有效状态变化数}}{\text{Turn 数}}

若连续两个 Turn 的工具集合、参数和状态摘要都不变,则可能发生循环:

Statek+1Statek\text{State}_{k+1} \approx \text{State}_k

这比单纯设置最大回合数更有诊断价值。


五、模型调用与工具调用:必须记录“意图”和“事实”

5.1 模型调用记录什么

一次模型生成至少需要记录:

{
  "span_type": "generation",
  "model": "model-name",
  "model_config": {
    "temperature": 0,
    "max_output_tokens": 800
  },
  "input_message_count": 8,
  "output_item_types": ["tool_call"],
  "usage": {
    "input_tokens": 2300,
    "output_tokens": 94,
    "total_tokens": 2394
  },
  "status": "ok"
}

Agents SDK 的 GenerationSpanData 包含模型输入、输出、模型名称、模型配置和 usage 数据。(openai.github.io)

生产环境不一定要保存完整 Prompt 和完整输出。可以采用分层策略:

  • 调试环境:保存完整内容;
  • 生产默认:保存摘要、哈希、消息类型和大小;
  • 高风险字段:脱敏或不采集;
  • 事故样本:在访问控制下保留完整证据。

SDK 文档特别提醒,Generation Span 会保存模型输入输出,Function Span 会保存函数输入输出,这些内容可能包含敏感数据;可通过 trace_include_sensitive_data 关闭敏感数据采集,且该选项默认开启。(openai.github.io)

5.2 工具调用记录什么

工具 Span 不能只记录:

query_order succeeded

至少应记录以下字段:

{
  "span_type": "function",
  "tool_name": "query_order",
  "call_id": "call_01",
  "arguments": {
    "order_id": "1001"
  },
  "argument_source": {
    "order_id": "model"
  },
  "downstream_request_id": "order-api-req-7788",
  "result_summary": {
    "status": "shipped",
    "tracking_no_present": true
  },
  "status": "ok",
  "duration_ms": 86
}

其中 argument_source 很重要。假设模型生成:

{"order_id": "1001"}

服务端却把它改成了当前登录用户的订单:

{"order_id": "1002"}

如果只记录最终发给下游的参数,就无法判断错误来自模型、参数校验、权限过滤还是业务重写。

对写操作还必须记录副作用状态:

{
  "side_effect": "payment_refund",
  "idempotency_key": "refund-order-1001",
  "authorization": "approved",
  "commit_state": "committed",
  "compensation_state": "not_needed"
}

工具调用的成功至少有三层含义:

  1. 调用请求被发出;
  2. 下游返回了成功响应;
  3. Agent 状态正确吸收了结果。

第三层失败时,工具和 HTTP 都可能显示成功,但最终 Agent 仍然错误。


六、Token:资源计量,不是质量计量

6.1 Token 的基本字段

对一次模型请求,常见字段包括:

  • input_tokens:输入 Token;
  • output_tokens:输出 Token;
  • total_tokens:输入与输出的总数;
  • cached_input_tokens:命中缓存的输入 Token;
  • reasoning_tokens:如果模型或适配器提供,表示推理相关输出消耗;
  • requests:模型请求次数。

Agents SDK 的 Usage 对象区分单次请求与运行累计值;其中输入、输出和总 Token 可以按请求记录,也可以在一次运行中聚合。(openai.github.io)

单个模型调用:

Ti=Ii+OiT_i = I_i + O_i

一次 Agent 运行:

Trun=i=1n(Ii+Oi)T_\text{run} = \sum_{i=1}^{n} (I_i + O_i)

其中:

  • IiI_i:第 ii 次模型请求的输入 Token;
  • OiO_i:第 ii 次模型请求的输出 Token;
  • nn:模型请求次数。

不要把一次模型调用的 Token 与一次用户请求的 Token 混为一谈。Agent 可能经过多个 Turn,因此:

一次用户请求
├── Generation 1: 1,200 input + 80 output
├── Generation 2: 2,100 input + 120 output
└── Generation 3: 2,900 input + 160 output

总 Token 是三次 Generation 的和,而不是最后一次调用的 usage。

6.2 为什么输入 Token 往往越来越大

工具结果会被追加到上下文:

Ik+1=Ik+Δtool+ΔhistoryI_{k+1} = I_k + \Delta_\text{tool} + \Delta_\text{history}

因此即使每次模型输出都很短,后续输入 Token 仍可能增长。一个常见的异常轨迹是:

Turn 1: input=1,000
Turn 2: input=2,400
Turn 3: input=5,100
Turn 4: input=9,800

这可能来自:

  • 工具返回了完整原始文档;
  • 每轮重复注入工具定义;
  • 历史消息未压缩;
  • 失败重试不断追加错误上下文;
  • 子 Agent 输出未进行边界控制。

诊断时应把 Token 与上下文来源关联起来,而不是只看总量:

{
  "input_tokens": 5100,
  "context_breakdown": {
    "system_prompt": 900,
    "conversation_history": 1800,
    "tool_definitions": 700,
    "tool_results": 1500,
    "current_user_input": 200
  }
}

这个拆分不是模型服务必然提供的字段,而是应用层建议记录的审计信息。它可以通过消息来源标签、序列化前统计或上下文构造器实现。

6.3 Token 与费用、延迟和质量的关系

Token 通常影响成本和延迟,但不是简单的一一对应关系:

Cost=i(Iipiin+Oipiout)\text{Cost} = \sum_i (I_i \cdot p^\text{in}_i + O_i \cdot p^\text{out}_i)

其中 piinp^\text{in}_ipioutp^\text{out}_i 是对应模型和计费类别的单价。

但以下情况会破坏简单估算:

  • 输入缓存和非缓存输入价格不同;
  • 不同模型价格不同;
  • 推理 Token 可能单独计量;
  • 工具耗时可能远大于模型耗时;
  • 并发工具调用会改变总延迟但不一定改变 Token。

因此生产面板至少应按以下维度聚合:

workflow_name
model
tenant_id
trace_status
tool_name
turn_count
model_request_count
input_tokens
output_tokens
cached_input_tokens
latency

七、关联 ID:让日志、Trace、工具和业务记录连成一条证据链

7.1 不同 ID 的职责

一个成熟系统通常需要多个 ID:

ID 作用 生命周期
trace_id 标识一次 Agent 工作流 一次端到端运行
span_id 标识一个操作区间 一个 Span
parent_id 指向父 Span 当前 Span 的结构关系
group_id 关联同一会话或长期流程 多次 Trace
request_id 标识一次业务入口请求 一次 HTTP、RPC 或消息消费
tool_call_id 标识一次模型发起的工具调用 一次工具意图
downstream_request_id 标识下游服务请求 一次下游调用
idempotency_key 防止副作用重复提交 一次业务操作

这些 ID 不能互相替代。

例如:

request_id = req-001
trace_id = trace-aaa
span_id = span-tool-01
tool_call_id = call-xyz
downstream_request_id = order-api-7788
idempotency_key = refund-order-1001

含义分别是:

  • req-001:用户请求进入了 Agent 服务;
  • trace-aaa:这次 Agent 工作流;
  • span-tool-01:其中某个工具操作;
  • call-xyz:模型要求调用工具的那一次意图;
  • order-api-7788:工具访问订单服务的网络请求;
  • refund-order-1001:退款副作用的幂等边界。

7.2 Span 的结构关联

一个 Span 的父子关系可表示为:

trace_id = trace-aaa
└── span_id = span-task
    └── span_id = span-turn-2
        ├── span_id = span-generation-2
        └── span_id = span-tool-1

其中:

span-tool-1.parent_id = span-turn-2
span-tool-1.trace_id  = trace-aaa

OpenAI Agents SDK 会自动将 Span 放入当前 Trace,并嵌套到最近的当前 Span 下;其 Python 实现使用 contextvar 跟踪当前 Trace 和 Span,从而支持并发上下文隔离。(openai.github.io)

但“自动传播”不是“跨进程自动传播”。当工具通过 HTTP、消息队列或任务系统离开当前进程时,应用必须显式传递关联信息,例如:

X-Request-ID: req-001
X-Agent-Trace-ID: trace-aaa
X-Agent-Parent-Span-ID: span-tool-1

下游服务收到后,应创建自己的服务 Span,并保存来源字段,而不是直接伪造父子关系。这样既能保留 Agent 轨迹,也能避免不同观测系统之间的 ID 语义冲突。

7.3 重试与关联 ID

重试是最容易造成重复计数的地方。

Trace trace-aaa
├── Function Span tool_call=call-1
│   └── downstream_request_id=req-down-1 -> timeout
└── Function Span tool_call=call-1
    └── downstream_request_id=req-down-2 -> success

这里:

  • 两个下游请求是不同网络尝试;
  • tool_call_id 可以相同,表示同一个工具意图;
  • 每次尝试应有新的 Span 或 attempt 序号;
  • 写操作必须复用同一个 idempotency_key

如果重试创建了新的 Trace,却没有保存原 Trace ID,那么事故复盘时会误以为是两次独立用户请求。


八、一个可执行的最小观测实现

下面示例使用 Python Agents SDK 的公开 tracing 结构,演示手动创建业务 Span。自动产生的 Agent、模型和工具 Span 仍应优先使用 SDK 默认能力;手动 Span 适合补充数据库、缓存、审批和补偿等业务操作。SDK 文档也说明,一般不需要手动创建所有 Span,必要时可以使用 custom_span()。(openai.github.io)

import json
from agents import Agent, Runner, custom_span, trace

agent = Agent(
    name="订单助手",
    instructions=(
        "你是订单助手。查询订单时使用工具;"
        "不得猜测订单状态;工具失败时明确说明失败。"
    ),
)

async def save_audit_record(request_id: str, result: str) -> None:
    # 这里用内存操作代替真实数据库,示例可直接运行。
    with custom_span(
        "audit_record",
        data={
            "request_id": request_id,
            "result_length": len(result),
        },
    ):
        print(json.dumps({
            "event": "audit_saved",
            "request_id": request_id,
        }, ensure_ascii=False))

async def handle_request(user_text: str, request_id: str, conversation_id: str):
    with trace(
        "order_support",
        group_id=conversation_id,
        metadata={
            "request_id": request_id,
            "service": "agent-api",
        },
    ):
        result = await Runner.run(agent, user_text)

        await save_audit_record(
            request_id=request_id,
            result=result.final_output,
        )

        return result.final_output

这个示例的生命周期是:

  1. 创建 Trace;
  2. Runner.run() 产生 Agent 运行及其内部 Span;
  3. Agent 可能产生模型生成和工具调用 Span;
  4. 业务代码创建 audit_record 自定义 Span;
  5. with trace(...) 退出,Trace 结束;
  6. 返回最终答案。

风险在于 result_length 不是业务正确性的证明。审计记录应同时保存:

  • trace_id
  • Agent 最终终态;
  • 工具调用是否成功;
  • 是否发生恢复;
  • 关键业务对象 ID;
  • 数据脱敏后的结果摘要。

九、用 Trace 评测 Agent,而不是只评测最终答案

最终答案评测只能回答“说得像不像正确”,不能回答“过程是否正确”。

OpenAI 的 Agent Evals 指南建议,在仍处于行为调试阶段时先使用 Trace;Trace grading 可以检查工具选择、交接、指令或安全策略违反,以及端到端行为是否因 Prompt 或路由变化而改善。(developers.openai.com)

9.1 步骤正确性

给定任务:

查询订单 -> 查询物流 -> 根据签收状态回答

可以定义期望约束:

P=[query_order,query_logistics,final_answer]P = [\text{query\_order}, \text{query\_logistics}, \text{final\_answer}]

实际轨迹:

A=[query_order,query_logistics,query_logistics,final_answer]A = [\text{query\_order}, \text{query\_logistics}, \text{query\_logistics}, \text{final\_answer}]

这不是简单的最终答案错误,而是效率和步骤正确性问题。评测器应分别给出:

{
  "step_correct": true,
  "duplicate_tool_call": true,
  "terminal_answer_correct": true
}

9.2 参数正确性

工具名正确不代表调用正确:

{
  "expected": {
    "tool": "query_order",
    "arguments": {
      "order_id": "1001"
    }
  },
  "actual": {
    "tool": "query_order",
    "arguments": {
      "order_id": "10001"
    }
  }
}

参数评测需要区分:

  • 字段是否齐全;
  • 类型是否正确;
  • 值是否来自用户输入;
  • 值是否满足权限范围;
  • 是否使用了上一步工具的真实输出;
  • 是否存在模型幻造字段。

9.3 恢复和终态

工具失败后的正确轨迹可能是:

query_order -> timeout
重试一次 -> timeout
停止继续调用
向用户说明暂时无法查询

错误轨迹可能是:

query_order -> timeout
query_logistics(order_id="1001") -> schema error
继续重复调用

因此 Trace 评测应包含:

  • 是否识别错误类别;
  • 是否按策略重试;
  • 是否停止无效循环;
  • 是否避免在副作用工具上盲目重试;
  • 是否进入明确终态;
  • 是否留下可复盘证据。

当掌握了“什么是好轨迹”后,再将单条 Trace 提炼成数据集,进行重复评测和版本比较。官方评测指南也将 Trace 调试与后续 Dataset、Eval Run 区分开:前者用于定位行为,后者用于可重复基准和持续比较。(developers.openai.com)


十、故障诊断:从终态反向沿着 Trace 定位

10.1 最终答案错误,但工具正确

检查顺序:

Generation Span
  ├── 工具结果是否完整进入上下文
  ├── 工具结果是否被截断或序列化错误
  ├── 下一回合是否引用了正确 call_id
  └── 最终 Generation 是否违反输出约束

若工具返回:

{"status": "delivered", "delivered_at": "..."}

但最终回答说“仍在运输中”,问题可能不在工具,而在上下文拼接、字段映射或模型回合。

10.2 延迟升高,但模型耗时没有升高

查看 Trace 时间线:

Task Span: 4.2s
├── Generation: 0.8s
├── Function: 0.3s
├── Function: 0.4s
└── 未归属等待: 2.7s

此时不能把 4.2 秒都归因于模型。未归属等待可能来自:

  • 任务队列;
  • 并发信号量;
  • 连接池;
  • 本地 JSON 序列化;
  • 事件循环阻塞;
  • Span 开始时间晚于实际等待开始时间。

可观测性本身也有边界:如果没有在排队前创建 Span,排队时间就不会出现在工具 Span 中。

10.3 成本升高,但用户请求量不变

按 Trace 聚合:

T=TrunTrace 数\overline{T} = \frac{\sum T_\text{run}}{\text{Trace 数}}

再拆解:

平均模型请求数增加
平均 Turn 数增加
输入 Token 增加
输出 Token 基本不变

这通常指向上下文膨胀、重复工具调用或路由循环,而不是回答变长。

10.4 生产中完全没有 Trace

应按以下顺序检查:

  1. 是否通过 OPENAI_AGENTS_DISABLE_TRACING=1 全局关闭;
  2. 是否调用了 set_tracing_disabled(True)
  3. 是否在单次 RunConfig 中关闭;
  4. 是否使用了 Zero Data Retention 组织策略;
  5. 是否只是批量导出尚未刷新;
  6. 是否替换了 Trace Processor 却没有配置后端 Exporter。

Agents SDK 默认启用 tracing,但支持环境变量、全局代码配置和单次运行配置关闭;官方文档同时说明,使用 ZDR 策略的组织无法使用该 tracing 能力。(openai.github.io)


十一、隐私、采样和证据完整性

可观测性数据往往比普通日志更敏感,因为它可能同时包含:

  • 用户原始问题;
  • 系统 Prompt;
  • 工具参数;
  • 数据库查询结果;
  • 访问令牌或个人信息;
  • 模型输出;
  • 内部错误信息。

因此要把“可诊断性”和“数据暴露面”分开设计。

11.1 分层记录

建议将数据分成三层:

索引层:
  trace_id, request_id, status, latency, model, token counts

摘要层:
  tool_name, argument schema, result schema, error class, state transition

证据层:
  脱敏后的完整输入输出,仅限受控访问

索引层用于监控,摘要层用于大多数诊断,证据层用于事故复盘。不要让所有生产 Trace 默认保存完整业务数据。

11.2 采样不能破坏错误证据

纯随机采样会漏掉低频高风险故障。更合理的策略是:

  • 成功请求低比例采样;
  • 失败请求全量保留;
  • 高 Token 请求保留;
  • 高 Turn 或循环请求保留;
  • 写操作、权限拒绝和人工接管请求保留;
  • 采样决策本身写入 Trace metadata。

例如:

{
  "sampled": true,
  "sample_reason": "tool_error",
  "retention_class": "incident"
}

采样只影响观测数据,不应影响 Agent 的业务状态。否则会出现“为了记录失败而改变失败行为”的观测扰动。


十二、规范保证、常见实现与经验建议

需要明确区分三类内容:

规范保证

指 SDK 或接口明确承诺的语义,例如:

  • Trace 表示端到端工作流;
  • Span 有开始和结束时间;
  • Span 通过 trace_idparent_id 建立关系;
  • turn_span 表示一次 Agent 循环回合;
  • generation_span 可包含模型输入、输出、配置和 usage。

这些语义可以直接作为系统数据模型的基础。(openai.github.io)

常见实现

指工程中常见但不应误称为协议保证的做法:

  • 使用 request_id 关联业务入口;
  • 使用 downstream_request_id 关联下游服务;
  • 为每次重试增加 attempt 序号;
  • 将工具参数来源标记为模型、用户或服务端;
  • 在消息队列中显式传递 Trace 关联字段。

经验建议

指需要结合业务验证的取舍:

  • 是否保存完整 Prompt;
  • 是否全量保留成功 Trace;
  • 是否把每次重试建模为独立 Span;
  • 是否将一个长期工作流拆成多个 Trace;
  • 是否把成本阈值、Turn 阈值接入自动止损。

这些建议没有脱离业务就能适用于所有系统的固定答案。关键是保证:发生故障时,工程师能从 request_id 找到 Trace,从 Trace 找到出错 Span,从出错 Span 找到模型回合、工具参数、Token 消耗和下游请求,并据此判断是决策错误、执行错误、状态错误还是外部依赖错误。

Agent 可观测性的最小闭环因此不是“把日志接入某个平台”,而是:

业务请求
  -> Trace
  -> Turn
  -> Generation / Tool Span
  -> 状态变化
  -> Token 与延迟
  -> 终态
  -> 评测与故障复盘

只要这条链条完整,Agent 的行为才从不可重复的黑箱输出,变成可以定位、比较、评测和恢复的工程轨迹。


系列导航与关联阅读

官方资料

本文依据 Agent、模型、协议与框架官方资料重新梳理;正文、示例与生产清单由 WR BLOG 编写。