开发大模型应用的同学,最近可能都听过一个词:Tokenmaxxing。它的字面意思很好理解——“把 Token 数量拉到极限”。但真正值得讨论的并不是这个词本身,而是它背后指向的优化观:我们评估一个 AI 功能,到底应该看“模型输出了多少字”,还是看“模型解决了多少问题”?
近期微软在工程师群体中反复强调一个原则:“Tokenmaxxing is not what we are optimizing for”,也就是“Token 数量最大化并不是我们要优化的目标”。这句话看似反直觉,因为很多人在验收大模型效果时,默认把“输出长度”等同于“输出质量”。本文将围绕这句话拆解它的技术含义,从 Token 计费、Prompt 设计、输出约束、评估体系到工程实践,完整梳理一套可落地的大模型应用优化方案。
1. 背景概念:Tokenmaxxing 到底是什么
1.1 Token 与 Tokenmaxxing 的基本概念
先说一下 Token 是什么。大模型并不像人一样逐字阅读文字,而是先把文本切分成更小的片段,这些片段称为 Token(词元)。
- 在英文场景里,一个单词通常对应 1 到 2 个 Token。
- 在中文场景里,一个汉字通常对应 1 到 2 个 Token。
- 在代码场景里,一个英文单词、符号、空格都可能占用一个 Token。
Token 数量同时决定了三件事:
| 维度 | 与 Token 的关系 |
|---|---|
| 输入成本 | 请求中的 Prompt 和上下文越长,输入 Token 越多,费用越高 |
| 输出成本 | 模型生成的回答越长,输出 Token 越多,费用越高 |
| 延迟 | 模型每生成一个 Token 都耗时,输出越长,用户等待越久 |
所谓 Tokenmaxxing,指的是一种把“最大化生成 Token 数量”当作优化目标的倾向。典型表现包括:
- Prompt 里写“请尽量详细回答”。
- 模型已给出结论,还要补一句“再扩展一下”。
- 把 300 字能说清的问题,硬生生扩写成 1500 字。
- 对输出长度不做任何约束,生成完整代码时不考虑冗余。
这种做法的初衷不难理解:在不少人的直觉里,“答得多”总比“答得少”显得专业。但从工程角度看,它带来的是费用上涨、响应变慢、可读性下降,甚至幻觉概率增加。
1.2 微软说的“不是优化目标”应该如何理解
“Tokenmaxxing is not what we are optimizing for”这句话,并不是说“不要省 Token”,而是说:Token 消耗量只是一个观测指标,不是优化目标。目标应当是任务完成度、用户体验、业务效果;Token 数量是约束条件,不是评分标准。
举个例子。一个客服知识库问答系统,它的优化目标应该是:
- 用户能否快速找到解决方案?
- 答案是否准确,能否通过人工质检?
- 用户是否还需要二次提问?
而 Token 消耗量应该被当作成本项纳入考量:在达到上述目标的前提下,Token 越少越好。顺序不能反。
一旦把“Token 少”当作唯一目标,就会走向反面:提示词太短导致上下文缺失、回答太短导致用户看不懂。这就是为什么“不是优化目标”和“不需要省 Token”必须是两回事。
1.3 这篇文章能带给你什么
下面我会从三个实操角度,帮你建立一套可复用的大模型应用优化方法:
- Token 计数与成本分析:知道 Token 花在了哪里。
- 输出约束与 Prompt 设计:用结构化方式倒逼模型输出高质量短文本。
- 评估体系搭建:不靠“字数”判断好坏的工程化方法。
2. 优化目标拆解:质量、成本、延迟的三角关系
2.1 一个 AI 功能的三元组指标
任何接入大模型的功能,在评估时都可以拆成三个互相制约的维度:质量、成本、延迟。
| 指标 | 含义 | 常见度量方式 |
|---|---|---|
| 质量 | 输出是否符合语义、格式、事实、业务规则 | 人工评分、自动指标、线上转化率 |
| 成本 | 单次请求的输入输出 Token 费用 | 单次调用价格、月成本、单用户成本 |
| 延迟 | 从发起请求到返回首个 Token 的时间 | P50、P95 响应时间 |
当你知道一个功能需要同时优化三个维度时,就会理解为什么“多发 Token”不一定是好消息。如果你的模型在回复“如何配置 ODBC 连接 SQL Server”时,输出了 1000 字的背景介绍才进入正题,那么用户前 5 秒可能什么都没看到——这就是典型的延迟变差;同时你还要为那 1000 字里的 800 字无关内容付费,这是成本变差。
2.2 输出长度其实是代理指标
在软件工程里有一个词叫“代理指标”(Proxy Metric),指的是用一个容易测量的变量代替一个难以直接测量的目标。输出长度就是最典型的代理指标。
为什么大家会用它?因为评价“回答好不好”成本很高,需要人工阅读、评审、标注;而“回答多长”是肉眼可见的,甚至可以直接用 Token 数统计出来。
但代理指标的陷阱在于:它测得了过程,却测不到结果。一段 200 字的回答可能精准解决了问题,一段 2000 字的回答可能包含了三次自相矛盾的观点。如果你把所有 prompt 都加上“请详细回答”,实际上是在激励模型堆砌文字,而不是激励模型深度思考。
2.3 一个可落地的评分公式
在工程实践里,我建议把大模型输出质量拆成客观分和主观分:
综合得分 = 0.5 × 客观分 + 0.5 × 主观分其中:
- 客观分由规则自动计算,包括:关键词覆盖率、格式合规率、事实冲突数、输出长度惩罚项。
- 主观分由人工或更强模型打分,包括:语义相关性、逻辑连贯性、可执行性。
注意,输出长度只应该出现在“惩罚项”里,当长度显著超过参考回答时给予轻微扣分,而不是作为加分项。这样才能让团队把注意力放在“内容有效性”上,而不是“内容量”。
3. Token 计数实践:先知道 Token 花在哪了
3.1 使用 tiktoken 统计 Token 数
在动手优化之前,第一步是掌握 Token 统计工具。OpenAI 官方提供了一个轻量级库tiktoken,可以按模型对应的编码器计算 Token 数。很多国产模型平台也提供了类似的 Tokenizer 工具,原理相通。
示例代码(Python):
# 文件路径:token_counter.py import tiktoken def count_tokens(text: str, model: str = "gpt-4o") -> int: """按指定模型的编码器统计 Token 数。 不同模型的编码器可能不同,如果传入模型不支持, 会回退到 cl100k_base 编码器。 """ try: encoding = tiktoken.encoding_for_model(model) except KeyError: # 部分新模型未提前注册时,可手动指定编码器 encoding = tiktoken.get_encoding("cl100k_base") return len(encoding.encode(text)) if __name__ == "__main__": # 模拟一段中文 Prompt prompt = "请回答:什么是 Tokenmaxxing?它为什么不是优化目标?" print("Token 数量:", count_tokens(prompt))运行后你会得到一个整数,这就是该文本在当前编码器下的 Token 数。注意,中英文混合文本的 Token 切分差异很大,实际生产环境建议用真实业务文本做采样统计。
3.2 输入输出 Token 的计费模型
不同平台计费方式不同,但大部分 OpenAI 兼容接口都遵循“输入 Token 和输出 Token 分开计价”的规则。示意如下:
单次请求费用 = 输入 Token 数 × 输入单价 + 输出 Token 数 × 输出单价通常在单价上,输入 Token 比输出 Token 便宜,但没有简单到“只要减少输出就能省钱”。如果为了减少输出而增加大量系统提示词,输入费用反而上升。优化时必须看总费用,而不是孤立的输出长度。
3.3 定位 Token 黑洞
一个比较实用的做法,是在接口调用层统一打印日志:
# 文件路径:call_llm_logger.py import json import time def log_llm_call(messages, response): """记录一次 LLM 调用的核心元信息,便于后续成本分析。""" prompt_tokens = response.usage.prompt_tokens if response.usage else 0 completion_tokens = response.usage.completion_tokens if response.usage else 0 log_entry = { "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), "prompt_chars": sum(len(m.get("content", "")) for m in messages), "prompt_tokens": prompt_tokens, "completion_tokens": completion_tokens, "total_tokens": prompt_tokens + completion_tokens, } print(json.dumps(log_entry, ensure_ascii=False))通过日志,你可以定期分析哪些请求的completion_tokens远超预期。如果发现 80% 的成本都集中在某类问题,接下来就优先优化那类问题的 Prompt。
4. 实战一:用 max_tokens 与 stop 参数控制输出边界
4.1 max_tokens 应该怎么设置
max_tokens是接口中最基础的长度控制参数,它决定模型最多生成多少个 Token。很多人要么不设置,要么设置成 4096 或 8192 的很大值。
关于 max_tokens,正确的做法是结合任务类型设置上限:
| 任务类型 | 建议 max_tokens | 说明 |
|---|---|---|
| 短问答 | 300 - 500 | 控制模型废话 |
| 代码补全 | 800 - 1500 | 给足核心代码空间 |
| 文本摘要 | 500 - 800 | 摘要本身就不应该太长 |
| 长文档处理 | 2000 - 4000 | 需要输出完整结构化方案 |
示例代码:
# 文件路径:llm_call_controlled.py from openai import OpenAI client = OpenAI( api_key="your-api-key", base_url="https://api.openai.com/v1", ) response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是技术支持工程师,回答要简洁、准确。"}, {"role": "user", "content": "如何排查 Microsoft ODBC Driver 17 for SQL Server 的 Named Pipes 连接错误?"} ], max_tokens=400, temperature=0.3, ) print(response.choices[0].message.content)这里的关键不是把 max_tokens 设得多小,而是先有一个合理预期:这个任务的标准答案大概需要多少字。200 字能答清的问题,不要给 2000 字的额度。
4.2 stop 序列:让模型在“不该继续”的地方停下来
stop参数可以让模型遇到指定字符串时停止生成。使用场景非常广:
- 生成 JSON 时,遇到
}后停止。 - 生成列表时,遇到固定结尾标记后停止。
- 生成对话时,遇到“用户:”后停止。
示例:
response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "user", "content": "列出 3 条大模型应用优化建议,每条不超过 30 字。"} ], max_tokens=300, stop=["\n\n"], )不过要小心:stop 是基于字符串匹配的,如果输出文本本身包含该字符串,可能被意外截断。实际项目中要在测试集上验证 stop 的稳定性。
4.3 结构化输出比长文本更省心
另一个控制输出量的方式,是直接要求模型返回结构化 JSON。结构化输出具备天然边界,模型不会无限扩展,因为 JSON 的结构本身就是约束。
示例:
response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你只输出 JSON,不要输出任何额外文字。"}, {"role": "user", "content": "分析下面这段日志的异常原因,并给出修复建议。日志:Named Pipes Provider: Could not open a connection to SQL Server"} ], response_format={"type": "json_object"}, temperature=0, ) content = response.choices[0].message.content print(content)输出示例:
{ "error_type": "connection_failed", "possible_cause": "SQL Server 未启用 Named Pipes 协议或网络不通", "fix_suggestions": [ "检查 SQL Server 配置管理器中的 Named Pipes 协议", "确认防火墙放行 1433 端口", "检查客户端连接字符串中的 Server 名称是否可解析" ] }结构化的好处是:输出 Token 稳定可控,下游解析简单,测试断言方便。这也是很多生产级 AI 功能的标配做法。
5. 实战二:Prompt 约束工程设计
5.1 系统提示词里先定“输出策略”
如果你在每一条用户请求里反复强调“请简洁回答”,系统提示词也写得很随意,模型依然可能自由发挥。更好的做法是把输出策略直接放进系统提示词。
对比一下:
弱提示:
你是一个 AI 助手,请回答用户问题。强提示:
你是一个 AI 助手。回答要求: 1. 先给结论,再给依据。 2. 控制在 200 字以内,不重复背景信息。 3. 如果问题无法确定,明确说明“信息不足”。 4. 不要使用列表以外的修饰性语言。同样的用户问题,强提示下的输出 Token 通常会明显下降,而且回答更直接。
5.2 用“格式模板”替代“详细回答”
把“请详细回答”替换成“请按下面的模板输出”,是抑制 Token 膨胀的有效方式。
# 文件路径:prompt_template.py prompt = """ 请根据下面的信息撰写一条 Release 说明。 项目名称:{project_name} 版本号:{version} 主要变更: - 修复了 ODBC 连接超时问题 - 升级了 Microsoft Visual C++ Redistributable 运行时 输出模板: ## 版本 {version} ### 新功能 (不超过 2 条) ### 修复 (按列表输出) ### 注意事项 (如果没有则写“无”) """.format(project_name="DataService", version="2.3.1")模板把输出结构固定下来,模型不需要通过堆字数来“显得完整”,它只需要填槽。这种方式特别适合生成日报、周报、Release Note、接口文档等格式化内容。
5.3 长度提示词的效果测试方法
判断一段长度约束是否有效,不能只看一次结果。建议准备 20 到 50 条测试问题,分别运行“有约束”和“无约束”两版 Prompt,统计平均输出 Token 数、包含无关背景的比例、用户满意度。
| 测试项 | 无约束版本 | 有约束版本 |
|---|---|---|
| 平均输出 Token | 680 | 210 |
| 包含冗余开场白比例 | 65% | 8% |
| 用户直接拿到解决方案比例 | 45% | 82% |
只有当长度约束没有导致质量下降时,才值得正式上线。
6. 实战三:RAG 场景下的 Token 预算控制
6.1 检索内容不能“全都要”
在检索增强生成(RAG)应用里,Token 消耗的大头往往不是模型输出,而是上下文输入。把 10 篇相关文档全部塞进 Prompt,上下文可能要几万 Token,不仅花钱,还会造成“上下文中海捞针”导致模型注意力分散。
一个基本思路是:为单次请求设置输入 Token 预算。
# 文件路径:rag_token_budget.py from typing import List def filter_chunks_by_budget( chunks: List[str], max_input_tokens: int = 2000, token_counter=None ) -> List[str]: """按输入 Token 预算过滤文本片段。 chunks: 检索模块返回的文本片段 max_input_tokens: 上下文上限 token_counter: 可传入 tiktoken 的 count_tokens 函数 """ selected = [] total_tokens = 0 for chunk in chunks: chunk_tokens = token_counter(chunk) if total_tokens + chunk_tokens > max_input_tokens: break selected.append(chunk) total_tokens += chunk_tokens return selected这个函数的核心思想是贪心策略:按相关性排序后,依次加入片段,直到预算满载。片段不在多,而在于是否覆盖了用户问题中的关键实体和约束条件。
6.2 检索块越小,控制越灵活
如果一段文本本身有 3000 Token,即使只检索到 1 段,也可能超出预算。所以 RAG 在建模阶段就应该控制文本块大小。
- 常见做法:按段落或固定长度(如 500 到 800 字)切块。
- 切块时保留标题、章节编号等元信息,方便拼接。
- 检索后可以根据元信息做进一步压缩,比如只保留命中关键词附近的内容。
6.3 多路召回后的去重与压缩
很多 RAG 系统会同时走关键词召回、向量召回、重排模型三个通道。召回结果之间可能大量重叠,直接拼接会产生大量重复 Token。
建议增加一个轻量级去重层:
# 文件路径:dedup_chunks.py def dedup_chunks(chunks: List[str], max_len: int = 300) -> List[str]: """通过文本哈希去重,同时截断超长片段。""" seen = set() result = [] for chunk in chunks: # 取前 max_len 个字符作为指纹,降低重复率 key = chunk[:max_len] if key in seen: continue seen.add(key) result.append(chunk[:max_len]) return result多路召回本身是为了提升覆盖率,但去重层能显著降低输入成本,同时减少模型被重复内容干扰的概率。
7. 如何评估优化效果:不要用 Token 数作为唯一标准
7.1 定义质量评估维度
Token 数下降是否意味着优化成功,取决于质量是否保持不变或提升。我建议从四个维度评估:
| 维度 | 说明 | 评估方式 |
|---|---|---|
| 正确性 | 事实、参数、代码是否准确 | 规则检测 + 人工抽检 |
| 完整性 | 是否覆盖问题中的所有子项 | 关键词覆盖 + 人工 |
| 格式合规 | 是否符合 JSON / Markdown 等格式要求 | 解析器自动判断 |
| 可执行性 | 用户按回答能否真正解决问题 | 线上效果 + 测试集演练 |
7.2 自动化评估脚本示例
这里给出一个最简单的“长度 + 关键词覆盖”双重评估脚本:
# 文件路径:evaluate_output.py import json import re def evaluate_single_case(reference_answers, model_output): """根据参考答案关键词评估单条输出。""" output_lower = model_output.lower() hit_count = 0 for keyword in reference_answers: if keyword.lower() in output_lower: hit_count += 1 coverage = hit_count / len(reference_answers) token_count = len(re.split(r"[\s,,。;;]+", model_output)) return { "keyword_coverage": coverage, "word_count": token_count, "too_long": token_count > reference_answers["max_words"], } if __name__ == "__main__": case = { "reference_answers": ["Named Pipes", "防火墙", "连接字符串"], "max_words": 200, } output = "请检查 SQL Server 的 Named Pipes 协议是否开启,同时排查防火墙和连接字符串配置。" print(json.dumps(evaluate_single_case(case, output), ensure_ascii=False))这只是一个示例思路,生产环境建议用更细致的人工评分表或 LLM-as-Judge 方案。但核心原则相同:优化前后必须在同一套测试集上对比。
7.3 引入用户反馈作为最终标准
在线上系统里,最终评判标准是用户行为:搜索后是否点击、提问后是否重试、客服会话是否减少。如果 Token 数下降了,但用户重试率上升,那说明优化方向错了;反之,如果 Token 数下降,用户满意度提升,说明你正在做正确的优化。
8. 常见误区与排查清单
8.1 常见误区
| 误区 | 后果 | 正确做法 |
|---|---|---|
| 追求输出越长越好 | 成本上升、延迟增加、质量不稳定 | 根据任务类型预设 Token 上限 |
| max_tokens 设置过小 | 回答被截断,语义不完整 | 在测试集上观察 P95 输出长度 |
| Prompt 只写“请简洁” | 约束不够明确,模型依然自由发挥 | 给出模板或格式要求 |
| 只按 Token 数评估 | 忽略语义质量 | 综合关键词、格式、人工评分 |
| 把所有文档都塞进上下文 | 输入 Token 爆炸、噪音干扰 | 设置上下文预算并做精排 |
8.2 排查清单
当你发现模型输出过长、成本异常时,按以下顺序排查:
- 检查 max_tokens 是否设置,是否过大。
- 检查系统提示词中是否包含“请详细、请深入、尽量多写”等膨胀指令。
- 检查用户输入中是否自带“详细回答”这类诉求。
- 检查是否有多个召回片段重复拼接进上下文。
- 检查是否打开了需要额外输出大量推理过程的设置。
- 在日志中统计平均输出 Token 和 P95 输出 Token,确认问题是否普遍。
9. 工程实践与最佳实践建议
9.1 在代码层固化 Token 约束
不要把控制 Token 的希望完全寄托在产品经理或运营同学手工写 Prompt 上,而应在代码层固化约束。具体手段包括:
- 为每个调用场景设置默认 max_tokens。
- 通过 response_format 强制结构化输出。
- 在网关层统一打印 Token 日志。
- 对超长输出结果做截断或二次处理。
9.2 分场景维护 Prompt 版本
同一个模型在不同场景下的 Token 策略应该不同。建议把 Prompt 按场景拆成独立文件,并用配置中心或环境变量管理。
prompts/ ├── qa_short/ │ ├── system.txt │ └── user_template.txt ├── code_review/ │ ├── system.txt │ └── user_template.txt └── summary_report/ ├── system.txt └── user_template.txt这样每一处 Prompt 都能单独做 AB 测试,也不会出现“调了一个 Prompt 影响所有功能”的连锁问题。
9.3 建立成本看板
推荐把每次调用的model、prompt_tokens、completion_tokens、total_cost写入日志系统,然后按天、按用户、按功能聚合。当某个功能的 Token 消耗异常增长时,可以第一时间发现并定位到 Prompt 或检索策略变化。
9.4 不要忽略运行环境
大模型应用通常会运行在 Windows 或 Linux 服务器上,依赖 Python、tiktoken、openai等库。Windows 环境下经常出现与 Microsoft Visual C++ Redistributable 相关的运行时问题,尤其是安装或升级 Python 依赖时。
如果你在安装tiktoken、pydantic等带 Rust/C 扩展的库时看到类似的报错,建议先安装最新版的 Microsoft Visual C++ Redistributable,再重新安装依赖。这虽然不是 Token 优化问题,但却是实际落地过程中最常见的环境阻塞点。
10. 总结
“Tokenmaxxing is not what we are optimizing for”这句话的核心,并不是“不要节省 Token”,而是提醒开发者:真正要优化的永远是业务目标。
- 对于问答系统,优化目标是让用户更快拿到准确答案。
- 对于代码生成,优化目标是让代码可运行、可维护。
- 对于内容摘要,优化目标是让信息密度足够高、遗漏足够少。
Token 数量更像是汽车仪表盘上的“油耗”,而不是目的地。你当然要关注油耗,但方向盘指向哪里,才决定你是否能到达目的地。
如果你正被“模型输出太长、效果却不好”的问题困扰,建议从这几步入手:先统计 Token 消耗分布,再为场景设置 max_tokens 和 stop 参数,接着优化 Prompt 约束和检索召回,最后用一套固定测试集评估优化效果。如果你对结构化输出、RAG 上下文压缩或 LLM 评估体系搭建有更多问题,欢迎在评论区一起讨论。