· 7 分钟 · API

通过工程优化降低 token 使用量,同时保持质量

通过工程优化降低 token 使用量,同时保持质量

上个月,我审查了一个客服智能体,它在写出任何内容之前,每张工单就要消耗 18,000–42,000 个输入 token。用户的实际问题通常不到 300 个 token。其余都是“以防万一”的上下文:完整的对话历史、通过关键词搜索检索到的所有内部政策页面、原样复制的 JSON 个工具结果,以及一份在六个月里不断堆积、长达 900 个 token 的系统提示词。

解决办法不是“使用更小的模型”,也不是“降低 max_tokens ,然后寄希望于结果”。我们通过一些朴实的工程工作,将 token 用量中位数降低了约 68%,同时保持回答质量稳定:裁剪上下文、检索更有针对性的证据、总结状态、强制执行输出模式,以及如实测量 token 用量。

本文讨论的就是这类工作。不是一味追求提示词最短,也不是让模型表现变差的技巧,而是在不降低系统能力的前提下,减少 token 消耗的实用方法。

token 用量是架构问题

大多数团队把 token 成本当作模型定价问题。实际中,它通常是信息路由问题。

典型的生产环境 LLM 调用包含四类 token:

  • 指令 token:系统提示词、开发者提示词、格式规则、安全约束。
  • 上下文 token:聊天历史、检索到的文档、工具输出、数据库记录。
  • 推理/任务 token:用户的实际请求和任务过程中的细节。
  • 输出 token 数:生成的响应、JSON 对象、SQL 查询、摘要或工具调用。

当 token 用量失控时,罪魁祸首通常是上下文和输出。模型成本高,并不是因为用户提出了难题,而是因为应用把所有信息都塞进了提示词,然后又允许模型长篇大论。

一个有用的起步练习是按部分记录 token 用量,而不只是记录请求的总大小:

{
  "request_id": "ticket_81291",
  "model": "sonnet-4.6",
  "tokens": {
    "system": 742,
    "history": 3890,
    "retrieval": 12640,
    "tool_results": 2110,
    "user": 184,
    "output": 931
  },
  "latency_ms": 4820
}

看到这份明细后,优化路径就会变得清晰。你不必抽象地争论 Claude Opus 4.8、Sonnet 4.6、GPT-5.5、Gemini 3、Fable 5,或 Qwen、DeepSeek 这样的开放模型哪个“最好”。你首先需要停止发送 12,000 个无关的检索 token。

先为每条业务路径设定 token 预算

我倾向于按产品业务路径分配预算,而不是设置全局预算。法律合同审查、编程智能体和常见问题机器人不应共用相同的 token 用量假设。

客服助手的预算示例:

途径输入预算输出预算模型类别备注
工单分类60050低成本/快速模型标签、紧急程度、语言
检索证据1,5000嵌入/搜索无需生成
起草回答4,000600能力较强的中高档模型使用选定的证据
总结对话1,000250低成本/快速模型更新记忆状态
升级处理分析6,000900高能力模型仅用于复杂情况

重点不在于这些数字是否普遍适用。它们是刻意设定的适度起始约束。真正有用的做法是要求每个阶段都说明其 token 消耗的必要性。

一个常见的陷阱是:如果你只设置 max_tokens 来限制输出,输入仍可能在不知不觉中膨胀。输出限制能防止回答过长,却无法阻止检索内容的堆砌。

在检索前裁剪上下文,而不是检索后

许多系统使用最新的用户消息来检索文档,却仍在最终提示词中包含完整的对话历史。这意味着旧的离题内容、先前失败的工具调用和过时的假设仍留在上下文中。

应改为维护一份精简的工作状态:

{
  "user_goal": "Get refund for duplicate annual subscription charge",
  "known_facts": [
    "User paid twice on 2025-01-08",
    "Invoice IDs: inv_1042 and inv_1077",
    "Account email: user@example.com"
  ],
  "constraints": [
    "Refund policy allows duplicate-charge refunds within 90 days",
    "User prefers email follow-up"
  ],
  "open_questions": [
    "Whether one charge has already been reversed"
  ],
  "last_updated_turn": 12
}

这样,最终调用会包含:

  • 精简的状态。
  • 最新的用户消息。
  • 仅包含相关的已检索政策片段。
  • 如果需要保持语气连贯,也可以包含最近一两轮对话。

不要盲目纳入所有先前消息。聊天历史不是记忆,而是对话记录。记忆是继续执行任务所需的提炼状态。

