yingjie@memoir
Skip to content

2026-03-04 对使用OpenAI SDK进行大模型请求的AI应用进行插桩

📆 记录于: 3/4/2026 尝试两个可观测后端平台,Langfuse(专为大模型应用设计)和Grafana。

Langfuse

分别使用Lanfuse原生插桩库和OTel对应用进行插桩,在Langfuse平台观察接收到的遥测信号。

Langfuse原生插桩

不使用observe装饰器

使用Langfuse自己的插桩库,在不使用@observer装饰器之前的效果是这样的。

PROJECT_NAME/observability/tracing:

Trace record 1:

Trace record 2:

奇怪的点是遥测没有被组合成一个结构化的内容,而是被分成两个记录了。 第一个记录包含用户的输入、大模型工具调用,第二个记录包含的是大模型的回复。

使用observe装饰器

仔细阅读 Langfuse的文档后发现可以在负责大模型请求的函数上面加一个observe装饰器来引入可观测能力。

python
from langfuse.openai import OpenAI
from langfuse import observe

client = OpenAI(
    base_url=os.environ.get("LLM_URL"),
    api_key=os.environ.get("OPENAI_API_KEY"),
)

@observe
def chat_with_agent(user_message: str) -> str:
    response = client.chat.completions.create(
        model=MODEL,
        messages=messages,
        tools=tools,
        tool_choice="auto",
    )

在使用observe装饰器后,原来两条记录似乎被组成一个了。

Trace record 北京的天气如何第一个sub-record: 第二个sub-record:

OpenTelemetry

使用 opentelemetry-instrumentation-openai-v2

对消息内容的捕获

这个库可以通过环境变量来设置是否要获取交互的内容:

保证OTEL_SEMCONV_STABILITY_OPT_IN设置为gen_ai_latest_experimental的前提下,给OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT设置上对应的值即可开启对消息内容的捕获。

yaml
- OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=span_only
- OTEL_SEMCONV_STABILITY_OPT_IN=gen_ai_latest_experimental
bash
opentelemetry-instrument python main.py

不知为何在“手动插桩”的模式下,没有捕获到消息内容。

手动插桩

opentelemetry-instrumentation-openai-v2使用Monkey Patching对OpenAI SDK进行了插桩,简单说就是在运行的时候将原本OpenAI SDK中用来进行大模型请求的函数进行了替换,换成了被可观测逻辑包裹的新函数,这样就可以产生遥测信号了。

python
from opentelemetry import trace

from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
    OTLPSpanExporter,
)
from opentelemetry.instrumentation.openai_v2 import OpenAIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

# configure tracing
trace.set_tracer_provider(TracerProvider())
trace.get_tracer_provider().add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))

OpenAIInstrumentor().instrument()

client = OpenAI(
    base_url=os.environ.get("LLM_URL"),
    api_key=os.environ.get("OPENAI_API_KEY"),
)

record 1 sub-tab 1:

record 1 sub-tab 2:

自动插桩

使用自动插桩后,获得了更多的内容,且可以获取用户输入和大模型的输出内容了。

record 1: record 2:

分析遥测信号

对比使用Langfuse原生插桩和OTel插桩,前者在Langfuse平台上展示的信息更丰富,但仅凭这一点无法判断是因为Langfuse平台对OTel的兼容问题还是使用OTel插桩的不到位导致的。

下一步通过查看OTel Collector收到的OTel遥测来确定这个问题。 在otel-collector-config.yaml中配置,将遥测存储到文件:

yaml
exporters:
  debug:
    verbosity: detailed
  file:
    path: /otel-logs/telemetry.json
    rotation:
      max_megabytes: 100
      max_backups: 10
    format: json
    create_directory: true

选取其中一次请求为例:

mermaid
sequenceDiagram
	actor User
	participant doubao-agent
	participant Model as doubao-seed-2-0-pro-260215
	
	User->>doubao-agent: 提问:北京天气怎么样?
	note over doubao-agent: 生成父Span(be924a34e4eaa643)<br/>记录:整体调用信息
	note over doubao-agent: 生成子Span(be2fc86423365f08)<br/>记录:详细交互信息  
	doubao-agent->>Model: 发起 chat 请求(携带系统提示+用户问题+工具定义)
	Model->>Model: 分析请求,决定调用 get_weather 工具
	Model-->>doubao-agent: 返回响应(工具调用指令:{"location": "北京"})
	note over doubao-agent: 结束两个Span(耗时约3.84秒)
	doubao-agent-->>User: 调用天气工具并返回北京天气结果

让我疑惑的是出现了两个部分信息重合的Span,仔细观察后发现携带消息内容(提示词、大模型响应) 的span是由opentelemetry.instrumentation.openai.v1产生的,另一个span是由opentelemetry.instrumentation.openai_v2产生的,且v2产生的span先于v1,v2产生的是父span。

