news 2026/9/25 8:20:04

Agent开发实战:用结构化技能库解决工具管理难题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent开发实战:用结构化技能库解决工具管理难题

做Agent开发这段时间,我踩得最深的坑,不是模型能力不够,而是"工具管理"这块烂摊子。业务方提需求很快,今天加个查天气的接口,明天补一个数据库查询的权限,后天再来一个导出报表的动作,一开始我全都堆在系统提示词里,塞了三十多个函数定义,结果模型开始胡言乱语,该调的工具不调,不该调的乱调,上下文还被占得差不多了。

后来我把所有工具整理成一个结构化的"技能库",给每个动作写清楚描述、参数、使用约束,并且让模型只能从库里挑技能来执行,整个系统的正确率一下子提上来了。这个方案我内部管它叫 agent-skills,它不是某个特定框架里的东西,而是一套组织Agent能力的方法论。这篇文章不聊理论,直接讲我怎么设计、怎么落地、遇到哪些坑,给正准备搞Agent工具层的人一个能直接上手的参考。

1. 内容整体设计与思路拆解

1.1 为什么Agent不能只靠"堆函数"

先说一个最直白的现象:当工具数量少于五个的时候,怎么传都行,模型基本能选对。但只要超过十个,尤其是函数之间长得有点像,就开始出问题。我见过一个项目里定义了 get_user_info 和 get_user_profile 两个接口,数据源完全不同,但描述写得几乎一样,模型选哪个全靠猜。

问题本质在于:LLM不是通过"代码逻辑"来调用工具的,它是通过"文本匹配"来决定调哪个工具。这意味着你的函数名、参数名、描述文本,本身就是在写提示词。如果这些信息组织得不好,模型就相当于拿到了一份混乱的说明书,自然没法稳定执行。

所以 agent-skills 的核心思路,就是放弃"把工具塞进代码里让模型随便调"的简单做法,改成"把Agent能做的所有事情,抽象成一份可被检索、可被理解、可被执行的技能清单"。模型不再直接面对一堆函数,而是先看到"技能列表",再根据用户意图选择技能,最后才走到执行层。

1.2 技能库的三个核心分层

我设计这套体系的时候,参考了人在职场里的分工逻辑:能力、任务、执行。对应到系统里就是三层抽象。

第一层是能力层,也就是这个Agent到底会做什么。比如"查询订单状态""计算运费""生成周报",这些描述要面向业务语义,而不是面向代码实现。第二层是参数层,也就是每个能力需要哪些输入、返回什么结构。这一层必须严格、明确,因为模型要根据参数Schema来生成调用请求,模糊的参数定义直接等于漏洞。第三层是策略层,也就是什么时候用这个技能、什么时候绝对不能用。比如"仅当用户明确要求导出数据时才调用导出技能""没有用户授权前禁止调用删除接口",这类约束必须写进技能描述里,而且要写得像规则而不是建议。

这三层合在一起,才是一个完整的"技能"。只写函数签名不写用途,那叫接口文档;只写用途不写限制,那叫宣传文案。Agent技能描述必须同时满足"让模型看懂"和"让系统安全"两个目标。

1.3 设计目标:可发现、可组合、可降级

动手之前,我给这套技能库定了几个硬性目标,这些目标直接影响后面所有的设计决策。

第一可发现:技能列表不能让模型一次性读完,否则技能多了照样爆上下文。所以要有检索机制,根据用户当前输入动态选出最相关的5个技能。第二可组合:复杂任务不能只靠一个技能搞定。比如"查完天气再推荐穿衣"至少要组合两个技能,所以技能描述里要支持"关联技能"提示。第三可降级:任何一个技能都可能失败,可能是外部接口挂了,也可能是模型生成了非法参数。系统必须有降级路径,不能一遇到报错就整体崩溃。

这三个目标听起来很理想,但落地时会遇到大量细节问题,后面每一节都会围绕它们展开。

2. 核心细节解析与实操要点

2.1 技能描述怎么写才不会被模型忽略

这是整个agent-skills体系里最容易被低估的部分。很多人写的技能描述是"获取用户信息",完了。这种描述对模型来说信息量几乎为零。我整理了一套自己的写法模板,每条技能描述基本固定为四段式:动作动词开头 + 目标对象 + 触发条件 + 反面约束。

举个例子,同样是获取用户信息,我会写成:获取当前登录用户的个人资料,包括姓名、手机号、邮箱、会员等级。当用户询问"我的账户信息""个人资料""我的会员等级"时使用。只有在用户明确提到查询自身信息时才可调用,禁止在未确认用户身份的情况下调用。

你仔细品一下这段话,每句话都有信息量。第一句告诉了模型这个技能"能做到什么程度",第二句给了一组高频触发短语,第三句堵住了误用的口子。我实测下来,描述分四段写之后,误调用率降了差不多40%。

