☰
Agent技能库设计指南:从提示词工程到技能工程
2026/9/25 13:36:06 网站建设 项目流程

最近在梳理手头几个 Agent 项目时,我越来越觉得,真正决定一个智能体能不能上生产环境的,往往不是模型本身的“智商”,而是它手边有没有一套像样的技能库。所谓 agent-skills,说白了就是把 Agent 要反复执行的一类能力——比如计算、查天气、解析文档、调 API——封装成可复用、可检索、可测试的技能模块,让模型在任务到来时能快速找到最合适的工具,而不是每次都用提示词从头现编。

这个思路能解释一个很常见的现象:同一个模型,有人调出来的 Agent 像毛手毛脚的新人,有人调出来的却像干了十几年的老手。区别基本不在提示词写得有多花哨,而在能力的组织方式。这篇文章我会从 agent-skills 的设计思路讲起,落到一套可以直接跑起来的轻量代码实现,再把生产环境里踩过的坑一并倒出来。适合正在做 Agent 应用开发,或者准备把多步骤业务流程交给模型去执行的团队参考。

1. 先搞清楚 agent-skills 到底要解决什么问题

1.1 Agent 的能力瓶颈不在模型,在技能复用

前两年大家做 Agent,普遍有个错觉:只要模型够强,把任务描述写清楚,它就能自己搞定一切。实测下来完全不是这么回事。模型每次处理同类任务时,都在“重新发明轮子”。比如让它写一条 SQL,它每次都从零构思表结构和字段命名,哪怕上一轮已经见过同一张表;让它调一次第三方 API,参数格式经常写错,GET 变 POST、字段名大小写对不上,这种问题每天都在发生。

这不是模型智力不够,而是缺少稳定的执行上下文。你可以想象一个聪明但毫无经验的实习生:每次交代任务,都要从最基本的东西教起,稍微复杂点就忘,忘了还得重新教一遍。agent-skills 要解决的核心问题,就是把这些“基本的东西”固化成技能卡片,让模型调用的时候直接照做,而不是重新推理一遍。

我见过很多团队把希望寄托在“更详细的 system prompt”上,结果提示词越写越长,行为却越来越不稳定。因为提示词本质上是一段一次性文本,它没法被单独测试,没法做版本回滚,更没法被多个项目复用。技能模块化之后,这些问题就变成了普通的工程问题——每个技能就是一个独立单元,功能单一、边界清晰,坏了拆下来单独修就行。

1.2 从提示词工程到技能工程

如果打个比方,提示词是给模型看的用户手册,那技能库就是给模型准备的操作清单、工具字典和犯错日志。前者描述“你应该怎么做”,是方向性的;后者定义“你可以调用什么”,是可执行、可观测的。

我刚开始探索 agent-skills 时也走过弯路,以为所谓技能就是写一堆函数让模型去选,那不还是 function calling 吗?后来才意识到区别。Function calling 只是把函数暴露给模型,它解决的是“模型如何调用工具”的通信问题;而技能系统还要管另外几件事:技能怎么被发现、怎么被描述、怎么验证结果、怎么沉淀迭代、不同任务之间怎么共享。这套体系才是 agent-skills 真正值钱的地方。

从工程角度看,技能化的收益非常直接。第一,可测试性提升了一个量级,每个技能都能写独立用例,跑 CI 时顺手把所有技能回归一遍,模型的幻觉问题能在开发阶段就暴露一半;第二,Token 消耗显著下降,因为不需要在每轮对话里都重复粘贴完整指令和示例;第三,故障定位变得简单,任务出错时可以直接看是哪个技能返回了脏数据,而不是在一大段对话里猜模型哪一句理解偏了。

2. 技能系统设计:核心模块与关键选型

2.1 原子技能、复合技能与技能注册表

在设计技能系统时,我习惯把技能分成两种粒度:原子技能和复合技能。原子技能是不能再拆的最小动作,比如“计算一个数学表达式”“读取某个 URL 的 JSON 内容”“把一段文本做摘要”,它们通常直接对应一个函数或一个 API 调用。复合技能是把多个原子技能按固定顺序编排起来的结果,比如“根据用户需求查数据库,再把结果格式化成 Markdown 表格返回”,这中间就串了查库、格式化两个动作。

