一、先说结论:Token 限制不是“省钱”,而是 Agent 工程化的生死线

很多初学者理解 Token 限制,第一反应是:

“是不是为了少花点调用大模型的钱?”

是,但不只是。

在真实的 AI 应用里,限制 Token 至少有四个核心原因:

  1. 控制成本
  2. 避免超过模型上下文窗口
  3. 提高响应速度
  4. 降低模型被无关信息干扰的概率

尤其是做 Agent 的时候,Token 会爆炸得非常快。

比如一次智能运维 Agent 的调用流程可能是:


用户问题
+ 系统提示词 System Prompt
+ 多轮历史对话
+ RAG 检索出来的知识片段
+ Planner 生成的计划
+ Executor 调用工具的结果
+ Replanner 的重新规划内容
+ 最终回答要求

如果你不限制 Token,最后传给大模型的内容可能非常臃肿。

表面上看是“信息更多了”,实际上可能是:


上下文太长 → 模型抓不住重点 → 回答变慢 → 成本变高 → 甚至直接报错

所以,Token 管理本质上不是一个“小优化”,而是 AI Agent 从 Demo 走向工程化必须解决的问题。


二、Token 到底是什么?别只理解成“字数”

Token 可以简单理解成模型处理文本时的最小单位。

它不完全等于字,也不完全等于词。

比如中文里,一个汉字可能接近一个 Token,也可能多个字组成一个 Token;英文里,一个单词可能被拆成多个 Token。

举个简单例子:


我正在学习 Spring AI

这句话在人眼看来是几个词,但模型内部可能会拆成类似:


我 / 正在 / 学习 / Spring / AI

而下面这句话:


MessageChatMemoryAdvisor

虽然只是一个类名,但可能会被拆成多个 Token:


Message / Chat / Memory / Advisor

所以在 AI 项目中,不能简单用“字符数”判断 Token 数量。

更重要的是,Token 不只计算用户输入,还包括:


System Prompt
用户问题
历史对话
RAG 检索内容
工具调用结果
模型输出内容

也就是说,模型的一次调用,其实是一次完整上下文的 Token 消耗。


三、为什么做 RAG 之后 Token 特别容易爆?

RAG 的核心流程是:


用户问题 → 检索知识库 → 拼接相关文档 → 交给大模型生成回答

问题在于,很多人刚开始写 RAG 时,会有一个误区:

检索出来的内容越多,回答越准确。

这句话只对一半。

如果检索内容质量高,确实能提升回答准确性。

但如果你把一堆相关性一般、重复、噪声很大的 chunk 都塞进去,模型不一定更聪明,反而可能更迷糊。

比如用户问:


Spring AI 中 ChatMemory 的作用是什么?

你检索出了 10 个 chunk:


chunk1:ChatMemory 基础介绍
chunk2:MessageChatMemoryAdvisor 代码
chunk3:RedisChatMemoryRepository 配置
chunk4:Prompt 封装流程
chunk5:ChatClient Builder 使用
chunk6:MCP 工具注册
chunk7:RAG 检索流程
chunk8:SSE 流式响应
chunk9:模型参数配置
chunk10:Docker Compose 启动 Milvus

看起来都和 Spring AI 有点关系,但真正对问题有用的可能只有前 3 个。

如果全塞进去,就会出现:


有用信息少
无关信息多
Token 消耗大
模型注意力被稀释

这就是 RAG 中非常典型的 Token 浪费。

所以 RAG 不是“检索越多越好”,而是:


召回要够多,送给模型要够精。

也就是:


先多路召回 → 再过滤/重排 → 最后只拼接最有价值的上下文

四、Agent 场景下,Token 爆炸更严重

如果说 RAG 的 Token 爆炸来自“文档内容太多”,那 Agent 的 Token 爆炸就更复杂。

因为 Agent 不只是问答,它还会:


思考
规划
调用工具
读取工具结果
重新规划
继续执行
总结输出

比如一个运维 Agent 处理问题:


用户:为什么今天订单服务响应变慢了?

Agent 可能执行:


1. 分析用户问题
2. 判断需要查询监控指标
3. 调用 Prometheus 查询接口
4. 分析 CPU、内存、QPS、RT
5. 调用日志查询工具
6. 读取错误日志
7. 判断是否需要查询数据库慢 SQL
8. 调用数据库工具
9. 生成诊断报告

每一步都会产生上下文。

