news 2026/9/28 19:42:38

一文看懂 Agent 对话历史压缩:用 TaoToken 统一 Key 打通摘要法上下文窗口治理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文看懂 Agent 对话历史压缩:用 TaoToken 统一 Key 打通摘要法上下文窗口治理

1. Agent 长对话为什么一定会撑爆上下文窗口

先说结论:Agent 的 messages 数组是只增不减的,只要任务够复杂,任何上下文窗口都会被填满。这不是模型能力问题,是架构决定的。

我拿一个真实任务举例:让 Agent 去分析 nginx 日志,提取 error,统计近一天内每半小时的 error 数量、每个接口的请求量,按数量降序排序,最后写入 statistic.txt。这个任务听起来不复杂,但 Agent 实际执行时会经历这些步骤:先 ls 找日志文件,再 grep 提取 error 行,然后 awk 按时间窗口聚合,再 sort 排序,中间可能还要 cat 看文件格式、head 确认字段、wc 统计行数。每一轮工具调用,messages 至少新增两条消息(assistant 的 tool_call 和 tool 的返回结果),而 grep 和 cat 的输出动辄几十上百行。

算一笔账:假设每轮工具输出平均 800 tokens,10 轮就是 8000 tokens,加上 system prompt、历史对话、工具定义,轻松突破 15000 tokens。如果用的是本地部署的小模型,context window 可能只有 4096 或 8192,跑三四轮就满了。就算用云端大模型,窗口有 128K,一个复杂任务跑 50 轮循环,照样能塞满。

有人会说:现在模型窗口越来越大了,还需要压缩吗?我的实测感受是,窗口变大只是把爆炸时间往后推,没有解决根本问题。三个原因:第一,token 费用是按输入量算的,历史越长每轮请求越贵;第二,超长上下文会让模型注意力分散,容易在无关信息里迷失重点;第三,响应速度会明显变慢,因为模型要处理的输入变多了。

所以上下文压缩不是可选项,是 Agent 工程化的必修课。这篇就聚焦摘要法这一条主线,把配置、阈值、提示词模板、验证动作全部拆开讲清楚,同时演示怎么用 TaoToken 的统一 Key 把 LLM 通道接起来,让压缩逻辑跑通。

2. 用 TaoToken 统一 Key 接入 LLM 通道

在写压缩代码之前,得先把 LLM 调用通道搭好。Agent 的压缩逻辑需要频繁调用 LLM 来生成摘要,如果每次都要切换不同的 API Key、不同的 base_url,维护成本会很高。TaoToken 的思路是提供一个统一的 API 入口,你只需要一个 Key,就能在多个模型之间切换,压缩用的摘要模型和主任务用的模型可以分开配置。

TaoToken 是什么:它是一个 LLM API 聚合通道,提供统一的 OpenAI 兼容接口。你可以把它理解成一个"API 路由器",底层对接了多种模型,上层暴露标准的 /v1/chat/completions 接口。对 Agent 开发者来说,好处是不用为每个模型单独写适配代码,换模型只改一个 model 字段。

适合谁用:正在做 Agent 长对话治理、需要频繁调用 LLM 做摘要压缩、或者想在多个模型之间做成本对比的开发者。如果你只是偶尔调一次 API,可能感受不到统一 Key 的价值;但如果你在跑 Agent 循环,每轮都要调 LLM,统一通道能省很多事。

接入前你需要准备的东西:一个 TaoToken 账号,一个 API Key。Key 在控制台的 API Keys 页面生成,地址是 https://taotoken.net/console/api-keys 。生成后保存好,后面配置里要用。

这里要区分两个地址:官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api ,注意 API 地址后面不加 UTM 参数,直接用于代码里的 base_url。

如果你还没决定用哪个模型做摘要,可以先去模型对话页面试一下不同模型的摘要效果,地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。摘要任务对模型要求不高,小模型也能胜任,关键是提示词要写清楚。

3. 可复制的 config.toml 与 settings.json 配置骨架

配置分两块:一块是 TaoToken 的接入配置,一块是压缩策略的参数配置。我习惯把接入信息放在 config.toml,把压缩阈值和提示词放在 settings.json,这样调参的时候不用动代码。

先看 config.toml:

# config.toml - TaoToken 接入配置 [llm] base_url = "https://taotoken.net/api" api_key = "sk-your-taotoken-key-here" main_model = "gpt-4o-mini" # 主任务模型 summary_model = "gpt-4o-mini" # 摘要专用模型,可以用更便宜的 timeout = 60 max_retries = 3 [agent] max_iterations = 30 # Agent 最大循环次数 compact_enabled = true # 是否开启压缩

