LLM 应用架构演进:应对模型频繁升级的工程化策略与实战

深入解析大模型频繁迭代下 LLM 应用的架构挑战,涵盖模型抽象层设计、Prompt 版本管理、输出质量回归测试、灰度切换与成本控制,附 TypeScript 完整实现与 Claude Fable 5 迁移实战案例。

开发者效率 2026-06-09 20 分钟

2026 年 6 月 9 日,Anthropic 发布了 Claude Fable 5——一个全新的 Mythos 级模型,定价 $10/$50 per million tokens,在软件工程、长上下文推理和自主任务执行上全面超越前代。与此同时,OpenAI 的 o3-pro、Google 的 Gemini 2.5 Ultra、DeepSeek 的 V4 也在最近几个月密集更新。对开发者来说,真正的挑战不是选择哪个模型,而是如何构建一个能平滑应对模型频繁升级的应用架构。 根据 LangChain 2026 Q2 的调查数据,62% 的 LLM 应用在模型升级后出现了输出质量退化,而其中 78% 的团队没有自动化回归测试机制。

本文将从工程实践角度,系统讲解如何设计一个模型无关的 LLM 应用架构——涵盖模型抽象层、Prompt 版本管理、输出质量回归测试、灰度发布策略和成本控制,并以 Claude Fable 5 迁移为实战案例,给出可直接复用的代码模板。

🏗️ 一、模型抽象层:解耦应用逻辑与模型细节

1.1 为什么需要模型抽象层

大多数 LLM 应用的初始架构是这样的——直接调用某个模型的 SDK:

// ❌ 直接耦合 Anthropic SDK 的写法
import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();

async function generateResponse(prompt) {
  const response = await client.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 4096,
    messages: [{ role: 'user', content: prompt }]
  });
  return response.content[0].text;
}

这种写法在模型稳定时没问题,但当 Claude Fable 5 发布时,你会面临一系列连锁问题:API 参数变了(如新的 thinking 配置)、Token 计费方式变了(输入 $10/M、输出 $50/M)、输出格式可能微妙变化(更长的推理链、不同的 Markdown 渲染风格)。如果你的业务逻辑和模型调用深度耦合,每次模型升级都意味着大面积代码修改。

正确的做法是引入一个模型抽象层(Model Abstraction Layer),将模型差异封装在统一接口之下:

// ✅ 模型抽象层设计
interface ModelProvider {
  readonly name: string;
  readonly modelId: string;
  readonly contextWindow: number;
  readonly pricing: { input: number; output: number }; // per million tokens

  generate(request: ModelRequest): AsyncGenerator<ModelChunk>;
  countTokens(text: string): Promise<number>;
}

interface ModelRequest {
  messages: Message[];
  system?: string;
  maxTokens?: number;
  temperature?: number;
  tools?: ToolDefinition[];
  thinking?: ThinkingConfig;  // 统一的思考模式配置
  responseFormat?: ResponseFormat;
}

interface ModelChunk {
  type: 'text' | 'tool_call' | 'thinking' | 'usage';
  content: string;
  metadata?: Record<string, unknown>;
}

interface ThinkingConfig {
  enabled: boolean;
  budgetTokens?: number;  // 思考 Token 预算
}

📌 记住: 模型抽象层的核心不是「让所有模型表现一致」——这是不可能的。而是让上层业务代码不需要感知模型切换,把差异处理集中在适配层。

1.2 多 Provider 适配器实现

有了统一接口后,每个模型 Provider 实现自己的适配器。以 Claude Fable 5 为例:

// Claude Provider 适配器
import Anthropic from '@anthropic-ai/sdk';

class ClaudeProvider implements ModelProvider {
  readonly name = 'claude';
  readonly modelId: string;
  readonly contextWindow: number;
  readonly pricing: { input: number; output: number };

  private client: Anthropic;