如果每一步都把完整历史带上,最后上下文可能变成这样:


系统提示词
+ 用户原始问题
+ 第一次思考
+ 第一次工具调用参数
+ 第一次工具返回结果
+ 第二次思考
+ 第二次工具调用参数
+ 第二次工具返回结果
+ 第三次思考
+ 第三次工具调用参数
+ 第三次工具返回结果
+ ...

这时候 Token 消耗会非常夸张。

更危险的是,工具返回结果通常很长。

比如日志工具返回 200 行日志,里面可能只有 5 行有用,但你如果全塞给模型,就会导致:


Token 暴涨
响应变慢
成本升高
关键异常被淹没

所以 Agent 工程里有一句很重要的话:

工具返回结果不能直接丢给模型,必须先做压缩、筛选和结构化。


五、Token 限制的核心思想:给上下文做“预算管理”

我们可以把一次模型调用看成一场考试。

模型的上下文窗口就像一张答题纸,空间有限。

你不能把所有资料都抄上去,而是要挑最有价值的内容。

一次请求的 Token 预算可以拆成:


总 Token 预算
= System Prompt 预算
+ 历史对话预算
+ RAG 知识预算
+ 工具结果预算
+ 用户问题预算
+ 模型输出预算

比如模型最大上下文是 32K Token,你不应该全部拿来塞输入。

因为还要给模型留输出空间。

可以设计成:


总上下文窗口:32000 tokens

System Prompt:2000
历史对话:6000
RAG 知识:12000
工具结果:6000
用户问题:1000
模型输出预留:5000

这就是上下文预算。

不是哪里内容多就塞哪里,而是每一类内容都有额度。

一旦超过,就要裁剪、压缩、摘要或者丢弃。


六、最常见的 5 种 Token 限制策略

1. 限制历史对话窗口

多轮对话中,历史消息不能无限保留。

最简单的方式是滑动窗口:


只保留最近 N 轮对话

比如只保留最近 10 条消息:


MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
        .chatMemoryRepository(new InMemoryChatMemoryRepository())
        .maxMessages(10)
        .build();

这种方式简单直接,适合普通聊天场景。

但它的问题是:


早期重要信息可能被丢掉

比如用户一开始说:


我这个项目用的是 Spring AI Alibaba + Milvus

后面聊了很多轮,这条信息可能被挤掉。

所以只用滑动窗口还不够。

更好的做法是:


短期记忆:保留最近对话
长期记忆:把重要信息摘要后存储

2. 历史摘要压缩

当历史对话太长时,可以让模型先生成一个摘要。

原始历史可能是:


用户前面聊了 RAG、Milvus、Spring AI、ChatMemory、MCP、ToolCallback、SSE 等内容。

压缩后变成:


用户正在学习 Spring AI Alibaba,项目包含 RAG、ChatMemory、MCP 工具调用和 SSE 流式响应,目前重点关注 Agent 工程化落地。

这样就能把几千 Token 压缩成几百 Token。

工程上可以设计成:


最近 10 条消息:原文保留
更早的历史:摘要保存

最终拼接上下文:


System Prompt
+ 长期摘要
+ 最近消息窗口
+ 当前用户问题

这比单纯保留全部历史更合理。


3. 限制 RAG 检索内容数量

RAG 中常见参数是 TopK。

比如:


SearchRequest searchRequest = SearchRequest.builder()
        .query(userQuestion)
        .topK(5)
        .build();

这里的 topK=5 就是限制最多返回 5 个知识片段。

但 TopK 不是越大越好。

如果 chunk 太大,TopK=10 可能已经非常长。

比如:


每个 chunk 800 tokens
TopK = 10
总计 = 8000 tokens

如果加上历史对话、系统提示词、工具结果,很容易超过预算。

所以 TopK 要结合 chunk 大小一起看。

一个比较稳的策略是:


chunkSize:500~800 tokens
TopK:3~5
必要时加 Rerank

也就是说,先多召回,再精挑细选,不要一股脑塞给模型。


4. 对工具返回结果做裁剪

Agent 中工具返回结果尤其要控制。

错误示范:


String logs = logTool.queryLogs(serviceName);
prompt.append(logs);

这等于把所有日志直接塞给模型。

更好的方式是先过滤:


List<String> errorLines = logs.stream()
        .filter(line -> line.contains("ERROR") || line.contains("Exception"))
        .limit(20)
        .toList();

然后再结构化:


日志摘要:
1. 订单服务在 14:03 出现大量 TimeoutException
2. 错误集中在 createOrder 接口
3. 同时数据库连接池出现等待超时
4. 最近 10 分钟错误量从 5/min 上升到 230/min

模型真正需要的不是 500 行日志,而是有价值的异常摘要。

所以工具调用结果一定要经过:


过滤 → 聚合 → 摘要 → 再进入上下文

5. 限制模型输出长度

除了输入 Token,输出 Token 也要限制。

如果不限制输出,模型可能啰嗦很久。

在模型参数中通常可以设置:


maxTokens
maxOutputTokens

比如伪代码:


ChatOptions options = ChatOptions.builder()
        .maxTokens(1000)
        .temperature(0.7)
        .build();

对于不同任务,可以设置不同输出长度。

比如:


普通问答:800 tokens
代码解释:1500 tokens
报告生成:3000 tokens
标题生成:200 tokens

不要所有场景都用同一个 maxTokens。

否则简单问题浪费 Token,复杂问题又不够用。


七、Token 限制不是简单截断,乱截会出问题

很多人一听限制 Token,就想到:


超过长度就 substring 截断

这在 AI 项目里是很危险的。

比如原文是:


数据库连接池耗尽的原因不是 MySQL 宕机,而是接口调用链中存在大量慢查询,导致连接长时间无法释放。

如果你粗暴截断成:


数据库连接池耗尽的原因不是 MySQL 宕机,而是……

模型可能误解,甚至产生完全相反的判断。

尤其是代码、日志、配置文件,不能随便从中间截。

错误示例:


spring:
  ai:
    dashscope:
      api-key:

如果截断在这里,模型就不知道下面还有什么配置。

所以 Token 裁剪要遵循几个原则:


优先按语义块裁剪
优先保留标题、结论、异常、参数
不要从代码块中间截断
不要从 JSON/YAML 中间截断
不要截断关键因果关系

更好的方式是:


按段落切
按 chunk 切
按日志事件切
按函数/类切
按配置块切

八、RAG 中的 Chunk 设计,本质上也是 Token 管理

很多人以为 chunkSize 只是切片参数,其实它和 Token 控制关系非常大。

如果 chunk 太小:


优点:检索更精确
缺点:上下文不完整,模型可能看不懂

如果 chunk 太大:


优点:上下文完整
缺点:Token 消耗大,召回噪声多

比如一篇技术文档:


什么是 ChatMemory
MessageChatMemoryAdvisor 的作用
Redis 持久化记忆
SessionId 隔离

如果 chunk 太小,可能切成:


MessageChatMemoryAdvisor 会自动从 ChatMemory 中取出历史消息。

这句话单独看能理解,但缺少上下文。

如果 chunk 太大,可能把 ChatMemory、RAG、MCP 全放进一个 chunk,检索时容易带入噪声。

比较合理的做法是:


chunkSize:500~800 tokens
overlap:50~100 tokens

overlap 的作用是防止上下文被切断。

比如:


上一段结尾:MessageChatMemoryAdvisor 会在调用模型前……
下一段开头:把历史消息拼接到 Prompt 中。

如果没有 overlap,这个语义就断了。

所以 chunk 设计不是随便切文本,而是在做上下文预算规划。


九、Agent 项目里可以怎么设计 Token 管理模块?

如果项目复杂一点,不建议把 Token 限制逻辑散落在各个 Service 里。

可以抽象一个上下文管理器:


ContextManager

它负责:


1. 接收用户问题
2. 加载历史对话
3. 加载长期摘要
4. 加载 RAG 检索内容
5. 加载工具执行结果
6. 统一计算预算
7. 生成最终 Prompt

结构可以类似:


public class ContextManager {

    private final int maxInputTokens = 24000;
    private final int reservedOutputTokens = 4000;

    public Prompt buildPrompt(UserQuery query,
                              List<Message> recentMessages,
                              String memorySummary,
                              List<Document> retrievedDocs,
                              List<ToolResult> toolResults) {

        // 1. 添加系统提示词
        // 2. 添加长期摘要
        // 3. 添加最近对话
        // 4. 添加 RAG 文档
        // 5. 添加工具结果摘要
        // 6. 超过预算时裁剪低优先级内容

        return new Prompt(/* messages */);
    }
}

这里有一个关键点:

不同上下文内容应该有优先级。

比如:


