跳到主要内容

装饰器工厂

本章讲你直接接触的那层:@agentops.agent@agentops.tool 这些装饰器。重点是——它们几乎都是同一个工厂函数生成的。

3.1 一个巧思:所有装饰器都是工厂产出

要解决的小问题: @agent@task@tool… 逻辑几乎一样(都是“开个 span、记输入、跑函数、记输出、结束 span”),只是 span 种类不同。怎么不重复写八遍?

思路: 用一个工厂create_entity_decorator(entity_kind)agentops/sdk/decorators/factory.py:25)接一个种类,返回一个装饰器。于是整个 __init__.py 只是十几行(agentops/sdk/decorators/__init__.py:11):

# 示意,接近真源码:所有装饰器一行一个
agent = create_entity_decorator(SpanKind.AGENT)
task = create_entity_decorator(SpanKind.TASK)
workflow = create_entity_decorator(SpanKind.WORKFLOW)
trace = create_entity_decorator(SpanKind.SESSION) # 注意:trace = SESSION
tool = create_entity_decorator(SpanKind.TOOL)
guardrail = create_entity_decorator(SpanKind.GUARDRAIL)
track_endpoint = create_entity_decorator(SpanKind.HTTP)
operation = task # alias —— @operation 实际产生 TASK 种类 span(见下)

重点看:trace 用的是 SESSION 种类——这意味着 @trace 会开一个“根 span”(一个完整 trace),而别的装饰器只开子 span。这个区别是后面分流的主线。

一个别名陷阱: operation = taskagentops/sdk/decorators/__init__.py:17)——@operation 只是 @task别名,产生的是 TASK 种类的 span,不是 OPERATION 种类。文件里另有一个真正的 OPERATION-kind 装饰器 operation_decorator = create_entity_decorator(SpanKind.OPERATION)__init__.py:13),但它没有被导出到包顶层(__all__ 里只有 operation,没有 operation_decorator)。所以从公开 API 看,没有任何装饰器会产出 OPERATION 种类的 span。

3.2 三重入口:裸用 / 带参 / 标类

工厂返回的 decorator 能扛三种用法(agentops/sdk/decorators/factory.py:31):

  • @tool(裸用)——wrapped 直接是被装饰的函数。
  • @tool(cost=0.01)(带参)——wrapped is None,返回 functools.partial 把参数记下,等下一次拿到函数。
  • @agent class Foo(标类)——走 inspect.isclass(wrapped) 分支(下节)。

常用参数:name(span 名,默认用函数名)、tagscost(仅 tool)、spec(仅 guardrail,取 "input"/"output")。

3.3 类装饰:包住 init 与 async 上下文

@agent 装一个时,工厂生成一个 WrappedClass(wrapped) 子类(agentops/sdk/decorators/factory.py:57),接管几个生命周期点:

  • __init__:创建并 enter 一个 span,记下构造参数为输入。
  • __del__:对象被回收时 exit span(兜底“忘了手动结束”)。
  • __aenter__/__aexit__:支持 async with Foo() as f:

源码注释特别提醒了一个坑:这里的 span 覆盖的是构造/上下文期间,不是对象的整个生命周期。

3.4 函数装饰:四种函数形态的分流

这是本章的核心。@wrapt.decorator 修饰的 wrapperagentops/sdk/decorators/factory.py:100)先探测被装饰函数是哪种,再选不同路径。最重要的分岔是 SESSION(@trace)vs 其他种类

@wrapt.decorator wrapper

├─ tracer 未初始化? → 直接原样跑函数(零开销)

├─ entity_kind == HTTP? → 特殊:起主 session span + request/response 子 span

├─ entity_kind == SESSION(@trace)?
│ ├─ 生成器 → 警告“只成单 span”,走 make_span
│ ├─ async → start_trace / 跑 / record_output / end_trace("Success")
│ └─ sync → 同上(同步版)
│ └─→ 这里用 start_trace/end_trace,因为 SESSION = 一个完整 trace

└─ 其他种类(agent/task/tool/…)
├─ 生成器 → make_span + _process_sync_generator
├─ async 生成器 → make_span + _process_async_generator
├─ async → _create_as_current_span(with 块)
└─ sync → _create_as_current_span(with 块)

为什么 SESSION 要走 start_trace、别的走 _create_as_current_span

  • @trace = 一个完整 trace,生命周期可能跨函数、要进活跃表 → 走 start_trace/end_trace(§01)。
  • 其他种类 = 一个子 span,完全包在函数调用里 → 用 _create_as_current_spanagentops/sdk/decorators/utility.py:77)这个 with 块就够,函数一返回 span 自动结束。

同步非-SESSION 的主路径长这样(agentops/sdk/decorators/factory.py:453):

# 示意,接近真源码:一个子 span 的完整一生
with _create_as_current_span(operation_name, entity_kind, version) as span:
_record_entity_input(span, args, kwargs, entity_kind) # 输入序列化进属性
if entity_kind == "tool" and cost is not None:
span.set_attribute(SpanAttributes.LLM_USAGE_TOOL_COST, cost) # 工具成本
result = wrapped_func(*args, **kwargs)
_record_entity_output(span, result, entity_kind) # 输出序列化进属性
return result
# 出错时:span.record_exception(e) 后 raise

3.5 输入/输出怎么变成属性

_record_entity_input/_record_entity_outputagentops/sdk/decorators/utility.py:138)把参数和返回值 safe_serialize 成 JSON,写到按种类命名的属性键上:

  • 输入键:agentops.{entity_kind}.input(如 agentops.tool.input)。
  • 输出键:agentops.{entity_kind}.output
  • (模板在 agentops/semconv/span_attributes.py:92 AGENTOPS_DECORATOR_INPUT

一个实用坑:超过 1MB 的输入/输出不记_check_content_sizeutility.py:33),避免把巨大负载塞进 span。

3.6 生成器为何要特殊处理

普通函数 with 块一出来 span 就该结束了。但生成器函数调用时不执行任何 body,只返回一个 generator 对象——真正的活发生在别人 for ... in 它的时候。所以不能用 with 块。

解法:用 make_span 手动起 span,把 span 的结束推迟到生成器耗尽(_process_sync_generator / _process_async_generatorutility.py:38):

# 示意,接近真源码:生成器耗尽才结束 span
def _process_sync_generator(span, generator):
yield from generator # 一边产出一边保持 span context
span.end() # 耗尽后才 end

代码地图

主题文件符号名
装饰器工厂agentops/sdk/decorators/factory.pycreate_entity_decorator
装饰器实例agentops/sdk/decorators/__init__.pytrace / agent / task / tool / guardrail / track_endpoint
子 span 上下文agentops/sdk/decorators/utility.py_create_as_current_span
输入输出记录agentops/sdk/decorators/utility.py_record_entity_input / _record_entity_output
生成器处理agentops/sdk/decorators/utility.py_process_sync_generator / _process_async_generator
属性键模板agentops/semconv/span_attributes.pyAGENTOPS_DECORATOR_INPUT / AGENTOPS_DECORATOR_OUTPUT / AGENTOPS_DECORATOR_SPEC