恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
dsh 跑长会话 Compaction,模型 Key 走 TaoToken
首页
资讯中心
/
dsh 跑长会话 Compaction,模型 Key 走 TaoToken
dsh 跑长会话 Compaction,模型 Key 走 TaoToken
发布时间:2026/9/14 14:28:56
Day 26 的 dsh 源码阅读笔记看到「Compaction 什么时候触发」这一节时答案其实很明确触发点在 deriveMessages 之后、组装模型请求之前。可真正动手复现时很多同学卡住的不是源码而是 context_length_exceeded 一直报Agent Loop 根本没机会走到 compact()。要让 BasicCompaction 的滑动窗口和 Tool Result Pruner 真正把消息压进上下文窗口先把模型通道打通就行打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建一个 KeyBase URL 填 https://taotoken.net/api不要加 /v1然后回到源码练习里观察压缩逻辑怎么跑。这个顺序一旦理顺Day 26 剩余内容会顺畅很多。1. Compaction 触发点context_length_exceeded 卡在哪一环1.1 长会话是怎么一步步走进上下文窗口极限的先把机制说清楚。普通问答场景里每条消息只有几百字符距离模型上下文上限很远Compaction 自然不会被触发。但带工具调用的 Agent 任务不同消息历史膨胀得极快常见有三个来源工具调用本身会增加消息assistant 发起 tool_call 是一条消息tool 返回 result 又是一条消息一次工具循环至少多两轮。工具返回内容往往很长web_fetch 抓取的完整网页可能有 8000 到 20000 字符shell_exec 跑一次编译可能输出几百行日志。Skill 加载后工具定义和系统提示词也会进入每次请求进一步抬高基础 token 占用。ddsh 的 Agent Loop 每轮都在做「派生消息历史 → 组装模型请求 → 发送请求 → 处理响应」这个循环。当 deriveMessages 把完整会话日志拉出来、组装请求时发现总 token 数超过模型最大上下文API 就会返回 context_length_exceeded。多数人在这时才想起 Compaction但此刻请求已经失败了。真正的机制是在请求组装之前就检查一遍该压缩就先压缩而不是等模型报错。1.2 触发点的位置deriveMessages 之后、组装请求之前读源码时重点关注这个顺序。Agent Loop 每执行一步大概走这样的流程// Agent Loop 中与 Compaction 相关的片段 async function runStep(session, agent) { // 1. 从会话日志派生完整消息历史 let messages deriveMessages(session) const options getCompactionOptions(agent.model) // 2. 组装模型请求之前先检查是否需要压缩 if (ctx.compaction.shouldCompact(messages, options)) { // 3. 需要压缩才执行 compact压缩结果作为请求的实际消息 messages await ctx.compaction.compact(messages, options) } // 4. 用压缩后的消息组装系统提示词、工具列表和请求体 const request buildRequest(messages, agent) // 5. 发送请求处理模型响应 const response await llm.chat(request) return response }这个位置有两个含义。第一压缩发生在完整历史派生之后压缩对象是真实完整的消息列表不是逐个消息边生成边丢弃。第二压缩结果只影响「发给模型的内容」会话日志本身完整保留方便之后随时回看。所以调试长会话时可以在 compact() 调用前后分别打印 countTokens 的值对比压缩前和压缩后的 token 变化这是最直观的观察方式。1.3 动手练习先看 BasicCompaction 的源码再动手跑原文的练习要求读 packages/compaction/compaction-basic/src 下的实现这一步保留。在项目根目录执行cat packages/compaction/compaction-basic/src/index.ts阅读时盯住两个关键点。一是 shouldCompact 怎么算它用 countTokens(messages) 对比 maxTokens 和 reserveTokens 的差值超过这个阈值才返回 true。二是 compact() 里从后往前保留消息的循环它先分离 system 消息再从消息列表末尾往前逐个保留直到累积 token 数逼近目标值如果丢弃了早期消息会在结果里插入一条截断提示。但静态看代码只能理解逻辑真正想确认「压缩后模型还能继续对话」需要先让模型通道可用。这就是下一步要做的把请求真正发到一个能访问的兼容 API 上。2. 把动手练习改一步去官网拿 KeyBase URL 指到 TaoToken2.1 准备材料注册并创建 API Key原笔记里没有专门讲模型通道怎么配因为它建立在 Day 22-25 已完成模型接入的基础上。如果之前只是把官方 Key 填进去试了几轮额度就紧巴巴或者多 Key 换来换去搞不清哪把 Key 生效这时可以统一收敛到一个兼容通道。打开 TaoToken 注册账号在控制台创建一个 API Key。要准备这 3 样东西TaoToken 账号以及对应的 API Key创建完成后会显示一次复制后保存好。Base URLhttps://taotoken.net/api注意末尾不带 /v1。模型 ID这个不在这里猜以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场里列出的名称为准。整个接入过程就是一个标准的 OpenAI/Anthropic 兼容调用TaoToken 在这里承担的是统一 API 通道角色把模型请求转发到对应的模型服务避免官方额度限制导致长会话实验被迫中断。2.2 把 Base URL 和 Key 写进环境变量dsh 的模型连接方式不同版本略有差异但本质都是三个值接口地址、API Key、模型 ID。用环境变量表达最通用export DSH_BASE_URLhttps://taotoken.net/api export DSH_API_KEYYOUR_API_KEY # 具体模型 ID 以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场为准例子里不写死 export DSH_MODELyour-model-id如果你用的 dsh 版本支持 cordis.yml 配置模型 Provider就把上面三个值对应填到 model 段的 baseURL、apiKey、model 字段里效果一致。这里有一个需要特别留意的习惯官网落地页改成 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 用于注册和看模型广场而真正填进 Base URL 的是 https://taotoken.net/api少一级路径也别追加 /v1。2.3 动手练习确认模型请求确实走通再谈压缩配好之后先做一个最小验证跑一个只有两三条普通消息的会话确认模型能正常回复。通过后再进入 Compaction 场景。验证目的只有一个排除模型通道问题确保后面所有现象都来自压缩策略而不是网络链路。3. BasicCompaction 滑动窗口maxTokens 与 reserveTokens 怎么算3.1 从后往前保留消息的循环逻辑BasicCompaction 的策略本质是滑动窗口加截断。它会先处理系统提示词把 system 消息从普通消息里分离出来保证角色定义不被丢弃然后从最新一条消息开始往前扫描只要当前累积 token 数没有超过 targetTokens就把消息保留下来一旦再加一条会超限就停止保留如果发现丢弃了早期消息会在保留消息的头部插入一段截断提示告诉模型有 N 条早期消息被移除了。简化后的实现思路大致是这样// BasicCompaction 简化示意保留滑动窗口核心逻辑 function compact(messages, options) { const maxTokens options?.maxTokens ?? 4096 const reserveTokens options?.reserveTokens ?? 512 const targetTokens maxTokens - reserveTokens const systemPrompt options?.keepSystemPrompt messages[0]?.role system ? messages[0] : null const others systemPrompt ? messages.slice(1) : messages const kept [] let used systemPrompt ? countTokens([systemPrompt]) : 0 for (let i others.length - 1; i 0; i--) { const nextTokens countTokens([others[i]]) if (used nextTokens targetTokens) break kept.unshift(others[i]) used nextTokens } if (kept.length others.length) { const dropped others.length - kept.length kept.unshift({ role: system, content: [Note: ${dropped} older messages were truncated to fit the context window.], }) } return systemPrompt ? [systemPrompt, ...kept] : kept }这段的核心价值在于 targetTokens 的计算。maxTokens 是模型上下文窗口上限reserveTokens 是给模型回复预留的 token 空间。压缩目标是让消息历史不超过 maxTokens 减 reserveTokens这样把请求发出去后模型生成回复时还有余量不会出现回复刚写一半又被截断的情况。3.2 用一个具体例子理解「从后往前保留」假设配置 maxTokens200reserveTokens40那么 targetTokens160。现在有四条消息token 数大致如下system30 token分离出来单独保留。user 第一条30 token。assistant 回复70 token。user 第二条20 token。assistant 回复60 token。从后往前保留先取 assistant 回复60累积 60再取 user 第二条20累积 80再取 assistant 第一条70累积 150再尝试取 user 第一条30累积 180超过 160停止。最终保留的是「assistant 第一条、user 第二条、assistant 回复」这三条user 第一条被丢弃同时插入提示说明有 1 条早期消息被截断。最后发送给模型的内容就是 system 截断提示 保留的三条消息。这个例子同时也说明一个容易忽略的细节如果某一条消息本身就超过 targetTokens为了不让模型完全没有上下文BasicCompaction 会至少保留最近一条消息。遇到这种情况更合适的做法是配合工具结果修剪先把消息内部的长度降下来。3.3 动手练习把 maxTokens 调低立刻触发 shouldCompact静态看源码始终隔一层。建议实际跑一个长会话把 maxTokens 设成比模型窗口小很多的数值比如模型上下文 8000就把 maxTokens 设 4000reserveTokens 保持默认 512。这样几轮工具调用之后countTokens 很快超过 3488shouldCompact 自动返回 true你能在控制台看到请求体里出现截断提示说明压缩确实执行了。4. Tool Result Pruner长工具结果先剪一刀再谈滑动窗口4.1 工具结果膨胀带来的独立问题滑动窗口处理的是「消息条数太多」的问题但长会话还有一个常见但更隐蔽的膨胀源单条工具结果太长。web_fetch 返回一整个网页、shell_exec 返回一屏编译日志、数据库查询返回几千行记录这些结果作为一条 tool 消息存在时只靠 BasicCompaction 的消息级滑动窗口是管不住的。原因很简单一条 10000 字符的网页内容在 BasicCompaction 的循环里也是一条消息把它保留下来可能就占掉了一半预算把它丢弃又可能丢掉关键信息。于是 dsh 单独提供了 Tool Result Pruner 这个 Provider。它只处理 role 为 tool 的消息把超过 maxToolResultLength 的内容从开头截断并在末尾追加一段说明标注原始长度。这样既保留工具调用的上下文关系又把单条消息的长度压到一个可控范围。// ToolResultPruner 简化示意只修剪过长工具结果 function compact(messages, options) { const limit options?.maxToolResultLength ?? 2000 return messages.map((message) { if (message.role ! tool || typeof message.content ! string) { return message } if (message.content.length limit) { return message } return { ...message, content: ${message.content.slice(0, limit)}\n\n... [truncated, original length: ${message.content.length} chars], } }) }4.2 组合流程先修剪工具结果再判断是否滑动窗口两个 Provider 的定位不同适合串成一条流水线使用。工具结果先修剪一遍token 总数下降后再用 shouldCompact 重新判断是否还需要丢弃早期消息。很多情况下先修剪工具结果就足够把 token 压回窗口内根本轮不到滑动窗口去丢消息。组合使用的示例流程原始消息system 3 轮对话 2 条超长 tool result → ToolResultPruner 修剪过长工具结果 → 重新计算 countTokens → 如果仍超过 maxTokens - reserveTokens执行 BasicCompaction 滑动窗口 → 发送给模型这个顺序也解释了为什么两个 Provider 都实现了 CompactionService 接口却可以同时注册它们各自的 compact() 是幂等的先修剪不会破坏后续的滑动窗口判断。4.3 动手练习抓一个带长工具结果的会话观察修剪标记想亲眼看到 Tool Result Pruner 生效可以让 Agent 执行一个会返回大量文本的工具比如用 web_fetch 抓一个内容较多的页面。把 maxToolResultLength 设为 2000等回调结束后在模型请求的消息列表里找 role 为 tool 的消息。如果内容长度超过 2000你会看到末尾出现[truncated, original length: ... chars]的标记。有这个标记出现说明长工具结果没有被整体丢弃而是被压缩后继续参与了上下文。5. 验证与排障从日志确认 compact() 真正执行5.1 验证方式跑完长会话后回官网看调用记录Compaction 是否生效最直接的验证不是看源码而是看长会话是否还能继续对话。具体做法打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 登录控制台找到本次会话对应的模型调用记录确认请求地址确实指向 https://taotoken.net/api调用没有返回 context_length_exceeded。同时回看会话日志早期消息已被截断提示替代而最近几轮的上下文仍然保留这就是一次成功的 Compaction 观察。把「官网看用量」这一步对应到原始笔记其实就是原文里「去控制台看用量」的动作正常情况下一次长会话会拆成多次模型调用压缩前后的 token 数会有明显落差这个落差能侧面印证 compact() 在请求组装前完成了它的工作。5.2 排障这三个问题最容易在复现时出现问题一401 Unauthorized。检查 API Key 是否真的从 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建并确认环境变量里没有混入换行符或空格。多份 Key 同时存在于配置里时确认当前生效的是哪一个。问题二404 Not Found。最常见原因是把 Base URL 填成了 https://taotoken.net/api/v1。TaoToken 的接口 Base URL 就是 https://taotoken.net/api末尾不带 /v1也不要因为其他服务习惯顺手补上 /v1。问题三模型 ID 报不存在。不要照抄其他文章里的模型名每个通道当前可用的模型 ID 以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场展示为准。配错的话请求会在模型校验阶段失败根本走不到 Compaction 环节。6. 把 Day 26 的验证清单补上「模型通道」这一项如果你按原笔记的顺序读源码可能已经看过 Compaction Service Definition、Basic Provider 和 Tool Result Pruner 三份源码。但只看不跑对上下文窗口的理解始终隔着一层。我的建议是把模型通道先收敛到 TaoToken再把 maxTokens 调低复现一次长会话之后回头看滑动窗口的倒序循环和截断提示会踏实很多。Day 26 的验证清单里原本列满了「理解 shouldCompact 与 compact 的区别」「理解滑动窗口策略」「理解工具结果修剪」这类勾选项。现在补最后一项跑通一次真实的超长会话观察压缩日志确认触发点在 deriveMessages 之后、组装请求之前。补完这一项再进 Day 27 读 settings、credentials、guard 那些外围包时就不会被长会话的基础设施问题绊住脚。