Langfuse 文档 中文 英文原文 ↗
文档 / 模型用量与成本追踪

模型用量与成本追踪

Langfuse UI 中的模型成本细分

Langfuse 追踪你 LLM generations 的用量和成本,并按用量类型提供细分。用量和成本可以在类型generationembedding 的 observations 上追踪。

用量类型可以是任意字符串,并因 LLM 提供商而异。在最高层级,它们可以只是 inputoutput。随着 LLM 变得更加复杂,需要额外的用量类型,如 cached_tokensaudio_tokensimage_tokens

在 UI 中,Langfuse 将所有包含字符串 input 的用量类型汇总为输入用量类型,类似地将 output 汇总为输出用量类型。如果没有接入 total 用量类型,Langfuse 会将所有用量类型单位求和为 total。为此,用量类型必须是互斥的桶

用量详情和成本详情都可以是

接入的用量和成本优先于推断的用量和成本:

mermaid
flowchart LR
  A[Ingested Observation]
  B["Usage (tokens or other unit)"]
  C["Cost (in USD)"]
  A --> D{Includes usage?}
  D -->|Yes| B
  D -->|No| E(Use tokenizer) --> B
  A --> F{Includes cost?}
  F -->|Yes| C
  F -->|No| G(Use model price/unit) --> C
  B -->|use usage| G

通过 Metrics API,你可以从 Langfuse 检索聚合的用量和成本指标,用于分析、计费和速率限制的下游使用。该 API 允许你按应用类型、用户或标签过滤。关于归因和控制支出的更广泛 playbook,参见我们的 LLM 成本管理 指南。

接入用量和/或成本

如果 LLM 响应中可用,接入用量和/或成本是在 Langfuse 中追踪用量最准确、最稳健的方式。

许多 Langfuse 集成会自动从 LLM 响应捕获用量详情和成本详情数据。如果这未按预期工作,请在 GitHub 上创建一个 issue

使用 @observe() 装饰器时:

python
from langfuse import observe, get_client
import anthropic

langfuse = get_client()
anthropic_client = anthropic.Anthropic()

@observe(as_type="generation")
def anthropic_completion(**kwargs):
  # optional, extract some fields from kwargs
  kwargs_clone = kwargs.copy()
  input = kwargs_clone.pop('messages', None)
  model = kwargs_clone.pop('model', None)
  langfuse.update_current_generation(
      input=input,
      model=model,
      metadata=kwargs_clone
  )

  response = anthropic_client.messages.create(**kwargs)

  langfuse.update_current_generation(
      usage_details={
          "input": response.usage.input_tokens,
          "output": response.usage.output_tokens,
          "cache_read_input_tokens": response.usage.cache_read_input_tokens
          # "total": int,  # if not set, it is derived as the sum of all usage types
        },
      # Optionally, also ingest usd cost. Alternatively, you can infer it via a model definition in Langfuse.
      cost_details={
          # Here we assume the input and output cost are 1 USD each and half the price for cached tokens.
          "input": 1,
          "cache_read_input_tokens": 0.5,
          "output": 1,
          # "total": float, # if not set, it is derived as the sum of all usage types
      }
  )

  # return result
  return response.content[0].text

@observe()
def main():
  return anthropic_completion(
      model="claude-3-opus-20240229",
      max_tokens=1024,
      messages=[
          {"role": "user", "content": "Hello, Claude"}
      ]
  )

main()

创建手动 generations 时:

python
from langfuse import get_client
import anthropic

langfuse = get_client()
anthropic_client = anthropic.Anthropic()

