多轮对话管理:你的上下文窗口正在被「蚕食」,每轮都在亏钱
🦞 一只用 AI Agent 搭副业产线的程序员
回忆一下我们在第三篇文章里说的:API 不会帮你「记住」任何东西。每一轮对话,你都得把整个历史重新发过去。
第 1 轮:10 条消息,500 token
第 10 轮:100 条消息,5000 token
第 50 轮:500 条消息,25000 token
你的 Agent 不是在做事情——是在烧钱,而且烧得越来越快。
这篇文章给你两套解决方案:滑动窗口和摘要压缩。带上代码,带上实测数据。
先看清楚问题有多大
我跑了一个模拟——Agent 跟 AI 进行 20 轮对话,记录每轮的 token 消耗:
轮次 累计消息数 输入 token 本轮花费(DeepSeek Pro) 第 1 轮 2 320 ¥0.0003 第 5 轮 10 1680 ¥0.0017 第 10 轮 20 3560 ¥0.0036 第 15 轮 30 5420 ¥0.0054 第 20 轮 40 7310 ¥0.0073 20 轮总花费:¥0.087 看起来不贵?换成 GPT-4o:
20 轮总花费:¥0.87换成 Claude Opus 4:
20 轮总花费:¥2.61一个 Agent 任务跑 20 轮很正常。如果你在用 Claude 跑一个每天 10 次任务的 Agent——一个月 ¥780?
更要命的是,这只是「聊天」。如果每一轮你还塞入了 RAG 检索的文档(5000 token/次),那数字要乘 5-10 倍。
方案一:滑动窗口——最简单粗暴的办法
思路:只保留最近 N 条消息,旧的全部丢掉。
packagecontexttypeSlidingWindowstruct{MaxMessagesint// 最多保留多少条消息}funcNewSlidingWindow(maxMessagesint)*SlidingWindow{return&SlidingWindow{MaxMessages:maxMessages}}func(sw*SlidingWindow)Trim(messages[]Message)[]Message{// 永远保留 System Promptiflen(messages)>0&&messages[0].Role=="system"{iflen(messages)<=sw.MaxMessages+1{returnmessages}// System Prompt + 最近 N 条start:=len(messages)-sw.MaxMessages result:=make([]Message,0,sw.MaxMessages+1)result=append(result,messages[0])// System Promptresult=append(result,messages[start:]...)returnresult}// 没有 System Prompt,直接截断iflen(messages)<=sw.MaxMessages{returnmessages}returnmessages[len(messages)-sw.MaxMessages:]}使用:
window:=NewSlidingWindow(10)// 只保留最近 10 条forround:=0;round<50;round++{// 构造本轮消息messages=append(messages,Message{Role:"user",Content:taskInput})result:=callLLM(messages,0.1,500)messages=append(messages,Message{Role:"assistant",Content:result})// 裁剪messages=window.Trim(messages)fmt.Printf("第 %d 轮: 消息数=%d, token 估算=%d\n",round,len(messages),estimateTokens(messages))}优点:简单,token 消耗恒定
缺点:旧信息永久丢失。如果用户在第 1 轮说了「我叫小王」,第 12 轮问「我叫什么?」——AI 已经忘了。
适合:短期交互(客服、代码生成辅助)、不需要长期记忆的场景
方案二:摘要压缩——保留信息密度
思路:当对话历史超过一定长度时,把旧的消息「压缩」成一段摘要。
typeSummaryCompressorstruct{MaxMessagesint// 超过这个数就压缩SummaryPromptstring// 摘要的 PromptllmClient*llm.Client}funcNewSummaryCompressor(maxMessagesint,client*llm.Client)*SummaryCompressor{return&SummaryCompressor{MaxMessages:maxMessages,llmClient:client,}}func(sc*SummaryCompressor)Compress(messages[]Message)([]Message,error){iflen(messages)<=sc.MaxMessages{returnmessages,nil}// 找出需要压缩的部分(中间部分,保留头尾)systemMsg:=messages[0]// System Prompt,不动recentMsgs:=messages[len(messages)-5:]// 最近 5 条,不动oldMsgs:=messages[1:len(messages)-5]// 中间部分,需要压缩// 把旧消息拼成文本varhistory strings.Builderfor_,msg:=rangeoldMsgs{history.WriteString(fmt.Sprintf("[%s]: %s\n",msg.Role,msg.Content))}// 让 AI 压缩成摘要summaryPrompt:=fmt.Sprintf(`将以下对话历史压缩为一段简洁的摘要(不超过 200 字)。 保留关键信息:人名、决策、承诺、数据、待办事项。 对话历史: %s 摘要:`,history.String())summary,err:=sc.llmClient.Chat([]Message{{Role:"user",Content:summaryPrompt},},0.1,200)iferr!=nil{returnnil,err}// 重建消息列表result:=[]Message{systemMsg}result=append(result,Message{Role:"system",Content:fmt.Sprintf("[对话历史摘要] %s",summary),})result=append(result,recentMsgs...)returnresult,nil}效果实测:
压缩前:35 条消息,约 12000 token 压缩后:1 条 System + 1 条摘要 + 5 条最近 = 7 条消息,约 800 token 压缩率:93% 信息损失: - 用户在第 2 轮说的名字 → 保留在摘要中 ✅ - 用户在第 3 轮提到的技术栈 → 保留在摘要中 ✅ - 用户在第 8 轮随口说的一句话 → 消失了 ❌ - 用户在第 12 轮说的具体数据 → 消失了 ❌(因为离得远且没有压进摘要)两种策略的对比实测
同一个任务—代码审查 Agent 跑 50 轮对话:
| 指标 | 无管理 | 滑动窗口(10) | 摘要压缩 |
|---|---|---|---|
| 50 轮总 Token | 187,000 | 24,000 | 31,000 |
| 每次成本(DeepSeek Pro) | ¥0.19 | ¥0.024 | ¥0.031 |
| 信息保留率 | 100% | ~20% | ~70% |
| 关键信息丢失 | 0 次 | 2 次 | 0 次 |
| 代码复杂度 | 无 | 10 行 | 40 行 |
无管理的最贵但信息最完整。滑动窗口最便宜但丢了 2 次关键信息(用户前面提过的需求,被滑动窗口裁剪掉了)。摘要压缩中间价位,关键信息都保留了。
混合策略:生产环境的选择
最佳实践是组合使用:
typeHybridManagerstruct{window*SlidingWindow compressor*SummaryCompressor thresholdint// 超过多少条消息触发压缩}func(hm*HybridManager)Manage(messages[]Message)([]Message,error){iflen(messages)<=hm.threshold{returnmessages,nil// 消息还不多,不做任何处理}iflen(messages)<=hm.threshold*2{returnhm.window.Trim(messages),nil// 消息较多,滑动窗口}// 消息很多,先压缩再滑动窗口compressed,err:=hm.compressor.Compress(messages)iferr!=nil{// 压缩失败,降级为滑动窗口returnhm.window.Trim(messages),nil}returnhm.window.Trim(compressed),nil}流程:
消息数 < 20 → 全部保留(成本可控,信息完整度优先) 消息数 20-40 → 滑动窗口,保留最近 20 条 消息数 > 40 → 压缩中间的旧消息 + 滑动窗口上下文预算意识
现在你对上下文管理的成本有了直观感受,我们来建立「预算意识」:
| 组件 | 占用 Token 预算 | 要不要省 |
|---|---|---|
| System Prompt | 100-500 | 尽量精简 |
| Few-shot 示例 | 200-500 | 只在必要时加 |
| RAG 文档 | 2000-8000 | 按相关性截断 |
| 对话历史 | 随轮次增长 | 用滑动窗口/压缩 |
| AI 输出 | 200-1000 | 设 MaxTokens 上限 |
你的上下文窗口不是无限的。每一段文字都在吃预算、都在花钱。写 Prompt 的时候,像写嵌入式程序一样精打细算。
一句话总结
多轮对话管理不是高级技巧——是做 Agent 的基本功。不管理上下文,你的 Agent 成本是 O(n²) 增长。用滑动窗口变成 O(1),用摘要压缩变成 O(log n)。
下一篇——模块二的收官之作——我把前面所有的 Prompt 工程知识打包成10 个拿来即用的模板。代码生成、代码审查、Bug 分析、日报生成……每个模板标注了适用场景和期望输出。
关注我,别错过。
🦞 一只用 AI Agent 搭副业产线的程序员
全平台同名:虾哥不加班
需要定制 AI 工具?来聊聊 → lob_ai源码:GitHub - lobster-bujiaban
