资讯详情

资讯详情

Agent技能库设计:从散装函数到可维护能力体系

第一次看到agent-skills这个项目标题时我脑子里立刻浮现出上一个Agent项目里那串越写越长的工具函数列表。初始阶段还能靠几个 if 判断撑过去等技能数量超过二十个连我自己都分不清哪个函数对应哪个意图更别提让模型在回答问题时精准选中它们。agent-skills解决的就是这个问题把Agent具备的各种能力从“散装的函数”整理成“可注册、可描述、可被发现、可独立维护”的技能库让大模型像翻阅菜单一样选择技能让开发者像维护插件一样迭代能力。这个项目适合两类人一类是把Agent从原型推向真实业务的开发者另一类是刚接触大模型应用、想让模型真正“动手做事情”而不是只会生成文本的学习者。它能解决的问题很具体技能如何组织、描述怎么写模型才看得懂、参数怎么约束才不会乱传、运行时如何隔离才不会被一次异常拖垮整个Agent。下面我会按照项目自身的拆解逻辑从结构设计、描述规范、加载器实现、运行时机制到踩坑记录把这些内容完整梳理一遍附带可以直接参考的代码实践。1. 项目整体定位把Agent从“会聊天”变成“会干活”1.1 为什么“技能”比“工具列表”更接近真实需求传统Agent实现里我们通常把能力做成一堆函数然后通过function calling机制让模型选择执行。这个模式在小规模场景下没有问题一旦技能数量上来问题就会集中爆发。第一是命名混乱。函数名往往带着代码命名规范比如create_calendar_event_with_reminder_v2模型看到这种名字只能靠猜。第二是描述缺失。很多开发者把函数名直接当作描述注入系统提示模型根本不知道这个函数什么时候该用、参数应该怎么填。第三是职责不清。有的函数只有一行逻辑有的函数内部又调了三个外部服务彼此之间的边界完全取决于写代码时的心情。agent-skills的做法是把每一个能力都当成一个“可插拔的技能”来对待。每个技能有自己独立的目录、说明文档、参数定义和实现脚本Agent通过统一的装载器发现这些技能再把技能清单注入到模型的上下文中。这样能力之间不再互相纠缠新增一个技能就像新增一个文件夹删除一个技能也只影响它自己。从业务视角看这种组织形式也更接近团队协作。我们不会让一个工程师同时维护所有业务逻辑而是按模块拆给不同人技能库本质上就是代码模块化在大模型场景下的延伸。你甚至可以想象一个团队里有人专门写技能描述、有人专门写执行逻辑大家通过同一套规范协作而不是在同一个函数文件里互相改代码。1.2 技能库设计的三条边界名字、描述、参数在动手写代码之前要先想清楚一个技能由哪几部分组成。我在项目中把边界收敛为三条技能名、技能描述、参数定义。技能名是模型看到的第一标识它需要足够短、足够直观。web_search就是比search_internet_and_return_top_results好因为模型做阅读理解时对长名称反而容易混淆。技能描述是模型判断“要不要用这个技能”的依据它必须包含触发条件、使用场景、典型输入示例尤其是要让模型知道“什么情况下不要用”。参数定义是模型生成结构化调用时的约束类型、必填项、枚举值都要写清楚越严格越好。这三条边界决定了技能库是否好用。描述写得模糊模型就会在相似技能之间反复横跳参数定义不严格模型就会把number类型传成字符串运行时再做一堆兼容逻辑。项目里有个很典型的教训一开始某个技能接受“任意字符串”结果模型时不时传一句完整的话进来最后不得不针对输入做格式清洗。与其在运行时补救不如在定义阶段就堵住漏洞。1.3 技能库与提示词工程的关系很多人在做技能库时会忽略一个重要事实技能清单最终是通过提示词工程呈现给模型的。系统的提示词实际上分成了两层一层是固定的角色设定和全局规则另一层是动态注入的技能清单。技能库负责生成后者但生成方式会影响模型的理解效果。在我这个项目里动态技能清单不是简单地把所有技能名拼成一段文字而是为每个技能生成一段“策略卡片”技能名、一句功能概述、触发条件、参数摘要。整体格式像菜单也像API文档的精简版。模型看到这份清单之后结合用户的实际问题决定要不要调用以及调用哪个。这里有一个值得反复调试的点技能清单不能太长。虽然大模型的上下文窗口越来越大但把五十个技能完整拼进提示词既浪费token也会稀释注意力。项目里有两种做法可以缓解这个问题一种是只注入“热门技能”另一种是先让模型做一个粗糙的意图分类再根据分类注入候选技能。后面章节会展开讲。2. 技能库的目录结构与描述规范2.1 每个技能应该长什么样技能库首先是一套文件结构。我采用的布局很直接一个顶级目录放置所有技能每个技能拥有独立子目录如下所示agent-skills/ ├── skills/ │ ├── calendar_create_event/ │ │ ├── SKILL.md │ │ ├── schema.json │ │ └── run.py │ ├── web_search/ │ │ ├── SKILL.md │ │ ├── schema.json │ │ └── run.py │ ├── code_runner/ │ │ ├── SKILL.md │ │ ├── schema.json │ │ └── run.py │ └── send_email/ │ ├── SKILL.md │ ├── schema.json │ └── run.py ├── loader.py ├── registry.json └── requirements.txtSKILL.md是人类和模型共同的阅读入口用Markdown写清楚技能的目标、触发条件、输入输出、注意事项。schema.json是给模型看的结构化约束描述参数的类型、默认值、枚举范围。run.py是真正的执行逻辑接收标准化输入返回标准化输出。目录名全局唯一同时作为技能名的来源。这个结构与浏览器插件有异曲同工之处。插件有一个manifest描述自身能力有一份脚本负责执行逻辑技能库里的SKILL.md和schema.json就相当于 manifestrun.py就是后台脚本。抽象层级清晰之后任何人都能照模板新加一个技能而不用理解Agent的全部代码。2.2 描述与入参的写法给模型一份“选择说明书”描述写得好的标准只有一个模型能在两句话之内判断这个技能是否匹配当前任务。我总结下来一个合格的技能描述必须包含以下要素。以web_search为例我不会只写“搜索引擎工具”而是会写成一段完整说明# web_search 在需要获取最新信息、事实核查、查找特定网站内容时使用。 典型输入包括新闻事件、人物背景、产品资料、技术文档检索。 若用户只是表达观点或情绪不需要实时数据则不要使用本技能。 参数 - query: 字符串必填检索关键词建议不超过50字 - max_results: 整数选填默认5取值范围1-10这段描述里最关键的部分其实是“什么时候不要用”。模型在训练过程中见过太多工具调用的案例它默认倾向于调用技能来证明自己“能干”如果不明确排除场景它会把任何问题都变成一次搜索请求。项目里有一次用户问“我明天要交报告了好紧张”模型居然想调用web_search就是因为描述里没有说明免责场景。加上这句话之后误用率明显下降。入参设计方面我的原则是“宁严勿松”。所有参数必须有类型约束能限定枚举就限定枚举能用字符串模板解析就不要开放自由文本。比如calendar_create_event的参数里时间字段直接要求ISO 8601格式而不是让模型传一个“明天下午三点”这种自然语言否则执行层还需要额外接一个时间解析器复杂度完全不可控。2.3 注册与装载机制让新技能自动被发现技能库不能靠手工维护一份巨大列表。项目里有一个registry.json做缓存但真正的数据源是文件系统。装载器的职责非常单一遍历skills/目录读取每个子目录下的SKILL.md和schema.json组装成统一的技能元数据对象再返回给上层使用。import json from pathlib import Path def load_all_skills(skills_dir: str skills) - dict: skills {} base Path(skills_dir) for skill_dir in base.iterdir(): if not skill_dir.is_dir(): continue skill_id skill_dir.name skill_md skill_dir / SKILL.md schema_file skill_dir / schema.json if not skill_md.exists() or not schema_file.exists(): continue with open(skill_md, r, encodingutf-8) as f: description f.read().strip() with open(schema_file, r, encodingutf-8) as f: param_schema json.load(f) skills[skill_id] { description: description, parameters: param_schema, runner: str(skill_dir / run.py), } return skills这个过程的重点是容错。任何一个技能目录缺失必要文件装载器不能让整个系统崩溃应当跳过该技能并输出警告。我在项目里额外做了一层校验如果两个技能目录名过于接近比如web_search和web_searcher装载器会给出提示。这类命名混淆问题在运行时很难排查早发现早处理。3. 核心环节实现Agent如何发现并执行技能3.1 动态技能清单注入系统提示实现了技能库之后下一步就是让Agent看到技能。这一步不在项目标题里直接体现但是决定技能库能否被模型真正使用。我采用的方案是在每次用户消息进来之后先获取用户输入再筛选候选技能最后拼装成系统提示的一部分。简单版本可以直接全量注入适合技能数量少于十个的场景。超过十个之后我推荐做一轮预筛利用embedding相似度或基于规则的关键词匹配筛出与当前问题最相关的三到五个技能。以web_search和send_email为例用户问“帮我把这份合同发给对方”关键词“合同”“发给”能直接命中send_email而web_search会被过滤掉这样既能节省token也能减少模型的选择负担。需要注意一个细节动态注入时不要改变系统提示的结构。固定部分在最前面动态技能清单放在其后最后再说一次通用规则比如“优先利用技能完成任务如果技能不能完全解决再结合自身知识作答”。模型对提示中间部分的内容敏感结构频繁变化会导致行为不稳定。3.2 模型调用与技能执行的循环系统运行循环可以概括为接收用户输入、注入技能清单、模型输出调用意图、执行技能、把结果反馈给模型、生成最终回复。伪代码如下def agent_run(user_input: str): skills load_all_skills() candidates filter_skills(skills, user_input) system_prompt build_system_prompt(candidates) messages [{role: system, content: system_prompt}] messages.append({role: user, content: user_input}) # 第一轮模型决定是否调用技能 response llm.chat(messages, toolsbuild_tool_schemas(candidates)) if response.tool_calls: for call in response.tool_calls: result execute_skill(call.name, call.arguments) messages.append({ role: tool, tool_call_id: call.id, content: result, }) messages [{role: system, content: system_prompt}] messages[2:] response llm.chat(messages) return response.content这个循环的要点在于工具结果回传之后的处理。如果不做额外处理直接把工具结果追加到对话末尾模型可能不知道这个结果来自哪里、该项结果是否可信。所以我在execute_skill返回的结果里做了标准化包装包含status、summary、data三个字段。summary是一段简短的中文摘要模型直接基于摘要生成回复data是完整数据只有在必要时才拼进上下文。实际项目里还有一个经常被忽略的点技能执行结果可能非常长比如web_search返回十篇网页摘要如果全部塞回上下文不仅token消耗高还会让模型忘记前面的用户意图。我的做法是让执行脚本在返回前先做一次内容压缩只保留标题、URL和两句话摘要。这个策略让后续生成回复的质量提升非常明显。3.3 技能运行时的隔离与超时控制技能执行不能直接在Agent主进程里裸奔。原因很简单任何一个技能里的第三方SDK崩溃、内存泄漏、死循环都会把整个服务拖垮。项目里我采用了子进程加超时控制的运行方式。import subprocess import json import sys def execute_skill(skill_name: str, args: dict, timeout: int 30) - dict: script_path fskills/{skill_name}/run.py try: proc subprocess.run( [sys.executable, script_path], inputjson.dumps(args), capture_outputTrue, textTrue, timeouttimeout, checkFalse, ) except subprocess.TimeoutExpired: return {status: error, summary: 技能执行超时, data: None} if proc.returncode ! 0: return { status: error, summary: f技能执行失败: {proc.stderr[:200]}, data: None, } try: output json.loads(proc.stdout) except json.JSONDecodeError: return {status: error, summary: 技能输出无法解析, data: None} return output子进程的输入输出都走标准流并强制JSON格式相当于给每个技能加了一层协议边界。timeout参数必须根据技能类型设置web_search依赖外部网络三十秒比较合理code_runner执行用户提交的代码必须限制在五秒以内否则恶意代码会消耗大量资源。更进一步的隔离还需要考虑资源限制容器或复杂沙箱系统可以做到CPU、内存的实时限制但项目初期用子进程加超时已经能拦住大部分问题。至少保证一个技能卡住时Agent主服务还能响应其他请求。4. 测试与踩坑实录Agent技能开发中最常见的五个坑4.1 模型完全不调用技能怎么办这是我被问得最多的问题。模型对技能清单视而不见根本原因通常是两个技能描述与用户问题之间的匹配信号太弱或者系统提示里出现了互相矛盾的指令。排查时先做最小化实验只注入一个技能再用一条明显匹配的测试文本去触发它。如果最小场景下模型都不调用那就是描述本身的问题。小技巧是把触发场景写成“if...then...”的形式。描述里明确说“如果用户请求中包含‘搜索’‘查一下’‘最新消息’应该使用 web_search 技能”这比单纯罗列功能更有效。还有一个常见原因系统提示里出现了类似“请直接回答问题不要调用工具”的字样哪怕其他位置写着“优先使用技能”模型也会被这条指令带偏。检查提示词里不要有互相打架的指令。4.2 模型在相似技能之间选错技能多起来之后分布重叠不可避免。send_email和send_slack_message都是“给某人发消息”模型很容易混淆。解决这个问题有两个方向一个是在描述里明确差异比如send_email强调异步、正式、可附附件send_slack_message强调即时、简短、适合群组另一个是直接减少技能数量合并同质能力。我在项目里还加了一个“否定清单”机制在每个技能描述末尾统一补充一行如果用户明确提到某种渠道则不要使用本技能。这听起来有点反直觉但实际效果很好模型在对比两个技能时会优先看排除条件。4.3 参数格式问题string类型的调用陷阱模型生成结构化参数时经常会把数字、日期、布尔值全部转成字符串这与训练数据中函数调用格式不统一有关。如果schema.json里定义了strict: true模型厂商的API可能会自动强转如果没有执行层就必须自己兜底。我的兜底策略是在技能脚本入口写一个normalize_args函数把所有字符串类型的数字和布尔值转回正确类型同时检查必填字段是否缺失。不推荐在业务逻辑里到处做类型判断那样不仅代码丑还容易漏掉边界情况。把参数校验集中放在执行脚本最前面报错信息统一返回给上层是最省心的做法。4.4 安全隐患技能变成了攻击入口技能一旦可以执行任意代码、发送邮件、写入文件系统它就不仅是Agent的延伸也成了攻击面。最常见的问题是用户输入被直接拼进技能参数比如code_runner让用户提交代码如果没做任何限制就可以执行任意系统命令。项目里我采取了最小权限策略。每个技能运行在独立子进程中进程的工作目录指向一个临时目录不允许访问Agent主目录技能脚本禁止接收任意shell命令字符串只接收结构化的指令对于code_runner这类高风险技能直接放弃完整执行环境只输出“该功能在演示模式下不可用”。虽然这会牺牲一部分能力但安全底线上不能妥协。4.5 常见问题速查表问题现象可能原因解决方案模型从不调用技能技能描述缺乏触发场景重写描述加入明确条件技能频繁误触发描述中没有排除场景增加否定清单两个技能被轮流调用功能重叠、差异表述不清合并技能或写明差异参数总是传字符串模型API对类型约束理解不足在执行层增加类型归一化技能执行后回复质量变差结果未压缩、上下文混乱返回summary加data结构一个技能崩溃影响整个服务缺少运行时隔离使用子进程加超时控制技能清单太长、token消耗高全量注入导致根据输入预筛候选技能这张表是我在实际项目中逐步整理出来的每一条都对应真实踩坑经历。把它们提前写在设计文档里可以省掉后面非常多的调试时间。5. 从Demo到可维护技能库的持续迭代思路5.1 技能版本与灰度技能是代码就会变化。某个技能的参数格式升级之后老版本的调用记录和日志分析就会失真。我建议在目录结构里加入版本概念比如web_search_v1、web_search_v2虽然会让技能名变长但能换来清晰的迁移路径。更轻量的做法是在schema.json里加一个version字段执行结果也带上版本号。这样如果模型选了新版本而技能脚本还没完全就绪调度层可以回退到旧版本。灰度发布时可以只让部分用户看到新版本技能描述观察调用成功率和用户反馈后再全量放开。5.2 测量每个技能的实际价值技能库不能只增不减。我在维护过程中发现有些技能看起来功能完整实际调用率极低还会干扰其他技能的选择。因此我建立了一份简单的统计表记录每个技能每日调用次数、成功率、平均耗时、导致对话中断的次数。技能名调用次数成功率平均耗时备注web_search34294%1.8s正常send_email18100%3.5s低频但价值高code_runner740%8.2s成功率低考虑下线这张表是项目迭代的决策依据。调用次数高不等于必须要保留还要看它是否创造了真正的业务价值调用次数低的技能如果风险高、耗时高就该想办法合并或下线。技能库的维护不是写完之后就不管了而是一个持续精简的过程。5.3 个人体会与建议在整个项目中我最大的感受是技能库的设计质量决定了Agent能力的上限。模型本身的能力再强如果技能描述混乱、参数失控它也无法发挥出真实水平。反过来说一个组织良好的技能库即使模型版本没有升级整体表现也会有明显提升因为模型能更准确地找到并调用合适的能力。这里还有一个小技巧想分享给正在做同类项目的人写技能描述时不要只站在开发者的角度试着把自己想象成一个刚入职的实习生只凭那份SKILL.md去判断该不该调用这个技能。如果连人都能看懂、能正确使用模型大概率也能。如果描述里全是代码术语和隐式的背景知识那模型的表现必然不稳定。用最简单的话把事情说清楚是技能库建设中最重要也最容易被忽略的一件事。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →