ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

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

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( namecreate_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_embeddingsTrue) skill_texts [s[description] for s in skills] skill_vecs model.encode(skill_texts, normalize_embeddingsTrue) scores skill_vecs query_vec.T # 余弦相似度 ranked sorted(range(len(scores)), keylambda i: scores[i], reverseTrue)[: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( modelgpt-4o-mini, messages[ {role: system, content: 你是任务助手仅通过可用工具完成用户请求。}, {role: user, content: user_input} ], toolstools, tool_choiceauto ) # 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的实践思路能帮正在跟工具调用较劲的同行少走几条弯路。
返回列表