news 2026/10/7 11:45:06

Agent技能包实战:如何让大模型稳定调用工具,告别参数幻觉

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent技能包实战:如何让大模型稳定调用工具,告别参数幻觉

这两周我一直在折腾agent-skills,起因特别简单:我搭的智能体越来越像个“会聊天但干不了活”的顾问,让它查个航班信息、改个配置文件,它要么嘴上答应,要么把参数拼得乱七八糟。后来我把大模型的“技能”从代码里拆出来,做成一个个可命名、可安装、可组合的技能包,整个执行链路才稳定下来。这篇文章就把我这套组织方式、设计取舍、踩坑记录和接入思路完整写出来,给同样在搭 Agent、做自动化流程、或者正被“模型老是瞎编工具参数”折磨的朋友一个可以直接参考的落地方案。

我默认你至少写过一点大模型应用,知道 function calling 大概是什么,但不需要你用过任何特定框架。文中所有代码片段都是我实际跑过的简化版本,你可以直接抄进自己的项目里改改。

1. 我为什么放弃“在一个文件里堆技能”

1.1 最初的做法,以及它为什么撑不住

最早我给 Agent 加工具能力的时候,走的是最朴素的路线:写一个tools.py,里面堆了二十多个函数,然后在系统提示词末尾贴一大段说明,告诉模型“你有这些工具可用,格式是 XXX”。一开始只有五六个工具时,效果还能接受。等工具数量涨到二十个以上,问题就开始冒头了。

最明显的是模型“选择性失忆”。明明工具列表里就有search_flight,模型遇到航班查询需求时却跑去调用get_weather,或者自己编一个不存在的参数进去。我调试了很久,发现根子在于:我把工具函数、参数说明、调用规则全混在一起,模型在长上下文里很难快速定位“当前这个任务应该用哪个技能、这个技能到底吃什么参数”。

另一个痛点更现实:每加一个新功能,我都得改tools.py,还得同步改系统提示词,一不留神就出现“代码里有这个函数,但提示词里没写”或者反过来。这种散点维护的方式,在项目早期没问题,一旦技能变多,人脑根本记不住谁依赖谁。

1.2 技能应该是什么:可发现、可校验、可调用

后来我重新想这个问题,把“技能”收敛成三个核心属性:

  • 可发现:模型需要知道系统里存在哪些技能,每个技能是干什么的,什么时候该用它。
  • 可校验:技能接收什么参数、参数类型是什么、哪些必填,这些必须在调用前就能被检查。
  • 可调用:技能背后必须有真正能执行的逻辑,而不是模型嘴上说说。

这三个要求听起来直白,但大部分项目只做到了第三个。可发现靠的是在提示词里写一段自然语言,可校验靠的是模型自觉,结果当然不稳定。

我用一个生活化的类比来理解这件事:模型就像办公室里的职员,技能就是墙上的插座。职员不需要知道插座背后是火电还是水电,只需要看见插孔形状,知道自己这台设备的插头能插进去,插进去就有电。你要做的不是教会职员发电,而是把插座设计得规范、标注清晰、协议统一。agent-skills做的就是这套“插座规范”。

1.3 一个具体场景:让 Agent 学会查航班

举一个我实际遇到的例子。我的智能体需要帮用户查询航班信息,底层对接的是某航司的开放接口。在没有拆分技能之前,我每次都得在提示词里写:

你可以调用航班查询接口,接口地址是 xxx,请求方法是 GET, 参数有 from_city, to_city, date,其中 date 的格式是 YYYY-MM-DD, 返回结果是 JSON,里面包含航班号、起降时间、价格等字段……

这段说明不仅占上下文,而且模型经常读歪。有一次它把date理解成“返回当天的日期”,直接把系统当前日期传了进去,导致查询结果永远是当天航班。

拆成技能包之后,提示词里只剩一句话:“你拥有flight_search技能,需要查询航班时使用它。”模型需要知道的所有细节都被封装在skill.json和对应执行函数里。调用时,模型只需要输出意图化的结构化指令,剩下的参数校验和 HTTP 请求都由执行器负责。这样改造后,同样场景下模型的正确调用率从大概六成提升到了九成以上。

2. agent-skills 是怎么组织一个技能包的

2.1 一个技能包的骨架

我用的目录结构非常简单,每个技能是一个文件夹,里面至少包含三个文件:

skills/ └── flight_search/ ├── skill.json ├── main.py └── requirements.txt
  • skill.json:技能的身份证,包括名称、描述、参数声明,是模型发现技能的依据。
  • main.py:技能的真实实现,接收解析好的参数,返回可读结果。
  • requirements.txt:技能的依赖清单,安装时单独处理,避免污染主环境。

一个最简的skill.json长这样:

{ "name": "flight_search", "description": "查询两个城市之间的航班信息,适用于机票预订、行程规划场景。当用户需要坐飞机出行时使用,不要用于查询火车票或酒店。", "parameters": { "type": "object", "properties": { "from_city": {"type": "string", "description": "出发城市名"}, "to_city": {"type": "string", "description": "到达城市名"}, "date": {"type": "string", "description": "出发日期,格式 YYYY-MM-DD"} }, "required": ["from_city", "to_city", "date"] } }

main.py里则是普通的 Python 函数,接收字典形式的参数,返回字符串或简单的数据结构:

def run(params: dict) -> str: from_city = params["from_city"] to_city = params["to_city"] date = params["date"] # 实际的 HTTP 请求逻辑省略 result = fetch_flights(from_city, to_city, date) return format_flight_summary(result)

这里有一个设计细节:run的返回值我尽量收敛成“模型能直接读懂的文本”,而不是原始 JSON。原因后面会详细讲,简单说就是模型读一长串嵌套 JSON 容易迷失重点,而一段“上海到北京,3月1日,共有5个航班,最早06:30,最晚21:00,经济舱均价850元”这样的摘要,模型可以直接引用回答用户。

2.2 描述与参数声明:为什么必须显式化

很多人在写技能描述时不够重视,觉得“描述嘛,随便写两句就行”。但技能描述其实是模型决策的核心依据。模型本质上是在做“文本匹配”:读你的描述,判断当前用户意图是否命中。描述写得含糊,模型就会在多个技能之间左右横跳。

我后来总结出一套描述写作规范:

  • 先说明这个技能“能做什么”,一句话说清。
  • 再说明“什么时候该用”,列出典型场景。
  • 最后画一条边界“什么时候不该用”,比如航班查询不要用于火车票查询。
  • 参数描述里写清楚单位、格式、取值范围,比如日期必须是YYYY-MM-DD,城市名用中文全称。

参数声明我直接用 JSON Schema 语法。它足够通用,既能喂给训练好的大模型做 function calling 格式转换,也能在运行时做严格校验。没必要自己发明一套 DSL。

2.3 调用约定:从 LLM 到函数执行的桥

既然技能是给大模型用的,就必须定义清楚“模型怎么表达调用意图”。我采用的做法是:模型不直接执行代码,而是输出结构化的调用指令,由执行器负责分发。

整个流程分四步:

  1. 系统提示词里附上所有已注册技能的清单,清单由skill.json自动生成。
  2. 模型根据用户问题,决定调用哪个技能,并生成符合参数声明的 JSON。
  3. 执行器解析模型输出,先做 JSON 校验,校验通过后调用对应技能的run。
  4. 技能返回结果,执行器把结果拼到上下文里,交给模型继续组织回答。

这个“模型只决定、执行器只执行”的桥接方式,核心价值在于职责分离。模型不接触真实的 HTTP 请求,不接触文件系统,也就不容易因为“想当然”而搞出危险操作。执行器这一侧可以做鉴权、限流、超时控制,这些都是模型侧很难做好的事。

3. 关键设计取舍:描述、校验、安全

3.1 技能描述比实现更重要

这句话我是在实际踩坑之后才真正信服的。技能实现写得再漂亮,模型看不到你的代码,它只能看到description字段。换句话说,描述就是模型眼中的技能全貌。

我对比过两种写法。第一种是过度自信型:

"description": "查询所有交通信息"

看起来覆盖面很广,但实际上技能只实现了航班查询。结果就是用户问“从上海到杭州的高铁”,模型自信地调用了这个技能,返回了一堆航班航班,用户一脸问号。

