做Agent开发这一年,我最大的感受是:大家比的已经不是谁的模型更强,而是谁的Agent真的“手里有活”。模型再聪明,如果到最后都只是纸上谈兵,用户很快就会失去耐心。真正决定一个智能体干活能力的,往往不是提示词写得有多花哨,而是那层看起来不起眼的agent-skills——也就是技能层。
agent-skills,说白了就是给模型准备的一组“手和脚”。模型本身只会生成文字,没法直接调用接口、读写文件、查数据库、发通知。技能层补上的正是这些具体动作。没有这层,模型只能给建议;有了这层,模型才能把建议变成行动,把对话变成结果。
这篇文章写给正在做Agent应用、已经过了demo阶段想推到工程化的团队,也写给刚接触工具调用不知道从哪里拆技能的开发者。我会从技能怎么拆、怎么设计、怎么注册、怎么排错,到怎么把技能变成长期可复用的资产,全部走一遍,尽量讲清楚每个步骤背后的理由。下面开始。
1. 先想清楚:Agent的“手”到底指什么
1.1 大模型没有手,技能层就是那双手
先把一个非常容易混淆的概念掰开:Agent和Chatbot的区别,不在于模型记忆,而在于是否有能力改变外部世界。一个支持技能层的Agent,拿到用户需求之后,不只是回复内容,还会执行动作。比如用户说“帮我把上周的库存异常汇总成表发到群里”,模型要做的不是写一段建议,而是依次调用查询库存、聚合数据、渲染表格、调用群机器人推送这几个动作。
可以这样类比:大模型是一个高智商但闭着眼睛、坐在工位上的专家,他非常善于分析和思考,但自己没法动手。技能层就是秘书放在他桌上的一堆工具,每件工具贴上标签,说明用途和用法。专家只需要说“我要用几号工具、传什么参数”,秘书替他把事办了,再把结果拿回来。
这个“专家+秘书”的分工,就是当前Agent的主流协作模式。但这里有个关键点:秘书能执行哪些工具、工具的说明书写得清不清楚,直接决定了专家能不能正确使用。我在项目里踩过最大的坑,就是模型明明知道有技能,却总在错误的时候调用,或者用错误的方式传参。最后排查下来,问题几乎都出在技能层设计,而不是模型本身。
1.2 技能拆解粒度:先粗后细,别一上来就拆原子
第一次做技能规划的时候,团队里很容易出现两种极端。一种是恨不得把每个函数都变成技能,一个查询接口拆出五六个细粒度的原子技能,结果技能列表上百个,模型选不过来;另一种是反过来,把所有逻辑揉进一个“万能处理函数”里,模型只需要调一个技能,但这个技能内部什么都干,没法复用,也没法排错。
我的建议是:先粗后细,场景驱动。第一步,从真实用户需求里找高频动作,拆出三到五个比较大的技能,比如“查库存”“做报表”“发通知”。每个技能对应一个完整可理解的任务,先把端到端跑通。第二步,跑通之后观察日志,看哪些内部子步骤被反复用到,再把这些子步骤抽成更小的公共技能。
我用一个表来对比这两种粒度的取舍:
| 粒度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 细粒度(原子技能) | 复用性强,组合灵活,调试定位快 | 技能数多时模型选择困难,编排复杂度高 | 团队对场景很熟,已有稳定的编排层 |
| 粗粒度(任务型技能) | 模型容易理解,调用准确率高,链路短 | 复用性差,内部逻辑一旦固化后难扩展 | 刚起步、场景明确、追求尽快跑通 |
这里说一个真实教训。我们最初做过一套电商客服Agent,把“查订单”“查退款”“查优惠券”拆成三个技能,结果模型经常在用户问运费险的时候去调退款查询。后来我们把三个技能合并成一个“查订单信息”,底层自动根据参数分流,模型准确率一下子从76%拉到92%。所以,在Agent早期阶段,“让模型少做选择题”比“让模型精细编排”更重要。
2. 技能库的设计细节:描述和参数决定模型动不动手
2.1 技能描述是给模型看的说明书,不是给人看的
这是很多团队会忽略的一点:技能描述不是写给同事看的功能注释,而是写给一个“读过很多书、但对你的系统一无所知的专家”看的操作说明。大模型理解自然语言的能力很强,但它没有你的业务背景,它只能根据描述来判断“这个场景该不该用这个技能”。
我在早期写过一个技能描述是“对数据进行处理”,结果模型几乎任何数值问题都会触发它。后来改成:“对二维表格数据执行清洗分析。仅当用户要求统计缺失值、异常值、重复值或做数据质量评估时使用。输出为统计摘要,不修改原始数据。”改动之后误调用率下降了六成。
写技能描述,我总结了一套四段式模板:
- 这个技能做什么:一句话说清楚动作和对象。
- 什么场景下使用:列出典型的用户意图关键词和句式。
- 什么场景下禁止使用:把容易混淆的边界情况写明白。
- 输入输出是什么:说明参数含义和返回值结构。
注意第四点很重要。模型如果不知道技能返回什么结构,它就无法在下一步合理利用结果。比如一个技能返回的是JSON格式的库存列表,但描述里没写,模型可能以为返回的是自然语言的结论,后续对话就会很别扭。
2.2 参数Schema设计的几个实用经验
如果说技能描述决定模型“会不会用”,参数Schema就决定模型“用得规不规范”。大模型填充参数靠的是理解自然语言,不可能像程序员一样严格对着接口文档写。所以Schema设计的原则,是主动给模型降低填参难度。
第一个经验:参数命名语义化,不要用缩写。比如qty不如quantity,itm_id不如item_id。模型看到语义清晰的参数名,更容易把用户话语里的信息对应进来。我自己还常用一个技巧,在字段description里给出参考示例,比如“商品编码,通常形如P-2024-001”,模型就有样可依。
第二个经验:能设默认值的参数尽量设默认值。用户经常说“查一下库存”,没说查哪个仓库,模型如果发现这个参数是必填的,就会反问用户。如果在Schema里把仓库字段设为默认“主仓”,整个流程会顺很多。很多情况下,追问用户不是模型的错,是我们把参数设计得太苛刻。
第三个经验:避免深层嵌套结构。模型生成JSON时,嵌套层数越深,出错的概率越高。一个params里套filters再套conditions的Schema,会让模型生成出各种稀奇古怪的JSON。尽量把输入拍平,能用顶层字段就不要嵌套。
第四个经验:错误信息要回流给模型,而不是直接抛异常。技能的运行结果最终要作为一种“证据”回到模型上下文里。当参数校验失败时,我习惯返回一个专门的错误结构,比如{"status": "check_error", "message": "item_id不存在"},模型看到后能自己修正参数再次调用。直接在代码里抛Exception,整个对话链路就断了。
2.3 权限与沙箱:给技能装一把“限位锁”
技能是模型操作外部世界的桥梁,也就意味着,如果技能权限过大,模型一旦走偏,造成的破坏是真实的。我见过一个团队把“执行Shell命令”直接开放给模型,模型在一次测试里把临时目录当成系统目录给清理了。所以,技能的安全边界必须在一开始就设计好,而不是出了问题再补。
我的操作经验是:按能力和范围双重限制。
能力上限就是“这个技能只能做什么”。比如文件操作技能只允许读文本文件,不允许写和执行;网络请求技能只允许访问预配置的域名白名单;数据库技能只允许执行SELECT,不允许UPDATE和DELETE。
范围限制就是“能操作哪些对象”。文件技能限定在/data/workspace/目录内,数据库技能限定在几个业务表内。这样即使模型被越狱指令引导,它的破坏半径也被锁死在一个可控范围,不至于波及整个系统。
另外建议给每个技能加超时时间。模型调工具如果半天没返回,会占用整个对话进程。网络请求类技能尤其明显,我一般的做法是统一设10秒超时,超时返回一个“请求超时”标记。
这里可以套用现实中的逻辑:给员工配电脑不会直接给管理员权限,而是按岗位最低权限分配,技能权限同理。你会惊讶地发现,权限收紧之后,Agent的稳定性反而提高了,因为模型可犯的错误空间变小了。
3. 手把手注册一个Agent技能并跑通
3.1 准备一个最简的技能运行环境
我第一次跑通技能调用,没有用任何重量级框架,就是最朴素的三件套:模型客户端、技能注册表、执行器。你可以理解成,模型客户端负责让模型“思考”,技能注册表负责存放“有哪些工具”,执行器负责“替模型动手”。
这里用一段极简的Python来演示结构:
SKILL_REGISTRY = {} def register(name, func, schema): SKILL_REGISTRY[name] = { "function": func, "schema": schema, "enabled": True } def execute_skill(name, params): if name not in SKILL_REGISTRY: return {"status": "skill_not_found", "message": f"技能 {name} 不存在"} skill = SKILL_REGISTRY[name] return skill["function"](**params)先把骨架搭出来,之后要加权限校验、超时控制、日志上报,都是在这个执行器里填肉。很多开源框架的核心逻辑也和这个差不多,复杂度主要在编排和并发控制上,但理解这个核心就能看懂它们的设计思路。
3.2 技能函数与Schema怎么写才干净
接下来,用一个“查库存”的例子完整走一遍。先写技能函数本身,保持函数足够简单,只做一件事,并且返回值结构稳定。
def query_stock(item_id: str): # 这里对接真实的库存系统,只保留必要字段 return { "item_id": item_id, "stock": 128, "unit": "件", "available": True, "status": "ok" }注意,我刻意让返回结果里带了status字段,这是给模型看的信号,表示这次调用是否正常。稳定的返回值结构,让模型在下一步判断时心里有底。
然后定义对应的Schema,也就是给模型的“填参规范”:
{ "name": "query_stock", "description": "根据商品编码查询当前可售库存数量。仅当用户询问库存、余量、可售数量时使用。商品编码不确定时,先向用户确认,不要猜。", "parameters": { "type": "object", "properties": { "item_id": { "type": "string", "description": "商品的唯一编码,通常形如 P-2024-001" } }, "required": ["item_id"] } }这里description里的最后一句很关键:“不确定时先向用户确认”。这类边界约束写进描述,能明显减少模型用幻觉参数去调用技能的频率。你可以理解成给服务生递菜单时顺带说了一句“龙虾需要提前预订,没有的话别硬点”。
注册时直接把函数和Schema绑定到一起:
register("query_stock", query_stock, stock_schema)3.3 调用链路:模型是怎么“想”到用这个技能的
技能注册好之后,真正的魔法在调用链路上。常规流程是这样的:
- 把当前对话历史+技能描述列表一起发给模型,让模型看到“现在有什么工具可用”。
- 模型判断当前任务是否需要某个技能,如果需要,就生成一个包含了技能名和参数的工具调用请求。
- 你的程序在输出里解析出技能名和参数,去执行器里跑这个技能。
- 把技能返回结果作为一个“观察结果”拼回对话历史。
- 模型基于这个观察结果,决定继续调用下一个技能还是给出最终回答。
这个循环可以用一个很短的伪代码表达:
for step in range(MAX_STEPS): prompt = build_prompt(skill_list, conversation) response = model.generate(prompt) if response.has_tool_call(): name = response.tool_name params = response.tool_params result = execute_skill(name, params) conversation.append_tool_result(name, result) else: break这里的“观察-行动-思考”结构,业内叫ReAct思路。你可以这样理解:模型先读工具清单,相当于看了一遍“技能墙”;然后选定一个技能调用,相当于“伸手拿工具”;拿到返回结果后,它会把这个结果当成新的证据,再决定下一步。
我自己实测下来,技能列表一次不要超过十五个。超过这个数量,模型的选择准确率会下降,而且每次请求都要把所有技能描述发给模型,token消耗也很大。如果场景复杂,可以把技能分组分阶段注入,先让模型选分组,再在组内选技能,这样可以显著降低选择压力。
3.4 用一组回归用例给技能打分
技能跑通只是开始,能不能稳定复现才是关键。我习惯给每个核心技能配一组回归用例,用来观察后续修改有没有把技能“改坏”。回归用例不追求多,每个技能准备五到八个典型场景就够了。
比如针对query_stock,可以这样设计:
| 用例编号 | 用户输入 | 期望行为 | 核心考察点 |
|---|---|---|---|
| TS-01 | 查一下P-2024-001还有多少货 | 调用query_stock,item_id=P-2024-001 | 参数提取准确性 |
| TS-02 | P-2024-001库存充足吗 | 调用query_stock并依据返回判断是否充足 | 返回值利用能力 |
| TS-03 | 帮忙看看这个商品 | 不调用技能,先反问“哪个商品” | 边界约束是否生效 |
| TS-04 | 查一下那只蓝色的笔的库存 | 不调用或结合上下文推理商品编码 | 混淆场景识别 |
每一轮版本迭代,我都在本地跑一遍这套用例,记录调用成功率、参数准确率、任务完成率和平均轮次。这四个指标,基本能反映一个Agent技能的健康度。调用成功率关注模型是否在正确场景触发,参数准确率关注解析和填充质量,任务完成率关注最终结果是否符合用户预期,平均轮次则反映效率,模型来回询问太多会影响体验。
这一套跑下来,你会对技能的真实能力有非常量化的感觉,而不是凭印象觉得“好像还可以”。
4. 实测最常见的翻车点与排查方法
4.1 幻觉调用:模型杜撰了一个不存在的技能
这是工具调用里最经典的一类问题:模型想要完成某个需求,但技能库里没有对应工具,它居然自己编了一个技能名出来调用。表现就是执行器返回skill_not_found,然后模型一愣,又重新解释或者勉强用自己的知识回答。
出现幻觉调用的根源通常有两个:一是提示词里暗示了模型可以调任意工具,二是技能清单里相似技能太多,模型模糊了边界。排查方式很简单,先把执行器的错误日志打开,统计所有skill_not_found记录。
解决方案我分三层来做:
- 第一层是执行器白名单校验,不知道的技能名一律拒绝,绝不能因为模型说了“调用某技能”就去搜一个同名函数执行。
- 第二层是收敛技能数量,减少模型的选择空间。技能从二十个减到八个后,幻觉调用发生的概率会立刻下降。
- 第三层是在系统提示词里加上明确约束,比如“只能使用给定的技能,不要假设存在其他功能”。这句话看似简单,实测很管用。
4.2 参数解析失败:模型填的JSON经常不合法
模型生成JSON的能力虽强,但偶尔会出现多一个逗号、引号没闭合、布尔值写成了字符串这类低级错误。很多人第一步会怪模型,然后试图加强提示词。我的看法是,与其期望模型完美,不如在解析侧做容错。
一个实用做法是“先修复再解析”:把模型输出的片段提取出来后,先做一轮轻量修复,把末尾多余的逗号去掉,把单引号替换成双引号,再交给JSON解析器。如果解析仍然失败,就把错误结构返回给模型,让它看到“JSON解析错误”,引导它重新生成一段合法的工具调用。
还有一种更隐蔽的情况:模型把参数值填成了无意义的文本。比如用户说“查一下明天上海的天气”,模型可能把location填成“明天上海”而不是“上海”。这时候就需要在技能内部做参数清理,把明显的日期词、语气词剥离,或者用正则做一次校验。我在Schema里要求的参数,在执行器里还会再做一遍轻量清洗,双重保障。
4.3 返回值太长:上下文被技能输出冲爆
很多技能的首次版本,都习惯把能返回的字段全返回。一个查库存的接口,可能把库存成本、供应商、最近入库时间、仓位编号全塞进结果。模型的上下文窗口是有限的,技能返回内容太长,既浪费token,又让模型抓不住重点。
我建议每个技能输出最多保留五个核心字段,并在函数内部做好摘要逻辑。比如一个日志分析技能,不应该返回原始日志,而应该返回“错误次数Top5、平均延迟、时间分布”。模型看到的是已经压缩过的结论,它的推理负担小很多。
如果确实需要返回长文本,可以在执行器层面设置最大返回长度,超出的部分截断,并在结果里附带一个提示“结果已被截断,如需完整数据请缩小查询范围”。这样模型知道还有数据,但不会被一整屏的乱码冲走注意力。这个设计非常实用,尤其是涉及到日志、表格、长文档处理的时候。
4.4 多技能互踩与死循环
当技能数量变多,会出现另一种问题:技能A调用了技能B的结果,技能B完成任务后又想调回技能A,两个技能互相咬住,Agent陷入死循环。还有的模型会连续十次调用同一个技能,每次都是同样的参数,每次都拿到同样的结果,然后还在重复。
这种情况在工程上不能只靠模型自觉,必须在编排层加护栏。我常用的三个护栏:
- 设定全局最大步数,比如十五步,超过就强制终止并提示“已接近操作上限,请收敛任务”。
- 设定单技能最大调用次数,比如三到五次,防止模型在一个技能上钻牛角尖。
- 把技能调用链设计成“主链+旁路”结构,明确哪个技能是终态技能,哪些技能的结果只是供主链参考,不参与后续循环。
光说可能不好理解,举个具体例子。比如一个“生成库存周报”的技能,它应该依次调用“查库存数据”和“格式化报表”,最后生成一个终态结论。我在技能编排里直接把顺序锁死,模型没有权限在两个技能之间来回横跳。这其实是从“让模型自由编排”退回到“让模型按预设流程走”的一种必要妥协。在复杂业务场景下,确定性比灵活性重要得多。
5. 从单技能走向技能网络
5.1 给技能装上日志与指标
单技能稳定之后,下一步是把技能层当成基础设施来运营。每个技能都应该产出一条结构化日志,至少包括:谁调用的、在哪个会话里、传入的参数、返回的状态、耗时多长、是否成功。把这些日志汇总之后,就能得到一套技能健康指标。
我自己最关注三个指标:调用成功率、P95耗时、错误分布。调用成功率低于90%的技能,往往不是模型的问题,而是技能本身的边界条约不清楚;P95耗时过高的技能,要考虑缓存或异步化;错误分布集中在一个返回码上,就说明某个参数设计有系统性缺陷。
这部分的最终目标,是让技能层的每次改动都有数据支撑。我见过太多团队升级了一个技能,结果把另一个流程搞挂了,最后靠用户投诉才发现。有了日志和指标,这种问题早就能从曲线里看出来。
5.2 用复合技能沉淀编排逻辑
当一个组合调用顺序被验证稳定后,可以把整套流程封装成一个复合技能。比如“语音转写”“要点提取”“待办整理”三个技能经常被一起用,就可以封装成一个“会议纪要生成”技能,模型只需要调一次,内部顺序由代码控制。
这样做的好处是双重的。第一,模型的选择维度减少了,一个复合技能替代了三个原子技能的编排压力;第二,编排逻辑沉淀在代码里,不会因为换了模型版本就重新随机波动。模型仍然负责理解和表达,但“先做什么后做什么”这类确定性逻辑交给代码,这是Agent系统从脆弱走向稳定的关键一步。
我经常把这比作点外卖:你可以自己分别点米饭、点菜、点饮料,也可以直接点一个套餐。套餐对用户更友好,对商家来说备餐流程也更可控。
5.3 技能复用:建一份“技能资产清单”
做到这里,技能层已经成为团队的一份资产,而不只是某个项目的临时工具。我建议维护一份“技能资产清单”,内容包括:技能名、功能描述、所属模块、负责人、版本号、最近修改时间、依赖的外部系统。这份清单的价值,在跨项目复用时尤其明显。
新项目上来,第一件事不是重新写技能,而是先查这份清单里有哪些技能可以直接用。查询库存的技能在电商项目能用,在供应链项目也能用;报表生成技能在运营项目能用,在财务项目也能用。技能层和具体业务场景解耦得越彻底,复用成本越低。
最后再分享一个小技巧:技能的版本号一定要独立于模型和框架的版本号。因为技能是相对稳定的,模型升级和框架升级是频繁的,把两者解耦之后,出现问题可以快速定位是模型侧变化还是技能侧变化。我自己吃过几次亏之后,现在新项目上来第一件事就是把这层技能资产清单搭起来,收益远比调提示词来得快。
做Agent越久越会发现,提示词是流水的兵,技能层才是铁打的营盘。把技能打磨扎实,模型无论怎么换代,系统都能稳稳站住。