Agent技能化改造:从散装函数到可编排的智能体技能库 1. 不要重复造轮子Agent 技能化改造的出发点我最初做智能体的时候习惯把每一项能力都拆成独立函数再一股脑挂到大模型的调用列表里。功能少的时候还好等函数数量超过二十个问题就开始扎堆出现模型频繁选错函数、系统提示词越写越长、加一个新功能要连带改旧逻辑……那段时间我几乎每天都在崩溃的边缘直到开始整理 agent-skills 这个项目才重新找到了一条相对清爽的技术路线。这篇文章想聊的就是我在这套技能体系上踩过的坑、做过的设计取舍以及最终沉淀下来的一套可以照着实现的方法。什么叫技能我的定义是一个自带说明、参数规范、执行逻辑和输出结构的完整能力包。它跟工具函数最大的区别在于粒度——工具往往只做一件事比如查天气、读文件技能则是一组相关操作的有序封装比如把一篇网页抓下来并提炼成摘要就是技能抓取网页只是技能内部的一个步骤。把技能作为 Agent 的基础复用单元相当于给智能体准备了一张能力菜单模型不需要理解每个技能内部怎么实现只需要看懂菜单上的描述就能知道自己该点什么菜。这个改造最直接的价值是解耦。技能之间通过结构化输入输出互相衔接内部实现完全独立你要给 Agent 增加新能力只需要往技能库里新增一个技能包而不是去翻动整套提示词和函数列表。第二个价值是可测试性普通函数只能验证输入输出对不对技能因为自带完整的工作流可以针对同一份输入做端到端回归测试。第三个价值是可共享——技能包一旦做成了就能沉淀下来跨项目复用这也是我把项目命名为 agent-skills 的初衷。有人可能觉得这不就是把函数换个名字吗其实区别比想象中大。函数是一张长长的扁平清单模型做选择时面对的是几十个孤立的入口技能则带着使用场景描述、适用边界、依赖关系和输出约定模型面对的是几十个有说明书的能力包。一个人学开车不需要从理解发动机活塞开始同样一个 Agent 调用网页摘要技能也不需要知道里面走了几步模型推理、调了什么服务。这种边界感恰恰是稳定性的来源。2. 技能定义规范让每个技能自带使用说明书技能包要想被 Agent 正确使用第一步是有一套严格的描述规范。我走过一段弯路早期技能描述写得很随意比如总结文档四个字结果模型在需要提取表格时也调它在只需要取一句话时也调它误用率高得离谱。后来我把技能元信息重构成了一个固定结构每个技能包都必须包含下面这些字段。skill: name: web_page_summarize version: 1.2.0 description: 抓取指定网页正文并生成结构化摘要。 适合在需要快速理解一篇长文、对比多篇网页核心观点时使用。 如果用户只想要某个精确数字或原文句子请不要使用本技能 改用 fetch_webpage 直接返回原文。 input_schema: url: type: string description: 待处理的网页完整链接 target_language: type: string enum: [zh, en] default: zh summary_length: type: integer minimum: 100 maximum: 500 default: 200 output_schema: title: string summary: string key_points: array source_url: string dependencies: - fetch_webpage tags: [research, web, summarization] timeout_seconds: 30整套规范里最容易被忽略但又最关键的是description字段。模型不会预先知道技能代码长什么样它唯一能看到的就是这段文字所以这段文字本质上是技能与模型之间的唯一沟通界面。好的描述应当包含三部分技能能做什么、适合什么场景、不适合什么场景。我专门在描述里写反例比如上面的技能明确写了如果用户只想要某个精确数字请不要使用本技能这比单纯写本技能用于抓取网页并总结能显著降低误调率。输入输出 schema 的意义也不只是约束参数格式它同时充当了技能之间的数据契约。我早期用纯字符串传参结果下游技能经常解析失败改成结构化 schema 之后技能 A 的输出可以直接作为技能 B 的输入中间不需要再经过大模型二次改写。每个技能包最终会被封装成一个执行类Python 下大概长这样class Skill: def __init__(self, manifest: dict): self.name manifest[name] self.version manifest[version] self.description manifest[description] self.input_schema manifest[input_schema] self.output_schema manifest[output_schema] self.dependencies manifest.get(dependencies, []) self._timeout manifest.get(timeout_seconds, 30) def validate_input(self, payload: dict) - dict: # 校验参数、填充默认值不符合直接抛错 return normalized_payload async def execute(self, payload: dict, context: dict) - dict: raise NotImplementedError一个技能就是一个黑盒给定结构化输入返回结构化输出。它内部可以调用普通函数、可以发起网络请求、可以调用大模型做中间步骤甚至可以嵌套调用其他技能。外部世界完全不关心这些细节。这种封装带来的直接好处是每个技能都能单独写测试、单独替换版本而不会影响其他能力的运行。我后来做技能评估时基本就是逐技能跑一批标准样例看命中率和输出质量效率比改造前高了一个量级。3. 技能注册与组合从孤立能力到可编排的技能链有了技能定义下一步就是搭建技能库的注册中心。注册中心承担三件事启动时扫描并加载所有技能包、校验元信息完整性、解析技能之间的依赖关系。我在实现中发现依赖校验这块如果放在运行时再去检查报错会非常难排查——一个技能找不到依赖往往要等到被调用时才炸出来现场乱成一团。所以我把全部检查都放到启动阶段。class SkillRegistry: def __init__(self): self._skills {} self._dependencies {} def load_skill(self, skill: Skill): if skill.name in self._skills: raise SkillConflictError( fskill {skill.name} 已存在版本 {self._skills[skill.name].version} ) self._skills[skill.name] skill def resolve_dependencies(self): for name, skill in self._skills.items(): missing [ dep for dep in skill.dependencies if dep not in self._skills ] if missing: raise DependencyError( fskill {name} 缺少依赖: {missing} ) def remove_skill(self, name: str): dependents [ s.name for s in self._skills.values() if name in s.dependencies ] if dependents: raise DependencyError( f无法移除 {name}以下技能仍依赖它: {dependents} )有了注册中心接下来要考虑的是技能与模型之间的协作模式。我用的是经典的选择-执行循环模型读取用户任务从注册中心拿到所有技能的名称和描述列表根据任务挑选合适的技能并填充参数系统执行技能后把结构化结果返回给模型模型再决定是继续调用下一个技能还是直接给出最终答复。这里有个关键设计模型每次选择技能到底应该看到全部技能还是只看到一部分我一开始把全部技能描述都塞进提示词结果上下文被撑得很长选择准确率反而下降。后来我改成两级策略先让模型从粗粒度分类中选一个大类再在大类内部做细粒度选择。技能打上tags字段就是为了这一步——模型先定位到研究类还是数据处理类再在对应子集里挑选具体技能。这个改进让每次调用的提示词长度降低了约四成选择准确率也有明显提升。技能组合的编排逻辑我把它称作技能链。比如调研助手这个 Agent收到任务对比三款数据产品的优缺点实际执行时可能依次命中四到五个技能search_knowledge_base搜索候选资料、fetch_webpage抓取详情页、web_page_summarize生成单篇摘要、build_comparison_table汇总成对比表。这四个技能串起来就是一条链每个技能的输出会转化成下一个技能的输入数据。要保证链条顺畅最关键的是控制状态传递。我不建议让技能直接操作共享内存那会让技能之间隐性耦合排查起来非常痛苦我更倾向让每个技能只接收明确参数、只返回明确结果链路的状态由一个轻量级的上下文对象维护对象里记录当前收集到的所有中间产物。class SkillChainExecutor: def __init__(self, registry: SkillRegistry): self.registry registry async def run_chain(self, skills: list[dict], initial_input: dict): context {} current initial_input for step in skills: skill self.registry.get(step[name]) current await skill.execute(step[args], context) context[skill.name] current return context这个执行器只是最简版本实际项目里我在每步之间加了日志埋点、超时控制和失败重试。但核心思想始终没变技能链的每一步都是可观测、可重放的出了问题能准确定位到具体是哪一个技能、在哪一个参数上失败了。相比改造前那种一个大函数里嵌套十层逻辑的写法这种编排方式几乎把排查难度降低到了看流水线的程度。4. 一次完整的落地实测搭一个带记忆的通用研究助手理论说再多不如直接搭一个东西跑一遍。我用 agent-skills 的框架做了个通用研究助手目标是从零开始接受一个调研问题最终产出一份结构化的对比报告。整个过程我拆成了五个技能知识库检索、网页抓取、内容摘要、对比表生成、报告落盘。agent: research_assistant skills: - search_knowledge_base: description: 根据关键词从内部知识库检索候选资料列表 input: { query, top_k } output: { results: [{ title, url, snippet }] } - fetch_webpage: description: 抓取网页正文内容保留必要的标题与结构 input: { url } output: { title, content, content_hash } - web_page_summarize: description: 将网页正文压缩为要点摘要 input: { url, summary_length } output: { title, summary, key_points } - build_comparison_table: description: 将多篇来源材料的要点合并为对比表格 input: { items: [{ source, dimension, value }] } output: { table: [[dimension, source_a, source_b]] } - save_report: description: 将最终报告写入本地文件 input: { path, content } output: { saved_path, size_bytes }搭建过程比想象中顺利真正的难点集中在这几个环节。第一是技能描述之间的互相打架fetch_webpage和web_page_summarize一开始描述边界不清晰模型经常在用户只要求抓原文时误调摘要技能。我花了两个晚上给这两个描述补了详细反例误调率才降下来。第二是中间结果的处理三个资料页的原文加起来可能超过两万字如果全部堆进对话历史第二轮模型调用直接上下文告急。解决方案是让每个技能默认只返回摘要级结果只有用户明确要求原文时fetch_webpage才返回完整内容。组装完成后的执行循环大概是这样的模型先调用search_knowledge_base拿到候选列表再循环调用fetch_webpage和web_page_summarize处理每个候选最后用build_comparison_table汇总、save_report落盘。我跑了两百条真实调研任务的样本记录了三个指标技能选择命中率、单任务消耗 token 数量、端到端平均耗时。指标改造前(单体实现)agent-skills 实现变化技能/函数选择命中率73%91%18 个百分点单任务平均 token 消耗约 18.2k约 11.4k下降 37%端到端平均耗时约 42 秒约 35 秒下降 17%最让我意外的是 token 消耗的变化。改造前最让我头疼的就是那句上下文越来越长每次迭代都感觉在往一个无底洞里塞内容技能化之后每次循环只需要把上一个技能的输出摘要传给模型而不是把所有原始材料都背着走消耗自然就降下来了。选择命中率从 73% 升到 91%则主要归功于描述规范和两级选择策略——模型面对的不再是毫无区分的二十个函数而是一组按语义区隔开的能力包。这次实测也让我确认了一个判断技能化的收益并不仅仅体现在代码组织上它直接改善了模型调用层面的稳定性。因为技能边界清晰模型的调用路径变得更容易预测出错了也更方便定位。如果你也在做类似需求我建议第一次落地时不要追求技能数量先把五到八个核心技能做扎实跑通一两个真实任务链路再逐步扩充。5. 实测中的高频坑描述幻觉、上下文爆炸与技能冲突实测过程中踩到的坑很有代表性几乎每个做技能化改造的人都会遇到我挨个说下根因和解决方案。第一个坑是描述幻觉。模型对技能的描述会过度解读比如某个技能描述里写了处理文档模型就认为它什么文档都能处理包括它根本没实现过的 PDF 解析。根因在于描述写得过于宽泛没有显式声明能力边界。我的解决办法是在描述模板里强制加一个not_suitable_for段落把不该调用的场景明确列出来同时每轮测试都要记录模型的误调用日志把高频误用场景反向补进描述里。这个办法听着笨实际效果比任何抽象调优都直接。第二个坑是上下文爆炸。技能链跑起来之后每个技能的中间输出都会留在上下文里链越长上下文越臃肿。尤其是fetch_webpage这类产出大文本的技能一次能塞进几万字模型后续几乎无法有效利用其余信息。解决思路是给技能输出加一层摘要投影机制链表里的每个技能都能声明自己的输出是完整版还是摘要版执行器默认只把摘要版写进上下文完整版放进磁盘缓存等模型明确需要时再按 ID 取回。这个机制上线后我的实测任务平均 token 消耗立刻下降了三成。第三个坑是技能冲突。不同技能之间如果名称接近或描述语义重叠模型经常选错。我遇到最典型的是content_parse和content_extract两个技能都能处理网页正文但一个偏向结构解析、一个偏向信息抽取模型完全分不清。最后我把这两个技能合并成了一个用入口参数区分两种模式。这是我从这次改造中学到的教训技能粒度不是越细越好如果两个技能在模型看来几乎分不清那就说明他们本来应该是一个技能通过参数区分内部模式即可。第四个坑是依赖问题。我的依赖检查虽然放在了启动阶段但有一次新加的技能依赖了一个还没发布的技能版本注册中心只校验了名称没校验版本号结果运行时调用了旧版本接口字段对不上直接报错。后来我把依赖字段改成了nameversion的完整格式启动时校验名称和版本两个维度这个问题才彻底解决。另外所有技能调用我都套了一层超时控制和重试逻辑默认超时 30 秒重试一次重试仍失败就把错误信息结构化返回给模型由模型决定是换技能还是换参数。这个兜底设计让长链路任务的失败率降低了一半以上。最后一个提醒是关于日志的。技能化系统的可观测性比单体函数式系统重要得多因为我需要在模型误选技能时快速定位原因。我现在每个技能调用都会记录一行结构化日志调用时间、技能名、参数摘要、返回状态、输出字节数、耗时。排查问题的时候我一般先看日志里模型到底选了哪个技能再看这个技能输入输出是否符合预期两步就能定位八成以上的问题。6. 技能的版本管理与持续沉淀把经验转成可复用资产技能做出来之后真正的考验才刚刚开始怎么让技能包长期维护下去怎么让它们跨项目复用。我参考了常规的版本管理思路给每个技能包打语义化版本号主版本号表示接口破坏性变更次版本号表示新增能力修订号表示修复缺陷。技能库目录按命名空间组织每个技能包自带测试用例、样例输入和文档。目录结构大概是这样的skills/ research/ search_knowledge_base/ 1.2.0/ manifest.yaml main.py tests/ examples/ web_page_summarize/ 1.1.3/ ... data_processing/ build_comparison_table/ 2.0.1/ ...技能版本不能只发布不管我建立了一套简单的持续改进流程。每周收集一次线上的技能调用日志重点看三类问题调用失败率超过 5% 的技能、描述被反复修正后依然误选的数据、用户明确反馈结果不对的案例。这些问题按照严重程度排优先级逐个修复后发一个新版本并更新对应的测试用例。这个过程很朴素但非常有效——因为我发现技能系统的绝大多数问题不是代码逻辑错而是描述补不准、边界不清晰这类元信息问题只有靠真实调用数据才能持续逼近最优。技能沉淀到一定程度我还做了个共享技能库把内部几个项目通用度高的技能抽出来统一维护。共享技能库有两条铁的纪律一是任何改动必须过回归测试不能破坏使用方二是技能接口要保持向后兼容确实需要破坏性变更时必须先发布新版本再给所有使用者留出迁移窗口。这两条纪律让我少了很多协调成本跨项目复用时基本可以做到拿来就用。做完整套 agent-skills 体系我最大的体会是技能化的本质是把过去写在提示词里的隐性知识逐步转写成了结构化的显性资产。以前给 Agent 加能力靠的是不断往提示词里堆描述现在加能力就是往技能库里新增一个经过测试、带好文档的技能包。这个转变看起来只是工程组织方式的变化但长期看它决定了一个 Agent 项目到底能走多远——单体的提示词项目越改越乱直到推倒重来而技能化的项目则能一年一年地持续积累越用越顺手。如果你正想把手里那个功能越堆越多的智能体体系救回来不妨从给现有函数包一层技能描述开始收益会比你想象得快。