开篇先交代个背景。我最近在重构自己手头一个基于大模型做的自动化助手折腾了一圈后发现真正决定这个助手好不好用的不是模型选得多大、Prompt写得多花哨而是技能这一层做得够不够扎实。我这里的技能不是指模型本身的推理能力而是指你赋予Agent的那些可复用的、能被大模型自主决策调用的能力单元。这个思路整理下来就是这篇博文想聊的东西一套完整的Agent技能架构从设计、定义、实现到排错全流程怎么落地。如果你正在做AI Agent相关的应用比如自动客服、内容生产助理、数据分析助手、内部知识库问答机器人或者你只是好奇大模型应用里工具调用到底怎么做得优雅、可扩展、不出幺蛾子这篇文章应该能给你一套可以直接参考的骨架。我会把我在实际项目里踩过的坑、验证过的方案、以及最后沉淀下来的代码结构都摊开来讲不整虚的。1. 先想清楚Agent技能到底解决什么问题1.1 没有技能体系的Agent是什么样的如果你只是写一个单轮的对话接口把用户问题丢给大模型然后拿回复给用户那确实不需要技能体系。但一旦你要做的正经Agent应用需要让模型去查数据库、调接口、发通知、算指标、改配置问题马上就来了。最粗暴的做法是把所有工具函数一股脑塞进System Prompt里。我早期就是这么干的一个Agent挂了十几个函数定义每个函数的描述、参数、注意事项加起来接近三千个token。结果有两个问题特别致命一是上下文被工具定义占掉太多模型真正用来推理的空间变小了到了复杂任务上明显变笨二是函数一多模型就开始选择困难经常调错函数、传错参数甚至为了完成任务虚构一个根本不存在的函数名出来。你可能觉得这是模型能力不够换更强的模型就好了。但实际体验下来就算是目前top级的模型在管理大量工具时也会出现类似的混乱只是阈值不同而已。本质问题在于你没有给模型建立一套清晰的能力地图它只知道你有一堆工具却不知道在什么场景下该选哪个、怎么规规矩矩地选。1.2 技能的定义边界我后来把思路从工具转向了技能。工具是离散的函数技能是带语义边界、带使用约束、可以组合的能力单元。一个技能背后可以是一个API调用也可以是一段几十行的业务逻辑甚至可以是一个工作流。举个例子查询天气是一个工具但根据用户地理位置和当前日期给出穿衣建议就是一个技能。前者只做一件事后者包含了规则判断和结果组装。在Agent系统里让模型直接使用后者成功率会高很多因为模型不需要自己组合多个工具来完成一个完整的业务动作组合逻辑已经在技能内部封装好了。所以我的定义是技能 能力封装 语义描述 参数协议 执行策略。这四个部分缺一不可。能力封装是实际干活的代码语义描述是让模型知道什么时候该用我参数协议是让模型知道该怎么用我执行策略是让系统知道用我出错了怎么办。1.3 我的技能体系选型思路在动手之前我对比了几条路线。第一是直接用LangChain的Tool抽象好处是社区生态成熟现成的工具多坏处是抽象层级偏薄对于业务复杂的场景组合和管理能力不太够用。第二是自己从零写一套灵活度最高但需要自己处理很多边界问题。第三是基于语义内核的思路把技能当作一组可注册、可发现、可编排的插件模块来管理这也是我最终采用的方案。选型理由很简单我希望技能不仅能被静态调用还能被Agent动态发现。也就是说Agent在执行一个复杂任务时能自己判断我需要先调用A技能再调用B技能如果A的结果不满足条件我还可以调用C技能。这要求技能注册表里保存的不只是函数指针还有技能的元数据、执行策略、以及技能之间的关系。2. 技能如何设计从工具到能力的抽象2.1 技能、工具、插件三者的区别很多初学者把这三个概念混着用但在设计技能体系时必须分清它们的分工。工具是最底层的能力单元它不关心业务语义比如执行SQL查询是一个工具调用外部HTTP接口是另一个工具。插件是工具的打包分发形式比如数据分析插件里面可能包含多个工具和一个预置的Prompt片段。而技能是面向任务的能力封装它可以选择调用一个或多个工具也可以自己实现逻辑关键在于它直接对应一个相对完整的用户意图。比如说你的Agent要支持周报自动生成这个能力如果在工具层做模型需要自己组合取数、汇总、生成文本三个工具才能完成任何一个环节判断出错都会失败。而如果封装成一个技能模型只需要把周报这个任务连同时间范围、关注指标传进去技能内部自己负责取数、汇总最后把素材交给模型生成文本。从模型视角看任务复杂度瞬间降了一个量级。2.2 技能注册表的设计技能注册表是整个技能体系的中枢它负责保存所有技能的定义并在Agent需要时提供查询和加载。我把它做成了一个类核心是三个字典一个按技能名索引一个按功能标签索引一个按适用场景索引。按技能名索引很好理解就是通过唯一标识获取技能执行器。按功能标签索引的意义在于支持动态发现比如Agent遇到一个统计类任务可以先查注册表看看有哪些技能带统计标签再根据语义匹配选一个最合适的。按场景索引则是为了实现技能的环境隔离比如在仅查询场景下只加载只读技能避免模型误调用写操作相关的技能。注册表本身不直接存储技能实现代码它存储的是技能描述对象包括名字、版本、描述、参数Schema、执行入口、超时时间、重试策略、依赖关系。执行入口是一个可调用对象可以是一个本地函数也可以是一个远程服务的客户端封装。这层抽象让我后面做技能热更新时轻松了很多——只要描述对象不变执行入口随时可以替换。2.3 技能描述怎么写给模型看这是我在实践中花时间最多的地方。技能描述直接决定了模型能不能在正确的时候调用正确的技能所以它不能像写函数注释那样随便。写好技能描述有几个关键原则。第一描述要以该技能用于解决什么问题为主体而不是该技能能做什么。比如一个天气查询技能好的描述是根据城市名查询未来三天的天气预报适用于用户询问天气、出行建议、活动安排等场景差的描述是这是一个天气查询API。第二要写明技能的触发条件边界包括什么时候不该用。比如一个数据库查询技能要明确指出仅允许执行SELECT语句其他类型的SQL操作需经人工审核。第三要给参数加约束说明不只是类型还包括取值范围、单位、可选值因为模型经常对参数的语义理解有偏差。我自己的习惯是给每个技能写一段三十到五十字的核心描述再用一个可选的使用建议字段补充边界信息。在构造给模型的系统提示时只把核心描述塞进去使用建议单独存放。这样既保证了模型有足够信息做决策又不会让上下文太长。2.4 技能的命名与分类规范命名这种看似不起眼的事实际对调用成功率有很大影响。我一开始用过一种描述性的长名字比如query_weather_by_city_and_return_advice结果模型经常记不全生成调用时函数名被截断。后来我改成短名字加语义标签的组合比如weather_advice同时打上weather、travel、clothing三个标签效果好了很多。分类规范也值得认真设计。我的建议是按业务域划分一级分类再按任务类型划分二级分类。比如一个企业内部的Agent一级分类可能是数据查询、内容生成、流程操作、系统管理二级分类再往下细化。这样做的目的是帮助Agent在多层级的技能库中快速缩小匹配范围减少每次加载的技能数量提高决策准确率。3. 核心实现一个可用的技能管理模块3.1 项目结构规划我最终实现的技能管理模块放在了项目的skills目录下结构如下skills/ ├── registry.py # 技能注册表核心管理类 ├── schema.py # 技能描述Schema定义 ├── loader.py # 技能加载器负责动态导入 ├── executor.py # 技能执行器封装调用与重试 ├── builtin/ │ ├── calendar_skill.py │ ├── sql_query_skill.py │ └── document_summary_skill.py └── custom/ └── example_skill.pybuiltin目录放的是系统内置技能custom目录放的是需要按项目定制扩展的技能。设计上遵循一个原则注册表不直接依赖任何具体技能实现它只依赖技能描述对象和入口可分发的协议。这样新增技能时不需要改动注册表代码只要实现协议并调用注册函数即可。3.2 技能描述的Schema定义技能描述对象我定义成了dataclass核心字段有这些name唯一名字使用小写下划线风格version技能版本号遵循语义化版本description给模型看的核心描述tags功能标签列表用于动态发现parameters参数Schema遵循JSON Schema子集entry_point可调用对象或模块:函数名字符串timeout_seconds执行超时默认30秒retry_policy重试策略最多重试次数和退避策略dependencies依赖的其他技能名称列表参数Schema是整个协议里最敏感的部分。我一开始用了非常严格的JSON Schema要求模型一定得传type、properties、required这些字段结果发现模型经常在参数格式上报错。后来我放宽松了只在description层级做语义约束在参数类型层面只检查必需的强类型字段其他弱类型字段允许模型自动转换。实际调用技能的执行流程分四步查询技能描述、校验参数、执行入口、返回标准化结果。校验参数这步不是简单用jsonschema库跑一遍还要做业务级校验比如日期格式是否合法、枚举值是否在允许范围内、ID是否存在。这些校验逻辑我放在技能实现代码里而不是Schema里因为Schema只负责类型框架业务规则还是得靠代码跑。3.3 注册与加载机制技能加载我用了延迟加载策略。系统启动时只加载技能描述对象的轻量级元数据不执行技能模块的import。真正触发调用时才动态导入对应模块这样启动速度快也避免了一条技能代码出错拖垮整个Agent系统的问题。关键实现如下# loader.py import importlib class SkillLoader: def __init__(self): self._cache {} def load_entry(self, entry_point: str): if entry_point in self._cache: return self._cache[entry_point] module_name, func_name entry_point.split(:) module importlib.import_module(module_name) func getattr(module, func_name) self._cache[entry_point] func return func这个加载器的好处是模块只有在首次被调用时才会真正加载到内存里后续调用走缓存。我在一个项目里注册了二十多个技能但Agent执行单次任务通常只会用到其中两三个剩下的模块根本不会被import资源开销控制得很好。技能注册则提供了一个注册函数外部模块只要调用一次就能把技能挂载到注册表上# registry.py class SkillRegistry: def register(self, skill: SkillDescriptor): if skill.name in self._skills: raise DuplicateSkillError(skill.name) self._skills[skill.name] skill for tag in skill.tags: self._by_tag.setdefault(tag, []).append(skill)每个项目里我会建一个skills/init.py集中调用所有内置技能和自定义技能的注册函数类似于一个装配入口。新加技能时只需要在该文件里加一行import和一行注册调用改动的代码面非常小。3.4 技能调用的执行策略执行器的设计参考了服务治理里的思想给每个技能加了一层管控逻辑。核心的返回结构我定义为dataclass class SkillResult: success: bool data: Any None error: str latency_ms: int 0 retries: int 0统一的结果结构意义很大。它让Agent在处理技能返回时不需要关心每个技能内部的返回格式差异统一用success字段判断是否成功。在给模型的上下文里我会把成功和失败的结果分开标注失败的返回会附带简短错误原因帮助模型决定下一步是调整参数重试还是换方案。超时和重试的执行策略是这样的单个技能默认超时三十秒超过就中断并标记失败对于网络类技能最多重试两次采用指数退避第一次等一秒第二次等两秒对于计算类技能不自动重试因为重试大概率还是同样结果不如让Agent换思路。重试策略我放在技能描述里这样不同技能的容错表现可以差异化。实际编写技能执行器时我会在调用入口函数前先记录时间戳调用结束后统一计算耗时。所有异常都被捕获并转为SkillResult防止一次技能运行异常导致整个Agent会话崩溃。这一点在长时间运行的任务里尤为重要因为一个未捕获的异常可能会让整个工作流状态错乱。3.5 技能编排与组合让Agent能链式调用单技能调用只是基本功。真正让Agent变得强大的是它能够组合多个技能来完成复杂任务。我的实现里有一个任务编排层它并不需要像流程引擎那样预先定义固定的DAG而是允许模型在对话循环中动态决定下一步调用哪个技能。具体做法是利用注册表的场景索引在每个Agent执行步骤前把跟当前任务相关的技能描述动态注入上下文。比如模型判断当前用户意图是查天气并生成出行建议任务编排层会从注册表中查询标签包含weather和travel的技能把这两个技能的描述注入上下文然后模型通过一次或多次调用完成整个任务。这一步是整套架构的精华也是初期最容易翻车的地方。我的经验是不要让模型一次看到所有技能只让它在当前上下文里看到跟本步骤相关的候选技能。这样既减少了token消耗又大幅降低了模型乱选技能的概率。注册表的标签索引就是为这个场景服务的。4. 踩坑实录常见问题与排查思路4.1 模型死活不调用技能怎么办最让人头疼的问题就是你明明把技能描述写得清清楚楚模型就是不调用反而靠自己的常识硬答。排查下来原因往往出在两个方面。一是技能描述的位置不对。有些框架把所有工具定义放在用户消息之后、系统消息没有覆盖到的地方模型可能没有把它当成可操作的定义只当成了普通文本。我的做法是把技能描述统一放到系统消息的可用技能段落并且每个技能描述前加当且仅当这样的话术约束。二是技能描述与用户意图的语义距离太远。比如你的技能叫check_stock_price描述里全是通过证券代码查询最新价格这类机械表达模型可能联想不到你觉得这只股票现在能买吗这个意图。我的调整思路是给技能增加场景化示例在描述里写一两个典型用户问题的变体。比如check_stock_price的描述末尾加一句适用于用户询问股票行情、咨询买卖时机、查看持仓收益等场景。这比强行让模型做语义联想可靠得多。4.2 技能参数被模型解析错参数解析错误是最常见的运行时故障。典型的场景是技能期望一个datetime对象模型传了一个字符串技能期望一个枚举值模型传了一个同义但不在枚举里边的词。我在早期很依赖在参数Schema里加enum约束但模型对enum的遵守程度取决于预训练语料里对该领域枚举值的理解深度。比如传周一还是星期一、传上午还是AM模型可能基于语感做选择不一定会严格对齐你Schema里的值。对此我的方案是在技能执行器内部加一层参数归一化函数专门把模型传入的常见变体映射到标准值。另外我发现让模型自己决定参数值往往不如让它先复述参数再确认。所以我在人物Prompt里加了这么一条规则在调用技能前先用自己的话简述你理解的参数含义确认后再执行。这一步让参数理解错误率降低了差不多一半。4.3 技能执行超时与失败重试超时问题多出现在调用外部API的场景。外部服务不稳定几十秒的请求太常见。如果技能超时就标记失败Agent就直接放弃了体验很差。我的处理是在executor中实现基于retry_policy的重试逻辑核心代码如下# executor.py import time def execute_with_retry(entry_func, params, policy): max_retries policy.get(max_retries, 0) base_delay policy.get(base_delay, 1.0) last_error for attempt in range(max_retries 1): try: start time.time() data entry_func(**params) return SkillResult(True, datadata, latency_msint((time.time()-start)*1000), retriesattempt) except Exception as e: last_error str(e) if attempt max_retries: time.sleep(base_delay * (2 ** attempt)) return SkillResult(False, errorlast_error, retriesmax_retries)重试背后的考量是大多数外部接口错误是瞬时性的网络抖动或服务端短暂不可用隔一两秒再试往往能成功。但对于业务逻辑错误比如参数校验不通过、数据不存在重试没有意义。所以我在技能描述里用一个布尔字段explicit_retry标识该技能是否适合重试由技能开发者决定策略。4.4 技能描述注入导致的上下文膨胀技能描述本身也是token消耗的一部分。如果一个Agent的技能库越来越大每次把所有技能描述都塞进上下文模型上下文很快就不够用了。我实测过一个包含三十个技能描述的上下文光描述部分就可能占掉八千到一万token这还没算参数Schema和用户输入。解决思路我前面提过一点就是按标签动态注入。但还有一个更细的维度按会话阶段注入。比如一个客服Agent在开场阶段只需要用户识别、订单查询这两个技能等真正到了处理阶段再加载退款申请、人工转接这些技能。分阶段注入既有技术上的必要性也符合用户交互的自然节奏。我实现里维护了一个当前可用技能集的列表Agent每完成一步就由编排层更新这个集合。这样上下文里永远只保留当前阶段真正可能用到的技能描述其他技能即使在注册表里也完全不可见。5. 进阶方向如何让技能体系持续生长5.1 技能热更新与灰度上线技能不是写完就完了线上Agent的技能需要持续迭代。我在实践中实现了技能热更新机制核心逻辑是注册表支持在运行过程中覆盖同名校验过的技能描述和执行入口。这样发布新版本技能时不需要重启整个Agent进程只要调用更新接口即可。灰度逻辑也很简单技能描述对象里加一个enabled字段和一个weight字段。新技能先以10%的权重上线流量按权重分配到新旧技能观察调用成功率、平均延迟、用户反馈这些指标达标后再逐步放量。这个机制虽然实现不复杂但对线上稳定性帮助很大能有效避免一次错误发布影响全量用户。5.2 技能调用质量评估我建议每个技能都记录运行日志包括调用时间、参数快照、返回结果、耗时、是否重试、错误信息。这些日志除了排查问题更重要的是积累技能质量的评估数据。有一个指标我特别关注技能的无效调用率。也就是模型调用了某个技能但最后发现结果对最终任务没有帮助甚至要返工。无效调用率高说明该技能的描述边界可能不够清楚或者触发条件设置得过宽。通过定期分析这些指标我可以针对性地调整技能描述而不是拍脑袋改Prompt。5.3 多Agent间的技能共享如果你的项目有多个不同角色的Agent——比如一个负责客服、一个负责内容审核、一个负责报表生成——你会发现很多底层能力是重复的比如查询用户信息、校验权限、发送通知。为了避免每个Agent各自维护一套重复的技能我做了技能共享层。共享的方式是把通用技能下沉到基础技能库特定业务的技能保留在各自Agent的技能集里。运行时每个Agent可以按需加载共享技能也可以在共享技能基础上叠加自己的业务约束。这样做既减少了重复开发成本也保证了同一个底层能力在不同Agent中的行为一致——比如权限校验逻辑不可能客服和审核两个Agent执行出两套标准。最后再多说一句我在实际项目里的体会技能体系的设计没有标准答案每个项目的业务边界不一样技能划分的粒度也会不同。但有一个原则是通用的——技能的粒度应该以模型一次能正确理解并调用为准而不是以代码复用的便利性为准。把技能拆得太细模型组合的负担会变大把技能做得太粗又失去了灵活性。我踩过几次坑之后现在的习惯是先粗后细先用一个大技能验证场景跑通了再根据调用数据拆分成更精细的技能。这个内容后续还可以往技能自动生成、技能质量自动优化的方向扩展不过那是另一个话题了。