实际中,我使用三个层次:

  1. 最近的对话轮次:最近 1–4 轮,具体取决于任务的敏感程度。
  2. 持续更新的摘要:稳定的事实和已作出的决定。
  3. 外部检索:文档、数据库记录、工具输出。

这通常优于直接堆入原始对话记录,因为模型看到的噪声更少,相互冲突的指令也更少。

检索应有选择,而不是用来装点上下文

检索增强生成往往起初是为了提升质量,后来却成了 token 浪费的来源。这种失败模式很容易识别:top-k 被设为 10 或 20,每个分块有 1,000 个 token,而最终提示词包含一堆只有模糊关联的文档。

更好的检索通常也更省成本。

实用的检索流程:

def build_context(query, user_state):
    search_query = rewrite_for_search(query, user_state)

    candidates = vector_search(
        query=search_query,
        top_k=30,
        filters={"product": user_state["product"]}
    )

    reranked = rerank_cross_encoder(
        query=search_query,
        docs=candidates,
        top_k=6
    )

    selected = []
    token_budget = 1800

    for doc in reranked:
        snippet = extract_relevant_span(doc.text, query=search_query, max_tokens=300)
        if token_count(selected) + token_count(snippet) <= token_budget:
            selected.append({
                "doc_id": doc.id,
                "title": doc.title,
                "text": snippet
            })

    return selected

关键细节:

  • 检索的候选内容要多于计划纳入的内容。
  • 在生成前重新排序。
  • 提取相关片段,而不是传入完整分块。
  • 严格执行检索 token 预算。
  • 保留文档 ID,以便模型在内部引用或关联证据。

分块大小很重要。如果分块太小,模型会丢失上下文;如果太大,每次匹配都会带入无关文本。对于产品文档和政策页面,我通常从每个分块约 300–600 个 token 开始,并保留标题,然后根据失败案例进行调整。

塞入过多检索上下文时,实际会发生什么?模型往往会变得不够精确。它会试图协调内容相近却不相关的政策,吸收过时的附加条件,或回答问题的错误版本。减少 token 可以提升质量,因为这能移除干扰信息。

摘要是需要对准确性负责的压缩

摘要可以节省大量上下文,但草率的摘要很危险。如果摘要遗漏了某项约束,智能体可能会自信地做出错误操作。

相比叙述性文字,我更倾向于使用结构化摘要:

{
  "task": "Debug failing OAuth callback in staging",
  "environment": {
    "service": "auth-api",
    "branch": "release-2025-02-14",
    "runtime": "node 20"
  },
  "confirmed": [
    "Callback returns 302 to /login",
    "Provider sends code and state",
    "State validation passes"
  ],
  "suspected": [
    "Session cookie is not persisted after callback"
  ],
  "rejected": [
    "Provider credentials are invalid",
    "Redirect URI mismatch"
  ],
  "next_steps": [
    "Inspect Set-Cookie attributes in staging response",
    "Compare SameSite and Secure flags with production"
  ]
}

与以下做法相比,这样更节省 token,也更可靠:

用户正在调试 OAuth,并已尝试过多种方法……

有效的摘要步骤应保留:

  • 用户目标。
  • 确凿事实。
  • 已作出的决定。
  • 约束和偏好。
  • 失败的尝试。
  • 待解答的问题。
  • ID、文件名、时间戳及其他精确标识。

除非确实无关,否则不要在摘要中省略精确值。“用户遇到了发票问题”虽然省 token,却毫无用处。“重复扣款: inv_1042 和 inv_1077 ,发生于 2025-01-08”才是你需要的压缩方式。

对于长时间运行的智能体,我还会保存摘要的版本。如果模型生成了不准确的摘要,你需要能够检查状态何时发生了偏移。

用模式约束输出,避免冗长赘述

输出端浪费的 token 多得令人意外。提示词写着“简洁回答”,模型却写出了开场白、注意事项、回顾,以及继续提供帮助的提议。

对于供机器处理的输出,应使用模式约束。不要随意要求“一个 JSON 响应”;应定义其结构,并拒绝任何不符合结构的输出。

分类调用示例:

{
  "type": "object",
  "additionalProperties": false,
  "required": ["category", "priority", "needs_human", "reason"],
  "properties": {
    "category": {
      "type": "string",
      "enum": ["billing", "bug", "account", "how_to", "other"]
    },
    "priority": {
      "type": "string",
      "enum": ["low", "normal", "high", "urgent"]
    },
    "needs_human": {
      "type": "boolean"
    },
    "reason": {
      "type": "string",
      "maxLength": 160
    }
  }
}