with langfuse.start_as_current_observation(
    as_type="generation",
    name="anthropic-completion",
    model="claude-3-opus-20240229",
    input=[{"role": "user", "content": "Hello, Claude"}]
) as generation:
    response = anthropic_client.messages.create(
        model="claude-3-opus-20240229",
        max_tokens=1024,
        messages=[{"role": "user", "content": "Hello, Claude"}]
    )

    generation.update(
        output=response.content[0].text,
        usage_details={
            "input": response.usage.input_tokens,
            "output": response.usage.output_tokens,
            "cache_read_input_tokens": response.usage.cache_read_input_tokens
            # "total": int,  # if not set, it is derived as the sum of all usage types
        },
        # Optionally, also ingest usd cost. Alternatively, you can infer it via a model definition in Langfuse.
        cost_details={
            # Here we assume the input and output cost are 1 USD each and half the price for cached tokens.
            "input": 1,
            "cache_read_input_tokens": 0.5,
            "output": 1,
            # "total": float, # if not set, it is derived as the sum of all usage types
        }
    )

使用上下文管理器时:

ts
import {
  startActiveObservation,
  startObservation,
  updateActiveObservation,
} from "@langfuse/tracing";

await startActiveObservation("context-manager", async (span) => {
  span.update({
    input: { query: "What is the capital of France?" },
  });

  // This generation will automatically be a child of "user-request"
  const generation = startObservation(
    "llm-call",
    {
      model: "gpt-4",
      input: [{ role: "user", content: "What is the capital of France?" }],
    },
    { asType: "generation" }
  );

  // ... LLM call logic ...

  generation.update({
    usageDetails: {
      input: 10,
      output: 5,
      cache_read_input_tokens: 2,
      some_other_token_count: 10,
      total: 27, // optional, it is derived as the sum of all usage types
    },
    costDetails: {
      // If you don't want the costs to be calculated based on model definitions, you can pass the costDetails manually.
      input: 1,
      output: 1,
      cache_read_input_tokens: 0.5,
      some_other_token_count: 1,
      total: 3.5,
    },
    output: { content: "The capital of France is Paris." },
  });

  generation.end();
});

使用 observe 包装器时:

ts
import { observe, updateActiveObservation } from "@langfuse/tracing";

// An existing function
async function fetchData(source: string) {
  updateActiveObservation(
    {
      usageDetails: {
        input: 10,
        output: 5,
        cache_read_input_tokens: 2,
        some_other_token_count: 10,
        total: 27, // optional, it is derived as the sum of all usage types
      },
      costDetails: {
        // If you don't want the costs to be calculated based on model definitions, you can pass the costDetails manually.
        input: 1,
        output: 1,
        cache_read_input_tokens: 0.5,
        some_other_token_count: 1,
        total: 3.5,
      },
    },
    { asType: "generation" }
  );

  // ... logic to fetch data
  return { data: `some data from ${source}` };
}

// Wrap the function to trace it
const tracedFetchData = observe(fetchData, {
  name: "observe-wrapper",
  asType: "generation",
});

const result = await tracedFetchData("API");

手动创建 observations 时:

ts
const span = startObservation("manual-observation", {
  input: { query: "What is the capital of France?" },
});

const generation = span.startObservation(
  "llm-call",
  {
    model: "gpt-4",
    input: [{ role: "user", content: "What is the capital of France?" }],
    output: { content: "The capital of France is Paris." },
  },
  { asType: "generation" }
);

generation.update({
  usageDetails: {
    input: 10,
    output: 5,
    cache_read_input_tokens: 2,
    some_other_token_count: 10,
    total: 27, // optional, it is derived as the sum of all usage types
  },
  costDetails: {
    // If you don't want the costs to be calculated based on model definitions, you can pass the costDetails manually.
    input: 1,
    output: 1,
    cache_read_input_tokens: 0.5,
    some_other_token_count: 1,
    total: 3.5,
  },
});

generation
  .update({
    output: { content: "The capital of France is Paris." },
  })
  .end();

span.update({ output: "Successfully answered user request." }).end();

你也可以通过 generation.update() 更新用量和成本。

用量类型是互斥的桶

Langfuse 将 usage_details 中的每个键视为一个独立的、不重叠的桶:每个 token 必须恰好在一个键中计数。特别是,input 必须排除已在另一个 input_* 键(如 input_cached_tokensinput_cache_creation)中计数的 token,output 必须排除在另一个 output_* 键(如 output_reasoning_tokens)中计数的 token。唯一的例外是 total:它本身不是一个桶,而是跨越所有桶并等于它们的和。

