ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

手把手搭建Agent技能库:从Function Calling到工具调用工程落地

手把手搭建Agent技能库:从Function Calling到工具调用工程落地 做 Agent 开发的朋友应该都体会过那种感觉模型动不动就能引经据典、侃侃而谈可真让它干点正事——查个天气、算笔账、改个文件名——它就原地傻眼。原因很简单LLM 没有手既摸不到外部世界的实时数据也没法主动改变任何系统状态。agent-skills 这个项目概念核心就是解决这个问题——把“模型会说话”变成“模型会干活”。这篇文章我会从技能库的架构设计讲到具体落地给出一套既能跑通、又能扛住真实业务的技能化方案适合正在做 Agent 原型验证、或者已经卡在工具调用稳定性上的开发者参考。先说我自己的背景。过去一年多我一直在做企业级 Agent 平台前后给客服、运维、数据分析几个场景搭过技能系统踩过不少文档里不会写的坑。agent-skills 这个方向看起来简单实际做起来涉及技能建模、参数约束、调用链路、错误恢复、动态扩展一堆细节。这篇就当是阶段性的经验复盘想到哪写到哪。1. agent-skills 到底是什么从“会说话”到“会干活”1.1 核心概念拆解agent-skills 说白了就是给大模型配一套可以随时调用的“技能库”。这里的技能不是指 prompt 里写几句提示词而是指有完整定义、有参数约束、有对应执行逻辑的工具模块。每个技能包含三样东西技能名称、技能描述、参数 Schema。模型在对话过程中根据用户请求的意图主动挑选一个或多个技能生成结构化的调用参数然后由运行时环境真正执行这些技能。我习惯用一个生活化的类比来解释大模型像一个新入职的实习生脑子聪明知识面广但什么实际业务都不会。agent-skills 相当于给他一本《岗位操作手册》上面写着“打印文件时使用打印机技能参数为文件路径、打印份数、单双面”。实习生不需要知道打印机驱动怎么写只需要按手册勾选参数剩下的活由技能实现者搞定。这个机制的技术基础叫 function calling也叫 tool calling。主流大模型对外提供的 API 里都有对应接口你把技能列表以 JSON 格式传给模型模型在需要时返回一个结构化的调用指令而不是直接输出自然语言。运行时解析这个指令执行代码再把结果喂回给模型模型继续后续的推理。1.2 为什么技能化设计比“让模型自由发挥”更靠谱我在早期做 Agent 的时候习惯把所有能力写进系统提示词里你可以用 python 执行你可以访问数据库你可以读取文件……结果就是模型经常“想当然地执行”它以为自己执行了实际上什么都没发生或者干脆生成一段永远跑不起来的伪代码。教训很明显你越给模型抽象的权力它越容易发挥想象力。技能化设计把抽象能力变成确定性的接口。每个技能背后都是经过测试的代码它的输入有约束输出有格式异常有兜底。模型只能在这些边界内做选择不能自由发挥。这样一来系统的可测试性、可观测性、安全性都上了一个台阶。另外技能是可复用的资产。一个“发送邮件”技能今天在客服 Agent 里用明天在审批 Agent 里也能用。技能库越攒越厚新项目启动就越快。我们内部现在有个原则凡是抽象逻辑出现第二次就封装成技能出现第三次就重构成公共技能库。这跟代码重构里“三次法则”是同一种思路。2. 技术架构与技能注册机制2.1 三种主流实现架构对比技能库设计最核心的一个问题是技能怎么组织、怎么被模型发现。我见过三类主流做法各有优劣。第一种是集中式注册表。所有技能启动时在一个全局 Registry 里完成注册生成技能清单请求模型前统一注入。优点实现简单技能可见性高排查问题时直观。缺点所有技能无论是否相关都会被注入当技能数量很大时token 消耗会剧增。第二种是目录式自动发现。把每个技能写成一个独立文件放在固定的 skills 目录下运行时自动扫描、加载、注册。这种方案可扩展性很好新增技能不用改主代码适合团队并行开发。缺点需要处理依赖加载顺序和命名冲突调试起来要查文件系统。第三种是能力域分组。按场景或者领域把技能分组再通过路由策略只注入与当前对话相关的技能组。这种方案在技能数量达到几百个之后几乎是必选项否则模型面对几百个候选技能选择准确率会肉眼可见地下降。代价是你要额外设计和维护一套分组与路由逻辑。架构选型本质上取决于技能规模。如果你只做五六个工具的 Demo第一种最省事如果你在做正式产品直接从第二种起步预留第三种的扩展位。我见过不少团队一上来就想做动态加载和语义路由结果项目还没跑通先被架构复杂度拖垮了。2.2 技能描述与参数 Schema决定成功率的关键很多人在 function calling 上翻车不是模型不够聪明而是技能的描述和参数写得太烂。模型判断“该不该调用这个技能、参数怎么填”完全依赖你提供的描述文本和字段定义。描述写得含糊模型就会犹豫犹豫就会编一个参数给你。我先给一个反例{ name: send_message, description: 发送消息, parameters: { type: object, properties: { content: {type: string} }, required: [content] } }这个描述等于没写。发送什么消息发送到哪通过什么渠道模型不知道只能猜。再来一个正例{ name: send_wecom_message, description: 向指定的企业微信群机器人发送文本消息。适用于向某个工作群推送告警、通知、报告等场景。群机器人的 webhook 地址在技能配置中预先绑定。, parameters: { type: object, properties: { content: { type: string, description: 要发送的文本内容最长 4000 字 }, mentioned_list: { type: array, items: {type: string}, description: 需要 的成员手机号列表可为空数组 } }, required: [content] } }一眼就看得出差距。正例里模型知道这个技能是做什么的、适用于什么场景、参数边界在哪。我把描述准则总结成一句话假设使用者完全不了解你的业务你要用一段话让他决定何时使用、何时不用。参数 Schema 方面我的建议是尽量精细化。能写 enum 就写 enum能给 default 就给 default能在描述里注明格式约束就注明。模型不是人它不会主动追问你给的信息越完整它编错的概率越低。3. 实操手把手搭建一套 agent-skills 技能库3.1 基础工程结构与技能注册器实现我直接给出一套我在项目里实际用过的轻量实现。用 Python 写核心依赖是 Pydantic 做参数校验。工程目录大概是这个形态agent_skills/ ├── core/ │ ├── registry.py # 技能注册器全局唯一 │ ├── schema.py # 技能定义与参数模型 │ └── executor.py # 技能执行器负责调用与结果包装 ├── skills/ │ ├── weather.py # 天气查询技能 │ ├── calculator.py # 计算器技能 │ └── git_tools.py # Git 操作技能 ├── agent/ │ └── runner.py # 接入 LLM API 的运行链路 └── main.py # 入口启动加载registry.py 的核心逻辑是维护一个名称到技能对象的映射同时提供注册和获取两个接口。我选了最简单的注册表模式因为起步期你最大的敌人是过度设计而不是扩展性。# core/registry.py from typing import Dict, Type from core.schema import BaseSkill class SkillRegistry: 技能注册表保存所有已注册的技能定义。 _skills: Dict[str, Type[BaseSkill]] {} classmethod def register(cls, skill_cls: Type[BaseSkill]) - Type[BaseSkill]: 将技能类注册到全局注册表。 if skill_cls.name in cls._skills: raise ValueError(f技能名称冲突: {skill_cls.name}) cls._skills[skill_cls.name] skill_cls return skill_cls classmethod def get(cls, name: str) - Type[BaseSkill]: if name not in cls._skills: raise KeyError(f技能未注册: {name}) return cls._skills[name] classmethod def all_skills(cls) - list: 返回所有技能定义用于注入到 LLM 上下文中。 return [skill_cls.to_definition() for skill_cls in cls._skills.values()]这里有一个容易踩的坑技能名称冲突。团队并行开发时两个人很可能都写了search技能一个搜数据库一个搜文件系统。所以在注册时一定要做重名校验宁可启动时报错也不要在运行时悄悄覆盖。3.2 用装饰器技能定义与参数校验按惯例我定义一个抽象基类BaseSkill每个技能只需要实现execute方法。参数校验放在基类里通过 Pydantic 自动完成。# core/schema.py from abc import ABC, abstractmethod from typing import Dict, Any from pydantic import BaseModel, Field, ValidationError class BaseSkill(ABC): name: str description: str parameters: Dict[str, Any] {} classmethod def to_definition(cls) - dict: 将技能转换为 LLM 工具接口格式。 return { type: function, function: { name: cls.name, description: cls.description, parameters: { type: object, properties: cls.parameters, required: cls.required_fields } } } abstractmethod def execute(self, params: dict) - str: 执行技能返回结果字符串。 参数 params 是模型生成的 JSON 对象执行前需要校验。 def run(self, params: dict) - dict: 统一的执行入口封装异常与校验。 try: validated self._validate(params) result self.execute(validated) return {success: True, result: result} except ValidationError as e: return {success: False, error: f参数校验失败: {e.errors()}} except Exception as e: return {success: False, error: str(e)} def _validate(self, params: dict) - dict: # 这里用 Pydantic 动态创建校验模型略去具体实现 return params然后你写技能的时候就很简单了。以天气查询为例# skills/weather.py from core.registry import SkillRegistry from core.schema import BaseSkill SkillRegistry.register class WeatherSkill(BaseSkill): name get_weather description 获取指定城市当天和未来 3 天的天气预报。当用户询问天气、气温、降雨概率时使用。 parameters { city: { type: string, description: 城市中文名如杭州、上海 }, days: { type: integer, description: 查询天数1 表示今天3 表示未来 3 天, default: 1 } } required_fields [city] def execute(self, params: dict) - str: city params[city] days params.get(days, 1) # 这里换成真实天气 API 调用 return f{city}未来{days}天天气晴转多云气温 22-28℃装饰器注册的方式新手友好能很直观地看到技能注册的过程。注意name、description、parameters是类属性必须定义完整缺一个后面生成工具列表时就会出问题。3.3 接入模型调用链路与核心执行循环现在到了关键环节怎么让模型在对话中自动调用这些技能。核心执行循环通常是四步将注册表里的所有技能定义传给模型接口模型返回自然语言回复或工具调用请求如果有工具请求执行对应技能把结果附加到对话消息列表再次把完整的对话历史发给模型直到模型不再请求工具。这一轮我贴一段伪代码能跑通主流程# agent/runner.py from core.registry import SkillRegistry def run_agent(user_input: str, messages: list, llm_func): messages.append({role: user, content: user_input}) for _ in range(5): # 限制循环次数防止模型无限调用工具 response llm_func(messages, toolsSkillRegistry.all_skills()) if response.tool_calls: for tool_call in response.tool_calls: skill_name tool_call.function.name args json.loads(tool_call.function.arguments) skill_cls SkillRegistry.get(skill_name) result skill_cls().run(args) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) else: return response.content return 达到最大工具调用轮次限制这里有两个经验值得说一说。第一循环次数一定要设上限。我见过模型在某个工具结果不符合预期时反复调用同一个工具十几遍最后把自己绕晕。设个 3 到 5 次的上限配合结果中的错误信息让模型有机会调整策略但别让它无限重试。第二工具结果一定要是可以被模型理解的文本。不要直接返回一个 Python 对象或者一个裸的 JSON 堆栈。给模型的字符串越规整它后续的总结能力就越好。我在执行器里会做一个统一处理成功时返回结果摘要失败时返回错误信息加简短排查提示。def execute_with_context(skill, params): result skill.run(params) if result[success]: return f[技能执行成功] {result[result]} else: return f[技能执行失败] {result[error]}请检查参数或联系管理员3.4 技能清单的动态裁剪与按需注入如果技能库规模很小每次都把全部技能注入到上下文里没什么问题。但一旦技能超过二三十个模型的选择准确率会明显下滑同时 token 消耗也在持续飙升。我的做法是引入一个简单的路由层把技能打上标签根据用户当前对话的分类来裁剪候选集。比如有finance、it、hr三个标签用户问的是“报销流程怎么走”就只注入finance标签下的技能其他的不注入。路由层不需要做得很复杂。先用一个分类模型或者简单关键词规则判断意图域然后从注册表里挑选对应技能注入。我实际测试过一个不错的指标候选技能从 50 个降到 8 个之后工具选择的准确率从 82% 提升到 96%代价是 token 消耗减少了大半。如果你不想引入额外的分类模型也可以用另一种更轻的方案把技能分成“通用技能”和“领域技能”。通用技能如计算器、查日历始终注入领域技能按场景分组、按需激活。通用技能数量控制在十个以内领域技能组之间再做一个互斥规则。这种方式实现成本低效果也不错。考虑到很多团队的 Agent 一开始就奔着“什么都干”去的反而应该从“分组限定”开始再逐步开放规模这是我踩坑之后反推出来的结论。4. 常见问题与排查技巧实录4.1 模型返回的参数永远对不上 Schema最典型的报错是ValidationError模型生成了{ user: 张三 }但技能期望的是{ name: 张三 }。排查思路是回看注入的 Schema 里字段名、类型、必填项与模型实际输出的差距。我处理这类问题有一套固定步骤把模型实际返回的原始 arguments 抓出来打印到日志里对照着你定义的 Schema 检查是不是字段名跟描述文案不一致检查是不是required把太多字段设为必填。模型一旦判定期望字段缺失就会编造一个值。能设默认值的就别设为必填看描述里是否给出了示例值。模型对示例值非常敏感一个好例子能显著提升参数命中率。我举一个真实案例。早期我的“创建工单”技能要求参数里有priority描述写的是“优先级”没有给枚举值。模型有时候传high有时候传High有时候传紧急导致下游解析失败。后来我把参数改成enum: [低, 中, 高]并在描述里注明“必须是三者之一”这个问题再没出现过。4.2 技能描述含糊导致模型该用不用这个问题比参数错误更隐蔽。你有个数据库查询技能描述写“执行 SQL 查询”结果用户问“上个月订单总量”模型死活不肯调用它而是自己编了个数字。原因是模型不知道这个技能能回答这类问题。描述里只写了“什么是这个技能”没有写“什么场景下使用这个技能”。我后来会在每个技能描述里固定加一句“当用户询问 X 类型信息时应使用此技能”相当于给技能画了一个清晰的触发范围。写描述还要避免过于宽泛。我有个同事写过一个技能描述叫“执行日常操作”模型把所有操作都往它身上套结果日常操作接口里又没有相应逻辑整个 Agent 行为直接乱了。技能描述要表达的是“我是专才不是通才”把适用场景边界说得越窄模型调用反而越准确。4.3 多技能冲突与命名空间设计当技能库逐渐变大你会遇到名称冲突和职责重叠的问题。刚才提到的search就是典型的冲突重灾区。我建议做两件事一是命名上加入领域前缀比如db_query_order、file_search_report而不是裸的search二是在技能描述里明确写“本技能只负责 XX不处理 YY 情况”把边界画出来。职责重叠更麻烦。你有“get_user_info”和“get_user_orders”用户问“帮我查一下张三的账户情况”模型可能两个都调也可能一个都不调。我的建议是定义复合技能把相关操作聚合到一个技能里技能内部自己做分支。这样模型面对的技能粒度更粗决策负担更小。技能也不是越小越好合理的粒度取决于你希望模型做多少步推理来决定调用哪个工具。4.4 上下文膨胀与执行超时每轮工具调用都会把工具结果追加到消息列表里多轮下来上下文很容易爆炸。我在实际运行中碰到过某次对话累计达到 30 万 token调一次模型二十几秒用户体验直接崩盘。缓解手段有三个控制最大工具调用轮数别让模型无限追问给工具结果做摘要。返回给模型的不是完整查询结果而是“本次查询共返回 87 条记录前 5 条为……”定期对早期对话做压缩或者滑动窗口裁剪只保留最近几轮关键消息。这三个手段都不复杂但收益非常明显。尤其是工具结果摘要很多人会忽略。模型不需要看 80 条原始数据它只需要基于总结继续往下推理就行。4.5 常见问题速查表现象常见原因解决建议模型不调用技能描述不够明确无法判断适用场景补充“当用户询问……时使用”句型收窄技能职责参数校验报错字段名、类型、枚举值不匹配参数描述加示例值必填字段设默认值多个技能同时触发技能职责重叠合并成复合技能或加领域前缀区分工具结果模型看不懂返回内容太复杂、无结构用固定格式摘要成功/失败标记清晰上下文爆炸、响应慢工具结果过大历史消息过多裁剪结果、压缩早期消息、设置最大轮次技能升级后模型表现下降行为变化导致模型推理路径改变技能版本号纳入日志做 A/B 回归测试排查这类问题我的一大心得是先看日志升级到固定格式日志时间戳、技能名、入参、出参、耗时、错误信息后以前靠猜的事故全都变成了可复现的定位。5. 从技能库到技能编排扩展思路与实测心得5.1 技能编排让多个技能协作完成复杂任务单个技能解决的是“一件事”但实际业务往往是“一串事”。比如“帮我把最新的销售周报整理一下发给 leader”这个需求可能涉及读取报表、生成摘要、查找收件人、发送邮件四个环节。如果你把四个技能丢给模型让它自己想编排模型很容易出错因为它不知道这四个动作之间的依赖关系。我在这方面的实践是引入工作流模板把技能编排的路径预先定义好。工作流模板本质上是一个有向无环图节点是技能边是依赖关系。Agent 接收用户请求后先匹配工作流模板再按模板顺序调度技能。这样模型不需要想“下一步该干嘛”只需要按流程执行。这种方式带来的好处是稳定性大幅提升。自由编排的准确率可能只有 60%配上固定工作流之后能到 90% 以上。缺点是灵活性下降了处理不了模板之外的请求。我的策略是两层结构先尝试匹配模板模板匹配不上再退化为模型自由选择技能。这种“先规矩、后自由”的路线在业务中表现比较稳定。5.2 技能库维护的版本管理与灰度控制技能代码本身会迭代而模型对技能“行为变化”非常敏感。你刚把某个技能从同步调用改成异步调用模型可能还是按旧逻辑等待结果行为就乱了。这是技能库维护里最容易被忽视的风险。我现在强制要求所有技能接口保持语义稳定只允许内部实现变化不允许对外输入输出格式变化。如果确有必要调整参数结构或返回格式就在技能名称后面加版本号比如send_message_v2并且在下游工作流里做切换而不是直接覆盖旧版。这样即使用户的旧会话还在跑也不会因为技能行为突变导致失败。日志层面我也加了一层技能级监控每个技能执行的耗时、成功率、错误分布都单独看。哪段时间某个技能成功率掉下去了马上能定位到代码变更或者提示词调整。这个监控在技能规模还小的时候看不出价值等技能数量上到几十个它就变成排查问题的主力工具。建议不要把监控想得太复杂简单记录入参、出参、错误信息、耗时四项就够。5.3 我在项目实战中积累的几条独家经验写到这里分享几个我在实战里反复印证过的体会。技能描述是一个要反复打磨的文本工程。我见过很多人把时间花在算法选型和架构设计上却不愿意花半小时打磨技能描述。实际上在模型能力固定的前提下描述质量决定了上限。同样的技能描述差一个量级调用准确率差二十个百分点都很正常。另一个容易被忽视的点是技能结果给模型的反馈信息要“结构化”。除了成功的结果内容还要有足够的错误上下文。比如数据库技能执行失败时返回的信息里应该包含“表不存在”还是“连接超时”模型才能给出正确的补救方案。我见过模型因为不知道具体错误原因在同一个错误上反复重试的情况那就是给它的反馈太单薄了。不要一上来就贪多求全。技能库是慢慢长起来的不是一步到位的。先从三五个高频技能开始跑通链路再逐步往库里加。很多人一开始就仿照 OpenAI 的官方示例塞了十几个技能进去结果模型面对一长串候选选择困难行为飘忽不定。技能数量要跟模型的上下文窗口和推理能力匹配这个平衡点只能用自己的数据测出来。我的经验是宁可让技能少而精少让模型做无谓的“选哪个好”多把每个技能的准确率打磨到极致。最后想强调的是无论你的 Agent 应用在哪个行业agent-skills 这个思路的核心价值都一样把不可控的模型推理变成可控的工具调用把模型的“行为随机性”驯化在一个个明确的边界里。用户问一个模糊问题的时候模型负责理解意图模型确定要做什么的时候技能负责把事做对。各司其职Agent 才真正可靠。
返回列表