☰
Agent技能体系构建指南:从行为组件到可治理的工程实践
2026/10/8 11:33:17 网站建设 项目流程

最近总有人问我,手头的 Agent 项目明明接了模型、写了 Prompt,可一到真实场景就露怯——要么任务执行得磕磕绊绊,要么换个输入就不会干活了。“agent-skills”这个概念我最近反复提起,它听起来像个新名词,其实本质就是给 Agent 建一套可复用、可编排、可治理的行为组件库。这套东西不解决“模型聪不聪明”的问题,解决的是“模型干不干活、能不能稳定干活”的问题。这篇内容适合正在做 AI 应用落地、被 Agent 任务稳定性折磨过的开发者,也适合准备从零搭一套 Agent 工程体系的产品技术负责人。我会把技能拆分、注册与调用的工程实现、评测方法和避坑经验一条条摊开讲,不绕弯子。

我最早接触到 agent-skills 这个方向,是在处理一个文档处理助理的场景:用户丢进来一份混乱的 CSV,要求“帮我整理成标准格式,顺便算一下每列缺失率”。直接丢给模型,它确实能写代码,但每次生成的结果差异极大,路径是临时的、字段名是乱的、异常处理看心情。把“CSV 清洗”“缺失率统计”“列类型推断”拆成独立技能并用描述性 Schema 注册之后,同样的需求变成了稳定的三条技能调用链,准确率从 70% 上下直接拉到了 95% 以上。

1. 项目背景与整体思路:为什么 Agent 必须“会技能”而不是“会聊天”

1.1 技能体系的本质:从“模型即产品”到“模型+技能即产品”

先说一个认知层面的问题。很多人把 Agent 理解成“一个非常聪明的对话机器人”,给足上下文它就能完成任务。这个想法在 demo 阶段完全成立,一进生产环境就崩。原因很简单:大模型的输出是概率性的,同一句话换个说法、同一任务换个上下文,结果就会漂移。而真实业务需要的不是“有时候能行”的聪明,而是“这次能行、下次也能行”的确定性。

agent-skills 的出发点就是把这种确定性从“靠模型临场发挥”变成“靠工程预先定义”。每个技能是一个被明确描述的行为单元:它接收什么参数、内部做什么处理、返回什么结构、在什么条件下允许调用。模型在工作时不是在空白画布上自由发挥,而是从一个已注册的能力清单里做选择,把大任务拆成技能调用序列。这本质上把无限可能压缩成了有限选择,把概率性执行变成了接近确定性的路由与组合。

用通俗的比方来说:没有 skills 体系的 Agent 像一个只读过菜谱、但从来没进过厨房的新手厨师,你告诉他“做一道红烧肉”,他只知道大概步骤,火候、比例、替代方案全靠猜,做出来一次一个味道。而有了技能体系,相当于厨师手里有一整套标准化操作卡,每张卡写着固定的流程、参数和验收标准,他需要做的只是判断“当前这道菜该用哪几张卡、按什么顺序执行”。

1.2 这个项目处理了哪几类核心痛点

第一是行为不可复用。没有技能层的时候,每次跟模型对话都要把任务描述、工具说明、输出格式塞进上下文。同一个“读取 Excel 并做数据透视”的操作,在这个会话里写一遍,下个会话里又得重写一遍,成本高、效果差。

第二是行为不可测试。对话式执行是端到端黑盒,你无法单独验证“文件读取”这一步是否正确还是后续处理逻辑拖了后腿。技能层天然是单元化的边界,可以单测、可以打桩、可以做回归。

第三是行为不可组合。业务需求几乎都是复合型的,“处理这份报告”= 读取文件 + 提取关键信息 + 生成摘要 + 格式化输出。零散依赖模型自发组织这些步骤,结果就是步骤缺失、顺序错乱。技能层提供了显式的编排入口,你可以让模型自主编排(适合探索性任务),也可以由代码预设编排流程(适合常规任务)。

第四是行为不可治理。生产环境必须知道 Agent 做了什么、用了什么能力、调用了哪些外部资源。技能的注册表天然提供了审计清单,每一步调用对应唯一技能 ID,日志里追踪起来非常清晰。

1.3 心智模型:技能是“行为的接口”