Langfuse 在三个地方依赖此契约:

如果桶重叠——例如,如果 input 仍包含在 input_cached_tokens 中报告的 token——那些 token 会显示两次,推断的成本会重复计算它们:Langfuse 中显示的成本会高估你的提供商实际收取的费用。直接接入的 cost_details 按原样使用,不受影响。

许多 LLM 提供商报告包含性计数。例如,OpenAI 的 prompt_tokens(Chat Completions API)和 input_tokens(Responses API)包含缓存的 token。相比之下,Anthropic 的 input_tokens 已经排除了缓存读取和缓存写入。包含性计数在存储之前必须转换为排他性桶,通过从顶层计数中减去明细计数。

例如,一个 OpenAI 风格的响应,有 17,903 个提示词 token(其中 17,817 个是缓存命中)和 188 个补全 token:

提供商报告(包含性) 存储在 Langfuse(排他性)
prompt_tokens: 17903 input: 86
prompt_tokens_details: input_cached_tokens: 17817
completion_tokens: 188 output: 188
total_tokens: 18091 total: 18091

Langfuse 何时为你转换

你是否需要自己做此转换,取决于用量数据如何到达 Langfuse:

如果你编写自己的埋点,用扁平键设置用量详情,请检查你的提供商如何报告用量,并在将它们传递给 Langfuse 之前,从顶层 input/output 计数中减去缓存或明细 token 计数——恰好一次。

与 OpenAI 的兼容性

为提高与 OpenAI 的兼容性,你也可以使用 OpenAI Usage schema。prompt_tokens 将映射到 input,completion_tokens 将映射到 output,total_tokens 将映射到 total。嵌套在 prompt_tokens_details 中的键将以 input_ 前缀展平,completion_tokens_details 将以 output_ 前缀展平。由于 OpenAI 包含性地报告这些明细计数,Langfuse 会分别从 inputoutput 中减去它们,以便存储的桶是互斥的

Schema 识别是严格的:用量对象必须包含下面显示的 OpenAI 用量字段。如果它包含任何额外的键(例如,某些网关附加一个 cost 字段),它不会被识别为 OpenAI 风格的用量,而是作为扁平键按原样存储——没有映射也没有减法。此失败模式是静默的,因此在接入前剥离额外的键。

python
from langfuse import get_client

langfuse = get_client()

with langfuse.start_as_current_observation(
    as_type="generation",
    name="openai-style-generation",
    model="gpt-4o"
) as generation:
    # Simulate LLM call
    # response = openai_client.chat.completions.create(...)

    generation.update(
        usage_details={
            # usage (OpenAI-style schema)
            "prompt_tokens": 10,
            "completion_tokens": 25,
            "total_tokens": 35,
            "prompt_tokens_details": {
                "cached_tokens": 5,
                "audio_tokens": 2,
            },
            "completion_tokens_details": {
                "reasoning_tokens": 15,
            },
        }
    )
ts
import { startObservation } from "@langfuse/tracing";

const generation = startObservation(
  "openai-style-generation",
  {
    model: "gpt-4o",
    usageDetails: {
      // usage (OpenAI-style schema)
      prompt_tokens: 10,
      completion_tokens: 25,
      total_tokens: 35,
      prompt_tokens_details: {
        cached_tokens: 5,
        audio_tokens: 2,
      },
      completion_tokens_details: {
        reasoning_tokens: 15,
      },
    },
  },
  { asType: "generation" },
);
generation.end();

你也可以通过 generation.update()generation.end() 接入 OpenAI 风格的用量。

推断用量和/或成本

如果用量或成本未被接入,Langfuse 将尝试在接入时基于 generation 的 model 参数推断缺失的值。这对于某些模型提供商或不在响应中包含用量或成本的自托管模型尤其有用。

Langfuse 自带一份预定义的流行模型及其分词器列表,包括 OpenAI、Anthropic、Google。查看完整列表(你需要登录)。

你也可以添加自己的自定义模型定义(见下文),或通过 GitHub 请求对新模型的官方支持。