这两种粒度各有各的用途。原子技能讲究小而稳,方便复用和测试;复合技能讲究编排效率,避免模型每次都要自己串联多步操作,增加犯错概率。我见过一个很经典的案例:同样一个“从订单列表里算总金额”的需求,让模型直接写代码去处理,偶尔会漏掉税费、折扣这些字段;但封装成一个复合技能之后,逻辑固定了,只要输入格式对,输出就一定正确,稳定率从 80% 直接拉到 99% 以上。

不管原子还是复合,技能最终都要注册到一个统一的地方,这就是技能注册表。注册表管理技能的元信息:技能名称、描述、参数 schema、版本号、所属目录。模型本身不需要知道技能怎么实现,它只需要通过注册表拿到一份“技能清单”,像翻菜谱一样挑选合适的菜来做。选型上,注册表不必引入重框架,用简单的字典或者 SQLite 都行,关键是访问接口要稳定。

关于代码和配置的边界,我的建议是:凡是经常逻辑调整的,尽量写成配置驱动;凡是固定不变的,写成代码。比如技能的参数定义、使用场景说明、返回格式约定,这些属于“会频繁微调”的部分,放 YAML 或 JSON 里非常合适,改完不用重新部署就能生效。

2.2 技能描述写不好,再强的检索也白搭

技能系统里最容易低估、也最影响效果的是描述。很多人在注册表里只写一句话:“计算器,用于计算”。这种描述基本等于没写,模型根本不知道什么场景该选它、参数怎么传、返回什么格式。

一套好的技能描述,至少要包含四块内容:它做什么、什么场景下用、参数的具体含义和类型、返回数据的结构。举个例子,一个查天气的技能,如果描述写成“根据城市名返回实时天气数据”,模型大概率会在需要“未来三天预报”时也误调它;如果写成“仅支持实时天气查询,参数 city 是城市中文名,返回包含 temperature、wind、precipitation 字段的 JSON”,模型就能准确判断什么时候该用它、什么时候不该用。

这里要给个提示:描述里一定要写清楚边界和限制。模型对技能的选择很大程度依赖描述里的“允许/禁止”信号,你不写限制,它就会默认技能什么都能干。我见过一个查电商订单状态的技能,因为描述里没写只支持近 90 天订单,模型就直接拿它去查一年前的订单,返回空结果后还一本正经地告诉用户“查无此单”,误导性极强。

编写描述时可以参考一个简单模板:能力概述 + 适用场景 + 禁止场景 + 参数说明 + 返回示例。其中返回示例非常关键,模型拿到返回示例后,后续的回答格式会稳定很多,相当于给它做了一个 few-shot 示范。实际写下来,一个技能的描述通常在 200 到 500 字之间,长点没关系,关键是信息密度要高。

2.3 技能的可测试性与版本管理

技能一旦多了,可测试性就是生命线。我在自己的技能系统里给每个技能配了一个最小测试用例,输入是一份固定的样例数据,输出要校验字段是否存在、类型是否正确、核心逻辑是否跑通。每次修改技能代码或描述之后,第一件事就是跑一遍技能级测试,通过后再接入整体链路。

测试用例的设计我建议遵循“三条线”:正常路径、边界值、异常输入。正常路径覆盖典型场景,边界值覆盖比如空字符串、超大输入、特殊字符,异常输入则专门验证技能在拿不到数据时会不会优雅报错。有一个很实际的例子:查数据库的技能,如果查询结果为空,返回格式是{"data": [], "error": null}还是直接抛异常?这个看似无关紧要的决定,会让模型后续行为完全不一样。前者模型会告诉用户“没查到数据”,后者模型可能会编造一条不存在的结果。

版本管理同样不能省。技能部署到线上后,不是永远不变的,今天优化了算法,明天调整了描述,后天新增了功能。如果不做版本管理,出问题的时候连对比的基准都没有。我在注册表里为每个技能加了个version字段,格式用x.y.z:主版本号变动代表不兼容更新,次版本号代表功能增强,补丁号代表描述修正。模型调用时可以在消息里带上版本信息,出问题后通过日志排查当前用的是哪个版本的技能,直接对比就能知道是不是升级引发的。