json
{
    "resourceSpans": [
        {
            "resource": {
                "attributes": [...]
            },

            "scopeSpans": [
                {
                    "scope": {
                        "name": "opentelemetry.instrumentation.openai.v1",
                        "version": "0.52.5"
                    },
                    "spans": [
                        {
                            "traceId": "7d234113586922e7048973b1055ea751",
                            "spanId": "be2fc86423365f08",
                            "parentSpanId": "be924a34e4eaa643",...
                        }
                    ]
                },
                {
	                "scope": {
                        "name": "opentelemetry.instrumentation.openai_v2"
                    },
                    "spans": [
                        {
                            "traceId": "7d234113586922e7048973b1055ea751",
                            "spanId": "be924a34e4eaa643",...
                        },
                    ...
                }
             ]
        }

对于这个现象,豆包总结道:

  • 父 Span 聚焦「整体」:记录 AI 调用的宏观指标(耗时、总 Token、核心结果),是本次调用的顶层标识;
  • 子 Span 聚焦「细节」:记录 AI 调用的全量业务参数(prompt、工具定义、调用参数),是本次调用的详细日志;

父span(v2)对应的:

子span(v1)对应的

大无语事件:依赖管理没做好

怎么会出现两个版本的插桩库同事运行的情况呢?

我查看了依赖,发现确实同时存在v1和v2两个版本的插桩库,应该是之前配置的时候跟着文档咣咣敲,完全没想这样会出现什么问题。

官方插桩库文档没更新

删除v1版本的依赖后,就恢复一个span了。为什么在v2的span里看不到捕获的消息内容呢?

我打开v2版本插桩库对应的仓库,问Kimi Code。

v2插桩库通过检查环境变量中是否存在指定的值来觉得是否要开启对内容的捕获,从代码中可以看到开启捕获的方式是将 OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT设置为true

python
def is_content_enabled() -> bool:
    capture_content = environ.get(
        OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT, "false"
    )
    return capture_content.lower() == "true"

可我记得在哪里看到过可以使用span_only这样的值啊? 猜测可能是这个目录下的README没有更新吧:opentelemetry-python-contrib/instrumentation-genai/opentelemetry-instrumentation-openai-v2 at main · open-telemetry/opentelemetry-python-contrib

消失的消息内容

从OTel Collector中可以看到Logs中记录了消息内容:

log
otel-collector-1   | 2026-03-04T09:22:29.617Z   info    ResourceLog #0
otel-collector-1   | Resource SchemaURL:
otel-collector-1   | Resource attributes:
otel-collector-1   |      -> telemetry.sdk.language: Str(python)
otel-collector-1   |      -> telemetry.sdk.name: Str(opentelemetry)
otel-collector-1   |      -> telemetry.sdk.version: Str(1.39.1)
otel-collector-1   |      -> service.name: Str(doubao-agent)
otel-collector-1   |      -> telemetry.auto.version: Str(0.60b1)
otel-collector-1   | ScopeLogs #0
otel-collector-1   | ScopeLogs SchemaURL: https://opentelemetry.io/schemas/1.30.0
otel-collector-1   | InstrumentationScope opentelemetry.instrumentation.openai_v2
otel-collector-1   | LogRecord #0
otel-collector-1   | ObservedTimestamp: 2026-03-04 09:22:27.074357614 +0000 UTC
otel-collector-1   | Timestamp: 1970-01-01 00:00:00 +0000 UTC
otel-collector-1   | SeverityText:
otel-collector-1   | SeverityNumber: Unspecified(0)
otel-collector-1   | EventName: gen_ai.choice
otel-collector-1   | Body: Map({"finish_reason":"stop","index":0,"message":{"content":"北京当前的天气为晴天,气温25℃,体感比较舒适哦。","role":"assistant"}})
otel-collector-1   | Attributes:
otel-collector-1   |      -> gen_ai.system: Str(openai)
otel-collector-1   | Trace ID: baa91d5ef281ad4b80f49c04cd6f9610
otel-collector-1   | Span ID: 89980c430c47742d
otel-collector-1   | Flags: 1

但Langfuse平台目前只接受Traces,所以就看不到消息内容了。

总结

  • 起因:我想给ZeroClaw这样的agent项目实现可观测能力,我得先知道对于大模型的可观测,具体的效果是什么样的。从OpenAI SDK开始,看看对其插桩之后,可以捕获哪些信息,指标的选择可以为后续的开发提供参考,也可以验证自己对可观测的实现是否正确。
  • 经过:选择Langfuse作为大模型可观测后端。使用Langfuse原生和OTel的方式对应用进行插桩。
  • 结论:
    • Langfuse原生插桩库的使用最为方便,且能在Langfuse云平台看到丰富的内容
    • 使用 opentelemetry-instrumentation-openai-v2 插桩,可以看到大部分指标,但看不到消息内容
    • 做实验的时候要认真思考每一步的意义和对应的后果,不能开“自动驾驶”一路狂奔
  • 下一步: