我最早接触Agent开发的时候和大多数人一样把精力全放在了怎么写好prompt上。直到项目越做越深才发现真正的瓶颈根本不是提示词而是怎么让Agent稳定、安全、可复用地去调用外部能力。后来在实践里慢慢沉淀下一个叫agent-skills的技能库模式今天就把这套思路完整拆开聊一聊。简单说agent-skills是一个围绕LLM Agent构建的“技能管理层”它把Agent所有能做的事——查天气、查数据库、调接口、操作文件、执行脚本——都封装成一个个标准的、可注册、可发现、可编排的技能单元。你不需要把每个能力写死在业务代码里而是通过一套统一的协议让Agent自己发现和调用。它解决的最大问题是让Agent从“聊天机器人”变成“能干活的操作系统”。这篇文章适合正在做Agent落地的人不管你是想给LLM接工具还是想把一团乱麻的工具函数整理成可维护的框架都可以参考我这里的设计思路和踩坑记录。1. Agent开发最大的坑把能力写死在Prompt里先说一下我为什么会走到技能库这条路。早期给客户做Agent最常见的方式就是拿一个大模型把所有功能描述堆进System Prompt里。开头一版还挺顺隔几天需求一加prompt越来越长模型开始“精神分裂”今天能用工具明天就忘了后天又自己编了个不存在的参数。这块真实的痛处在于你并没有真正把能力交给Agent而是交给了概率。1.1 为什么需要标准化的技能层把工具能力直接写死在Prompt里本质上是在用自然语言承载代码逻辑。但自然语言天生有歧义、有遗漏、有不稳定而代码世界讲究的是确定性和可验证性。一个标准的技能层就是在这中间架一座桥工具该做什么严格由代码定义Agent在什么时机调、怎么调才由模型决策。两端各取所长。技能层还能解决另外一个很实际的问题——复用。我手里几个项目都是重复造轮子每个项目都写一套“调最新消息”的代码参数还不一样负责人也换了几个。后来我把所有基础能力统一收口到agent-skills里新的Agent项目只需要声明需要哪些技能包几分钟就能把底层能力接上。这个体验说实话像极了从“每个项目自己拧螺丝”到“用统一工具箱”的转变。1.2 agent-skills要解决的核心问题这个项目名字之所以叫agent-skills核心就是围绕“技能”这一层做文章。它要处理的不是某一个工具的细节而是一类共性问题。技能注册如何把一个普通函数变成Agent能理解的“技能”并且补充描述、参数、示例、权限等元信息。技能发现Agent面对一堆技能的时候怎么快速判断哪些技能适合当前场景。技能编排多个技能如何组合成更复杂的任务比如“查天气”和“提醒我出门带伞”是两个技能但可以编成一个天气助理流程。技能治理技能由谁维护、怎么上线、怎么下线、调用失败了怎么处理这些都必须有规则。这四个问题揉在一起就是一个完整的技能生命周期管理。缺了任何一环项目规模一大准出幺蛾子。实际做下来我的体感是技能注册是最容易的治理是最容易被忽视但后期最痛的。尤其是多人协作的项目里如果没定好规范技能库最后会变成垃圾堆各种同名技能、参数风格不一致、无主代码全堆在里面谁都不敢动。2. 技能库的整体设计从注册到调用的完整链路在设计agent-skills的时候我没有一上来就写代码而是先画了一条完整的链路Agent接收用户指令 → 理解意图 → 从技能库匹配可用技能 → 获取技能Schema → 生成调用参数 → 执行技能函数 → 把结果返回给模型 → 模型组织最终回答。每一步之间都是一个清晰的模块模块之间通过标准接口通信。2.1 技能的本质对LLM能力的确定性补充这里先明确一个概念技能到底是什么我把它定义为“LLM能力之外的确定性补充”。LLM擅长的是语言理解、推理、生成但它不擅长计算精确值、读取实时数据、操作外部系统。技能就是把这些“LLM不擅长但程序很擅长”的事情封装起来供LLM按需调用。所以设计技能时有一个黄金法则技能必须是确定性的。同样的入参必须有同样的出参。如果技能内部依赖了随机数、外部瞬时状态要么消除这些因素要么在描述里明确标注“结果可能存在波动”。这个原则直接决定了Agent最终的稳定程度千万别在这一点上将就。2.2 核心技术拆解技能Schema与运行时技能Schema是整个框架的心脏。它告诉模型“这个工具是什么、需要什么参数、参数长什么样子”。早期我仿照OpenAI Function Calling的格式设计后来发现不够用就逐渐扩展出了自己的一套结构。一个完整的技能Schema大概长这样{ name: fetch_weather, description: 根据城市名获取实时天气信息支持摄氏度和华氏度, version: 1.0.0, tags: [utility, weather], parameters: { type: object, properties: { city: {type: string, description: 城市名如北京、上海}, unit: {type: string, enum: [celsius, fahrenheit], default: celsius} }, required: [city] }, returns: { type: object, properties: { temperature: {type: number}, condition: {type: string} } }, permission: user-read, rate_limit: 10 }你不用完全照搬这个结构但有几个字段值得重视。version字段极易被忽略实际上它是排查线上事故的第一抓手permission是安全底线以后细聊rate_limit是保护后端系统不被Agent循环调用打爆的关键。运行时容器则负责三件事加载已注册的技能、校验入参、执行技能函数。校验这一步不能省因为LLM生成参数时偶尔会漏传必填项、传错类型甚至传一个完全不在schema里的字段。在运行时做一层严格的schema校验能避免大量脏数据打到你的后端服务上。我这里用的是轻量级的jsonschema库几百行代码就能封装得比较完善。2.3 为什么选JSON Schema而不是自然语言描述可能有人会问既然大模型能理解自然语言直接用一段自然语言描述工具不就行了我的答案是对于单技能可以对于技能库不行。自然语言描述有两个硬伤。第一是无法严格校验模型理解错了你也不知道第二是解析成本高你想在代码里判断“用户有没有权限调这个技能”还得去读自然语言没法自动化。JSON Schema是结构化数据是现成的行业标准校验库一堆生态成熟而且能被模型精确理解。它等于一套“机器可读的技能说明书”Agent和框架都能用。踩过坑之后我的体会是Schema设计是技能库最容易一改全改的部分。所以一开始就尽量把公共字段抽出来做复用别让每个技能各写各的否则后面统一加字段时你会哭着改所有技能定义。3. 从0到1搭建自己的Agent技能库理论说完了下面进入实操。这一节我带你从零开始搭一个可用的技能库框架。选型上我用Python主要因为它生态成熟、写起来快而且对接大模型SDK很方便。如果你用TypeScript/Node.js思路完全一致把装饰器和注册表换成对应语法即可。3.1 环境准备与项目结构先准备好基础环境其实不需要太多依赖核心只需要一个有openai接口兼容的大模型SDK外加一个jsonschema库做入参校验。pip install openai jsonschema然后建一个清晰的项目目录。我的习惯是把技能定义、运行时、调度逻辑分开这样技能越来越多时不至于变成一座屎山。agent-skills-project/ ├── app.py # 主入口Agent调度逻辑 ├── registry.py # 技能注册表核心 ├── runtime.py # 技能执行运行时 ├── skills/ # 放具体技能的目录 │ ├── __init__.py │ ├── weather.py # 示例技能一 │ ├── database.py # 示例技能二 │ └── file_ops.py # 示例技能三 └── schemas/ # 独立维护的JSON Schema这个目录结构越早定下来越好。我们之前有个项目开始没规划技能目录所有函数都堆在一个大文件里后来改成按技能域拆分时光梳理依赖就花了两天。3.2 技能注册的代码实现技能注册这块我用装饰器来做好处是改动成本低——你只需要在普通函数上加一行skill它就变成了Agent可识别的技能。先来实现最基础的注册表# registry.py from dataclasses import dataclass, field from typing import Callable, Dict, Any, Optional dataclass class SkillDef: name: str description: str parameters: dict handler: Callable version: str 1.0.0 tags: list field(default_factorylist) permission: str user rate_limit: int 10 enabled: bool True _SKILL_REGISTRY: Dict[str, SkillDef] {} def skill(schema: dict, name: Optional[str] None, version: str 1.0.0, tags: Optional[list] None, permission: str user, rate_limit: int 10): 技能注册装饰器。 schema 必须包含 description 和 parameters 两个字段。 def decorator(func: Callable) - Callable: skill_name name or func.__name__ _SKILL_REGISTRY[skill_name] SkillDef( nameskill_name, descriptionschema.get(description, func.__doc__ or ), parametersschema.get(parameters, {}), handlerfunc, versionversion, tagstags or [], permissionpermission, rate_limitrate_limit, ) return func return decorator def list_skills() - list: 返回所有启用的技能定义不含handler避免暴露实现细节 return [ { name: s.name, description: s.description, parameters: s.parameters, version: s.version, tags: s.tags, permission: s.permission, } for s in _SKILL_REGISTRY.values() if s.enabled ] def get_skill(name: str) - Optional[SkillDef]: return _SKILL_REGISTRY.get(name)这样注册表就建好了。以后每新增一个技能就是在对应模块里写一个函数配上schema装饰器不用改任何注册逻辑。爽点在于你写技能的时候完全感觉不到框架的存在框架只是帮你做好了登记和分发。再写一个具体的技能示例这里以天气查询为例# skills/weather.py from registry import skill import requests skill( schema{ description: 根据城市名获取实时天气信息支持摄氏度和华氏度适合回答关于天气、温度、出行建议的问题, parameters: { type: object, properties: { city: {type: string, description: 城市名如北京、上海}, unit: {type: string, enum: [celsius, fahrenheit], default: celsius} }, required: [city] } }, version1.0.0, tags[utility, weather], permissionuser, rate_limit20 ) def fetch_weather(city: str, unit: str celsius) - dict: 真实项目里换成你的天气API这里演示标准请求 resp requests.get( https://api.example.com/weather, params{city: city, unit: unit}, timeout8 ) resp.raise_for_status() data resp.json() return { city: data[city], temperature: data[temp], condition: data[condition], humidity: data[humidity] }注意一个细节description字段不要只写“查天气”要写清楚它适合回答什么问题、有没有约束条件。我自己测试下来描述写得越贴近业务场景模型选对技能的概率越高。同样的天气查询技能描述写成“获取天气数据”和“根据城市名获取实时天气信息支持摄氏度和华氏度适合回答关于天气、温度、出行建议的问题”后者的命中率明显高一大截。3.3 让Agent学会发现与调用技能技能注册好了下一步就是把技能列表同步给Agent并实现调用逻辑。这一环节的核心代码其实就是把list_skills()的结果注入system prompt然后解析模型返回的tool_calls。# runtime.py import json import jsonschema from registry import get_skill, list_skills def _build_skill_prompt() - str: 把技能定义转成系统提示词这里可以按你需要的方式拼接 skills list_skills() lines [你是一个智能助手可以调用以下技能来帮助用户完成任务, ] for s in skills: lines.append(f技能名称{s[name]}) lines.append(f用途{s[description]}) lines.append(f参数定义{json.dumps(s[parameters], ensure_asciiFalse)}) lines.append(---) return \n.join(lines) def _validate_args(skill_name: str, args: dict) - dict: 按schema校验并修正参数 skill get_skill(skill_name) if skill is None: raise ValueError(f技能 {skill_name} 不存在) # 这里用jsonschema做严格校验 jsonschema.validate(args, skill.parameters) # 可按需补充默认值等 return args def execute_skill(skill_name: str, args: dict) - dict: 执行技能统一入口方便加日志、鉴权、限流 skill get_skill(skill_name) if skill is None: return {error: f未知技能: {skill_name}} # 权限与限流在这里检查 if not skill.enabled: return {error: 技能已被禁用} try: # 参数校验 validated _validate_args(skill_name, args) # 真正执行技能函数 result skill.handler(**validated) return {ok: True, data: result} except jsonschema.ValidationError as e: return {error: f参数校验失败: {e.message}} except Exception as e: return {error: f技能执行异常: {str(e)}}Agent主调度循环里我习惯用OpenAI兼容的function calling格式直接传skills列表模型会自己决定调哪个技能。核心逻辑长这样# app.py import json from openai import OpenAI from registry import list_skills from runtime import execute_skill client OpenAI() def run_agent(user_input: str) - str: messages [{role: user, content: user_input}] # 把技能列表转成 OpenAI Function Calling 支持的格式 tools [] for skill in list_skills(): tools.append({ type: function, function: { name: skill[name], description: skill[description], parameters: skill[parameters] } }) for _ in range(5): # 限制最大调用轮数防止死循环 resp client.chat.completions.create( modelgpt-4o-mini, # 换成你实际使用的模型 messagesmessages, toolstools or None, tool_choiceauto ) msg resp.choices[0].message # 如果模型没有调用技能直接返回最终回答 if not msg.tool_calls: return msg.content or # 追加 assistant 消息 messages.append(msg) # 逐个处理模型请求调用的技能 for tc in msg.tool_calls: func_name tc.function.name args json.loads(tc.function.arguments or {}) print(f[Agent] 调用技能: {func_name}, 参数: {args}) # 日志 result execute_skill(func_name, args) # 把技能执行结果回传给模型 messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse) }) return 执行轮数超限请简化任务或检查技能逻辑 if __name__ __main__: print(run_agent(上海今天需要带伞吗))这段代码已经是一个最小可用的Agent技能框架了。模型看到用户问“上海今天需要带伞吗”自行决定调用fetch_weather(city上海)拿到天气结果后判断是否建议带伞组织成最终回答。整个过程不需要在prompt里写死任何业务规则模型在运行时自行决策。3.4 技能热更新与动态装载项目上线后你一定会遇到一个需求技能要能快速上线和下线不能每次加个技能就要重启服务。热更新这块我一开始没做后来被运营催着加了。设计上我采用目录扫描加文件监听的模式# loader.py import importlib import inspect import pkgutil import skills def load_skills_from_package(packageskills): 扫描技能包动态导入所有模块触发装饰器完成注册 for module_info in pkgutil.iter_modules(package.__path__): importlib.import_module(f{package.__name__}.{module_info.name}) # 启动时调用一次 load_skills_from_package()热更新最简单的方式是用watchdog监听技能目录文件变更后重新加载对应模块。注意importlib.reload只对模块级代码生效如果技能内部依赖了长连接对象reload后要记得处理资源释放。这块踩过的坑我已经记录在第5节到时候展开讲。4. 技能库要落地的安全与治理细节技能库能跑起来不难但要长期稳定运行安全和治理必须跟上。这里说的安全不只是防外部攻击更多是防内部错误和滥用。Agent执行技能的时候一旦权限控制不到位后果比人操作严重得多——因为模型可能在毫秒内连续调用数百次API。4.1 权限控制技能层必须做的事我给每个技能都配置了permission字段取值为user、admin、system三级。在execute_skill的入口处做一次检查只有当前会话角色满足权限要求才能执行。实际中还有一个更细的要求技能内部要区分“读”和“写”操作。比如查数据库是读改配置是写这两类的权限级别完全不同。一个容易踩的坑是模型可能通过组合技能实现越权。比如“删除用户”这个技能不开放给普通用户但“更新用户信息”开放了模型有可能用“更新用户信息”传入空参数来变相清空数据。我在实际项目中遇到过这种case后来在每个技能入口都加了一层危险操作确认——如果技能被标记为高风险执行前必须让用户显式确认一次。虽然多了一步交互但对B端系统来说是值得的。4.2 可观测性技能调用日志与审计技能层必须要做全量日志。因为Agent的行为是模型生成的你没法提前预测它会调用哪些技能、以什么顺序调用。我在日志里记录的信息包括调用时间、技能名、入参、出参、耗时、错误信息、会话ID。这样出了问题能快速回放Agent的完整决策链路。完整的调用日志结构参考{ timestamp: 2025-02-14T10:22:31.123Z, session_id: sess_abc123, skill_name: fetch_weather, args: {city: 上海}, result: {ok: true, data: {city: 上海, temperature: 18}}, duration_ms: 234, error: null }日志这块我建议直接结构化输出不要用纯文本拼接——将来你要做统计、告警、训练数据分析结构化日志能省你大量时间。4.3 灰度发布与A/B测试技能库能不能滚动发布也很重要。我这边用的笨办法是给技能加version字段线上同时保留多版本。新版本技能先给5%的流量对比一下调用成功率、延迟、用户满意度确认没问题再加量。基于version做路由不复杂在execute_skill里加一个version参数就行。def execute_skill(skill_name: str, args: dict, version: str None) - dict: skill get_skill(skill_name, versionversion) # 支持按版本获取 ...这里有个细节同一个技能不同版本的参数Schema可能不一样灰度期间可能出现新旧版本参数不兼容的报错。解决方案是灰度阶段把所有入参跑一遍旧版本校验器确保新版本能兼容旧参数格式等比例到100%后再切换校验器。5. 实战踩坑记录与排障速查表最后分享几个我在这套框架落地过程中真实遇到的坑。有些问题看起来很小但排查起来相当费神写出来给各位参考省得再走一遍弯路。5.1 高频问题实录第一个坑模型生成参数时经常带多余字段。一开始我在执行阶段用jsonschema严格校验结果发现GPT-4o有时会在参数里多出schema里没有的字段比如传入一个null值。一开始我直接报错后来改成“校验过滤”模式校验required字段必须存在、类型必须正确多余字段直接忽略再加warning日志。这样既保障了安全又不会因为小问题打断整个流程。第二个坑技能循环调用导致超时。有一次线上Agent陷入循环不停调用“查询订单状态”这个技能几秒内打了后端几十次请求。排查发现是技能返回结果里有一句话让模型误判“订单还没处理好需要继续查”。解决方法是给每个技能加上限流逻辑同时在Agent主循环里限制最大调用轮数上面代码里已经写了5次。两个措施加上之后再也没出过类似问题。第三个坑热更新后技能状态丢失。一开始做技能热更新reload模块之后发现技能调用报错“外部资源未初始化”。原因是技能模块的全局变量比如数据库连接池只在模块首次加载时初始化reload后没有重建。后来我在每个技能模块里定义了一个标准的init()函数reload后自动调用才彻底解决。5.2 排障速查表为了方便排查问题我整理了一个速查表团队新人照着就能处理大部分常见情况。问题现象可能原因排查思路解决方案模型从不调用某个技能技能描述不清晰/参数太复杂检查技能是否注册成功、description是否易懂简化描述增加使用示例技能报“参数校验失败”LLM生成的参数不符合Schema查看实际入参与Schema差异校验改为“过滤必填校验”加warning日志Agent频繁调用同一个技能技能返回结果诱导继续调用检查技能输出文本是否包含误导性描述技能返回尽量用纯数据加限流调用超时技能内请求外部API过慢查看日志中耗时与依赖服务状态技能内部加超时重试设置合理timeout热更新后技能异常模块reload后状态丢失检查全局变量初始化为技能模块定义init()并自动调用新技能上线后效果变差技能数量太多模型选择困难查看技能列表是否过于臃肿按域拆分技能或引入技能分组机制5.3 关于技能粒度的独家心得说到技能粒度这可能是全篇最想强调的经验。粒度过粗一个技能干太多事模型传参时经常不知道该怎么填粒度过细技能数量爆炸模型选择困难加上prompt里塞满技能定义反而影响理解和判断。我的经验是以“用户能说出的自然任务”为粒度标准。比如“帮我把订单导出成Excel发到邮箱”这是一个自然任务。你不会让用户说“先查询订单再创建Excel再发邮件”那样体验太割裂。所以对应技能应该是一个组合技能export_orders_to_email内部编排三个子步骤。反过来如果技能本身是原子能力例如读取账单就不该把它和“发送邮件”强行合并成一个技能否则其他流程没法复用。实践里我建议对技能做分层底层原子技能负责单一动作上层组合技能负责编排。这样既保证了复用性又提升了Agent对复杂任务的处理能力。组合技能本质上就是一段“小Agent逻辑”模型决策要不要启用它内部执行可以走固定的代码流程不一定非要实时调用LLM。最后分享一点个人体会做agent-skills这套框架最大的收获不是代码而是想通了一个问题Agent的边界不该由模型决定而该由工程决定。你在技能层做多少设计Agent就能多可靠多少。模型始终会有概率性的失误但如果你把一切不确定因素都收敛在技能描述、参数校验、权限控制这些确定性模块里系统的整体可靠性反而可以做到很高。如果你现在正打算给自己项目的Agent接工具我的建议是别一上来就整复杂框架先按本文第三节的最小实现跑通然后一步步加上热更新、权限、审计、限流。等技能数量超过10个你就会发现这套架构带来的收益远超预期。后面我还会拆一下“技能编排引擎”和“多Agent协作”这两个进阶方向感兴趣的话可以继续关注。