最高优先级:当前用户问题
高优先级:系统提示词、关键约束
中优先级:最近对话、RAG 高分文档
低优先级:较早历史、低相关文档、冗余工具结果

当 Token 超预算时,不是随机删,而是按照优先级删。

可以设计成:


1. 永远保留当前问题
2. 永远保留系统提示词核心规则
3. 优先删除低相关 RAG 文档
4. 再压缩工具结果
5. 再裁剪历史对话
6. 最后才压缩长期摘要

这才是工程化的 Token 管理。


十、一个更真实的 Prompt 拼接示例

错误写法:


你是一个智能助手。

历史对话:
{全部历史}

知识库:
{全部检索结果}

工具结果:
{完整日志}

用户问题:
{question}

这种写法最大的问题是没有预算,没有优先级,没有结构。

更好的写法:


你是一个面向 Java 后端与 AI Agent 项目的技术助手。
回答时优先基于给定上下文,不要编造不存在的信息。

【用户长期背景摘要】
{memory_summary}

【最近对话】
{recent_messages}

【相关知识片段】
只包含和当前问题最相关的 Top 5 文档:
{retrieved_docs}

【工具结果摘要】
只保留关键异常、指标变化和结论:
{tool_result_summary}

【当前问题】
{question}

【回答要求】
1. 先给结论
2. 再分层解释
3. 结合 Java/Spring AI 项目说明
4. 不要输出无关背景

这个 Prompt 看起来更长,但其实更省 Token。

因为它减少了废话和噪声,让模型更容易抓住重点。


十一、Token 限制和“模型幻觉”也有关系

很多人以为幻觉只和模型能力有关。

其实上下文设计不好,也会增加幻觉。

比如你给模型塞了太多无关内容:


ChatMemory
MCP
Docker Compose
Milvus
Redis
SSE
ToolCallback

用户只是问:


ChatMemory 和 RedisChatMemoryRepository 有什么关系?

但上下文里混入太多东西,模型可能会把 MCP、RAG、Redis、Session 全混在一起讲,最后回答变得很散。

这不是模型完全不会,而是你给它的信息太乱。

所以 Token 限制还有一个隐藏价值:

通过减少无关上下文,降低模型胡乱联想的空间。

RAG 里尤其明显。

高质量上下文应该是:


少而准
结构清晰
和问题强相关
带有明确来源

而不是:


多而杂
内容重复
相关性模糊
没有优先级

十二、不同场景下 Token 限制应该怎么配?

普通聊天场景

重点是历史对话控制。

推荐:


最近 10~20 条消息
长期摘要
输出限制 800~1500 tokens

适合:


学习助手
客服问答
个人助理

RAG 知识库问答

重点是控制检索内容。

推荐:


chunkSize:500~800 tokens
overlap:50~100 tokens
TopK:3~5
必要时 Rerank
输出限制:1000~2000 tokens

适合:


文档问答
企业知识库
项目资料查询

Agent 工具调用场景

重点是工具结果压缩。

推荐:


工具原始结果不直接进 Prompt
先做过滤、聚合、摘要
每个工具结果设置最大 Token
保留关键字段和结论

适合:


AIOps
数据分析 Agent
自动化运维 Agent
多工具协作 Agent

报告生成场景

重点是输出预算。

推荐:


输入内容先结构化
输出 maxTokens 适当放大
分段生成
必要时采用 map-reduce 总结

适合:


日报
诊断报告
技术总结
CSDN 草稿生成

十三、Token 管理的一个实战套路:分层压缩

对于复杂 Agent,不要指望一次模型调用解决所有问题。

可以采用分层压缩:


第一层:原始数据
第二层:局部摘要
第三层:全局摘要
第四层:最终回答

比如日志分析:


原始日志 10000 行
↓
按服务聚合异常
↓
每个服务生成摘要
↓
汇总成故障诊断上下文
↓
交给模型生成最终报告

这样做的好处是:


避免一次性塞爆上下文
减少无用 Token
让模型逐层聚焦

这其实也是很多 Agent Framework 背后的思想。

不是所有信息都直接进入最终 Prompt,而是先被处理成更适合模型理解的结构。


十四、在 Spring AI 项目中可以怎么落地?

在 Spring AI / Spring AI Alibaba 项目中,可以从几个地方入手。

1. ChatMemory 控制历史窗口


MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder()
        .chatMemoryRepository(new InMemoryChatMemoryRepository())
        .maxMessages(20)
        .build();

