☰
Agent技能体系设计指南:从Function Calling到agent-skills工程化落地
2026/10/14 9:23:25 网站建设 项目流程

聊到 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_type="line") doc = self.doc_sk.handle(chart, template="weekly_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(default="month", 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_n=5) 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 真正的价值在于组合。我做过一个很典型的功能:用户上传一份产品评论 Excel,Agent 自动做情绪分析、提炼问题关键词、最后生成一张改进建议清单。这个任务如果整成一个超级技能,代码会膨胀得非常厉害,而且可复用性极差。我的做法是拆成三个技能: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 的每一次行动都变得“有据可查、有理可依”,这比单纯追求模型的聪明程度要重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询