在设计 agent-skills 时,我建议把每个技能当成一个拥有四要素的接口来想:意图描述(Intent),即这个技能是干什么的,用一句或几句话写清楚,用于模型做语义匹配;参数模式(Parameter Schema),即调用它需要哪些入参、各是什么类型、有什么约束;执行逻辑(Executor),即真正干活的代码或外部工具调用;回调协议(Response Protocol),即执行完毕后返回什么结构,供上层编排或模型评估使用。

举个例子,一个叫analyze_data_quality的技能,意图描述是“对二维表格数据执行质量分析,包括缺失率、唯一率、类型分布,并以结构化报告返回”,参数 Schema 接收data_path和columns_to_analyze,执行器内部用 pandas 完成统计,返回固定结构的 JSON。模型不需要知道“缺失率怎么算”这种底层细节,它只需要知道“有这么一个技能可以完成这件事,参数是什么”。这个抽象层级一旦建立,整个 Agent 的复杂度就大大降低了。

适合谁来参考这套方案?正在用 LangChain、AutoGen、自建 Agent 框架做应用开发的工程师、想把 RAG 升级成真正“能做事的业务助手”的产品团队、以及做内部自动化工具的运维和效率团队,都可以从这套体系里找到直接落地的思路。

2. 技能层设计与原子技能拆分:粒度、命名与分层

2.1 定义技能时需要想清楚的几件事

技能不是“把代码包成函数”那么简单。我见过不少团队把技能做成了“一堆函数互相调用”,结果模型根本不知道该在什么时候选哪个,效果甚至还不如不拆。核心问题在于:他们定义技能时只写了函数签名,没有写“模型视角的触发条件”。

一个合格的技能定义至少包含四部分:意图描述(给模型看的话术)、参数 Schema(给模型看的入参规范)、执行逻辑(给代码跑的流程)、质量守卫(给结果设的最低标准)。其中最容易忽略的就是质量守卫。比如summarize_document这个技能,模型可能在文档为空时也照常调用,返回一个空摘要。你需要在执行逻辑里加入“输入为空则直接抛错、不调用模型”的守卫逻辑,而不是把这种概率性行为留给上层编排去猜。

参数 Schema 的定义也要注意一个细节:尽量给出默认值和可选范围,减少模型的推测空间。例如:

{ "name": "analyze_data_quality", "description": "对 CSV 或 Excel 表格数据执行质量分析,返回缺失率、唯一率、类型分布等指标", "parameters": { "type": "object", "properties": { "data_path": {"type": "string", "description": "数据文件的路径"}, "columns": {"type": "array", "items": {"type": "string"}, "description": "待分析列名列表"}, "output_level": {"type": "string", "enum": ["basic", "full"], "default": "basic", "description": "分析详细程度"} }, "required": ["data_path"] } }

这里把output_level设成枚举加默认值,模型就少了一个自由发挥的空间,调用出现“乱传参”的概率明显下降。

2.2 技能拆分到什么粒度才合适

粒度是技能体系里争论最多的一个问题。拆得太细,技能数量爆炸,模型在几十个相似技能里做选择反而容易出错;拆得太粗,一个技能内部塞入了太多逻辑,可复用性又变差。

我个人的经验是遵守“原子粒度 + 流程粒度”两层原则。原子技能指无法再往下拆的单一动作,比如“读取文件”“写文件”“调用 HTTP 接口”“发邮件”,这类技能通常一个函数就能完成,输入输出高度可预测。流程技能指由多个原子技能组合而成、具备业务含义的复合动作,比如“生成周报”= 读取数据 + 分析趋势 + 生成 Markdown 文本 + 发送邮件。

这里有一条关键的取舍逻辑:模型适合做的选择是“做什么”和“按什么顺序做”;不适合做的是“每个环节内部怎么做”。所以原子技能要拆到“内部逻辑对模型有意义的程度”,流程技能则要拆到“业务对用户有意义的程度”。不要在原子层就引入业务含义,比如“读取销售数据文件”这种描述写进技能摘要就不合适——原子技能应该表述为“读取指定路径下的数据文件,支持 CSV/Excel 格式”,业务信息放在编排层去判断用哪个文件。

2.3 分层设计:给技能建立目录结构