3. 从零搭一个轻量技能系统:实操记录

3.1 技能基类定义

下面我直接给出一套在实际项目里跑通的轻量实现,技术栈是 Python 3.10+,依赖只有一个 pydantic,方便做参数校验。如果项目里不是 Python,这套结构也可以平移,核心思想是一致的。

from abc import ABC, abstractmethod from typing import Any from pydantic import BaseModel class SkillResult(BaseModel): ok: bool = True data: Any = None error: str | None = None class Skill(ABC): name: str = "" description: str = "" parameters: dict = {} version: str = "1.0.0" @abstractmethod def execute(self, **kwargs) -> SkillResult: ...

这里有个设计细节值得说明:所有技能统一返回SkillResult对象,而不是直接抛异常或返回裸数据。这样做的原因是,技能的执行结果会被模型当作上下文继续使用,格式必须统一,模型才知道怎么处理。ok字段表示执行是否成功,error用于携带错误信息,data存放真正的结果。有了这个统一外壳,后续做日志采集、结果校验就非常方便。

3.2 技能注册中心与加载机制

注册中心是整个技能系统的枢纽,所有技能都要在这里登记。我习惯写成单例模式,方便在多个模块里共享同一个技能池。核心逻辑就是注册、获取、列出,非常简单,但也非常稳定。

class SkillRegistry: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._skills = {} return cls._instance def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(f"skill {skill.name} already registered") self._skills[skill.name] = skill def unregister(self, name: str): self._skills.pop(name, None) def get(self, name: str) -> Skill | None: return self._skills.get(name) def list_skills(self) -> list[dict]: return [ { "name": s.name, "description": s.description, "parameters": s.parameters, "version": s.version, } for s in self._skills.values() ]

在实际项目里,我还会加一个load_skills_from_dir的方法,自动扫描指定目录下的技能文件,按模块名动态导入并注册。这样新增技能就不再需要改注册代码,只要往目录里丢一个新文件就行。对团队协作来说,这个改动体验提升非常明显。

3.3 技能检索与路由的设计

技能注册好之后,下一步就是让模型能“找到”合适的技能。这里有两种策略:一种是全量塞给模型,让模型自己选;另一种是先做粗筛,缩小候选集,再让模型精准选。策略选择取决于技能总量。技能少于十个,全量塞过去没毛病;技能超过二十个,描述加起来可能就几千个 Token,再有其他上下文,模型容易“看花眼”。

我一般在代码里先做一个简单的关键词粗筛,再用模型做最终选择。粗筛的逻辑可以非常朴素:把技能描述和用户查询都转成小写,计算关键词重叠度,取 Top 10 作为候选。如果团队有条件,也可以接入向量检索,效果会更好,但起步阶段不必上重武器。

def rank_skills(query: str, skills: list[dict], top_k: int = 8) -> list[dict]: query_tokens = set(query.lower().split()) scored = [] for skill in skills: desc_tokens = set(skill["description"].lower().split()) score = len(query_tokens & desc_tokens) scored.append((score, skill)) scored.sort(key=lambda x: x[0], reverse=True) return [s for _, s in scored[:top_k]]

粗筛过后,系统会把候选技能的名称、描述、参数定义发给模型,模型用 function calling 的方式返回它想调用的技能名和参数。这一步本质上是在做“决策”,把决策权留给模型,而不是用硬编码规则写死,这样才能应对用户各种千奇百怪的问法。

3.4 完整调用链路:从用户输入到技能执行

把前面的模块串起来,一套完整的调用链路是这样的:用户输入 → 粗筛候选技能 → 模型选择技能并填参数 → 执行技能 → 把结果回填给模型 → 模型生成最终回复。我用一个伪代码把这套链路完整展示出来,方便直接对着抄。

