Agent技能系统实战:从工具封装到稳定执行的核心方法论
发布时间:2026/9/26 14:43:10来源:尧图网络
在搞Agent相关项目时我最大的感受是模型能力再强如果每次任务都靠现场自由发挥结果就是时好时坏、难以复用。真正让Agent从玩具变成生产力工具的恰恰是它背后能不能沉淀一套稳定、可插拔、可验证的能力集合。这个agent-skills项目本质上就是给Agent装配一套技能系统——把高频、确定性的操作封装成技能让模型通过调用技能去完成任务而不是每次都从零推理。这篇文章我会从设计思路、核心机制、完整实现到踩坑排查把怎么搭一套可用的Agent技能系统讲透。适合正在做Agent应用开发、想提升模型任务执行稳定性的工程师也适合刚接触这一块但对工具调用「函数调用」已有基本概念的读者——只要你清楚Agent本质是LLM 工具 循环后面的内容都能跟上。1. 为什么Agent需要一套技能系统1.1 从会说话到会干活Agent能力封装的必然选择早期的Agent应用很简单给模型扔一段Prompt让它直接输出结果。但一碰到需要操作外部系统、查文件、调API、处理结构化数据的情况这种裸奔式写法立刻露馅模型记不住对话外的状态、输出格式飘忽不定、同样的任务换个说法可能就执行失败。后来大家开始给Agent挂工具用Function Calling让模型选择调用哪个函数。这一步确实解决了不少问题但也暴露了新的麻烦——工具的粒度怎么定如果把查数据库做成一个工具那查用户表和查订单表是分开还是合并如果每个细粒度操作都做成工具函数列表会膨胀到模型难以选择如果做大而全的工具参数设计又变得极其复杂模型经常传错参数。agent-skills想解决的就是这个中间层问题在模型和具体工具之间加一层技能抽象。一个技能不只是单个函数它可能包含触发条件、前置校验、执行流程、结果格式化、失败降级策略——它是一整套可复用的行为封装。模型不需要知道技能内部怎么实现只需要理解这个技能是干什么的、什么时候该用、需要什么参数。1.2 技能、工具、工作流概念边界与项目定位聊技能系统之前得先把这个概念和目标对齐。在agent-skills的语境里**工具Tool**是最底层的能力单元对应一个具体的函数或API调用比如read_file、execute_sql。**技能Skill**是面向任务的能力封装内部可以编排多个工具调用包含自己的参数校验、中间逻辑、异常处理比如代码仓库检索技能内部会依次调用list_files、read_file、grep_search。**工作流Workflow**是跨技能的流程编排比如从issue到PR的完整链路。agent-skills聚焦在第二层。它不替代工具层也不强行做工作流引擎而是把那些经常组合使用、步骤相对稳定的操作固化下来让模型用一个语义化的技能名就能触发一整段逻辑。这套设计最直接的价值是降低了模型的选择成本。模型面对的是十个技能而不是一百个工具选择精度高得多。同时技能的复用性也上来了——同一个技能可以在不同任务里反复调用不需要针对每个场景重写Prompt。1.3 agent-skills的核心能力清单从落地角度看这套技能系统需要具备六个基础能力技能注册把技能元信息集中管理包括名称、描述、参数Schema、版本号。技能发现让LLM能够根据任务描述自动匹配合适的技能靠的是技能描述的质量和参数约束的清晰度。技能执行按定义好的流程调用底层工具处理中间状态和结果格式化。技能组合支持一个技能内部调用其他技能形成层级化的能力结构。技能验证离线跑测试用例确保技能在给定输入下行为符合预期避免上线后出乱子。技能热更新在不重启服务的情况下新增或调整技能。这六个能力对齐了Agent的技能体系才算真正立住了。下文我会逐个讲清楚实现要点。2. 整体架构与核心设计思路2.1 技能的三段式结构清单、描述、实现在agent-skills里一个完整的技能由三部分构成技能清单Registry全局维护一份技能元数据列表相当于技能的通讯录。每条记录包含技能名、简介、参数JSON Schema、入口函数名、版本号、依赖关系。这份清单既给LLM做选择用也给执行引擎做路由用。技能描述Descriptor每个技能都有自己的描述文件通常用Markdown或YAML维护。描述文件是对何时该用、何时不该用、参数怎么填、返回什么的详细说明。描述写得好不好直接决定模型会不会选对技能——后面第四章会专门讲这个坑。技能实现Impl实际执行的代码逻辑。可以是单个Python函数也可以是一组函数加配置文件。实现层不直接暴露给LLMLLM只跟描述和参数打交道。三段式的核心用意是解耦描述可以随时调整而不动代码实现可以单独测试而不依赖LLM清单则提供一个统一视图方便调试时查看模型到底有哪些技能可选。2.2 技能注册与发现让模型知道你会什么技能注册发生在服务启动阶段。系统会扫描配置好的技能目录解析每个技能的描述文件校验参数Schema合法性然后构建Registry对象。这个Registry会被注入到两处一是LLM的函数列表用来生成调用候选二是执行引擎的路由表用来把技能名映射到实际函数。技能发现的核心挑战是候选太多时模型容易蒙。实践下来技能数量控制在8到12个以内时模型的选择准确率明显更高超过20个误选率就会上升。如果你确实有几十个技能得加一层分组发现先按领域分几大类第一轮让模型选类第二轮在类内选具体技能。技能描写的格式也直接影响发现准确率。我的建议是每个技能至少包含四段信息名称与别名名称简短达意别名覆盖常见说法。用途描述一两句话说清楚干什么用避免含糊的表达。适用场景什么时候该用这个技能最好带正反例。参数说明每个参数的用途、类型、必填性、取值范围。这块内容看起来简单但对效果的影响远超想象。描述仔细打磨过的技能选择准确率能提高两成以上。2.3 技能编排引擎从单技能到复合技能复杂任务往往不能靠单个技能解决比如帮我总结一下这个仓库的代码结构并生成架构文档至少涉及文件遍历、代码阅读、文档模板三个环节。如果硬把整个过程塞进一个技能里技能会变得臃肿难维护。更好的做法是把拆出来的小能力做成原子技能再通过编排层组合调用。agent-skills里做了一个轻量的编排机制技能实现内部可以通过self.call_skill(skill_name, params)来调用其他技能。每个技能可以声明依赖哪些子技能执行引擎在调用前会先校验依赖是否可用。这个设计借鉴了函数调用的思路——技能之间是树状调用关系而不是平铺的互相调用。需要特别注意的是循环依赖问题。技能A依赖B、B依赖A这种Case必须静态检测在注册阶段就报错否则运行时就直接爆栈了。校验算法很简单解析依赖图做拓扑排序发现环就抛异常。2.4 为什么用JSON Schema描述技能约束技能参数如果只写描述而不做结构约束模型传参时就会出现各种自由发挥参数名对不上、类型传错、必填项遗漏。agent-skills在技能定义里强制要求提供JSON Schema这有几点实质好处第一结构化约束让模型更容易生成合法参数。大模型对JSON Schema的理解能力已经相当不错只要把type、required、properties、enum这些字段写清楚模型生成的参数基本能通过基础校验。第二Schema可以复用去做校验和重试。模型第一次传参不符合Schema时执行引擎可以直接把校验错误返回给模型让它根据错误信息重新生成。这比让模型瞎猜参数要高效得多。第三Schema能生成文档和测试用例。基于Schema自动生成Mock参数可以批量跑技能的单测和回归测试省了很多手写测试的功夫。这里有个小技巧描述里别用可以传入任意字符串这种话尽量给每个参数加上明确的description必要时候用enum限定取值范围。模型对边界清晰的参数理解得远比开放参数更准。3. 实操从零搭建一个技能系统3.1 项目结构与依赖准备我直接用一个最小可复现的工程来演示。项目结构如下agent-skills/ ├── skills/ │ ├── registry.py │ ├── base.py │ └── builtin/ │ ├── file_reader/ │ │ ├── SKILL.md │ │ └── impl.py │ └── repo_searcher/ │ ├── SKILL.md │ └── impl.py ├── engine.py ├── llm.py └── main.py依赖方面只需要两个核心库openai或其他模型SDK用于LLM调用jsonschema用于参数校验。为了方便演示我没有引入重型框架实际的技能调用分发逻辑全部自己实现这样你能看清底层原理换成LangChain这类框架时也更容易对应上。3.2 定义技能清单SKILL.md与技能目录规范每个技能目录下放一个SKILL.md作为技能的描述文件。以file_reader为例--- name: file_reader version: 1.0.0 description: 读取指定文本文件的内容支持按行范围截取。 when_to_use: 当需要查看文件内容、提取文件片段、确认代码实现细节时使用。 when_not_to_use: 需要搜索文件时请使用repo_searcher不要使用本技能。 params: path: type: string description: 文件绝对路径或相对项目根路径。 required: true start_line: type: integer description: 起始行号从1开始缺省表示从文件开头。 required: false end_line: type: integer description: 结束行号包含该行缺省表示读到文件末尾。 required: false returns: type: object properties: content: type: string description: 读取到的文件内容。 total_lines: type: integer description: 文件总行数。这个Markdown头部实际是一份可解析的元数据。注册程序在扫描目录时会读取---之间的YAML块转成技能描述对象。正文部分不会被解析进元数据主要给开发者自己看方便维护。when_to_use和when_not_to_use这两项是我强烈建议保留的。它们相当于在告诉模型边界在哪里比单纯描述功能更管用能显著降低误调用率。3.3 技能实现层以文件检索技能为例技能实现就一个普通Python类继承BaseSkill即可。以下是repo_searcher的核心实现注意它内部组合了file_reader技能# skills/builtin/repo_searcher/impl.py import os from skills.base import BaseSkill class RepoSearcherSkill(BaseSkill): name repo_searcher version 1.0.0 def run(self, params: dict, context: dict): keyword params.get(keyword) path params.get(path, .) file_patterns params.get(file_patterns, [*.py]) max_results params.get(max_results, 20) results [] matched_files self._find_files(path, file_patterns) for file_path in matched_files[:50]: content self.call_skill(file_reader, { path: file_path, })[content] if keyword in content: lines content.splitlines() for idx, line in enumerate(lines, 1): if keyword in line: results.append({ file: file_path, line: idx, text: line.strip(), }) return {results: results[:max_results]} def _find_files(self, root, patterns): matched [] for dirpath, _, filenames in os.walk(root): # 跳过隐藏目录和依赖目录 if any(part.startswith(.) for part in dirpath.split(os.sep)): continue if node_modules in dirpath or venv in dirpath: continue for fname in filenames: if any(fname.endswith(p.replace(*, )) for p in patterns): matched.append(os.path.join(dirpath, fname)) return matched关键技术点是self.call_skill这个方法它由基类提供执行引擎注入一个技能分发器后技能之间就能互相调用了。这样做的好处是技能实现里不需要关心调用来源是LLM还是其他技能统一走同一套分发逻辑行为一致也方便追踪。3.4 技能执行引擎注册、分发、校验一条龙引擎是整个系统的承重墙。它的职责有三块注册技能、接收LLM的技能调用请求、执行技能并返回结构化结果。# engine.py import inspect import jsonschema from skills.registry import SkillRegistry class SkillEngine: def __init__(self, registry: SkillRegistry): self.registry registry def register_skill(self, skill_instance): descriptor self.registry.get_descriptor(skill_instance.name) schema descriptor[params] # 预检参数Schema是否合法 jsonschema.Draft7Validator.check_schema(schema) self.registry.add(skill_instance) def execute(self, skill_name: str, params: dict, context: dict): skill self.registry.get(skill_name) if skill is None: raise ValueError(funknown skill: {skill_name}) descriptor self.registry.get_descriptor(skill_name) schema descriptor[params] # 校验参数 try: jsonschema.validate(instanceparams, schemaschema) except jsonschema.ValidationError as e: return { status: error, error_type: invalid_params, message: str(e), } # 执行 try: # 注入技能分发器 skill.set_dispatcher(self.execute) result skill.run(params, context) return {status: success, data: result} except Exception as e: return { status: error, error_type: runtime_error, message: f{type(e).__name__}: {str(e)}, }这里有一个设计细节值得注意参数校验失败并不直接抛异常而是返回一个结构化错误对象。原因在于当调用方是LLM时抛异常会导致整个Agent链路中断而返回结构化错误可以让上层把message回传给模型让模型自行修正参数后重新发起调用。这比一错就挂的体验好太多。3.5 接入LLM调用层函数调用与结果回填有了技能注册表和引擎剩下的就是把技能列表暴露给LLM处理模型发来的函数调用请求。我用工具调用Function Calling的方式实现这也是目前最主流的方式。# llm.py import json from openai import OpenAI client OpenAI() def build_tools(registry): 把技能列表转换成OpenAI Function Calling格式 tools [] for descriptor in registry.list_descriptors(): tools.append({ type: function, function: { name: descriptor[name], description: descriptor[description], parameters: descriptor[params], } }) return tools def run_agent(user_query): registry build_registry() # 注册扫描 engine SkillEngine(registry) tools build_tools(registry) messages [{role: user, content: user_query}] for step in range(5): resp client.chat.completions.create( modelgpt-4o, messagesmessages, toolstools, ) msg resp.choices[0].message if not msg.tool_calls: return msg.content # 执行技能调用 for tool_call in msg.tool_calls: skill_name tool_call.function.name params json.loads(tool_call.function.arguments) result engine.execute(skill_name, params, context{}) # 把结果回填到对话让模型继续推理 messages.append({ role: assistant, content: None, tool_calls: [tool_call.model_dump()], }) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse), }) return reach max steps核心逻辑不难理解循环里把技能的返回值拼到对话消息里模型根据这些结果决定下一步动作直到它认为任务完成、不再发起技能调用为止。这个循环就是Agent最基本的执行形态agent-skills只负责让技能选择和执行质量更可控而循环本身不设限。实际项目里我会把循环轮数调高到10到15轮并加上任务终止条件判断避免模型在某些失败场景下无限兜圈子。3.6 技能验证链路离线测试与回归Agent赛道最大的痛点是回归问题——昨天能跑通的流程今天换了个模型版本或调整了提示词结果就变了。技能系统能缓解这个问题前提是对技能做离线验证。我在项目里为每个技能配了一个tests.yaml记录典型的输入输出对cases: - name: 读取文件头部 params: path: README.md end_line: 5 expect: status: success data.total_lines: 10 - name: 文件不存在 params: path: not_exist.md expect: status: error error_type: runtime_error验证跑起来很简单加载一个技能遍历测试用例执行并对比期望结果。这一步做不了全自动的智能判断但能防住大部分低级错误。更重要的用途是配合效果评估把技能描述调整后跑一遍全量用例看看哪些用例从success变成error或者参数Schema变更后哪些调用会挂掉。这套机制保证你在快速迭代的时候不至于把之前的成果改没了。4. 实战中的常见问题与排查技巧4.1 技能描述太笼统模型总是选错技能这是我被问得最多的一个问题。症状表现是任务明明应该走A技能模型却选了B技能或者干脆编造一个技能名。排查思路先看注册表确认模型实际能看到哪些技能描述。然后逐条审视描述看是否存在语义重叠。比如file_reader描述成读取文件内容和repo_searcher描述成搜索代码内容模型在遇到看一下这个文件里的某段代码时就可能摇摆不定。解决方法是把描述改成决策导向而不仅是功能导向。例如file_reader仅用于读取文件内容适用于已知具体文件路径的场景。如果不知道路径、需要搜索包含某个关键字的文件请使用repo_searcher。repo_searcher在项目内搜索包含指定关键字的文件和行号适用于需要找到文件但不清楚文件位置的场景。这种该用我和别用我并存的写法模型踩坑的概率会大幅下降。4.2 技能执行失败后模型嘴硬不承认技能调用返回了错误码但模型在下一轮回复里直接说已完成完全不引用错误信息。这种情况在复杂任务里很常见。根因多半是工具调用结果里携带的信息不够扎眼模型在一堆消息里没能有效识别出错误。我的做法是统一错误响应格式并且把status字段放在最前面同时在错误信息里加上error_type和message这样的结构化字段。像这个例子{ status: error, error_type: invalid_params, message: 参数path不能为空请检查后重试 }一旦执行引擎返回status: error上层Agent循环里要强制要求模型重新规划。可以加一句系统提示只有当所有工具调用的结果都是success时才允许输出最终回复。这一步能显著减少假装成功的情况。4.3 多技能组合时的上下文污染技能编排时容易踩一个隐形坑A技能返回的结果里带了大段无关信息模型在后续推理时被这些冗余内容带偏生成结果变得奇怪。尤其是把大文件全文返回给LLM的情况费token还容易超上下文窗口。对策有两个方向。一是按需裁剪技能返回结果尽量精简只保留与任务直接相关的片段。比如file_reader默认只返回前200行或根据参数截取指定行区间而不是无脑全量返回。二是分段消费如果必须读大文件让技能只返回每个段落的摘要或命中关键字的位置等模型明确需要看某段原文了再回过头来读取。4.4 技能热更新的版本管理开发调试阶段经常要改技能描述或者实现又不想每次重启服务。我在引擎里加了一个reload_skill(skill_name)方法内部会重新扫描技能目录、校验Schema、替换注册表里的实现和描述。但这里有个隐患如果在线请求刚好在reload的间隙命中这个技能可能拿到新旧混搭的配置。稳妥的做法是把版本带上技能元信息里维护version字段reload时会把它写入注册表并打印一条操作日志。排查线上问题时看到日志就知道当前生效的是哪个版本不至于出现我改了代码但线上行为不对这种灵异事件。另外所有技能变更都走git记录发布到生产环境之前先跑一遍第三章说的离线回归用例。4.5 排查技巧速查表我把常见问题整理成了一张速查表遇到问题可以直接对照着看现象可能原因排查与解决办法模型选错技能技能描述语义重叠、边界不清增加when_not_to_use字段重写描述参数频繁校验失败JSON Schema约束过宽松或过严精简参数数量用enum限定取值技能执行慢技能内部同步调用了太多子技能检查是否有重复读取文件、重复遍历目录返回结果被截断结果超出模型上下文窗口技能层做裁剪只返回关键信息技能改了没生效缓存问题或reload失败查看reload日志确认version已经变更模型陷入死循环技能反复返回同一错误在循环层加最大轮数超过就强制终止嵌套技能调用爆栈存在循环依赖注册阶段做依赖图拓扑排序发现环直接报错这张表不是标准答案但它覆盖了我在实际项目里遇到频率最高的几类问题。你在使用技能系统的过程中大概率也会碰到到时候可以对照着排查。5. 后续还能怎样扩展技能系统一旦跑通后续扩展空间是很大的。我现在在尝试的方向主要有三个分享出来给你参考。第一个方向技能自动编排。目前技能组合靠的是LLM在运行时的动态选择但有些固定流程比如先检索再总结再输出完全可以沉淀成预置的编排模板。我在agent-skills里加了一个skill_chain配置允许把多个技能按顺序串起来类似于一个迷你版工作流。遇到每次都要先查文件再调API再格式化这种常规操作直接用编排模板跑速度和稳定性都更好。第二个方向技能效果评估。前面提到的离线用例是基础但真正有价值的评估是端到端的效果评估——给定一个有明确标准的任务看Agent最终能不能完成。比如从项目里找出所有调用过某API的地方并统计次数跑完Agent后和真实结果对比得到一个分数。这套评估跑得越勤技能描述和参数约束的优化就越有依据。第三个方向技能共享与复用。不同项目之间其实有很多技能是相通的比如文件检索、网页抓取、数据库查询。我正打算把内置技能做成一个独立包发布出去让其他项目直接依赖而不是每个项目都重新写一遍。这个思路如果走通Angent开发的重心就会转向怎么组合技能而不是怎么实现技能。关于agent-skills这个项目我最终的一个体会是Agent工程质量的关键不在模型选得有多新也不在提示词写得有多花哨而在你给模型搭的这套脚手架够不够稳。技能系统就是脚手架的重要一环——它能让你沉淀经验、提升稳定、减少重复劳动。如果这篇文章能帮你在这儿少走几步弯路我就觉得值了。
网站建设高端定制企业官网