当技能数量超过二十个时,平面的技能清单对模型已经不够友好。我建议做四层结构:全局技能(文件读写、网络请求、加解密等通用能力)、领域技能(文档处理、数据分析、代码执行等面向业务域的能力)、实体技能(专为某个数据实体定制的能力,比如“用户画像解析”“订单状态判断”)、通用工具(可调用外部系统功能的适配器,比如 Slack 发送、数据库查询)。

skills/ ├── global/ │ ├── read_file │ ├── write_file │ └── http_request ├── domain/ │ ├── document/parse_pdf │ ├── document/generate_markdown │ ├── data/analyze_data_quality │ └── code/run_python_code ├── entity/ │ ├── user_profiling │ └── order_status_judge └── tool/ ├── slack_send_message └── sql_query

这样一个目录结构既方便人维护,也方便程序在给模型拼装上下文时按域做过滤。比如一个“订单相关”的任务,初始化时就不必把parse_pdf的描述塞给模型,上下文短了、选择面小了,准确率自然就上来了。

2.4 命名风格与描述写法:让模型一眼懂

技能命名建议用动词_对象结构,比如read_file、send_email、parse_pdf;描述则用“该技能用于……,接收……输出……”的句式。描述里别带否定句,模型对“这个技能不要用来做XX”的关注度远低于对正面用途的关注。如果确实有误用风险,在参数 Schema 里加约束字段即可。

描述的长度也有讲究。太短的描述不能区分相似技能,太长的描述占用上下文还干扰决策。我测试下来,一句话功能概括 + 一句话适用场景 + 一句话输入输出说明是最稳定格式。比如:

该技能用于从 PDF 文件中提取文本内容。适用于需要读取 PDF 数据、做全文检索或配合其他技能做文档解析的场景。输入为 PDF 文件路径,输出为纯文本字符串。

这种写法既明确了能力边界,也给了模型足够的匹配依据。

3. 技能注册与调用的工程实现:从注册表到执行器

3.1 设计一个可检索的技能注册表

技能注册表是 agent-skills 体系的心脏。它的功能不只是“存一个技能列表”,而要支持注册、注销、探查、路由四件事。我建议把注册表实现为一个轻量的内存索引,启动时从配置目录加载全部技能,运行时提供 HTTP 或进程内接口供 Agent 查询。

每个技能在注册表里的记录,除了上节提到的定义之外,还应包含:版本号(技能的迭代需要向后兼容)、负责人/权限组(谁允许调用)、健康状态(在线、熔断、已下线)、指标锚点(历史平均调用耗时、失败率)。

# skill_registry.py class SkillRegistry: def __init__(self): self._skills = {} def register(self, skill: SkillDefinition): if skill.name in self._skills: raise KeyError(f"skill {skill.name} already exists") self._skills[skill.name] = { "definition": skill, "version": skill.version, "status": "online", "metrics": {"calls": 0, "failures": 0, "avg_latency_ms": 0} } return skill.name def unregister(self, name: str): self._skills.pop(name, None) def lookup_by_name(self, name: str) -> SkillDefinition | None: return self._skills.get(name) def search(self, query: str, domain: str | None = None) -> list[SkillDefinition]: # 简单路由:先按域过滤,再做描述文本的匹配评分 candidates = [s for s in self._skills.values() if s.status == "online"] if domain: candidates = [s for s in candidates if s.domain == domain] scored = [(self._score(query, s.definition), s.definition) for s in candidates] scored.sort(key=lambda x: x[0], reverse=True) return [s for score, s in scored if score > 0.5]

这个实现很朴素,但足够支撑生产初期的需求。如果技能数量超过一百个,再把search方法切换成向量检索也不迟——前期不要为不需要的复杂度买单。

3.2 执行器的两种实现模式

注册好技能之后,真正执行时有两种选择:Process-Scoped Executor(在 Agent 所在进程内直接执行,适合读写文件、调用本地库)和Subprocess-Scoped Executor(在独立进程/容器中执行,适合运行模型生成的代码、调用不可信的外部命令)。

