资讯详情

Agent技能体系设计:从函数堆叠到结构化技能库的工程实践

📅 2026/9/17 7:20:10 | 华诺云谱 👁 阅读
Agent技能体系设计:从函数堆叠到结构化技能库的工程实践
做过大模型 Agent 的同行应该都有这种感受最头疼的往往不是模型本身而是怎么让 Agent 稳定可靠地把活儿干完。我早期给智能体写工具函数什么get_stock_price、send_email、calc_order_amount一股脑堆在代码里。结果模型经常选错函数、漏传参数同一个功能改一版就要全量排查复用更是想都别想。后来我把所有能力收敛成了一套结构化的技能体系项目名就叫agent-skills。这篇文章就完整梳理一下我是怎么设计、实现和维护这套技能库的内容包括技能粒度怎么划分、描述怎么写模型才听得懂、参数约束怎么设计、以及一个可以直接抄的 Python 技能注册与调度实现。适合正在做智能客服、自动化办公、数据分析 Agent 的开发者参考。1. Agent技能体系的设计思路1.1 Agent技能到底是什么先下一个简单的定义Agent 技能是对智能体某一项能力的完整封装它不只是“一个可以被调用的函数”而是把“模型如何理解这项能力、调用时需要什么参数、执行成功后返回什么、失败时该反馈什么”全部打包在一起。我见过不少人把技能和工具函数画等号这是第一个误区。一个函数在代码层面当然可以独立存在但模型并不直接看代码它看到的是函数的名字和描述。如果描述写得含糊参数约束不清楚模型就会把两个八竿子打不着的技能混在一起用。技能化要做的事情就是把这层“模型理解”和“代码执行”之间的缝隙填上。在我的项目里一个技能大致包含五块技能名称、自然语言描述、参数声明、执行函数、异常处理。这五块缺一不可。名称负责给模型一个索引锚点描述负责告诉模型“你什么时候该用它”参数声明负责约束模型如何调它执行函数干实际活异常处理负责把失败原因翻译成模型能看懂的反馈。这五块组合在一起才叫一个完整技能。1.2 为什么技能化比堆函数更靠谱早期很多人习惯把 Agent 的能力写成一批普通函数然后通过提示词把所有函数说明都塞给模型。函数少的时候还行一旦超过二十个问题就来了上下文被函数说明撑爆、模型混淆相似功能、参数格式不统一每次改一个函数都要重新设计提示词。技能化把这些散落的函数收拢成统一结构后收益是立竿见影的对比维度普通函数堆叠结构化技能库模型理解成本依赖函数名和提示词描述容易混淆统一规整的描述和参数范式模型识别准确率明显更高复用性改一个业务场景就要复制粘贴注册一次多处复用按场景动态加载可观测性调用日志靠手工埋点注册中心统一拦截天然可审计扩展成本新增能力要同步改提示词加一个装饰器函数即注册完成容错能力异常直接抛给用户异常被转译成模型可理解的语言Agent 可自行纠正举一个我踩过的例子我之前有个函数叫query_user用来查用户信息还有一个叫query_user_orders用来查用户订单。提示词里写得很清楚但模型在用户问“帮我看看张三上次买了什么”的时候居然调了query_user然后拿返回的用户 ID 去编造订单。问题就出在描述没有明确“边界条件”。技能化改造之后我在描述里加了“本技能只返回用户基础资料不包含订单记录若要查询订单使用 query_user_orders 技能”模型选错的情况就少了很多。1.3 技能粒度怎么定太大太小都难受技能粒度是个非常主观但又非常关键的问题。拆得太细Agent 需要多轮调用才能完成一个任务上下文里绕来绕去反而容易出错。拆得太粗一个技能内部塞了太多业务逻辑复用性几乎为零。我自己的判断标准是三条第一一个技能应该能独立完成一次“子任务”不要拆到函数级别第二参数个数尽量控制在五个以内超过五个就要考虑是不是职责过于复杂第三技能之间要有清晰的副作用边界不要让同一个技能既读数据又写数据。拿发送营销邮件这个场景举例笨办法是做一个send_marketing_email大技能内部自己查用户、自己生成文案、自己发信。这样技能很“好用”但换个业务方想复用查用户那段逻辑就完全白搭。合理的拆法是拆成get_target_users查目标用户、generate_email_content生成邮件文案、send_email发信三个技能由 Agent 自行编排。这样每个技能的粒度都足够小且可以被其他场景复用。当然这里要补一句粒度没有银弹跟你的模型能力和使用场景强相关。如果你的模型是强推理型比如最新几代大模型可以适当放宽技能粒度让模型自己编排。如果是弱模型宁可拆细一点把编排逻辑留给代码层去固定。2. 技能的核心构成与描述工程2.1 一个标准技能由哪几部分组成我最终落地的技能结构包含以下字段逐个解释一下name技能唯一标识使用小写字母和下滑线比如get_weather_cn。命名尽量具体一点避免go、run这类抽象动词。description给模型看的自然语言说明这是最重要的字段后面单独展开。parameters参数声明我用 JSON Schema 结构描述每个参数的类型、含义、取值范围。handler实际执行函数接收参数返回结构化结果。required必填参数列表模型没拿到这些参数就不应该调用技能。categories技能归属的领域分类用于动态加载和权限控制。examples典型调用示例可以辅助模型更准确地填参数。用一个实际例子来演示{ name: query_weather, description: 根据城市名获取指定城市当前实时天气数据包含温度、湿度、风力等级。适用于用户询问今天天气、是否适合外出、穿衣建议等场景。若用户未提供城市名请通过 ask_location 技能向用户确认不要直接调用本技能。, parameters: { type: object, properties: { city: {type: string, description: 城市名称如北京、上海} }, required: [city] } }这个结构对应的执行函数就是普通 Python 函数def handler(city: str) - dict: data weather_api.fetch(city) return {city: city, temperature: data[temp], humidity: data[humidity]}2.2 描述是Agent选技能的命门如果说技能库里只有一个环节值得花最多时间打磨那一定是描述。模型不像人一样能看代码它对技能的“理解”完全来自描述文本。描述写得烂后续所有调优都是白费功夫。好的技能描述至少包含四层信息能力边界、输入要求、典型场景、排除情况。我见过最差的描述就是“获取天气信息”这种一句话。模型确实知道它能拿天气但不知道什么时候该用、城市名从哪儿来、拿不到城市名怎么办。最后模型就瞎猜。比较理想的描述是根据用户提供的城市名称查询该城市当前实时天气返回内容包括温度、湿度、风速和天气现象。 适用场景用户询问今日天气、周末出行是否合适、穿什么衣服等。 前置条件用户消息中已经包含明确的城市名或能推断出具体城市。 如果城市不明确请使用 ask_location 技能向用户询问禁止用默认城市查询。这个描述里模型能明确知道触发条件、前置条件、以及“不能做什么”。写描述这件事本质上是在给模型做行为约束不是写给人看的注释。你在写的时候要时刻提醒自己现在是在训练一个“会看说明书的工作人员”不是在写 API 文档。我的经验法则是描述放在技能文档的顶部然后花一半篇幅写“适用场景和触发条件”再用一两句话写“什么时候不要调用它”。不要小看后半句模型犯的大多数错误都是因为描述里没有排除项。2.3 参数设计让模型一次把参数传对参数设计直接决定了模型会不会把调用搞砸。JSON Schema 是目前最通用的参数声明方式主流模型对它的识别都很稳定。我在设计参数时有几个习惯第一每个参数都要写 description且描述要具体到“值怎么获取”。比如{ type: object, properties: { city: { type: string, description: 城市中文名例如北京、上海。必须来源于用户的原话不要自行翻译成拼音。 } }, required: [city] }这里“必须来源于用户的原话”就是关键提示。模型一旦看到这句话就不会自己去发明一个城市名。第二能用枚举约束的尽量用枚举。比如查询语言参数{ type: string, enum: [zh, en, ja], description: 内容语言没有明确要求时默认 zh }第三默认值写进描述不要只写在底层代码里。模型不看你的代码它需要看到“缺省时用什么”这个信息。第四必填参数要克制的设置。能通过追问补齐的参数就不要设置为必填否则模型在缺参时会强行编造一个值。在上面天气案例里如果city设为必填但用户没说城市名模型可能会直接传一个“全国”或“北京”上去。更好的做法是允许城市为空然后在描述里写“城市缺失时调用 ask_location”。3. 从零实现一套可用的Agent技能库3.1 先分清楚四层职责动手写代码之前先把整体架构分清楚后面扩展才不会乱。我习惯把技能库分成四层注册层负责收集和登记所有技能维护技能元数据。调度层接收模型返回的函数调用请求校验参数并路由到对应执行函数。执行层真正跑业务逻辑包含超时控制、异常捕获、结果规范化。观测层记录完整调用链包括模型选了什么技能、参数是什么、执行多久、返回什么。这四层不是必须分四个文件但逻辑上要清晰。很多项目做到后面变乱都是因为把注册和调度混在一起写。3.2 技能定义与注册中心怎么实现这一步直接上代码。我用装饰器 注册表的方式代码量小扩展也方便。先定义一个技能数据类# skill_base.py from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional dataclass class Skill: name: str description: str parameters: Dict[str, Any] handler: Callable[..., Any] required: List[str] field(default_factorylist) categories: List[str] field(default_factorylist) examples: List[str] field(default_factorylist) SKILL_REGISTRY: Dict[str, Skill] {} def register_skill( description: str, parameters: Optional[Dict[str, Any]] None, required: Optional[List[str]] None, categories: Optional[List[str]] None, examples: Optional[List[str]] None, ): def decorator(func): skill Skill( namefunc.__name__, descriptiondescription, parametersparameters or {}, handlerfunc, requiredrequired or [], categoriescategories or [], examplesexamples or [], ) if skill.name in SKILL_REGISTRY: raise ValueError(f技能 {skill.name} 重复注册) SKILL_REGISTRY[skill.name] skill return func return decorator然后实际使用# skills/weather_skill.py from skill_base import register_skill register_skill( description根据城市名获取指定城市当前实时天气数据包含温度、湿度、风力等级。 适用于用户询问今天天气、是否适合外出、穿衣建议等场景。 若用户未提供城市名请通过 ask_location 技能向用户确认不要直接调用本技能。, parameters{ type: object, properties: { city: { type: string, description: 城市中文名例如北京、上海。必须来源于用户原话。 } }, required: [city] }, categories[weather], ) def query_weather(city: str) - dict: # 这里替换成真实天气 API return {city: city, temperature: 26, humidity: 40, wind: 3级}注册中心的核心逻辑就这么多。每次新增能力只需要新写一个函数加一个装饰器主代码一行不用改。3.3 把技能安全地暴露给大模型技能注册完成之后下一步是把技能目录转换成模型能读取的结构。目前主流做法有两种Function Calling 格式和提示词注入格式。如果模型支持 OpenAI 风格的工具调用直接用前者模型对参数的理解最准确。# adapter.py from skill_base import SKILL_REGISTRY def build_tools_payload() - list[dict]: tools [] for skill in SKILL_REGISTRY.values(): tools.append({ type: function, function: { name: skill.name, description: skill.description, parameters: skill.parameters, } }) return tools这样一次转换就把所有技能全部暴露给模型了。模型决定调用某个技能后返回的响应里会包含tool_calls字段里面有技能名和参数。调度层拿到这个结果后去注册中心查对应 handler 执行即可。如果用的是不支持函数调用的模型就只能把技能描述拼进系统提示词里用 JSON 字符串再接一次 LLM 来产出调用参数效果会差一些二次解析还容易出错。我的建议是能用 Function Calling 就一定优先用它是在模型层面对“调用规范”做了强约束比我们自己用提示词约束要稳定得多。3.4 执行层容错让报错变成模型能看懂的话执行层是最容易被低估的一层。很多 Agent 项目在模型选对技能、参数也对的情况下依然经常“翻车”原因是执行函数抛了异常而异常信息直接暴露给模型时是一堆堆栈模型根本看不懂。我自己封装了一个安全执行的调度函数统一捕获异常并转成自然语言反馈给模型# executor.py import traceback from skill_base import SKILL_REGISTRY def execute_skill(skill_name: str, arguments: dict) - dict: skill SKILL_REGISTRY.get(skill_name) if not skill: return {status: error, message: f未知技能{skill_name}} # 必填参数校验 missing [k for k in skill.required if k not in arguments] if missing: return { status: error, message: f参数不完整缺少{, .join(missing)}。请补充后重试。 } try: result skill.handler(**arguments) return {status: success, result: result} except Exception as e: traceback.print_exc() return { status: error, message: f技能 {skill_name} 执行失败原因{str(e)}。 f请检查参数是否合理或更换其他技能解决问题。 }这个返回结构有讲究status让代码层可以做流程判断message是给模型看的“可读报错”。当模型收到“执行失败原因城市名不能为空。请补充完整后重试”这种反馈时它能自动进入修复循环要么换参重试要么问用户补信息。这种自愈能力是 Agent 和普通脚本最大的区别之一。4. 常见问题与排查技巧实录4.1 高频问题速查表症状可能原因排查方法Agent 频繁选错技能描述缺少排除条件或两个技能描述高度相似检查描述前两句加“本技能不适用于…”说明参数总是传错或编造参数描述过于抽象没有说明取值来源在参数描述中加“必须来源于用户原话/上下文”技能被调用后反复报错必填参数设置不合理或执行层没做参数清洗打开调度日志确认实际收到的参数再调整 schema上下文爆炸技能描述太长或系统提示词里灌入了全量技能按场景动态加载技能不要一次性暴露全部技能技能执行结果模型不用返回结构过于复杂模型没提取到关键信息规范返回结构把核心结论放在 result 的最前面技能之间状态污染执行函数用了全局变量缓存技能的 handler 必须无状态共享状态请显式传入4.2 三个真实坑位详解第一个坑是“描述里没有写排除项”症状是 Agent 总爱用一个看似万能的大技能。我有个技能get_user_info能查用户资料。写的时候我觉得“查用户”很清楚结果模型在用户问“帮我算一下这个用户去年消费总额”时也调它然后拿用户资料自己编了个总额数字。后来我在描述末尾补了一句“本技能不返回任何消费统计信息若要统计消费金额使用 analyze_user_spending 技能”错误率直线下降。第二个坑是“参数注释写得像写给人看的”。我之前写了个参数叫date_range描述是“时间范围”结果模型传了一堆千奇百怪的格式2024-1-1~2024-12-31、last year、过去一年。后面我把描述改成“参数为 JSON 字符串格式为 {start: YYYY-MM-DD, end: YYYY-MM-DD}例如 {start: 2024-01-01, end: 2024-12-31}”模型的正确率接近满分。参数描述的定位不是人是模型不要说“大致意思”要给精确范式。第三个坑是“技能共享全局状态”。最早我的日历技能和提醒技能共用一个全局列表存事件结果一个 Agent 会话里A 技能新增事件B 技能读不到隔几分钟数据还变没了。后来所有技能改成纯函数模式依赖都通过参数传状态统一放数据库或 Redis问题立刻消失。技能 handler 一定不能带隐式状态否则并发一上来排查成本高到让你怀疑人生。4.3 给技能库加“护栏”权限、限流与审计技能库一旦上生产就不能只考虑功能好不好用还要考虑安全和可控。权限方面不同技能对敏感数据的访问级别不同。我在注册表里加了categories字段调度层在 execute 之前检查当前会话是否具备该分类的权限。比如admin分类的技能只允许内部员工创建的管理员 Agent 调用普通用户触达不了。限流方面有些技能调用的外部 API 是有成本的不加限制就会被一个死循环打爆。我在执行层外面套了一层计数同一会话内单技能调用超过 N 次就强制中断并返回“该操作执行次数过多请检查流程是否正确”。审计方面我在调度入口统一落日志记录时间、会话 ID、技能名、参数、执行耗时、返回状态。这不仅是排查问题的关键也是后续优化技能描述的数据来源。我会定期从日志里统计哪个技能被高频调用、哪个技能从来没被选中、哪个技能错误率最高这些数据直接驱动下一轮技能治理。最后分享一个我养成的习惯每隔两周翻一次执行日志重点看不小于三次调用且成功率低于 80% 的技能。这类技能不需要重新设计大多数情况下就是描述里漏了某个边界条件补一句排除说明就能带来可观的提升。如果你也在维护 Agent 技能库建议从日志驱动迭代开始这比闷头重写架构要高效得多。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。