做Agent开发快三年,我最深的体会是:智能体的上限,很大程度不是模型的聪明程度,而是你给它搭的“手和脚”好不好用。早期我做自动化工具,喜欢把所有逻辑都揉进一个巨大的系统提示词里,结果上下文越塞越满、指令互相打架,稍微换一个需求就牵一发动全身。后来我全面转向agent-skills这种设计——把agent能执行的每一个动作拆成独立技能,用统一接口注册起来,再让模型根据用户意图去选择合适的技能。这套思路帮我解决了大量复用和扩展问题。这篇文章我会讲清楚agent-skills从设计到落地的完整链路,包括接口定义、注册调度、function calling对接、真实技能示例,以及我在线上跑了大半年后踩过的坑。不管是正在做LLM应用开发的工程师,还是刚开始接触智能体、想搭建自己技能库的朋友,都可以把这篇当一份操作参考。
1. 为什么一定要把智能体能力“技能化”:先说清楚底层逻辑
1.1 硬编码Prompt的三种常见病
很多开发者初次做Agent时,习惯把所有能力塞进提示词里。我接过一个项目,系统提示词从最初的800字膨胀到3000多字,里面塞了工具用法、输出格式、回答风格、权限提醒,甚至还有几个完整示例。结果模型经常“失忆”,用户问一句无关的细节,就开始答非所问。这就是典型的上下文过载:模型不是不聪明,而是被海量指令分散了注意力。
第二个问题是指令冲突。我在提示词里同时写了“回答要简洁”和“给出详细步骤和理由”,这俩目标本身就需要权衡,模型每次都在猜。上线后同一个问题,测试时一个样,生产又是另一个样。这种不确定性来自prompt本身的脆弱性,根本不值得靠堆字数去对抗。
更让我受不了的是复用。A项目里实现好的“搜索资料”逻辑,想搬到B项目里,要么复制一长段prompt,要么重新写一遍函数,两种方式都让维护成本翻倍。后来我才真正明白:我应该把Agent的能力抽象成一个个独立技能,让模型通过函数调用去选择执行——这正是agent-skills这套思路的核心。
1.2 agent-skills到底帮我解决了什么
agent-skills不是什么神秘框架,它就是一套工程组织方式:把Agent能执行的每个动作定义为skill。每个skill包含四样东西:名字、描述、参数定义、执行函数。模型只负责判断“现在该用哪个技能”,具体执行交给代码。这就像给Agent准备了一套工具箱,而不是逼它把工具箱背在脑子里。
我自己最直观的感受有三个。第一是组合性,技能像乐高积木,搜索、提取、总结各管一段,按需拼装就能应对新需求;第二是可观测性,每个技能都能单独测试,出一类问题只查一段代码,不用回归整个prompt;第三是可扩展性,新增一个能力就是新增一个skill文件,主流程完全不用动。
可以看一个直观对比:
| 维度 | 全塞Prompt | 代码硬编码函数 | agent-skills技能化 |
|---|---|---|---|
| 上下文占用 | 高,随功能线性增长 | 低 | 低,只暴露技能摘要 |
| 可测试性 | 差,依赖端到端验证 | 中等,靠主流程触发 | 高,单技能独立验证 |
| 扩展成本 | 每加个功能都要改长文档 | 需要改if-else分支 | 新增一个注册文件 |
| 模型自由度 | 低,常被指令束缚 | 无,需要外部硬调度 | 高,模型自主匹配 |
| 可观测性 | 差 | 中 | 好,调用日志天然完整 |
2. 技能系统设计:接口、注册表和调度的三角关系
2.1 技能粒度:一个技能只做一件事
设计技能库时最先会遇到的问题就是粒度。有人把“搜索并总结新闻”做成一个技能,理由是“这俩经常一起用”。我建议千万不要这么干。搜索是搜索,总结是总结,拆开才能组合出更多玩法。
我通常按“动作内聚性”来拆:一个技能内部只做一种类型的事情,不跨数据源,不混业务规则。比如“从网页提取正文”和“解析PDF内容”是两个技能,虽然它们都叫“提取”,但内部实现完全不同。拆细了还有一个好处:模型可以只调用需要的部分。用户问“今天的天气”,Agent只需要调用天气查询,完全不用加载网页解析那套逻辑,少走很多弯路。
粒度的判断标准其实很简单:如果这个技能在另一个场景里会被单独复用,那它就该独立存在。如果两个技能永远同时出现,再考虑合并。刚开始宁可拆细一点,也别贪图省事拼成一个大杂烩。
2.2 技能描述是灵魂,参数结构是骨架
在agent-skills体系里,description不是写给人看的注释,而是模型做技能匹配的核心依据。写得太抽象,模型不知道什么时候该调用;写得太啰嗦,模型抓不住重点。我的经验是,一段合格的描述至少包含三部分:触发场景、执行目标、明确反例。
拿实时搜索技能举例。差的描述是“搜索互联网信息”,模型碰到“帮我查一下明天上海天气”可能去调它,也可能不调,因为“查天气”听起来不像“搜索”。好的描述是:当用户需要获取实时信息、最新新闻、某个事件的最新进展,或者需要验证过时知识时使用;如果是纯粹的数据计算、常识问题、个人主观观点,不要使用这个技能。这样模型才有边界感。
参数结构也直接决定成功率。我全部用JSON Schema定义,不能写自由格式的说明。因为function calling返回的参数是结构化的,Schema写不严,模型就会自由发挥。比如定义搜索分页参数page_size,如果只写“type: integer”,模型可能输出“20”,也可能输出“二十”,后端解析直接炸掉。补上minimum、maximum、default,模型就会规规矩矩生成合法的JSON。
2.3 注册表与运行时调度
技能注册表是整个系统的中枢,本质就是一个映射表:技能名到技能对象的字典。我用Python实现时大概长这样:
SKILL_REGISTRY = {} def register(skill_cls): inst = skill_cls() SKILL_REGISTRY[inst.name] = inst return skill_cls有了注册表,调度就有了基础。我的调度分两层:第一层由LLM做意图识别,从注册表里选技能,这层靠的是description写得够不够好;第二层是代码层的兜底校验,模型说“调用search_internet”,代码就去注册表里查这个key,查不到就返回明确错误,不会让异常流到用户面前。
运行时调度还需要处理三件事:超时、重试、错误归一。任何一个技能都可能因为第三方服务慢而卡住,我要求所有技能的执行函数返回统一的结果结构,比如{"status": "success", "data": ...}或{"status": "error", "message": ...}。这样上层LLM能读懂结果,用户也不会看到一堆裸异常。
3. 把agent-skills落到代码里:一份可直接参考的实现
3.1 技术选型:为什么是Python+Function Calling
技能库的语言我选了Python,没有别的原因,就是生态最全。搜索、解析、向量检索、调用第三方API,Python都有现成库,写几行就能跑起来。服务用FastAPI暴露接口,因为异步支持好,后面接Agent框架、接前端都不费劲。
和LLM的对接我优先用function calling。现在主流模型都提供了工具调用能力,接口风格也基本一致。我不建议自己在Prompt里仿造“调用指令”,让模型输出JSON再去解析,那样格式不稳定,维护成本极高。function calling让模型原生输出结构化参数,后端拿到就能执行,是我认为最稳妥的路线。
3.2 定义技能基类
技能基类不需要复杂,核心是让所有技能长得一样。我的最小实现如下:
from typing import Any, Dict class Skill: name: str = "" description: str = "" parameters: Dict[str, Any] = {} async def run(self, **kwargs) -> Dict[str, Any]: raise NotImplementedErrorname必须是全局唯一的标识,description是给模型看的,parameters是JSON Schema,run就是真正干活的函数。所有技能都继承这个基类,好处是上层代码可以完全统一处理,不用为每个技能写特例。
如果后续需要鉴权,我还会加一个required_auth字段,在调度层先校验用户权限再放行。这个看你的业务需求,一开始可以不放。
3.3 实现技能注册和动态发现
注册有两种方式:手动import和自动扫描。项目初期技能少,手动import完全够用:
from skills.search import SearchInternet from skills.retrieve import RetrieveDocs register(SearchInternet) register(RetrieveDocs)技能超过20个之后,我改成自动扫描。把所有技能文件放在一个包目录下,启动时遍历加载:
import pkgutil import importlib import skills def load_all_skills(): for mod_info in pkgutil.iter_modules(skills.__path__): importlib.import_module(f"skills.{mod_info.name}")这段代码会在skills目录下找所有.py文件并加载。每个模块内部自己调用register注册自己的技能类,主程序完全不用关心新增了谁。后续团队成员要加技能,只需要按规范新建一个文件,系统重启后自动生效。
3.4 对接LLM:把技能转成工具并完成调用闭环
注册表里的技能不能直接给模型用,要转换成一个“工具列表”。以OpenAI格式为例:
def build_tools(): tools = [] for skill in SKILL_REGISTRY.values(): tools.append({ "type": "function", "function": { "name": skill.name, "description": skill.description, "parameters": skill.parameters, } }) return tools这样,build_tools()就能原样传给chat.completions.create的tools参数。完整的一次调用闭环大概是:
messages = [{"role": "user", "content": "帮我查一下最近一周AI行业融资新闻"}] resp = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=build_tools(), tool_choice="auto", ) tool_calls = resp.choices[0].message.tool_calls if tool_calls: messages.append(resp.choices[0].message) for call in tool_calls: skill = SKILL_REGISTRY[call.function.name] args = json.loads(call.function.arguments) result = await skill.run(**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": json.dumps(result, ensure_ascii=False), }) # 再次调用模型得到最终回答这里有个细节容易被忽略:模型第一轮返回tool_calls时,原始message对象也要加入messages,后面每个技能结果都要用tool_call_id对应上,顺序错了模型会懵。这段代码是所有技能调用的公共通道,业务逻辑完全不用改。
4. 三个可以直接抄作业的技能示例
4.1 实时搜索技能
搜索是最常用也最典型的技能。我封装了一个调用搜索API的技能,参数包括query、limit和可选的region。代码如下:
class SearchInternet(Skill): name = "search_internet" description = ( "当用户需要获取实时信息、最新新闻、某个事件的最新进展时使用。" "如果问题是纯粹常识、数据计算或个人主观观点,不要使用本技能。" ) parameters = { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "limit": {"type": "integer", "description": "返回结果数量", "default": 5} }, "required": ["query"], } async def run(self, query: str, limit: int = 5): # 用你的搜索服务商API results = search_api(query, limit=limit) return {"status": "success", "data": results}描述里的反例非常重要。早期我没写“不要用于纯常识问题”,结果模型经常用搜索回答“太阳从哪边升起”,白白浪费几秒延迟。加了反例之后,这类问题模型会直接凭内部知识回答,体验提升明显。
4.2 本地文档检索技能
第二个技能是让Agent读本地知识库。这里的底层是向量检索:先把文档切片转成embeddings存入向量数据库,查询时把query转成向量,做相似度检索。技能只暴露一个干净的接口:
class RetrieveDocs(Skill): name = "retrieve_docs" description = ( "当用户的问题涉及公司内部文档、产品手册、历史项目资料时使用。" "从知识库检索相关片段返回给用户。" ) parameters = { "type": "object", "properties": { "query": {"type": "string", "description": "用户问题原文"}, "top_k": {"type": "integer", "description": "返回片段数", "default": 3} }, "required": ["query"], } async def run(self, query: str, top_k: int = 3): embedding = embed_text(query) docs = vector_db.search(embedding, top_k=top_k) return {"status": "success", "data": [d.to_dict() for d in docs]}注意query字段我特意说明要传“用户问题原文”,而不是让模型自己改写。因为检索质量取决于语义匹配,模型改写后可能丢关键信息。这是我在实践中反复调整过的一个点。
4.3 组合技能:一条指令生成一份简报
单个技能只能做单点动作,真正的价值在组合。我做过一个“生成行业简报”的场景,用户只需要说“帮我整理一下最近一周的AI行业动态”,Agent会自动走完搜索、提取、总结、写报告四步。
这套流程不写死在代码里,而是靠模型自主编排:
skills_needed = ["search_internet", "extract_webpage", "summarize_text"]模型发现用户要的是“整理动态”,会先调用search_internet搜索出十几条结果,再调用extract_webpage打开关键链接抽取正文,然后调用summarize_text压缩成摘要,最后自己生成结构化简报。整个过程都在第三节那条调用闭环里自然发生,不需要额外写流程控制代码。
这种编排方式的好处是灵活。用户中途改需求,比如“只看融资相关的”,模型下一轮就会调整后续动作,而不是重新从头跑。坏处是依赖模型能力,偶尔会选错技能组合,所以第5部分我会专门讲怎么排查。
5. 线上跑了大半年,我遇到的5类问题与排查记录
5.1 模型老选错技能
症状是让Agent总结PDF,它去调网络搜索;让Agent查内部资料,它去调实时搜索。多数情况下不是模型笨,是技能描述写得太像。
排查技巧:打开调用日志,看模型实际选择了哪个技能,对比两个技能的description。如果两个技能描述里都出现“信息”“查询”这类词,模型就会随机选。我后来统一用“实时信息”和“内部文档”做了强区分,并在各自描述里加上对方场景的排除条件。改动之后,选错率大幅下降。
5.2 参数类型和必填项失控
模型偶尔会传无效参数,比如搜索技能传一个空字符串query,或者把limit传成“三”。我做了两层防护:第一层,JSON Schema里明确minimum和maximum;第二层,run函数内部再校验一遍,不合法就直接返回错误结果给模型,让它重新生成。
实践下来,光靠Schema不够,函数内部兜底是必须的。因为模型可能完全绕过Schema的某些约束,比如传一个不在枚举里的值,虽然不常见,但一旦发生,后端异常会中断整轮对话。兜底校验让技能更健壮。
5.3 技能超时拖垮整个对话
最典型的是搜索API偶尔慢,一个技能卡20秒,用户等待超时,整个对话结束。解决方案一句话:给技能执行加超时控制。
try: result = await asyncio.wait_for(skill.run(**args), timeout=10) except asyncio.TimeoutError: result = {"status": "error", "message": "技能执行超时,请稍后重试"}实际情况里,不同技能超时时间不一样。搜索我设8秒,向量检索设5秒,外呼接口可以放宽到15秒。统一封装一个函数根据技能类型动态取timeout,不要全用同一个值。
5.4 状态在技能之间残留
早期我喜欢在模块里用全局变量缓存中间结果,比如把上次搜索的结果放在全局list里,让后续技能直接读。结果用户连续问两个问题时,第二个问题拿到的是第一个问题的中间数据,答案完全错乱。
后来我规定:技能必须是“无状态”的,所有输入都通过参数传入,所有输出都通过返回值传出。如果确实需要跨技能共享数据,就在对话层级维护一个上下文对象,每个技能只从参数里取值,绝不偷偷读全局状态。排查这类问题时,要先看技能里有没有模块级别的可变变量。
5.5 function calling格式随模型升级变化
模型版本升级后,偶尔会遇到function calling参数格式的小变化,比如OpenAI新版本里工具结构从functions换成了tools。这个问题最坑的是不报错,只是模型偶尔不输出tool_calls。
我的对策是封装一个适配层,不让业务代码直接调SDK的completions方法。适配层负责把统一的tools结构转换成当前模型SDK需要的格式,这样换模型时只改适配层,所有技能代码不用动。即使模型升级,我也可以快速切换回旧接口。
6. 关于agent-skills,我的几条个人经验
最后分享几个碎片化的心得。第一,技能不是越多越好。我见过有人一口气注册50个技能,模型面对选择困难,结果还不如10个精心设计的技能。技能库要克制,定期清理使用率低的技能,或者把它们合并进更通用的技能里。
第二,先跑通再抽象。第一版不需要设计多么宏大的技能框架,先为实际要做的1到2个场景写2个简单技能,跑通闭环。当第三个场景出现并发现重复代码时,再抽象公共部分。过早抽象通常是在猜需求,很容易设计出没人用的接口。
第三,把技能描述当代码一样评审。每次改description都要看调用日志,确认改动后模型选技能的行为符合预期。不要一个人闷头改,拉上同事互相评审,很多边界情况是别人提醒才发现。
第四,保留技能调用日志。我会把每次技能调用的名字、参数、结果耗时、是否成功都记下来,定期分析。这个日志是优化agent最宝贵的数据,比任何主观猜测都准。
我现在做新Agent项目时,第一步永远是先列出“这个Agent需要哪些技能”,而不是先写系统提示词。这个习惯帮我避免了很多上线后的返工。如果你也在纠结Agent能力组织的问题,不妨从今天开始,把你的第一个动作拆成skill试试。