做AI应用一段时间后你会发现,真正让大模型发挥价值的不只是“提示词写得好”,而是它手上有没有一套趁手的“技能”。这也是我整理 agent-skills 这件事的初衷:把Agent需要的能力拆成可复用、可组合、可评估的独立模块,让模型不再是一个只会聊天的接口,而是一个能查数据、能写代码、能操作业务系统的执行者。这篇文章会从设计思路、技能定义、落地实现到排查调优,完整过一遍,适合正在做智能客服、自动化办公流、知识库问答这类Agent应用的开发者参考。
1. 项目整体设计与思路拆解
1.1 为什么Agent不能只靠聊天能力
大语言模型本质上是一个“文本生成器”,它能根据输入产出回答,但没法自己去实时数据库拉订单、没法调用外部接口发通知、也没法在你服务器上安全执行一段脚本。如果把Agent直接暴露给用户,它只能信口开河,给出看似合理其实并不可用的答案。
基础的解决方式是给模型接“函数调用”能力。也就是为模型提供一组函数和参数说明,模型根据用户意图决定调用哪个函数、传什么参数,再把函数返回结果重新组织成自然语言回答。这个思路没错,但真正做起来就会发现,零散函数很难管理。三个工程师各写各的函数,命名混乱,参数格式不统一,模型经常选错,出了问题还不知道该看哪个日志。
agent-skills 要解决的就是这个问题。它不是某个单一功能,而是一套关于“技能”的组织规范:每个技能都是一个可独立开发、独立测试、独立部署的能力单元,并且自带一套让模型能“读懂”的说明书。系统运行时把这些技能统一注册到技能中心,Agent需要时按需调用,整个过程可观测、可控制、可审计。
1.2 设计目标与整体架构
在设计这套体系时,我给自己定了四个目标:
- 技能必须可复用:同一个“搜索知识库”技能,在客服机器人、内部问答、文档助手等多场景里都能直接用,不需要为每个场景重写一套。
- 技能必须可测试:每个技能可以在没有大模型参与的情况下单独调用,输入固定,输出可校验。
- 技能必须可编排:复杂任务不是让模型一步完成,而是拆成多个原子技能,用工作流串联。
- 技能必须可观测:每一次被调用、传了什么参数、返回了什么结果、耗时多久,都能通过日志回溯。
架构上,agent-skills 采用“注册中心 + 执行器 + 技能库”的分层方式。技能库里的每个技能在启动时通过装饰器自动注册到注册中心,注册中心维护一张“技能名——技能对象”的映射表,同时把模型需要的技能描述列表(技能名、描述、参数JSON Schema)传给它。模型一旦决定调用某个技能,执行器就会从注册中心取出对应的函数执行,并把执行结果封装好交还给模型。这个流程和具体Agent框架无关,可以适配任何一种底层模型或框架。
1.3 核心设计原则与取舍
技能设计不是写个函数就行,背后有一套需要长期遵循的原则。
第一,原子性。每个技能只做一件事。例如“搜索文档”和“根据文档生成摘要”应该拆成两个技能,而不是揉成一个“搜索并总结”。原子技能更容易复用,也更容易测试。一个技能如果承担太多职责,描述很难写准确,模型也会犹豫到底该不该调用它。
第二,自描述。技能在注册时要写出完整的描述,包括触发条件、输入参数、输出结果、边界情况。描述是写给模型看的“使用说明”,必须保证模型在正确场景下能想起它,在错误场景下不会被误用。
第三,无状态优先。技能执行时不应该依赖上一次调用的内部状态。如果确实需要保存用户偏好、历史记录这类状态,应该通过专门的记忆技能来读写,而不是让普通技能内部自持缓存。这样做的原因是当技能被编排、并发调用时,状态容易互相污染,排查起来也会很痛苦。
第四,权限内聚。每个技能在定义时就声明自己需要哪些权限,而不是运行时动态去申请。比如代码执行技能只能在沙箱目录里写文件,数据库查询技能只能使用只读账号。权限跟着技能走,Agent的权限边界才容易做限制。
下面用一个表格把原则和它们规避的问题放在一起,方便照检查:
| 设计原则 | 核心含义 | 主要规避的问题 |
|---|---|---|
| 原子性 | 每个技能只做一件事 | 技能职责不清,复用困难 |
| 自描述 | 技能描述必须具体可读 | 模型不调用或乱调用技能 |
| 无状态优先 | 技能不持有会话状态 | 并发调用数据错乱 |
| 权限内聚 | 技能声明并限制权限 | 高危操作越权执行 |
| 可观测 | 全调用链路有日志 | 故障无法定位 |
2. 技能分类与定义
2.1 高频技能类型与典型场景
从实际项目看,Agent技能大致可以分为五类,覆盖绝大多数业务场景。
第一类是检索技能。包括数据库查询、知识库搜索、网页内容抓取、文档读取。典型场景是用户问“有没有上周的测试报告”,Agent调用检索技能从文件系统或知识库中定位报告,再决定如何回复。
第二类是文本处理技能。包括摘要、翻译、格式转换、实体抽取。这类技能不依赖外部系统,适合作为组合工作流的中间环节。例如用户上传一篇长文要求整理摘要,Agent先读文本,再调用摘要技能,最后返回结构化结果。
第三类是代码执行技能。在沙箱环境中运行SQL、Python、Shell命令,适合做数据分析、批量文件操作、定时任务。这类技能风险最高,必须配置权限和沙箱隔离。
第四类是API交互技能。负责与外部业务系统对接,创建工单、发送通知、查询物流状态、更新CRM记录。典型场景是用户说“帮我在项目管理系统里创建一个任务”,Agent调用对应API技能完成操作。
第五类是记忆技能。用于读写短期和长期信息,例如保存用户偏好、记住上次沟通到哪一步、跨会话恢复上下文。记忆技能可以被其他技能组合,比如用户前面透露过“我更喜欢表格形式”,后续报告类技能可以提示模型调用记忆技能来调整输出格式。
2.2 技能描述Schema怎么设计
每个技能在上线前都要先写一份结构化描述,也就是技能Schema。我习惯使用下面这个结构:
{ "name": "search_knowledge_base", "description": "在内部知识库中搜索与给定关键词相关的文档。当用户询问产品使用、故障处理、政策规范时使用。返回文档标题、链接以及匹配片段。", "parameters": { "type": "object", "properties": { "keywords": { "type": "array", "items": {"type": "string"}, "description": "搜索关键词列表,最多5个,不要使用过于宽泛的词语。" } }, "required": ["keywords"] }, "output": { "type": "array", "items": { "title": "string", "url": "string", "snippet": "string" } }, "permission": "read-only" }这里的重点在 description。它不应该只有一句“搜索知识库”,而是要写清楚“什么情况下应该用、输入怎么传、返回什么”。我在早期写技能时经常偷懒,描述写成 “A tool to search knowledge base.” 结果是模型面对真实提问时经常识别不出该调用它,等到我改成带触发条件的描述,命中率明显上来了。
参数部分除了类型和字段说明,最好加上约束和示例值。例如搜索关键词不能超过5个,如果用户提问非常宽泛,可以先让模型判断是否需要拆词。约束写不写,直接决定模型传参质量。
2.3 技能描述太宽泛会出什么问题
有次项目里需要让Agent既能查天气又能给穿衣建议,我图省事把两个技能集中成一个“weather_and_clothing”技能,描述写得模棱两可:“处理天气与穿搭相关的问题”。结果模型在用户只问“明天温度多少”时也调用了它,这个技能内部却傻傻地等待用户提供穿衣场景,最终报错。
后来按原子性原则拆分成两个技能,并且把描述写清楚:
- weather_query:仅当用户询问当前或未来天气、气温、降水时使用,参数是地点和时间。
- clothing_advice:仅当用户希望基于天气获得穿衣建议时使用,参数是场合和地区。
模型在决策时其实非常依赖技能名称和描述的字面匹配。你给它的选项越清晰,它就能越准确地选中正确工具。本质上是把一部分“路由决策”从模型那里剥离出来,提前在技能层面做了结构化约束。
3. 从零构建一个技能模块
3.1 最小工程结构怎么搭
我建议每个技能都放在独立文件中,目录结构类似下面这样:
agent_skills/ ├── registry.py ├── runner.py └── skills/ ├── __init__.py ├── search_docs.py ├── summary_text.py └── create_ticket.py把技能拆成独立文件的好处很多。第一,开发时可以分开写,不容易出现多人改同一个文件导致的冲突。第二,测试时可以直接对技能文件做单元测试,不用把整个Agent拉起来。第三,部署时可以按需打包,比如某些技能涉及敏感API,只让指定服务加载。
新人可能会觉得这有点小题大做,直接在一个文件里定义几十个函数不更省事?但技能数量一旦超过十个,单文件会变得难以维护,更重要的是自动化加载、动态注册这些能力没法复用。技能体系增长到一定程度,结构规整带来的收益会非常明显。
3.2 用Python定义技能对象
在Python里我一般用一个Skill数据类来描述技能,并用一个注册装饰器完成自动注册。这段代码的核心思路是:让函数只关注业务逻辑,注册和元数据通过装饰器统一处理。
# registry.py from dataclasses import dataclass from typing import Callable, Any, Optional _SKILL_REGISTRY: dict[str, "Skill"] = {} @dataclass class Skill: name: str description: str parameters: dict func: Callable[..., Any] permission: str = "read-only" def register_skill( name: str, description: str, parameters: dict, permission: str = "read-only", ): def decorator(func): _SKILL_REGISTRY[name] = Skill( name=name, description=description, parameters=parameters, func=func, permission=permission, ) return func return decorator def get_skill(name: str) -> Optional[Skill]: return _SKILL_REGISTRY.get(name) def list_skills(): return list(_SKILL_REGISTRY.values())然后在技能文件中这样使用:
# skills/search_docs.py from registry import register_skill @register_skill( name="search_knowledge_base", description="在内部知识库中搜索与给定关键词相关的文档。当用户询问产品使用、故障处理、政策规范时使用。", parameters={ "type": "object", "properties": { "keywords": { "type": "array", "items": {"type": "string"}, "description": "搜索关键词列表,最多5个。" } }, "required": ["keywords"] }, permission="read-only", ) def search_knowledge_base(keywords: list[str]) -> list[dict]: # 这里执行真实的搜索逻辑,例如调内部搜索API或查向量数据库 results = [] for keyword in keywords: results.append({ "title": f"关于{keyword}的文档", "url": f"https://internal.example/doc/{keyword}", "snippet": f"这篇文档介绍了{keyword}相关的内容。", }) return results这样写出来,技能的业务逻辑和元数据放在一起,谁负责维护这个技能,一眼就能看清它暴露给模型的是什么。不用额外维护一份配置文件,避免元数据与实际函数不一致的问题。
3.3 把技能注册到Agent运行时
注册完成之后,关键是让Agent运行时能拿到完整的技能列表,并且在模型发出工具调用请求时正确执行技能。
构造工具描述列表的代码很简单:
def build_tool_schema(): tools = [] for skill in list_skills(): tools.append({ "name": skill.name, "description": skill.description, "parameters": skill.parameters, }) return tools真正核心的是执行循环。模型会收到用户消息和工具列表,如果它决定调用某个技能,返回内容里会带着技能名和参数。Agent运行时需要拦截这个调用,去注册中心取出对应的函数,传入参数,然后执行:
def dispatch_tool_call(tool_name: str, tool_args: dict): skill = get_skill(tool_name) if skill is None: return {"error": f"Skill {tool_name} not found"} # 参数校验、权限检查可放在执行前 try: result = skill.func(**tool_args) return {"success": True, "result": result} except Exception as exc: return {"success": False, "error": str(exc)}这里的思路不绑死任何具体框架。不管底层用的是哪一种Agent框架,本质上都是“工具列表 + 调用循环”的范式。你可以把这套注册中心直接嵌入自己的Agent服务,也可以让某个已有框架通过适配器来加载技能。
实际项目中还要额外做几件事:参数校验、超时控制、日志上报。参数校验我推荐在注册装饰器里根据Schema自动生成一个Pydantic模型,这样调用前就能拦截类型错误,而不是等函数内部发现了才抛异常。
3.4 技能测试与调试的实操细节
技能测试分成三个层次,缺一不可。
第一层是函数单元测试。直接调用技能函数,检查正常输入下的返回结果是否符合Schema,检查缺失必填参数时是否报错。这一层可以交给CI流程去跑。
第二层是模型选择测试。真实环境中更要关注的是“模型在给定问题下能不能选中正确技能”。你需要准备一组用户问题,让模型基于技能列表生成选择结果,然后人工标注判断是否选对。选错的案例往往是描述不够清晰,优化描述后重新跑。
第三层是端到端联调。模拟真实用户会话,看模型是否先调技能、再根据结果生成最终回复。这里建议加一条“探针日志”,记录当前模型看到了哪些技能、哪些参数约束,这样出了问题能立刻定位是模型选择错,还是执行器执行错。
4. 多技能编排与协作
4.1 模型自主选择技能还是预设工作流
单技能模型自主选择适合开放场景,例如客服机器人,用户的问题千奇百怪,不可能提前预设路径。但复杂任务需要多个技能按顺序协作,如果全部靠模型自由发挥,结果不稳定。这时就要引入工作流预设。
两者可以共存。基本策略是:有稳定业务路径的任务用预设工作流,没有固定路径的任务采用模型自主选择。例如“用户想创建任务工单”是一个稳定路径:先查用户身份、再创建工单、最后发通知。你可以把这三个技能串成固定流程。而“用户问为什么登录失败”则开放很多,可能需要查日志、查文档、查账号状态,让模型临场决定。
用表格对比一下两种模式的特点:
| 编排方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 模型自主选择 | 开放式问答、客服 | 灵活,能覆盖未知场景 | 不稳定,依赖模型能力 |
| 预设工作流 | 固定业务流程 | 结果可控,可调试 | 需要提前梳理路径 |
| 混合模式 | 大部分生产场景 | 兼顾灵活和稳定 | 设计复杂度高 |
4.2 实现一个“资料整理工作流”
我来演示一个混合编排的简单实现。假设用户输入一个主题,Agent需要搜索资料、生成Markdown格式长文、并保存到个人笔记。把它拆成三个技能:search_docs、generate_markdown、save_note。
三个技能分别幂等或半幂等,然后用一个非常轻量的Pipeline执行器串联:
def run_pipeline(start_args, steps): current_input = start_args for step in steps: skill = get_skill(step["skill"]) if skill is None: raise ValueError(f"missing skill: {step['skill']}") current_input = skill.func(**current_input) return current_input pipeline = [ {"skill": "search_docs", "args_from": "first"}, {"skill": "generate_markdown", "args_from": "prev_output"}, {"skill": "save_note", "args_from": "prev_output"}, ]这种流水线式的编排非常适合“输入稳定、步骤固定”的任务。每一步的输入输出都有Schema约束,中间出了问题可以精准定位。比让模型一次性调用多个工具要稳定得多,因为模型不需要在一轮里同时维护多个状态。
对于更复杂的非串行流程,比如“先并行搜索多个数据源,再合并结果”,可以引入简单的DAG调度,但大多数场景用串行流水线就够了。我的经验是:不要一开始就把编排器设计成通用工作流引擎,先用最简单的数组加循环,等流程复杂到现有模式撑不住时再重构。
4.3 技能冲突场景如何排序
技能越来越多时,会发现两个技能描述高度相似。比如“查询订单状态”和“查询物流轨迹”,用户一句“我的货到哪了”可能同时激活两个技能。这时候需要处理冲突和优先级。
我会在技能Schema里增加一个可选字段trigger_examples,列出两三个典型触发语句。模型看到具体示例时,会比抽象描述更容易判断边界。同时在执行层可以做一个简单的候选评分器:对用户问题做关键词抽取,计算和技能描述的相似度,如果分数最高的技能和模型选择的技能不一致,就记录一次告警,后续用来优化描述。
另一种做法是在技能描述里显式声明“如果你想了解商品配送节点,请使用另一个技能”。这种交叉指引能让模型更快厘清边界。我试过在多个团队项目里用这个方法,技能误用率下降非常明显。
5. 常见问题与排查技巧实录
5.1 模型死活不调用技能怎么办
这个问题几乎每个项目都会遇到。最常见的根因是技能描述与用户问题的字面距离太远。你描述里写的是“搜索知识库”,用户问的是“帮我看下产品说明书里怎么设置静音”,模型可能觉得这不需要技能,直接凭训练记忆回答了。
排查步骤:
- 看日志里模型到底有没有收到技能列表。有时候是技能列表构建失败,导致模型根本没有选项。
- 看用户问题是否命中了技能描述中的关键词。如果没命中,尝试在描述中加入更多同义触发词。
- 看技能数量是否太多。直接在模型上下文里塞20个技能,一些老模型可能会忽略部分技能。数量多时应该先按场景分组,减少单轮可见技能数量。
优化描述时不要只加一句,要给出具体触发语句。比如:
# 差 在内部知识库中搜索文档 # 好 当用户询问产品使用、故障处理、操作手册时使用。例如“怎么设置静音”“邮件退信怎么处理”“保修多久”。搜索关键词应从用户问题中抽取,最多5个。这样模型更容易建立“问题——技能”的连接。
5.2 技能参数传得驴唇不对马嘴
模型生成的参数经常会出现类型不对、缺字段、格式错误的情况。比如搜索技能要求 keywords 是数组,模型却传了一个字符串;日期参数要求YYYY-MM-DD,模型传了2024年5月1号。
这种情况靠两层防护。第一层是在参数Schema里尽量写清楚格式和示例,加入枚举和正则约束;第二层是在执行器内做宽容解析,不要直接把参数丢给业务函数。例如日期参数就写一个宽松解析函数:
from datetime import datetime import re def parse_date(value): if isinstance(value, datetime): return value if isinstance(value, str): m = re.match(r"(\d{4})年(\d{1,2})月(\d{1,2})日", value) if m: return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))) m = re.match(r"(\d{4})-(\d{1,2})-(\d{1,2})", value) if m: return datetime(int(m.group(1)), int(m.group(2)), int(m.group(3))) raise ValueError(f"无法解析日期: {value}")宽容解析不是让你忽略错误,而是把模型常见的错误表达归一成标准格式,减少无谓的失败调用。如果真的解析不了,就返回一个清晰的错误信息,让模型修改参数后重新调用。
5.3 技能执行超时和重试要怎么设计
外部API调用的超时不能随便拍脑袋。我常用的方法是:先记录技能在过去一段时间的历史执行耗时,取中位数和P95值,超时时间设置为中位数的3~4倍,但不要低于2秒,也不要超过业务容忍上限。比如某个技能历史耗时中位数是1.2秒,P99是3.5秒,那超时设置到5秒左右大概率合理。
重试要谨慎。对于幂等的查询技能可以重试,对于非幂等的创建类技能不能盲目重试,否则可能产生重复数据。我建议给技能Schema增加一个字段retryable,由技能开发者显式声明是否允许重试。重试策略用指数退避,第一次等1秒,第二次等2秒,最多重试2次。
熔断机制也很重要。某个技能连续失败超过5次,自动暂停该技能一段时间,避免Agent反复卡在同一个失败调用上。这个开关要在运行时可配置,运维人员发现外部系统故障后可以手动恢复。
5.4 权限与安全管控要点
技能执行涉及代码、文件系统、外部API,安全漏洞会直接被模型利用。常见风险和控制手段我列了一个速查表:
| 风险点 | 表现 | 最小化控制手段 |
|---|---|---|
| 代码执行 | 模型生成恶意或错误代码 | 沙箱隔离,限制网络,禁止连接外网 |
| 文件读取 | 模型读取任意服务器文件 | 路径白名单,禁止绝对路径穿越 |
| API凭证 | 技能代码泄露密钥 | 凭据加密存储,运行时注入,日志脱敏 |
| 高危操作 | 误删数据、创建违规资源 | 写操作默认需二次确认 |
| 提示注入 | 外部文本诱导技能执行异常 | 技能内部不执行来自内容的外部指令 |
安全不是做完功能后补的,而是每个技能在定义权限时必须声明清楚。我为每个技能都指定了最小权限模型,查询类只有只读权限,写操作类必须单独标记并走审批链路。这样做虽然增加了配置工作量,但能在模型误操作时避免真正的事故。
6. 技能评估与持续优化
6.1 评估指标体系怎么建
技能体系上线后需要持续评估。我一般关注四个指标:
- 技能调用准确率:在评测集上,模型选中的技能与标注结果一致的比例。这个指标衡量技能描述和模型理解是否匹配。
- 技能执行成功率:技能函数实际执行成功的比例。失败可能是参数错误、依赖服务异常或业务逻辑bug。
- 无效调用率:模型调用技能,但返回结果对最终回复没有帮助的比例。经常发生在“问天气也会带上穿衣建议”这种误用场景。
- 端到端任务完成率:用户需求最终被正确满足的比例。这是业务最关心的指标。
如果技能准确率低,优先优化描述;如果执行成功率低,优先检查参数解析和依赖稳定性;如果无效调用率高,考虑给技能加更严格的触发边界。
6.2 构建评测任务集做离线回放
评测集不用很大,但要有代表性。我从历史用户会话里挑出50条问题,覆盖每个技能至少5个触发场景,并且故意加入几个容易混淆的边界问题。然后把评测集离线跑一遍,让模型基于当前技能列表选工具,自动比较选择结果和标注结果。
简单实现思路是写一个回放脚本:
test_cases = [ {"query": "怎么设置静音", "expected": ["search_knowledge_base"]}, {"query": "帮我查下这两个订单哪个先到", "expected": ["query_order", "query_logistics"]}, ] for case in test_cases: tools = build_tool_schema() selected = model_choose_tool(case["query"], tools) is_match = set(selected) == set(case["expected"]) log(case["query"], selected, is_match)评测集要随业务变化持续更新。每出现一次线上误用案例,就把这条案例加入评测集,防止后续改动又把相同问题带回来。
6.3 日志复盘与技能版本管理
每次技能调用都要记录完整上下文:用户原始问题、模型选择结果、传入参数、执行返回、耗时、错误信息。这些日志是优化技能最宝贵的素材。我每个星期会做一次复盘,把误用样本挑出来,分类为“描述问题”“参数问题”“编排问题”,再逐条优化。
技能同样要做版本管理。我给每个技能加了一个version字段,当description、参数Schema或执行逻辑发生变化时递增版本。特别是描述变了,必须发新版本,不能在同一个技能上直接改动线上描述,否则无法对比新旧版本的准确率差异。兼容规则参照语义化版本:字段增加小版本,字段变更或删除大版本。
在实际运营中,一个技能从上线到稳定通常会经历三轮描述调整。第一轮是刚上线,描述写得太窄,很多真实问题漏触发;第二轮是变宽,又把边界问题误触发;第三轮通过添加反向约束和示例,终于稳定下来。所以不要怕技能描述频繁改,怕的是改完不看效果。
最后分享一个个人经验:技能体系做得好的团队,往往不是那些写了最多技能的团队,而是最会做减法的团队。很多项目初期会放上二十几个“看起来有用”的技能,结果模型常常挑错工具,后来把技能列表从三十个裁到十二个,准确率反而上来了。如果你也要走这条路,克制,可能比努力更重要。先把最核心的十个技能做扎实,再去扩展长尾能力,这套 agent-skills 的思路才能真正发挥价值。