先讲一个我自己的经历。去年我做了一个客服问答Agent,最初把所有功能都塞进系统提示词里:会查订单、会算运费、会推荐商品、会转人工……prompt写了快三千字。上线第一周还行,第二周开始翻车——用户换个问法,Agent就忘了该调用哪个工具;我改了一条规则,另外三个功能跟着出问题。那段时间我每天的工作就是"提示词排雷"。后来我痛定思痛,用两周时间把整个Agent重构了一遍,核心改动只有一件事:把"能力"从对话文本中抽出来,做成独立的skill(技能)模块。今天想把这套设计思路、实现细节和踩坑复盘完整写下来。文章会围绕agent-skills这套方法展开,包括Skill怎么定义、怎么注册、怎么被Agent动态发现和调用,以及多技能协同时的调度与容错。无论你是在做大模型应用、做自动化机器人,还是想给现有产品接上智能体能力,这篇都能给你一套可以直接落地的参考。
1. 从"提示词堆叠"到"技能层设计":Agent能力边界的三个失控信号
1.1 三个失控信号:改一坏十、上下文挤爆、无法测试
先说说我是怎么被逼上这条路的。最初的Agent只有一个system prompt,里面事无巨细列了各种指令。表面看,模型确实"知道"很多,但这种知道是假的。我总结出三个典型的失控信号:
第一,改一坏十。你为了让Agent在某个场景下表现更好,在prompt里加了一段规则,结果它在另一个完全不相关的场景里也开始套用这段规则。因为LLM的指令遵循是概率性的,你给它看的字越多,它越容易把"示例"当成"通用原则"。我加了一条"用户生气时先道歉",结果Agent在所有对话开头都先道歉。
第二,上下文挤爆。功能越加越多,prompt越来越长。长prompt不仅浪费token,更严重的是中段信息会被模型选择性遗忘。有测试表明,prompt超过4千token后,模型对中段的注意力明显下降。你精心写的规则放在第20行,可能根本不会被读到。
第三,无法测试。一个纯文本prompt没有单元测试的概念。你没法单独验证"运费计算规则"是否正确,因为它是和"换货流程""退款策略"混在一起的。每次改动都是全量回归,纯手工,靠感觉。
这三个信号其实指向同一个问题:Agent的能力被写成了散文,而不是被结构化成模块。散文改起来牵一发动全身,模块才能单个替换、单独验证。
1.2 Skills把"能力"从"对话文本"里抽出来
"技能层设计"的出发点很简单:把Agent能做的事拆成一个一个独立的skill,每个skill有名字、有描述、有输入输出契约、有可执行的逻辑体。LLM的角色从"记住所有规则"变成"根据用户需求,从技能目录里选择合适的技能并调用"。这就像人不需要把微波炉说明书背在脑子里——想热饭的时候,找到微波炉,按几个按钮就行。你不需要知道微波炉内部的磁控管怎么工作,你只需要知道它能热饭、怎么把饭放进去、按哪几个键。
对应到Agent上,每个skill就是一个"微波炉"。LLM只需要知道这个技能是干什么的、输入什么、输出什么,不需要把技能内部的实现细节灌进上下文。这样有几个立竿见影的好处:
- 单个技能可以独立测试、独立迭代,不影响其他技能。
- 技能的调用逻辑是确定性的代码,不是概率性的prompt,稳定性大幅提升。
- 上下文里只放"技能目录"和"当前会用到的技能定义",其他技能的细节按需加载。
1.3 与Function Calling/Tooling的关系:不是替代而是组织
这里要澄清一个常见混淆:skill和函数调用(Function Calling)、工具(Tool)到底什么关系?
我的理解是:Function Calling是底座机制,Tool是单次调用单元,Skill是面向任务的完整能力封装。一个Skill可以由多个Tool组合而成,也可以携带状态、内部校验、重试逻辑。举个例子,"创建定时任务"这个skill,内部可能需要调用parse_time(时间解析)、schedule_task(任务入库)、send_notification(通知用户)三个工具函数。直接把三个函数暴露给LLM,它很可能会漏掉其中一步;封装成skill之后,LLM只需要说"我要创建一个定时任务",剩下的流程由skill内部的代码按顺序执行。
| 维度 | 纯提示词方案 | 技能层方案 |
|---|---|---|
| 能力存储 | 散文式文本 | 结构化模块 |
| 上下文占用 | 随功能线性增长 | 只暴露目录和当前技能 |
| 修改影响面 | 改一句动全局 | 改一个skill不影响其他 |
| 可测试性 | 几乎不可测 | 每个skill可单独测试 |
| 升级方式 | 重写prompt | 替换skill模块 |
所以,agent-skills不是一个独立的算法,它是一套组织代码和prompt的架构方法。底层模型还是那个模型,但你把"它该怎么表现"变成了"它能调用什么模块",这是两种完全不同的工程路径。
2. Skill的最小解剖:描述、参数契约与执行体的设计红线
2.1 Skill的六个构成零件
一个可以被Agent稳定使用的skill,我在实践中至少包含六个部分。缺任何一块,都会在线上暴露出问题。
- name:技能的唯一标识,英文小写加下划线,如
scheduler、web_fetcher。这个名字不用给用户看,但LLM会通过它来引用技能。 - description:给LLM看的能力说明。这是整个skill里最重要的一块,直接决定"模型能不能在正确的时候选中它"。后面单独说怎么写。
- triggers:触发条件的补充说明,描述什么场景下必须用、什么场景下千万别用。很多初学者忽略这一块,结果模型在"查询天气"时调用了"订机票"技能。
- parameters:入参契约,我统一用JSON Schema定义,支持类型、必填项、枚举值、描述。参数契约越清晰,模型的"自由发挥"空间就越小。
- handler:执行体。我坚持一个原则——执行体必须是确定性代码。也就是说,给定一组入参,输出是确定的,不做任何模糊发散。
- exit_callback:执行完之后的回调。通常是把结果格式化后返回给LLM,让它组织成人话回复用户。也有人把这步并入handler,但单独拆出来会更清晰,因为给LLM看的结果和用户看到的结果往往需要不同形态。
2.2 面向LLM的描述文案:写"什么时候用"而不是"是什么"
描述怎么写,决定了检索准确率。我走过一段弯路:一开始我把描述写成了"这是什么",比如"这个技能可以用来解析用户输入的日期"。听起来没毛病,但实际命中率很差。问题在于LLM做技能选择时,它是一个"语义匹配"过程——它看到用户说"帮我定个闹钟",要去描述库里找最像的那条。
改成"什么时候用"之后,效果立刻不一样了。比如:
- 差:「解析用户输入的日期时间字符串,支持多种格式。」
- 好:「当用户提到明天、后天、下午三点、下周一等任何时间表述,并需要基于该时间执行任务(如提醒、定时、闹钟)时使用。」
注意第二句出现了大量"用户可能怎么说话"的词汇和场景。这本质上是把技能描述写成了检索索引的摘要文本,让用户query和description之间的语义距离更近。我后来还会要求描述里必须出现至少两个"用户原话可能长这样"的例子,比如"提醒我明天9点开会"、"每两个小时采集一次数据"。
2.3 参数契约:JSON Schema与严格校验
给LLM的参数定义越宽松,出脏数据的概率越高。我早期的某个技能允许模型自由传入逗号分隔字符串,结果模型传出了各种诡异的格式,下游解析直接崩溃。后来我统一改用严格JSON Schema,每个字段强制声明类型和必填性,能列枚举就列枚举。
拿定时任务里的"重复规则"举例:
{ "type": "object", "properties": { "task_name": { "type": "string", "minLength": 1, "maxLength": 50, "description": "任务名称,如:每日数据备份" }, "when": { "type": "string", "description": "执行时间,尽量传ISO 8601格式,如2025-07-01T09:00:00+08:00" }, "repeat": { "type": "string", "enum": ["once", "hourly", "daily", "weekly", "monthly"], "default": "once" }, "command": { "type": "string", "description": "要执行的动作说明,支持命令或指令文本" } }, "required": ["task_name", "when", "command"] }我在handler里还会做一道"物理校验":时间能不能解析、日期是不是过去、任务名有没有违禁词。模型传过来的参数要当成"不可信的第三方输入"来对待,不能因为是模型传的就默认正确。
2.4 执行体的纯度原则
这是我在重构过程中领悟最深的一条:skill内部能不用LLM就不用LLM。原因很简单——LLM调用意味着延迟、成本和不确定性,而你封装skill的目的恰恰是为了消灭这三样东西。
例如用户问"查一下我的订单",正确链路是:LLM识别意图,返回{"skill": "order_query", "params": {"order_id": "..."}},然后skill内部直接查数据库、算状态矩阵、返回固定格式结果。如果把"判断订单状态"也丢给LLM做,等于把同一个模型的概率性问题搬到了你已经确定要执行的地方,完全得不偿失。
当然有些任务是纯LLM更擅长的,比如"总结一篇长文"。对这种任务,我通常把"是否调用LLM"做成skill内部的一个策略开关,而不是让外部模型替我做判断。边界在skill内部可控,而不要扩散到全局。
3. 手写一个"定时任务管理"Skill并接入Agent的全过程
3.1 需求界定与数据设计
理论部分讲太多容易飘,我们落地一个实战例子:做一个定时任务管理skill,要求Agent能创建、查询、取消定时任务。先定义数据模型。
我用的存储就是最简单的JSON持久化,生产环境可以换成SQLite。字段设计如下:
import json, os, uuid from datetime import datetime TASKS_FILE = "tasks.json" def load_tasks(): if not os.path.exists(TASKS_FILE): return {} with open(TASKS_FILE, "r", encoding="utf-8") as f: return json.load(f) def save_tasks(tasks): with open(TASKS_FILE, "w", encoding="utf-8") as f: json.dump(tasks, f, ensure_ascii=False, indent=2)一个任务记录长这样:
{ "task_id": "a1b2c3d4", "task_name": "每小时备份数据库", "when": "2025-07-01T09:00:00+08:00", "repeat": "hourly", "command": "backup_db --target /data/backup", "status": "active", "created_by": "user_001", "created_at": "2025-06-30T18:22:10+08:00" }我不在这里实现真正的定时触发(那是调度器的事),只做任务的增删改查,这正好演示skill的"确定性逻辑体"部分。
3.2 三个核心动作的实现代码
接下来实现create_task、list_tasks、cancel_task三个函数。注意每个函数都尽量返回一段JSON字符串,这个字符串是给LLM看的。LLM拿到JSON,再组织成自然语言回复用户。
class SchedulerSkill: name = "scheduler" description = ( "当用户需要创建、查看、取消定时任务时使用。" "包括不限于:提醒我明天上午9点开会、每天中午12点备份数据、" "查一下我有哪些定时任务、把那个备份任务取消。" ) triggers = ( "必须使用:用户明确表达需要某个任务在指定时间自动执行," "或需要管理已有的定时任务。" "禁止使用:用户只是闲聊提到时间,没有执行意图时不要调用。" ) def create_task(self, task_name: str, when: str, command: str, repeat: str = "once"): try: parsed_time = datetime.fromisoformat(when) except ValueError: return json.dumps({"success": False, "error": "时间格式无法解析,请要求用户提供更具体的时间"}) if parsed_time < datetime.now(parsed_time.tzinfo): return json.dumps({"success": False, "error": "任务时间不能早于当前时间"}) tasks = load_tasks() task_id = uuid.uuid4().hex[:8] tasks[task_id] = { "task_id": task_id, "task_name": task_name, "when": when, "repeat": repeat, "command": command, "status": "active", "created_at": datetime.now().isoformat() } save_tasks(tasks) return json.dumps({"success": True, "task_id": task_id, "status": "created"}) def list_tasks(self): tasks = load_tasks() active = [t for t in tasks.values() if t["status"] == "active"] return json.dumps({"success": True, "tasks": active}) def cancel_task(self, task_id: str): tasks = load_tasks() if task_id not in tasks: return json.dumps({"success": False, "error": "任务ID不存在"}) tasks[task_id]["status"] = "cancelled" save_tasks(tasks) return json.dumps({"success": True, "task_id": task_id, "status": "cancelled"})注意我把每个函数的入参都定义成具名参数,而不是一个泛化的kwargs字典。这样后续做参数schema时,可以直接从函数签名生成,减少手写不一致的问题。
3.3 注册与接线的完整链路
写完了核心逻辑,还得让Agent能调用它。这里我做了两件事:
第一步,把函数参数定义转成JSON Schema。我用的是Python的inspect库自动生成,再手动补描述。
import inspect, json def generate_schema(func): sig = inspect.signature(func) properties = {} required = [] for name, param in sig.parameters.items(): if name == "self": continue properties[name] = {"type": "string", "description": f"{name}参数"} if param.default is inspect.Parameter.empty: required.append(name) elif param.default is not None: properties[name]["default"] = param.default return { "type": "function", "function": { "name": func.__name__, "parameters": { "type": "object", "properties": properties, "required": required } } }第二步,把Skill注册表接入Agent的推理循环。我这里用一个极简的Registry,先把skill实例挂进来,然后每次对话时把"技能目录"拼进system prompt。
class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill): self._skills[skill.name] = skill def get_manifest(self): return {name: skill.description for name, skill in self._skills.items()} def execute(self, skill_name, func_name, params): skill = self._skills.get(skill_name) if not skill: return json.dumps({"error": "skill not found"}) handler = getattr(skill, func_name, None) if not handler: return json.dumps({"error": "handler not found"}) return handler(**params)接入后的system prompt只保留一小段说明:
可用技能目录: - scheduler:定时任务管理。当用户需要创建、查看、取消定时任务时使用。 你需要调用技能时,返回JSON格式:{"skill": "scheduler", "func": "create_task", "params": {...}} 我会执行后将结果返回给你,由你组织成对用户友好的回复。这里的关键是:我不在prompt里写scheduler的具体参数格式。模型只需要先把用户意图映射到某个技能的某个函数上,参数抽取是下一步的事。如果你同时暴露所有的参数定义,模型反而更容易在抽取时出错。
3.4 多轮对话测试与边界问法
接好线路之后,我一般会跑一组多轮对话用例,覆盖各种问法。摘几个印象深刻的:
- 用户说"明天下午3点提醒我开周会"——模型正确调用
create_task,when被转成2025-07-02T15:00:00+08:00。这个转换是LLM完成的,为了降低出错率,我在参数描述里明确给了ISO8601示例。 - 用户问"我有哪些任务"——模型调用
list_tasks,拿到JSON后组织成"你有2个活跃任务:每小时备份数据库、每日清理临时文件。"这种回答。 - 用户说"把那个备份取消了"——这里有个坑,模型不知道具体task_id,如果直接调用
cancel_task就会参数缺失。我后来在triggers里加了一条规则:"当用户未明确给出任务ID时,先调用list_tasks查询,再把任务列表呈现给用户,请用户确认后再取消。"这样一来,"取消动作"被拆成了查询加确认两个回合,反而比一次性尝试更可靠。
边界问法原则:宁可让模型多问一轮,也不要让它瞎猜参数。瞎猜是skill被"打回原形"的最常见原因——原本已经脱离概率的确定性逻辑,又被不确定的参数拖入了泥潭。
4. Registry与动态装载:上百个技能共存而不撑爆上下文的实践
4.1 Registry:内存字典还是持久化存储
当你的Agent只有两三个技能时,内存字典就够了。但技能数超过二十后,我强烈建议把注册表做成持久化存储。原因有两个:一是热更新需要,你上线一个新版本技能,不能靠重启进程来生效;二是审计需要,你总得知道一段时间内哪些技能被调用过、成功率如何。
我的设计方案是分两层:上层是内存缓存(继承自dict的SkillRegistry),下层是SQLite表。
CREATE TABLE IF NOT EXISTS skills ( name TEXT PRIMARY KEY, version TEXT, description TEXT, triggers TEXT, module_path TEXT, enabled INTEGER DEFAULT 1, updated_at TEXT );启动时从表里加载enabled=1的技能注册进内存。修改技能后,既可以重启加载,也可以做一遍增量reload——把新版本重新register一次覆盖旧版本。这种事在纯prompt方案里完全做不到,你现在知道模块化的好处在哪了。
4.2 给LLM一份"好用的检索目录"
当技能数量上去之后,"模型能不能一眼选中正确的技能"就变成了核心指标。我实测过一个数据:在没优化描述之前,技能命中率只有62%。也就是说,一百次请求里将近四十次,模型要么选错技能,要么犹豫不决反复问用户。优化描述后,命中率提到了91%。这不依赖模型升级,纯粹是描述文案的工程优化。
优化方法就一条:把技能描述当成搜索索引来写,而不是技术文档来写。我分享一个模板:
当用户{描述具体场景},并期望{描述具体动作}时使用此技能。 必须使用的信号:{列2-3个用户原话示例}。 禁止使用:{列1-2个容易混淆的场景,明确划清边界}。 示例输入:{给一个LLM可以直接参考的入参示例}。"禁止使用"这一段很关键。模型犯错的常见模式不是"在无关处召唤技能",而是"在疑似相关处召错了技能"。比如"设定每周五下午清理日志"这个需求可能会被模型同时匹配到logger_cleaner和task_scheduler。如果你在logger_cleaner的禁止使用里写"当用户要求的是未来某个时间自动执行清理时,不要用本技能,改用scheduler",误选就会显著减少。
4.3 按需装载:先看目录再翻详情
暴露给LLM的信息量,我用了一个"两级曝光"策略:
- 第一级,只暴露技能目录(name + 一两句description),这部分常驻system prompt。
- 第二级,当模型决定调用某个技能时,我再把该技能的完整JSON Schema塞进这一轮请求的tool definitions里。
这样做的收益很直观。我做过一次对比:一个加载了50个技能的Agent,如果把每个技能的完整schema都塞进上下文,光工具定义就消耗约12000 token;用两级曝光后,常驻上下文只占约1500 token,只有当某个技能被选中时才额外加载。差异接近十倍。
| 方案 | 常驻Token | 模型选错率 | 响应速度 |
|---|---|---|---|
| 全部技能定义常驻 | 约12000 | 较高(干扰项多) | 慢,首字延迟明显 |
| 两级曝光(目录+按需加载) | 约1500 | 显著降低 | 快,上下文更清爽 |
两级曝光还有个隐藏好处:它倒逼你把目录描述写得好。如果你知道模型只靠这句话去做选择,你自然会在"什么时候用/什么时候不用"上花足功夫。
5. 多Skill协同调度:编排、冲突仲裁、失败回退与死循环治理
5.1 链式、分支、并行的三种编排模式
一个稍微复杂一点的任务,往往不是一个skill能搞定的。比如"每天早上9点检查服务器磁盘,如果超过80%就发告警到钉钉群",这里面就涉及定时任务、检查命令、告警通知三个技能。我常用的编排模式有三种:
- 链式编排:A的输出作为B的输入。典型场景是"先查询订单状态,再根据状态触发退款或催发货"。这种模式最可靠,我通常把链路预置进一个"流程skill"里,而不是让LLM自由编排。
- 条件分支:根据某个确定性判断走不同分支。比如"订单状态为已发货则调用物流查询,为未付款则调用支付提醒"。这种判断我在代码里用if-else写死,不交给LLM。
- 并行汇聚:多个无依赖的查询同时发起,比如需要同时查天气、查航班、查酒店再汇总行程建议。并行能显著降低总延迟,但要注意LLM在等待多个结果时需要"挂起"状态,工程上有一定复杂度。
一个常被忽略的点:很多"多技能协同"场景,其实不需要模型参与。如果你明确知道步骤顺序,就把它固化成一个复合skill。LLM介入得越少,系统越稳定。
5.2 技能冲突仲裁:谁更具体谁上
当用户表述比较模糊,Model可能同时匹配到两个技能。比如"帮我把这份文档翻译成英文并总结要点",理论上可以有translate_doc和summarize_doc两个技能。我的仲裁策略是按特异度打分:
- 用户原话命中技能示例句子的个数,每个+1分。
- 技能描述里的"必须使用"关键字命中的,+2分。
- 两个技能打分相同,默认优先执行"处理面更窄"的那个。因为窄技能通常有针对性,而宽技能往往需要额外处理。
在这个例子里,translate_doc的示例如果包含"把文档翻译成英文",而summarize_doc只包含"总结文档要点",那么翻译技能分数更高,先翻译再总结。如果模型识别到两步都要做,那就走链式编排,而不是二选一。
5.3 失败回退链路设计
Skill执行失败时,最忌讳的是让整个Agent直接断掉。我设计的回退链路分四档:
第一档,重试。网络波动、临时超时这类可恢复错误,自动重试一次,隔几秒再执行。
第二档,降级。主技能失败,换一个功能相近的备用技能。比如web_fetcher抓取失败,降级到web_fetcher_simple用另一种请求方式。
第三档,交还LLM决策。把错误信息原样打包返回给模型,由模型根据上下文决定是换一种问法、还是让用户补充信息、还是给出替代方案。这是"Agent感"的来源——它不是死板的报错,而是像人一样"换个思路"。
第四档,人工兜底。连续两次失败,直接转人工,并把完整的错误调用链附上。日志里必须能看到"模型选择了哪个技能、传了什么参数、底层报了什么错",否则转人工等于扔黑盒给客服。
5.4 死循环与成本失控的治理
多技能协同最大的隐藏风险是无限循环。模型可能在一个失败回退里反复选择同一个技能,或者AB两个技能互相激活。我上过线之后专门做了四道闸门:
- 最大迭代次数:一次用户请求最多执行5次工具调用,超过就强制结束并回复"当前无法完成,已为您转接人工协助"。
- 重复调用检测:如果模型在连续3轮内调用了同一个技能且入参完全相同,说明陷入死循环,直接中断。
- 时间预算:整个Agent响应延迟上限设成20秒,超时即返回当前已获得的部分结果。
- 成本预算:每次请求往上限住,按token消耗或按调用次数,防止某个失控逻辑烧掉API账单。
这四道闸门不是可选项,是技能线上跑起来之后的生存底线。只在demo里玩的Agent可以不管,但要放到真实用户面前,一个死循环就能打崩整条链路。
6. 上线一周后的四个踩坑复盘与技能层铁律
6.1 踩坑一:描述越"漂亮",选错率越高
我第二个技能是"天气查询"。一开始描述写得特别华丽:"实时获取全球任意城市的天气信息,支持温度、湿度、风速、紫外线指数、空气质量、未来七天预报……"结果模型开始了花式滥用:用户问"明天适合爬山吗"会触发它,用户问"上海今天冷不冷"也触发它,甚至用户问"帮我看看附近有什么餐厅"它都能绕到天气上——因为模型觉得"餐厅"需要考虑下雨。后来我学乖了,把描述改成带"必须使用"和"禁止使用"的朴素版本,命中率反而从76%直接干到了94%。
所以,skill描述不是营销文案,它是给模型看的路由规则。该用的时候狠一点,不该用的时候也要坚决地说"不"。
6.2 踩坑二:工具定义膨胀吃掉上下文
前期我没做两级曝光,把所有技能的定义全部塞进请求。24个技能、18个工具函数,两万多token。效果反而很差——模型在分析"该用哪个工具"时自己先"蒙圈"了,偶尔还会把不同工具的参数混在一起传。后来切到"目录+按需加载",上下文瘦身90%,选错率直接降了一半。这个教训让我彻底明白:上下文不是用来跟模型闲聊的,省下来的token要留给真正的用户对话和推理。
6.3 踩坑三:参数校验松,脏数据进下游
有个临时写的小技能,接收"文件名"参数,我图省事没做校验。结果模型在几次对话里传出了带路径穿越符的字符串,下游模块尝试读文件时差点读到系统目录。事后我补了两道防线:第一道,handler入口统一过白名单校验,文件名禁止包含/、\、..等字符;第二道,把参数schema里的每个字段都标上pattern正则限制。虽然模型传脏数据的概率不高,但一旦传到核心系统,代价是毁灭性的。技能层的每个入参,都要当作外部用户的恶意输入来设计。
6.4 踩坑四:没有评估指标,优化全凭感觉
重构完成后的前两周,我遇到一个问题:某个技能在新版本prompt调整后,表现到底是变好了还是变差了?我的"感觉"在第三天失灵了——隐约觉得变差了,但没有数据佐证。于是我建了一个最简评估表,每一条线上调用记录一行:用户原话、模型选中的技能、参数抽取是否正确、handler是否报错、用户是否追问。跑了一周,统计出三个核心指标:
- 技能命中率:用户意图与所选技能的匹配度,我目标定在90%以上。
- 一次成功率:第一次调用就完成且用户没追问的比例。
- 修正率:需要模型再次调用同一技能或换技能的占比。
有了这些数字,技能优化就从"猜"变成了"查"。哪条用户原话修正率高,就把那条原话的变体加进对应技能的示例里。一周下来,"我有哪些没付款的订单"这类问题的一次成功率,从79%提到了96%。
6.5 我给自己定的三条技能层铁律
踩完这些坑,我给自己定了三条规则,现在每次设计新技能都会先过一遍:
第一,一个技能只做一件事,能做到80分就不贪多。想同时管理任务又管理日历?拆开。模型的意图识别在单意图上最准,多意图场景很容易走神。
第二,凡是代码能写死的行为,绝不让LLM自由发挥。执行顺序、判断条件、异常处理都放代码里。LLM只做"选技能、填参数"两件事。
第三,每个技能都要有止损线。最大调用次数、超时时间、失败降级方案,写在skill定义里。没有止损线的技能上线,就等于把一个定时炸弹装进Agent肚子里。
最后再分享一个我觉得最值钱的实用小技巧。找半天选技能不准,有时不是描述问题,而是你的用户query被前面的对话污染了。你会看到模型把"用户上一句提到的天气"当成"当前这句要查的天气"。解决方法是建立会话内的意图快照——在每轮工具调用的上下文里,单独维护一个字段,记录"当前正在处理的意图"和"已经获取到的关键参数"。模型看这个快照,而不是自己去翻海量对话历史,技能选择的准确率会明显提升。我自己的项目里,只加了这一层,命中率又往上走了好几个点。这套agent-skills体系,不是一次建完就一劳永逸的,它需要你像维护代码库一样持续迭代每个技能。但换个角度想,这也正是它迷人的地方——Agent的能力边界,终于变得可以被你稳稳握在手里了。