def handle_user_message(user_message: str, client, model="gpt-4o"): registry = SkillRegistry() all_skills = registry.list_skills() candidates = rank_skills(user_message, all_skills, top_k=10) tools = [ { "type": "function", "function": { "name": s["name"], "description": s["description"], "parameters": s["parameters"], } } for s in candidates ] messages = [{"role": "user", "content": user_message}] response = client.chat.completions.create( model=model, messages=messages, tools=tools, ) msg = response.choices[0].message messages.append(msg) if msg.tool_calls: for tc in msg.tool_calls: skill = registry.get(tc.function.name) if skill is None: messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps({"ok": False, "error": "skill not found"}), }) continue try: args = json.loads(tc.function.arguments) result = skill.execute(**args) if isinstance(result, SkillResult): result = result.model_dump() messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps(result, ensure_ascii=False), }) except Exception as e: messages.append({ "role": "tool", "tool_call_id": tc.id, "content": json.dumps({"ok": False, "error": str(e)}), }) final_response = client.chat.completions.create( model=model, messages=messages, ) return final_response.choices[0].message.content

这段代码的精髓不是某一行写得有多漂亮,而是它把“技能执行”和“模型决策”做了明确分层。模型只负责决定“调用谁、怎么调”,技能层负责“执行并返回结果”,两边不互相干扰。这种分层带来的好处是,任何一个环节出问题,都能单独定位、单独修复,而不是让模型在对话里兜底。

写技能的时候,参数定义要用 JSON Schema 格式,而且要尽量把参数约束写清楚。比如某个技能接受一个limit参数,最好写成{"type": "integer", "minimum": 1, "maximum": 100},这样模型在生成参数时就能自动约束取值范围,减少非法调用。

4. 实际项目中踩过的坑和排查技巧清单

4.1 技能返回格式“偶尔漂移”怎么办

技能系统的第一个坑往往不在技能本身,而在模型对返回结果的处理。早期我把技能执行结果原样丢给模型,发现模型偶尔会忽略某些字段,甚至自己编造数据。后来才明白,模型不是真的“忽略”,而是它不确定这些字段的含义,只能发挥想象力。

解决办法是给每条技能结果加一层“解释性包装”。执行完技能后,在返回内容里补一段简短的使用说明,告诉模型这些数据是什么、可以怎么用。比如天气技能返回{"temperature": 28}时,可以包装成「当前气温 28 摄氏度,数据来自城市天气 API,可直接用于回答用户」。这样模型就不需要猜,回答的准确率会高很多。

4.2 技能描述撞车导致的误调用

技能多了之后,很容易出现两个描述高度相似的技能。比如“查订单详情”和“查订单物流”,名字像、描述也像,模型经常选错。这个问题在前期几乎不可避免,只能靠线上日志和 eval 数据不断修正描述。

我处理这类问题的套路是给技能描述增加“负向说明”。在“查订单物流”的技能描述末尾加一句注意:本技能仅用于查询物流轨迹,不返回商品明细、金额信息,请勿用于查订单详情。这句负向说明看着啰嗦,实际效果非常显著,能把误调用率降一半以上。

4.3 技能数量膨胀后的检索失效

技能库超过三四十个之后,关键词粗筛就有点力不从心了。用户问法稍微口语化,关键词重叠度可能就为零,导致候选列表里根本不含正确技能。这时候最明显的现象是,模型经常说“我没有找到相关能力”,但实际上技能库里有现成的。

这个阶段我建议引入向量检索。把技能描述用 embedding 模型编码,用户查询也编码,用余弦相似度做召回,跟关键词结果做加权融合。代码思路不复杂,但检索效果提升非常明显。如果不想引入额外服务,也可以先试试提高粗筛候选数、优化描述措辞,这些低成本手段能续一段时间。

4.4 技能内部异常导致整个任务失败

年轻的时候写技能,习惯直接把数据库连接、HTTP 请求这些放技能里,结果一旦网络抖动或数据库超时,异常就往上抛,整个 Agent 任务直接失败。后来我把技能内部所有的网络操作都包了一层超时和重试,并且把失败信息转成用户可理解的错误描述,而不是堆一堆堆栈。

一个推荐的做法是:技能内部不轻易抛异常,而是返回SkillResult(ok=False, error="数据库连接超时,请稍后重试")。这样模型拿到错误后,还能根据上下文决定是重试、换技能还是如实告诉用户。把“技能执行失败”当作一种正常结果来处理,系统的鲁棒性会有质的变化。

4.5 排查技巧:日志里必须能看到调用前后

技能系统的排查,一定要靠结构化日志。我在每个技能执行前后都打一条日志,记录技能名、参数、耗时、返回结果摘要、请求 ID。这样一旦线上出问题,按请求 ID 拉出整条链路的日志,立马就能看到哪个技能执行了、参数对不对、结果是否正常。

