LangChain.js 实战系列:如何统计大模型使用的 token 使用量和花费

'# LangChain.js 实战系列:如何统计大模型使用的 token 使用量和花费

一、背景与问题

在大模型应用开发中,token 使用量和费用统计是核心指标。以通义千问、GPT-4等模型为例,单次调用的token消耗可能高达数千,而单token价格可达0.0001美元。若不进行精细统计,一个简单的对话系统可能在短时间内产生数万美元的费用。

传统方案存在三个核心痛点:

  1. 模型差异:不同模型的token价格差异可达300%
  2. 计算复杂度:需同时统计输入和输出token
  3. 性能瓶颈:高并发场景下统计效率要求

在LangChain.js中,我们需要通过自定义中间件和费用计算逻辑,构建一个完整的统计体系。这涉及多个技术点:模型调用链路拦截、token计数算法、费用计算模型以及性能优化方案。

二、基本原理

1. Token统计原理

每个模型调用的token消耗包含两部分:

  • 输入token:用户输入内容经过tokenize后的数量
  • 输出token:模型生成内容经过tokenize后的数量

计算公式:

total_tokens = input_tokens + output_tokens

2. 费用计算模型

费用计算需考虑:

  • 模型价格(如Qwen-7B的价格为$0.0001/token)
  • 价格单位(部分模型按输入输出分别计费)
  • 累计统计(需要记录历史数据)

3. 中间件设计

通过LangChain的LLMChain和StreamingAgent,我们可以在调用链路中插入统计逻辑:

const { LLMChain, PromptTemplate } = require("langchain");
const { Qwen } = require("langchain/llms");

const chain = new LLMChain({
  llm: new Qwen(),
  prompt: PromptTemplate.fromTemplate("问:{question}\n答:"),
  // 插入统计中间件
});

三、环境准备

# 安装依赖
npm install langchain
npm install @types/langchain -D

四、核心实现

1. 基础Token统计器

// token-counter.ts
export class TokenCounter {
  private modelPrices: Record<string, number> = {
    "qwen:7b": 0.0001, // 0.1美分/token
    "gpt-4": 0.001,     // 1美分/token
    "llama-3": 0.00005,  // 0.05美分/token
  };

  async countTokens(input: string, model: string): Promise<number> {
    // 实际应用中需调用模型的tokenize方法
    const inputTokens = await this.tokenize(input, model);
    const outputTokens = await this.estimateOutputTokens(input, model);
    return inputTokens + outputTokens;
  }

  private async tokenize(text: string, model: string): Promise<number> {
    // 模拟tokenize过程
    return Math.floor(text.length * 0.5); // 2个字符约1个token
  }

  private async estimateOutputTokens(input: string, model: string): Promise<number> {
    // 简单估计:输入长度的1/3
    return Math.floor(input.length * 0.3);
  }
}

关键点解释:

  • 模拟了tokenize过程,实际应调用模型的tokenize方法
  • 采用简单的长度估算,实际应用中需使用模型的tokenize方法
  • 支持不同模型的价格配置

2. 费用计算中间件

// cost-middleware.ts
import { BaseMiddleware } from "langchain";

export class CostMiddleware extends BaseMiddleware {
  constructor(private tokenCounter: TokenCounter) {}

  async run(input: any, model: any) {
    const { question } = input;
    const modelType = model.modelName;
    
    const totalTokens = await this.tokenCounter.countTokens(question, modelType);
    const pricePerToken = this.tokenCounter.modelPrices[modelType];
    
    const cost = totalTokens * pricePerToken;
    console.log(`Token usage: ${totalTokens}, Cost: $${cost.toFixed(6)}`);
    
    // 继续执行后续处理
    return await super.run(input, model);
  }
}

关键点解释:

  • 通过BaseMiddleware实现中间件功能
  • 在调用模型前统计token和计算费用
  • 支持不同模型的费用计算

3. 综合使用示例

