ARTICLE DETAIL

资讯详情

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

Agent技能库设计指南:从原子工具到动态编排的工程实践

Agent技能库设计指南:从原子工具到动态编排的工程实践 从去年开始我一直在做 AI Agent 相关的落地项目最强烈的感受就是Agent 本身只是一个会说话的大脑真正让它变成生产力的是它有没有一套能持续沉淀、可复用、能组合的技能库。这也是我启动agent-skills这个项目的直接原因。简单来说agent-skills 解决的并不是怎么让 Agent 学会某个具体动作而是怎么让 Agent 知道自己会哪些动作、如何规范地调用这些动作、以及当新技能需要加入时应该以什么样的方式被注册和发现。我见过太多团队把技能写成一堆散落的函数最后 Agent 连哪个函数是干嘛的都分不清。这篇文章就把我在 agent-skills 里踩过的坑、想清楚的架构、以及建议照抄的实操方案一起捋一遍适合那些已经跑通了 Agent 基础对话、正在琢磨怎么把 Agent 接进真实业务流程的开发者。1. 为什么需要一个技能库Agent 光会说话远远不够如果你试用过纯 Prompt 驱动的 Agent一定会遇到这种场景你问它帮我检查一下昨天的日志有没有报错它能给你讲出一大套日志排查方法论但就是不执行任何实际动作。这不是模型能力的问题而是 Agent 缺少行动接口。几年前大家都在卷模型推理能力到了现在这个阶段决定一个 Agent 好不好用的往往不是它的脑力而是它有多少高质量的手脚。1.1 技能和工具到底是不是一个东西很多框架把工具Tool和技能Skill混着叫一开始我也没太纠结但实际做项目后发现把两者区分开非常有必要。在我对 agent-skills 的定义里工具是底层的最小操作单元。比如读取文件内容发送 HTTP 请求执行 SQL 查询这些动作是原子的不包含任何业务逻辑判断。技能则是工具与业务逻辑、甚至与其他技能的组合。比如分析数据库慢查询并生成优化建议它需要先调用 SQL 执行工具拿到慢查询列表再对每条查询做解释计划分析还可能要把结果交给另一个技能去生成报告。如果你只给 Agent 一堆底层工具它面对稍微复杂一点的任务时就得自己临场编排所有步骤。LLM 在选工具、排顺序这件事上并不可靠尤其当工具列表超过二十个以后选错率会明显上升。技能的存在就是把那些已经被验证过的、固定的编排路径沉淀下来让 Agent 不用每次都从头思考直接按套路出牌。1.2 我需要的不只是技能集合而是一套管理机制实际开发第一版的时候我做的事情很简单把十几个函数塞进一个 tools 字典注释里写清楚参数说明然后祈祷模型每次都能选对。后面技能数量到了三十多个问题就开始暴露了技能之间的依赖关系没人维护想给生成周报这个技能加上自动拉取工时数据的功能得同时改三个函数新同事接手项目根本不知道哪个技能该在什么场景下用技能描述里也没写清楚测试只能靠手动问一句话试试没有一个可靠的单元测试方式更坑的是两个技能用了同一个内部函数改了一个另一个悄悄坏掉。所以我做 agent-skills 时核心目标已经变了我要设计一个让 Agent 的技能可以增量式迭代、可以被标准化描述、可以被动态发现的架构。这本质上就是在给 Agent 构建一套操作系统式的能力管理框架而不是简单收拢几个函数。2. 技能单元怎么拆从动作原子化到业务编排如果你一上来就闷头写技能很容易写出一个特别胖的技能函数体里塞满了各种业务的 if-else技能描述恨不得写一千字参数列表连编剧部的剧名都想好了。我的建议很简单先拆动作再谈编排。所有技能的设计都要遵守一条硬性原则——一个技能只做一件完整的小事。2.1 原子技能一个动作对应一个明确输出所谓完整的小事指的是这个技能被调用之后能产出一个明确的结果对象不需要再让 Agent 猜你运行完没。比如我早期做的一个技能fetch_article_content接收一个 URL返回网页正文。这个技能的核心点是输出规范化。我不会让它返回一坨原始 HTML 文本而是返回一个结构{ url: https://example.com/post/123, title: 如何构建Agent技能库, author: 张三, content: ..., word_count: 4821, extracted_at: 2025-01-15T10:30:0008:00 }为什么这样设计原因很简单一旦输出被规范化下一步无论是让大模型做概括还是把这个结构化数据传入别的技能都不需要额外做文本清洗。技能与技能之间的对接也不再依赖自然语言这种脆弱的接口而是有明确的数据契约。原子技能的参考判断标准有三个输入参数不超过五个输出可以用 JSON schema 描述执行时间不能太长如果超过 10 秒考虑拆成异步流程。2.2 复合技能让编排逻辑进代码而不是依赖模型临场发挥当一个技能需要依次调用多个原子技能并且在过程中还要做条件判断时它就升级为复合技能了。举一个我在 agent-skills 里实现的例子generate_meeting_minutes会议纪要生成。这个技能输入一个会议录音转写文本流程是这样的调用extract_participants提取参会人名单调用segment_by_topic把转写文本按话题切分为段落对每个段落执行summarize_text生成话题摘要调用extract_action_items从每个话题段落中抽取行动项并标注负责人和截止时间。如果把这些步骤全部交给 Agent 的想法去编排每次执行顺序都可能不一样返回的格式也会五花八门。但我在代码里写死这个流程之后整个技能的执行过程就变得可预测输出永远是同一套结构。在设计复合技能时我有一条很重要的经验不要把复合技能写成一条流水线死链。也就是说技能内部要有容错分支。还是拿会议纪要举例如果extract_participants识别出来的参会人为空流程不应该直接崩溃而应该自动跳过这一步在最终结果里加一个participants: []空数组并附带warning字段。这样 Agent 拿到结果后仍然可以继续处理而不是因为一个异常就终止整个任务。2.3 技能描述怎么写才能让 Agent 正确调用这是我在 agent-skills 项目里花了不少心思的部分。很多开发者写技能描述喜欢写本技能用于获取网页正文并返回标题和正文内容但描述的核心不是给人看的而是要能触发 LLM 的匹配直觉。我总结了一套技能描述模板建议照抄技能用途一句话说明这个技能解决什么问题避免使用术语 适用场景列出三个典型调用场景用用户的自然语言风格描述 不适用的场景明确说明哪些场景不应该调用本技能防止误用 输入说明每个参数的语义解释以及范围 输出说明返回结果的结构化说明强调关键字段 示例给一个最小可运行的输入输出对以fetch_article_content为例它的描述我会写成技能用途抓取网页文章正文内容并提取标题、作者和发布时间。 适用场景 - 用户说帮我看看这篇文章讲了什么并提供URL时 - 需要把网页正文作为后续摘要或翻译的原材料时 - 判断某个URL是不是有效文章链接时 不适用场景 - 用户提供的是PDF或图片链接技能不支持解析 - 用户需要获取实时股票价格请调用行情技能 输入说明 - url: 完整的HTTP或HTTPS链接必须是可直接访问的公开页面 输出说明 - 返回文章元数据和纯文本正文正文长度不超过5000字超出部分自动截断 示例 输入{url: https://example.com/article/1} 输出{url: ..., title: ..., content: ..., author: ...}这段描述看起来很长但对模型的引导作用极强。技能多了以后模型就是靠这段描述在几十个技能里去匹配用户意图的描述写得越具体选错的概率越低。我实测过在技能数量超过三十个之后把描述从一句话扩写成上面这个模板选工具准确率可以从 71% 提升到 89%。3. 技能注册与发现机制让 Agent 知道自己会什么假如所有技能都已经以函数的形式实现好了接下来要做的事情就是注册和发现。这两个词听起来高大上实际上要回答的就是两个问题技能以什么元数据格式登记在册以及Agent 在运行时怎么知道自己可以调用哪些技能。我见过非常极端的做法把所有技能按字母排序输出到 Prompt 里一口气塞给模型结果 Prompt 快要爆掉模型也经常混淆相似功能的技能。agent-skills 的处理方式是引入技能清单 按需加载的机制。3.1 技能清单与目录结构首先我需要一个全局的技能清单。我把这个清单设计成manifest.json每个技能都有唯一的skill_id不允许重复。这个skill_id使用domain.action的格式比如meeting.minutes_generate、database.slow_query_analyze。命名空间的好处在于当两个技能分别从不同模块加载时代码冲突的概率会被大幅降低。我的技能目录结构长这样skills/ ├── registry.json ├── meeting/ │ ├── __init__.py │ ├── minutes_generate/ │ │ ├── skill.py │ │ ├── manifest.json │ │ └── tests/ │ └── participant_extract/ │ ├── skill.py │ ├── manifest.json │ └── tests/ └── database/ ├── slow_query_analyze/ ├── schema_inspect/ └── index_optimize/每个技能目录里必须有一个manifest.json格式大致这样{ skill_id: meeting.minutes_generate, version: 1.2.0, entry: skill.py:MinutesGenerateSkill, description: 从会议转写文本中生成结构化会议纪要包含话题摘要和行动项, dependencies: [ meeting.participant_extract, text.segment_by_topic, text.summarize, action.extract_action_items ], tags: [meeting, productivity] }dependencies这个字段是我特别强调要加的它在注册阶段就能校验出技能之间的依赖关系是否完整。如果某个技能依赖的另一个技能没有被注册加载时就直接报错不会拖到运行时才暴露问题。3.2 按需加载别把一百个技能全塞进上下文技能数量超过五十个以后把所有技能的描述全部拼进系统 Prompt 是不现实的。token 消耗是一方面更重要的是模型在上下文里塞了大量无关技能之后注意力会被稀释。agent-skills 的按需加载逻辑很简单初始化时只加载一个技能路由模块它根据用户请求的关键词和意图从注册表中召回最相关的 5-8 个技能描述然后只把这些描述拼进 Prompt。回复速度和准确率都提升了一大截。按需加载的核心是一个召回器。我第一个版本用的就是简单的关键词匹配加余弦相似度把用户输入和每个技能的 description 文本做了 embedding 匹配。后来发现一个问题有些技能虽然在语义上相关但在特定场景下不该被调用比如用户问数据库查询很慢怎么办语义匹配可能召回database.slow_query_analyze这是对的但它同时召回了database.index_optimize而后者在当前环境没有权限执行就会导致 Agent 选到一个没法运行的技能。所以在召回路里我又加了一道信息每个技能在 manifest 里可以声明disallowed_context条件比如当数据库连接不可用、或者当前用户不是管理员时该技能不会被返回。这实际上等于给技能装了一个准入控制。3.3 开发期调试手动注入技能绕过召回器按需加载机制确实省 token但也有一个麻烦开发新技能的时候召回器可能因为语义判断不准而没有召回这个新技能导致 Agent 一直没机会调用到它。我的解决办法是在本地调试环境加一个FORCE_SKILLS环境变量可以手动指定强制加载某些技能跳过召回阶段。FORCE_SKILLSmeeting.minutes_generate,meeting.participant_extract python debug_server.py这个方法看起来很简单但在调试链路里非常管用。它把一个玄学问题Agent 为什么不调用我的新技能变成了一个确定性的开发问题在强制加载后技能是否正常工作。如果强制加载后技能也报错说明是技能实现的问题如果强制加载后一切正常说明召回器的语义匹配需要优化。两者定位问题的路径是完全不同的。4. 从零手写一个技能完整实操流程概念讲得再多不如实际跑通一个技能。这一节我以周报自动生成这个业务场景为例把从定义规范到编写实现的完整过程走一遍。这个技能本身不复杂但可以在它身上看到 agent-skills 项目里所有核心机制的落地动作。4.1 定义输入输出契约写代码之前先画数据契约。这一步千万别省。我在最初做技能时经常一上来就写函数体结果写到一半发现输入参数考虑不周又回头改反而浪费时间。周报生成技能的输入{ user_name: 张三, project_list: [商品中心重构, 支付链路压测, Agent技能库建设], date_range: { start: 2025-01-13, end: 2025-01-17 } }输出{ weekly_report: [ { project: 商品中心重构, completed: [完成购物车模块接口改造, 编写单元测试用例24条], in_progress: [订单状态机迁移预计下周完成], blocked: [], next_week_plan: [完成订单状态机联调] } ], generated_at: 2025-01-17T18:00:0008:00 }之所以把输出设计得这么细是因为周报生成后往往还要对接发送到钉钉群导入到项目管理系统这类下游动作。如果输出有一个完整清晰的 JSON 结构下游就能直接消费不再需要大模型二次整理。4.2 技能实现逻辑我使用的技能基类定义非常简单核心方法就两个validate_input和execute。前者做输入校验后者做实际业务逻辑。from typing import Any, Dict from agent_skills.core import BaseSkill, SkillInput, SkillOutput class WeeklyReportGenerate(BaseSkill): skill_id work.weekly_report_generate description 根据用户填写的项目列表和时间范围自动生成结构化周报包含已完成事项和下周计划 def validate_input(self, input_data: Dict[str, Any]) - SkillInput: required_keys [user_name, project_list, date_range] for key in required_keys: if key not in input_data: raise ValueError(f缺少必要参数: {key}) if start not in input_data[date_range] or end not in input_data[date_range]: raise ValueError(date_range必须包含start和end字段) return SkillInput(**input_data) def execute(self, input_data: SkillInput) - SkillOutput: weekly_report [] # 实际场景中这里会从项目管理系统拉取真实任务状态 # 下面的代码是本地演示时的简化实现 for project in input_data.project_list: weekly_report.append({ project: project, completed: self._fetch_completed_tasks(project, input_data.date_range), in_progress: self._fetch_in_progress_tasks(project), blocked: [], next_week_plan: self._fetch_next_week_plan(project), }) return SkillOutput( output{weekly_report: weekly_report}, metadata{generated_at: ...} ) def _fetch_completed_tasks(self, project: str, date_range: Dict[str, str]) - list[str]: # 伪代码从项目管理API按日期范围查询 tasks self.external_api.get_tasks( projectproject, statusdone, from_datedate_range[start], to_datedate_range[end] ) return [task.title for task in tasks]这里的核心不是业务代码本身而是validate_input和execute分离的设计。这样技能在开发期就可以用假数据做单元测试而不会因为外部 API 不稳定导致测试挂掉。4.3 注册与测试技能写好之后在registry.json里登记一行然后在技能目录下写一个tests/test_weekly_report.py用 mock 数据验证输出结构。import pytest from skills.work.weekly_report_generate.skill import WeeklyReportGenerate def test_weekly_report_generate_valid_input(): skill WeeklyReportGenerate() result skill.execute(SkillInput( user_name张三, project_list[商品中心重构], date_range{start: 2025-01-13, end: 2025-01-17} )) assert weekly_report in result.output assert len(result.output[weekly_report]) 1 assert completed in result.output[weekly_report][0] def test_weekly_report_generate_missing_param(): skill WeeklyReportGenerate() with pytest.raises(ValueError): skill.validate_input({user_name: 张三, project_list: []})这套测试跑通以后再把它接入 CI之后每次改动任何技能整个技能库的回归测试都会被触发。这是 agent-skills 项目里我觉得最值的一笔投入——它让技能变成可以安全迭代的工程产物而不是某个脚本文件里的一个魔法函数。5. 实战中最容易翻车的四个环节技能库真正跑起来之后问题往往不出在某个技能内部而出现在几个边缘地带。我把实测中遇到的坑按翻车频率排了个序这几个坑值得每一个做 Agent 技能系统的人提前预防。5.1 技能描述与真实行为不一致这是踩过最隐蔽的一个坑。某个版本的fetch_article_content明明已经加了如果页面是登录后才能访问的返回403状态码提示但技能描述里没写。有一天用户问帮我抓一下那个会员文章的内容Agent 毫不犹豫地调用了这个技能然后拿回一个403错误。问题的根因不是 Agent 乱选技能而是技能描述没有跟着行为更新。从那之后我把描述和实现同步作为代码评审的一个强制检查项只要技能实现有任何行为变化manifest 里的 description 必须同步修改。这个看似简单的规则直接把技能误选率降了一个量级。5.2 参数校验缺失导致的连锁失败没有做严格参数校验的技能就像一个不设防的接口一旦上游传进来一个边界值整个链路都会遭殃。我举一个真实例子有个技能parse_duration用来把文本里的时间转换为秒数接收一个duration_str参数。早期版本里没有对负数做校验结果某次用户输入倒计时负5分钟这个技能返回了-300。下游的定时任务技能拿到负数之后创建一个已经过期的定时任务直接跑飞。查问题的时候费了很大劲最后才发现源头就是个负数。现在我在validate_input里所有边界值都会做处理负数、空串、超出范围的值、格式不对的值一律在进入执行逻辑之前就拦截。宁可让技能因为参数不合法而拒绝执行也绝对不要让脏数据继续往链路下游传递。5.3 上下文长度估算失误另一个容易翻车的地方是技能返回结果过大。比如fetch_article_content在抓取一篇万字长文时返回值轻松超过 1 万 token那么 Agent 的上下文窗口很快就被塞满了后续的分析、总结、回复都被迫截断。我的解法是在技能里对返回内容做分层截断策略。以文章抓取为例我不再直接返回全文而是返回三个层级由 Agent 按需使用{ title: 文章标题, meta: {author: ..., word_count: 12800}, truncated_content: 文章前2000字的纯文本, full_content_path: /tmp/articles/12345.txt }如果 Agent 只是做摘要用truncated_content就够了如果它需要引用原文细节可以通过full_content_path再触发一个读取指定文件片段的技能。这样就避免了一次性把大量文本灌进 Prompt。5.4 相似技能的召回竞争技能多了以后很容易出现两个技能在功能上长得像。比如fetch_article_content和extract_news_content都能从网页中提取正文但后者专门处理新闻站点的结构化信息会额外输出发布时间、来源媒体。如果描述里没有明确区分模型经常选错。我处理这类相似技能的方式是在两者的描述里都加上明确的互相排除说明。extract_news_content的描述末尾会写一句如果目标页面不是新闻类站点请优先选择 fetch_article_content。这个方法看起来笨但实测下来比只调 embedding 阈值带来的准确率提升更明显。6. 技能组合与升级路径从会做到会规划把单个技能打磨好了以后真正的价值释放点在于技能的组合与自动编排。这一节聊几个我在 agent-skills 里尝试过的方向有些已经稳定跑在生产环境有些还在实验阶段。6.1 技能的配方把常见组合固化下来与代码里的复合技能不同这里说的组合是在技能层之上的一层配方。我举个实际场景用户想了解某个竞品的最新动态。单靠一个技能做不到但通过组合可以完成search_web搜索竞品名称相关的最近新闻fetch_article_content抓取搜索结果中排名靠前的几篇文章正文summarize_text分别生成每篇文章的摘要extract_entities提取文章中提到的产品名称、价格、发布时间输出结构化对比。在这个配方里我是用 YAML 描述的组合流程把它定义为一个模板化的高级技能skill_id: competitor.track_latest_moves version: 1.0.0 steps: - skill: search_web params: query_template: {company} 最新动态 {time_range} top_k: 5 - skill: fetch_article_content params: url_source: steps[0].output.urls - skill: summarize_text params: input_source: steps[1].output.content max_words: 150 - skill: extract_entities params: input_source: steps[1].output.content entity_types: [product, price, date]这样做的最大好处是业务人员可以不用写代码只改模板里的参数就能生成新的自动分析流程。开发新技能的周期从写函数变成了配参数门槛低了很多。6.2 技能的自动组合边界我试过让 Agent 自己动态组合技能来处理复杂任务结果是好坏参半。对于步骤明确、依赖关系清晰的任务Agent 组成的链路还挺靠谱但一旦链路超过六七个节点或者存在复杂的条件分支模型就开始出现重复调用、漏调、甚至循环调用同一个技能的情况。所以我在 agent-skills 里做了一个「混合编排」的架构简单任务完全交给 Agent 自由挑选技能执行复杂任务则要求 Agent 先从技能配方库中选一个预置的模板如果模板匹配就按模板的固定流程执行只有匹配不到任何模板时才允许 Agent 自行编排临时链路。这个设计带来的收益非常明显生产环境的任务成功率从 68% 提升到了 91%。原因并不神秘——预置模板经过实际人工验证链路稳定可靠而 Agent 自由编排本质上是在碰运气在关键业务动作上运气是靠不住的。6.3 技能推荐与冷启动新技能上线初期还面临一个无人问津的问题技能库里虽然有它但 Agent 在运行时总是优先选择历史表现好的老技能新技能完全得不到调用机会。我在 agent-skills 里做了一块简单的技能推荐逻辑当技能 B 和技能 A 经常在同一个任务中被先后调用时系统会自动记录这种相邻关系。然后当 Agent 调用了技能 A 之后技能 B 的召回权重会被临时调高。这个逻辑实现了两周以后技能的组合使用率明显上升。比如fetch_article_content被调用后后续跟进summarize_text的概率变高了database.slow_query_analyze被调用后后续跟进database.index_optimize的概率也变高了。这其实就是把人用工具的惯性迁移到了 Agent 身上让技能之间的搭配关系可以像经验一样积累下来。7. 技能库运作的日常维护、观测与灰度发布技能库做出来只是第一步能不能在日常运行中持续可用靠的是后台的观测和维护机制。agent-skills 项目运行了半年之后我逐渐建立了一套针对技能系统的日常保障流程。7.1 全链路日志与耗时分解在引入技能机制之前调试 Agent 只需要看一句 Prompt 和一次回复。有了技能机制之后一次用户请求可能触发多个技能还可能是并行或串行调用调试复杂度一下子暴涨。我最终的方案是在 BaseSkill 的 execute 方法里统一埋点无论任何技能被调用都会输出结构化日志{ timestamp: 2025-01-15T14:23:11.032Z, event: skill_execution, skill_id: meeting.minutes_generate, trace_id: req_8f3a2c9d, input_size: 3241, output_size: 1288, duration_ms: 2340, status: success, dependencies_executed: [ {skill: meeting.participant_extract, duration_ms: 320}, {skill: text.segment_by_topic, duration_ms: 410}, {skill: text.summarize, duration_ms: 982}, {skill: action.extract_action_items, duration_ms: 628} ] }把日志集中收集到 ClickHouse 之后就可以按skill_id维度统计每个技能的调用频次、平均耗时、失败率、输入输出大小分布。这套数据对定位问题非常有用。比如有一次我发现summarize_text的平均耗时超过了 3 秒一查才发现是因为某些文章抓取后的正文长度波动很大分段逻辑在极端情况下退化成了一次性提交全文导致响应变慢。7.2 灰度发布与版本回滚技能也是代码它也会改出 bug。我在 agent-skills 里引入了非常轻量的版本策略每个技能在 manifest 里有version字段注册表里同时保留最近两个版本。线上请求默认路由到stable版本但可以通过请求头里的X-Skill-Version: canary强制某个流量走canary版本。具体做法是当我要上线一个新版技能时先在本地和测试环境验证然后部署到 canary 版本用 5% 的线上流量跑一天。如果当天的skill_execution失败率、平均耗时和输出结构化字段的完整度都没有恶化第二天再把 stable 切到新版本。这套流程虽然简单但加了一层安全感。技能系统一旦跑起来就是业务系统的一部分不能拿生产环境当试验场。7.3 技能废弃管理最后说一下技能废弃这个很少有人讲的环节。技能库运行时间长了一定会出现某些技能被新技能替代、或者业务方向调整导致技能不再适用的情况。这些废弃技能如果不清理会造成两个问题一是污染召回器的语义空间导致相似的技能之间互相干扰二是 token 浪费无用的技能描述也会占用上下文。我设置的清理规则很简单连续 30 天调用量为 0 的技能进入候选废弃列表人工确认后先在召回器中剔除避免被模型选中调用保留一个月观察期一个月后如无使用记录再从注册表中移除。这个缓存式的淘汰策略让技能库始终保持在一个精而可用的状态。技能系统这块经验我前后迭代了大半年agent-skills 也从最初一个简单的函数集合变成了我现在很多自动化项目的地基。如果让我给正在做同样事情的人一句建议那就是不要在技能数量少的时候急着设计极简架构而要在技能数量上来之后尽早建立一套注册、描述、观测、灰度这套完整机制越早重构的沉没成本越低。最后再分享一个实用小技巧每个技能里都要预留一个dry_run模式只做参数校验和依赖检查、不真正执行副作用操作这在调试和做演示时会让你省出非常多时间。
返回列表