很多开发者在第一次把大模型接入业务时,收到的成本账单往往会吓一跳:看起来只是让模型总结了一段对话、写了一封邮件,怎么一个月就花了上千元?如果再去细看推理日志,会发现真正的问题不在模型本身,而在我们送给模型的文本里。Prompt 里藏了几千个无用 Token,RAG 检索回来的文档有一大半和当前问题无关,多轮对话每次把历史原样堆进去,几轮之后直接顶到上下文窗口上限。
这篇文章的核心判断是:Token 优化不是“把 Prompt 写短一点”这种小技巧,而是 LLM 应用上线后必须建立的一项基础工程能力。它直接影响 API 成本、响应延迟、上下文窗口利用率,甚至影响模型输出质量。本文会从 Token 本质、Prompt 侧、RAG 侧、多轮对话侧、系统侧几个维度拆解优化思路,并提供可以在项目中直接落地的代码、配置和监控指标。
1. 为什么 Token 优化值得单独写一份手册
先看三个真实场景:
第一个场景,客服知识库问答。技术团队把几十份产品文档全部切块塞进向量数据库,每次用户提问,召回 Top 20 个片段拼进 Prompt。表面上检索很精准,但其中 12 个片段和问题毫无关系,白白浪费了 3000 多个 Token。
第二个场景,AI 写作助手。每次生成内容都在 Prompt 里附上完整用户资料、历史作品、平台要求,总共 4000 Token。但模型真正需要的,可能只是用户本次要写的主题、风格偏好和一条字数限制。
第三个场景,多轮对话应用。对话历史不做任何裁剪,用户问 20 轮之后,光历史记录就占了 8000 Token。此时要么触发窗口上限,不得不把最早一轮丢弃,要么每次请求都要支付高额输入费用,响应也越来越慢。
这三个场景有一个共同点:程序员并没有“主动设计”Token 消耗,只是默认把信息堆给模型。结果就是成本不可控、效果不可控、延迟不可控。
Token 优化手册的作用,就是把“随意堆信息”变成“存量预算 + 优先级分配”的工程过程。优化之后,同类场景通常能把输入 Token 降低 30% 到 60%,而输出质量不会下降,甚至因为噪声减少而更稳定。
2. 先搞懂 Token:它为什么是成本与性能的最小单位
2.1 Token 不是字数:中英文的差异
Token 是模型处理文本的最小粒度。一个中文词语、一个英文单词片段、一个标点符号,都可能对应一个或多个 Token。不同模型有不同分词器,同一个词在不同分词器里的 Token 数也可能不同。
这里有一个很容易被忽略的事实:英文中一个单词平均约 1.3 个 Token,而中文中一个汉字的表达力更高,通常一个汉字可能对应 1 到 2 个 Token,但一个中文词语往往需要 2 个以上 Token。这意味着,如果你用英文思维去估算中文文本的 Token 数,往往偏差很大。
具体到开发中,之前需要用tiktoken来精确计算:
# 文件路径:token_count_demo.py import tiktoken enc = tiktoken.get_encoding("cl100k_base") text = "这是一段用于统计 Token 数量的中文测试文本。" tokens = enc.encode(text) print(f"字符数: {len(text)}") print(f"Token 数: {len(tokens)}") print(f"Token 内容: {tokens}")运行这段代码,你会看到 Token 数和字数并不是一一对应。这个差异直接影响了成本估算。
2.2 费用模型中的输入、输出与缓存
大部分商业大模型 API 按 Token 计费,而且输入 Token 和输出 Token 的价格往往不同。输入 Token 是发送给模型的全部文本,包括系统提示词、用户消息、历史记录、RAG 召回片段;输出 Token 是模型生成的内容。
成本公式可以用一句话概括:
单次调用费用 = 输入 Token 数 × 输入单价 + 输出 Token 数 × 输出单价多轮对话中,这个费用是累积的。如果每次请求都带上全部历史,随着会话变长,每一轮新增请求的输入 Token 都会增加,成本曲线会越来越陡。
部分平台还提供 Prompt Caching 能力,即相同前缀在短时间内再次请求时可以享受价格折扣。这给了我们一个很重要的设计方向:尽量让请求的前缀保持稳定,而不是每次都重新拼一遍。这一点的工程实现,会在后文系统侧优化中展开。
2.3 隐形的 Token 消耗:系统提示词和输出格式约束
很多团队只盯着用户输入和 RAG 片段,却忽略了系统提示词。每次调用模型时,系统提示词都会完整发送。如果系统提示词是 1500 Token,每天调用 10000 次,仅系统提示词就要消耗 1500 万 Token,这可能比用户输入本身还要多。
同样的问题存在于输出格式约束。用 JSON 模式时,如果你在 Prompt 里写一份超长的 JSON 示例,这些示例也会全部进入输入 Token 计算。合理的做法是直接通过模型的 response_format 参数声明 JSON 输出,而不是在 Prompt 里堆砌大量示例。
3. Prompt 侧优化:在不牺牲效果的前提下砍掉冗余
3.1 识别三类无效信息
Prompt 优化不是“越短越好”,而是“每一段信息都要有明确用途”。无效信息通常分三类:
第一类:重复指令。同一句话在系统提示词里写了,又在用户消息里写了一遍。模型不会因为重复而提升理解,只会浪费 Token。
第二类:背景信息过载。为了让模型“更懂业务”,把完整的业务文档、产品介绍、内部规范全部塞进系统提示词。实际上,模型只需要与本次任务强相关的规则。
第三类:与任务无关的示例。示例能提升输出稳定性,但不需要每次都给 5 个。如果模型在同领域已经足够稳定,保留 1 到 2 个用例即可。
3.2 一个优化前后的完整对比
我们以“生成商品标题”为例。优化前的系统提示词可能是这样的:
你是一个电商平台的运营助手。我们平台叫云购商城,成立于2018年,目前有超过2000万注册用户,覆盖服装、数码、家居、美妆等多个品类。你的任务是帮助运营人员生成商品标题。商品标题要包含品牌词、品类词、核心卖点、人群词,同时要遵守广告法,不能使用“最”“第一”等极限词,也不能虚构产品功效。以下是更多说明:标题长度建议在20到30个字之间,要突出产品的差异化卖点,也要考虑搜索关键词覆盖。你可以参考平时的运营经验,结合用户提供的商品信息来完成标题生成。这段系统提示词约 180 字,中文大约对应 150 到 200 个 Token。里面“平台成立年份”“注册用户数”“品类范围”都不是生成标题的必要信息。优化后可以压缩为:
你是电商运营助手。根据商品信息生成商品标题。 规则: 1. 长度 20 到 30 字; 2. 包含品牌词、品类词、核心卖点; 3. 遵守广告法,禁用极限词。 商品信息如下:优化前 150 到 200 Token,优化后约 60 到 80 Token。单次调用省 100 Token,看起来不多,但如果每天调用几千次,省下的就是每月几百万 Token。
3.3 结构化提示词的额外收益
除了缩短长度,还要结构化。用清晰的分段和标记,让模型更快定位关键信息。例如用输入、要求、输出格式三个分段来组织 Prompt。这种做法的另一个好处是:模型会减少“猜测”成本,输出更稳定,从而降低因输出不符合格式而重试的次数。
4. RAG 场景的 Token 治理:从源头控制上下文体积
RAG 是 Token 消耗的大头。很多团队的 RAG 并没有做精细控制,结果就是:检索召回 20 个片段,每个片段 500 Token,光召回内容就 10000 Token。如果问题本身只需要其中 2 个片段,优化空间非常明显。
4.1 分块策略决定 Token 利用率
分块大小直接影响 Token 利用率。分块太小,单个片段语义不完整,模型理解需要更多片段,反而增加 Token;分块太大,片段中无关信息变多,单次调用可能超预算。
更通用的实践是按语义边界分块,而不是简单按固定字符数切。例如按段落、按标题、按 Markdown 结构切分,让每个块在语义上独立。
如果必须要固定长度切分,建议加入重叠区:
# 文件路径:chunk_splitter.py def split_text_by_chars(text, chunk_size=300, overlap=50): if len(text) <= chunk_size: return [text] chunks = [] start = 0 while start < len(text): end = start + chunk_size chunks.append(text[start:end]) if end >= len(text): break start = end - overlap return chunks text = "这里是一段很长的技术文档内容," * 50 chunks = split_text_by_chars(text, chunk_size=300, overlap=50) print(f"分块数量: {len(chunks)}") print(f"各块长度: {[len(c) for c in chunks]}")固定长度分块的问题是,可能在句子中间截断,导致语义割裂。更好的方案是优先在段落边界切,再按 Token 上限合并。
4.2 检索数量控制:从“多多益善”到“够用就好”
召回 Top K 的数量不要盲设 10 或 20。它应该和分块大小、模型窗口、任务类型一起评估。一个简单的指导原则是:
召回的片段总 Token 数 ≤ 模型上下文窗口的 30% 到 40%这样既给用户输入和模型输出留出空间,又不会因为召回过多引入噪声。检索结果不是越多越好。当 Top 20 里只有 3 个真正相关时,模型会被另外 17 个无关片段干扰,输出质量反而下降。
4.3 召回后的后处理与压缩
很多团队在向量检索之后,直接把召回片段拼接进 Prompt。这忽略了一个关键环节:后处理。
可以做三件事:
第一,重排序。向量检索只解决语义相似,不保证答案相关性。可以用更精细的模型或规则二次排序,把最相关的片段放在最前面。
第二,去除重复。多个片段可能包含相同内容。去重能直接省 Token。
第三,判断是否需要召回。有些问题不需要 RAG。比如用户只是说“帮我解释一下什么是多线程”。这时候直接让模型用自身知识回答即可,不必进入检索环节。给整个流程加一层“是否需要检索”的判断,能省下大量不必要的 Token。
4.4 完整示例:可控 Token 的 RAG 查询函数
# 文件路径:rag_query.py MAX_CONTEXT_TOKENS = 2000 def build_rag_prompt(query, retrieved_docs, system_prompt): used_tokens = count_tokens(system_prompt) + count_tokens(query) context_parts = [] for doc in retrieved_docs: doc_tokens = count_tokens(doc["content"]) if used_tokens + doc_tokens > MAX_CONTEXT_TOKENS: break context_parts.append(doc["content"]) used_tokens += doc_tokens context = "\n\n".join(context_parts) return f"{system_prompt}\n\n背景资料:\n{context}\n\n用户问题:{query}"这个函数在把召回片段拼接进 Prompt 之前,先做 Token 预算检查。超出预算的片段直接丢弃,而不是一股脑全塞进去。
5. 对话历史与长上下文管理:如何让 Token 在多轮会话中不失控
5.1 滑动窗口:只保留最近 N 轮
最简单也最有效的方法,是只保留最近几轮对话。例如只保留最近 6 轮,超过的旧消息直接丢弃。适用于大多数轻量问答场景。但它的问题是,模型可能会忘记用户早期提到的重要信息,比如用户的姓名、偏好、某项前提条件。
5.2 摘要压缩:用少量 Token 保留关键信息
更稳妥的方案是分层管理:早期对话用摘要保存在上下文里,最近几轮用完整文本。具体实现可以这样:
- 每 5 轮生成一次已有对话的摘要;
- 新请求的 Prompt = 摘要 + 最近 3 轮完整对话;
- 摘要只保留事实性信息,不保留寒暄和重复内容。
核心判断是:历史对话的价值密度并不均匀。90% 的历史内容是寒暄、确认、重复表达,真正对后续回答有用的可能只有几个事实。摘要的作用就是把这些事实提取出来。
5.3 关键信息结构化存储
比摘要更进一步,是在业务层维护一个“用户状态”对象。例如:
{ "user_id": "12345", "preferences": { "language": "中文", "style": "简洁", "city": "上海" }, "facts_mentioned": [ "在准备面试 Java 后端岗位", "有 3 年开发经验" ], "last_topic": "Spring Boot 事务失效排查" }每次请求只把这份结构化信息注入系统提示词,而不是把完整聊天记录都发给模型。这种做法在客服、教育、写作助手等场景效果很好。
5.4 对话管理器示例
下面这段代码演示了如何实现“摘要 + 最近 N 轮”的混合策略:
# 文件路径:dialog_manager.py class DialogManager: def __init__(self, max_keep_rounds: int = 4, summary_model=None): self.messages = [] self.summary = "" self.max_keep_rounds = max_keep_rounds self.summary_model = summary_model def add_message(self, role: str, content: str): self.messages.append({"role": role, "content": content}) if len(self.messages) > self.max_keep_rounds * 2: self.compress() def compress(self): older = self.messages[:self.max_keep_rounds * 2] self.messages = self.messages[self.max_keep_rounds * 2:] if self.summary_model: new_summary = self.summary_model.compress(self.summary, older) self.summary = new_summary def build_messages(self): if self.summary: summary_msg = { "role": "system", "content": f"历史对话摘要:{self.summary}" } return [summary_msg] + self.messages return self.messages这个类虽然简化了真实场景中摘要生成、并发写入等逻辑,但已经把 Token 优化的核心思想体现出来了:不让历史无限膨胀,用摘要替代原始内容,用最近消息保留细节。
6. 结构化输出与工具调用:用格式约束减少无效生成
6.1 输出格式控制:节省重复尝试成本
如果对模型输出没有硬性约束,它可能会在结果前后加上解释、补充、抱歉话术,或偶尔不按格式返回。这些情况可能不会显著增加单轮 Token,但会导致下游解析失败、脚本重试,整体成本翻倍。
更合理的做法是尽量使用模型的响应格式参数。例如 OpenAI 兼容接口的 JSON 输出模式,或 vLLM 部署时的 guided decoding。即使不用这些高级功能,也要在 Prompt 中明确输出结构,例如:
请严格输出 JSON,格式如下: {"title": "商品标题", "keywords": ["词1", "词2"], "reason": "创意依据"}6.2 工具调用中减小参数描述
在 Agent 场景中,工具定义的 JSON Schema 也会占用 Token。工具参数描述写得越长,每次调用时发送的 Token 就越多。一种常见的优化是:
- 只描述必要参数,不填写冗余说明字段;
- 把参数的示例值尽量缩短;
- 多个工具共用的参数描述,统一用“见系统提示词”来引用,不重复展开。
这里有一个权衡:参数描述太短可能导致模型理解不准确,导致功能调用错误和重试。建议先以 1 到 2 个真实调用作为验证,再逐步精简。
7. 系统层面的 Token 优化:缓存、批处理与模型选择
7.1 Prompt Caching:让前缀变成复用资产
部分大模型平台支持 prompt caching,即相同前置文本在一定时间内再次提交,不再全额计费。要利用这一点,技术上要满足“前缀稳定性”:
第一,系统提示词放在最前面。把会经常变化的用户输入放在后面,固定指令放在前面,保证公共前缀完整。
第二,尽量避免在系统提示词中拼接动态信息。动态内容放在用户消息或独立的“动态上下文”位置,否则每次前缀都不同,缓存无法命中。
第三,相同前缀的文本要完全一致。任何字符差异都会导致缓存失效。
7.2 批处理与业务聚合
如果大量请求是独立的、不需要实时响应的,可以打包成批量任务提交。批量接口通常有更低价格,还能统一控制并发。更常用的优化是在同一次请求内完成多个子任务,例如一次让模型完成“标题 + 摘要 + 关键词”三项生成,而不是调用三次模型。
7.3 模型选择:小模型优先
不是所有任务都需要旗舰模型的能力。简单分类、信息抽取、格式转换等任务,用更小的模型可能效果差异不大,但 Token 单价明显更便宜,响应也更快。
判断标准是:跑一批代表性用例,对比大模型和小模型在相同 Prompt 下的输出通过率。这个验证过程虽然耗时,但对长期成本影响巨大。
7.4 模型参数的影响
还有一个容易忽略的环节:温度、top_p 等采样参数本身不消耗额外 Token,但会影响重试率。采样参数设置过于随机,输出不稳定,重试次数就会增加,Token 消耗自然上升。在任务允许时,适当降低温度能减少格式错误和逻辑跳跃。
8. 成本估算与监控:把 Token 优化变成可量化指标
8.1 前置估算:上线前就能算出成本
在实际调用前,可以做一次静态估算。假设每天调用 10000 次,平均每次输入 2000 Token,输出 300 Token,那每天约消耗 2000 万输入 Token 和 300 万输出 Token。按月计算,乘以对应单价,就能得到一个可接受的成本区间。
8.2 运行时监控:记录每一笔 Token 消耗
在生产环境中,需要在应用层记录每次请求的输入和输出 Token 数,并写入日志或指标系统:
# 文件路径:token_monitor.py import time class TokenUsageRecord: def __init__(self): self.records = [] def log_request(self, api_name, prompt_tokens, completion_tokens, latency_ms): self.records.append({ "timestamp": time.time(), "api_name": api_name, "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens, "latency_ms": latency_ms }) def summary(self): total_prompt = sum(r["prompt_tokens"] for r in self.records) total_completion = sum(r["completion_tokens"] for r in self.records) return { "total_requests": len(self.records), "total_prompt_tokens": total_prompt, "total_completion_tokens": total_completion, "total_tokens": total_prompt + total_completion, }更进一步的方案是,为每一次业务请求设置 Token 预算,预算超限时触发告警。这样做的好处是,你不再等到月底看账单才发现成本失控,而是在当天就能看到异常。
8.3 建立基线指标
建议为项目建立三个基线指标:
| 指标 | 含义 | 优化目标 |
|---|---|---|
| 单次请求平均输入 Token | 每次调用发送给模型的 Token 数 | 持续下降或稳定 |
| 单次请求平均输出 Token | 模型生成的 Token 数 | 匹配任务需求,不冗余 |
| Token 有效率 | 真正被下游使用的 Token 占总 Token 比例 | 越高越好 |
“Token 有效率”是很多人不关注但很有价值的指标。一个 RAG 请求召回了 8000 Token,但模型真正参考的只有其中 2000 Token,Token 有效率就是 25%。通过调整召回数量和重排序策略,Token 有效率可以提升到 50% 以上。
9. Token 优化的常见误区与排查
9.1 误区一:把“少写 Prompt”等同于 Token 优化
缩减 Prompt 长度必须建立在“信息不缺失”的基础上。如果为了省 Token 把关键约束删掉,导致模型输出不合格、需要重写多次,整体成本反而更高。Token 优化的真正原则是:减少无价值信息,保留高价值信息。
9.2 误区二:只优化第一次请求,忽视多轮累积
很多团队只关注首轮问答时的 Token 数,却没有处理第二轮、第五轮、第十轮的累积增长。真正要控制的是“整个会话生命周期内的总 Token 消耗”,至少要做到历史裁剪或摘要化。
9.3 误区三:盲目把 RAG 召回数量降到极低
召回数量太少会导致模型缺少必要信息,输出只能靠模型固有知识。这个问题比 Token 浪费更严重。不要只看 Token 指标,还要看回答质量和准确性。
9.4 常见问题排查表
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 输入 Token 异常高 | 历史消息未裁剪 | 检查构建 messages 的逻辑,查看每条消息大小 | 引入对话管理器和摘要压缩 |
| RAG 请求超时 | 召回片段过多,模型处理慢 | 打印召回片段的 Token 数 | 限制召回数量,增加后处理 |
| 输出经常不符合格式 | 约束描述不够明确 | 检查 Prompt 中是否有输出格式示例 | 使用 JSON 输出模式或精简示例 |
| Prompt Caching 不生效 | 前缀不稳定 | 对比每次请求的前缀是否完全一致 | 固定系统提示词,动态内容后移 |
| 成本突增 | 某类任务输出过长或重试次数多 | 查看输出 Token 和重试记录 | 降低温度、增加输出长度上限、优化示例 |
| 模型回答质量下降 | 过度压缩上下文导致信息缺失 | 对比压缩前后的回答 | 增加摘要保留关键事实,或调整裁剪窗口 |
10. 最佳实践清单与工程建议
把 Token 优化落地到团队项目,建议按以下顺序推进:
第一步,建立 Token 观测。在没有数据之前不要做任何优化。先对每一次模型调用记录输入、输出 Token 数,按业务场景聚合出成本分布。这既能为优化提供方向,也能在优化后量化收益。
第二步,给 Prompt 做一次体检。检查系统提示词是否有冗余背景、重复指令、过长示例。重点看公共前缀,因为它影响缓存命中率。
第三步,改造 RAG 链路。调整分块策略,加入召回数量控制和 Token 预算检查,增加必要的后处理和重排序。
第四步,处理多轮对话。引入滑动窗口和摘要压缩,至少做到“最近 N 轮完整保留,更早历史摘要化”。如果业务有明确状态对象,优先用结构化信息代替原始会话记录。
第五步,系统侧配置。合理规划 Prompt 前缀顺序,评估小模型替代大型模型的可能性,为不同任务选择合适的模型策略。
除了流程,这里还想强调三个工程习惯:
- 把系统提示词和用户输入分开管理,不要拼成一个超长字符串再发给模型;
- 把 Token 消耗记录和业务日志放在一起,出现问题能快速定位到具体业务场景;
- 每次重大优化前,保留一组固定测试用例,用来对比优化前后的输出是否退化。
关于安全与合规的提醒:在生产环境中接入大模型,不要随意在 Prompt 中拼接来源不明的内容,尤其是外部输入的文本,可能包含注入指令。RAG 检索回来的文档内容也应该做过滤和脱敏,避免把敏感信息直接暴露给模型或写入日志。对涉及权限、支付、数据库变更等高风险操作,必须在应用层做鉴权和人工复核,不能单纯依赖模型的判断。
11. 总结与下一步实践
Token 优化并不是一个“一次性做完”的工作。当业务增长、Prompt 内容变化、基础模型升级时,Token 消耗结构都会随之变化,需要持续观测和调整。
这篇文章真正讲清楚了几件事:
- Token 是成本与性能的最小单位,它与字数不是一回事;
- Prompt 优化的关键不是越短越好,而是保留有用信息、去掉噪声;
- RAG 场景要同时控制分块大小、召回数量和 Token 预算;
- 多轮对话必须管理历史,不让会话无限膨胀;
- 系统层可以通过 Prompt Caching、批处理、小模型来降低单位成本;
- 监控指标和成本基线是对优化效果做评判的前提。
如果你现在正负责一个 LLM 应用,建议先花半小时做一件最简单的事:写一段脚本,统计最近 1000 次线上请求的平均输入 Token、输出 Token 和顶部分布。这个数据会让你立刻知道最该优化的是 Prompt、RAG 还是对话历史。
之后可以继续深入几个方向:更精细的检索重排序策略、摘要模型的选择与缓存、面向业务场景的语料结构化。这些每一步都能继续压降 Token 消耗,但每一步之前,都要先确认优化没有影响回答质量。
Token 优化的最后一条准则是:省 Token 必须为效果服务。当成本优化和输出质量冲突时,优先保证质量,再想办法通过工程手段降低成本。