尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Agent技能库设计:让大模型按需选对工具,告别调用混乱
如果你试过给大模型 Agent 塞一堆工具函数大概率会遇到一个尴尬的场景工具太少模型办不了事工具一多模型就开始瞎选。我最近把一个内部项目里散落的函数调用整理成了一个叫 agent-skills 的技能库效果比预期好不少。这个思路并不复杂把 Agent 能做的事都包装成技能每个技能带清晰描述、参数定义和独立执行入口让模型按需挑选、按格式调用整套东西跑起来之后再也不用对着几百行 if-else 发愁了。先同步一下背景。这个项目原本是一个多场景的办公自动化助手需要在对话里处理日程查询、文档摘要、数据计算、消息推送等一大堆操作。最早版本就是经典的把所有函数塞进工具列表模型确实能调用一部分但错误率很高维护起来更痛苦。后来我把能力层全部重构为技能注册表加执行引擎也就是 agent-skills 这个库的核心线上调用成功率明显提升新增能力也不再碰主逻辑代码。下面把这些设计取舍、实现细节和踩过的坑完整记录下来正在给 Agent 做工具层的同学可以直接参考。1. 为什么要把 Agent 能力拆成技能从一堆函数到一张技能清单1.1 工具一多模型就开始选择困难最早我把几十个函数一股脑放进工具列表表面上看模型拿到了一整箱工具箱实际上问题很快就暴露出来。最典型的场景是用户说帮我看看明天下午有没有空模型需要从查询日历读取邮件查看待办三个工具里选一个这三个工具的说明如果写得不够清晰模型选错的概率非常高。它可能调用了一个和意图相近但完全没有必要执行的函数或者因为工具描述太相似干脆编造了一个不存在的参数。这背后的原因不难理解。大语言模型在做工具选择时本质上是把自然语言意图与工具描述文本做语义匹配。工具描述太笼统比如查询日历模型知道该用它但不确定参数该怎么填工具描述太细碎比如每个函数都写三大段使用说明模型反而被噪声淹没了。更麻烦的是这些工具之间还有隐式的前置条件比如查询日程前可能需要先获取时区这种逻辑如果全让模型自己在运行时推断几乎一定会出错。1.2 技能库要解决的三个问题我在项目里踩了一圈坑之后把对工具层的诉求总结成了三个问题agent-skills 就是围绕这三个问题重新设计的。第一个问题是可发现性。模型必须能在几十上百个技能里快速找出当前需要的那一个。这要求每个技能有稳定且可检索的名字、标签和一句高质量描述而不是让模型在长文本里大海捞针。第二个问题是可校验性。技能参数在进入真实函数之前必须经过严格检查。很多调用失败的案例并不是函数本身有 bug而是模型生成了缺失参数、错误类型或越界枚举值。技能库要在执行前做一道闸门不合格的调用直接给出结构化错误信息方便模型自我纠正。第三个问题是可复用性。一个技能不应该只服务于某一个对话场景它应该作为独立单元被注册、被缓存、被多个 Agent 共享。我之前在项目里遇到过同样的日历查询逻辑被写进三个不同模块后来改了一处其他两处还在用旧逻辑这种问题在技能库模式下从根上消失了。1.3 设计目标可发现、可校验、可复用确定了这三个目标之后整个 agent-skills 的架构就顺理成章了。技能库最底层是注册表负责保存所有技能定义往上一层是描述协议规定每个技能如何向模型展示自己再往上是参数校验器负责在执行前拦截非法调用最顶层是执行引擎负责把合法的技能调用跑起来并把结果回传给 Agent。各层只依赖标准库和少量通用组件不绑定任何特定的大模型服务或第三方框架这样无论是接某个云端模型 API还是跑本地开源模型都能直接使用。这里有一个我后来才意识到的重要原则Agent 的工具层代码要尽量和模型提供商解耦。很多框架天然帮你做了模型接入但换来的是强烈的框架绑定技能定义方式、调用方式都要按框架的格式去写。agent-skills 选择只做一个纯 Python 的执行层模型的接入方式全部由上层游刃有余地适配这一层稳如磐石换模型服务商就像换一个配置项那么轻松。2. 技能注册与描述规范让模型看得懂、选得对2.1 Skill 数据结构的取舍技能库的第一步是把所有能力抽象成统一的数据结构。我看过不少工具注册方案喜欢用纯字典保存等用到时再做字段提取这样确实灵活但代价是缺少类型约束和代码补全维护起来容易在某个环节漏掉一个 key。agent-skills 里我用了数据类来定义 Skill既保留 JSON 化的能力又让代码里写起来顺手。from dataclasses import dataclass, field from typing import Callable, Any, Optional dataclass class Skill: name: str description: str parameters: dict handler: Callable[..., Any] version: str 1.0 tags: list field(default_factorylist) timeout: float 30.0 examples: list field(default_factorylist) def to_dict(self) - dict: return { name: self.name, description: self.description, parameters: self.parameters, version: self.version, tags: self.tags, examples: self.examples, }字段设计上name 是技能唯一标识必须全局唯一且用英文小写加下划线方便模型精确引用。description 是给模型看的参考信息这段文本的质量直接影响选择准确率后面单独讲。parameters 是一个 JSON Schema 风格的字典描述参数名、类型、必填性、枚举值等信息。handler 是真正的 Python 函数它的参数名必须和 parameters 里的 key 一一对应这是最简单也最不容易出错的约定。我还专门留了 version 字段。别小看这个字段技能在迭代过程中接口签名可能变化模型在长上下文里偶尔会把旧参数名带出来有了版本号注册表就能区分新旧技能甚至可以在版本冲突时拒绝加载避免一半调用用新逻辑、一半调用用旧逻辑的混乱场面。2.2 描述字段怎么写模型才买账专业技能描述是我这次重构里收益最大的一块。之前写工具说明非常随意比如get_weather(city)后跟一句获取天气模型能用但不好用。它不知道该在什么时候调用、参数填中文还是拼音、返回结果长什么样。agent-skills 的 description 描述遵循一个原则先写触发场景再写参数要求最后写返回结果同时避免和别的技能混淆。以查询天气为例我会写成当用户询问某个城市当前或未来几天的天气、气温、降水概率时使用。 参数 city 为城市中文名如北京days 为查询天数整数可选默认为 1。 返回内容包括城市、日期、天气现象、最高气温和最低气温。这句话前半段是触发条件后半段是参数规范模型看到之后基本不会再问该传什么参数这种问题。反过来如果要写一个查询空气质量技能我会在 description 里额外加一句本技能仅查询空气质量指数不包含天气温度信息直接和天气技能做语义区隔。这种反例描述非常有效模型不再犹豫两个相似功能选哪个。另外一个容易被忽略的细节是给技能取名。名字不要做语义压缩比如一个将科学计算表达式求值的技能不要叫calc叫evaluate_math_expression更稳。有些场景下名字和描述构成了双重语义信号名字越明确模型选错的概率越低。但也要控制长度比如calculate_daily_sales_forecast_for_regional_teams这种过长名字反而会干扰上下文适度精确就好。2.3 参数 Schema 与校验规则参数这块我踩过最大的坑就是模型返回的 JSON 根本不按 Schema 来。技能定义里明明写了 days 是整数模型可能输出days: 2或者days: 2.0。如果直接把这些参数扔进 Python 函数虽然大多数情况下运行没问题但一旦参数是从用户输入里拼接来的就有被注入的风险。所以注册表的参数 Schema 必须真实校验。我用了一个精简版 JSON Schema 校验器直接写在 skill 模块里不依赖第三方库。核心逻辑是遍历参数定义检查必填项是否存在、值类型是否正确、枚举值是否合法同时做一层基础的类型宽松转换字符串形式的数字可以被转成整数或浮点数布尔值接受 true/false 与 true/false 两种写法。这个宽松层很重要因为不少模型服务在输出 JSON 时会自动把数字序列化成字符串直接报错会导致模型整轮重试效率极低。校验失败时返回错误信息也要结构化比如{error: param_not_found, missing: [city]}。模型拿到这种错误之后下一轮基本就能自动补全参数而不是像以前那样报一大段 Python traceback 过去模型看到长错误就蒙了。这里的关键是错误信息是写给模型看的不是写给人看的所以越结构化越好。3. 调度与执行引擎技能是怎么被选中、跑起来、拿到结果的3.1 单轮调用的核心把技能选择变成结构化输出技能库的执行流程可以拆成三步模型看到技能清单和用户输入输出一个结构化调用解析器读取这个调用去注册表里找到对应技能校验参数后执行最后把结果交给模型继续处理。为了让模型输出稳定agent-skills 的 prompt 里明确要求模型只返回 JSON格式固定为{skill: skill_name, arguments: {param1: value1}}如果模型觉得当前没有任何技能能满足用户需求必须返回{skill: no_skill}而不是自由发挥解释一段文字。这个约束看着简单实际效果立竿见影。没有这个约束时模型偶尔会把叫我用哪个技能和直接回答用户问题混在一起输出一串自然语言。现在解析器只要做一次 JSON 解析失败就交给错误重试流程不用写一堆正则去猜。解析模块我用了非常保守的策略。拿到模型输出文本后先尝试用 JSON 解析器直接解析如果失败就找第一个{和最后一个}之间的内容再解析一次。两层兜底之后如果还是失败就把原始输出交给一个重新格式化的技能让模型自己把刚才的输出改成合法 JSON。这种方法比任何字符串修正都可靠因为模型自己知道刚才想表达什么。3.2 多技能串联时的上下文传递单技能调用只是基础真实场景里 Agent 经常需要连续执行多个技能。例如用户说查一下明天的天气如果下雨就提醒我改成线上会议这就需要先执行天气查询再根据结果决定要不要改会议安排。agent-skills 用一个 ExecutorContext 对象来承载这类跨技能的数据传递。class ExecutorContext: def __init__(self): self.variables {} self.history [] def set(self, key, value): self.variables[key] value def get(self, key, defaultNone): return self.variables.get(key, default) def record(self, skill_name, arguments, result): self.history.append({ skill: skill_name, arguments: arguments, result: result, })每次技能执行完成后返回值会写入 context后续技能可以用context.get(weather_result)拿到前一步的结果。这相当于给技能之间搭了一座桥但桥是显式的不会出现暗地里共享全局变量的情况。每个技能 handler 的签名统一为def handler(arguments: dict, context: ExecutorContext)这样就能在不改变技能函数内部逻辑的前提下把上下文传给所有技能。全局变量这条老路我坚决不走。技能函数如果直接读写模块级变量重构时会非常难排查一次调用里技能 A 改了全局状态而技能 B 依赖那个状态模型稍微变化一下调用顺序就出 bug。改成 context 对象之后每个 ExecutorContext 只属于一次完整的 Agent 任务并发任务之间天然隔离这个设计帮我省掉了至少十次线上问题定位。3.3 超时、重试与并发控制模型调用技能时最怕遇到 handler 阻塞不返回。尤其是那些涉及外部 HTTP 请求的技能一旦上游服务慢整个 Agent 对话就会被拖死。我在执行引擎里为每个技能设置了独立的超时时间默认 30 秒执行时通过线程池提交任务再用 future 的超时机制拉闸。from concurrent.futures import ThreadPoolExecutor, TimeoutError executor ThreadPoolExecutor(max_workers8) def execute_skill(skill, arguments, context): validated validate_arguments(skill.parameters, arguments) future executor.submit(skill.handler, validated, context) try: result future.result(timeoutskill.timeout) return {ok: True, result: result} except TimeoutError: return {ok: False, error: timeout, skill: skill.name}这里有一个实际使用中容易忽略的点future 超时之后底层线程其实还在运行并没有真的被杀死。所以超时返回后我会把对应技能标记为不健康当次 Agent 任务里不再重复调用它同时记录日志方便排查。某些技能比如查询外部订单状态天然适合做重试我会在注册时给 Skill 加一个 max_retries 字段超时或返回网络错误时自动重试一次重试时把 context 里已有的中间结果传下去避免重复执行前置技能。并发控制方面8 个 worker 是跑本地服务时的相对均衡值。如果技能里有 CPU 密集型的 Python 计算GIL 会让线程池的并发收益有限此时可以再包一层进程池或者干脆把这类技能标记为串行执行。不要在一个执行引擎里无脑堆线程否则技能没快多少内存先爆了。我实测过普通办公自动化类技能大多是 I/O 密集线程池完全够用只有涉及大量数值计算的技能才需要额外优化。4. 从零新增一个技能的完整实操4.1 技能目录与代码模板agent-skills 的项目目录我保持了最简单直观的布局agent-skills/ core/ registry.py executor.py validation.py context.py skills/ __init__.py weather.py calendar.py python_eval.py examples/ demo_agent.py每新增一个技能就在 skills 目录下新建一个文件里面定义一个生成 Skill 对象的函数。这样技能文件之间完全解耦删掉一个文件就等于删除技能不会影响其他模块。为了让技能开发像填空一样简单我提供了一个模板函数def create_skill(): return Skill( nameskill_name_here, description当用户需要XXX时使用。参数xxx为XXX返回XXX。, parameters{ param1: {type: string, required: True, description: 参数说明}, param2: {type: integer, required: False, default: 1, minimum: 1, maximum: 30}, }, handlerhandler_func, tags[category], version1.0.0, timeout10.0, )4.2 示例内置一个执行 Python 代码技能我把执行 Python 代码作为一个示例技能放进了项目里这个技能最能体现技能需要校验的重要性。handler 接收一段 Python 源码字符串在受限环境里执行并返回输出。def eval_python_handler(arguments, context): code arguments.get(code, ) env { __builtins__: {}, math: math, json: json, context: context, } try: exec(code, env) output env.get(_result, ) return {ok: True, output: str(output)} except Exception as e: return {ok: False, error: str(e)}这里我把__builtins__置为空字典只允许显式导入的 math、json 和 context 被使用。它不是一个严格沙箱但对于内部办公场景已经够用如果要接收不可信用户代码就必须放到容器或者隔离进程里跑这点我劝你千万不要马虎。技能注册时我把这个技能的超时设置成 20 秒因为代码执行可能涉及一些稍重的计算而且还要避免死循环把线程池拖垮。注册方式非常简单from core import SkillRegistry from skills.python_eval import create_skill registry SkillRegistry() registry.register(create_skill())注册表内部维护了一个字典和一组标签索引。按 name 查找技能走的是 O(1) 字典查询按标签做技能候选筛选用的是倒排索引。目前技能数量只有几十个性能几乎无感但如果以后技能库膨胀到几百个技能描述文本全部塞给模型会导致上下文爆炸那时就必须做标签粗筛加向量召回两级流程先让模型从十几个候选里选而不是从几百个里选。4.3 本地测试先绕过模型直接验证新增技能后我建议不要直接接入对话流程先用一段本地脚本模拟模型调用把技能跑通再说。具体做法是写一个假的选择函数直接返回目标技能名和参数然后执行引擎走完整链路。skill registry.get(eval_python) arguments {code: _result math.sqrt(16) context.get(base, 0)} context ExecutorContext() context.set(base, 10) result execute_skill(skill, arguments, context) print(result) # 期望 {ok: True, output: 14.0}这一步能过滤掉绝大多数低级问题比如参数名不一致、handler 内部异常、context 取不到值等。确认技能本身没问题后再把它加进 Agent 的技能清单里这样后面模型出问题的时候你就能把技能实现 bug和模型选错技能分离开定位效率大幅提升。没有这一步隔离遇到一个错误你根本分不清是技能的锅还是模型的锅实际排查非常痛苦。5. 常见问题与排查技巧实录5.1 模型总选错技能问题不在模型如果你发现模型频繁选错技能比如用户问天气它调了日历技能我的第一反应是去看技能的 description 是不是互相混淆了。很多情况下两个技能的触发场景确实有重叠比如查询今日安排和查询当前任务模型很难分清。处理办法是在两者的 description 里互相加一句本技能不包括XX。另一个隐蔽原因是技能清单顺序。模型对列表靠前的技能有轻微偏好因为前面的词在语义上下文中权重更高。这不是玄学我在 experiment 里真实测过把某个低概率技能从第 20 位移到第 1 位后它的被选率从 12% 升到了 21%。所以高频技能应该排前面低频技能排后面或者干脆在 prompt 里明确告知模型技能列表无序请根据语义选择也能缓解顺序偏见。5.2 技能执行超时接口 hang 住怎么办外部接口 hang 是执行层最常遇到的问题有时候并不是超时时间不够而是底层线程池的连接全被占满了。比如某个下游服务突然变慢所有 worker 都在等同一个 HTTP 响应后续技能全部阻塞。针对这个问题我给执行引擎做了熔断机制技能连续失败 3 次后注册表会把它临时标记为不可用后续同一轮对话里不再调用直接返回技能当前不可用给模型。同时超时时间不要拍脑袋设置。一个调用下游 HTTP 接口的技能如果平时 P99 是 2 秒超时设 5 秒就足够如果一个技能内部要跑多步骤流水线可能就需要 60 秒甚至更长。建议每个技能都按自己的实际耗时曲线单独设置 timeout而不是全局统一 30 秒。我见过有人把全局超时设成 5 秒结果查询报表生成技能永远超时后来改为 60 秒一切恢复正常。这个时间不需要很精确但至少要比历史 P99 高出一倍以上。5.3 参数校验的坑Schema 和函数各说各话我第二次重构校验器时遇到一个经典问题技能定义里 parameters 写了city但 handler 函数里实际使用的参数名是city_name。模型按 Schema 传了 city执行时函数收到的是 None又不会立刻报错等到下一段逻辑用到 city 时才炸出来一个玄妙的错误。所以后来我把参数对齐做成了登记时的自动检查注册技能时直接反射 handler 的签名对比 parameters 里的 key 是否一致一旦发现差异就在注册阶段抛出异常。另一个坑是默认值。parameters 里写了default: 1但 handler 的默认参数却写成了2两者不一致。校验器校验通过后把默认值当作显式参数传给 handler反而覆盖了 handler 原本的默认逻辑。这种问题毫无技术含量但特别容易让人排查很久。agent-skills 的做法是校验器负责补默认值handler 不做默认值处理。也就是说handler 签名里所有参数都必须是必填的所有默认逻辑都写在 parameters 的 default 里这样两层之间就不会有分歧。5.4 问题速查表现象可能原因处理动作模型调用了完全不相关技能description 语义不够精确重写描述加入触发场景和反例技能返回参数缺失错误参数 schema 与 handler 不一致检查注册时的自动签名对比执行超时但日志无异常外部依赖 hang 住单独设置较长超时启用熔断模型返回 JSON 解析失败模型输出了自然语言JSON 混合两层解析兜底失败后让模型重格式化参数类型总是不匹配模型把数字序列化成字符串校验层做宽松类型转换同一技能并发重复执行多个 Agent 任务共享了同一注册表使用独立的 ExecutorContext 和独立线程池6. 技能库的下一步复用、共享和持续迭代agent-skills 目前的形态只覆盖了单个项目内部的能力管理但它给了我一个很重要的启示Agent 的能力组织方式应该像软件工程里的模块化一样强调接口、版本和契约而不是一味地把所有功能堆在同一个 prompt 里。我已经开始把一些通用技能抽成独立的技能包比如时间处理技能包、文本摘要技能包、数据格式化技能包这些包内部有稳定接口外部通过注册表统一加载。跨项目复用时只需要把技能目录整个拷过去或者在配置里声明依赖注册表会在启动时自动扫描并加载对应技能。还有一条我强烈建议走的路给技能增加自动评测。每次技能描述改动之后用同一组用户问题跑一遍离线测试集对比技能选择准确率和参数正确率有回归就立刻发现。我第一次做这个测试就抓到了描述改动导致选错率上升 5% 的问题如果没有自动化回归这类退化很难在开发阶段感知到。最后分享一个我个人的心得体会Agent 工具层的核心并不在模型怎么调用那一环而在你如何组织能力边界。一个好的技能库能让模型像插件一样自由组合各种能力也能让你在新增功能时不动主流程代码。从最初的一堆 function 到现在的 agent-skills我最大的感受是把能力当产品来设计Agent 的表现才会有质的提升。下一次遇到调用混乱的问题试试先审视一下技能描述和注册表往往比调模型参数更管用。
RELATED

