
最近折腾 Agent 上下文优化,踩了几个坑,这篇把问题说清楚。大家都在看第一张账单,几乎没人发现第二张。
什么意思?打开你的 Agent 上下文,在你输入任何字符之前数数已经加载了多少东西。这有两项,来自两个不同的地方:
SKILL.md 都是整体加载的。我用 tiktoken(OpenAI 官方的分词器),采用 cl100k_base 编码来测量这两项。工具那边已经很糟糕了,技能这边更糟。而解决这两个问题的思路是一样的。
连接足够多的 MCP 服务器,光是工具描述就会吃掉你上下文窗口的很大一块。我的基准测量是一个包含 255 个工具的目录。渲染成完整的 JSON schema,那是 71,929 个 token。
而且这不是一次性成本。清单位会在每次对话轮次重新发送。在一个会话里问十二个问题,你就付了十二次这笔账。
根本原因是结构性的。选择一个工具只需要它的名字。你不需要完整的 schema 来决定是否调用某个东西。但默认的实现会在每次请求时预先发送所有工具的 schema。所以你实际上是在为那些大部分时候都不会打开的文档付钱。
技能这边更大,因为它本质上就更大。工具是名字加 schema,技能则是整个文档:指令、示例、工作流、检查清单。当目录被暴露为"读取你需要的 SKILL.md"时,朴素实现会读取整个目录。
在这台机器上,技能目录包含 418 个技能。全部拼接在一起,每个 SKILL.md 是 1,109,242 个 token。
这塞不进上下文窗口,所以大多数方案做了那件看起来很谨慎但实际不是的事情。它们只加载部分内容,由 Agent 猜测它需要什么,然后每个技能付几千个 token,每轮都付,永远。你注意不到,因为账单分散在许多看起来合理的小决策里。
这个洞察一旦你看清了,其实挺无聊的。选择某样东西只需要它的名字。
所以先暴露名字,按需获取内容。
工具。 255 个工具只保留名字的索引是 581 个 token。比原来的 71,929 减少了 99.2%。完整 schema 只在你真正调用工具时才出现。
技能。 一个宿主页指针 39 个 token 替换了整个目录。单次查询返回匹配的那几个技能,查询成本是 501 个 token。你只对胜出的技能读取完整的 SKILL.md 文件。同一个思路,另一张账单。
指针的全部职责就是一句话:不要加载目录,请求匹配结果,然后只读取那些文件。
我把这个做成了 mcptoon,一个零依赖的命令行工具。纯 Python 标准库,227KB,它让每个服务器和每个技能都保持配置就绪、可寻址,同时把它们的完整文本挡在上下文窗口之外。mcptoon manifest 给你工具的名字-only 视图。mcptoon skills resolve "<你想做什么>" 返回最佳匹配的技能,格式是 JSON。没有什么是预装的,在选择之前什么都不加载。
别拿我的目录当你的。税跟你接线多少成正比,修复也是。
统计你实际携带的内容。加载你真实的工具列表和技能目录,用 tiktoken 处理一遍,然后把完整内容跟名字-only 视图对比。如果差距很大,你有一张值得削减的账单。如果差距小,就不用。
这个测量才是关键。也是几乎所有人都跳过的步骤,这就是为什么第二张账单长期无人注意。
当你有很多工具和技能时这才值得。如果你就跑几个服务器和十几个技能,这些都不重要。税是真实存在的但很小,而网关是又一个需要维护的东西。不要为了优化而优化。
我的经验法则:先审查,只有测量结果告诉你上下文真的拥挤了,才考虑用名字索引层。
工具 schema 这边已经是个公认的问题了。Anthropic 自己的工程文档论证了这一点,Firecrawl 的基准测试也测量了同样的任务:1,365 个 token 通过 CLI 对比 44,026 个 token 通过 MCP,差距是 32 倍。这个讨论正在进行中。
技能这边还没有。而它其实是两者中更大的那个,因为技能是文档,不是 schema。
所以我想在评论区得到这个问题:当你的 Agent 加载一个技能时,加载的是指针还是整个文件?如果你测量过这个数字,我想看看,因为我觉得大多数人在付第二张账单却从未见过发票。