第二种是边界明确型:

"description": "查询国内主要城市之间的航班信息。用于机票预订、行程规划。不适用于火车、汽车、轮船等地面交通查询。"

这会让模型在遇到高铁需求时,明确知道不该调用它,从而转向其他技能或者直接说明自身能力不足。宁可让模型的技能利用率低一点,也不要让它错误地调用一个看似万能、实则残缺的技能。

我还试过在描述里加上一些“触发词”,比如“当用户提到起飞、航班号、机票时优先考虑”。这种方式对某些模型有效,但也可能造成误伤。需要结合你的实际模型效果持续调优,没有一劳永逸的写法。

3.2 参数校验与失败重试:让 Agent 失败得“体面”

模型生成参数时,即使有了 JSON Schema,也难免出现类型错误、缺字段、枚举值超范围的情况。比如参数声明里date是字符串,模型却给了{"date": 20250301},没有引号。

我在执行器里加了一个统一校验层,所有技能在真正执行前都过一遍:

from jsonschema import validate, ValidationError def dispatch(skill, raw_args): try: validate(instance=raw_args, schema=skill.parameters) except ValidationError as e: return { "status": "invalid_params", "error": str(e), "hint": "请检查参数类型和必填项,重试一次" } return skill.run(raw_args)

这样做的妙处是:校验失败不会直接中断对话,而是把结构化的错误信息抛回给模型,模型看到hint后会理解“哦,我少传了参数或者格式不对”,然后自我纠错重新发起调用。

实测下来,很多表面上的“模型乱调参数”问题,其实可以通过这种“校验—反馈—重试”闭环解决一大部分。模型不是故意犯错,它只是看不见自己的输出格式。你给它一次纠错机会,比在提示词里反复强调“必须严格按照格式”有效得多。

3.3 安全边界:技能运行在受限环境里

技能一旦可以执行真实操作,安全问题就绕不过去。我的原则很简单:让技能跑在尽可能小的权限范围内。

具体做了三件事:

  • 超时控制:每个技能调用默认最多跑 30 秒,超时直接终止,防止模型陷入死循环式的重试。
  • 网络白名单:技能默认不能访问公网,只有显式申请网络权限的技能才会被授予访问特定域名的资格。比如flight_search只能访问航司接口域名。
  • 高危操作确认:涉及写文件、发送消息、支付等操作,执行器不会直接执行,而是先返回一个“待确认意图”,由用户在前端点击确认。

这些机制不复杂,但能避免很多“模型一句话就帮你下了订单”的惊悚场景。agent-skills这类方案本身只解决“技能组织”问题,安全边界必须自己在执行器层面补上。

4. 真实运行记录:从“模型出幻觉”到“稳定执行”

4.1 一次端到端调用:查火车票

我挑一个可复现的简单案例来展示完整链路。技能是train_query,描述是“查询 12306 上指定日期两城之间的车次信息”。用户输入:“帮我看看这周五从北京到南京的高铁。”

模型的思维链大致是这样的:

用户需要查询火车票,这属于 train_query 技能的职责范围。 日期是“这周五”,需要转成具体日期,假设今天是3月1日,本周五是3月3日。 调用 train_query,参数:from_city=北京, to_city=南京, date=2025-03-03, train_type=高铁

模型输出结构化调用指令后,执行器解析并校验,然后调用技能的run函数去请求 12306 接口。返回结果被改写成摘要:

北京到南京,2025-03-03,高铁共12个车次。 最早 G101,06:45 出发,09:48 到达,商务座还有少量余票; 最快 G115,07:30 出发,10:12 到达,二等座一等座余票充足……

模型拿到这段摘要后再组织自然语言回答,整个过程没有任何人手工干预。

4.2 踩坑:技能名冲突

有段时间我在技能库里加了第二个查询类技能train_search,当时脑子里没多想,以为和train_query是不同名字。结果执行器注册时发现,有两个技能的name字段在网络传播后变成了同一个规范化名字,新技能覆盖了旧技能。

这个坑的直接后果是:模型明明调用了train_query,执行器却路由到了新技能,返回的数据结构完全不兼容,上下文里出现一堆解析错误。