对于面向用户的响应,应指定格式并设置严格的长度上限:

Write the answer in this structure:

- First sentence: direct answer, max 25 words.
- Then 2-4 bullets with concrete steps.
- Do not include a greeting.
- Do not include "let me know if you need anything else."
- Stop after the final bullet.

这比“简短一些”更有效,因为它为模型规定了输出结构。

一个常见陷阱:过低的 max_tokens 可能会在思路尚未表达完整时截断有用的输出。模式约束或受限格式能在保留完整性的同时缩短输出。截断是最后的手段,不是格式策略。

谨慎使用停止序列

当生成内容有自然边界时,停止序列很有用。例如,如果模型正在为模板化文档生成一个章节,可以让它在下一个章节标记处停止。

{
  "model": "gpt-5.5",
  "messages": [
    {
      "role": "user",
      "content": "Generate only the Risk Summary section for this review..."
    }
  ],
  "max_tokens": 500,
  "stop": ["\n## Mitigations", "\n## Appendix"]
}

停止序列对开放式助手回复的作用较小,因为它可能截断有效内容。不同 API 和模型系列对停止序列的处理也有所不同,尤其是在涉及工具调用或结构化输出时。应将它们视为边界防护,而不是主要的压缩技术。

适合的用法:

  • 生成已知模板中的一个章节。
  • 防止模型继续生成示例。
  • 在批处理中遇到分隔符时停止。
  • 约束传统文本补全式提示词。

有风险的用法:

  • 遇到常见标点符号就停止。
  • 遇到可能出现在正常回答中的词语就停止。
  • 用停止序列代替适当的输出模式。
  • 假设 Claude、GPT、Gemini 和开放模型网关的行为完全相同。

将任务拆分并分配给成本更低的模型

并非每个步骤都需要最强的模型。在生产系统中,我经常将子任务路由到更小或成本更低的模型:

子任务典型需求适合的选择
语言检测简单分类小型或低成本模型
工单分类结构化标签输出小型或中型模型
查询改写简短转换小型或中型模型
文档重排序专用重排序器或小型模型尽可能采用非生成式方案
最终回答推理与语气能力更强的模型
政策例外处理细微差别与风险最强的模型

这正是多模型访问在实际运营中发挥作用的地方。你可能希望将 Sonnet 4.6 用于大多数智能体写作任务,将 GPT-5.5 用于复杂的推理路径,将 Gemini 3 用于某些多模态或长上下文工作流,在确实需要 1M 上下文窗口时使用 Fable 5,并将 Llama、Qwen、DeepSeek、MiniMax 或 Kimi 等开放模型用于受控的内部任务。工程上的重点是路由,而不是品牌忠诚。

其局限在于:编排开销确实存在。每增加一次模型调用,都会增加延迟、故障模式、日志复杂度,以及需要评估的范围。如果一个低成本分类器节省了 300 个 token,却增加了 600 毫秒延迟和另一条重试路径,那可能并不划算。

我喜欢的一种模式:

if route == "simple_faq":
    model = "haiku-4.5"
elif route == "standard_support":
    model = "sonnet-4.6"
elif route == "legal_or_financial_exception":
    model = "opus-4.8"
else:
    model = "sonnet-4.6"

具体名称会变化,但原则应保持不变:使用能够可靠达到任务质量要求的最便宜模型。

如实衡量输入和输出 token

当团队只关注每次请求的平均成本时,token 优化就会失败。你需要查看按路由和阶段细分的分布。

至少跟踪以下指标:

  • 按提示词各部分统计的输入 token。
  • 输出 token。
  • 检索到的文档数量和检索 token。
  • 模型名称和版本。
  • 延迟百分位数。
  • 缓存命中率(如果使用提示词缓存或上下文缓存)。
  • 重试次数。
  • 用户可感知的质量指标。
  • 转人工处理率或纠正率。

有用的仪表板应展示 p50、p90 和 p99。token 相关问题往往出现在分布尾部。

日志行示例:

{
  "route": "support.draft_answer",
  "model": "sonnet-4.6",
  "input_tokens": 3820,
  "output_tokens": 514,
  "retrieved_docs": 4,
  "retrieval_tokens": 1320,
  "latency_ms": 3100,
  "cache_hit": false,
  "quality": {
    "user_thumb": "up",
    "human_edited": false
  }
}

