先说一个自己踩过的坑。一开始我只做单轮问答把用户问题丢给大模型 API拿回结果就完事每月的 Token 账单小得几乎可以忽略。后来产品要支持连续对话我天真地以为无非就是把聊天记录存下来下次一起发过去。结果上线第二个月账单数字直接翻了几十倍——我才发现同一段历史对话每一轮请求都会原封不动地重新发给模型Token 消耗是逐轮累加的不是我想象中的增量保存。这个事让我彻底意识到从单轮到多轮根本不是加个数组存聊天记录那么简单它背后是上下文窗口的取舍、消息重复计费的逻辑、缓存命中的利用以及一整套可落地的上下文治理手段。这篇文章就围绕 Ark 类长上下文模型与 OpenAI 风格的 API聊聊我实测下来的一套上下文管理与 Token 成本分析思路。不管你是刚开始接 API 的新手还是已经在做客服机器人、Agent 编排的老手这篇文章应该都能给你一些可以直接抄作业的参考。1. 单轮调用的真相为什么它省 Token也注定走不远1.1 单轮接口背后的请求-响应模型几乎所有 OpenAI 风格的大模型 API底层调用都长一个样你往messages数组里塞角色和内容模型返回一条补全结果。最常见的三个角色是 system、user、assistant分别对应系统设定、用户输入、助手回复。单轮问答的本质就是只发一次请求请求里没有历史包袱。import os from openai import OpenAI client OpenAI( api_keyos.environ.get(API_KEY), base_urlhttps://your-gateway.example/v1, ) resp client.chat.completions.create( modelark-like-chat, messages[ {role: system, content: 你是某电商平台的客服助手回答要简洁。}, {role: user, content: 我想改一下我的套餐。}, ], ) print(resp.choices[0].message.content) print(resp.usage)这段代码里模型收到的上下文只有 system 提示词和当前这一条用户问题没有上一轮聊了什么的概念。API 本身是无状态的模型不记得任何历史。体现在账单上就是每次请求只花一份的钱自然便宜。从工程角度说单轮请求的成本极低、响应速度快、并发安全也好做——不存在两个人同时操作一个会话导致历史串位的问题。这也是很多早期问答类产品能靠极低 Token 成本撑起大量流量的原因。1.2 单轮的应用边界与多轮需求从哪来但单轮有一个致命缺陷它没办法处理需要依赖上下文才能回答的问题。我举几个场景你就明白了。用户问我的订单到哪了单轮接口可以答用户接着问那退款什么时候到如果模型不知道前一句里说的是哪笔订单它就只能瞎猜。客服、销售、售后这类连续对话场景模型必须知道用户前面说了什么。再比如 Agent 类应用模型需要先调用搜索工具、再调用计算器、再根据结果做判断每一步的结果都要放到下一轮输入里否则整个流程根本走不下去。所以多轮需求不是锦上添花而是从单轮问答走向真正的对话式产品的必经之路。但问题也随之处处而来历史消息到底保留多少每轮都发给模型Token 账怎么算得过来上下文窗口满了怎么办这也是为什么标题里把从单轮到多轮和Token 成本放在一起——这两件事本质上是矛盾的轮数越多历史越长Token 消耗越猛你既要让模型记得住又不想让账单爆掉。2. 多轮会话的连接件messages 数组、上下文窗口与逐轮膨胀2.1 多轮不是更聪明的单轮而是把历史塞进同一请求多轮对话的工程实现最直白的方式就是把所有历史消息都放进messages数组然后在每次请求时完整发送。伪代码大概是这样的history [ {role: system, content: 你是某电商平台的客服助手回答要简洁。}, ] def chat(user_input): history.append({role: user, content: user_input}) resp client.chat.completions.create( modelark-like-chat, messageshistory, ) assistant_reply resp.choices[0].message.content history.append({role: assistant, content: assistant_reply}) return assistant_reply这段代码看起来人畜无害跑起来就知道痛了。假设第一轮你发的内容是 1000 token第二轮请求时 messages 里已经有两轮的内容大约 2000 多 token第三轮 3000 多……每新增一轮请求体积就线性增加。第 20 轮时单次请求可能已经要发送 2 万 token 的历史内容。这还没算更隐蔽的情况。如果中间夹着工具调用messages 里还会混入 tool call 和 tool 返回结果这些 JSON 动辄上千 token如果用户上传了图片图片还会被转成视觉 token一张图可能吃掉几百到上千 token。历史体积的膨胀速度远超你凭直觉的判断。2.2 上下文窗口的硬边界1M 窗口能装多少轮模型能接收的上下文是有限的这个上限就是上下文窗口。市面上常见的窗口有 8k、32k、128k、200k以及标题里提到的 Ark 类长上下文模型的百万级窗口1M 甚至更高。窗口越大能装的轮数越多但成本也会跟着变。先给一个直观的换算概念。中文在大模型分词器里大约是一个汉字 1 到 2 个 token我按 1.5 来估算。假设一个典型的客服对话用户每条问题 200 字助手每条回复 400 字一轮合计 600 字折算下来大约 900 到 1000 token。再加上 system 提示词和一些转义符号按每轮 1000 token 计算上下文窗口去掉 system 和输出预留后可用空间大概能装多少轮完整对话8k约 6k约 6 轮32k约 24k约 24 轮128k约 110k约 110 轮200k约 170k约 170 轮1M约 800k约 800 轮注意这里的能装多少轮只是理论值。实际生产里输出也需要占用窗口空间你要给模型留出足够长的回复空间工具调用结果、检索返回的文档碎片都会挤占空间。所以我会把可用窗口打个六七折来用1M 窗口实际规划在 50 到 70 万 token 以内不要真的跑到清算线边缘。这里也顺便回答一个很多人问过的问题窗口越大是不是我就可以无脑把所有历史都塞进去我个人的答案是窗口大是底气不是挥霍的理由。百万窗口虽然能装下几百轮历史但模型对距离较远的早期内容注意力会衰减而且这些历史每一轮都要付钱。2.3 隐式压缩与显式截断真实对话变成什么样当消息超过窗口时你会被迫做取舍。最常见的做法是直接把最早的几条消息丢掉也就是截断。但这种做法有个隐蔽的问题被截断的历史里往往藏着用户最早的诉求和关键约定。举一个我在某个客服 Demo 里遇到的真实情况。用户刚开始说我要退掉 3 月 1 号下单的那双鞋然后跟助手来回聊了 20 轮换货细节。如果按窗口截断把最早几句删掉第 21 轮时模型可能根本不记得退鞋这回事面对用户问所以你们什么时候上门取件模型会一脸茫然。所以上下文管理从来不是等窗口满了再处理这种事后动作而是在设计多轮会话时就要想清楚哪些信息必须长期保留哪些信息过期就可以扔。这也是后面第四节要展开讲的核心内容。3. Token 成本的三层拆解单价、缓存与结构性浪费3.1 输入与输出为什么价格不一样先看计费模型。OpenAI 风格 API 的 Token 费用通常分两部分输入 tokenprompt 部分和输出 tokencompletion 部分。输出价格通常是输入价格的好几倍因为生成是逐个 token 推理的计算量远大于并行处理输入。Ark 类长上下文平台通常选择另一个策略把输入单价压得很低鼓励你把大段历史、长文档都塞进去然后按总 token 量收费。这意味着如果你做的场景是长文本分析、超长对话、文档问答长上下文模型会非常划算但如果你做的是高频短对话仍需要注意输出 token 的单价占比。为了统一讨论我定义一个虚拟计价模型假设计费项目虚拟单价元 / 百万 token输入 tokenA输出 tokenB通常为 2A 到 4A缓存命中 tokenC通常为 0.1A 到 0.2A实际各家模型价格差异很大但分析方法是一样的。你只要知道自己用的模型的 A 和 B 是多少就能套公式算成本def estimate_cost(input_tokens, output_tokens, cached_tokens, price_in, price_out, price_cache): cost ( input_tokens * price_in output_tokens * price_out cached_tokens * price_cache ) / 1_000_000 return cost3.2 缓存段带来的机会前缀命中省 90%很多 OpenAI 风格 API 现在支持一种隐式缓存机制如果请求的前缀通常是 system 提示词 开头几轮消息与历史请求完全一致这部分的输入 token 可以按缓存价格计费大约是正常输入价的十分之一甚至更低。这对多轮对话来说是决定性的利好因为前缀恰好是每次都要重发但内容不变的部分。但要注意缓存命中有几个前提前缀必须逐字节一致模型名一致请求内容顺序一致。只要 system 提示词里夹了一个时间戳、随机数或者你把前两轮的顺序调换了一下缓存就可能失效全部按正常输入价计费。这也是我在踩坑后总结的一条铁律system 提示词必须稳定能不改就不改即便要动态插入内容也尽量把可变内容放到前缀之后。结构上让不变的大块内容尽量堆在前面才能最大化命中缓存。3.3 逐轮重发的累计成本对比有无缓存的差距有多大我用一个模拟算账的方式把从单轮到多轮的成本膨胀量化出来。假设一个会话跑 100 轮每轮新增内容折算 1000 tokensystem 固定 500 token模型输出每轮平均 700 token。再看看两种策略策略一不做任何处理每轮发送全部历史也没有缓存命中策略二上下文前缀稳定历史全部命中缓存每轮只付新增内容的输入费用。我不带具体价格只看 token 量级指标策略一全量重发无缓存策略二前缀缓存命中第 1 轮输入15001500其中 1500 计费第 50 轮输入495001500 命中缓存 1000 新增计费第 100 轮输入995001500 命中缓存 1000 新增计费累计输入 token约 500 万约 10 万累计输出 token约 7 万约 7 万看清楚这个差距了吗同样是 100 轮对话有缓存和没缓存输入 token 差了接近 50 倍。策略一的 500 万 token 里绝大部分是被反复重发的历史消息真正有价值的新内容只有末尾那一小段。这就是我开头说账单翻了几十倍的原因——不是模型变贵了是我把同样的历史反复买了 100 次。3.4 为什么贵往往不是模型单价高而是重复发送历史把这句话单独拎出来说是因为我觉得这是很多人对 Token 成本最大的误解。看到一个模型单价高就急着换便宜模型但实际上在多轮对话场景里成本的炸弹藏在重复二字里。就算换成单价便宜的长上下文模型如果你每一轮都原封不动地重发几百轮历史账单照样起飞。所以做成本优化的第一动作永远是减少重复发送的量而不是纠结那一点点单价差。单价是乘法因子重复发送量是幂次放大后者才是真正烧钱的地方。4. 可落地的上下文管理策略滑动窗口、摘要与分层切分4.1 滑动窗口策略只保留最近 N 轮最简单也最不容易出错的策略就是维护一个固定大小的窗口只把最近 N 轮对话发给模型。窗口之前的消息不再进入 API 请求但可以继续保存在本地数据库里等用户明确问到的时候再按需捞回来。MAX_ROUNDS 8 SYSTEM_PROMPT 你是某电商平台的客服助手回答要简洁。 def build_messages(history, max_roundsMAX_ROUNDS): system {role: system, content: SYSTEM_PROMPT} # 每轮有 user 和 assistant 两条消息所以取最后 max_rounds*2 条 recent history[-(max_rounds * 2):] return [system] recent这种做法的优点很明显请求体量可控成本可控模型对近期内容的注意力也更集中。缺点是会丢掉早期信息那部分信息要么靠摘要承接要么等用户主动提出再补充。我在实际运营里还会加一个小细节窗口截断处插入一句轻微说明。比如在第一条可见消息前加一条 system 或 assistant 消息注意更早的对话内容已经被省略。如果用户提到的内容不在当前上下文中请向用户确认。这样能显著减少模型胡编乱造的概率。4.2 分层记忆用摘要压缩早期对话滑动窗口解决的是最近的对话怎么办摘要解决的是被扔掉的历史怎么保留。具体做法是当一个会话的轮数达到阈值我常用 12 到 20 轮把当前所有历史交给模型让它生成一段摘要然后把摘要作为一条新的上下文记录替换掉全部旧消息。下次请求时messages 变成system 摘要 最近几轮明文对话。def summarize_history(history): dialogue_text \n.join( f{m[role]}: {m[content]} for m in history ) prompt f 请把下面的对话压缩成 200 字以内的中文摘要保留 1. 用户的身份与核心诉求 2. 已经达成的关键结论 3. 待办事项和承诺的时间点 4. 不能丢失的订单号、地址、金额等硬信息 对话内容 {dialogue_text} resp client.chat.completions.create( modelark-like-chat, messages[{role: user, content: prompt}], ) return resp.choices[0].message.content注意摘要本身也是一次 API 调用会产生输出 token。所以摘要不能每轮都做合理阈值是窗口快满时或对话达到一定轮数时。另外摘要会丢失细节尤其容易丢数字和小约定。我的做法是摘要只负责语义记忆把订单号、地址、金额等结构化字段剥离到会话元数据里不走 LLM 摘要直接存变量。这样即使模型对早期细节失忆服务端也能在必要时把准确数据取出来回填。4.3 存储侧兜底把消息落库按需抬回先别急着把历史从 API 请求里删掉就完事——你得有地方找回来。我的标准架构是三段式消息流水全部落库会话内存只放最近窗口的内容当用户问到窗口之外的内容或者系统检索判断需要早期信息时再从库里取回相关片段拼进请求。这里可以简单用关键词匹配也可以上向量检索对大多数轻量场景来说关键词匹配加时间就近排序已经够用。比如用户第 30 轮问我第一句话说的那款鞋到底能不能退你用关键词鞋去库里捞最早的消息把它重新拼到本次请求里模型就能准确回答。这个思路比把所有历史都塞给模型让它自己找要便宜得多也精准得多。4.4 一条完整的成本降低流水线把这些策略串起来我现在的多轮请求处理流程大致是这样的请求入口 - 读取会话元数据 - 组装短期窗口最近 8 轮明文 - 判断是否需要摘要补充 - 按需检索历史片段 - 拼接最终 messages - 统一调用 API - 记录 usage 与费用每一步都在控制成本优先只发最近 8 轮明文把请求体量压到最小会话超过阈值时用摘要替换更早的明文替代无脑全量重发结构化字段订单号、地址、价格、时间全放会话元数据不塞进上下文只有用户明确涉及早期信息时才按需检索拼接而不是每轮都带上所有历史请求发送前检查 messages 总 token 量超过预算直接降级为摘要最近一轮。这五步走完我的多轮会话成本相比最初版本降了大约 80%。具体降多少取决于对话长度分布但思路是通用的。5. 观测与迭代让 Token 成本变得可视化5.1 从 response 的 usage 字段读取 token 数不观测就谈不上管理。好在 OpenAI 风格 API 每次响应都会带上 usage 字段这是我最先用来做数据分析的地方{ usage: { prompt_tokens: 18234, completion_tokens: 765, total_tokens: 18999, prompt_tokens_details: { cached_tokens: 15200 } } }注意cached_tokens这个字段不是所有模型都返回但如果有一定要记下来。它能直观告诉你这次请求里有多少输入 token 走了缓存低价通道。如果这个值长期偏低说明你的 system 提示词可能不稳定或者请求前缀变化太频繁缓存命中率出了问题。5.2 自家日志里的三张表光看单次响应的 usage 还不够我会在服务端把请求日志沉淀下来至少维护三张非常简单的表表记录内容主要用途请求明细表会话 ID、请求 ID、模型、输入 tokens、输出 tokens、缓存 tokens、估算成本、耗时按天核对费用定位异常高耗请求会话汇总表会话总轮数、累计 tokens、累计成本、是否触发摘要、是否触发截断看单个用户的成本上限是否失控模型损耗表按模型维度聚合日均 token 量、平均成本、缓存命中率决定要不要换模型或调 prompt这个表不用做得很复杂关键是数据要持续记录。我第一次做完这三张表就发现自己某条线上其实只有百分之二三十的高耗会话把它们的成本压下来整体立刻就降了。5.3 压测与基线先定一个能接受的单次成本上限在把多轮功能上线前我会跑一轮多轮对话压测。方法很简单准备一批模拟对话脚本按照用户提问 - 模型回答 - 再提问的节奏连续跑 50 轮、100 轮把每一轮的 prompt_tokens、completion_tokens 打点记录。压测之后给两个基线值单次请求成本上限以及单会话累计成本上限。比如我会定单次请求成本不超过 0.01 元单个会话累计不超过 0.5 元超过就强制触发摘要压缩或提示用户开新会话。这样做不是抠门而是防止出现用户挂机一整晚服务端默默重发了 500 轮历史这种异常账单。另一个压测时值得关注的点是耗时。发送的 tokens 越多响应延迟越高——尤其长上下文模型token 一大首字延迟明显上升。所以压测时除了看成本还要记录首 token 延迟。很多时候压 Token 预算本身就是压响应速度这两件事是一起影响的。6. 一些容易被忽视的 Token 隐性消耗6.1 工具调用结果和 Function Call 的 JSON 开销做 Agent 类多轮时messages 里会有大量 tool 消息。每个工具参数是 JSON工具返回也是 JSON一轮工具调用可能轻易吃掉 1000 到 3000 token而且这些 token 还不是用户能看到的对话内容。我的建议是工具返回只保留必要字段。很多外部 API 会返回一长串无关字段直接全文塞给模型就是浪费。用一段简单的清洗函数把返回体精简到模型真正需要判断的字段Token 消耗能立刻降下来。6.2 多轮里被反复带上的图片与长文档如果用户上传了一张图片OpenAI 风格接口通常会把图片转成视觉 token一张图大约几百到上千 token按窗口内的位置反复计费。长文档同理一份几十页的 PDF 塞进上下文就是几万 token。处理方式跟文本一样不能被窗口足够大迷惑。图片只在有明确引用需求的最近几轮里保留超过阈值就把图片描述文字化存在会话元数据里不再跟着每一轮请求走。长文档则优先走检索取出相关片段而不是全文重发。6.3 重试机制和请求异常导致的双倍消耗还有一个账很多人不算。当一次请求因为超时重试或者因为用户断线导致响应没有接收到、然后客户端又发了一遍同样的请求这部分重复消耗是不在正常对话轮数统计里的。我在日志里看到过同一条 prompt 连续打三次 API 的案例账单白付了两倍。所以生产环境一定要做幂等控制同一轮请求只发一次失败了最多重试有限次数并且重试之前确认上一次请求没有成功返回。更稳妥的做法是把模型的最后一条 assistant 回复缓存下来用户重连时直接读缓存不重新调用模型。7. 我的取舍原则长上下文模型不是万能药最后想聊聊我对要不要无脑上大窗口的判断。长上下文模型Ark 那类百万窗口、低价输入确实给了我很大的想象空间很多场景里我可以直接把整本手册丢进上下文做问答不用自己做检索。但对于高频多轮产品我仍然坚持混合策略长文本分析、文档问答、一次性长输入的任务用长上下文模型发挥低输入单价优势高频客服、Agent 多轮用小窗口模型加缓存加滑动窗口把逐轮成本压到最低复杂推理的最终生成用更强的高单价模型但只传给它精简后的必要上下文。这个策略的核心逻辑是Token 成本分析不只是一道乘法题更是一道选择题——不同任务的上下文构成完全不同你要为每个任务选择匹配的模型和匹配的上下文策略。做这一整套改造的过程中我最大的体会是上下文管理不是窗口快满了才想怎么办的应急方案而是从产品设计第一天就要考虑的架构问题。你把 system 提示词想得越稳定缓存命中率就越高你把会话元数据与模型上下文分离得越彻底Token 浪费就越少你记录 usage 越勤快异常账单就越早暴露。最后再分享一个小技巧每次调整完上下文策略不要只看总费用降了没有要看每次请求的输出 token 占总 token 的比例。这个比例越高说明你发给模型的输入里有效信息占比越大。如果这个比例长期萎缩说明你的历史仍然太重还没压到位。
阅读完成 · 觉得有帮助?