用量

如果为模型指定了分词器,Langfuse 会自动计算接入的 generations 的 token 量。

目前支持以下分词器:

模型 分词器 使用的包 备注
gpt-4o o200k_base tiktoken
gpt* cl100k_base tiktoken
claude* claude @anthropic-ai/tokenizer 据 Anthropic 称,他们的分词器对 Claude 3 模型不准确。如果可能,请发送我们来自其 API 响应的 token。

成本

模型定义包括每用量类型的价格。用量类型必须与 generation 的 usage_details 对象中的键精确匹配。

如果 (1) 用量被接入或推断,且 (2) 匹配的模型定义包含价格,Langfuse 会在接入时自动计算接入的 generations 的成本。

定价层级(Pricing Tiers)

某些模型提供商根据使用的输入 token 数量收取不同的费率。例如,Anthropic 的 Claude Sonnet 4.5 和 Google 的 Gemini 2.5 Pro 在使用超过 200K 输入 token 时应用更高的定价。

Langfuse 支持模型的定价层级,以便为这些依赖于上下文的定价结构实现准确的成本计算。

层级匹配如何工作

每个模型可以有多个定价层级,每个层级具有:

计算成本时,Langfuse 按优先级顺序评估层级(排除默认层级)。使用第一个条件满足的层级。如果没有条件层级匹配,则应用默认层级。

条件格式:

例如,Claude Sonnet 4.5 的 "Large Context" 层级有一个条件:input > 200000,意味着当匹配模式 "input" 的所有用量明细之和超过 200,000 个 token 时应用。

自定义模型定义

你可以灵活地将自己的模型定义(包括定价层级)添加到 Langfuse。这对于未包含在 Langfuse 维护模型列表中的自托管或微调模型尤其有用。

要在 Langfuse UI 中添加自定义模型定义,你可以点击模型名称旁边的 "+" 号,或导航到 Project Settings > Models 添加新的模型定义。

然后你可以添加每 token 类型的价格并保存模型定义。现在所有带此模型的新 traces 都将推断出正确的 token 用量和成本。

模型定义也可以通过 Models API 以编程方式管理:

bash
GET    /api/public/models
POST   /api/public/models
GET    /api/public/models/{id}
DELETE /api/public/models/{id}

模型基于以下内容与 generations 匹配:

Generation 属性 模型属性 备注
model match_pattern 使用正则表达式,例如 (?i)^(gpt-4-0125-preview)$ 匹配 gpt-4-0125-preview

用户定义的模型优先于 Langfuse 维护的模型。

更多细节

使用 openai 分词器时,你需要指定以下分词配置。你也可以从预定义 OpenAI 模型列表中复制配置。更多细节参见 OpenAI 文档tokensPerNametokensPerMessage 对聊天模型是必需的。

json
{
  "tokenizerModel": "gpt-3.5-turbo", // tiktoken model name
  "tokensPerName": -1, // OpenAI Chatmessage tokenization config
  "tokensPerMessage": 4 // OpenAI Chatmessage tokenization config
}

推理模型的成本推断

通过对 LLM 输入和输出分词来进行成本推断,不支持 OpenAI o1 模型系列等推理模型。也就是说,如果没有接入 token 计数,Langfuse 无法为推理模型推断成本。

推理模型需要多个步骤才能得出响应。每个步骤的结果产生作为输出 token 计费的推理 token。因此具有成本效益的输出 token 计数是所有推理 token 与最终补全的 token 计数之和。由于 Langfuse 无法看到推理 token,它无法为没有提供 token 用量的 generations 推断正确的成本。

要受益于 Langfuse 成本追踪,请在接入 o1 模型 generations 时提供 token 用量。当使用 Langfuse OpenAI 包装器LangchainLlamaIndexLiteLLM 等集成时,token 用量会自动为你收集和提供。

更多细节参见关于推理模型如何工作的 OpenAI 指南

故障排查

非官方中文翻译 · 图片/视频/代码均链接官方资源 · 版权归 Langfuse GmbH 所有 查看英文原文 ↗