日志的作用平时体现不出来,出问题的时候就是救命稻草。我见过有团队上线 Agent 后一个多月没看日志,直到用户投诉才排查,结果发现日志里什么都没打,只能靠猜。我的建议是,技能系统从第一天起就必须把日志设计进架构里,这比任何监控告警都重要。

经过几轮迭代,我把这套技能系统的排查经验整理成一张速查表,每次出问题都按这个顺序过一遍。

现象优先排查方向常见原因
模型没选对技能技能描述、粗筛召回描述边界不清、检索漏召
技能执行成功但结果不对技能逻辑、参数校验参数类型解析错误、边界未处理
技能执行抛异常网络、超时、依赖服务外部服务不稳定、缺少重试
模型忽略技能结果返回格式、结果包装结果缺少解释性说明
响应 Token 超长候选技能数量、上下文长度全量塞技能、历史消息截断策略不当

5. 再往后走:如何让技能系统持续进化

5.1 从固定技能到动态技能生成

技能系统稳定运行一段时间后,就会进入新的阶段:你不再只是手动添加技能,而是想让 Agent 自己从任务历史中提炼技能。思路是记录那些执行成功、效果稳定的多步任务,把关键步骤提取成模板,保存为新技能。这一步做好了,Agent 的成长性会变得非常惊人,相当于它有了一本自己写的工作手册。

实操上可以每隔一段时间,人工审核一批高质量历史对话,标注出哪些子任务适合沉淀为技能,再用脚本半自动生成技能代码和描述。前期人工审核不能省,因为自动提炼出来的东西如果不经筛选,会把很多一锤子买卖的逻辑固化下来,反而污染技能库。等积累了足够的评测集,才能逐步自动化。

5.2 技能系统的评测是长期投入

我越来越觉得,做 agent-skills 本质上是在做一套能力管理系统,而管理就需要度量。给技能库配一套评测集,包含各种常见用户问法和预期技能选择,每次改动技能描述、新增技能、调整检索逻辑,都在评测集上跑一遍,能直观看到是否引入了回归。

评测集的维护成本不低,但收益非常高。它相当于给了技能系统一张“考卷”,让所有修改都有据可依。有了它,团队成员就能放心大胆地调整技能描述而不用担心把线上搞坏。建议把评测结果接入 CI,技能描述或代码有变更时自动跑一遍。

提示:这个领域迭代频率很快,技能描述的写法、检索策略、评测方法都在快速演化,但核心原则不会变——技能要能被稳定地发现、可靠地执行、清晰地度量。

5.3 团队协作中的技能共享与沉淀

最后说一个容易被忽视的点。技能系统做大了之后,它不只是一个技术组件,更像团队的“能力资产库”。新人入职,直接翻技能清单就能了解系统能做什么;不同业务线之间,也能互相引用已经验证过的技能,避免重复造轮子。把技能当成产品来迭代,给每个技能设置负责人,定期 review 描述质量和执行效果,会让整个系统长期保持干净可用。

我在实际维护中发现,技能库里真正吃灰的往往是那些一开始兴致勃勃写出来、后来没人维护的边角技能。所以现在我加技能的原则是:先确认至少有两个真实场景会用到,才允许注册。宁缺毋滥,比看起来丰富更重要。

写在最后的几点个人体会

整套技能系统的搭建,本质上是在给模型构建一个稳定、可靠、能成长的“工作环境”。模型的能力边界固然重要,但给它配一套趁手的技能库,往往比换一个更大的模型更能提升最终效果。我自己最大的体会是:技能库的维护成本,前期看似额外负担,越到后面回报越大,但前提是必须把描述写清楚、测试做到位,否则几周之后,你连自己写的技能是干什么用的都未必想得起来。

如果只能从这篇文章里带走一句话,我的建议是:不要等到技能多了才去想管理方案,哪怕现在只有三个技能,也把描述规范、注册中心、结果包装这些地基打好。Agent 应用上线之后,迭代最快的不是模型,不是提示词,而是你给 Agent 配的这套技能体系。地基稳了,后面怎么盖楼都不慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询