最近一直在折腾 agent-skills 相关的工程化方案,说白了就是琢磨怎么让大模型从“只会聊天”变成“能稳定干活”。这半年下来,踩过的坑、推倒重来的设计都不少。今天不写抽象理论,把我实际用下来的一套技能体系设计思路、关键实现细节,以及几次印象深刻的排障经历,一次性整理出来。
1. 为什么需要“技能”这层壳:整体设计思路拆解
1.1 从“会聊天”到“会干活”的断层
先聊一个很现实的场景。你让模型直接调用一个天气 API,它可能生成了接口地址,但参数格式错了;你让它查数据库,它三天两头编造字段名;你让它连续多步操作,它经常在第二步就把前面拿到的数据丢得干干净净。这些问题不是模型“笨”,而是你给了它一个裸模型,却没给它可以稳定依赖的工具抽象。
我做 Agent 应用的一条铁律是:模型负责决策,工程负责执行。而技能层,正是“决策”和“执行”之间的那个接缝。大模型本质上是个超大型语言模型,最擅长的是预测下一个 token。你问它问题,它给你一个看起来合理的回答;可一旦让它去完成“查询订单状态—核对库存—生成发货单”这种多步骤任务,每一步都涉及外部系统交互,每一步的返回结果都是实时变化的。这时候如果所有逻辑都堆在提示词里,模型既要记业务规则,又要记工具用法,还要处理各种返回数据,上下文稍微长一点就开始“放飞自我”。
我在实际项目里见过最夸张的一次,模型把接口返回的订单号当成了库存数量去参与计算——因为它根本不知道自己调用的函数返回了什么语义。说白了,裸模型拿到的是一堆孤立函数的字符串描述,它对这些函数之间的逻辑关系和业务含义一无所知。技能这个概念的引入,就是要把这些碎片化的函数调用、业务规则、数据处理逻辑,组织成一个个有明确边界、有清晰语义的“独立工种”。
1.2 技能的本质:把不确定装进确定
所以我最终把技能(skill)看作一个“封装单元”,它把三样东西绑在一起:一段能被模型理解的功能描述、一组结构化入参出参定义、一段可执行的业务逻辑。模型不需要知道技能内部怎么实现,它只需要根据描述判断“要不要用这个技能”,然后把参数填对,剩下的交给技能本身去执行。
这个设计最大的好处,是把不确定性关进了笼子里。模型的自由度被严格限制在“选技能、填参数”这两件事上,而这两件事恰恰是模型相对擅长、也最容易用工程手段去约束的。技能内部则是完全确定性的代码,连接数据库、调用 API、处理文件、做计算,每一步都是可预期的、可测试的。你可能会说,这不就是把一个函数包了一层壳吗?确实,但这一层壳决定了模型能不能正确找到你写的函数。
一开始我也觉得奇怪:“不就是给函数写了段注释吗,为什么效果差别那么大?”后来发现问题根本不在“有没有注释”,而在“描述的质量”。传统编程里,函数名和注释是给人看的,人本身有常识和上下文,能猜个八九不离十;但在 Agent 体系里,这段描述是给模型看的,模型没有任何隐性上下文,它只能靠你写在 description 里的字面信息来做判断。同样的功能,你写“获取天气信息”和写“根据城市名称返回实时天气及未来三天预报,当用户询问天气相关问题时使用”,后者在实测中的调用准确率能高出二三十个百分点。这二十个百分点,就是工程化设计带来的直接收益。
1.3 一个通用技能框架的模块划分
我沉淀下来的技能框架,最终分成四个层次:技能描述层、技能注册层、技能执行层、技能运行环境。
技能描述层就是上面说的元数据,包括技能的唯一标识、简短的功能描述、入参出参的 Schema。这一层面向模型,核心目标是降低调用的误判率。技能注册层维护一份“技能清单”,相当于公司花名册,模型需要某个能力时先来这里查一查,确认技能存在且可用。技能执行层是真正干活的部分,接收已经校验过的参数,调用外部工具,返回标准化结果。技能运行环境则负责资源隔离、超时控制、日志采集这些“不那么性感、但少一个就出事故”的底层能力。
这四个层次从设计上就是松耦合的:你可以在描述层不断调优措辞,完全不影响执行层代码;你也可以替换执行层的实现,从模拟数据切到真实 API,而不需要改描述。我经常跟伙伴们说,技能体系的设计目标不是“写出一堆函数”,而是“让技能像乐高积木一样可以拼装、替换、扩展”。当技能数量多起来之后,这种模块化的好处会指数级放大——不是每个新功能都要从零开始写代码,而是从现有技能中挑选、组合、微调,效率完全不一样。
2. 技能设计与核心实现细节
2.1 技能元数据:你给模型看的“说明书”
这是整个技能体系的灵魂。我把技能元数据设计为统一的 JSON 结构:
- name:技能唯一名称,建议用“动词_对象”的格式,比如 search_documents、cancel_order。
- description:一段自然语言描述,模型靠它判断是否调用。
- parameters:JSON Schema 格式的入参声明。
- returns:出参结构声明。
- timeout:超时时间,属于运行层信息。
- version:技能版本号。
技能命名的规范不是小事。技能名会被模型作为工具名填入调用请求里,命名越直观,模型选错的概率越低。我早期吃过亏,把一个“订单取消”技能命名为 order_cancellation_v2_quick,结果模型在用户要求“退单”的时候反复挑选其他名称更贴近的技能,最后才勉强选中它。改成 cancel_order 之后,调用率立刻上来了。从这里我学到一个经验:给技能命名,要假设模型对业务一无所知,它只能从字面意思里猜用途,名字就是第一道引导。
description 的写法也有一些经过验证的技巧。我总结出三个要点:第一,开头第一句话直接说明这个技能是干嘛的,不要铺垫;第二,列举典型的触发场景,给模型提供判断锚点;第三,如果技能有适用边界,一定要写清楚“不处理什么”,避免模型滥用。比如做摘要技能的描述可以这样写:“对给定的长文本生成不超过 500 字的中文摘要。适用于会议纪要、新闻文章、政策文档等长文本的要点提炼。不适用于问答、翻译、关键词提取等任务。”这样写,模型在“帮我总结一下这个会议纪要”时就会优先想到它,而在“这段翻译成英文”时就不会硬套它。
2.2 参数声明与校验:把坑挡在进门之前
参数设计上,我强制使用 JSON Schema 来声明入参。很多人觉得这一步多此一举,反正模型生成的参数合不合法,执行的时候验证一下就行。但我实测下来,参数校验前置化是成本最低、收益最大的优化方式。因为模型在生成调用请求的时候,会参照你提供的 Schema 来构造参数。如果你只写“params:需要城市名”,模型可能传 city,可能传 location,还有可能把“北京”和“北京市”同时传进来;但如果你声明了 enum 或者明确的 type 和 description,模型生成参数的合规率会显著提升。
JSON Schema 里有两个字段特别值得重视:一个是 description,写在每个字段上,给模型详细解释这个参数应该填什么;另一个是 required,明确告诉模型哪些参数是必须的。举个例子:
- 参数名 city:城市名称,使用标准中文名(例如“北京”“上海”),必填。
- 参数名 days:预报天数,可选,整数,取值范围 1 到 7。
实践下来,这种带语义的 Schema 声明比“宽松入参、执行时兜底”的方案稳定得多。兜底逻辑还是要写的,模型毕竟不保证 100% 遵守规则,但要把它当成“最后一道防线”,而不是“主要依赖”。校验返回的错误信息也要设计得对模型友好,比如“参数 city 必须为字符串类型,当前收到 int 类型”,模型看到这条信息后,下一轮往往就会自我纠错,重新生成正确的参数。
2.3 技能执行体的分层实现
技能执行体建议按三个层次去写:入口层、校验层、业务逻辑层。
入口层做的事情很少:接收模型传过来的 JSON 参数,把参数转换成内部对象。校验层负责对参数做严格的合法性检查,类型不对就抛异常返回给模型,而不是让它一头扎进业务逻辑里。业务逻辑层则是真正干活的地方,调用 API、访问数据库、组装返回结果。
这里有个容易忽略的点:业务逻辑层返回给模型的结果,不一定是原始的 API 返回。模型能理解的是文本和结构化数据,但它对超长 JSON 的解析能力是有限的。所以执行体最后要有一个“结果归一化”处理:把原始返回精简成模型决策所需的最小信息量。比如调用一个查询天气的 API,原始返回可能有湿度、风速、气压、紫外线指数、降水概率等二十多个字段,但模型下一步可能只需要“今天是否下雨”。那就直接在技能内部把结果压缩成“今天有雨,建议带伞”这样的中文短句,模型拿到之后直接就能用,不需要再从一团 JSON 里翻找、理解、转述。
我一直强调一个观点:技能的输出,不是给程序看的,是给模型看的。程序可以直接消费原始 JSON,但模型不行。它需要的是“语义浓缩后的反馈”,而不是“结构完整但信息庞杂的数据转储”。
2.4 技能组合与编排思路
单技能只是第一步,真正的 Agent 应用需要技能协作。我的编排思路是:把技能连接成“技能链”,模型在多步任务中依次调用不同技能,前一个技能的输出作为后一个技能的输入。这个思路在实际落地中有两个重要注意点。
第一,技能间的数据流要显式传递,不要在技能内部藏着全局变量。模型在多步操作中需要看到上一步的结果才能做出下一步决策,如果数据被藏在技能内部的黑盒里,模型看不到,它就无从决策。第二,每步返回给模型的信息都要标注“这一步做到了什么、拿到了什么”,让模型清楚自己当前处于整个任务链的什么位置。否则模型很容易在前两步结果之间迷失,最后输出一个张冠李戴的报告。
我还尝试过把编排逻辑放在“技能调度器”里,由调度器决定调用顺序,而不是完全交托给模型。这种方式适合流程固定的场景,比如“先查库存再生成发货单”;而完全开放的方式适合探索型任务,比如“帮我看看这个领域有什么最新研究进展”。两种模式各有适用场景,一个成熟的 Agent 系统里,两者应该并存——用调度器兜住固定流程的下限,用模型的自由发挥兜住开放性任务的上限。
3. 实操:从零到一实现一个可用的技能
3.1 需求拆解与技能选型
用一个实际例子来演示。某团队想做一个“自动检索资料并写简报”的 Agent,用户给它一个主题,它需要完成三件事:搜索相关资料、从资料中提取要点、按简报格式输出。
这个需求看起来不复杂,但拆解之后会发现,如果写成“一个技能干所有事”,技能的 description 会非常冗长,模型反而不知道从何下手。正确的拆法是把任务切成三个子技能:
- search_info:根据主题检索资料,返回资料列表及摘要。
- extract_key_points:对给定文本提取关键要点,返回要点列表。
- generate_brief:基于要点列表生成简报。
三个技能职责单一、边界清晰,模型在多步任务中会依次调用,每一步的返回都只包含下一步所需的信息,整个链路非常干净。选型时我还有一个判断标准:如果某个技能的 description 超过三句话才说清楚,就该考虑拆分了。一个技能只做一件事,是技能设计的第一原则。
3.2 写一个“资料检索并生成摘要”技能
下面给出一个简化的 search_info 技能实现,用 Python 写。需要说明的是,这个示例把外部 API 调用部分做了抽象,实际接入时换成真实搜索服务的接口即可。
import json import requests from jsonschema import validate class SearchSkillError(Exception): pass class SearchInfoSkill: name = "search_info" description = "根据主题检索相关资料并生成摘要列表。适用于需要获取特定主题背景信息、文献资料、行业动态等场景。" parameters_schema = { "type": "object", "properties": { "topic": { "type": "string", "description": "需要检索的主题关键词,例如:神经网络压缩、碳达峰政策", }, "limit": { "type": "integer", "description": "需要返回的检索结果数量,默认5,最大10", "minimum": 1, "maximum": 10 } }, "required": ["topic"] } def execute(self, params: dict) -> dict: # 参数校验 try: validate(instance=params, schema=self.parameters_schema) except Exception as e: raise SearchSkillError(f"参数校验失败: {str(e)}") topic = params["topic"] limit = params.get("limit", 5) # 估算权重:此处以模拟数据代替真实检索服务 results = self._search(topic, limit) # 结果归一化,只保留模型决策所需要的信息 normalized = [] for r in results[:limit]: normalized.append({ "title": r["title"], "summary": r["summary"], "source": r["source"] }) return {"status": "success", "results": normalized} def _search(self, topic: str, limit: int) -> list: # 真实场景中接入搜索服务 API,示例仅返回模拟数据 mock_data = [ {"title": f"{topic}相关研究综述", "summary": "本文概述了该领域的关键进展与核心争议点,适合作为背景阅读。", "source": "示例库A"}, {"title": f"{topic}技术实践报告", "summary": "从工程角度介绍了落地实现方法、性能评估与注意事项。", "source": "示例库B"}, {"title": f"{topic}行业趋势分析", "summary": "总结了近期行业动态、主要参与者与未来趋势判断。", "source": "示例库C"}, ] return mock_data这个示例里有几个设计细节值得说明。
先看名称和描述:技能名 search_info 很短,description 里写了“检索相关资料并生成摘要列表”,并列举了两个典型的触发场景。模型在 function calling 机制中会把这个描述作为一个候选工具去匹配用户意图,描述写得越贴合,匹配就越准。
再看参数 Schema:topic 是必填的,limit 选填且有范围限制。JSON Schema 的 validate 函数在 execute 入口直接对参数做校验,如果模型生成参数不合法,会直接抛错,而不是带着非法参数去访问外部接口。
最后看结果归一化:真实搜索接口通常会返回非常多字段,但我只取了 title、summary、source 三个字段。这三个字段已经足够模型进行下一步“提取要点”了,而且能有效降低上下文噪音。这一步是我在实际运维中反复优化过的——一开始我偷懒直接把原始返回扔给模型,结果模型经常被一堆无关字段干扰,生成的报告质量忽高忽低。改成精简输出之后,稳定性明显提升。
3.3 注册技能并接入 Agent 主循环
有了技能类之后,下一步是把它注册进技能清单。我维护一个全局技能注册表,启动时扫描并加载所有技能,这样 Agent 在运行过程中可以随时查询有哪些可用技能。
skills_registry = {} def register_skill(skill_cls): instance = skill_cls() skills_registry[instance.name] = instance return instance def get_all_skill_definitions(): return [ { "name": skill.name, "description": skill.description, "parameters": skill.parameters_schema } for skill in skills_registry.values() ]Agent 主循环里,模型会收到“目前可用技能列表”,根据用户的自然语言指令决定调用哪个技能。调用时,Agent 框架把模型生成的目标技能名称和参数传给对应技能实例:
def call_skill(skill_name: str, params: dict) -> dict: if skill_name not in skills_registry: return {"status": "error", "message": f"技能 {skill_name} 不存在"} skill = skills_registry[skill_name] try: result = skill.execute(params) return {"status": "success", "result": result} except Exception as e: return {"status": "error", "message": f"技能执行异常: {str(e)}"}这里有一个容易忽略的细节:调用技能之后,返回结果一定要给模型一个“执行反馈”,包括成功或失败的标记。很多初版 Agent 应用把技能内部异常直接吞掉,模型看不到执行结果,后续步骤就会基于错误的状态继续“演下去”,最后给你一个看着很完整、其实全错的结果。我最开始做时也犯过这个错,直到有一次助理报告“报告里引用的数据明明不存在”,一查日志,才发现技能调用早就异常了,但模型硬着头皮编了下去。后来所有技能调用统一走这个对模型可见的 wrapper,情况才好转。
3.4 让技能“长”出记忆与状态
纯函数式的技能适合无状态操作,但很多实际场景需要技能在多次调用之间保存状态。比如一个“任务管理器”技能,用户先让它“记下明天下午三点开会”,再让它“提醒我明天的安排”,两次调用之间需要共享同一个任务列表。
我的做法是给技能实例增加一个可选的记忆区,每个技能可以持有自己的状态字典。为了保证状态不失控,我规定:技能的状态必须能在每次对话结束后序列化保存,下次会话时重新加载。这样技能既是“一次性的执行器”,也是“持久的记忆体”。
但这里我也要提醒一句:状态管理会增加复杂度和出错面。我的建议是,默认情况下技能做成无状态,只有当功能明确需要跨调用共享信息时,才引入记忆。尤其在多技能场景下,内存归属问题很容易踩坑。假设 search_info 和 generate_brief 都用到了“上一轮检索结果”,把这个信息放在哪个技能的记忆里都会造成逻辑混乱。我最终采用的方案是:所有技能共享一个 Agent 上下文缓冲区,技能间的数据传递通过这个缓冲区进行,而不是通过技能内部状态。每个技能只负责“读入参数、产出结果”,至于结果要不要被下一个技能使用,由 Agent 主调配逻辑去决定。这样虽然牺牲了一点“技能自治性”,但换来了整体流程的可控性。
4. 疑难问题与排查技巧实录
4.1 模型总是不调用技能怎么办
做 Agent 应用最先遇到的问题基本都是一样的:你辛辛苦苦写好了技能,但模型在对话里绕过技能,直接凭“想象”回答。
排查这类问题,第一步不是改代码,而是看日志里模型到底有没有看到技能定义。有些框架会把技能列表传给模型,但出于上下文裁剪策略,某些描述被截断了——模型压根不知道有这个技能,自然就不会调用。这种问题一查对话开始阶段的上下文内容就能发现。
第二步是检查 description 的触发场景是否清晰。我遇到过比较典型的情况:技能的 description 写的是“获取用户信息”,但模型面对的问题是“查一下这个用户的 VIP 等级”,语义上匹配不上。把 description 改成“查询用户的会员等级与权益信息,当需要了解用户级别、权益时使用”之后,调用率立竿见影地提升了。
第三步,也是很多人漏掉的一点:检查技能名是否太“抽象”或者太“相似”。当技能库超过十个,技能名的辨识度就变得极其重要。如果你有两个技能分别在名字里带 order 和 orders,模型会很容易混淆哪个对应哪个业务场景。
4.2 技能执行结果“因果错位”的排查
有一次,团队在调试一个多技能 Agent 时发现:模型明明调用了 A 技能获取用户列表,但最后输出的报告里引用的数据来自 B 技能的返回。这种“因果错位”问题,根源往往不在模型,而在上下文管理。
当模型执行多步调用时,Agent 框架会把每一步的工具返回结果一股脑地拼进对话历史。如果返回结果没有来源标识,模型分不清哪个结果对应哪次调用,尤其当多个技能返回结构相似的 JSON 时,很容易张冠李戴。
解决方法是:在返回结果的文本里自带“来源标识”。我们给每个技能的结果前缀加了一步编号,比如“【步骤2-查询库存】库存数量:120件”。模型在生成后续内容时就有了明确的指代锚点。这个改动很小,但在排障上省了大量时间——不只是让模型少犯错,更关键的是,出问题时你能一眼从对话历史里看出哪一步开始错了,而不是整段推倒重查。
4.3 长技能链的稳定性优化
技能链超过三步之后,模型的失误率会明显上升。我总结出三个有效的优化手段。
第一,上游技能的返回结果要“尽量薄”,只留下游技能真正需要的信息,减少无关数据对模型的干扰。这需要在设计技能时就想清楚“这个结果会被谁消费、消费什么字段”,而不是先把所有数据都倒上去,等出问题再删。
第二,在 description 里为每个技能指明它在任务链中的角色。比如“该技能通常在其他技能获取原始数据后调用”。模型看到这种提示,会更倾向于遵循合理的调用顺序。我把这理解成给模型一张“流程地图”,它不是万能的,但能让模型在两眼一抹黑时有方向。
第三,对特别复杂的任务,不要指望模型一次规划到底。要把大任务拆成多个子任务在 Agent 主循环里串行执行,每完成一个子任务就向用户反馈进度。这既是为了降低单次推理的复杂度,也是为了让用户能在任务执行的中间阶段介入纠偏——这一点在实际产品体验里非常重要。
搭配这几个手段,我自己项目的多步任务成功率从早期的大概六成,逐步稳定到九成以上。这里说的成功率,是“任务完整执行完成且结果可验证”的比例。可见工程化手段对 Agent 应用稳定性的提升,比换一个更大更强的模型要明显得多,而且成本更低。
4.4 多技能并发与状态隔离
最后聊聊并发。当 Agent 同时响应多个用户请求时,技能实例是否线程安全就成了必考题。我的做法是:技能类本身做成无状态单例,任何用户级别的数据都不存在技能实例上;如果有需要保存的用户数据,放到按 conversation_id 隔离的会话存储中。这样多个用户并发执行同一个技能时,不会互相污染。
更具体地说,我给每个会话分配一个独立的上下文 key,所有需要持久化的数据都挂在这个 key 下面,技能执行时按需读取。在代码层面,不要在技能类里定义可变的类属性,也不要在 execute 方法里使用全局变量。看起来简单的几条约束,在并发压测下能避免大量数据串扰问题。
我早期就吃过亏:两个用户同时让 Agent 查询各自的订单,结果 A 用户的结果里混进了 B 用户的订单信息。排查后才发现,技能类里放了一个用户级的缓存字典,两个请求的读写互相蹿了。把缓存改成会话级隔离之后,问题彻底消失。从那以后,我给自己定了一条规矩:技能设计阶段就把“并发安全”写进 checklist,而不是等线上出问题再补救。
我在实际把这整套技能体系落地到项目之后,最大的感受是:当技能数量超过十个,最大的瓶颈反而不是模型能力,而是技能设计的“元能力”。你得不断琢磨怎么给模型写清楚说明书,怎么把结果归一到最简,怎么把状态隔离干净。这套能力一旦建立起来,后续加新技能就像填表格一样简单——写描述、定 Schema、写执行体、注册,四步就完事。
如果大家正在做 Agent 应用,我的建议是:先把一个技能做到极致,再考虑扩展成技能库。一个描述精准、参数严谨、结果干练的技能,比十个描述含糊的技能加起来更能解决实际问题。技能从来不是越多越好,而是每个技能在模型眼里都“一眼可懂、一调就准”,这才是 agent-skills 这套设计真正值钱的地方。