资讯动态

给大模型装上手脚:智能体技能体系设计与实践

发布时间:2026/10/14 8:43:10 来源:尧图企业网站定制
相信不少做AI应用的朋友都有过这种体验模型对话能力很强但一让它“干点实事”——查个数据、调个接口、操作一下系统——就直接卡壳。问题不在于大模型本身而在于我们没给它一套“手脚”。我最近在梳理一个名为“agent-skills”的项目核心就是解决这件事给智能体构建一套结构化的技能体系让它能真正执行任务而不是只会聊天。这篇文章不聊虚的直接拆解这个项目的完整设计思路、落地实现、以及我在调试过程中踩过的坑。如果你正在做智能体开发或者想给自己的AI应用接入工具调用能力这篇文章应该能帮你省下不少摸索的时间。1. 技能体系的设计思路为什么不能把所有逻辑塞进提示词里先说一个最常被问的问题大模型本身已经能理解自然语言为什么还要单独搞一套“技能”体系直接把工具说明写进System Prompt不行吗答案是可以但只能应付非常简单的场景。一旦工具数量超过5个或者工具之间存在依赖关系纯靠提示词描述就会出现三个问题第一模型容易把工具参数搞混尤其是参数名相似的时候第二每次请求都要传一大段工具描述Token开销和响应延迟都上去了第三也是最关键的——工具调用的错误处理逻辑无法复用每次都要让模型重新“悟”。我在“agent-skills”里做的最核心的一个决策就是把技能定义从提示词里剥离出来做成一套独立的注册与调用机制。每个技能就是一个可复用的功能单元它有明确的名称、描述、输入参数定义和执行逻辑。模型需要做的是“选择技能”和“填充参数”至于这个技能内部怎么执行、怎么处理异常模型完全不用操心。这里有个很重要的架构对比需要理解传统的“提示词驱动工具调用”是让模型决定一切包括工具选择、参数解析、结果处理而技能化的方式是“模型做决策代码做执行”。后者的优势在于执行逻辑变得可控、可测试、可复用。比如“查询天气”这个技能不管模型在什么语境下触发它最终执行的永远是同一套经过验证的代码逻辑而不是每次都由模型现场发挥。从适用范围来看这套设计适合三类场景一是需要对接多个外部API的智能体应用二是需要长链路任务编排的场景三是在多个业务系统里需要复用同一套工具能力的平台型项目。如果你只是做个简单的问答机器人那确实用不上这么重的设计直接调API就行。1.1 技能的三层结构拆解我最终确定下来的技能结构分为三层描述层、契约层、执行层。很多初做技能系统的开发者容易把这三层混在一起导致后续扩展时牵一发动全身。描述层解决的是“模型怎么知道这个技能干什么”的问题。这一层包含技能名称、一句话功能描述、触发场景说明。写描述层的时候有个技巧不要用抽象词汇要用“动词宾语场景”的结构。比如“获取指定城市的实时天气数据并返回温度、湿度、风力信息”这就比“天气查询功能”要好得多。因为模型本身是靠语义匹配来选技能的描述越具体选错的概率越低。契约层解决的是“模型怎么调用这个技能”的问题。这一层定义了输入参数的结构、类型、必填项和约束条件。我在项目里用的是JSON Schema因为它的表达能力够强而且大模型对JSON的支持本身就很好。参数定义里有个容易忽略的点要给每个参数写清楚“备注”告诉模型这个参数的取值范围和格式要求。比如日期参数要注明“格式为YYYY-MM-DD”数字参数要注明“范围为0到100”。执行层就是实际的代码逻辑。这一层接收契约层传过来的参数执行具体操作返回结果。执行层必须做两件事参数校验和错误兜底。参数校验是为了防止模型填入了不符合约束的值错误兜底是为了当外部服务不可用或者数据异常时返回一个结构化的错误信息给模型让它有机会修正或换一种方式完成用户的需求。1.2 为什么选择“技能注册中心”而不是“即用即写”项目做到一半的时候我面临一个分岔路是每个技能都独立实现、用完即弃还是维护一个集中的技能注册中心我最终选了后者原因有三个。第一技能是可枚举的。在任何一个垂直领域里智能体需要的操作能力其实是有限的。拿客服场景举例无非就是查订单、查物流、处理退款、转人工这几个动作。把这些技能集中管理整个系统的能力边界就非常清晰。第二技能需要复用。同一个“用户身份校验”技能可能在多个业务流程里都要用到。如果每个流程都重新实现一遍代码冗余不说还容易出现行为不一致的问题。集中注册之后一个技能改一处所有用到它的地方都生效。第三调试和观测更方便。技能注册中心可以统一记录每个技能的调用次数、耗时、成功率、失败原因。这些数据对后续优化非常关键。比如你发现某个技能调用频率特别高但是成功率和模型选对率都不高那就说明技能的描述或参数定义需要调整。实际落地的时候我把技能注册中心实现成一个独立的Python模块技能信息以结构化数据的形式注册进去。注册中心本身不关心技能的实现细节它只负责维护技能清单、提供查询接口、把模型的选择结果路由到对应的执行函数上。2. 技能定义与注册从模型意图到函数调用的关键一跳技能体系设计的再好最终要落地到代码里。这一节直接展示我在“agent-skills”里实现的核心部分技能定义的数据结构、注册方式和模型调用的对接逻辑。2.1 技能定义的完整数据结构我用的技能定义结构包含以下几个关键字段。每个字段的选取都是有原因的我会在下面逐一说明。SKILL_SCHEMA { name: get_weather, description: 获取指定城市的实时天气数据返回温度、湿度、风力等级和天气现象, parameters: { type: object, properties: { city: { type: string, description: 城市名称使用中文全称如北京、上海, required: True }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位默认使用摄氏度, required: False } } }, handler: weather_handlers.get_weather, timeout_ms: 5000 }name字段用于技能的唯一标识。这里有两个规范需要强调一是全小写加下划线避免大小写混用导致的匹配问题二是要用动词开头比如get、create、update、delete。这样模型在语义匹配的时候能更快锁定候选技能。description字段是整个定义里最重要的内容。我在调试中发现description写得太长或太泛都会导致模型选错技能。好的description要满足两个条件一是能明确说清楚“什么场景用这个技能”二是要指出“什么情况下不要用这个技能”。比如“获取天气”的description里我会写明“仅用于查询当前天气状况不包含未来7天预报”这样模型遇到“明天天气怎么样”的问题时就不会错误调用这个技能。parameters字段定义了输入参数的结构。这里强烈建议使用JSON Schema规范因为很多模型API本身对JSON Schema的支持就很成熟比如OpenAI的Function Calling、Claude的Tool Use都兼容这个格式。2.2 技能注册与发现的实现逻辑技能注册中心的核心职责是“登记”和“查询”。登记是把技能定义保存到内存或数据库中查询是根据模型的意图把候选技能列表返回给模型。下面是我在项目里用的技能注册中心核心实现简化后的版本只有几十行代码class SkillRegistry: def __init__(self): self._skills {} self._handlers {} def register(self, skill_def, handler_func): self._skills[skill_def[name]] skill_def self._handlers[skill_def[name]] handler_func def get_skills_for_prompt(self): # 返回给模型使用的技能配置子集 return [ { name: s[name], description: s[description], parameters: s[parameters] } for s in self._skills.values() ] def execute(self, skill_name, **kwargs): if skill_name not in self._handlers: raise SkillNotFoundError(f技能 {skill_name} 未注册) return self._handlers[skill_name](**kwargs)注册中心能正常工作起来有一个设计细节值得注意技能定义的存储和技能执行函数是分开注册的。存储的是“元信息”用于给模型看执行的是“真实逻辑”用于实际干活。这样当模型需要调用某个技能时系统可以根据技能名直接路由到对应的执行函数而不用每次通过字符串匹配去动态导入模块。我建议技能清单的返回不要一次性全量返回给模型。当技能超过15到20个的时候模型在上下文里做工具选择的准确率会明显下降。更稳妥的做法是加一层“技能预过滤”根据用户问题的关键词或embedding相似度先筛选出3到5个最相关的技能再交给模型做精确选择。2.3 参数注入与严格校验防止模型“自由发挥”模型填参数这件事看起来简单实际坑非常多。最常见的几类问题日期格式不对、城市名带后缀比如“北京市”而不是“北京”、数字参数传了字符串、可选参数漏传导致执行逻辑崩溃。我的解决方案是在执行层做一刀切的校验不信任模型填的任何参数。校验逻辑分三档格式校验、范围校验、语义校验。格式校验是最基本的比如时间参数必须是“YYYY-MM-DD”格式邮箱参数必须符合正则表达式。范围校验针对的是数值型参数比如分页查询的page_size必须在1到50之间。语义校验需要结合业务逻辑比如“查询订单”技能要求传入的user_id必须存在于用户表中这通常需要查询数据库来确认。下面是我在项目中用的一段校验代码展示了参数注入前的处理逻辑def validate_parameters(skill_def, raw_params): # 过滤掉未定义参数 defined_params skill_def[parameters][properties] clean_params {k: v for k, v in raw_params.items() if k in defined_params} # 检查必填参数 missing [ param for param, meta in defined_params.items() if meta.get(required) and param not in clean_params ] if missing: raise ParameterValidationError(f缺少必填参数: {, .join(missing)}) # 枚举类型校验 for param, value in clean_params.items(): meta defined_params.get(param, {}) if enum in meta and value not in meta[enum]: raise ParameterValidationError(f参数 {param} 的取值必须为 {meta[enum]}) return clean_params这套逻辑跑了一段时间之后我发现一个现象模型填错的参数种类和概率比想象中要稳定得多。也就是说80%的错误集中在日期格式、单位混用、术语不统一这几个问题上。后来我在参数的description里直接把这些容易错的点写出来比如“温度单位可选值为celsius或fahrenheit不接收其他写法”错误率明显下降。这说明参数定义的描述质量直接决定了模型填参的准确率。3. 技能编排从单次调用到多步任务的关键升级单技能调用解决的是“一个动作”的问题。但真实场景里用户的需求往往是“一串动作”。比如“帮我查一下从北京到上海的高铁如果下午三点前有票就订一张没有的话告诉我最早几点的车还有票”。这个需求涉及查班次、查余票、判断时间、下单订票至少四个步骤。技能编排要解决的就是“怎么把这些技能串起来”的问题。我一开始的做法是让模型自己输出多步计划然后一步步执行。但实践下来效果不太稳定尤其是当两步之间有数据依赖时模型经常把上游的输出参数张冠李戴。后来我改用了“技能链”的方式把一些固定流程固化成模板。3.1 技能链与动态规划怎么选技能链就是把一个业务流程预先编排好不同技能按顺序执行前一个技能的输出作为后一个技能的输入。这适合流程固定、步骤已知的场景。比如“查天气再根据天气推荐穿衣搭配”这个流程的步骤是固定的直接用技能链处理最简单。动态规划则交给模型实时决策用户提出需求后模型从所有可用技能里选出合适的技能并按顺序编排执行计划。这适合开放式场景但风险在于模型可能会漏步骤或排错顺序。我在项目里做了一个折中方案系统内置了一批常用的技能链同时允许模型在技能链不匹配时自动构建新的执行序列。判断逻辑是这样的先让模型判断是否匹配已有技能链匹配则走技能链不匹配则走自由规划。这个折中方案的执行示意如下1. 接收用户请求 2. 在技能链模板库中查找候选 3. 若匹配加载技能链按顺序执行各技能节点 4. 若不匹配调用模型规划执行序列逐技能执行技能链和自由规划共存了几个月我遇到的主要问题是某些技能链过于死板无法应对用户的特殊要求。比如技能链里查天气后用“推荐的穿衣指数”作为输出但用户可能只想问“下雨概率”。这个问题的根源在于技能链模板的粒度太粗。后来我把每个技能链模板里的节点参数设计成可覆盖的用户有明确指定时直接用用户参数替换模板参数效果好了不少。3.2 上下文传递与状态管理多步任务最麻烦的还不是选技能而是怎么管理执行过程中的上下文状态。技能A的输出是技能B的输入如果执行到B的时候发现A的某个关键字段没保存就只能让A重新跑一遍。我的做法是在技能编排层引入了一个简单的“上下文存储”机制。每一步执行后系统会把技能输出的结构化数据存入上下文后续技能可以通过“参数引用”的方式从上一步中取值而不是每次都让模型重新生成参数。具体实现上上下文存储就是一个字典key是技能名加输出字段value是实际的输出值。在编排逻辑中如果检测到参数的值是“$skills.get_weather.output.temperature”这样的引用语法就自动从上下文中取出对应值。这样就构成了一个简单的数据流转管道。这种设计的好处在于多步任务里的数据依赖关系变得可追溯。出问题时可以直接看上下文的完整记录定位到底哪一步的产出不符合预期。3.3 技能编排中的循环与条件分支真实任务中还有两个不可回避的情况循环和条件分支。比如“批量查询多个城市的天气”如果5个城市依次调用同一个技能没必要让模型生成5个相同步骤直接在编排层写个循环遍历城市列表依次调用即可。条件分支则是根据某个中间结果决定后续走哪条路径。比如“先查用户所在地再根据当地是否下雨决定是否提醒带伞”。这种分支逻辑放代码里实现比让模型自己判断稳定得多。我给编排层设计了三种节点类型技能节点、条件节点、批量节点。技能节点是最基本的调用单元条件节点根据预设条件判断走向批量节点则是对一组参数反复执行同一个技能。三种节点可以嵌套组合用树形结构描述完整的执行流程。这一步做完之后整个技能编排体系才算是真正支撑得起复杂业务。4. 实操部署中的常见问题与解决方案技能系统的开发调试过程中踩过的坑和总结出的经验我认为这里值得展开讲一下。这些内容来自训练和实际使用的反复测试能帮大家少走弯路。4.1 模型不调技能或乱调技能怎么办症状模型面对用户请求时要么不使用任何技能直接依赖自身知识作答要么在无关的技能之间反复横跳最后返回“我无法处理”。排查维度里问题往往出在技能描述而不是模型本身。这里我经验是——把技能描述中“模型自身能答出什么”和“技能实际能返回什么”的边界写清楚。举例来说“查询天气”这个技能如果模型本身的知识中可能包含了部分城市的天气信息模型就会倾向用自己的知识回答而不是调技能。解决办法是在描述里加强提示语气明确写明“实时数据必须调用技能获取禁止使用模型内置知识回答”。另一个排查维度是技能数量太多导致选择困难。如果一次请求把30个技能全塞给模型选错概率很高。我采取的方案是动态技能过滤在前置阶段将技能候选缩减至5个以内。4.2 技能返回结果格式不统一如何解决早期我犯过一个错误——不同技能的执行函数返回的数据结构各不相同。有的返回JSON嵌套有的返回字符串有的是直接一个列表。这个对后续步骤的上下文传递造成了相当大的麻烦。技能编排层不得不针对每个技能单独写解析逻辑。后来我硬性规定所有技能的输出必须是一个统一包装的字典包含至少两个字段status和data。status只有两种取值success或error。data存真正的业务数据。这样一来所有下游逻辑只需要读status判断成功失败读data拿业务数据。统一格式带来的另一个好处是错误处理变得规范化。技能执行出错时data里会放一个error_message字段说明具体的失败原因。这个错误信息会被系统拼装后返回给模型让模型能够根据错误信息决定下一步是修正参数重试还是换一个技能或是直接放弃并告知用户。4.3 技能调用超时与重试策略外部API调用不可能百分之百稳定。一开始我用的策略是“失败就重试一次”但后来发现这样太过粗糙。有些技能的重试必须间隔一定时间比如第三方接口限流有些技能的重试需要变换参数比如上游服务临时不可用。统一的重试逻辑反而会放大问题。目前的方案是给每个技能单独配置重试策略包括重试次数、退避时间、重试条件。比如查询类技能设置最多重试3次每次间隔2秒写入类技能只重试1次因为重复提交可能产生脏数据。重试策略需要注册技能的时候一并配置这个配置保存在技能定义里。这里的“必须注意”像是基础常识——“幂等和不幂等操作要区别对待”。如果一个技能执行的是下单、转账这类会产生实质性影响的操作重试必须谨慎。更安全的做法是不自动重试而是把错误返回给模型由模型询问用户是否重新发起操作。4.4 调试技能调用链路时如何定位问题关于技能调用链路的排查我建议直接看三个环节其一是模型是否选对了技能其二是参数注入是否完整且符合校验规则其三是执行函数返回的结果是否符合预期。实践中最常见的定位方法是开启技能调用的日志记录把每轮请求的完整链路打印出来。我的日志记录格式是这样的【请求ID】req_20250101_001 【原始用户输入】请帮我查一下明天北京天气怎么样 【技能选择】get_weather (置信度: 0.87) 【参数字段】{city: 北京, unit: celsius} 【参数校验结果】通过 【返回结果】{status: success, data: {temperature: 3, humidity: 45, wind_level: 2}} 【耗时】360ms这套日志对排查问题的价值非常大。最典型的场景是执行结果报了参数错误但看日志发现参数校验环节明明是通过的那就说明错误可能是执行函数内部因为其他原因抛出的与模型填参无关。这类问题定位起来会快很多。4.5 测试技能系统最有效的方式单元测试、集成测试这些常规就不重复了。我想专门聊一个非常实用的测试手法——回归测试在技能系统的场景里使用起来价值极大。因为大模型的行为存在随机性同一套技能配置今天跑得好好的明天换了一个模型版本或者改了个描述可能就选错技能了。解决之道是维护一组覆盖各类场景的“标准测试用例”每次调整技能体系后都回归一遍。标准测试用例要覆盖四类场景正常场景、边界场景、错误场景、易混淆场景。错误场景就是用户请求里带了技能处理不了的内容看系统怎么拒绝。易混淆场景尤其值得留心比如同时存在“查询图书”和“查询作者”两个技能用户的请求是“我想了解某本书的作者是谁”模型很可能把两个技能都调用一遍。用这类用例来校准技能描述能让选技能准确率稳定维持在一个高位。5. 往工程化方向走治理、观测与持续迭代技能体系稳定运行只是第一步。真的把它作为产品能力提供给外部用户时还有三个层级的问题需要在工程化层面解决技能治理、质量观测、敏捷更新。技能治理关注的是“谁能注册技能、谁负责审核、怎么保证技能之间不冲突”。最初我允许所有人都可以自由注册技能结果没多久就出现两个技能功能高度重叠的情况。比如一个新同事定了“获取城市温度”的技能而实际上已有的“获取天气”技能可以覆盖这个场景。后来我加了技能注册审核机制新技能必须先提交定义检查是否与已有技能重复确认无冲突后才可执行注册。质量观测层面重点是两个指标选型准确率和执行成功率。选型准确率就是“模型在N次需要技能调用的请求里选对技能的比例”衡量的是技能描述的质量。执行成功率则是“技能调用后返回成功的比例”衡量执行层的稳定性。这两个指标要分开看因为它们优化的路径完全不同。在持续迭代方面建议按“描述优先、执行兜底”的优化顺序来调整问题。绝大多数选型问题通过修改技能描述就能解决轻易不要改动执行逻辑。执行逻辑的调整应该建立在对内部异常充分了解的前提下避免因为修复一个远程小概率问题而引入新的行为变化。技能更新的版本管理我强烈建议要做。每次修改技能定义或执行逻辑时打一个版本号并保留历史版本配置。这样如果新版本出现口碑问题可以一键回滚。我见过不止一个项目由于没有做版本管理改出一个缺陷后只能靠手动恢复最终耽误了不少时间。版本管理做简单点就好一个字典记录版本号对应的完整配置即可不需要一开始就引入复杂的发布系统。最后分享一点实在的体会如果把“agent-skills”从头再搭一遍我大概率会从第一天就做技能注册中心和标准日志而不是等到技能数量超过十个之后再回头补课。技能体系看上去是一个纯技术问题实际上考验的是系统设计能力——怎么描述能力边界、怎么管理上下文、怎么处理失败这些才是真正影响用户体感的地方。现在这个项目的技能模块已经服务了好几个应用场景从内部工具到对外的智能客服都有覆盖。近期我在尝试的一个方向是定期把技能日志里的失败样本拉出来用大模型自动分析规律、生成优化建议再人工确认后更新技能定义。这条路走通了技能体系的维护成本还能再降一截。如果你正在做类似的智能体项目建议从最小可用的技能定义开始跑不要一上来就想把所有场景都覆盖到。跑通一个闭环再逐步扩展技能的广度和深度是这条路线上比较稳的推进方式。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