  constructor(config: { modelId: string; apiKey: string }) {
    this.client = new Anthropic({ apiKey: config.apiKey });
    this.modelId = config.modelId;

    // 模型能力映射表
    const modelSpecs: Record<string, { ctx: number; price: { input: number; output: number } }> = {
      'claude-fable-5': { ctx: 200000, price: { input: 10, output: 50 } },
      'claude-opus-4-8': { ctx: 200000, price: { input: 15, output: 75 } },
      'claude-sonnet-4-20250514': { ctx: 200000, price: { input: 3, output: 15 } },
      'claude-haiku-3-5': { ctx: 200000, price: { input: 0.80, output: 4 } },
    };

    const spec = modelSpecs[config.modelId];
    if (!spec) throw new Error(`Unknown model: ${config.modelId}`);
    this.contextWindow = spec.ctx;
    this.pricing = spec.price;
  }

  async *generate(request: ModelRequest): AsyncGenerator<ModelChunk> {
    const params: any = {
      model: this.modelId,
      max_tokens: request.maxTokens ?? 4096,
      messages: request.messages.map(m => ({
        role: m.role,
        content: m.content,
      })),
    };

    if (request.system) params.system = request.system;
    if (request.temperature !== undefined) params.temperature = request.temperature;

    // Fable 5 支持 extended thinking
    if (request.thinking?.enabled) {
      params.thinking = {
        type: 'enabled',
        budget_tokens: request.thinking.budgetTokens ?? 10000,
      };
    }

    // 工具定义
    if (request.tools?.length) {
      params.tools = request.tools.map(t => ({
        name: t.name,
        description: t.description,
        input_schema: t.parameters,
      }));
    }

    const stream = this.client.messages.stream(params);

    for await (const event of stream) {
      if (event.type === 'content_block_delta') {
        if (event.delta.type === 'text_delta') {
          yield { type: 'text', content: event.delta.text };
        } else if (event.delta.type === 'thinking_delta') {
          yield { type: 'thinking', content: event.delta.thinking };
        }
      }
      if (event.type === 'message_delta' && event.usage) {
        yield {
          type: 'usage',
          content: '',
          metadata: {
            inputTokens: event.usage.input_tokens,
            outputTokens: event.usage.output_tokens,
          },
        };
      }
    }
  }

  async countTokens(text: string): Promise<number> {
    // Claude 的 Token 计数近似:1 token ≈ 3.5 字符(英文)/ 1.5 字符(中文)
    return Math.ceil(text.length / 2.5);
  }
}

💡 提示: 上面的 Token 计数使用了近似值。在生产环境中,建议使用 Anthropic.countTokens() API 获取精确计数,或者用 tiktoken 做本地估算以减少 API 调用。

1.3 模型能力映射表

不同模型的能力差异很大,你需要一个结构化的能力映射来指导路由决策:

能力维度 Claude Fable 5 Claude Sonnet 4 GPT-4o DeepSeek V4
上下文窗口 200K 200K 128K 128K
Extended Thinking
工具调用
流式输出
多模态(视觉)
输入价格 ($/M) 10 3 2.5 1
输出价格 ($/M) 50 15 10 2
编程能力 (SWE-bench) 顶尖 优秀 优秀 优秀
长任务自主性 极强 中等 中等 中等

关键结论: 没有「最好的模型」,只有「最适合当前任务的模型」。Fable 5 在自主长任务上最强,但 Sonnet 4 的性价比在日常对话场景下更优。你的架构应该支持按任务类型动态路由。

🔄 二、Prompt 版本管理与输出质量回归

2.1 Prompt 即代码:版本化管理

Prompt 是 LLM 应用中最脆弱的资产——同一个 Prompt 在不同模型上的表现可能天差地别。Prompt 必须像代码一样进行版本管理。

// Prompt 版本管理系统
interface PromptVersion {
  id: string;
  name: string;
  version: string;          // semver: 1.2.0
  template: string;
  targetModels: string[];   // 适用的模型列表
  variables: string[];      // 模板变量
  testCases: TestCase[];
  metrics: PromptMetrics;
  createdAt: Date;
  updatedAt: Date;
}

interface TestCase {
  input: Record<string, string>;
  expectedPatterns: string[];    // 输出应匹配的正则
  forbiddenPatterns: string[];   // 输出不应包含的内容
  maxTokens?: number;
  qualityScore?: number;         // 人工评分 1-5
}

interface PromptMetrics {
  avgQualityScore: number;
  avgTokensUsed: number;
  avgLatencyMs: number;
  successRate: number;           // 通过测试用例的比率
  lastTestedAt: Date;
}

class PromptRegistry {
  private prompts = new Map<string, PromptVersion[]>();