现在的做法是:每个技能包在注册时都会做唯一性校验,发现重名直接报错并拒绝加载。另外我引入了一个简单的命名空间概念,技能名格式统一为domain.action,比如transport.train_query、transport.flight_search。这样即使不同作者写的技能重名,只要域名不同就不冲突。

4.3 踩坑:描述太泛会带偏模型

前面提到过“描述太泛”的问题,我再展开一次具体现象。flight_search早期的描述是“查询航班信息,支持国内外航线”。结果用户问“北京飞纽约要多久”,模型调用了这个技能,但技能底层只对接了一家国内航司的接口,根本没有国际航线。后续重试三次都是同样的错误,浪费了一轮又一轮上下文。

这个案例的教训是:描述不仅要说清“能做什么”,更要说清“能力边界”。后来我干脆在描述里追加了“仅限国内主要机场之间的直飞航班,不包含中转、国际航班”,之后模型再也没有拿它处理过国际航线问题。

4.4 踩坑:技能返回结果太长,撑爆上下文

技能返回结果如果是一大段原始 JSON,模型读取时会非常吃力。我早期某个技能返回的是完整的航班列表,包含舱位代码、税费明细、机场三字码等三十多个字段,总长度接近三千个 token。模型为了组织回答,又把这些 token 原样引用了一部分,对话窗口很快就不够用了。

解决办法有两个:

  • 摘要化:技能在返回前,把原始数据浓缩成“人类可读的概要”,只保留用户最关心的字段。
  • 分页化:如果概要本身还是很长,就只返回前 5 条,并提供“获取更多结果”的技能入口。想看得更全,就让模型再去调用一次。

现在我对技能返回值有一条硬性标准:默认不超过 500 个 token,超过就要做截断或摘要。这条标准实施之后,整个对话的上下文利用率明显提高了。

5. 如何快速接入现有 Agent 框架

5.1 从零接入的最小执行循环

如果你现在用的是裸的大模型 API,想自己搭一套最小可用的技能执行循环,核心代码并不复杂:

message_history = [{"role": "system", "content": build_skill_list()}] message_history.append({"role": "user", "content": user_input}) while True: response = llm.chat(messages=message_history) if response.function_call is None: break skill_name = response.function_call.name args = response.function_call.arguments result = dispatcher.dispatch(skill_name, args) message_history.append(response.message) message_history.append({ "role": "function", "name": skill_name, "content": result })

build_skill_list会遍历技能库目录,把每个技能的name和description拼成一段系统提示词。dispatcher.dispatch就是上面提到过的“校验—执行—返回”流程。

这套循环足够应对大多数场景。如果你用的是 OpenAI 的 function calling 协议,只需要把skill.json转成它要求的 tool 定义格式,其他逻辑完全复用。

5.2 与主流框架对接的思路

我自己临时试验过把agent-skills风格技能接入现有的 LangChain 流程,思路也简单:

  • 用skill.json里的name、description、parameters生成框架要求的Tool。
  • 把main.run包一层,作为 tool 的可执行函数。
  • 注册到 Agent 的 tool 列表里。

这种对接方式的好处是:技能库本身不依赖任何框架,你在 LangChain 里能用,换到其他框架也能用。技能包是一个个独立的目录,复制粘贴就能迁移。

5.3 注册自有技能的完整步骤

如果我要新加一个“天气查询”技能,流程大概是:

  1. 在skills/下新建weather_query目录。
  2. 编写skill.json,注意描述里写清楚适用条件和边界。
  3. 在main.py中实现run(params)函数。
  4. 在本地跑一个测试脚本,模拟模型输出参数,验证执行器能正确返回。
  5. 把技能目录复制到运行环境,重启执行器。

你可能会觉得这比“直接写个函数挂上去”多了一步。没错,确实多了一步。但多出来的这一步换来了两个好处:一是模型对技能的理解更稳定,二是技能可以独立复用。当你的 Agent 项目不止一个,或者你打算把技能分享给团队其他人时,这个成本完全值得。

6. 后续可以扩展的方向,以及我个人的一点体会

6.1 给技能加版本号和回归测试

