返回首页

工具给太多,模型反而变笨了:聊聊上下文成本

工具数量从 3 涨到 30,准确率不升反降,token 账单还翻倍。这不是模型的问题,是你把选择题出得太难了。

一个反直觉的现象

做天气 Agent 的时候,我兴冲冲地把工具从 2 个加到了 12 个: 查天气、查空气质量、查日出日落、查紫外线、查历史天气…… 想着功能越全,Agent 越强。

结果准确率掉了。问"北京今天天气",它有时去调空气质量, 有时调历史天气。而且 token 消耗翻了三倍。

我以为是模型不行,换了个更大的模型,好了一点,但依然不如只有 2 个工具的时候。 后来才想明白:问题不在模型,在我。

原因一:所有工具定义每次都要发一遍

上一篇文章讲过,工具定义是提示词的一部分。关键是: 它在每一轮请求里都会被完整发送。

算一笔账:

项目单个工具12 个工具
工具名 + 描述~80 token~960 token
参数 Schema~60 token~720 token
小计(每轮固定开销)~140 token~1680 token

而这只是"固定开销"。一次工具调用至少两轮请求, 一轮对话十次交互就是 20 次请求——1680 × 20 = 33600 token 纯粹花在描述工具上,还没算真正的对话内容。

原因二:选择题变难了

这个更本质。模型每轮都在做一道题:"现在该用哪个工具?"

2 个工具时,这是一道二选一,而且两个工具差别很大(天气 vs 计算), 闭着眼睛都能选对。

12 个工具时,其中 6 个都是"查天气相关"的。它们的描述长这样:

get_weather        查询指定城市的当前天气
        get_air_quality    查询指定城市的空气质量
        get_sunrise        查询指定城市的日出日落时间
        get_uv_index       查询指定城市的紫外线指数
        get_weather_history 查询指定城市的历史天气
        get_forecast       查询指定城市未来天气
        

看出问题了吗?它们的描述高度相似, 都以"查询指定城市的"开头。模型很难从"当前 / 历史 / 未来" 这几个词的差别里稳定地做出正确选择。

原因三:注意力被稀释

即使不看成本,光从模型行为看:上下文里的选项越多, 每个选项获得的"注意力权重"越少。这不是玄学—— 上下文窗口是有限的,更多无关内容意味着关键信息的相对权重下降。

有实验表明,工具数量超过一定阈值(经验值大约 10-20 个, 取决于描述质量),选择准确率会明显下滑。

四个解法

解法一:拆分 Agent

不要一个 Agent 装所有工具,而是按领域拆开,由一个"路由"决定交给谁。

weather_agent = create_agent(model=model, tools=[get_weather, get_forecast])
        air_agent     = create_agent(model=model, tools=[get_air_quality, get_uv_index])
        search_agent  = create_agent(model=model, tools=[search, fetch_page])

        def route(query: str) -> str:
            """先用一次轻量请求判断该走哪个 Agent。"""
            ...
        

每个子 Agent 只有 2-3 个工具,选择题变得简单,准确率自然回升。 这也是"多 Agent 协作"最常见的动机之一。

解法二:把描述写出区分度

回到上面那 6 个工具,改成这样:

@tool
        def get_weather(city: str) -> str:
            """查询城市【当前此刻】的实时天气,包括气温和天气状况。

            只在用户问"现在""此刻""今天"天气时使用。
            不要用它查询历史数据或未来预报。
            """

        @tool
        def get_forecast(city: str, days: int) -> str:
            """查询城市【未来若干天】的天气预报。

            只在用户问"明天""后天""这周""未来几天"时使用。
            不要用它查询当前实时天气。
            """
        

关键是显式写出负向边界——"不要用它做什么"。 模型对这类指令很敏感,加上之后我的选择准确率明显改善。

解法三:动态加载工具

不要一次把所有工具都给模型,而是根据用户输入先做一轮粗筛, 只把相关的 3-5 个工具塞进这次请求:

def pick_tools(query: str, all_tools: list, top_k: int = 4) -> list:
            """用向量检索或关键词匹配,从全部工具里挑出最相关的几个。"""
            ...
        

这样每轮请求的固定开销从 1680 token 降到 500 左右, 而且选择题永远是 4 选 1。

解法四:压缩历史消息

工具调用结果会一直留在 messages 里。一个查天气的结果可能是 一大段 JSON,但真正有用的只有"晴,18℃"。

def summarize_tool_result(raw: str, limit: int = 200) -> str:
            """工具返回内容过长时,只保留关键部分。"""
            if len(raw) <= limit:
                return raw
            return raw[:limit] + "…(内容过长已截断)"
        

更激进的做法是用模型对早期对话做摘要,或者直接丢弃 N 轮之前的工具调用细节。

生产环境还要注意什么

  • 限流与重试:API 有 QPS 限制,要加退避重试
  • 超时控制:工具函数必须有 timeout,否则会挂住整个循环
  • 步数上限:防止模型陷入死循环烧钱
  • 成本监控:记录每次请求的 token 数,设置告警阈值
  • 可观测性:把每次工具调用和结果落日志,方便回溯

加工具之前先问自己:这个工具真的会被用到吗? 如果一年用一次,它给每轮对话增加的成本值不值?

小结

  • 工具定义是每轮请求的固定开销,数量直接乘上 token 成本
  • 描述高度相似会导致模型选择困难,边界必须写清楚
  • 工具多了应该拆 Agent、动态加载,而不是硬塞
  • 历史消息要压缩,工具返回结果要截断
  • 生产环境需要限流、超时、步数上限和成本监控