【Agent Harness】Gliding Horse 工具结果压缩体系:用“指针”驯服上下文膨胀的 config.toml 骨架
发布时间:2026/9/28 5:39:21来源:尧图网络
1. 为什么工具结果会把上下文撑爆做 Agent Harness 的朋友大概率都遇到过这个场景Agent 调用一次文件读取、一次日志抓取、一次网页解析工具返回几千甚至上万 token 的原始内容然后这段内容被原封不动塞进对话历史。下一轮推理时模型要重新读一遍这坨东西再下一轮还要读几轮下来上下文窗口就被填满了。问题的本质不是工具返回太多而是我们把工具结果当成了对话内容的一部分。对话历史本该是决策轨迹结果被塞进了原始数据。这两者的生命周期完全不同决策轨迹要长期保留原始数据往往只在当前这一步有用。Gliding Horse 的思路很直接工具结果不直接进上下文而是压缩成一个指针——包含工具 ID、结果摘要、以及一个可回溯的引用地址。上下文里只留指针需要完整数据时再按地址回读。这样上下文增长的是几十字节的摘要而不是几千字节的原文。这篇就聚焦落地在config.toml里怎么声明这套指针式压缩策略怎么触发一次长结果工具调用来验证压缩前后 token 占用以及怎么确认指针回读的数据和原始结果一致。适合谁看正在搭 Agent Harness、被上下文膨胀拖慢推理、想给工具结果加一层压缩层但不想改太多业务代码的人。下面所有配置都可以直接复制改。2. TaoToken 前置把模型接入和压缩层解耦在动手写config.toml之前先把模型接入这一层理清楚。Gliding Horse 的压缩策略是作用在工具结果进入上下文这个环节它不关心你用的是哪个模型、走的是哪条接入链路。所以模型接入部分可以独立配置压缩策略独立声明两者通过配置文件解耦。我这边模型调用统一走 TaoToken 的 API 入口官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 。这样做的好处是Harness 里只维护一份模型配置压缩策略、工具注册、指针存储都挂在同一份config.toml下改一处不用满项目找。如果你还没配过 key先去控制台建一个https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 然后在 API Keys 页面生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。生成后把 key 写进环境变量别硬编码进config.toml配置文件里用占位符引用。模型对话调试可以用这个入口快速验证https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 参数含义、返回结构、错误码都在里面配config.toml时对着看能少踩坑。有一点要提前说清楚TaoToken 在这里的角色是模型 API 接入层不是中转也不是代理它提供的是标准的模型调用能力。压缩策略、指针存储、回读逻辑全部跑在你自己的 Harness 里数据不出你的进程边界。3. 可复制配置config.toml 指针压缩骨架下面这份config.toml骨架是核心。它分四块模型接入、工具注册、压缩策略、指针存储。压缩策略这块就是 Gliding Horse 的声明式配置你只需要告诉 Harness哪些工具的结果要压缩、压缩到什么级别、指针存哪里。# config.toml —— Agent Harness Gliding Horse 指针压缩骨架 [model] provider taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY # 从环境变量读取不写明文 model claude-sonnet-4-20250514 max_context_tokens 200000 timeout_seconds 120 # ---------- 工具注册 ---------- [[tools]] name read_file description 读取本地文件内容 handler handlers.file_reader # 该工具结果参与指针压缩 compress true [[tools]] name fetch_log description 抓取指定服务的运行日志 handler handlers.log_fetcher compress true [[tools]] name get_weather description 查询城市天气 handler handlers.weather compress false # 结果很短不压缩 # ---------- Gliding Horse 压缩策略 ---------- [compression] enabled true strategy pointer # 指针式压缩 default_level L1 # 默认只留指针 摘要 summary_max_chars 240 # 摘要最大字符数 pointer_ttl_seconds 1800 # 指针生存时间 30 分钟 fallback_on_miss L3 # 指针失效时降级为保留完整结果 hash_algo sha256 # 指针 ID 生成算法 # 分级策略不同工具可以覆盖默认级别 [compression.levels] L1 { keep [pointer, summary] } L2 { keep [pointer, summary, partial], partial_max_chars 1200 } L3 { keep [pointer, summary, raw] } # 按工具覆盖压缩级别 [compression.overrides] read_file L2 # 文件读取保留部分内容方便模型直接引用 fetch_log L1 # 日志只留摘要需要时回读 # ---------- 指针存储 ---------- [pointer_store] backend sqlite # 本地 sqlite够用且可回溯 path ./.harness/pointers.db max_entries 5000 # 超过后按 LRU 淘汰 cleanup_interval_seconds 300 # 每 5 分钟清理过期指针几个关键参数解释一下。strategy pointer是开启指针式压缩的总开关关掉就退回原始结果直接进上下文的老行为方便做 A/B 对比。default_level L1表示默认只保留指针和摘要这是最省 token 的档位。fallback_on_miss L3是个安全阀如果指针指向的数据因为 TTL 过期被清了Harness 不会直接报错而是降级为把完整结果重新放进上下文保证 Agent 不会因为空指针卡死。[compression.overrides]这块是实战里最常用的。像read_file这种工具模型经常需要直接看到文件片段来推理压到 L1 反而会让它反复回读不如直接给 L2 保留 1200 字符的部分内容。而fetch_log这种动辄几万行的日志压到 L1 只留摘要需要细节时再按指针回读性价比最高。指针存储用 sqlite 是权衡后的选择单机 Harness 不需要上 Redissqlite 的读写足够快而且文件落盘可回溯出问题能直接查库。max_entries和cleanup_interval_seconds配合 LRU 淘汰防止指针库无限膨胀——注意指针库膨胀和上下文膨胀是两回事前者在磁盘上后者在 token 窗口里别搞混。4. 验证请求触发长结果工具调用并对比 token配置写完不算完得实际跑一次长结果工具调用看压缩到底有没有生效。下面这段 Python 是验证脚本它做三件事注册一个会返回长文本的假工具、跑一次带压缩的调用、打印压缩前后的 token 估算和指针回读结果。import json import hashlib import sqlite3 import time from pathlib import Path # 模拟一个返回长结果的工具 def fetch_log(tail_lines2000): lines [f2025-01-01T00:00:{i%60:02d} INFO serviceapi req_id{i} latency{i%300}ms status200 for i in range(tail_lines)] return \n.join(lines) # 指针压缩器对应 config.toml 的 [compression] 段 class PointerCompressor: def __init__(self, db_path./.harness/pointers.db, ttl1800, summary_max240): Path(db_path).parent.mkdir(parentsTrue, exist_okTrue) self.conn sqlite3.connect(db_path) self.conn.execute( CREATE TABLE IF NOT EXISTS pointers ( pointer_id TEXT PRIMARY KEY, tool_id TEXT, summary TEXT, raw TEXT, created_at REAL, expire_at REAL ) ) self.conn.commit() self.ttl ttl self.summary_max summary_max def compress(self, tool_id: str, raw: str, level: str L1): pointer_id hashlib.sha256(raw.encode()).hexdigest()[:16] summary raw[:self.summary_max].replace(\n, ) now time.time() self.conn.execute( INSERT OR REPLACE INTO pointers VALUES (?,?,?,?,?,?), (pointer_id, tool_id, summary, raw, now, now self.ttl) ) self.conn.commit() pointer { pointer_id: pointer_id, tool_id: tool_id, summary: summary, level: level, } return pointer def read_back(self, pointer_id: str): row self.conn.execute( SELECT raw, expire_at FROM pointers WHERE pointer_id?, (pointer_id,) ).fetchone() if not row: return None raw, expire_at row if time.time() expire_at: return None return raw def estimate_tokens(text: str) - int: # 粗略估算中文约 1.5 字符/token英文约 4 字符/token这里统一按 3 字符/token return max(1, len(text) // 3) if __name__ __main__: raw fetch_log(2000) print(f原始结果字符数: {len(raw)}) print(f原始结果估算 token: {estimate_tokens(raw)}) comp PointerCompressor() pointer comp.compress(fetch_log, raw, levelL1) pointer_json json.dumps(pointer, ensure_asciiFalse) print(f\n指针 JSON: {pointer_json}) print(f指针字符数: {len(pointer_json)}) print(f指针估算 token: {estimate_tokens(pointer_json)}) ratio estimate_tokens(raw) / estimate_tokens(pointer_json) print(f\n压缩比: {ratio:.1f}x) # 回读验证 back comp.read_back(pointer[pointer_id]) print(f\n回读是否与原始一致: {back raw})跑下来你会看到类似这样的输出原始 2000 行日志大约 12 万字符估算 4 万 token指针 JSON 只有 300 多字符估算 100 多 token压缩比在 300 倍以上。回读校验打印True说明指针指向的数据和原始结果完全一致。这里有个细节值得说estimate_tokens用的是粗略估算真实 token 数取决于模型的分词器。但压缩比这个量级几百倍已经足够说明问题——上下文里省下的是几万 token而不是几百。如果你要精确对比可以在调用模型时读返回的usage字段把prompt_tokens在压缩前后各记一次。5. 本篇常见错排查配置和验证跑通之后实际接入时还有几个坑容易踩这里集中列一下。指针回读返回 None。最常见的原因是 TTL 过期。pointer_ttl_seconds 1800意味着指针 30 分钟后失效如果 Agent 的长任务跨越了这个时间回读就会失败。解决办法有两个一是把 TTL 调大二是依赖fallback_on_miss L3让 Harness 自动降级。但要注意降级为 L3 意味着完整结果会重新进上下文token 会涨回去所以 TTL 要按任务时长合理设置。压缩后模型看不懂摘要。这是摘要质量的问题。summary_max_chars 240截的是原始结果的前 240 字符如果工具返回的开头是无关的头部信息比如日志的时间戳前缀摘要就没信息量。更好的做法是在 handler 里做结构化摘要比如日志工具提取错误数、警告数、时间范围三个字段作为摘要而不是简单截断。config.toml里可以给每个工具单独配摘要函数骨架里没展开但[compression.overrides]就是留这个扩展口的。指针库文件越来越大。检查max_entries和cleanup_interval_seconds是否生效。sqlite 的删除不会立即释放磁盘空间需要定期VACUUM。可以在 cleanup 逻辑里加一句self.conn.execute(VACUUM)但别每次清理都执行频率太高反而拖慢写入。压缩比看起来很高但 token 没降。这种情况通常是压缩只作用在了工具结果上但工具调用的参数、模型的推理文本、系统提示词这些没动。上下文膨胀往往是多来源的指针压缩只解决工具结果这一路。排查时把上下文的 token 占用按来源拆开看确认工具结果占比到底多少别指望一个策略解决所有膨胀。回读数据和原始不一致。如果工具结果里有非确定性内容比如带时间戳、带随机 ID两次读取本来就不一样这不是指针的问题。验证一致性时要用同一次工具调用的结果做对比别重新调一次工具再比。6. 下一步把压缩层接进你的 Harness指针压缩这套东西核心就三件事工具结果进上下文前先过压缩器、上下文里只留指针、需要时按指针回读。config.toml骨架给的是声明式配置实际接入时你需要在 Harness 的工具调用返回处插一个 hook把原始结果交给压缩器拿回指针再塞进对话历史。模型接入这块如果你还没定可以先用 TaoToken 的模型对话入口跑通链路https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。接入参数和返回结构对着文档配https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。key 在 API Keys 页面生成https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。如果你的 Harness 是长期跑编码任务或 Agent 工作流压缩策略要和模型调用配额一起规划Coding Plan 那边有按长期编码场景的配置说明https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 相关的接入细节在https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaude-codeutm_campaignrewrite 。最后留一个实操建议先把strategy设成pointer跑一周记录每次工具调用的压缩比和回读命中率。命中率低于 80% 说明 TTL 太短或摘要质量不够调这两个参数比调模型参数见效快。压缩层调稳了再考虑要不要上分布式指针存储——单机 sqlite 能扛住的量别过早引入复杂度。
网站建设高端定制企业官网