// main.ts
import { LLMChain, PromptTemplate } from "langchain";
import { Qwen } from "langchain/llms";
import { TokenCounter, CostMiddleware } from "./cost-middleware";

const tokenCounter = new TokenCounter();
const middleware = new CostMiddleware(tokenCounter);

const chain = new LLMChain({
  llm: new Qwen(),
  prompt: PromptTemplate.fromTemplate("问:{question}\n答:"),
  middleware: middleware,
});

async function test() {
  const result = await chain.call({ question: "如何统计token使用量?" });
  console.log("Answer:", result);
}

test();

关键点解释:

  • 将中间件集成到LLMChain中
  • 完整的调用链路包含统计和费用计算
  • 可扩展为更复杂的统计体系

五、完整案例

1. 完整的聊天系统实现

// chat-system.ts
import { LLMChain, PromptTemplate } from "langchain";
import { Qwen } from "langchain/llms";
import { TokenCounter, CostMiddleware } from "./cost-middleware";

const tokenCounter = new TokenCounter();
const middleware = new CostMiddleware(tokenCounter);

const chatChain = new LLMChain({
  llm: new Qwen(),
  prompt: PromptTemplate.fromTemplate("用户问:{question}\n助手答:"),
  middleware: middleware,
});

async function chatLoop() {
  const questions = [
    "如何统计token使用量?",
    "如何计算模型费用?",
    "什么是token?",
    "模型价格如何计算?"
  ];
  
  for (const question of questions) {
    console.log(`\nQuestion: ${question}`);
    const result = await chatChain.call({ question });
    console.log("Answer:", result);
  }
}

chatLoop();

2. 前端展示页面

<template>
  <div>
    <h2>Token统计系统</h2>
    <div v-if="results.length">
      <p>历史记录:</p>
      <ul>
        <li v-for="(record, index) in results" :key="index">
          <strong>{{ record.question }}</strong><br>
          Token使用: {{ record.tokens }}<br>
          费用: ${{ record.cost.toFixed(6) }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      results: []
    };
  },
  async mounted() {
    // 模拟获取历史记录
    const history = await this.getHistory();
    this.results = history;
  },
  methods: {
    async getHistory() {
      // 实际应用中应从数据库获取
      return [
        { question: "如何统计token使用量?", tokens: 123, cost: 0.0123 },
        { question: "如何计算模型费用?", tokens: 456, cost: 0.0456 }
      ];
    }
  }
};
</script>

六、源码解析

1. TokenCounter类分析

class TokenCounter {
  private modelPrices: Record<string, number> = {
    "qwen:7b": 0.0001, // 0.1美分/token
    "gpt-4": 0.001,     // 1美分/token
    "llama-3": 0.00005,  // 0.05美分/token
  };

  async countTokens(input: string, model: string): Promise<number> {
    const inputTokens = await this.tokenize(input, model);
    const outputTokens = await this.estimateOutputTokens(input, model);
    return inputTokens + outputTokens;
  }

  private async tokenize(text: string, model: string): Promise<number> {
    // 模拟tokenize过程
    return Math.floor(text.length * 0.5); // 2个字符约1个token
  }

  private async estimateOutputTokens(input: string, model: string): Promise<number> {
    return Math.floor(input.length * 0.3);
  }
}

关键点:

  • 使用对象存储模型价格,支持动态配置
  • tokenize方法应调用模型的tokenize方法
  • 估计输出token时考虑输入长度

2. CostMiddleware类分析

class CostMiddleware extends BaseMiddleware {
  constructor(private tokenCounter: TokenCounter) {}

  async run(input: any, model: any) {
    const { question } = input;
    const modelType = model.modelName;
    
    const totalTokens = await this.tokenCounter.countTokens(question, modelType);
    const pricePerToken = this.tokenCounter.modelPrices[modelType];
    
    const cost = totalTokens * pricePerToken;
    console.log(`Token usage: ${totalTokens}, Cost: $${cost.toFixed(6)}`);
    
    // 继续执行后续处理
    return await super.run(input, model);
  }
}