还有一个心得:反面约束要尽量具体,不要写"谨慎使用",要写"禁止在XX情形下调用"。"谨慎"是模糊词,模型不知道怎么执行,但"禁止+场景"是明确指令,效果好得多。

2.2 参数Schema设计里的三个典型坑

参数是模型调用技能时的"输入表单",Schema定义得不好,模型就会频繁出错。我总结了自己遇到最多的三个坑。

第一个坑是参数类型过于宽泛。比如一个日期参数,如果你定义成"string",模型可能传"明天""下周三"这种自然语言,导致后端解析失败。我的做法是:规范为 yyyy-MM-dd 格式,并在描述里写明"仅接受标准日期格式,不接受相对时间表达"。同时可以在技能层做一个时间解析的前置处理。

第二个坑是缺少枚举值限制。比如订单状态这个参数,如果后端只有 pending / paid / shipped / cancelled 四种状态,但你Schema里只写"string",模型就可能传"已完成""待付款"这类业务用语。解决方式是明确枚举,或者给一个"状态映射表"写在技能描述里。

第三个坑是可选参数策略模糊。很多技能有必填参数和可选参数,如果你的描述不区分,模型就会尽力把所有参数都填上,反而填错。我的建议是必填参数单独列一行,可选参数前加"可选"。这样模型在不确定的时候更倾向于省略而不是瞎编。

2.3 错误处理必须做成"结构化反馈"

技能执行失败是常态,但很多Agent系统的问题在于:失败之后没有给模型足够的反馈信息。模型调了个查库存的技能,接口返回500,系统只回一句"调用失败",模型根本不知道是参数错了还是服务挂了,只能再调用一次,结果还是一样,陷入死循环。

我改成结构化错误反馈之后,情况好转了很多。具体做法是:每次执行失败,返回给模型的信息包含三个部分——错误码、可读描述、可执行建议。比如"ERROR_PARAM_INVALID:日期格式不正确,应为YYYY-MM-DD;请修正后重试"。这种方法让模型有机会自我纠正,而不是在同一个坑里反复跳。

还有一点必须强调:技能执行层要对高频失败做熔断控制。如果同一个技能连续失败三次,这一次对话里就不要再给模型提供这个技能。我在这上面吃过亏,模型被一个故障接口拖住,整个会话的后续操作全部卡死,就是因为没做熔断。

3. 实操过程与核心环节实现

3.1 最小可用的技能库目录结构

先看一套我实际在用的目录结构,它是从抽象设计落到工程实现的关键一步。

agent_skills/ ├── registry.py # 技能注册中心,负责收集所有技能 ├── router.py # 技能选择器,根据用户输入匹配最相关技能 ├── executor.py # 技能执行器,负责调用具体函数并处理错误 ├── schemas/ │ ├── order.py # 订单相关技能 │ ├── user.py # 用户相关技能 │ └── weather.py # 天气相关技能 └── skills.json # 编译后的技能清单,供LLM读取

我见过很多团队把技能定义和业务逻辑混在一起,最后代码和提示词都难以维护。拆出 schemas 目录的好处是:技能清单可以单独导出成JSON,直接喂给模型,不用从代码里反推。这个JSON就是"模型的说明书",它必须始终保持最新。

3.2 技能注册与动态路由怎么实现

这一步是整个系统的骨架。先看技能注册的Python实现,用装饰器就能把任意函数变成一个"技能"。

# registry.py import inspect import json from typing import Callable, List SKILLS = [] def skill(name: str, description: str, parameters: dict, tags: list = None): """把普通函数注册为Agent技能""" def decorator(func: Callable): skill_info = { "name": name, "description": description, "parameters": parameters, "tags": tags or [], "function": func } SKILLS.append(skill_info) return func return decorator def build_skill_list() -> List[dict]: """生成给LLM看的技能清单(去掉函数引用)""" return [{ "name": s["name"], "description": s["description"], "parameters": s["parameters"] } for s in SKILLS]

配套的技能定义,这里我用一个任务管理场景举例。

# schemas/task.py from registry import skill @skill( name="create_task", description="创建一条新的待办任务。当用户说'添加任务''记一下''提醒我'时使用。" "需要标题和截止时间。禁止在用户未确认截止时间时自动假设截止时间。", parameters={ "type": "object", "properties": { "title": {"type": "string", "description": "任务标题,简短明确"}, "due_date": {"type": "string", "description": "截止日期,格式YYYY-MM-DD"}, "priority": {"type": "string", "enum": ["high", "medium", "low"]} }, "required": ["title", "due_date"] }, tags=["task", "todo"] ) def create_task(title: str, due_date: str, priority: str = "medium"): # 这里写真正的业务逻辑,比如写入数据库 return {"status": "ok", "task_id": 12345, "title": title, "due_date": due_date}