再看 settings.json,这里放压缩策略的核心参数:

{ "compact": { "threshold": 20, "keep_recent": 6, "summary_prompt": "Summarize the following conversation history. Keep all important facts, file paths, command results, and decisions. Be concise but don't lose critical details.", "summary_prefix": "[Previous conversation summary]: ", "ack_message": "Understood. I have the context from our previous conversation. Let me continue." } }

参数解释一下。threshold 是触发压缩的消息条数阈值,超过 20 条就压缩。keep_recent 是保留最近多少条不压缩,设为 6 意味着最近 3 轮对话(每轮 user+assistant 两条)保持原样。summary_prompt 是发给 LLM 的摘要指令,要求保留事实、文件路径、命令结果和决策。summary_prefix 是摘要插入 messages 时的前缀,让模型知道这是历史摘要。ack_message 是压缩后插入的 assistant 确认消息,保持对话角色交替的完整性。

为什么 threshold 设 20、keep_recent 设 6?这是我试过几组参数后的经验值。threshold 太小会导致压缩过于频繁,每次压缩都要调一次 LLM,反而增加开销;太大则压缩效果不明显。20 条大约对应 10 轮工具调用,是一个比较自然的压缩点。keep_recent 设 6 是因为最近几轮对话往往包含当前任务的上下文,压缩掉会导致模型"失忆",保留 6 条能覆盖最近 3 轮的关键信息。

如果你用的是本地小模型,context window 只有几千 tokens,建议把 threshold 降到 10,keep_recent 降到 4。如果是云端大模型,可以适当放宽到 30 和 8。

4. 摘要法压缩的核心实现与提示词模板

配置准备好之后,核心逻辑就是一个 compact_messages 函数。它的职责很明确:检查消息条数,没超阈值就原样返回,超了就压缩旧消息、保留最近消息、重新组装。

import json import toml from openai import OpenAI # 加载配置 config = toml.load("config.toml") settings = json.load(open("settings.json")) client = OpenAI( base_url=config["llm"]["base_url"], api_key=config["llm"]["api_key"] ) COMPACT_THRESHOLD = settings["compact"]["threshold"] KEEP_RECENT = settings["compact"]["keep_recent"] SUMMARY_PROMPT = settings["compact"]["summary_prompt"] SUMMARY_PREFIX = settings["compact"]["summary_prefix"] ACK_MESSAGE = settings["compact"]["ack_message"] SUMMARY_MODEL = config["llm"]["summary_model"] def compact_messages(messages): if len(messages) <= COMPACT_THRESHOLD: return messages system_msg = messages[0] old_messages = messages[1:-KEEP_RECENT] recent_messages = messages[-KEEP_RECENT:] old_text = "" for msg in old_messages: role = msg.get("role", "unknown") content = msg.get("content", "") if content: old_text += f"[{role}]: {content}\n" summary_response = client.chat.completions.create( model=SUMMARY_MODEL, messages=[ {"role": "system", "content": SUMMARY_PROMPT}, {"role": "user", "content": old_text} ] ) summary = summary_response.choices[0].message.content return [ system_msg, {"role": "user", "content": f"{SUMMARY_PREFIX}{summary}"}, {"role": "assistant", "content": ACK_MESSAGE}, *recent_messages ]

这段代码有几个关键点。第一,system_msg 永远保留,因为它是 Agent 的核心指令,压缩进摘要会丢失基础设定。第二,old_messages 是要被压缩的部分,recent_messages 是保留原样的部分,切分位置在 messages[1:-KEEP_RECENT]。第三,摘要调用用的是 summary_model,可以和主任务模型不同,摘要任务对模型能力要求不高,用便宜的小模型就行。

摘要提示词模板是压缩效果的关键。我用的模板是:

Summarize the following conversation history. Keep all important facts, file paths, command results, and decisions. Be concise but don't lose critical details.

这个模板强调了三件事:保留事实(facts)、保留文件路径(file paths)、保留命令结果(command results)。为什么强调这些?因为 Agent 后续决策依赖这些具体信息。比如前面 grep 出来的日志路径、awk 统计出的数字、用户指定的输出文件名,这些如果丢了,Agent 就得重新执行工具去获取,压缩就白做了。

