AI Agent工具Schema精简47%:重复YAML键的隐形陷阱

自托管AI Agent的瓶颈多在工具Schema预填充,精简47%可每轮省约9秒,但需警惕重复YAML键静默失效。
文章记录了开发者在自建Hermes Agent上优化响应速度的完整过程。通过抓包与`prompt-size`测量,发现工具Schema占固定提示词的58%–67%,prefill阶段才是6GB GPU上单轮超90秒的真凶,而非模型能力。开发者统计约1,900条消息的工具调用记录,据此禁用零调用工具集,使专业profile的Schema字节减少47%、固定提示词减少27%,每轮省约5,400 token、约9秒。过程中踩到两个隐性陷阱:YAML重复键被PyYAML静默保留最后一个值,导致新配置被空列表覆盖;CLI将列表写成带引号字符串而被忽略。核心启示是先测量再动手、用解析器而非grep验证配置、每次改动后重新测量。
AI Agent工具Schema精简47%:重复YAML键的隐形陷阱

自托管AI Agent响应缓慢时,大多数人的第一反应是换模型或加显存。但真正的瓶颈往往藏在更隐蔽的地方——每次请求中,Agent都在向模型重复描述那些从未被调用的工具。一位开发者在自建Hermes Agent系统上,通过精简工具Schema将固定提示词减少了27%,而过程中遇到的一个重复YAML键问题,让改动"看起来生效"却实际上什么都没做。
时间到底花在了哪里
该开发者运行Hermes Agent,配置了多个专业profile——编码、审查、侦察、安全和写作,外加一个默认的"幕僚长"profile,全部指向一台6GB GPU的本地模型。单轮对话经常超过一分半钟,直觉上应该归咎于模型能力不足。
实测排除了这个假设:更大的候选模型在任何上下文长度下都装不进6GB显存,而最接近的替代方案在关键指标上打平。真正关键的是prefill阶段——模型返回第一个token之前处理提示词的过程。一次真实请求捕获显示,prompt总量约18,400个token,其中13,541个(74%)是27个已启用工具的JSON Schema。换个角度衡量,工具Schema占每个profile固定提示词的58%–67%。以该机器约600 token/秒的prefill速度计算,每一轮对话都有大量时间花在模型"思考之前"。
结论很明确:杠杆不在模型,在工具列表。
Prefill 与 Decode 的分工。 大模型推理通常分为两个阶段:prefill 和 decode。prefill 是模型一次性并行处理整段输入提示词、建立 KV Cache 的过程,它决定了首个 token 的返回延迟(TTFT),其耗时可近似为 提示词 token 数 ÷ prefill 吞吐(token/秒)。decode 则是模型逐 token 生成输出的阶段,受显存带宽和模型规模制约。在自托管、多轮对话的场景中,如果系统提示词和工具 Schema 很长而输出很短,prefill 往往占据每轮总时延的大头——这正是文中 6GB GPU 上单轮超过 90 秒的主要原因。这也是为什么削减提示词中占比最高的工具 Schema,比更换大模型更能立竿见影地提速。
KV Cache。 prefill 阶段建立的键值缓存(KV Cache)保存了已处理 token 的注意力中间结果,使后续 decode 无需重复计算前缀。它直接消耗显存,因此提示词越长,可留给生成长度和并发的显存越少。在显存紧张的机器上,精简固定提示词不仅省时间,也间接提升了可用上下文预算。
先测量,再动手
Hermes提供了离线命令hermes prompt-size --json,可以分解固定提示词的构成:系统提示词、技能索引、记忆、工具数量与Schema总字节数,以及按工具集细分的明细。
hermes --profile coder prompt-size --json
一个容易踩的坑:按工具集细分的数组叫toolsets_breakdown,不是toolsets。
为了获得真实token数而非估算值,开发者将文本发送到Ollama的/api/generate接口,读取响应中的prompt_eval_count。针对这些Schema,实测约3.97字节/token,这让prompt-size输出的字节数可以方便地换算。
此外还使用了之前为调试其他问题搭建的抓包装置:一个伪装成OpenAI兼容端点的服务,记录实际传输内容。两条经验:保留一次运行中最大的请求而非第一个(首次调用通常不含工具);按请求从文件打标签,而不是从服务端环境读取。
为什么字节数需要换算成 token。 hermes prompt-size 输出的是 Schema 的字节数,而模型计费和推理成本按 token 计。不同分词器、不同字符集下字节与 token 的比值差异很大:英文文本约为 4 字节/token,中文约 1.5–3 字节/token,而 JSON Schema 因大量标点、键名和重复结构,比值常见于 3.5–4.5 之间。文中实测约 3.97 字节/token 属于典型值。使用 prompt_eval_count 这类由推理引擎真实返回的计数,比用字符数除以经验系数更可靠,也避免了对优化效果的误判。若手头没有真实计数,可用 Ollama 的 /api/generate 或 OpenAI 兼容接口返回的 usage 字段做交叉校准。
按使用记录做决策
开发者没有凭直觉裁剪工具,而是先查了Agent实际调用了什么。Hermes将每个profile的消息历史保存在本地SQLite数据库中,于是统计了约1,900条消息中的工具调用。
- 10个
browser_*工具、text_to_speech、clarify和process的调用次数为零。 session_search和delegate_task有使用,但仅限默认profile(分别为5次和21次)。
基于此做出有据可依的裁剪:
| profile | 禁用的工具集 |
|---|---|
| default | tts |
| coder, reviewer, scout, sec, writer | browser, tts, clarify, session_search, delegation |
上线前有一件事必须验证:browser工具集是一个包含13个工具的捆绑包,其中含有Agent实际在用的web_search。在抓包装置上,禁用browser只移除了10个browser_*工具,web_search因属于受保护的核心工具而保留。仅看捆绑包列表会得出错误结论,实测才能确认。
陷阱一:重复的YAML键
配置项是每个profile配置文件中的agent.disabled_toolsets。开发者在agent:行后直接添加:
agent:
disabled_toolsets: [browser, tts, clarify, session_search, delegation]
# ... 大量其他配置 ...
disabled_toolsets: []
看起来没问题。grep -A6显示新列表就在预期位置。但它什么都没做。
同一个agent:块的下方,文件本来就有disabled_toolsets: []。YAML对映射中的重复键不会报错;PyYAML直接保留最后一个。于是新列表被解析为空列表,所有工具保持启用,没有任何警告。
修复方法平淡但重要:
- 添加前先数键:对文件执行
grep -c disabled_toolsets。如果已存在,编辑现有行。 - 用真实解析验证,绝不用grep:
python3 -c "import yaml; print(yaml.safe_load(open('config.yaml'))['agent']['disabled_toolsets'])"
grep展示的是文本,只有解析器才能展示程序实际看到的内容。后来编写编辑profile配置的辅助脚本时,开发者使用了拒绝重复键的YAML加载器,先备份,写入后重新读取验证,失败则报错。
为什么重复键会被静默吞掉。 YAML 规范中的映射(mapping)要求键唯一,但并未强制解析器对重复键报错,如何处理交由实现决定。PyYAML 等主流实现的默认策略是“后者覆盖前者”,即保留最后出现的值,且不产生任何警告。这与其他配置格式形成危险反差:JSON 解析器通常对重复键也取最后一个,而许多静态检查工具(如 yamllint 的某些规则)只检查缩进和语法,不检查语义重复。因此一个文件里两处 disabled_toolsets 完全合法地“通过校验”,却让早先的配置失效。防御手段有三:一是编辑前用 grep -c 统计键出现次数;二是用 ruamel.yaml 等能在重复键时报错或保留注释的加载器;三是写入后立刻用 yaml.safe_load 读回断言,把“文本正确”与“语义正确”分开验证。
陷阱二:CLI写不了列表
手工编辑的替代方案是命令行:
hermes config set agent.disabled_toolsets '["browser","tts"]'
同样无效。它把值写成了带引号的字符串:
disabled_toolsets: '["browser","tts"]'
过滤器期望列表,静默忽略字符串,检查时tts依然存在。列表值必须通过编辑文件写成真正的YAML序列。
两个陷阱形状相同:配置看起来已设置,工具不报错,行为却不变。只有测量结果才能发现问题。
CLI 与 YAML 类型系统的错配。 命令行只能传递字符串,而 YAML 区分标量、序列和映射三种复合结构。hermes config set agent.disabled_toolsets '[...]' 中的方括号参数被 shell 原样传入后,配置工具将其当作普通字符串写入,于是 YAML 里出现 disabled_toolsets: '["browser","tts"]'——它在语法上是一个合法的标量字符串,而非列表。下游代码若用 if "browser" in disabled_toolsets 判断,字符串包含判断甚至会“碰巧”命中,但在多数使用 set() 或列表相等比较的实现中会被忽略。此类配置工具应支持显式的类型标记(如 --type json)或断言写入后 isinstance(value, list);若工具不支持,则任何列表/字典型配置都应回归文件编辑,并用解析器验证类型。
精简效果
来自prompt-size的前后对比:
| profile | 工具数 | 工具Schema字节 | 固定提示词总量 |
|---|---|---|---|
| 专业profile(×5) | 27 → 13 | 46,403 → 24,795(−47%) | 80,057 → 58,262(−27%) |
| default | 24 → 23 | 50,058 → 48,188(−3.7%) | 74,699 → 72,829 |
每个专业profile每轮节省约5,400个token,按实测prefill速度约合9秒。
改动后验证了可能出问题的地方:网关正常启动运行,编码Agent正确执行了真实终端命令,仍能访问MCP服务器——位于其前面的tool_search/tool_call/tool_describe三件套在裁剪后存活。默认profile保留了网页搜索和委派功能。所有文件事先备份,回滚只需恢复文件并重启网关。
剩余空间
default profile上有5个记忆集成工具,共7,663字节,从未被调用。它们不出现在常规工具集下,因为记忆插件直接注入,disabled_toolsets触及不到。移除它们意味着关闭该集成,这属于产品决策而非简单裁剪。
剩余的大型工具集——file、skills、terminal、memory——都在正常使用。固定提示词的另一半现在是系统提示词(约21–24 KB)和技能索引(约9 KB),这是下一个值得关注的方向。
核心经验
- 先测量提示词,再怪模型。 小显存GPU上prefill占主导,工具Schema可能占其中大部分。
- 按记录的使用情况裁剪。 消息历史就是证据;约1,900条消息中零调用是明确信号。
- 绝不相信用grep验证过的配置编辑。 用解析器。重复YAML键不报错,最后一个生效。
- 检查CLI如何写入列表。 被引号包成字符串的列表看起来正常,实际无效。
- 每次改动后重新测量。 唯一能证明裁剪生效的是字节数下降。
相关推荐

uGAP:统一100+非洲地理数据集的AI平台
uGAP是开普敦大学团队开发的非洲地理空间AI平台,统一120余个非洲数据集并提供Python SDK,支持数据发现、处理与模型训练一站式工作流,已用于气旋洪水制图等实战场景。

Cursor能控制你的电脑了:AI代理可驱动桌面应用测试
Cursor 新增桌面控制能力,AI 代理可驱动 iOS 模拟器等桌面应用测试自己构建的功能。本文分析这一更新对开发工作流的影响、技术实现可能路径及可靠性考量。

Vibecoding自研推理引擎:理想丰满,现实骨感
Reddit开发者分享Vibecoding自研推理引擎的真实经历:用WebGL2和GLSL在浏览器跑GGUF Q4模型,却陷入tokenizer调试地狱。本文分析浏览器端LLM推理的工程难点,以及为何直接clone llama.cpp是更理性的选择。