1. 记忆膨胀的现场:Hermes Agent 为什么会把上下文吃满
Hermes Agent 的记忆容量管理,说白了就是解决一个很现实的问题:Agent 跑得越久,记忆越多,上下文窗口越容易被撑爆。它适合谁?适合那些把 Agent 放在本地长期跑、每天几十上百轮对话、还希望它记住历史经验的开发者。能做什么?它把记忆拆成分层结构,让"存"和"用"分开,避免所有历史都往上下文里塞。
我最早接触 Hermes Agent 的时候,遇到一个很典型的场景:一个本地跑的编码助手,连续用了两周,MEMORY.md 从几百字符涨到接近上限,每次会话启动的固定开销越来越重,长会话到后半段开始出现响应变慢、工具调用错乱。查下来不是模型的问题,是记忆层没有治理。
Hermes 的记忆分四层,理解这四层是治理容量的前提:
文件层是持久语义记忆,MEMORY.md 和 USER.md 两份文件,有硬字符上限。MEMORY.md 约 2200 字符,折算下来大概 800 tokens;USER.md 约 1375 字符,约 500 tokens。这两份文件在每次会话启动时以冻结快照的形式注入 system prompt,也就是说不管你这轮对话多短,这约 1300 tokens 的固定开销都要先付掉。
情景记忆层走的是另一条路,用 SQLite 加 FTS5 全文检索,全量历史存档,不设容量上限。关键在于它不直接注入上下文,而是等 Agent 主动调用 session_search 工具,按关键词或正则检索,检索结果以摘要形式注入。存的时候不省,注入的时候省。
工作记忆层就是当前会话的窗口,也是最容易溢出的地方。Hermes 的 ContextCompressor 用四阶段管道处理,不是简单截断。
自我进化层是每 15 个任务触发一次 nudge,Agent 回顾最近完成的任务,提炼可复用经验,尝试写入 MEMORY.md,超限就触发精简。
这四层里,真正决定"记忆容量"体感的是文件层和工作层。文件层管固定开销的天花板,工作层管单次会话的溢出。SQLite 那层虽然叫"无容量限制",但它决定了检索时的 Token 消耗——检索命中越多,注入的摘要越多,工作窗口压力越大。所以容量治理不是单点问题,是三层联动。
下面我会从 SQLite 表结构开始,一路讲到上下文压缩阈值配置,最后用 TaoToken 统一通道观察每次调用的 Token 开销。整个过程都可以在本地复现。
2. TaoToken 前置:统一 Key 与 API 通道,让 Token 开销可观测
在动手改记忆表结构和压缩阈值之前,先把调用通道理顺。原因很简单:你要治理 Token 消耗,前提是能看见 Token 消耗。如果每次请求散落在不同的 Key、不同的 Base URL 上,账单和用量对不上,压缩阈值调了也不知道效果。
TaoToken 在这里的角色是统一入口。它提供兼容主流协议风格的 API 通道,你可以在一个控制台里管理 Key、查看调用记录、观察模型维度的用量。对 Hermes Agent 这种会频繁调用辅助模型做记忆精简和上下文压缩的场景,统一通道的价值很直接:压缩用的廉价模型和主对话模型走同一个出口,用量能合并看。
官网入口在这里:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。注册后在控制台创建 API Key,地址是 https://taotoken.net/api,注意这个 API 地址不带 UTM 参数,配置的时候别把查询串带进去。
你需要准备三样东西,这三样在后面的配置里会反复出现:
Base URL:https://taotoken.net/api
API Key:在控制台 https://taotoken.net/console/api-keys 生成,形如 sk- 开头的一串
Model ID:按你实际要用的模型填,比如做记忆精简这种低价值任务,选一个便宜的小模型就够,主对话再选能力强的
如果你只是想先验证通道通不通,可以用模型对话页面直接发一条测试消息:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。这一步不写代码,纯网页操作,确认 Key 有效、模型能返回内容,再往下做本地配置。
有一点要提醒:TaoToken 是 API 通道,不是编辑器替代品,也不是让你绕过什么限制的工具。它的定位就是统一管理模型调用,方便你观察开销。这个认知摆正了,后面的配置才不会跑偏。
对于长期跑 Agent 的开发者,如果调用量稳定,可以看一下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。它的意义在于把长期编码和 Agent 场景的调用成本固定下来,避免按量计费在记忆精简这种高频小请求上失控。
接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,配置细节以文档为准。下面我给出的配置片段是通用形态,你按文档核对字段名。
3. 可复制配置:SQLite 记忆表结构 + 压缩阈值 + settings 片段
这一节是全文最核心的部分,全部可复制。分三块:SQLite 记忆表结构、上下文压缩阈值配置、以及把 TaoToken 通道写进 settings 的片段。
3.1 SQLite 记忆表结构
情景记忆层用 SQLite 加 FTS5。下面这份表结构是我实测下来比较稳的版本,包含主表、FTS5 虚拟表和触发器三部分。字段设计上,把"原始内容"和"摘要"分开存,检索时优先返回摘要,避免原始全文直接进上下文。
-- 主表:情景记忆条目 CREATE TABLE IF NOT EXISTS memory_episodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, -- user / assistant / tool content TEXT NOT NULL, -- 原始内容 summary TEXT, -- 压缩后的摘要,检索优先返回 token_count INTEGER DEFAULT 0, -- 该条目的 token 估算 importance INTEGER DEFAULT 0, -- 重要度打分,0-10 created_at INTEGER NOT NULL, -- unix 时间戳 last_hit_at INTEGER, -- 最近一次被检索命中的时间 hit_count INTEGER DEFAULT 0 -- 被命中次数 ); -- 索引:按会话和时间检索 CREATE INDEX IF NOT EXISTS idx_episodes_session ON memory_episodes(session_id, created_at DESC); -- 索引:按重要度筛选,用于精简时优先淘汰低价值条目 CREATE INDEX IF NOT EXISTS idx_episodes_importance ON memory_episodes(importance, last_hit_at); -- FTS5 虚拟表:全文检索 CREATE VIRTUAL TABLE IF NOT EXISTS memory_episodes_fts USING fts5( content, summary, content='memory_episodes', content_rowid='id', tokenize='unicode61' ); -- 触发器:插入时同步 FTS CREATE TRIGGER IF NOT EXISTS episodes_ai AFTER INSERT ON memory_episodes BEGIN INSERT INTO memory_episodes_fts(rowid, content, summary) VALUES (new.id, new.content, COALESCE(new.summary, '')); END; -- 触发器:更新时同步 FTS CREATE TRIGGER IF NOT EXISTS episodes_au AFTER UPDATE ON memory_episodes BEGIN INSERT INTO memory_episodes_fts(memory_episodes_fts, rowid, content, summary) VALUES ('delete', old.id, old.content, COALESCE(old.summary, '')); INSERT INTO memory_episodes_fts(rowid, content, summary) VALUES (new.id, new.content, COALESCE(new.summary, '')); END; -- 触发器:删除时同步 FTS CREATE TRIGGER IF NOT EXISTS episodes_ad AFTER DELETE ON memory_episodes BEGIN INSERT INTO memory_episodes_fts(memory_episodes_fts, rowid, content, summary) VALUES ('delete', old.id, old.content, COALESCE(old.summary, '')); END;这份结构的关键点在于 summary 字段和 importance 字段。summary 让检索结果以摘要形式注入,而不是原始全文;importance 让精简时有依据,优先淘汰低分且长期没被命中的条目。
检索时用这样的查询,只取摘要,控制注入量:
SELECT e.id, COALESCE(e.summary, substr(e.content, 1, 200)) AS inject_text, e.importance, e.created_at FROM memory_episodes_fts f JOIN memory_episodes e ON e.id = f.rowid WHERE memory_episodes_fts MATCH ? ORDER BY e.importance DESC, e.last_hit_at DESC LIMIT 5;LIMIT 5 是刻意的,检索命中越多,注入越多,工作窗口压力越大。控制在 5 条以内,配合摘要,单次检索注入通常能压在 1K tokens 以内。
3.2 上下文压缩阈值配置
工作记忆层的四阶段压缩,阈值配置决定了什么时候触发、保护多少、压缩多少。下面这份 TOML 配置是我调过几轮之后的版本,字段名按你的实际实现对齐。
[context_compressor] # 触发压缩的上下文占用比例,超过 0.75 开始压缩 trigger_ratio = 0.75 # 头部保护消息条数:系统提示 + 早期上下文 head_protect_messages = 3 # 尾部按 token 预算动态保留,保证当前任务细节不丢 tail_protect_tokens = 20000 # 旧工具输出清理阈值:超过该字符数的工具结果替换为占位符 tool_output_char_limit = 200 # 压缩用的辅助模型,走统一通道 compress_model = "your-cheap-model-id" # 压缩后是否修复孤立的 tool_call / tool_result 配对 repair_tool_pairs = true # 单次压缩生成摘要的最大 token summary_max_tokens = 1500 [memory_files] # 文件层硬上限,按字符计 memory_md_char_limit = 2200 user_md_char_limit = 1375 # 接近上限的触发比例,超过则触发 LLM 自主精简 compact_trigger_ratio = 0.9 [nudge] # 每 N 个任务触发一次自我进化回顾 interval_tasks = 15trigger_ratio = 0.75 意味着上下文用到 75% 就开始压缩,留出余量。tail_protect_tokens = 20000 是尾部保护预算,最近约 20K tokens 的对话保持原始形态。tool_output_char_limit = 200 对应四阶段管道的第一步,超过 200 字符的旧工具输出直接替换占位符,这一步纯规则处理,不经过 LLM,零延迟。
3.3 TaoToken 通道写进 settings
把统一通道写进配置,让主对话和压缩辅助模型都走同一个出口。下面这份 JSON 是通用形态,字段名以接入文档为准。
{ "api": { "base_url": "https://taotoken.net/api", "api_key": "sk-你的Key", "timeout_seconds": 60 }, "models": { "main": { "model_id": "your-main-model-id", "max_tokens": 4096 }, "compressor": { "model_id": "your-cheap-model-id", "max_tokens": 1500 } }, "memory": { "sqlite_path": "./data/memory_episodes.db", "memory_md_path": "./data/MEMORY.md", "user_md_path": "./data/USER.md" } }如果你用的是 Claude Code 这类工具做 Agent 的编码侧,配置形态是 settings 文件,三件套同样是 Base URL、Key、Model ID:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的Key", "ANTHROPIC_MODEL": "your-model-id" } }注意 ANTHROPIC_BASE_URL 填的是 https://taotoken.net/api,不要带任何查询参数。Key 从控制台生成,Model ID 按你实际选的填。这三件套缺一不可,少任何一个都会在请求阶段报错。
4. 验证请求:从一次调用看 Token 用量与压缩效果
配置写完,必须验证。验证分两步:先确认通道通,再确认压缩生效、Token 开销可观测。
4.1 通道连通性验证
先用 curl 发一条最小请求,确认 Base URL、Key、Model ID 三件套正确。
curl -s https://taotoken.net/api/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: sk-你的Key" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "your-model-id", "max_tokens": 64, "messages": [ {"role": "user", "content": "回复两个字:收到"} ] }'返回里会带 usage 字段,包含 input_tokens 和 output_tokens。这一步通了,说明通道没问题。如果返回 401,看第 5 节的排查。
4.2 记忆写入与检索验证
往 SQLite 里插几条测试数据,验证 FTS5 检索和摘要注入。
sqlite3 ./data/memory_episodes.db <<'SQL' INSERT INTO memory_episodes (session_id, role, content, summary, token_count, importance, created_at) VALUES ('sess-001', 'user', '项目使用 Python 3.11,依赖管理用 uv', 'Python 3.11 + uv', 12, 8, strftime('%s','now')), ('sess-001', 'assistant', '已记录项目环境:Python 3.11,包管理 uv', '环境已记录', 10, 6, strftime('%s','now')), ('sess-002', 'user', '数据库用 SQLite,路径 ./data/app.db', 'SQLite ./data/app.db', 11, 7, strftime('%s','now')); SQL然后跑检索查询,确认只返回摘要、条数受 LIMIT 控制:
sqlite3 ./data/memory_episodes.db <<'SQL' SELECT e.id, COALESCE(e.summary, substr(e.content,1,200)) AS inject_text FROM memory_episodes_fts f JOIN memory_episodes e ON e.id = f.rowid WHERE memory_episodes_fts MATCH 'Python' ORDER BY e.importance DESC LIMIT 5; SQL预期返回一条,inject_text 是 "Python 3.11 + uv",而不是原始全文。这就是"存的时候不省,注入的时候省"的落地效果。
4.3 压缩触发验证
构造一段长对话,让上下文占用超过 trigger_ratio,观察压缩是否触发。可以在 Agent 里加一行日志,打印每次请求前的上下文 token 估算和压缩动作。
def log_context_state(ctx_tokens, window_size, compressed): ratio = ctx_tokens / window_size print(f"[ctx] tokens={ctx_tokens} window={window_size} " f"ratio={ratio:.2f} compressed={compressed}")跑几轮长对话,你会看到 ratio 爬到 0.75 附近时 compressed 变成 True,之后 ratio 回落。回落幅度取决于中间区域被压缩掉多少。如果压缩后 ratio 还是很高,说明 tail_protect_tokens 设大了,或者检索注入的摘要太多,需要回头调 LIMIT 和 tail 预算。
4.4 Token 用量对照
在 TaoToken 控制台的调用记录里,按时间对齐你的日志。你会看到两类请求:主对话请求 input_tokens 较大,压缩辅助请求 input_tokens 中等但调用频繁。把这两类分开看,就能算出记忆治理的实际开销。
一个健康的比例是:压缩辅助请求的 Token 总量不超过主对话的 20%。如果超过,说明压缩触发太频繁,或者摘要生成用了太贵的模型。前者调高 trigger_ratio,后者把 compress_model 换成更便宜的。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
配置和验证过程中,下面这几类报错出现频率最高。逐个对照。
5.1 401 Unauthorized
最常见的原因是 Key 没填对,或者 Base URL 带了多余路径。检查三点:Key 是不是从控制台 https://taotoken.net/console/api-keys 生成的完整串;Base URL 是不是 https://taotoken.net/api,没有多余的 /v1 或查询参数;请求头字段名对不对,有的协议用 x-api-key,有的用 Authorization: Bearer。
还有一种情况是 Key 复制时带了空格或换行。把 Key 放进环境变量再引用,避免手抖:
export TAOTOKEN_API_KEY="sk-你的Key"5.2 local proxy failed
这个报错通常出现在本地 Agent 配置了代理转发,但转发目标不可达。检查你的 settings 里有没有残留的代理配置指向一个没启动的本地端口。把代理相关字段清掉,直接用 Base URL 直连。
如果你在配置里看到类似 proxy_url、http_proxy 的字段,且值指向 127.0.0.1 的某个端口,而那个端口没有服务在跑,就会报这个错。删掉这些字段,或者确认本地服务已启动。
5.3 reading choices 相关报错
这类报错一般出现在响应解析阶段,提示读取 choices 字段失败。原因是请求发出去返回的不是预期的 JSON 结构,可能是错误页、可能是协议不匹配。检查你的请求体格式和 Base URL 对应的协议是否一致。用 curl 先手动发一条,看原始返回长什么样,比在代码里猜快得多。
如果返回的是 HTML 错误页,说明请求根本没到 API,检查 URL 拼写。如果返回 JSON 但没有 choices 字段,说明协议形态不对,对照接入文档调整请求体。
5.4 OAuth 相关报错
有的工具链默认走 OAuth 流程,配置里如果同时存在 OAuth 和 API Key 两套凭证,可能冲突。排查方法是把 OAuth 相关配置注释掉,只保留 API Key 三件套。对于 Claude Code 这类工具,确认 settings 里是 ANTHROPIC_API_KEY 而不是 OAuth token 字段。
5.5 压缩后 Agent 循环崩溃
这个不是请求报错,是逻辑报错。压缩后如果 tool_call 和 tool_result 配对断裂,Agent 会在下一轮解析消息时崩溃。这就是配置里 repair_tool_pairs = true 的作用。如果你关掉了这个选项,压缩后务必手动检查消息序列,确保每个 tool_call 都有对应的 tool_result。
排查时打印压缩前后的消息列表,对比 tool_call_id 是否成对出现。缺失的那一侧就是问题所在。
6. 把记忆治理变成日常:从观察到调优的闭环
走到这里,你已经有了完整的可运行配置:SQLite 表结构、压缩阈值、TaoToken 通道、验证脚本、排错清单。剩下的就是把它变成日常习惯。
我的做法是每周看一次 TaoToken 控制台的用量,重点看压缩辅助请求的占比。占比升高就说明记忆层在膨胀,要么是 MEMORY.md 接近上限触发了频繁精简,要么是情景检索命中太多导致注入膨胀。前者去精简 MEMORY.md,后者去调检索的 LIMIT 和 importance 阈值。
MEMORY.md 的精简不要等它撞上限。compact_trigger_ratio = 0.9 是触发线,但你可以在 0.8 的时候就手动过一遍,把过时条目删掉,把重复条目合并。LLM 自主精简是兜底,不是主力。
情景记忆的 importance 打分值得花点心思。检索时按 importance 排序,高分条目优先注入。你可以给不同类型的信息定不同基准分:环境配置类 8 分,临时调试信息 3 分,用户偏好 7 分。这样精简时低分的先淘汰,高分的留住。
上下文压缩的 tail_protect_tokens 是体感最明显的参数。设小了,当前任务细节丢失,Agent 会重复问已经说过的信息;设大了,压缩效果不明显,窗口还是紧张。20000 是个起点,按你的任务复杂度上下调。
最后,所有调优都要有数据支撑。每次改完阈值,跑一轮标准测试对话,记录压缩触发次数、检索注入 Token、主对话 Token,对齐 TaoToken 控制台的用量。改一次看一次,别凭感觉调。
如果你还没开始,从模型对话页面发一条测试消息开始,确认通道通,再按第 3 节的配置落地。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,配置字段以文档为准。长期跑 Agent 的话,Coding Plan 能把成本固定下来,避免记忆精简这种高频小请求把账单推高。