路由器的实现要解决一个核心问题:如何从几十个技能里挑出最相关的几个。我在生产环境里优先选用了"语义检索+关键词兜底"的双路召回。

# router.py from sentence_transformers import SentenceTransformer import json model = SentenceTransformer("BAAI/bge-small-zh-v1.5") def route(user_input: str, skills: list, top_k: int = 5): """双路召回:向量相似度 + 关键词命中,合并去重后返回top_k""" query_vec = model.encode(user_input, normalize_embeddings=True) skill_texts = [s["description"] for s in skills] skill_vecs = model.encode(skill_texts, normalize_embeddings=True) scores = skill_vecs @ query_vec.T # 余弦相似度 ranked = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)[:top_k] result = [] for idx in ranked: s_link = skills[idx] score = float(scores[idx]) if score > 0.25: # 低于阈值不要,防止乱召回 result.append((s_link, score)) return result

注意阈值0.25不是拍脑袋定的,我测过一段时间的召回日志,低于这个分数的技能基本跟用户意图无关,拉进来只会增加模型选择负担。实际使用中需要根据你的向量模型和技能描述风格做校准。

3.3 与LLM调用集成:从技能列表到最终动作

路由选出来的技能清单需要拼接到模型请求里,然后让模型输出结构化的调用指令。这里以OpenAI兼容接口为例,其他推理框架的写法也大同小异。

# executor.py import json import openai def run_agent(user_input: str): # 1. 路选出候选技能 skill_list = build_skill_list() candidates = route(user_input, skill_list) # 2. 拼装给LLM看的工具列表 tools = [] for skill, score in candidates: tools.append({ "type": "function", "function": { "name": skill["name"], "description": skill["description"], "parameters": skill["parameters"] } }) # 3. 第一轮:让模型决定调用哪个技能 response = openai.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": "你是任务助手,仅通过可用工具完成用户请求。"}, {"role": "user", "content": user_input} ], tools=tools, tool_choice="auto" ) # 4. 执行模型选出的技能 msg = response.choices[0].message if msg.tool_calls: for call in msg.tool_calls: fn_name = call.function.name args = json.loads(call.function.arguments) # 从SKILLS里找到对应函数并执行 for s in SKILLS: if s["name"] == fn_name: result = s["function"](**args) return result # 5. 如果模型没选工具,直接返回回复文本 return msg.content or "没有找到可用的技能"

我特别想强调一下这个集成阶段的体验:第一版我图省事,把所有技能一股脑塞给工具列表,结果上下文爆了,效果很差。后来改成效,现在成了整个系统性能提升最大的一次优化,效果确实立竿见影。

3.4 技能组合与降级:一次任务调多个技能

现实里很多业务不是单技能能搞定的。比如用户说"帮我看看明天北京适合穿什么衣服",正确流程是:先调 get_weather 拿到温度和天气,再调 clothing_suggestion 根据天气给建议,两个技能串联。

我的做法是在技能描述里显式声明"关联技能",让模型有路径可循。例如 get_weather 的描述末尾加上一行:关联技能:clothing_suggestion,获得天气数据后建议调用此技能继续推荐衣物。模型在推理时就会倾向于按这个顺序执行。

降级逻辑同样要做在设计里,不然就是事故。executor 层我用了一个简单的函数装饰器,统一捕获异常并做三次重试判断。

# executor.py from functools import wraps def with_retry_and_fallback(max_retries: int = 2, fallback_result: dict = None): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(max_retries + 1): try: result = func(*args, **kwargs) # 如果业务返回显式的错误码,也触发重试 if isinstance(result, dict) and result.get("error_code"): raise RuntimeError(result["error_code"]) return result except Exception as e: last_err = e continue return fallback_result or {"status": "error", "message": str(last_err)} return wrapper return decorator

用的时候只要给技能函数加上装饰器就行,熔断和降级都可以做成内置逻辑,业务代码保持纯净。

4. 常见问题与排查技巧实录

4.1 模型反复调用同一个失败技能

这是我调试期间遇到的最头大的问题。一个查询接口挂了,模型得到"ERROR_SERVICE_UNAVAILABLE"之后,不重新组织思路,反而把参数改一改继续调,最多的时候连续调了七次,整个对话被卡死。

排查思路是分两步:第一步,看调用日志里模型的tool_calls记录,确认它是不是一直重复选同一个工具而不考虑其他路径。第二步,在executor层加会话级熔断,同一个技能在一次对话内最多失败两次,之后就把它从工具列表里移除。移除之后模型没有别的选择,就会自然地说"当前服务暂时不可用",至少不会死循环。

