
1. 项目缘起当Agent的“技能包”成为成本负担最近在折腾一个基于大语言模型的智能体项目目标是让它能处理一些复杂的、多步骤的任务比如分析一份财报然后生成投资建议。为了让这个Agent足够“聪明”我给它装备了各种各样的“技能”——从调用外部API获取实时数据到执行复杂的Python代码进行数据分析再到调用专门的文本处理工具进行信息抽取。一开始效果确实不错Agent能像模像样地完成一系列操作。但问题很快就来了。每次调用这个Agent看着账单上飞速消耗的Token我的心都在滴血。仔细分析日志后发现一个简单的用户查询比如“帮我总结一下上周的销售数据”Agent在内部会触发一连串的“技能调用思考”。它会先“思考”是否需要调用数据库查询技能然后“思考”是否需要调用数据可视化技能再“思考”是否需要调用总结归纳技能……每一次“思考”都是一次对LLM的调用都在疯狂燃烧Token。更糟糕的是很多“思考”是冗余的。对于“总结销售数据”这种明确的任务它根本不需要去“思考”是否要调用“发送邮件”这个技能。这让我意识到我们给Agent赋予的“技能”越多它的“认知负荷”就越大每次决策的成本就越高。这就像一个工具箱你把所有工具都摊开在地上每次干活前都得把所有工具看一遍才能决定用哪个效率极低。SkillReducer这个概念就是为了解决这个问题而生的。它的核心目标不是让Agent变得更“笨”而是让它变得更“精”——通过优化和精简其技能调用逻辑用最少的Token消耗完成最高效的任务执行。2. 理解Agent技能调用的Token消耗机制要优化首先得知道“油”都烧在哪里了。在一个典型的LLM Agent架构中一次完整的技能调用其Token消耗可以拆解为以下几个部分2.1 系统提示词System Prompt的固定开销这是最大的、也是最容易被忽视的成本。为了让Agent理解并能够调用技能我们会在系统提示词里详细描述每一个技能的功能、输入参数、输出格式。例如你是一个数据分析助手。你可以调用以下技能 1. 技能名称query_database - 描述执行SQL查询从数据库中获取数据。 - 参数sql_query (字符串类型有效的SQL语句) - 返回查询结果集JSON格式或错误信息。 2. 技能名称generate_chart - 描述根据提供的数据生成图表。 - 参数data (JSON格式的数据) chart_type (字符串如‘bar‘ ‘line‘ ‘pie‘) - 返回图表的Base64编码图片或图表配置对象。 ...可能还有10个甚至20个其他技能问题在于无论用户当前的问题是否需要用到generate_chart这个技能这段关于它的描述可能占50-100个Token都会随着每一次对话被完整地发送给LLM。技能越多这段“技能说明书”就越长固定开销就越大。2.2 思维链Chain-of-Thought的决策开销当Agent收到用户请求后它需要“思考”该用什么技能。这个过程通常被设计为让LLM输出一个结构化的“思考过程”例如用户 “对比一下A产品和B产品上季度的销售额。” 思考 用户需要对比两个产品的销售额。这需要先获取A产品和B产品上季度的销售数据然后进行对比分析。因此我需要 1. 调用 query_database 技能执行SQL查询获取A产品的销售额。 2. 调用 query_database 技能执行SQL查询获取B产品的销售额。 3. 调用 data_comparison 技能对两组数据进行分析和对比。 最终行动 我将按顺序执行以上三个技能调用。这段“思考”本身也是由Token构成的。技能越多、任务越复杂LLM需要“权衡”和“规划”的路径就越多产生的思维链就可能越长、越曲折消耗的Token也就越多。2.3 技能描述在上下文中的重复出现在多轮对话中为了维持Agent的“记忆”我们通常会将对话历史包括之前的技能调用和结果也放入上下文。如果上一轮刚详细描述了generate_chart技能并调用了一次那么在这一轮的上下文里这些描述和调用记录依然存在。当技能描述本身很长时它们对上下文窗口的占用会持续累积挤占处理新问题所需的空间可能迫使你更早地进行摘要或遗忘间接增加成本或降低效果。2.4 一个简单的成本估算模型假设你的系统提示词中描述了10个技能平均每个技能描述消耗80个Token那么仅这一项固定开销就是800 Token。一次简单的用户查询20 Token。Agent产生一段中等长度的思考链150 Token。LLM根据思考链格式化输出第一个技能调用的参数50 Token。那么完成一次最简单的、仅调用一个技能的请求其输入Token至少为800 20 150 970 Token。这还没算上输出Token和可能的历史上下文。如果这个请求本身的价值比如一个简单的数据查询并不高那么这个成本效益比就非常差了。3. SkillReducer的核心优化策略与实践理解了成本结构我们就可以有针对性地动刀了。SkillReducer不是某个单一的算法而是一套组合策略。下面是我在项目中实践并验证有效的几种方法。3.1 策略一动态技能加载On-demand Skill Loading这是最直接、效果也最显著的策略。核心思想是不要一次性把所有技能说明书都塞给LLM而是根据对话的上下文动态地只加载最可能被用到的技能。如何实现为技能建立索引为每个技能创建简短的“标签”或“关键词”。例如query_database技能的标签可以是[“数据” “查询” “SQL” “数据库”]。send_email技能的标签可以是[“邮件” “通知” “发送”]。实现一个轻量级的路由器这个路由器可以是一个简单的规则引擎或者一个微型的、专门训练的分类模型成本远低于LLM。它的任务是根据当前用户query和最近的对话历史快速判断可能需要的技能类别。动态组装系统提示词在每次调用LLM Agent的主模型之前先让路由器跑一遍。假设路由器判断当前对话可能涉及“数据查询”和“分析”那么系统提示词就只加载query_database、data_analysis这两个技能的详细描述其他如send_email、generate_chart的描述则被暂时省略。实操示例与代码片段 假设我们有一个技能注册表skill_registry和一个简单的基于关键词匹配的路由器。class SkillRegistry: def __init__(self): self.skills { “query_database”: { “description”: “执行SQL查询从数据库中获取数据。参数: sql_query”, “tags”: [“数据” “查询” “sql” “数据库”] }, “generate_chart”: { “description”: “根据数据生成图表。参数: data, chart_type”, “tags”: [“图表” “可视化” “画图” “数据展示”] }, “send_email”: { “description”: “发送电子邮件。参数: recipient, subject, body”, “tags”: [“邮件” “通知” “发送”] } # ... 更多技能 } class SkillRouter: def __init__(self, registry): self.registry registry def select_skills(self, user_query, history, top_k3): “”“根据查询和上下文选择最相关的top_k个技能”“” query_keywords self._extract_keywords(user_query) skill_scores {} for skill_name, skill_info in self.registry.skills.items(): # 简单的关键词匹配计分 score sum(1 for tag in skill_info[“tags”] if tag in query_keywords) skill_scores[skill_name] score # 返回得分最高的top_k个技能名 selected sorted(skill_scores.items(), keylambda x: x[1], reverseTrue)[:top_k] return [skill[0] for skill in selected if skill[1] 0] # 只返回有得分的 def build_system_prompt(selected_skill_names, registry): “”“根据选中的技能名动态构建系统提示词”“” base_prompt “你是一个AI助手可以调用以下技能\n\n” for name in selected_skill_names: skill registry.skills[name] base_prompt f“技能{name}\n描述{skill[‘description’]}\n\n” base_prompt “请根据用户需求思考并决定是否需要以及如何调用上述技能。” return base_prompt # 使用示例 registry SkillRegistry() router SkillRouter(registry) user_ask “帮我查一下上个月的订单总数” selected_skills router.select_skills(user_ask, [], top_k2) # selected_skills 可能是 [‘query_database’] system_prompt build_system_prompt(selected_skills, registry) # 此时 system_prompt 只包含 ‘query_database‘ 的描述 Token大大节省。注意路由器的准确性是关键。如果路由器错误地过滤掉了某个必要技能Agent就会“失能”。因此初期可以设置一个“保底技能集”如始终包含一个通用的fallback或reasoning技能或者将路由器的置信度阈值设低一些宁可多加载一两个技能也不要漏掉关键技能。3.2 策略二技能描述的极致压缩与抽象化如果动态加载实现起来比较复杂或者对延迟要求极高那么优化技能描述本身就是第二选择。目标是用最少的Token传达最精确的技能信息。具体手法使用缩写和约定与其写“执行SQL查询从数据库中获取数据”不如写成“db(query)执行SQL”。在系统提示词开头统一说明“db代表数据库查询技能”。结构化而非自然语言LLM对结构化信息的理解能力很强。将技能描述从段落改为JSON Schema或类似格式通常更省Token。优化前自然语言 “调用此技能可以给用户发送电子邮件。你需要提供收件人邮箱地址、邮件主题和正文内容。”优化后结构化“send_email”: { “action”: “发送邮件” “params”: { “to”: “string (邮箱地址)”, “subject”: “string”, “body”: “string” } }分层描述为每个技能提供“一句话摘要”和“详细说明”。在系统提示词中只放入“一句话摘要”当Agent在思考链中初步决定要使用某个技能时再在后续的提示中或通过函数调用获取该技能的详细参数规范。这需要更精巧的Agent框架支持。3.3 策略三预测与缓存思维模式这个策略更进阶一些旨在减少Agent“思考”过程Chain-of-Thought的Token消耗。其灵感来源于我们发现对于特定领域的任务Agent的思考模式往往是可预测的。实践方法归纳常见任务模板分析历史日志总结出高频任务类型及其对应的标准技能调用流程。例如“数据报表请求”的模板可能是[query_database] - [data_aggregation] - [generate_summary]。实现模板匹配当新的用户请求进来时先用一个非常轻量级的模型或规则去匹配已知的任务模板。如果匹配成功则直接“注入”预设的、简化的思考链。例如用户说“给我看看上周的销售日报”。匹配到“数据报表请求”模板。注入的提示 “这是一个标准的数据报表请求。标准处理流程是1.查询数据库2.汇总数据3.生成文本摘要。请直接按此流程执行。”这样一来LLM就不需要从零开始“头脑风暴”技能调用顺序大大缩短了思维链。你甚至可以极端到对于匹配模板的请求直接让框架按流程执行技能绕过LLM的规划步骤仅让LLM负责参数填充和结果润色。踩坑记录模板匹配的颗粒度很重要。一开始我定义的模板太粗如“处理数据”导致很多不相关的请求也被匹配执行了错误的技能流。后来细化为“时间序列数据查询对比”、“多维数据下钻分析”等具体模板准确率才提上来。同时一定要保留“未知模板”的兜底路径让LLM用完整的思维链去处理新异问题。3.4 策略四技能组合与宏技能创建如果发现某些技能总是被连续调用且顺序固定那么就应该考虑将它们组合成一个新的“宏技能”Macro Skill。案例我的Agent经常先调用search_web搜索信息然后调用summarize_text总结信息。每次都是两个步骤两次LLM调用规划搜索、规划总结。优化后我创建了一个新的宏技能search_and_summarize。它的描述是“根据查询主题搜索网络信息并返回总结”。在内部这个宏技能用一个简单的脚本串联起了搜索API和总结API。收益系统提示词简化少了一个技能描述summarize_text可以移除或保留给其他用途。规划过程简化LLM现在只需要思考一步——“调用search_and_summarize”而不是两步。规划所需的Token减少。减少LLM调用在某些实现中甚至可以将宏技能作为一个独立模块运行只在最终输出时调用一次LLM进行润色进一步节省中间步骤的Token。4. 效果评估与权衡不只是Token数字的游戏实施了上述优化策略后我们需要一套方法来评估效果。不能只看Token消耗降低了多少还要关注对Agent能力的影响。4.1 量化评估指标平均每轮对话输入Token数这是最直接的指标。在优化前后对同一批测试用例进行统计观察下降百分比。任务成功率优化后的Agent是否还能像以前一样可靠地完成任务定义清晰的成功标准如正确调用技能、返回有效结果在测试集上计算成功率。Token的降低不能以成功率的显著下降为代价。响应延迟动态加载技能可能需要额外的路由计算时间。需要评估端到端的延迟变化确保在可接受范围内。成本下降比例结合LLM模型的定价如GPT-4每千Token输入/输出各多少钱直接换算成预估的成本节约。4.2 可能带来的副作用与应对技能召回率下降动态加载或过度压缩描述可能导致Agent“想不起”某个小众但关键时刻必需的技能。应对维护一个“核心技能”白名单无论什么上下文都始终加载或者为路由器设置较低的召回阈值宁可多加载。规划能力僵化过度依赖任务模板策略三可能会让Agent处理新问题时缺乏灵活性。应对将模板作为“建议”而非“指令”提供给LLM或者设计一个置信度机制只有高置信度匹配时才使用模板。系统复杂性增加引入了路由器、模板匹配器等新组件增加了系统的维护和调试难度。应对确保这些组件的逻辑相对简单、可观测性强打好日志避免过度工程化。4.3 我的实测数据分享在我自己的项目中综合运用了策略一动态加载和策略二描述压缩后得到了以下数据基于约1000次模拟对话平均输入Token下降约42%。这主要归功于将平均每次加载的技能描述从12个减少到了3-4个。任务成功率从优化前的96.5%略微下降到95.8%。下降的0.7%主要来自一些边缘案例其中路由器未能识别出某个生僻关键词。通过丰富技能标签和加入简单的同义词扩展这个问题基本被解决。响应延迟增加了约50-80毫秒主要来自路由计算对于总时长在2-3秒的交互来说用户体验无明显感知。预估成本节约按当时的API价格计算预计每月可节省15-20%的模型调用成本。这对于一个高频使用的生产系统来说是一笔可观的数目。5. 进阶思考SkillReducer与Agent架构的协同设计优化到最后我发现SkillReducer不仅仅是一个后期“打补丁”的优化技巧它更应该被融入到Agent架构的早期设计中去。5.1 技能设计的“原子性”与“可组合性”在设计技能之初就要考虑SkillReducer的需求。这意味着技能粒度要适中不要设计一个“分析财报并生成PPT”的巨无霸技能而应拆分为fetch_financial_data、calculate_ratios、generate_summary、create_slide等原子技能。这样路由器才能更精细地按需加载。技能接口要规范输入输出尽量采用统一的结构如JSON Schema这有利于动态提示词的生成和宏技能的创建。技能元信息要丰富除了名称和描述主动为技能设计好“标签”、“适用场景”、“前置技能”、“后置技能”等元数据。这些元数据是路由器进行智能选择的基础燃料。5.2 分层决策与混合编排一个高效的Agent系统可能不是单一LLM驱动的而是分层决策的轻量级路由层用低成本模型如小型微调模型或规则引擎快速判断任务领域和所需技能大类。这一层几乎不消耗Token。技能专用层对于某些复杂但固定的技能如写SQL可以训练专用的微调小模型它们在该任务上比通用大模型更准、更快、更便宜。通用规划与协调层只有当前两层无法处理或者需要高层次协调和推理时才请出昂贵的“主力”大模型如GPT-4并且此时提供给它的技能列表已经是经过过滤和优化的上下文清爽让它专注于真正的复杂规划。这种架构本质上是将SkillReducer的思想固化到了系统层面让合适的组件做合适的事最大化Token的使用效率。在我实际构建Agent系统的过程中Token效率是一个从第一天起就必须紧绷的弦。SkillReducer相关的优化不是一个一蹴而就的项目而是一个需要持续观察、分析日志、迭代策略的过程。核心心法就是像管理一个精英团队一样管理Agent的技能让每个Token的消耗都产生明确的价值杜绝任何形式的能力浪费和无效思考。每一次优化不仅是在节省成本更是在让整个系统变得更加清晰、健壮和高效。