Source: https://aiprimetech.io/zh/blog/reduce-llm-token-usage/ 2026 年 20 日,六月 · 7 分钟 · API # 通过工程优化降低 token 使用量,同时保持质量 A 作者 [AI Prime Tech](https://aiprimetech.io/zh/) · AI 辅助撰写 上个月,我审查了一个客服智能体,它在写出任何内容之前,每张工单就要消耗 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 用量假设。 客服助手的预算示例: | 途径 | 输入预算 | 输出预算 | 模型类别 | 备注 | 工单分类 | 600 | 50 | 低成本/快速模型 | 标签、紧急程度、语言 | 检索证据 | 1,500 | 0 | 嵌入/搜索 | 无需生成 | 起草回答 | 4,000 | 600 | 能力较强的中高档模型 | 使用选定的证据 | 总结对话 | 1,000 | 250 | 低成本/快速模型 | 更新记忆状态 | 升级处理分析 | 6,000 | 900 | 高能力模型 | 仅用于复杂情况 重点不在于这些数字是否普遍适用。它们是刻意设定的适度起始约束。真正有用的做法是要求每个阶段都说明其 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–4 轮,具体取决于任务的敏感程度。 - 持续更新的摘要:稳定的事实和已作出的决定。 - 外部检索:文档、数据库记录、工具输出。 这通常优于直接堆入原始对话记录,因为模型看到的噪声更少,相互冲突的指令也更少。 ## 检索应有选择,而不是用来装点上下文 检索增强生成往往起初是为了提升质量,后来却成了 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 系统时,通常会按以下顺序处理: - 先建立监测 添加按部分统计的 token 用量。不要盲目优化。 - 限制检索上下文 为检索设置严格的 token 预算。进行重排序并提取文本片段。 - 用状态替代对话记录堆砌 保留最近的对话轮次和结构化记忆。 - 约束输出 对机器处理的输出使用模式约束,对面向用户的输出使用明确格式。 - 路由子任务 在可靠的情况下,将分类、改写和摘要任务交给成本更低的模型。 - 在任务层面评估质量 比较成功解决任务的成本,而不只是单次调用成本。 - 关注尾部情况 排查 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 用量。 - 长上下文模型功能强大,但并不能免除对信息架构的需求。 [推理](/zh/blog/tag/inference/)[提示词](/zh/blog/tag/prompts/)[模型](/zh/blog/tag/models/)[API](/zh/blog/tag/api/) A [AI Prime Tech](https://aiprimetech.io/zh/) 本博客由 AI Prime Tech 发布。文章借助 AI 辅助起草;在据此采取行动前,请对照官方文档核实模型 ID、价格和限额等特定版本的细节。 以更低价格接入 Claude API 一个 API 密钥即可使用 Claude Opus 5.5、Sonnet 5、Haiku 4.5 和 Fable 5.1,以及 GPT-6 个模型。按量付费,无需订阅。 [获取 API 密钥 →](/register) AI Prime Tech 是独立的第三方 API 网关。Claude™ 和 Anthropic® 是 Anthropic, PBC 的商标。此处不暗示任何关联或背书。