Agentic Skill Routing 实战:别再把所有 Skill 塞进 AI Agent 上下文

为什么需要 Skill Routing?在构建 AI Agent(如基于 GPT/Claude/本地 LLM)时,一个常见的错误是把所有技能(Skill)和工具(Tool)的描述一股脑塞进系统提示(System Prompt)或上下文窗口。这会导致三个严重问题:- Token 膨胀:上下文窗口被大量工具描述占满,留给用户对话和推理的空间变小,成本飙升。- 指令混淆:Agent 在数百个工具描述中迷失,容易调用错误的 Skill 或反复犹豫。- 性能下降:大模型在过长上下文中注意力分散,推理质量显著降低。Skill Routing 的核心思想是:动态选择最相关的少量技能注入 Agent 上下文,而不是全量注入。这就像人类专家不会把所有专业知识放在大脑前台,而是根据当前问题“路由”到对应知识库。## 实战案例:从全量注入到动态路由我们用一个实际场景来演示:一个多功能 Agent 拥有 6 种技能(天气查询、数学计算、知识问答、翻译、文件搜索、笑话生成)。传统做法是把所有技能描述塞入提示,而路由做法是先用一个“路由层”判断用户意图,再只注入 1-2 个相关技能。### 示例 1:传统全量注入(反面教材)pythonimport openaiimport json# 所有技能描述硬编码在 System Prompt 中ALL_SKILLS_PROMPT = """你是一个多技能AI助手。可用技能如下:1. 天气查询:获取任意城市的实时天气,参数:城市名(string)2. 数学计算:执行数学运算,参数:表达式(string)3. 知识问答:回答事实性问题,参数:问题(string)4. 翻译:将文本翻译成目标语言,参数:文本(string), 目标语言(string)5. 文件搜索:在本地文件系统中搜索文件,参数:关键词(string)6. 笑话生成:生成一个随机笑话,参数:主题(string) 可选请根据用户问题选择最合适的技能,并以JSON格式返回技能名称和参数。"""def traditional_agent(user_input: str) -> dict: """传统方法:每次请求都发送完整技能列表""" response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": ALL_SKILLS_PROMPT}, {"role": "user", "content": user_input} ], temperature=0.1 ) # 解析返回的JSON try: result = json.loads(response.choices[0].message.content) return result except: return {"error": "无法解析技能调用"}# 测试:用户问天气,但上下文里塞了5个无关技能print(traditional_agent("今天北京天气怎么样?"))# 输出可能正确,但消耗了约800 tokens(包括5个无关技能描述)问题分析:每次调用都发送 6 个技能描述,即使只用到天气技能。如果技能增加到 50 个,上下文将被占满。### 示例 2:基于语义相似度的 Skill Routing下面实现一个轻量级路由系统,使用嵌入向量(Embedding)进行意图匹配,只注入最相关的 2 个技能。pythonimport openaiimport numpy as npfrom typing import List, Tuple# 定义技能库(每个技能包含名称、描述、参数模板)SKILL_LIBRARY = [ { "name": "weather_query", "description": "获取任意城市的实时天气信息", "examples": ["北京天气", "今天纽约热吗", "上海下周降水概率"], "param_schema": {"city": "城市名(string)"} }, { "name": "math_calculate", "description": "执行数学运算,包括算术、三角函数等", "examples": ["2+3等于多少", "sin(30度)是多少", "计算平方根"], "param_schema": {"expression": "数学表达式(string)"} }, { "name": "knowledge_qa", "description": "回答事实性问题,基于知识库", "examples": ["地球周长多少", "爱因斯坦的生日", "Python是什么"], "param_schema": {"question": "问题(string)"} }, { "name": "translate", "description": "将文本翻译成指定语言", "examples": ["把hello翻译成中文", "翻译这段英文到法语"], "param_schema": {"text": "原文(string)", "target_lang": "目标语言(string)"} }, { "name": "file_search", "description": "在本地文件系统中搜索文件", "examples": ["找一下昨天的报告", "搜索PDF文件"], "param_schema": {"keyword": "关键词(string)"} }, { "name": "joke_generate", "description": "生成一个随机笑话", "examples": ["讲个冷笑话", "程序员笑话"], "param_schema": {"topic": "主题(string)", "optional": True} }]class SkillRouter: """基于嵌入向量的技能路由器""" def __init__(self, skills: List[dict]): self.skills = skills # 预计算每个技能的嵌入向量(基于例子和描述) self.skill_embeddings = self._compute_skill_embeddings() def _get_embedding(self, text: str) -> np.ndarray: """调用OpenAI Embedding API获取文本向量""" response = openai.Embedding.create( model="text-embedding-3-small", input=text ) return np.array(response['data'][0]['embedding']) def _compute_skill_embeddings(self) -> np.ndarray: """为每个技能计算平均嵌入向量(基于描述+例子)""" embeddings = [] for skill in self.skills: # 将描述和所有例子拼接作为技能表征 skill_text = skill['description'] + " " + " ".join(skill['examples']) emb = self._get_embedding(skill_text) embeddings.append(emb) return np.array(embeddings) def route(self, user_input: str, top_k: int = 2) -> List[dict]: """根据用户输入,返回最相关的 top_k 个技能""" input_emb = self._get_embedding(user_input) # 计算余弦相似度 similarities = [] for skill_emb in self.skill_embeddings: cos_sim = np.dot(input_emb, skill_emb) / ( np.linalg.norm(input_emb) * np.linalg.norm(skill_emb) ) similarities.append(cos_sim) # 获取 top_k 索引 top_indices = np.argsort(similarities)[-top_k:][::-1] # 返回选中技能及其相似度 selected = [] for idx in top_indices: selected.append({ "skill": self.skills[idx], "similarity": float(similarities[idx]) }) return selected# 初始化路由器router = SkillRouter(SKILL_LIBRARY)def routed_agent(user_input: str) -> dict: """路由版Agent:先路由再注入""" # 第一步:路由,只选最相关的2个技能 top_skills = router.route(user_input, top_k=2) # 第二步:动态构造 System Prompt,只包含选中技能 skill_descriptions = [] for s in top_skills: skill = s['skill'] desc = f"- {skill['name']}: {skill['description']},参数:{json.dumps(skill['param_schema'])}" skill_descriptions.append(desc) dynamic_prompt = f"""你是智能助手。当前可用的技能如下(仅这些):{chr(10).join(skill_descriptions)}请根据用户问题选择合适的技能,并以JSON格式返回技能名称和参数。""" # 第三步:调用LLM,上下文仅包含2个技能 response = openai.ChatCompletion.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": dynamic_prompt}, {"role": "user", "content": user_input} ], temperature=0.1 ) # 返回结果和路由信息 try: result = json.loads(response.choices[0].message.content) return { "skill_call": result, "routing_info": [s['skill']['name'] for s in top_skills], "tokens_saved": "减少了4个无关技能描述的token消耗" } except: return {"error": "无法解析技能调用"}# 测试路由效果test_input = "今天北京天气怎么样?"result = routed_agent(test_input)print("路由结果:")print(f"选中的技能:{result['routing_info']}")print(f"技能调用:{result['skill_call']}")print(f"Token节省:{result['tokens_saved']}")# 输出示例:# 选中的技能:['weather_query', 'knowledge_qa']# 技能调用:{'skill': 'weather_query', 'parameters': {'city': '北京'}}# Token节省:减少了4个无关技能描述的token消耗## 进阶技巧:多层路由与缓存### 1. 路由缓存对于高频意图(如“天气”“翻译”),可以缓存路由结果,避免重复计算嵌入。python# 简单的LRU缓存示例from functools import lru_cache@lru_cache(maxsize=100)def cached_route(user_input: str) -> List[dict]: return router.route(user_input)### 2. 分层路由对于超大规模技能库(100+),可先分大类(如“工具类”“知识类”“娱乐类”),再细粒度匹配。这能大幅降低计算开销。### 3. 混合路由策略对于简单意图(如“讲个笑话”),可以用规则匹配(正则或关键词),避免调用 Embedding API 产生额外费用。## 性能对比| 指标 | 全量注入 | Skill Routing ||------|----------|---------------|| 每次请求 Token 消耗 | ~800 | ~200(节省75%) || 技能数量上限 | 10-20(受上下文限制) | 100+(可扩展) || 调用准确率 | 随技能数增加下降 | 保持稳定 || 响应延迟 | 较高(长上下文) | 较低(短上下文) || 维护成本 | 修改一个技能需更新全量 | 独立更新技能库 |## 总结别再把所有 Skill 塞进 AI Agent 上下文——这就像让一个厨师每次做菜前都背诵整本菜谱,既浪费又低效。通过 Agentic Skill Routing,我们可以:1. 动态选择:基于语义相似度或规则,只注入最相关的 2-3 个技能2. 大幅降本:减少 60-80% 的 Token 消耗,响应更快3. 提升质量:Agent 在精简上下文中决策更准确、更稳定实战中,你可以从简单的 Embedding 路由开始,逐步加入缓存、分层、规则混合等优化。记住:Agent 不是百科全书,而是智慧的路由器。让每个技能在正确的时间被调用,才是高效 AI 系统的精髓。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