  register(prompt: PromptVersion): void {
    const versions = this.prompts.get(prompt.name) ?? [];
    versions.push(prompt);
    this.prompts.set(prompt.name, versions);
  }

  // 获取指定模型的最新可用 Prompt 版本
  getLatest(modelId: string, promptName: string): PromptVersion | null {
    const versions = this.prompts.get(promptName) ?? [];
    return versions
      .filter(v => v.targetModels.includes(modelId) || v.targetModels.includes('*'))
      .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())[0] ?? null;
  }

  // 渲染模板
  render(prompt: PromptVersion, variables: Record<string, string>): string {
    let result = prompt.template;
    for (const [key, value] of Object.entries(variables)) {
      result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
    }
    return result;
  }
}

警告: 永远不要在代码中硬编码 Prompt 字符串。当 Claude Fable 5 的输出风格与 Sonnet 4 不同时,你需要快速切换 Prompt 版本——如果 Prompt 散落在几十个文件中,这个过程会非常痛苦。

2.2 自动化回归测试

模型升级后,你需要自动验证现有 Prompt 是否仍然有效。下面是一个轻量级的回归测试框架:

// Prompt 回归测试运行器
interface RegressionResult {
  promptName: string;
  modelId: string;
  totalTests: number;
  passed: number;
  failed: number;
  failures: { testName: string; reason: string }[];
  avgQualityDelta: number;  // 与基准的质量差异
}

async function runRegression(
  provider: ModelProvider,
  registry: PromptRegistry,
  promptName: string
): Promise<RegressionResult> {
  const prompt = registry.getLatest(provider.modelId, promptName);
  if (!prompt) throw new Error(`No prompt found for ${promptName} on ${provider.modelId}`);

  const failures: { testName: string; reason: string }[] = [];
  let passed = 0;

  for (const testCase of prompt.testCases) {
    const rendered = registry.render(prompt, testCase.input);
    let output = '';

    for await (const chunk of provider.generate({
      messages: [{ role: 'user', content: rendered }],
      maxTokens: testCase.maxTokens ?? 2048,
    })) {
      if (chunk.type === 'text') output += chunk.content;
    }

    // 检查期望模式
    const allExpected = testCase.expectedPatterns.every(p =>
      new RegExp(p, 'i').test(output)
    );

    // 检查禁止模式
    const noForbidden = testCase.forbiddenPatterns.every(p =>
      !new RegExp(p, 'i').test(output)
    );

    if (allExpected && noForbidden) {
      passed++;
    } else {
      failures.push({
        testName: JSON.stringify(testCase.input),
        reason: !allExpected ? 'Missing expected pattern' : 'Contains forbidden pattern',
      });
    }
  }

  return {
    promptName,
    modelId: provider.modelId,
    totalTests: prompt.testCases.length,
    passed,
    failed: failures.length,
    failures,
    avgQualityDelta: 0, // 需要人工评分或 LLM-as-judge
  };
}

在 Claude Fable 5 发布后,你可以用这个框架快速跑一遍所有 Prompt 的回归测试:

# 运行回归测试
npx tsx scripts/regression-test.ts --model claude-fable-5 --prompt all

# 输出示例:
# ✅ prompt: code-review        15/15 passed (model: claude-fable-5)
# ⚠️ prompt: data-extraction    12/15 passed (3 failures)
#    - Missing expected pattern: "confidence": 0.95
#    - Contains forbidden pattern: I'm not sure but...
# ✅ prompt: summarization       10/10 passed (model: claude-fable-5)

🎯 三、灰度发布与成本控制

3.1 模型灰度切换策略

不要一次性把所有流量切到新模型。使用灰度发布策略,逐步验证新模型的稳定性:

// 模型灰度路由器
interface RoutingRule {
  taskType: string;        // 任务类型:chat、code-review、extraction 等
  primaryModel: string;    // 主力模型
  fallbackModel: string;   // 降级模型
  canaryPercent: number;   // 灰度比例 0-100
  qualityThreshold: number; // 质量阈值,低于此值自动回滚
}

class ModelRouter {
  private rules = new Map<string, RoutingRule>();
  private metrics = new Map<string, { success: number; total: number; avgScore: number }>();

  constructor(private providers: Map<string, ModelProvider>) {}

