资讯动态

Agent技能体系设计指南:从Function Calling到agent-skills工程化落地

发布时间:2026/10/14 9:23:28 来源:尧图企业网站定制
聊到 Agent 开发很多人第一个卡住的地方往往不是模型本身而是技能。模型的推理能力再强如果不接上工具、函数、数据源它就像一个只有大脑没有手脚的人什么都能想什么都做不了。这两年我花了不少时间折腾 Agent 相关项目做得越多越觉得真正决定一个 Agent 能跑多远、跑多稳的不是那层花哨的 Prompt而是它背后有没有一套结构清晰、可扩展、可复用的技能体系。标题里的 agent-skills说白了就是这个东西一套把 Agent 能用到的能力抽象成标准化模块然后让模型在运行时自己去“按说明书调用”的工程方案。这篇文章我会直接结合我实际做过的一个模拟项目 X把 agent-skills 的核心设计、Schema 规范、动态加载机制、安全边界和一些容易踩的坑全部摊开讲。适合正在搭 Agent 框架、或者已经跑过几个 Demo 但觉得不好维护的开发者。文章里不会贴大段平台文档只会给你我在落地过程中验证过的做法和思考过程。1. agent-skills 到底在解决什么问题1.1 从 Function Calling 说起做了 Agent 开发的人肯定都接触过 Function Calling也就是让大模型输出一个结构化的调用意图比如查询天气(city北京)然后程序去执行这个函数再把结果回传给模型。这个模式很香它让模型第一次有了“动手”的能力。但 Function Calling 如果只是零零散散地堆函数问题马上就来了。我最早做的第一个 Agent Demo里边就放了二十多个函数查天气、算汇率、搜新闻、解析简历、生成图片占位符、读写本地文件……每个函数都要写一份描述文档传给模型。有一次我在描述文档里写错了一个参数名结果是模型明明知道要查城市气象却总是把城市名填到city_name这个不存在的字段里报错报得人头皮发麻。更头疼的是函数越加越多Prompt 越来越长模型的选择准确率反而下降了。这就引出一个很核心的问题Agent 需要的能力不是“一堆函数”而是一套有组织的技能库。技能库里的每一项不仅要能被模型发现还要能被模型正确理解、正确调用并且在调用失败时能给出可诊断的信息。agent-skills 想要解决的正是这个“如何把 Agent 能力做标准化”的工程问题。1.2 技能和工具不是一回事在我自己的项目里“工具”和“技能”是刻意做了区分的。工具是执行层的东西比如“用某模型服务商的 API 做文本向量化”“读取某个数据库的表”。而技能是面向任务的一层封装比如“搜索资料并整理摘要”“分析一份本地 Excel 并生成统计图表”。一个技能内部可能串起三四个工具也可能只有一次 API 调用。这个区分很有用。因为模型的注意力是有限的你给它 200 个细粒度工具它会在选择上反复横跳但你给它 20 个粗粒度的技能每个技能都有明确的输入、输出和适用场景它反而更稳。我在模拟项目 X 里把技能库分成了三层基础原子技能发请求、读文件、领域技能简历解析、合同关键信息抽取、还有流程技能先做数据分析再生成报告。模型的调用目标永远是领域技能和流程技能原子技能则被封在内部不让模型直接碰。这么做还有一个好处技能的可复用性大幅提高。同一个“查询数据库”的原子技能可以被“生成周报”和“排查订单异常”两个流程技能共用不会出现两套实现互相打架的情况。2. 技能库的架构设计与核心模块2.1 技能注册与元数据设计一个技能要能被 Agent 使用光有实现代码是不够的它还需要一份“说明书”。我在项目里把这份说明书叫技能元数据统一用 YAML 维护。一个典型的技能元数据长这样name: analyze_sales_data description: 读取指定路径的销售数据文件执行汇总统计并生成一张趋势图。适合处理月度销售分析需求。 category: data_analysis parameters: - name: file_path type: string required: true description: 销售数据文件的完整路径支持 xlsx 或 csv 格式。 - name: group_by type: string required: false default: month description: 统计维度可选值包括 month、week、product。 returns: - type: image description: 生成趋势图的文件路径。 - type: text description: 汇总统计的 Markdown 表格内容。这段元数据有四个关键点值得展开说。第一是description这是模型判断“要不要调用这个技能”的最重要依据。描述写得太抽象模型不知道该用写得太啰嗦又浪费 token还容易把关键信息淹没。项目里我总结了一个经验描述要写清楚“这个技能处理什么输入、给出什么输出、适合哪一种任务”尽量控制在两到三句话以内。比如“适合处理月度销售分析需求”这句就是专门给模型看的提示词。第二是parameters的严格定义。这里每个字段不仅要声明类型还要写清楚可选值、默认值和边界情况。实践下来required标记特别重要。有一次我漏了对group_by的可选值说明模型自由发挥填了一个by_region技能执行时直接抛异常而且异常信息没被 Agent 捕获最后给用户返回了一个编造的“分析失败”。后来我在参数定义里加了枚举校验并在异常信息里回传给模型“可选项是 month/week/product请基于这个约束重新调用”错误率立刻降下来。第三是returns也就是输出声明。很多初版技能不写输出声明模型调用后拿到了原始返回结果却不知道该怎么解读。我在项目里统一要求每个技能必须在元数据里声明返回类型和结构模型就能把技能输出准确翻译成用户能懂的回答。第四是category。技能库大了以后这个字段是给语义检索用的后面专门讲。2.2 技能执行器的三种典型形态技能元数据只是“说明书”真正干活的是技能执行器。我在项目里把执行器做了三种形态按复杂度递增排列。第一种是最简单的函数式执行器直接给 Python 函数加一个注册装饰器把它挂进技能库。register_skill(analyze_sales_data) def analyze_sales_data(file_path: str, group_by: str month): # 读取文件、统计、生成图表 return {chart_path: ..., summary: ...}第二种是类式执行器适合有内部状态需要维护的技能。比如一个“监控文件目录变化”的技能它需要在多次调用之间保持对上次文件快照的记忆函数式写法会显得很别扭。class DirectoryMonitorSkill: def __init__(self, watch_dir: str): self.watch_dir watch_dir self._snapshot None def run(self): ...第三种是流程式执行器也叫技能编排器。它自己不直接干活而是把多个原子技能串起来。这种技能通常长这样class WeeklyReportSkill: def run(self, report_type: str, period: str): data self.query_sk.handle(SELECT * FROM orders WHERE ...) chart self.chart_sk.handle(data, chart_typeline) doc self.doc_sk.handle(chart, templateweekly_report) return doc形态选择没有绝对的优劣关键看你的技能是需要长期记忆还是无状态的一次性任务。我的建议是能不用类就不用类函数加注册表最容易维护状态越少出 bug 的概率越低。2.3 为什么需要语义检索技能数量超过 30 个以后我遇到一个特别现实的问题把所有技能的 description 全塞进系统提示词模型到后期根本记不住。当时做了一次评测40 个技能的时候模型选对技能的概率只有 68%这对一个要稳定上线的项目是不可接受的。解决办法是在系统提示词里只放少量“必选技能”的完整描述其余技能走语义检索根据用户问题动态召回最相关的 Top N 个技能的元数据再注入到提示词里。这个思路其实很朴素用户问“这周销售额怎么样”那大概率跟“查询销售数据”和“生成统计图表”有关跟“解析合同”没啥关系。与其让模型在几十个技能里大海捞针不如我们提前把针捞出来。具体实现上我会把每个技能的 name、description、category 拼成一段文本用某个向量化模型转成向量存进一个本地向量数据库。用户提问进来后先用同样的向量化模型对问题编码做相似度检索取 Top 5 到 Top 8 个候选技能再把它们的完整元数据拼进上下文。这一步提升非常明显技能召回准确率能到 95% 以上。唯一要注意的是技能升级后要同步更新向量库里的向量我在初期踩过这个坑改了技能描述但忘了刷新索引结果模型一直按旧描述行事。3. 从零搭建一套可用的技能系统3.1 目录结构与注册机制知道了原理我们直接过一遍我在模拟项目 X 里实际采用的目录结构。这算是我个人比较推荐的工程布局skills/ base_skills/ http_client/ skill.yaml __init__.py file_reader/ skill.yaml __init__.py domain_skills/ sales_analyzer/ skill.yaml __init__.py contract_extractor/ skill.yaml __init__.py workflow_skills/ weekly_report/ skill.yaml __init__.py每个技能目录内部skill.yaml就是前面说的元数据__init__.py里是执行器的实现。项目启动的时候会有一段扫描注册代码遍历整个skills/目录读取出所有skill.yaml再用 importlib 动态导入对应的模块。这样每加一个新技能只需要新建一个目录不需要改动 Agent 主程序的任何配置代码。这里有一个值得写进备忘录的细节技能目录名必须和 yaml 里的 name 保持一致而且执行器模块里必须暴露一个统一的入口函数我这边约定俗成叫handle。否则扫描注册时对不上排查起来非常痛苦。3.2 输入校验到底校验什么技能层级的输入校验我一开始觉得没必要后来被现实教育了。大模型生成的参数有时候就是不可靠它可能把 1 填成1可能漏掉必填字段也可能把枚举值写错。为了不让这些错误直接炸到执行器内部我在技能注册的必经路径上加了一层校验。校验用 JSON Schema 就能实现也可以直接用 Pydantic 模型。我更推荐 Pydantic因为可以把校验失败的详情自动转成模型能看懂的提示。举个例子from pydantic import BaseModel, Field class SalesAnalyzeInput(BaseModel): file_path: str Field(description销售文件的完整路径) group_by: str Field(defaultmonth, pattern^(month|week|product)$) def validate(skill_name: str, params: dict): schema get_schema(skill_name) model type(SkillInput, (BaseModel,), {__annotations__: schema.annotations}) try: return model(**params) except Exception as e: return { error: str(e), hint: 请检查参数类型和可选值范围重新调用该技能 }把校验放在执行器外面还有一个额外好处它把“模型瞎传参数”和“代码真的出了 bug”这两类问题彻底分开了。前者是常见的上下文理解偏差后者是需要人工排查的故障。分开之后Agent 的日志会清晰很多后续做问题复盘也轻松。3.3 把技能接入 Agent 主循环技能库本身是静止的它得接入 Agent 的主循环才算真正活起来。我用了一段极简伪代码来描述这个流程def agent_loop(task: str, timeout: int 30): context load_context(task) for turn in range(MAX_TURNS): candidates retrieve_skills(task, top_n5) plan llm_plan_and_call(context, candidates) if plan.action finish: return plan.answer if plan.action call_skill: result execute_skill(plan.skill_name, plan.params) if result.error: context.add(f技能调用失败{result.error}) else: context.add(f技能返回{result.value}) else: # 模型没有明确选择技能说明上下文有歧义追问澄清 context.add(信息不足请向用户询问具体需求)从这里能看出一个关键点execute_skill的返回结果不只是给用户看的它要先回填到 context让模型看得到“这个技能实际发生了什么”模型才能决定下一步是继续调用别的技能还是直接总结答案。很多初版 Agent 跑不好就是因为把技能结果直接丢给用户模型完全不知道这一步执行得怎么样下一步自然就是瞎编。另外主循环里一定要给技能调用设超时和时间预算。我的经验是每个技能默认 10 秒超时整个 Agent 任务不超过 60 秒。技能调用如果超时优先重试一次再不行就放弃并把超时原因传给模型让它换一个思路。不加这个机制一个死循环技能能把整个 Agent 拖到不可用。3.4 给技能做单元测试技能系统是给大模型用的但你绝对不能只靠“跑一次看看效果”来验证。我后来在项目里固定了一套自动化测试套路每个技能至少有三类测试用例。第一类是正常路径测试用一个典型的输入参数验证返回结构和期望一致。第二类是参数边界测试故意传缺失参数、错误枚举值、类型不符的参数验证校验层能返回友好错误。第三类是噪声测试这个比较有意思。我会把用户的模糊提问拿来做测试比如“给我看下这个月买卖的情况怎么样”然后跑一遍完整的技能召回加调用链路看最终能不能正确落到“sales_analyzer”这个技能上。噪声测试在初期最容易暴露问题。我印象最深的是一次合同提取技能用户在问题里写了“帮我看看第二页的违约责任”当时召回排序把contract_extractor排到了第四名没有进入 Top 3最终模型选择了一个完全不相干的文件转换技能输出了一堆无意义的路径。这个案例直接推动我把召回数量从 3 改成 5并且在召回得分低于阈值时追加一轮反问澄清。4. 技能编排的高级玩法4.1 多技能组合把一个复杂任务拆成“技能食谱”单一技能只能解决单点问题Agent 真正的价值在于组合。我做过一个很典型的功能用户上传一份产品评论 ExcelAgent 自动做情绪分析、提炼问题关键词、最后生成一张改进建议清单。这个任务如果整成一个超级技能代码会膨胀得非常厉害而且可复用性极差。我的做法是拆成三个技能parse_excel解析表格、review_sentiment情绪分析、suggest_action生成建议。Agent 主循环里模型会按顺序调用三个技能每步的结果都作为下一步的输入。这种编排方式最理想的实现是让模型在系统提示词里看到一条很明确的流程线“先解析文件 → 再分析情绪 → 最后生成建议”但模型也是会犯浑的。有一天一个用户只上传了文件没说要分析什么模型居然直接调用了suggest_action导致结果不完整。我在复盘时发现原因是流程链路的提示在上下文中位置太靠后模型注意力没有抓住。后来我加了一层显式的“流程技能”不是靠模型自由发挥而是把这种稳定场景做成一个 workflow 技能内部代码按固定顺序执行只有流程中的某个环节失败时才让模型介入。这样做的代价是牺牲了一点灵活性换来的是极高的稳定性。我的结论是如果你的场景是固定需求别堵模型的临场发挥直接用流程技能锁死流程。4.2 技能间的状态共享与隔离组合技能绕不开状态问题。我先说我在模拟项目 X 里踩过的大坑A 技能处理完数据后生成了中间文件B 技能需要读这个文件但我没有在技能调用的上下文里传递文件路径。结果 B 技能拿不到路径只能自己再猜一个路径重新生成产出了一堆重复文件。解决这个问题的标准做法是把技能间的共享信息收进一个统一的“执行上下文”。每调用一个技能都会把它的关键输出做命名化存储。比如parse_excel返回一个parsed_data_path上下文里就登记context[parsed_data_path] /tmp/xxx.csv。后边review_sentiment可以明确声明它依赖parsed_data_path。这里依赖关系的表达最好写在技能元数据里模型和调度器都能看到双方就不靠猜了。不过要提醒一点技能间的数据最好通过外部存储传递尽量避免在进程内直接用 Python 对象传引用。因为 Agent 主循环和技能执行器未来很可能要拆分到不同进程甚至不同机器直接传引用会让系统失去横向扩展的能力。统一用文件路径、数据库记录 ID、消息队列主题这类可序列化标识是最稳妥的做法。4.3 权限与安全边界Agent 的技能既是能力也是一把枪。你给 Agent 一个可以执行 Shell 命令的技能就意味着模型一旦被注入攻击、或者用户刻意引导它可能真的在服务器上跑任意命令行。我在项目里对技能权限做了四级控制。第一级是白名单模式高风险技能比如执行 Shell、删除文件、发送邮件必须显式声明danger: true配置文件中还要开一个允许列表。模型可以调用但调用前必须先触发人工确认。第二级是资源限制所有技能执行都要跑在带内存和 CPU 上限的沙箱里避免模型生成一个死循环把机器打满。第三级是敏感参数掩码凡是技能参数里出现路径、URL、IP 地址这类信息统一在日志里做脱敏处理防止上下文泄漏。第四级是调用频率限制一个技能默认一分钟最多调用 20 次超了直接熔断。这些安全设计看起来增加了很多配置负担但说真的等 Agent 真的要放开给真实用户用的时候每一个环节都可能救你一命。我见过一个实际案例某团队给了 Agent 写文件权限但没有路径校验用户引导模型把结果写到了一个系统关键目录把服务器搞崩了。安全不是可选项是上线前必须过的闸口。5. 常见问题与排查技巧实录5.1 问题速查表跑了一段时间 agent-skills 体系后我把遇到的高频问题整理了一张表这里的每一个问题我都真实遇到过对应的排查思路也是验证过的。现象可能原因排查思路模型总是选错技能技能 description 太模糊或太长重写 description控制在两句话内明确“输入什么、输出什么、适合哪种任务”技能调用报参数错误参数枚举值没写全在元数据里加正则或枚举校验并给模型返回可选的合法值多个技能产出冲突结果技能职责边界重叠重新划定技能粒度明确两个技能之间的边界必要时合并技能召回命中率暴跌技能升级后没有更新向量索引检查向量库是否有更新流程给技能注册增加版本号技能执行很慢技能内部串行调用过多考虑拆成并行分支或者换成流程技能提前规划调度Agent 在技能调用后开始胡编技能的返回结果没被正确回填到上下文检查主循环中是否把技能输出追加到了 context一次任务里技能被反复调用缺少任务级去重与截止条件增加已调用技能缓存达到最大轮次后强制结束本地文件被技能误改文件路径无白名单校验给文件类技能加根目录限制只允许访问指定目录这张表我打印出来贴在了工位旁边。每个新问题我都会往里补一条三个月下来整个技能库的稳定性提升非常明显。5.2 三个让我印象最深的线上事故第一个事故是“描述污染”。有一段时间我在某个技能元数据的 description 里补充了很多内部细节比如“这个技能内部使用了 /tmp 作为临时目录如果目录不存在请先创建”。结果模型读到了这段描述后居然把它当成了一条用户隐含指令在调用技能前擅自执行了一系列文件操作。那次之后我给自己立了一个规矩技能描述里的每一句话都要问一句“这是给模型看的正确信息还是我写给自己的备注”凡是不该让模型知道的一律不写进元数据只放到技能内部注释里。第二个事故是“上下文压垮技能”。技能库初期所有技能都是完整元数据塞进提示词的技能数量一多上下文暴涨到接近模型输出上限导致模型根本没法在上下文中找到正确的调用指令。后来直接用语义检索解决召回 Top 5上下文瞬间降了一半多。这让我意识到“把描述塞给模型”和“让模型找到描述”是两个完全不同的设计思路。第三个事故最玄乎也最值得讲。某天用户反馈“查不到上季度的销售数据”我一看日志技能调用确实成功了返回结果也确实有数据。但模型最后给出的答案却是“没有找到相关数据”。排查到最后发现是技能返回的 Markdown 表格里月份格式是2024-Q1而模型内部习惯把它理解成英文缩写把Q1当成了不起眼的修饰词干出了一场“数据明明在眼前却视而不见”的乌龙。解决办法很简单在技能输出里把月份显式改成2024年第一季度模型立刻就看懂了。这个事故让我彻底明白了技能的返回结果不是给程序看的是给模型“读”的。所以输出格式要尽量贴近模型容易理解的自然语言而不是程序员自己看着舒服的紧凑结构。5.3 日志与可观测性给技能链条装上行车记录仪Agent 的技能调用链路比传统函数调用长很多链路里任何一环出了错问题都可能被下一环“带偏”。我强烈建议在技能系统里顺手把可观测性做好。具体来说每个技能的调用日志至少要包含这样几项调用的任务 ID、用户原始问题、召回技能候选列表及其得分、最终选中技能名称、传入参数、执行耗时、返回摘要、是否重试。这组信息组合起来能让你完整回放出模型每一步的思考过程。有一次排查“用户说生成图表失败但代码明明没报错”的问题我从日志里看到Agent 确实成功生成了图片但路径是一个临时目录下的文件用户根本访问不到。如果没有这种细颗粒度的链路日志这个问题就算排查一整天也未必能找到。所以我现在给新开发者的建议就是技能系统可以没有花哨的控制台但一定不能没有结构化日志而且日志要按任务 ID 归档排查问题的时候按 ID 去拉效率会高很多。6. 写在项目之外的经验做 agent-skills 这套体系给我的收获远不止“让 Agent 能干活”这一点。它逼着我重新思考了模型和代码之间的分工边界代码负责确定性和安全性模型负责灵活性和理解力技能元数据就是两者之间的翻译层。这套关系理清楚了Agent 项目的复杂度才真正可控。我最后再分享一个实用的技巧。每次给技能库新增技能之前先写一条典型的用户提问然后手动跑一遍“从问题到技能候选”的检索过程看看这个新技能是否真的能被准确召回再决定要不要调整描述。这个习惯帮我挡住了至少三个“自己觉得很有用、但模型根本调不到”的无效技能。技能库不是越搭越大就好而是要保证每一个技能都有清晰的存在价值能被恰当的问题在恰当的时候唤醒。如果你也在做 Agent 相关的项目我建议你从最小闭环开始先做一个带技能注册、参数校验、语义召回、主循环调用这四件套的系统跑通以后再慢慢加权限、加编排、加可观测性。这套东西做完你会明显感觉到Agent 的每一次行动都变得“有据可查、有理可依”这比单纯追求模型的聪明程度要重要得多。

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

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

免费获取报价 →
↑