如果你做的是结构化任务,比如订票、查数据,可以把提示词改成要求输出 JSON 格式:

{ "summary_prompt": "Summarize the conversation into a JSON object with keys: entities, state, intent. Keep all concrete values." }

这样摘要结果本身就是结构化的,后续解析更方便。

压缩在 Agent 循环里的调用位置也很重要。我的做法是在每轮循环开始前检查一次:

def run_agent(user_message, max_iterations=30): messages = [ {"role": "system", "content": "You are a helpful agent..."}, {"role": "user", "content": user_message} ] for i in range(max_iterations): messages = compact_messages(messages) # 每轮开始前检查 response = client.chat.completions.create( model=config["llm"]["main_model"], messages=messages, tools=tools ) # ... 处理工具调用,追加消息到 messages

这样 messages 的数量会像锯齿波一样:涨到阈值 → 压缩回去 → 继续涨 → 再压缩。永远不会超过阈值太多,Agent 可以持续工作下去。

5. 验证压缩效果:token 占用对比与请求测试

代码写完了,怎么验证压缩真的生效了?我一般做两个动作:一是打印压缩前后的消息条数和 token 估算,二是实际发一次请求看返回是否正常。

先加一段验证代码:

def estimate_tokens(text): # 粗略估算:英文约 4 字符 1 token,中文约 1.5 字符 1 token return len(text) // 3 def print_compact_stats(messages_before, messages_after): before_count = len(messages_before) after_count = len(messages_after) before_tokens = sum(estimate_tokens(str(m.get("content", ""))) for m in messages_before) after_tokens = sum(estimate_tokens(str(m.get("content", ""))) for m in messages_after) print(f"压缩前: {before_count} 条消息, 约 {before_tokens} tokens") print(f"压缩后: {after_count} 条消息, 约 {after_tokens} tokens") print(f"压缩率: {(1 - after_tokens / before_tokens) * 100:.1f}%")

跑一个模拟场景:构造 25 条消息,其中包含几段长文本(模拟工具输出),然后调用 compact_messages,打印统计。预期结果是消息条数从 25 降到 9(1 条 system + 1 条摘要 + 1 条确认 + 6 条最近消息),token 占用减少 60% 到 80%。

实测下来,一个包含 10 轮工具调用的对话,压缩前约 12000 tokens,压缩后约 3000 tokens,压缩率 75% 左右。摘要本身消耗约 500 tokens 的输入和 200 tokens 的输出,但这是一次性开销,后续每轮请求都省下了 9000 tokens 的输入。

第二个验证动作是发一次真实请求,确认压缩后的 messages 能被模型正常理解:

def test_compressed_context(): messages = build_long_conversation() # 构造长对话 compressed = compact_messages(messages) response = client.chat.completions.create( model=config["llm"]["main_model"], messages=compressed ) print(response.choices[0].message.content)

如果模型能基于摘要继续回答,说明压缩没有丢失关键上下文。如果模型回答"我不知道之前发生了什么",说明摘要提示词需要调整,要更强调保留事实和决策。

这里有个细节:压缩后插入的 assistant 确认消息("Understood. I have the context...")很重要。它保持了对话角色的交替,让模型知道摘要已经被"接受"了。如果省略这条,messages 会变成 user(摘要)后面直接跟 user(最近消息),角色不交替,有些模型会报错或行为异常。

6. 压缩场景下的常见报错与排查

压缩逻辑跑起来之后,容易遇到几类问题,我按出现频率排一下。

第一类:摘要调用超时或返回空。原因是 old_text 太长,摘要模型处理不过来。排查方法是打印 old_text 的长度,如果超过 10000 字符,考虑分批摘要或者先截断。解决方式是在 config.toml 里把 summary_model 换成上下文窗口更大的模型,或者把 threshold 调小,让每次压缩的旧消息少一些。

第二类:压缩后模型"失忆",重复执行已完成的步骤。原因是摘要丢失了关键状态信息。排查方法是把摘要内容打印出来,看是否包含任务进度、已完成步骤、待办事项。解决方式是修改 summary_prompt,明确要求保留 task progress 和 completed steps。我常用的增强版提示词是:

Summarize the conversation. Keep: 1) all file paths and command results, 2) task progress and completed steps, 3) pending actions, 4) user preferences and constraints. Be concise.

第三类:messages 角色顺序错乱导致 API 报错。原因是压缩后组装的消息列表角色不交替。排查方法是打印压缩后每条消息的 role,确认是 system → user → assistant → user → assistant 的交替顺序。解决方式是确保摘要消息用 user 角色,确认消息用 assistant 角色,最近消息保持原有角色。

