跳到主要内容

import 钩子与自动拦截

本章回答一个魔法问题:为什么你不改一行业务代码,openai.chat.completions.create(...) 就被记录了?答案是两个机制:拦 import + wrapt 包方法

3.1 零侵入拦截的难点

要解决的小问题: 你想拦 openai 的调用,但你不能要求用户改代码,也不知道用户会在哪一行 import openai。怎么才能“一旦 openai 进来就装上探针”?

思路: Python 的所有 import 最终都走 builtins.__import__。把它接管了,就能在任何包被 import 的那一刻插手。

3.2 import 钩子:接管 import

instrument_all()agentops/instrumentation/__init__.py:523)做两件事:

  1. builtins.__import__ = _import_monitor——从此所有 import 都经过它。
  2. 扫一遍 sys.modules,把已经 import 进来的目标包补上探针(因为可能 init() 之前就 import 了 openai)。

_import_monitoragentops/instrumentation/__init__.py:375)是被换上的新 __import__

# 示意,接近真源码:拦 import 的主骨架
def _import_monitor(name, globals_, locals_, fromlist=(), level=0):
if _has_agentic_library: # 已有主框架,不再拦任何东西
return _original_builtins_import(name, ...)
module = _original_builtins_import(name, ...) # 1. 先真正 import
# 2. 看 name / fromlist 是不是命中 TARGET_PACKAGES(openai、crewai…)
# 3. 命中就 _perform_instrumentation(该包)
return module

重点看第一句:一旦某个 agent 框架被拦了(_has_agentic_library 为真),钩子就“退场”,后面什么都不拦了——这是下面冲突解决的伏笔。

哪些包是目标?看 PROVIDERSAGENTIC_LIBRARIES 两张表(agentops/instrumentation/__init__.py:48:79)。每项记了模块名、instrumentor 类名、最低版本。

3.3 provider 与框架的冲突:为什么只能有一个主框架

这是本章最巧妙的设计。 问题是:如果你用 CrewAI,而 CrewAI 内部又调 openai,那么同一次 LLM 调用会被拦两次(CrewAI 探针一次、openai 探针又一次),出现重复 span。

规则(_should_instrument_packageagentops/instrumentation/__init__.py:238): 一个进程只能有一个“主框架”。

要 import 一个目标包 X

├─ X 已被拦? → 跳过

├─ 已经有 agent 框架在场(_has_agentic_library)?
│ └─ 不管 X 是框架还是 provider,都跳过 ← 只认第一个框架

└─ 还没框架?
├─ X 是 agent 框架 → 先 _uninstrument_providers()(把已装的 provider 探针卸掉),再装 X
└─ X 是 provider → 直接装

重点看那一步 _uninstrument_providers()agentops/instrumentation/__init__.py:211):第一个 agent 框架一接管,就把之前可能已经装上的底层 provider 探针全部卸掉。逻辑是:框架自己会产生更高层、更语义化的 span,provider 层的重复拦截反而是噪声。

3.4 版本门控:不够新就不拦

InstrumentorLoader.should_activateagentops/instrumentation/__init__.py:469)检查实际装的包版本是否 >= 配置的最低版本(用 packaging.version)。不够新就跳过并打 debug 日志——这是为了不在不兼容的旧版库上表现崩坏。

还有个坑:_is_installed_packageagentops/instrumentation/__init__.py:143)会区分“真的 site-packages 库”还是“本地同名目录”。比如你本地有个叫 agents 的文件夹,不该被误认为 OpenAI Agents SDK。

3.5 wrapt 包方法:探针怎么生成 span

拦到包后,真正“包方法”的是每个 provider 的 instrumentor。它们都继承 CommonInstrumentoragentops/instrumentation/common/instrumentor.py:30),这个基类把“按配置表批量包方法”抽成样板。

每个要包的方法是一条 WrapConfigagentops/instrumentation/common/wrappers.py:26):记住“包哪个包.类.方法、用哪个 handler 抽属性、span 叫什么名”。OpenAI 的表在 OpenaiInstrumentor._get_wrapped_methodsagentops/instrumentation/providers/openai/instrumentor.py:192),例如:

# 示意,接近真源码:包 openai 的 embeddings.create
WrapConfig(
trace_name="openai.embeddings",
package="openai.resources.embeddings",
class_name="Embeddings",
method_name="create",
handler=handle_embeddings_attributes, # 从 args/kwargs/返回值里抽 span 属性
)

真正的包装函数 _create_wrapperagentops/instrumentation/common/wrappers.py:89)模式很固定:

# 示意,接近真源码:每个被包方法都是这个套路
with tracer.start_as_current_span(wrap_config.trace_name, kind=...) as span:
_update_span(span, handler(args=args, kwargs=kwargs)) # 调用前:输入属性
return_value = wrapped(*args, **kwargs) # 调真正的 openai 方法
_update_span(span, handler(return_value=return_value)) # 调用后:输出属性
_finish_span_success(span)
# 出错时:record_exception + set_status(ERROR) 后 raise

一个细节:开头检查 _SUPPRESS_INSTRUMENTATION_KEYwrappers.py:111)——如果当前 context 明说“别拦”就直接跳过,这是 OTel 生态的标准“防重入”开关。

3.6 流式响应为何单独包

LLM 的流式(streaming)响应不是一次返回,而是一个迭代器。“生成完”的时点不是函数返回,而是迭代完。所以 OpenAI 探针把 chat completions / responses 的 streaming 方法单独用专门的 stream wrapper 包(OpenaiInstrumentor._custom_wrapagentops/instrumentation/providers/openai/instrumentor.py:92),而不走通用 WrapConfig——这跟 §02 里生成器要特殊处理是同一个道理。

代码地图

主题文件符号名
启动拦截agentops/instrumentation/__init__.pyinstrument_all
import 钩子agentops/instrumentation/__init__.py_import_monitor
冲突规则agentops/instrumentation/__init__.py_should_instrument_package / _uninstrument_providers
目标包表agentops/instrumentation/__init__.pyPROVIDERS / AGENTIC_LIBRARIES
版本门控agentops/instrumentation/__init__.pyInstrumentorLoader / should_activate
探针基类agentops/instrumentation/common/instrumentor.pyCommonInstrumentor
包方法配置agentops/instrumentation/common/wrappers.pyWrapConfig / wrap / _create_wrapper
OpenAI 探针agentops/instrumentation/providers/openai/instrumentor.pyOpenaiInstrumentor / _get_wrapped_methods / _custom_wrap