  addRule(rule: RoutingRule): void {
    this.rules.set(rule.taskType, rule);
  }

  async route(taskType: string, request: ModelRequest): Promise<ModelProvider> {
    const rule = this.rules.get(taskType);
    if (!rule) throw new Error(`No routing rule for task: ${taskType}`);

    // 灰度判断:按百分比决定是否使用新模型
    const useCanary = Math.random() * 100 < rule.canaryPercent;
    const modelId = useCanary ? rule.primaryModel : rule.fallbackModel;

    const provider = this.providers.get(modelId);
    if (!provider) throw new Error(`Provider not found: ${modelId}`);

    return provider;
  }

  // 根据质量指标自动调整灰度比例
  autoAdjust(taskType: string): void {
    const rule = this.rules.get(taskType);
    if (!rule) return;

    const primaryMetrics = this.metrics.get(rule.primaryModel);
    if (!primaryMetrics) return;

    const successRate = primaryMetrics.success / primaryMetrics.total;

    if (successRate >= rule.qualityThreshold && primaryMetrics.avgScore >= 4.0) {
      // 质量达标,增加灰度比例
      rule.canaryPercent = Math.min(100, rule.canaryPercent + 10);
      console.log(`[Router] ${taskType}: canary → ${rule.canaryPercent}%`);
    } else if (successRate < rule.qualityThreshold - 0.05) {
      // 质量严重下降,回滚灰度
      rule.canaryPercent = Math.max(0, rule.canaryPercent - 20);
      console.log(`[Router] ${taskType}: ROLLBACK canary → ${rule.canaryPercent}%`);
    }
  }
}

使用示例——Claude Fable 5 的灰度发布配置:

const router = new ModelRouter(providers);

// Fable 5 灰度:先用 5% 流量验证
router.addRule({
  taskType: 'code-review',
  primaryModel: 'claude-fable-5',
  fallbackModel: 'claude-sonnet-4-20250514',
  canaryPercent: 5,        // 初始 5% 流量走 Fable 5
  qualityThreshold: 0.95,  // 95% 的请求需要通过质量检查
});

router.addRule({
  taskType: 'chat',
  primaryModel: 'claude-fable-5',
  fallbackModel: 'claude-sonnet-4-20250514',
  canaryPercent: 0,        // 聊天场景暂不灰度(成本敏感)
  qualityThreshold: 0.90,
});

关键结论: 灰度发布的核心价值不只是「安全上线」,更是收集新模型的真实表现数据。在灰度期间,你应该对每个请求记录输入 Token、输出 Token、延迟、是否触发工具调用等指标,为后续的全量切换提供数据支撑。

3.2 成本工程:Token 预算控制

Claude Fable 5 的定价是 $10/$50 per million tokens,比 Sonnet 4 贵 3-4 倍。如果不做成本控制,一次模型升级可能让你的 API 账单翻几倍:

场景 Sonnet 4 月成本 Fable 5 月成本 差异
日均 10K 次对话 (avg 2K tokens) $90 $300 3.3x
日均 1K 次代码审查 (avg 8K tokens) $432 $1,440 3.3x
日均 500 次长任务 (avg 50K tokens) $1,350 $4,500 3.3x

成本控制的关键策略:

// Token 预算管理器
class TokenBudgetManager {
  private dailyBudget: number;       // 每日预算(美元)
  private dailySpent = 0;
  private taskBudgets = new Map<string, number>();

  constructor(config: { dailyBudget: number }) {
    this.dailyBudget = config.dailyBudget;
  }

  // 为不同任务类型设置预算上限
  setTaskBudget(taskType: string, maxCostPerRequest: number): void {
    this.taskBudgets.set(taskType, maxCostPerRequest);
  }

  // 检查是否还有预算
  canProceed(taskType: string, estimatedTokens: number, pricing: { input: number; output: number }): boolean {
    // 检查每日总预算
    const estimatedCost = (estimatedTokens / 1_000_000) * pricing.output;
    if (this.dailySpent + estimatedCost > this.dailyBudget) {
      console.warn(`[Budget] Daily budget exceeded: $${this.dailySpent.toFixed(2)} / $${this.dailyBudget}`);
      return false;
    }

    // 检查任务级预算
    const taskBudget = this.taskBudgets.get(taskType);
    if (taskBudget && estimatedCost > taskBudget) {
      console.warn(`[Budget] Task budget exceeded for ${taskType}: $${estimatedCost.toFixed(4)} > $${taskBudget}`);
      return false;
    }

    return true;
  }