相关推荐

LiteLLM:大模型调用层的标准化中间件

LiteLLM:大模型调用层的标准化中间件

1. 为什么“litellm”突然在开发者圈里高频出现——它不是新模型,而是新范式 最近两周,好几个不同技术背景的朋友都在 Slack 群、内部分享会甚至咖啡闲聊中提到 “litellm” 这个词。有人问:“这玩意儿是不是又一个大模型?” 有人…

📅 2026/10/21 18:27:11
Fluent多相流模型选型与设定避坑指南:VOF、Mixture、Eulerian实战

Fluent多相流模型选型与设定避坑指南:VOF、Mixture、Eulerian实战

简介:这份PDF资料面向CFD仿真初学者与工程技术人员,系统梳理Fluent中多相流模型的选择逻辑与设定方法,帮助解决气-液、气-固、液-固等流动场景下模型选型困难的问题。内容从多相流分类入手,覆盖气泡流、液滴流、活塞流、分层自由面…

📅 2026/10/21 18:22:04
自动驾驶全栈技术解析:算法、通信与服务器三层架构

自动驾驶全栈技术解析:算法、通信与服务器三层架构

第一次接触自动驾驶的时候,我犯过一个很典型的错误:一头扎进感知算法,翻来覆去调检测模型参数,觉得只要让车“看清世界”就够了。结果在仿真器里跑得顺风顺水的路径规划,一旦接上真实那套通信基设和计算平台&#xff0…