关键点:

  • 通过super.run()实现中间件链式调用
  • 支持不同模型的费用计算
  • 可扩展为支持多模型的统计系统

七、进阶使用

1. 支持多模型统计

// multi-model-counter.ts
export class MultiModelCounter {
  private modelPrices: Record<string, Record<string, number>> = {
    "qwen:7b": {
      input: 0.0001,
      output: 0.0002
    },
    "gpt-4": {
      input: 0.001,
      output: 0.0015
    }
  };

  async countCost(input: string, model: string, isOutput: boolean): Promise<number> {
    const price = this.modelPrices[model];
    if (!price) throw new Error(`Unsupported model: ${model}`);
    
    const tokens = await this.tokenize(input, model);
    return tokens * (isOutput ? price.output : price.input);
  }
}

2. 历史记录存储

// history-storage.ts
import { MongoClient } from "mongodb";

export class HistoryStorage {
  private client: MongoClient;
  
  constructor(private uri: string, private dbName: string) {
    this.client = new MongoClient(uri);
  }

  async saveRecord(record: { question: string, tokens: number, cost: number }) {
    const db = this.client.db(this.dbName);
    const collection = db.collection("token_usage");
    
    await collection.insertOne({
      timestamp: new Date(),
      question: record.question,
      tokens: record.tokens,
      cost: record.cost
    });
  }
}

八、性能与工程实践

1. 性能优化方案

优化点方法效果
缓存模型价格使用Redis缓存减少配置加载时间
异步处理使用Promise池提高并发处理效率
限流控制使用Redis的计数器防止恶意调用

2. 异常处理机制

// error-handling.ts
export class TokenCounter {
  async countTokens(input: string, model: string): Promise<number> {
    try {
      const inputTokens = await this.tokenize(input, model);
      const outputTokens = await this.estimateOutputTokens(input, model);
      return inputTokens + outputTokens;
    } catch (error) {
      console.error("Token counting error:", error);
      throw new Error("Token counting failed");
    }
  }
}

3. 安全考虑

  • 价格信息安全:防止恶意用户篡改价格配置
  • 输入验证:防止注入攻击
  • 访问控制:限制对统计数据的访问权限

九、常见问题与踩坑

1. 常见错误及解决方案

问题现象解决方案
价格未配置显示"Unsupported model"检查modelPrices配置
token计算错误统计结果与实际不符检查tokenize方法实现
费用计算错误费用计算不准确检查价格单位和计算方式

2. 典型陷阱

  • 模型价格混淆:输入和输出价格混淆
  • token计数不准确:未使用模型的tokenize方法
  • 并发问题:未处理多线程下的统计问题

3. 调试技巧

  • 使用console.log输出关键变量
  • 使用单元测试验证token计数逻辑
  • 使用性能分析工具定位瓶颈

十、最佳实践

  1. 模型价格配置:使用配置文件管理不同模型的价格
  2. 统计中间件:在调用链路中插入统计逻辑
  3. 历史记录:保存历史记录用于审计和分析
  4. 性能优化:采用缓存和异步处理提升性能
  5. 安全措施:实施访问控制和输入验证

十一、总结

在LangChain.js中实现token和费用统计,需要结合中间件、token计数和费用计算三个核心模块。通过合理的设计,可以构建一个完整的统计系统。实际应用中需要注意:

  • 模型价格差异带来的计算复杂度
  • 高并发场景下的性能优化
  • 安全性和数据准确性保障

建议在以下场景使用本方案:

  • 需要精确统计token和费用的生产环境
  • 有复杂的费用计算需求
  • 需要历史记录审计的系统

不建议在以下场景使用:

  • 对性能要求极高的实时系统
  • 无需费用统计的简单应用
  • 无法获取模型价格信息的场景

通过本文的深入分析,开发者可以构建一个既符合业务需求又具备扩展性的token统计系统,为大模型应用的可持续发展提供重要支持。

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日