  // 记录实际消耗
  recordUsage(inputTokens: number, outputTokens: number, pricing: { input: number; output: number }): number {
    const cost = (inputTokens / 1_000_000) * pricing.input +
                 (outputTokens / 1_000_000) * pricing.output;
    this.dailySpent += cost;
    return cost;
  }

  // 当预算不足时,推荐降级模型
  recommendFallback(taskType: string, currentModel: string): string {
    const fallbackChain: Record<string, string> = {
      'claude-fable-5': 'claude-sonnet-4-20250514',
      'claude-opus-4-8': 'claude-sonnet-4-20250514',
      'claude-sonnet-4-20250514': 'claude-haiku-3-5',
    };
    return fallbackChain[currentModel] ?? currentModel;
  }
}

💡 提示: Claude Fable 5 的 Extended Thinking 功能会产生额外的推理 Token。如果任务不需要深度推理(如简单的文本分类、格式转换),在请求中关闭 thinking 可以显著降低成本。

3.3 智能路由:按任务复杂度选模型

不是所有任务都需要最贵的模型。构建一个基于任务复杂度的智能路由器:

// 任务复杂度评估与模型路由
interface TaskClassification {
  complexity: 'simple' | 'medium' | 'complex';
  suggestedModel: string;
  reasoning: string;
}

function classifyAndRoute(taskType: string, inputText: string): TaskClassification {
  // 简单任务:格式化、翻译、分类
  const simplePatterns = /^(翻译|格式化|分类|提取|总结|简短回答)/;

  // 复杂任务:代码审查、架构设计、多步推理
  const complexPatterns = /(分析|设计|重构|调试|优化|对比.*方案|从零.*实现)/;

  // 中等任务:问答、解释、生成
  const mediumPatterns = /(解释|如何|为什么|生成|写一个|创建)/;

  if (simplePatterns.test(inputText) && inputText.length < 500) {
    return {
      complexity: 'simple',
      suggestedModel: 'claude-haiku-3-5',
      reasoning: '简单任务,使用 Haiku 即可,成本最低',
    };
  }

  if (complexPatterns.test(inputText) || inputText.length > 5000) {
    return {
      complexity: 'complex',
      suggestedModel: 'claude-fable-5',
      reasoning: '复杂任务需要 Fable 5 的深度推理和长上下文能力',
    };
  }

  return {
    complexity: 'medium',
    suggestedModel: 'claude-sonnet-4-20250514',
    reasoning: '中等复杂度,Sonnet 4 性价比最优',
  };
}

⚠️ 四、模型升级的避坑指南

4.1 常见陷阱与解决方案

在多次模型升级的实战中,以下是最高频的问题:

❌ 坑 1:假设输出格式不变

Claude Fable 5 的输出可能比 Sonnet 4 更长、更详细,包含更多的推理步骤。如果你的下游代码用正则提取 JSON,可能会因为额外的 Markdown 标记而失败。

解决方案: 始终使用 Structured Output(如 tool_useresponse_format)约束输出格式,而不是用正则从自由文本中提取。

❌ 坑 2:忽略 Token 计费差异

Fable 5 的输出价格是 $50/M tokens,是 Sonnet 4 的 3.3 倍。如果你的应用平均输出 4000 tokens,每次调用的成本从 $0.06 变成 $0.20。

解决方案: 在模型抽象层中集成成本追踪,设置每日/每月预算告警。对于成本敏感的场景,使用 Haiku 或 Sonnet 作为默认模型,只在需要时升级到 Fable 5。

❌ 坑 3:没有回滚机制

全量切换到新模型后发现质量下降,但没有快速回滚的能力。

解决方案: 使用灰度发布(如上文的 ModelRouter),保留旧模型的配置,确保能在 5 分钟内回滚到旧模型。

❌ 坑 4:Prompt 不兼容

同一个 Prompt 在不同模型上的表现可能完全不同。Fable 5 的推理能力更强,可能不需要那么多 few-shot examples,而 Sonnet 4 可能需要更详细的指令。

