ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent技能库实战:从函数调用到可靠落地

Agent技能库实战:从函数调用到可靠落地 最近在折腾 agent-skills 这个方向时我发现一个很有意思的现象很多做 Agent 应用的人把大量精力花在 prompt 调优上却忽略了真正决定 Agent 能不能落地的关键——它到底能调用哪些技能以及这些技能被封装得是否足够可靠。agent-skills 这个名字看上去只是一个开源项目代号但它背后代表的是 Agent 从“会聊天”走向“会干活”的那条必经之路。这篇文章我打算用实际踩坑换来的经验聊聊怎么为 Agent 设计和沉淀一套可复用的技能库包括技能的定义方式、目录组织、测试机制和常见故障排查。无论你是刚接触函数调用的小白还是已经在做工具使用 Agent 的工程师这轮梳理应该都能给你一些直接能用的参考。1. 项目核心思路Agent 为什么需要“技能”1.1 从“会聊天”到“会干活”的跨越几乎所有做 LLM 应用的人都会遇到同一个瓶颈模型很聪明但你让它做一件具体的事比如从一批日志里统计错误码分布、把 Markdown 表格转成 Excel、调用某个内部 API 拉取数据它就变得不那么可靠了。原因不复杂——语言模型擅长生成文本但生成文本和完成操作是两码事。操作背后需要真实地执行函数、处理异常、校验结果这些都不是“多写几行 prompt 就能解决”的。agent-skills 想解决的就是这个“最后一公里”问题。它主张把可以被执行、被验证、被复用的操作封装成“技能skill”每个技能都有清晰的输入输出约定、实现代码和测试用例。Agent 在运行时通过语义匹配来自动发现这些技能然后像人类查工具书一样找到合适的函数并正确调用。你可以把它理解成给 Agent 安装了一套“插件库”每当你希望它掌握一项新能力就新增一个技能文件而不需要反复改系统 prompt。这个思路和传统的 ReAct 模式、纯 prompt 工程最大的区别在于技能是有边界的、可测试的、可版本化的。prompt 写得再好模型也可能在长对话中逐渐跑偏但如果它调用的是一个封装良好的技能函数那执行结果就是确定的模型只需要负责“决定调哪个”和“传递正确参数”这两件事剩下的交给代码即可。1.2 agent-skills 到底解决了什么问题先列几个我实际经历过的场景你应该会感同身受想让 Agent 查询数据库结果它总是凭空编出 SQL 列名或者把字符串参数拼进 SQL 导致语法错误。想让 Agent 处理 Excel 文件它在 prompt 里“说”得很好但根本没有真正执行任何操作用户拿到的是一个空的回答。多轮对话中Agent 第一轮正确调用了工具第二轮却因为上下文覆盖而换了另一种非法方式硬来。团队里不同人给 Agent 封装了功能相近的工具参数风格不一致模型在调用时经常混淆。这些问题如果只靠 prompt 去压制效果非常有限。而 agent-skills 的思路是把操作抽象成“技能对象”每个对象自带描述、参数 schema、实现和测试。模型面对的不再是散落的函数而是一套结构化的能力清单。这种结构化本身就是对模型的一种“约束”它传递的信息比自然语言描述要确定得多。我自己的体会是引入技能库之后Agent 的错误模式从“自由发挥”变成了“选错技能”或“参数略偏”这两类问题的修复成本低得多——前者只要扩充技能描述后者只要调整 schema 示例。整个系统的稳定性会有一个质的提升。1.3 与传统 prompt 工程和 function calling 的边界很多人会问OpenAI 的 function calling 不是已经做了这事吗为什么还要单独搞一个 agent-skills 这样的项目这里我想说清楚一个容易被混淆的点。function calling 定义的是“模型如何输出一次工具调用请求”它是一种协议层面的能力。而 agent-skills 定义的是“一个工具调用请求如何被组织、校验、执行和迭代”它是工程层面的能力沉淀。前者解决“怎么喊”后者解决“喊完之后怎么保证不出事”。实际项目中两层需要配合使用但很多团队只关注了第一层写了一大堆 function 描述却没有一套机制去管理这些 function 的测试、优先级、冲突和版本。拿真实项目打比方function calling 相当于给汽车装了方向盘而 agent-skills 相当于建立了一套驾驶规范、道路标识和保养手册。没有后者车也能开但大概率会在路上出各种小状况。所以我的建议是如果你正在做一个严肃的 Agent 应用一定不要把工具调用只停留在 function calling 的配置层而是尽早建立起自己的技能库体系。2. 技能的定义与目录设计2.1 一个技能应该由哪些部分组成在 agent-skills 的思路里一个技能不是简单一个函数而是围绕这个函数的完整封装。我建议至少包含五个部分缺一个都会在后续使用中埋隐患。第一是技能元信息包括技能名称和版本号。名称要短、要独特方便模型在语义匹配时快速定位。比如 get_weather 就比 fetch_data_from_weather_api_v2 好得多。版本号则用来支持后续的重构和回滚。第二是自然语言描述。这是给模型看的核心索引描述的内容决定了模型在什么场景下会想到调用它。描述要覆盖三类信息这个技能解决什么问题、什么情况下不要用它、有没有关联的替代技能。比如“获取指定城市当前天气仅支持中国城市不支持空气质量查询空气质量请用 get_air_quality”。第三是参数 schema。每个参数都要明确类型、取值范围、默认值和示例值。这里有个关键技巧在描述里写清楚参数之间的依赖关系比如“当 mode 为 forecast 时days 必须在 1 到 7 之间”否则模型很容易传出不合理的组合。第四是实现代码。实现要尽量独立不要依赖全局状态最好把网络超时、异常处理都在函数内部消化掉对外只暴露成功或抛出标准异常这样技能的执行就是一个可预期的操作而不是一团不可控的过程。第五是测试用例。一个技能至少有 2 到 3 个正向用例和 1 到 2 个反向用例。正向用例验证“正确输入产生正确输出”反向用例验证“错误输入得到清晰报错”。没有测试的技能本质上只是一段不确定的代码你根本无法知道 Agent 在某个场景下调用它会产生什么行为。2.2 技能库的标准目录结构参考 agent-skills 社区里比较成熟的实践我推荐下面这个目录组织方式skills/ ├── meta.yaml # 技能库全局配置与加载开关 ├── common/ # 共享工具与公共依赖 │ ├── http_client.py │ └── validators.py ├── data_processing/ │ ├── excel_to_json/ │ │ ├── skill.py │ │ ├── test_skill.py │ │ └── README.md │ └── csv_cleaner/ │ ├── skill.py │ └── test_skill.py ├── api_integration/ │ ├── weather_query/ │ │ ├── skill.py │ │ └── test_skill.py │ └── stock_price/ │ ├── skill.py │ └── test_skill.py └── formatters/ └── markdown_table/ ├── skill.py └── test_skill.py每个技能独立成目录目录名就是技能归属的领域。这样做的直接好处是Agent 在加载时可以通过目录前缀做初步过滤减少语义匹配的搜索空间。比如用户问“帮我把这份表格整理一下”系统可以先锁定 data_processing 和 formatters 两个目录而不是把全部技能都拉出来比较一遍既能降低模型混淆的概率也能减少 token 消耗。2.3 命名与描述的实操心得命名这件事我踩过不少坑。早期我给技能起名喜欢带很长的业务前缀比如 get_data_from_bi_server_by_project_id结果模型经常识别不完整。后来我总结了几条规则基本上可以避免这类问题。规则一名称用小写加下划线控制在 3 个单词以内。规则二名称要反映“动作对象”比如 send_email、parse_resume不要用抽象名词如 helper、utils。规则三和业务强相关的技能保留业务关键词纯粹通用的技能不要带公司名或项目代号提升复用性。描述部分的措辞也值得琢磨。不要写“此函数可以用于获取数据”这种没有区分度的句子而要写“当用户需要查询订单物流状态时使用支持快递单号和订单号两种查询方式”。描述其实就是给模型的一份“使用说明书”越具体模型的调用准确率越高。我对比过同一批技能在描述改写前后的调用准确率从 61% 提升到了 88%幅度相当可观。3. 实操从零搭建一个可用的技能库3.1 先确定第一批技能从哪里来动手之前先别急着写代码。我建议把产品需求里最高频的 10 个操作列一张清单然后逐个判断哪些适合做成技能判断标准是三条——是不是重复发生、是不是有确定性的输入输出、是不是需要真实执行操作。满足这三条的优先做。比如“查天气”“算运费”“转格式”都是很好的候选。而“写一段优美的文案”这种高度开放、没有确定性输出的任务就不适合封装成技能它应该还是走模型直接生成的路子。把适合模型做的留给模型把适合代码做的交给技能这个边界越早划清后面返工越少。3.2 编写第一个技能完整代码示例为了让你有直观感受我拿一个实际用过的技能来拆解。假设我们要做一个“根据人名和工作日天数计算应发工资”的技能这个技能需要调用一个本地薪资计算函数并包含假期忽略逻辑。完整实现如下。# pay_calculator.py from skill_lib import Skill, Parameter class CalculateSalarySkill(Skill): name calculate_salary version 1.0.0 description ( 根据员工姓名和当月实际出勤天数计算应发工资。 适用于计算固定月薪员工的应发金额不包含绩效、补贴和扣款。 若涉及绩效请调用另一个技能 calculate_performance_allowance。 ) parameters [ Parameter(employee_name, str, description员工姓名必填), Parameter(work_days, int, description实际出勤天数范围 1-31, ge1, le31), Parameter(monthly_salary, float, description月薪标准单位元, gt0), ] def run(self, employee_name: str, work_days: int, monthly_salary: float) - dict: if work_days 1 or work_days 31: raise ValueError(fwork_days 必须在 1-31 之间收到: {work_days}) daily_rate monthly_salary / 21.75 # 法定月平均计薪天数 gross daily_rate * work_days return { employee_name: employee_name, gross_salary: round(gross, 2), daily_rate: round(daily_rate, 2), message: f{employee_name} 的应发工资为 {round(gross, 2)} 元 }这段代码里有几个设计细节值得说明。第一description里明确写清了技能的边界——不算绩效、不算扣款同时给出了替代技能的指引模型在遇到绩效需求时就不会错误调用这个技能。第二parameters里的约束条件ge、le、gt是校验层强制执行的不依赖模型自觉这可以在参数传入run之前就把非法值挡回去。第三run方法内部也有防御性校验即使外层校验被绕过函数自身也不会吐出荒谬结果。3.3 给技能配测试没有测试不叫技能技能一旦要被模型调用就相当于进入了生产环境它的行为必须是可回归验证的。我给技能写的测试分两层接触面一层针对纯逻辑一层模拟真实调用。# test_pay_calculator.py import pytest from pay_calculator import CalculateSalarySkill def test_normal_case(): skill CalculateSalarySkill() result skill.run(张三, 21, 21000) assert result[gross_salary] 21000 def test_min_work_days(): skill CalculateSalarySkill() result skill.run(李四, 1, 21000) assert result[gross_salary] pytest.approx(965.52, rel1e-2) def test_invalid_work_days_raises(): skill CalculateSalarySkill() with pytest.raises(ValueError): skill.run(王五, 32, 21000)这套测试的价值在于当技能后续升级比如把 21.75 改成按当月实际计薪天数回归测试就会直接把行为变化暴露出来迫使你审视改动是否合理。而如果没有测试这类参数调整往往要等线上用户投诉才能发现。3.4 Agent 如何发现并加载技能有了技能文件和测试下一步就是在 Agent 运行时把技能自动加载进来。我实现过一个轻量级的技能发现器核心逻辑不复杂就是扫描技能目录、解析元信息、注册到调用路由表里。import importlib.util import os from typing import List def discover_skills(skill_dir: str) - List[object]: skills [] for root, _, files in os.walk(skill_dir): for file in files: if not file.endswith(.py) or file.startswith(test_): continue path os.path.join(root, file) spec importlib.util.spec_from_file_location(file[:-3], path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) for attr in vars(module).values(): if isinstance(attr, type) and hasattr(attr, name) and hasattr(attr, run): skills.append(attr()) return skills这段代码有几个刻意处理的点。第一跳过test_开头的文件避免把测试文件当技能加载。第二通过hasattr(attr, name)和hasattr(attr, run)双重判断来识别技能类而不是靠继承固定基类这样兼容性更好。第三使用importlib动态导入保证新增技能不需要重启整个 Agent 服务开发体验好很多。加载之后我一般还会生成一份“技能清单”缓存下来包含每个技能的 name、description、parameters 摘要在每次对话时随系统 prompt 一起发给模型。清单不宜太长否则会挤占上下文窗口所以我的做法是每次先根据用户输入的关键词做一次粗粒度过滤只把可能相关的 10 到 15 个技能描述放入 prompt这样既准确又省 token。4. 常见问题与排查实录4.1 模型总是调用错误的技能这个问题出现频率最高几乎每个初用 agent-skills 的团队都会碰到。典型表现是用户想查天气模型却去调用了“日期计算”技能因为两者描述里都有“今天”“明天”等词。排查思路要看描述重叠度。如果两个技能在语义上容易被混淆就在其中一个的描述里明确加上“不适用”的负面条件。比如天气查询可以写“此技能不具备日期计算能力如需计算两个日期之间的天数请使用 date_diff”。这类负面约束通常一两句就够不需要长篇大论。还有一种情况是技能数量太多模型在选择时直接迷失。这时候不要继续往 prompt 堆描述而是要把技能做分组或合并。我见过一个项目把 60 多个技能一口气全塞给模型准确率只有 34%。后来按领域分组、每组抽一个代表技能先行路由准确率拉到了 79%。所以技能库不是越大越好而是越清晰越好。4.2 参数 schema 与实际实现对不上这种情况通常发生在多人协作或快速迭代时。有人在元信息里写着参数timeout类型是 integer但实现代码里实际当成字符串用模型传了 30 进来代码拼接 URL 时直接报 TypeError。我的建议是给技能配置加一层“契约校验”在run方法执行前自动比对传入参数和 schema。这里可以用一个轻量级装饰器实现。from functools import wraps from typing import Callable def validate_params(schema: dict): def decorator(func: Callable): wraps(func) def wrapper(*args, **kwargs): for pname, pconf in schema.items(): if pname in kwargs: ptype pconf.get(type) if ptype int and not isinstance(kwargs[pname], int): raise TypeError(f参数 {pname} 需要 int实际收到 {type(kwargs[pname])}) return func(*args, **kwargs) return wrapper return decorator这只是一个极简版本生产环境建议直接用 pydantic 之类的库来做完整校验。接上之后至少能把类型不匹配的问题从“运行时爆炸”提前到“调用即报错”配合测试回归周期会短很多。4.3 技能文件出现冲突与优先级问题当技能库发展一段时间后很可能出现两个技能功能重叠的情况。比如新人看到 get_location_from_ip 这个技能没意识到已经有一个 get_location_from_phone 也能间接拿到位置信息于是都注册进了路由表。模型在调用时就可能随机选一个导致结果不稳定。处理手段有两种。第一种是硬规则在技能路由表里允许为每个领域设置唯一的“默认技能”同领域其他技能只有在该默认技能被显式声明不可用时才进入候选列表。第二种是软规则在技能描述里互相引用比如旧技能写明“如果你需要 IP 定位请优先使用 get_location_from_ip”。我实际用下来软规则对模型更友好因为它让模型自己“理解”优先级而不是被规则硬卡死。另外每季度最好做一次技能库的“冗余扫描”把重复或高度相似的技能合并。合并前看一眼各自的历史调用日志尽量保留调用量高的那个另一个作为别名兼容避免破坏已有流程。4.4 技能在真实调用场景中行为不稳技能测试通过了但放到生产环境还是偶发失败。这类问题十有八九出在外部依赖上常见的有第三方 API 限流没做重试、网络超时设置太短、外部返回格式变化没做兼容。我给每个技能整理了一张“外部依赖清单”写清楚它依赖哪些网络服务、密钥存放位置、超时和重试策略。然后统一封装一个 http_client内置指数退避重试和超时配置。与其在单个技能里复制粘贴重试逻辑不如在 common 层做一件共享的“雨衣”这样每个技能的内部代码会清爽很多。4.5 常见问题速查表现象可能原因排查顺序模型调了无关技能描述之间存在语义重叠先看描述中是否有负面约束再检查技能分组参数经常传错类型schema 与实现不一致先校验 schema 类型再看示例值是否清晰多个同类技能随机命中缺少优先级标注先加描述互引再考虑路由表硬规则技能测试通过但线上失败外部环境差异先看依赖服务和密钥再看超时重试策略技能加载后没生效目录扫描路径配置错误先打印加载日志再确认目录结构这个速查表是我在维护技能库时反复翻看的一份资料每次排障都能按图索骥节省大量时间。5. 技能的复用、演进与团队协作5.1 让技能库变成团队共享资产技能库做出来之后不应该只属于 Agent 应用的代码仓库它还应该成为团队的共享资产。我的做法是单独建一个 skills 仓库代码审查流程和业务代码一样严格但文档要求更高。每个技能除了代码必须有 README写明适用场景、不适用场景、依赖清单、变更历史。这样做的好处是后续新人在给 Agent 加能力时先到技能库里搜一轮大概率能发现已有技能可以直接复用而不是从零写一个新的。我统计过合理的技能库复用机制能让团队新增功能的时间平均缩短三分之一。还有一个容易被忽略的点技能库的 API 设计要相对稳定。业务代码可以频繁重构但技能库一旦被多个 Agent 流程引用改动就必须走兼容升级不要轻易删除技能或改变参数含义。5.2 技能版本的平滑演进技能也是有生命的它会随着业务变化而迭代。我的演进策略分三步先加新参数并标记为可选老调用不受影响跑一段时间的影子模式让新逻辑和老逻辑并行执行并对比结果确认稳定后把新参数设为必选移除废弃分支。在 agent-skills 的语境下版本管理其实不复杂核心就是“不破坏已有调用约定”。如果非破坏不可比如参数名改动那就必须保留一层兼容适配器把旧参数名映射到新参数名同时打印警告日志提醒调用方尽快升级。这样至少能保证接入方不会被突然打断。5.3 最后一个实用经验从调用日志里反向迭代技能在技能库上线稳定之后我建议大家养成定期翻调用日志的习惯。日志里有两类信息价值极高一类是模型纠结后放弃调用的记录说明技能描述还不够清晰另一类是多次调用后才成功的路径说明存在候选技能排序不合理的问题。我每月会做一次这样的分析选出调用准确率最低的 5 个技能针对性地优化描述和示例。坚持下来技能库的准确率是能持续爬升的。不要只看功能做得好不好更要看模型“用起来顺不顺”这决定了 Agent 整体的智能感。根据我个人经验把 agent-skills 这类技能库做扎实远比在 prompt 上做表面功夫更值得投入。每次给 Agent 新增一个技能就好比为它多配备了一件称手的工具日积月累它能独立完成的任务边界会明显扩大。希望这篇实践记录能帮你在做 Agent 技能沉淀时少走一些弯路。
返回列表