如果技能涉及执行模型写的代码,强烈建议用第二种模式。模型生成的 Python 代码即使逻辑正确,也可能因为环境冲突、路径不存在、库版本不匹配而崩溃,放进子进程跑还能做超时控制,主进程不会被拖死。这里加一个 Timer 是必要动作:

import subprocess, time def run_in_subprocess(cmd: str, timeout: int = 30): start = time.time() result = subprocess.run( cmd, shell=True, capture_output=True, text=True, timeout=timeout ) latency = int((time.time() - start) * 1000) return { "stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode, "latency_ms": latency }

3.3 自由格式技能的描述:给非结构化任务一条退路

不是所有技能都能用 JSON Schema 描述清楚。比如“判断一段代码的圈复杂度”“给文章起十个别名”,这些任务输入高度自由,强行结构化只会让参数校验更僵硬。这种技能我建议用Backus-Naur Form (BNF)描述输入输出格式,模型解析起来比 JSON Schema 更自然。

input_grammar := "text:" TEXT output_grammar := "aliases:" ALIAS ("," ALIAS)* TEXT := any natural language text ALIAS := a short phrase without punctuation

模型先按语法理解“哦,输入就是一段文本,输出是一个逗号分隔的别名列表”,再按你的系统提示做字符串解析,效果比硬套 JSON 好得多。这种折中方案虽然牺牲了一点结构化程度,换来的是更多任务能被“技能化”覆盖。

3.4 调用技能的完整流程

整个技能调用链路我用一个统一的入口函数管理,模型不需要直接接触注册表内部细节:

def execute_skill(model, user_input, registry, context): # 1. 基于用户输入与上下文检索候选技能 candidates = registry.search(user_input, domain=context.get("domain")) # 2. 让模型从候选列表中选择技能并填充参数 selected = model.select_skill(candidates, user_input, context) if not selected: return {"error": "no suitable skill found"} # 3. 参数校验,不符合 Schema 则直接拒绝,不让执行器兜底 validated_params = validate_parameters(selected.schema, selected.params) # 4. 执行技能并记录指标 with track_metrics(registry, selected.name): result = selected.executor.run(**validated_params) return result

这里有个容易被忽略的经验:第 3 步参数校验一定要严格,不要“尽量补全”。模型给的入参缺了必填字段或者类型不匹配,宁可返回错误让 Agent 重试,也不要自行填默认值。自行补全的后果是技能执行时出现诡异行为,而且极难排查——你不知道那个默认值是从哪来的。我踩过最大的坑是output_level缺省时我偷偷填了full,结果所有摘要任务全输出了完整版,用户投诉了好几次才定位到是在这里被“好心”填错了。

3.5 版本管理、路由与权限清理

技能定义也是会迭代的。read_file最初只支持 UTF-8 编码,后来加了编码自动检测。这个升级是向后兼容的,直接覆盖注册表记录即可。但如果技能的输入输出发生了破坏性变更,比如返回值从{"content": str}改成{"content": list[dict]},就不能直接覆盖了。

生产环境我建议按语义化版本维护,注册表里保留一个default_version和一个compatible_versions列表。路由时优先选 default,若调用方显式指定了版本号且不在兼容列表里则拒绝。这样既保证老调用不受影响,又允许新技能逐步灰度。同时在每次技能请求时做一层权限校验,通过技能名称匹配当前用户的权限列表,没权限直接拦截,避免 Agent 被诱导调用高权限技能。

可观测性也不能省。每个技能调用我都在日志里记录:用户请求原文、所选技能 ID、入参摘要、出参摘要、耗时、失败原因。这些数据既用于问题排查,也用于后续做技能路由的评测基线。

4. 评测、防幻觉与错误处理:把技能做成可信组件

4.1 建设“用户请求到技能”的评测集

技能体系上线之后,面临的第一个问题就是:怎么知道它有没有选对、用对?仅靠模型自己的“判断”是远远不够的。我建议在项目早期就建一个skill_benchmark目录,里面放三种评测数据:语义匹配测试(请求应命中哪个技能)、参数抽取测试(请求中的信息应填到哪些字段)、端到端任务测试(请求应该触发哪几个技能的编排序列)。

举一个语义匹配测试的样例:

{ "input": "帮我把这份销售报表里每个地区的缺失值统计一下", "expected_skills": ["analyze_data_quality"], "expected_params": { "analyze_data_quality": {"data_path": "sales_report.csv", "columns": ["region", "amount", "date"]} } }

评测跑起来后,把“命中率”作为发版门禁。我定的标准是:核心技能语义匹配命中率低于 90% 不允许上生产,参数抽取准确率低于 85% 要回去优化技能描述和 Schema。这个机制逼着你在“改善描述”和“调小粒度”之间持续打磨,而不是任由模型自由落体。

4.2 从源头抑制幻觉:约束策略与守卫条件

Agent 的幻觉在技能体系里主要表现为三种:选错技能、填错参数、虚构执行结果。选错技能多发生在相似描述之间,比如有两个技能一个叫generate_daily_report一个叫generate_weekly_report,描述里都提到“统计”“报表”,模型难以区分。解决办法是在描述里明确区分触发场景:“日常日报仅覆盖当前工作日的单一数据”“周报涵盖过去七天的汇总趋势”。参数填错则普遍因为 Schema 里的字段名太抽象,比如period不如start_date和end_date直接;字段说明里给一个示例值也非常管用。

虚构执行结果比较隐蔽:技能执行失败后,模型可能会“假装”一个合理结果返回给用户。这个问题必须靠执行器自身守卫来解决——执行器返回失败状态码时,Agent 的响应不可能伪造成功。我会在统一入口处加一个“结果校验器”:

def validate_result(skill_name, raw_result): expected_schema = registry.lookup_by_name(skill_name).response_schema if raw_result.get("status") == "failed": raise SkillExecutionError(raw_result.get("error")) if expected_schema and not matches_schema(raw_result.get("data"), expected_schema): raise SkillExecutionError("response schema mismatch") return raw_result

4.3 错误处理与回退机制

技能执行不会永远成功,文件可能不存在、接口可能超时、数据可能不符合预期。在技能层做错误处理时要明确一件事:哪些错误需要重试,哪些错误需要直接给用户一个可理解的失败信息,哪些错误可以回退到另一个技能。

我习惯在技能返回值里带一个error_code,分为RECOVERABLE、PERMANENT、NEED_HUMAN三类。RECOVERABLE指临时性错误(网络超时、资源锁),Agent 可以等待后重试,最多重两次;PERMANENT指逻辑性错误(文件不存在、Schema 不匹配),Agent 应当向用户报告失败而非反复尝试;NEED_HUMAN指需要人工介入的异常情况(权限不足、数据疑似被篡改),此时不仅要报告失败,还要把推理链和技能调用上下文打包保留供人工查看。

这个分类想清楚后,Agent 的“失败表现”就不会那么蠢了——不会在一个注定失败的任务上重复循环,也不会在用户面前抛出一堆技术栈栈帧。好的错误处理不是让 Agent 永远成功,而是让它在失败时也能给出有意义的行为。

4.4 防幻觉的评测 trick:加入“反向用例”

正向用例测的是“该调用时能调用”,反向用例测的是“不该调用时不能调用”。我把这两类都放进评测集。比如一个用例是“帮我把这份数据画个饼图保存下来”,如果技能库里没有画图技能,评测期望是“清晰告知不具备此能力”,而不是硬编一个方案或者报错崩溃。反向用例通过百分比直接反映技能体系的“克制力”,如果一个 Agent 对什么请求都敢接,说明它的技能路由缺少了拒绝机制。

这个反向用例集的价值在长尾场景会体现得特别明显。越大的技能库,“像但其实不是”的边界情况就越多,没有反向用例约束,等到线上才发现误调用,代价就大了。

5. 常见问题、排查技巧与维护策略实录

5.1 调试技能调用时的三板斧

技能调用出问题时,第一反应不要去看模型 Prompt,而是先确认技能本身的输入输出是否符合预期。我调试的顺序是:先手动执行一次技能,用命令行的方式带着固定参数跑一遍;手动通过后,再让模型选择技能、填充参数;最后再看编排层是不是把技能顺序搞错了。这个“自底向上”的顺序能快速切分问题出在技能实现、技能路由、还是编排逻辑。

如果发现模型总是选错技能,我通常做三件事:第一件,把候选技能列表打印出来,看看模型到底看到哪些描述,是不是描述有歧义;第二件,在评测集里手动标记一次“错误选择”的样本,然后调整描述措辞或增加约束;第三件,限制模型可见的技能范围,比如明确只传入domain="data"的技能,让候选集合缩小到五六个。

5.2 路由命中率总上不去,怎么办

这是个高频问题。路由命中率不稳,九成原因是技能之间边界不清晰,而不是模型笨。出现两个技能相互覆盖时,我先检查描述里是否有重叠关键词,比如generate_summary和extract_key_points都可能被用于“帮我总结一下这篇文章”——这时候要么删掉其中一个,要么合并成一个技能,让模型没有纠结空间。其次检查候选集大小,一次给模型二十个技能选项,准确率必然下降,可以尝试在上下文里只给每个技能三行描述,把详细的参数 Schema 放到第二步模型确认技能后再动态补充。

最后还有一招:给技能打标签,比如#data、#document、#communication,用户在输入时如果带上了域名词,就用这个标签过滤候选项。这个看似简单的 trick 能显著缩小选择范围,效果比优化 Prompt 快得多。

5.3 多次调用链太长,效果下滑严重

复合任务动辄调用五六个技能,Token 消耗和错误概率都会飙升。我常用的缓解方案是给技能链做“结果摘要折叠”,每个技能只把“执行摘要”和“关键返回值”传给下一步,完整的原始输出放到独立存储里。这个做法的副作用是可能会丢失细节,但好处很直观:上下文变短、模型感知更清晰、整体成功率明显上升。

如果编排特别长,我还会把“流程技能”(上一节提到的复合技能)拿出来做硬编码编排——由代码固定调用序列,模型只在分支判断处参与决策。结果是牺牲了一点灵活性,换来了高度的稳定性。适合那种“用户每次基本都是同一套路”的业务场景。

5.4 技能冲突与退化的治理

技能会随着业务演进而“长坏”。最典型的是:新技能为了兼容旧行为,把描述写得越来越大、参数越加越多,最终模型根本不知道新技能的边界在哪。我建议每次新增技能前,先回答三个问题:新技能与已有技能的重叠程度有多高?能否通过只修改旧技能描述来满足新需求?三个以上技能做同一件事,是不是应该合并成一个技能并加一个mode参数区分?

老技能退役也要果断。我会用调用指标做决策:如果某个技能连续三十天调用成功率为零或命中率极低,就标记为deprecated,一个月后如果仍然零调用,直接从注册表移除。这个动作能防止技能库“腐烂”,保持每个新增技能都是经受过实际考验的。技能多了之后,注册表里禁用比删除更安全,因为日志和评测集还留有关联数据。

5.5 维护策略:小团队也能持续运营

维护技能库不一定要很大的工程团队,做好三件事就能基本保证健康度。第一件是把技能文档作为“代码的一部分”管理,每次改动技能定义都走 Code Review,描述语句的任何修改都会影响路由效果,不能随便改。第二件是保留真实的调用日志作为持续评测的种子数据,每周拿最近一周的请求跑一遍自动评测,观察路由命中率和参数抽取准确率的趋势。第三件是把“贡献新技能”模板化,团队成员只要填好“技能描述、参数 Schema、执行逻辑、测试样例”四块内容,就能提交一个可用的技能包。这个模板化流程是我实践下来维护成本最低的方案。

写在最后的经验

用 agent-skills 这套思路重构过 Agent 项目之后,我最大的体会是:大模型应用的工程质量,不是靠“选个更强的模型”就能兜底的。真正决定天花板的,是你有没有把模型的行为边界变成可定义、可测试、可治理的工程组件。技能的拆分粒度、描述写法、注册机制、评测兜底,任何一个环节偷懒,线上都会用“看起来很像回事、实际不能用”的结果加倍回报你。最初我也是从只想“让模型把任务完成”的思维走过来的,直到我把重点从“调模型”切换到“建技能库”,项目的稳定性才真正有了质的改变。如果你也有 Agent 任务执行飘忽不定的困扰,不妨从整理一份技能清单开始,把模型要做的事一件一件落到可执行的组件上,你可能会发现,绝大多数“模型能力不足”的问题,其实都是工程基础设施不足的问题。

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

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

立即咨询