📅 2026/10/21 18:22:04
MORE NEWS

更多资讯

📰

视频配乐新范式:语义、时间、节奏三重对齐的AI生成系统

1. 项目背景与核心思路拆解1.1 这个项目到底解决了什么问题视频配乐生成,听起来好像不是什么新鲜事。很多剪辑软件里都有“一键配乐”功能,你拖一段视频进去,它给你生成一段氛围音乐,看起来好像也能用。但真正用过的人应该都有体会…

📰

Windows 安装 Redis 全攻略:原生 exe 与容器化部署详解

如果你在 Windows 上装过 Redis,大概率经历过这一幕:翻遍官方下载页只看到 Linux 和 macOS 的入口,Windows 用户被假装不存在。于是很多人转头去搜索引擎找“Windows Redis 安装包”,结果下载到一堆七八年前的版本,装完…

📰

毕业设计选题系统怎么从零搭建?Java+SSM与Django双版本实战解析

每年到了选题季,最焦头烂额的人往往不是学生,而是负责收选题表格的教务老师和辅导员。几十个老师、几百个学生,靠Excel来回传递题目清单和志愿表格,光做重名检测就能耗掉一个礼拜。更麻烦的是,学生改了一次志愿&#x…

📰

对称零和博弈中的开放式学习:从自我博弈循环到种群多样性

训练对抗性智能体的同学,大概率都撞上过这样一堵墙:最新一版策略在内部对练里胜率已经压倒性领先,可一旦丢给一个没见过的新打法,立刻被打回原形。更迷惑的是,如果让智能体只跟自己的当前版本对打,它常常不…

📰

视频配乐生成难在哪?语义、时间与节奏三对齐全解析

视频配乐生成听起来像是个老问题——往画面上铺一首歌就行了,但真做过的人都知道,这件事的难度一点不比"看图写话"低。视频是流动的,有镜头、有动作、有情绪起伏,配乐不能只考虑"风格对不对",还得…

📰

迷你C编译器手记:结构体内存对齐的规则与调试实录

写C的时候,你有没有被 sizeof 的结果惊到过?明明结构体里每个成员的大小加起来不过几个字节,算出来却莫名其妙多了几个甚至十几个字节。以前当学生时只知道“内存对齐”四个字能解释一切,但真正自己动手实现一个C语言编译器&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