技能包现在支持version字段,比如1.2.0。当技能实现更新但参数接口不变时,模型不需要感知;但当参数结构有明显变化时,版本号能帮我在日志里快速定位问题。我还给每个技能配了一个test.json,里面存放几组典型的输入输出样例。每次改动之后跑一遍回归,确保新代码没有破坏旧场景。

这听起来很“工程化”,但对于真实的 Agent 项目来说非常必要。模型应用最怕的就是“昨天还能用,今天突然疯了”,而原因往往出在某个工具函数的细节改动上。

6.2 把技能做成分发式的“插件市场”

如果你把技能包想象成一个个独立目录,那么它们天然适合打包分发。我在内部折腾过一个简单的技能仓库:目录里每个技能都有一个metadata.json,包含作者、版本、依赖说明,别人可以一键安装到自己的 Agent 里。这很像 VS Code 插件市场的思路,其实没有什么新概念,但一旦技能数量超过三十个,统一分发和版本管理能省下大量时间。

6.3 我的真实体会

最后说点私货。agent-skills本质上不是什么高深莫测的技术,它只是把软件开发里早已成熟的“模块化”思想搬到了大模型应用层。真正让项目产生质变的,不是某个炫酷的执行引擎,而是你认认真真把一个技能的描述写清楚,把它该做什么、不该做什么划清楚边界。

如果你想动手试试,我建议不要一上来就设计一个庞大的技能框架。先挑两三个你日常最高频的工具,比如查天气、查地图、查文件,按上面这套结构搭起来,跑通一两个端到端场景,感受一下模型选择技能和纠错的过程。等你熟悉了这套“契约感”,再逐渐扩充技能库,会顺手很多。我自己就是在搭到第六七个技能时才慢慢体会到:技能管理这件事,做减法比做加法更重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 11:45:00

多智能体接入与调度层设计:Agent-Reach 注册、路由与容错实践

多智能体系统这几年看着挺热闹,但真正把几个智能体接到一块干活的时候,坑比想象中多得多:有的模型只认自己的工具格式,有的服务时不时超时,有的节点明明注册了却找不到,最恶心的是一旦某个环节挂了&#xf…

作者头像 李华
网站建设 2026/10/7 11:43:50

虚拟电厂VPP能源数字化:数据采集、功率预测与调度实战

简介:一份聚焦虚拟电厂(VPP)能源数字化与碳中和的Word文档,面向新能源、电力系统及碳管理领域从业者与研究者。内容以平衡机器科技(深圳)的Smartrams产品为切入,系统讲解云端风光功率猜想、虚拟…

作者头像 李华
网站建设 2026/10/7 11:43:33

Agent技能包:从描述文件到调度策略的完整实践

1. 项目定位:Agent 系统的“技能包”到底在解决什么问题 做 Agent 的同学应该都有体会:模型再聪明,工具调不好也白搭。大语言模型本身只负责“理解”和“规划”,真正落地干活靠的是工具调用、API 请求、脚本执行这些外围能力。而 …

作者头像 李华
网站建设 2026/10/7 11:43:00

Nginx负载均衡实战:从原理到配置、调优与排障

看到“Nginx搭建负载均衡”这个标题,我估计不少朋友的第一反应是:这不就是个upstream加proxy_pass的事儿吗?网上教程一抓一大把。但真到自己上手配置,或者接手一个已经跑着的集群时,问题就来了——为什么我的请求总是打…

作者头像 李华
网站建设 2026/10/7 11:41:58

Agent技能设计实战:从Function Calling到行为封装

1. 先搞清楚:Agent技能到底解决了什么问题 做Agent落地这一年多,我最强烈的体感是:大模型本身的“思考能力”已经不怎么卡脖子了,真正卡脖子的是 Agent能不能稳定地把想法变成动作 。你让LLM写一首诗、总结一份文档,…

作者头像 李华
网站建设 2026/10/7 11:41:57

分布式光伏集群划分与电压协调控制:从机理到Matlab实现

中午十二点,光照最强,负荷低谷,分布式光伏大面积出力,10kV馈线末端电压被顶到 1.07 p.u. 以上,逆变器一台接一台过压脱网。这个画面我相信很多做配电网仿真的朋友都不陌生。做含分布式光伏的配电网研究,绕不…

作者头像 李华