第四类:压缩频率过高导致 token 费用不降反升。原因是 threshold 设得太小,每几轮就压缩一次,摘要调用的开销超过了节省的输入 token。排查方法是统计压缩次数和摘要调用总 token。解决方式是把 threshold 调大,比如从 10 调到 20 或 30,让每次压缩处理更多旧消息,摊薄摘要成本。

第五类:工具调用消息在压缩后丢失 tool_call_id,导致后续请求报错。原因是摘要只保留了 content,没有保留 tool_calls 结构。排查方法是检查 old_messages 里是否有带 tool_calls 字段的消息。解决方式是在压缩时跳过带 tool_calls 的消息,或者把它们的内容提取出来放进摘要文本,但不要保留原始结构。更稳妥的做法是确保 keep_recent 覆盖到最近一次工具调用,这样 tool_call_id 不会进入压缩区。

如果你在接入 TaoToken 的过程中遇到 Key 无效或 401 报错,先去 API Keys 页面确认 Key 是否正确生成、是否过期,地址是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。如果确认 Key 没问题但还是报错,检查 base_url 是否写成了 https://taotoken.net/api ,注意末尾不要加斜杠,也不要在 API 地址上加 UTM 参数。

接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有完整的接口说明和错误码对照。如果你打算长期跑 Agent 任务,压缩逻辑会频繁调用 LLM,可以考虑 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,适合需要稳定通道和批量调用的场景。

最后说一个我踩过的坑:压缩后的摘要消息不要用 system 角色。我一开始图省事,把摘要塞进 system 消息里,结果模型把摘要当成了系统指令,行为变得很奇怪。正确做法是用 user 角色,让模型把它当作"用户提供的上下文信息"来处理。这个细节在接入文档里没有明说,但实测下来影响很大。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 19:41:05

嵌入式通信协议选型实战:UART/I2C/SPI/I2S四大接口深度对比

1. 这不是协议说明书&#xff0c;是嵌入式工程师的“通信选型决策手册”i2c、i2s、spi、uart——这四个缩写几乎刻在每个嵌入式工程师的键盘上&#xff0c;也反复出现在原理图评审、PCB布线、驱动调试和深夜抓波形的屏幕里。但真正能说清“为什么这里必须用I2C而不是SPI”“I2S…

作者头像 李华
网站建设 2026/9/28 19:39:32

LoRa1276-C1-915在应急灯低功耗无线通信中的实战应用

1. 项目概述&#xff1a;为什么应急灯需要LoRa1276-C1-915&#xff1f;LoRa1276-C1-915不是一块普通射频芯片&#xff0c;它是专为北美915MHz ISM频段设计的超低功耗LoRa收发器模块&#xff0c;内置SX1276核心、匹配电路、TCXO温补晶振和优化天线接口。我第一次在消防演练现场看…

作者头像 李华
网站建设 2026/9/28 19:38:42

开发者都在用的开源工具箱

一、项目背景及简介你是否遇到过这种情况&#xff1f;临时要转个时间戳&#xff0c;得去搜索引擎翻半天&#xff1b;想做个 URL 编码&#xff0c;得打开某个满是广告的在线网站&#xff1b;要生成一串随机密码&#xff0c;还得先登录注册。开发者的日常&#xff0c;总被这些零碎…

作者头像 李华
网站建设 2026/9/28 19:38:37

MCU开发核心链路:编译、烧录与仿真全流程解析

1. 从“写代码”到“跑起来”&#xff0c;MCU开发到底卡在哪几步搞嵌入式MCU开发的都清楚&#xff0c;日常动作翻来覆去就那么几件事&#xff1a;写代码、编译、烧录、仿真调试。听起来是个标准流水线&#xff0c;但真正上了项目就会发现&#xff0c;每个环节的坑多到能出一本书…

作者头像 李华
网站建设 2026/9/28 19:36:20

一键开关机芯片选型与实战避坑指南

1. 什么是“一键开关机芯片”&#xff1f;它到底解决什么实际问题&#xff1f;你有没有遇到过这样的场景&#xff1a;给老人买的智能药盒&#xff0c;每次开机要长按电源键5秒&#xff0c;关机又要按住不放3秒&#xff0c;结果老人记不住&#xff0c;要么一直开着耗电&#xff…

作者头像 李华