我后来还把"不可用技能"也变成了一个显式信息回传,告诉模型"技能A当前不可用,你可以尝试技能B或直接回复用户"。这种做法比单纯移除更友好,模型能自主给出替代方案。

4.2 技能之间的参数命名冲突

当技能数量到一定规模,来自不同业务模块的技能很可能共用参数名。比如订单模块有个 status 是"订单状态",物流模块也有个 status 是"物流轨迹状态",Schema一合并,模型直接混淆。

解决方式有两个,我都用上了。第一个是按业务域加参数前缀,比如 order_status、shipment_status,让参数名自带语义空间。第二个是在路由阶段按用户意图先分类,技能清单本身就尽量控制在同一个业务域内,不混着给。比如用户聊订单,路由后返回的基本都是订单域技能,物流状态只在需要时作为关联技能出现。

4.3 技能描述太长导致决策变慢

最初版本每条技能描述追求全量信息,平均每条约180字,一次路由5个技能就是900字的工具定义,加上对话上下文,模型推理速度明显下降,JSON输出的稳定性也变差了。

后面我做了两个优化:一是把技能描述的"触发条件"部分压缩成高频短语列表,而不是完整句子。二是把特别细的约束从主描述抽到"use_note"字段里,让模型在需要判断边界时再读取。这样主描述平均压到90字左右,决策延迟降了一大截,误调用没有因此反弹。

4.4 常见问题速查表

症状可能原因推荐处理方法
模型不调用任何技能技能描述缺少触发短语,或路由没召回检查路由得分,低于阈值需要调低或改写描述
模型总是选错技能两个技能描述边界重叠太高为每个技能增加"禁止"场景,明确边界
参数频繁格式错误Schema描述太宽泛,缺少格式示例在描述里加示例值,如"2024-01-01"
工具执行成功后模型仍然重复调用缺少执行结果回填确认结果以tool message形式回传给模型
技能列表多了之后效果大幅下降工具定义撑爆上下文收紧路由top_k,或对技能描述做压缩

5. 把agent-skills变成团队共识

写到这里,这套东西已经不是一个个人的代码项目了,它慢慢沉淀成了一套团队协作的规范。我现在的做法是:所有技能的新增必须走一个记录模板,包含触发场景、参数契约、反面约束、关联技能四个段落。不是代码层面的强制校验,而是在Code Review层面要求每条技能描述都必须经过这些字段的审视。

这套体系还可以继续扩展。比如技能库加上埋点数据之后,可以看到每个技能的真实调用频率和失败率,哪些技能长期不被模型选中、哪些技能描述拼命误触发,都能从数据里看出来。根据数据分析再回头改描述,才能真正把Agent工具层调得越来越聪明。

至少现阶段,我还没有找到比"结构化技能库"更好的干活方式。希望这套agent-skills的实践思路,能帮正在跟工具调用较劲的同行少走几条弯路。

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

ESP32 动态加载 WebAssembly 应用:打造嵌入式应用平台

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 8:14:29

Cookie与JWT全解析:从登录状态原理到安全攻防实战

做Web开发,基本没人能绕开一个问题——用户的登录状态怎么保存。网上关于Cookie和JWT的讨论一搜一大把,但大部分内容都停留在“Cookie存浏览器、JWT存客户端”“Session在服务端、JWT在客户端”这种表面区分。你拿着这种认知去做技术选型,大概…

作者头像 李华
网站建设 2026/9/25 8:06:03

ax调度是什么?从贝叶斯优化到自动试验循环的完整实战解析

最近后台总有朋友问我同一个问题:你说的ax调度到底是什么?其实我第一次看到“ax调度”这个说法也愣了一下,后来才明白,大家说的就是把Meta开源的Ax平台用起来。Ax本身是一个面向自适应试验的开源平台,它最早用于内部的…

作者头像 李华
网站建设 2026/9/25 8:04:31

Vivado工程迁移指南:用TCL脚本实现版本兼容与IP核优化

前阵子合作团队发来一个老工程,2018.3版本建的,我本机装的是2022.2。双击.xpr弹了个版本升级提示,点完Upgrade之后,综合跑到一半报了几个IP核错误,其中一个MIG的DDR4控制器直接锁死状态。折腾了大半天,最后…

作者头像 李华
网站建设 2026/9/25 8:00:17

Atlas 300V 24G部署YOLO全流程:从硬件选型到CANN模型转换推理调优

做AI推理部署的同学,这两年应该都绕不开Atlas这个名字。尤其是当你在电商、安防、工业质检这些场景里做视觉检测时,昇腾的Atlas系列加速卡几乎是性价比绕不过去的选项。最近后台收到不少留言,问Atlas 300V 24G到底是不是一张运算加速卡&#…

作者头像 李华