AI API Token 管理实践:几个真实场景里的成本优化思路 这篇继续围绕 Token 展开不过不再只讲概念而是结合几个实际场景来分析为什么请求次数不高Token 消耗却很大 为什么批量任务容易突然把额度打满 为什么聊天上下文越聊越贵 为什么知识库问答看起来只问一句话实际消耗很高这些问题在 AI 项目里都很常见。很多时候我们以为自己是在优化接口性能实际上更应该先优化 Token 使用方式。一、先明确一个问题Token 不是请求次数很多系统刚开始统计 AI 用量时习惯只看请求次数。例如今天请求 1000 次 昨天请求 800 次 调用量上涨 25%这个指标有参考价值但对于 AI API 来说远远不够。因为一次请求可能只消耗几十个 Token也可能消耗几万个 Token。举个简单例子。请求 A短文本分类输入这条评论情绪是正面还是负面 输出正面这种请求很短消耗很低。请求 B长文档总结输入一篇 2 万字文档 输出一份 1000 字总结这也是一次请求但成本完全不是一个级别。所以在 AI API 管理里不能只问今天调用了多少次更应该问每次平均消耗多少 Token 哪个任务消耗最多 输入 Token 多还是输出 Token 多 哪个 Key 的 Token 增长异常二、实例一批量摘要任务为什么突然消耗暴涨假设有一个后台任务用来给文章批量生成摘要。最开始的数据量不大每天处理 100 篇文章。代码可能类似这样async function batchSummary(articles) { for (const article of articles) { await callAI({ model: summary-model, messages: [ { role: user, content: 请总结下面这篇文章\n\n${article.content} } ] }); } }一开始运行没问题。后来业务方把数据源扩大了从每天 100 篇变成每天 3000 篇。如果没有 Token 预算和任务限流这个脚本很容易在短时间内消耗大量额度。三、这个场景应该怎么拆对批量任务我一般不会和线上业务共用同一个 Key。比较推荐这样划分prod_chat_api - 线上聊天业务 prod_kb_qa - 知识库问答 batch_article_summary - 批量文章摘要 batch_translate_job - 批量翻译任务 dev_local_test - 本地开发测试批量摘要任务单独使用batch_article_summary这样一旦它消耗异常不会影响线上聊天业务。如果所有任务都共用一个 Key批量任务一旦跑飞线上业务也可能受影响。四、给批量任务设置 Token 上限对于批量任务可以给它单独设置 Token 预算。例如batch_article_summary 每日上限500,000 tokens batch_translate_job 每日上限300,000 tokens dev_local_test 每日上限50,000 tokens prod_chat_api 每日上限2,000,000 tokens这样即使批量任务出问题也只是这个任务暂停不会影响整体服务。从工程角度看这种隔离非常重要。批量任务有几个特点调用频率高 单次输入长 容易循环执行 容易被重复触发 失败后可能重试所以它一定要单独管理。五、实例二知识库问答为什么 Token 消耗比想象中高知识库问答是 AI 应用里很常见的场景。用户看起来只是问了一句话这个系统支持 API Key 管理吗但后端真正发给模型的内容可能很长。例如系统提示词 你是一个专业的技术文档助手请基于资料回答用户问题。 资料 1 这里是 1000 字文档片段…… 资料 2 这里是 1200 字文档片段…… 资料 3 这里是 800 字文档片段…… 资料 4 这里是 1500 字文档片段…… 历史对话 用户之前问过…… 助手之前回答过…… 用户问题 这个系统支持 API Key 管理吗 回答要求 请用中文回答分点说明不要编造。用户只输入了十几个字但系统拼接出来的 prompt 可能已经几千字。这就是知识库问答 Token 消耗高的主要原因。六、知识库问答的 Token 优化思路知识库问答里最重要的不是限制用户问题长度而是控制上下文。可以从几个方面优化。1. 限制检索片段数量不要每次都塞很多片段。例如原来取 10 段const chunks searchResults.slice(0, 10);可以先改成取 4 段const chunks searchResults.slice(0, 4);很多时候前几段高相关内容已经足够回答问题。2. 限制单个片段长度即使只取 4 段每段如果很长也会消耗很多 Token。可以做简单截断function limitText(text, maxLength 1200) { if (!text) return ; return text.length maxLength ? text.slice(0, maxLength) : text; }拼接上下文时const context chunks .map(item limitText(item.content, 1200)) .join(\n\n);3. 不要无脑带历史对话知识库问答不一定每次都要带完整历史。可以只保留最近几轮或者只保留和当前问题相关的历史。例如function getRecentMessages(messages, maxCount 6) { return messages.slice(-maxCount); }如果历史对话太长可以先总结成一段摘要再放入上下文。七、工具体验前面这些问题如果全部自己写日志、做 Key 隔离、做 Token 统计当然也可以实现但维护成本不低。我最近体验的是斑马 API这类 AI API 统一入口工具它比较适合用来做 Key 管理、模型接入、用量统计和团队共享。比如一个项目一个 Key、一个批量任务一个 Key再配合 Token 记录和额度控制整体会比所有服务直接拿上游 Key 调用更清楚。目前新用户有一个月体验权益邀请新用户也有额外体验时长。https://bmapi.020212.xyz/register?affYU55ECFS8AF2八、实例三聊天应用为什么越聊越贵聊天应用还有一个隐藏成本上下文。很多聊天系统会把历史消息一起传给模型。一开始可能是这样[ { role: system, content: 你是一个 AI 助手 }, { role: user, content: 帮我解释一下 API Token } ]消耗不高。但聊了几十轮之后消息可能变成[ { role: system, content: 你是一个 AI 助手 }, { role: user, content: 第一轮问题... }, { role: assistant, content: 第一轮回答... }, { role: user, content: 第二轮问题... }, { role: assistant, content: 第二轮回答... }, { role: user, content: 第三轮问题... }, { role: assistant, content: 第三轮回答... } ]如果每次都带完整历史越聊越贵是必然的。尤其是这些场景代码调试 论文阅读 合同分析 长文改写 需求讨论 知识库连续追问上下文会越来越长。九、聊天上下文应该怎么控制可以分几个层次处理。1. 简单做法只保留最近 N 轮例如只保留最近 6 轮对话function keepRecentRounds(messages, rounds 6) { return messages.slice(-rounds * 2); }这种方式简单直接适合大多数普通聊天场景。2. 进阶做法旧对话做摘要如果用户聊了很久可以把早期对话压缩成摘要。结构可以变成[ { role: system, content: 以下是之前对话摘要用户主要在讨论 API Token 管理、Key 隔离和成本统计。 }, { role: user, content: 最近一轮问题... } ]这样既保留上下文又不会无限增加 Token。3. 按任务决定上下文长度不同任务对上下文要求不同。闲聊保留少量历史即可 代码调试需要保留关键错误和代码 文档分析更关注当前文档 知识库问答更关注检索结果 写作润色通常不需要太多历史不要所有场景都用同一套上下文策略。十、实例四前端重复提交导致 Token 浪费有些 Token 消耗不是后端逻辑导致的而是前端交互没处理好。常见问题包括按钮没有 loading 状态 用户连续点击生成 页面刷新后重复提交 自动保存触发 AI 分析 输入框每次变化都调用 AI 网络慢时用户多次重试比如一个“生成摘要”按钮如果没有防重复点击用户可能连续点几次。前端可以简单处理let loading false; async function handleGenerate() { if (loading) return; loading true; try { await generateSummary(); } finally { loading false; } }这类保护很基础但很有用。尤其是 AI 接口成本比普通接口更高前端重复提交会直接造成 Token 浪费。十一、后端也要做幂等和去重只靠前端不够后端也要做保护。比如同一篇文章生成摘要可以根据文章内容生成 hash。import crypto from crypto; function createHash(text) { return crypto .createHash(sha256) .update(text) .digest(hex); }然后用这个 hash 做缓存 Keyasync function getSummary(article) { const cacheKey createHash(article.content); const cached await cache.get(cacheKey); if (cached) { return cached; } const result await callAI(article.content); await cache.set(cacheKey, result); return result; }这样同一篇文章多次请求时可以直接返回缓存结果。适合缓存的场景包括文章摘要 关键词提取 商品标题生成 固定模板文案 文档分类 批量翻译不太适合缓存的场景包括强实时问答 个性化聊天 依赖最新上下文的回答 用户每次输入都不同的任务十二、实例五Prompt 模板过长导致长期成本增加Prompt 模板也会带来隐形成本。很多项目一开始的系统提示词很短你是一个专业助手请回答用户问题。后来不断加规则你是一个专业助手请回答用户问题。 回答必须准确。 不知道就说不知道。 请使用 Markdown。 请分点说明。 请不要编造。 请保持语气自然。 请给出示例。 请避免重复。 请按照指定格式输出。规则越加越多系统提示词越来越长。如果这个接口每天调用几万次哪怕每次多 300 个 Token长期成本也很明显。所以 Prompt 模板也要定期整理。可以做几件事删除重复规则 合并相似要求 不同任务拆不同模板 简单任务不要使用复杂提示词 输出格式尽量简洁例如分类任务不需要复杂 prompt。原来可能写成你是一个专业文本分析助手请仔细阅读用户输入判断它属于哪一种类型。 请确保你的判断准确避免输出无关内容。 请不要解释太多只需要返回分类结果。 分类包括咨询、投诉、建议、其他。可以简化成判断文本类型只输出一个分类咨询、投诉、建议、其他。效果可能差不多但 Token 更少。十三、实例六输出内容不受控也会增加成本有些任务并不需要模型输出长文。比如情绪分类 意图识别 关键词提取 标题生成 标签生成 是否违规判断如果不限制输出模型可能会解释一大段。例如你只想要投诉模型却返回这段文本属于投诉类型因为用户表达了明显的不满情绪并且提到了服务体验问题所以可以判断为投诉。这对业务没有必要还增加 Token。可以在 prompt 里明确要求只输出分类结果不要解释。或者输出 JSON{ type: 投诉 }对于短任务输出越稳定后端解析越容易Token 也更可控。十四、一个比较完整的 Token 日志结构如果要排查 Token 问题日志一定要记录得足够细。可以参考这种结构{ request_id: req_20250101_001, api_key: batch_article_summary, project: content_center, task_type: article_summary, model: summary-model, prompt_tokens: 2800, completion_tokens: 500, total_tokens: 3300, latency_ms: 4200, status_code: 200, cache_hit: false, created_at: 2025-01-01 10:30:00 }有了这些字段就可以分析哪个 Key 消耗最高 哪个任务平均 Token 最高 哪个模型输出最长 缓存命中率是多少 失败请求是否也产生消耗 哪类请求耗时最长如果只记录“请求成功/失败”后面基本没法做成本优化。十五、一个 Token 异常排查流程假设某天发现 Token 消耗突然上涨。可以按这个顺序查。第一步按 Key 聚合prod_chat_api8% prod_kb_qa12% batch_article_summary260% dev_local_test3%如果某个 Key 特别明显就先查它。第二步看请求次数batch_article_summary 请求次数从 300 次涨到 1800 次说明任务量变大了或者重复触发了。第三步看平均 Token平均 total_tokens 从 1200 涨到 3500说明不只是请求变多了每次请求也变长了。第四步拆输入和输出prompt_tokens 增长明显 completion_tokens 基本稳定这说明问题主要在输入。可能原因文章全文变长 上下文片段变多 历史记录被带进去了 Prompt 模板变长了 原来传摘要现在传全文第五步看缓存命中cache_hit 从 60% 降到 5%可能说明缓存 Key 设计变了或者输入内容每次都多了动态字段导致缓存失效。例如请总结这篇文章。当前时间2025-01-01 10:30:00如果每次都带当前时间即使文章一样hash 也不同缓存就无法命中。十六、一些容易忽略的 Token 浪费点整理一下常见问题每次请求都带完整历史对话 知识库检索片段过多 单个文档片段过长 Prompt 模板越写越长 简单任务输出太多解释 按钮重复点击 失败请求无限重试 批量任务没有限速 开发环境和生产环境共用 Key 缓存设计不合理 日志只记录请求次数不记录 Token这些问题单独看都不大但叠加起来成本会非常明显。十七、我现在比较推荐的设计方式如果是一个长期运行的 AI 项目我会优先考虑这些设计1. 每个项目独立 Key 2. 每个环境独立 Key 3. 批量任务单独 Key 4. 为 Key 设置 Token 预算 5. 日志记录 prompt_tokens 和 completion_tokens 6. 长文本任务限制上下文 7. 聊天任务控制历史轮数 8. 简单任务限制输出格式 9. 重复任务加缓存 10. 前端防重复提交 11. 后端做输入长度校验 12. 定期分析异常消耗这些并不复杂但能避免很多后期问题。AI API 接入不是只要能调通就结束了。真正要长期运行Token 管理、Key 隔离和日志统计都需要提前设计。十八、总结这篇主要通过几个实例讲 Token 管理批量摘要任务容易因为循环和数据量扩大导致消耗暴涨 知识库问答用户问题短但上下文可能很长 聊天应用历史对话越多Token 消耗越高 前端重复提交一次点击问题可能变成多次请求 Prompt 模板越写越长会带来长期成本 输出不受控简单任务也可能产生多余 Token我现在越来越觉得AI 项目的成本控制不是上线后再优化而是设计阶段就应该考虑。一开始就把 Key、Token、日志、缓存、限流这些基础能力设计好后面会少很多排查和重构成本。对于个人 Demo 来说直接调用接口当然最快。但对于长期项目、团队协作、多模型接入、批量任务和生产环境统一管理会更稳妥。