对于“我们将 token 减少了 40%”这类说法,要谨慎看待。重试是否增加了?转人工处理是否增加了?用户是否因为回答过于简略而提出了更多后续问题?更便宜的模型是否生成了更多无效的 JSON,导致不得不发起修复调用?

真正应衡量的不是每次调用消耗的 token,而是每个成功完成的任务消耗的 token。

长上下文不是偷懒的理由

大上下文窗口确实很有用。Fable 5 的 1M 上下文规格、Gemini 在长上下文方面的优势,以及各类前沿模型的长上下文模式,让几年前还难以开展的工作流成为可能:全仓库代码分析、大型文档审阅、数小时的转录文本处理,以及内容密集的研究资料包分析。

但长上下文是一种能力,而不是默认架构。

以下情况下,长上下文调用可能适用:

  • 答案依赖输入中相距较远的多个部分。
  • 检索会遗漏跨文档关系。
  • 任务具有探索性,你还不知道哪些内容重要。
  • 遗漏细节的代价高于读取更多内容的成本。

以下情况下,它们通常会造成浪费:

  • 用户提出的是范围很窄的事实性问题。
  • 少量源文档片段已足够。
  • 上下文包含重复或过时的信息。
  • 模型被用作数据库筛选器。

一种常见模式是使用长上下文进行离线分析,并生成精简的产物用于在线服务。例如,完整分析一次政策文档库,然后生成结构化的政策映射、嵌入向量、摘要和测试用例。运行时的助手不应为每张工单重新读取整个文档库。

实用的优化工作流

当我接手一个 token 消耗很高的 LLM 系统时,通常会按以下顺序处理:

  1. 先建立监测
    添加按部分统计的 token 用量。不要盲目优化。

  2. 限制检索上下文
    为检索设置严格的 token 预算。进行重排序并提取文本片段。

  3. 用状态替代对话记录堆砌
    保留最近的对话轮次和结构化记忆。

  4. 约束输出
    对机器处理的输出使用模式约束,对面向用户的输出使用明确格式。

  5. 路由子任务
    在可靠的情况下,将分类、改写和摘要任务交给成本更低的模型。

  6. 在任务层面评估质量
    比较成功解决任务的成本,而不只是单次调用成本。

  7. 关注尾部情况
    排查 p95 和 p99 的 token 用量峰值。这些峰值往往会暴露检索故障或失控的历史记录。

下面是我在服务中使用过的一项简单防护措施:

TOKEN_LIMITS = {
    "support.classify": {"input": 800, "output": 80},
    "support.retrieve": {"input": 500, "output": 0},
    "support.draft": {"input": 4500, "output": 700},
    "support.summarize": {"input": 1800, "output": 300},
}

def assert_token_budget(route, prompt_sections, max_output_tokens):
    input_tokens = {
        name: count_tokens(text)
        for name, text in prompt_sections.items()
    }

    total_input = sum(input_tokens.values())
    limits = TOKEN_LIMITS[route]

    if total_input > limits["input"]:
        raise TokenBudgetError(route, total_input, input_tokens)

    if max_output_tokens > limits["output"]:
        raise TokenBudgetError(route, max_output_tokens, {"output": max_output_tokens})

快速失败,总比一周后才发现某个糟糕的检索查询把预算都花在了无关的 PDF 上要好。

实用要点

  • 减少 token 用量应从建立可观测性开始:按提示词部分、路由和模型记录输入与输出 token 数。
  • 有选择性的检索优于一股脑塞入内容:进行重排序、提取文本片段,并严格执行上下文预算。
  • 摘要应是结构化状态,而不是含糊的文字描述。保留准确的事实、ID、约束和失败的尝试。
  • 相比单纯调低,输出模式能更安全地减少冗长赘述 max_tokens.
  • 停止序列是有用的边界防护措施,但不能替代结构化输出。
  • 只有在新增的延迟和失败模式值得承担时,才将简单子任务路由到成本更低的模型。
  • 用每个成功任务的 token 用量来衡量节省效果,而不是每次调用的 token 用量。
  • 长上下文模型功能强大,但并不能免除对信息架构的需求。
A

本博客由 AI Prime Tech 发布。文章借助 AI 辅助起草;在据此采取行动前,请对照官方文档核实模型 ID、价格和限额等特定版本的细节。

以更低价格接入 Claude API

一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。

获取 API 密钥 →
AI Prime Tech 是独立的第三方 API 网关。Claude™ 和 Anthropic® 是 Anthropic, PBC 的商标。此处不暗示任何关联或背书。