解决方案: 为每个模型维护独立的 Prompt 版本(如上文的 PromptRegistry),通过回归测试验证兼容性。

📌 记住: 模型升级不是一个「替换 API Key」的操作,而是一个需要完整工程流程的变更——包括回归测试、灰度发布、成本评估和回滚预案。

4.2 模型升级 Checklist

每次模型升级前,按以下清单逐项检查:

  • 回归测试:用现有 Prompt + 测试用例跑新模型,确认通过率 ≥ 95%
  • 成本评估:用真实流量数据估算新模型的月度成本变化
  • 延迟测试:对比新旧模型的 TTFT(首 Token 延迟)和 TPS(每秒 Token 数)
  • 灰度配置:设置初始灰度比例(建议 5%),配置质量阈值
  • 监控告警:确保 Token 用量、延迟、错误率的监控和告警已就绪
  • 回滚预案:确认能快速切回旧模型,记录回滚触发条件
  • Prompt 适配:检查是否有需要为新模型优化的 Prompt

🔧 五、实战:迁移到 Claude Fable 5

以一个真实的代码审查 Agent 为例,展示从 Sonnet 4 迁移到 Fable 5 的完整流程:

// 代码审查 Agent 的模型迁移实战
async function codeReviewAgent(
  diff: string,
  router: ModelRouter,
  budgetManager: TokenBudgetManager,
  promptRegistry: PromptRegistry
): Promise<CodeReviewResult> {
  const taskType = 'code-review';

  // 1. 智能路由:选择模型
  const provider = await router.route(taskType, {
    messages: [{ role: 'user', content: diff }],
  });

  // 2. 预算检查
  const estimatedTokens = Math.ceil(diff.length / 2) + 2000; // 输入 + 预估输出
  if (!budgetManager.canProceed(taskType, estimatedTokens, provider.pricing)) {
    // 降级到更便宜的模型
    const fallbackId = budgetManager.recommendFallback(taskType, provider.modelId);
    const fallbackProvider = getProvider(fallbackId);
    return codeReviewAgentWithProvider(diff, fallbackProvider, promptRegistry);
  }

  // 3. 获取适配当前模型的 Prompt
  const prompt = promptRegistry.getLatest(provider.modelId, 'code-review');
  if (!prompt) throw new Error('No code review prompt found');

  // 4. 调用模型
  const startTime = Date.now();
  let output = '';

  for await (const chunk of provider.generate({
    messages: [{ role: 'user', content: promptRegistry.render(prompt, { diff }) }],
    maxTokens: 4096,
    thinking: { enabled: true, budgetTokens: 8000 }, // Fable 5 的思考模式
  })) {
    if (chunk.type === 'text') output += chunk.content;
  }

  // 5. 记录使用量
  const latency = Date.now() - startTime;
  const cost = budgetManager.recordUsage(
    Math.ceil(diff.length / 2.5),  // 估算输入 Token
    Math.ceil(output.length / 2.5), // 估算输出 Token
    provider.pricing
  );

  console.log(`[CodeReview] model=${provider.modelId} latency=${latency}ms cost=$${cost.toFixed(4)}`);

  return parseCodeReviewResult(output);
}

💡 总结与建议

核心观点: LLM 应用的架构设计应该以「变化」为前提,而不是以「稳定」为前提。模型会频繁升级,价格会波动,能力会此消彼长。你的架构能否在 30 分钟内完成模型切换,决定了你在 AI 赛道上的迭代速度。

三条核心建议:

  • 抽象层先行:在写第一行业务代码之前,先建好模型抽象层。这不是过度设计,而是最基本的工程纪律
  • Prompt 版本化:把 Prompt 当作代码管理——有版本号、有测试用例、有变更记录
  • 灰度 + 预算:每次模型切换都走灰度流程,每次调用都追踪成本

相关工具推荐:

  • 🔧 LangSmith — LLM 应用的可观测性和评估平台
  • 🔧 Braintrust — AI 产品的评估和迭代工具
  • 🔧 LiteLLM — 统一的 LLM API 代理,支持 100+ 模型 Provider
  • 🔧 Promptfoo — Prompt 评估和红队测试框架
  • 🔧 Anthropic Token Counter — Claude 模型的精确 Token 计数

📚 相关文章