这一步解决的是:


历史对话不能无限增长

2. RAG 检索控制 TopK


SearchRequest searchRequest = SearchRequest.builder()
        .query(question)
        .topK(5)
        .build();

这一步解决的是:


不能把所有知识片段都塞给模型

3. 工具结果先摘要再返回

比如日志工具不要直接返回完整日志:


public ToolResult queryErrorLogs(String serviceName) {
    List<String> logs = logService.query(serviceName);

    List<String> errors = logs.stream()
            .filter(line -> line.contains("ERROR") || line.contains("Exception"))
            .limit(30)
            .toList();

    return ToolResult.builder()
            .summary("发现 30 条关键异常日志,主要集中在订单创建接口。")
            .details(errors)
            .build();
}

这一步解决的是:


工具结果太长、太乱

4. Prompt 模板中明确上下文边界


【长期摘要】
{summary}

【最近对话】
{recentMessages}

【知识库内容】
{documents}

【工具结果摘要】
{toolSummary}

【当前问题】
{question}

这一步解决的是:


上下文结构混乱

5. 按任务类型设置不同输出长度

比如:


public ChatOptions buildOptions(TaskType taskType) {
    return switch (taskType) {
        case SIMPLE_QA -> ChatOptions.builder()
                .maxTokens(800)
                .temperature(0.5)
                .build();

        case CODE_EXPLAIN -> ChatOptions.builder()
                .maxTokens(1800)
                .temperature(0.4)
                .build();

        case REPORT -> ChatOptions.builder()
                .maxTokens(3000)
                .temperature(0.6)
                .build();
    };
}

这一步解决的是:


不同任务共用同一个 Token 配置

十五、Token 限制最容易踩的坑

坑 1:只限制输出,不限制输入

很多人只设置 maxTokens,以为就安全了。

但实际上,输入上下文一样可能超限。


输入太长 → 请求失败
输出太短 → 回答残缺

输入和输出都要控制。


坑 2:RAG TopK 设置过大

TopK 太大不一定更准。

尤其是 chunk 本身很长时,TopK=10 可能已经非常夸张。

RAG 要追求:


相关性,而不是数量。

坑 3:工具返回原始数据

日志、数据库查询结果、监控指标,不能原样全部进入 Prompt。

应该先变成:


异常摘要
趋势变化
关键字段
可能原因

坑 4:历史对话无限堆叠

多轮对话如果不做窗口和摘要,迟早会爆。

正确方式是:


最近消息窗口 + 长期摘要

坑 5:用字符数代替 Token 数

字符数只能粗略估算。

尤其是代码、英文类名、JSON、YAML、日志,很容易和真实 Token 数差距很大。


十六、我现在对 Token 限制的理解

之前我对 Token 的理解可能还停留在:


Token = 大模型计费单位

但现在结合 RAG、Agent、ChatMemory、工具调用之后,会发现 Token 更像是:


大模型的上下文资源

它决定了模型一次能看到多少信息。

而 Agent 工程化真正难的地方,不是简单调用一次模型,而是如何决定:


哪些信息该给模型
哪些信息不该给模型
哪些信息要原文保留
哪些信息要摘要压缩
哪些信息要完全丢弃

所以 Token 限制背后,其实是上下文工程。

一个好的 AI 应用,不是把所有东西都塞给模型,而是把最有价值的信息,以最清晰的结构,放到模型最容易理解的位置。

这才是 Token 管理真正的意义。


十七、最后总结

Token 限制不是一个简单的参数配置,而是一套完整的工程设计。

在 RAG 中,它决定知识片段怎么切、怎么召回、怎么拼接。

在 ChatMemory 中,它决定历史对话怎么保留、怎么摘要、怎么压缩。

在 Agent 中,它决定工具结果怎么过滤、怎么总结、怎么参与下一步推理。

如果不做 Token 管理,AI 应用很容易变成:


能跑 Demo
但慢、贵、不稳定、不准确

如果做好 Token 管理,系统才有可能变成:


上下文清晰
成本可控
响应稳定
回答更准
工程上可维护

所以限制 Token 不是限制模型能力,而是在帮模型聚焦。

真正成熟的 Agent,不是“什么都记住”,而是“知道什么时候该忘,什么时候该保留,什么时候该压缩”。

这也是 AI Agent 从玩具 Demo 走向真实项目时,必须补上的一课。

更多推荐