ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Agent技能体系构建指南:从行为组件到可治理的工程实践

2026/10/8 11:33:41 拓冰建站 浏览量
Agent技能体系构建指南:从行为组件到可治理的工程实践 最近总有人问我手头的 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(fskill {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(keylambda x: x[0], reverseTrue) 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, shellTrue, capture_outputTrue, textTrue, timeouttimeout ) 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, domaincontext.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_result4.3 错误处理与回退机制技能执行不会永远成功文件可能不存在、接口可能超时、数据可能不符合预期。在技能层做错误处理时要明确一件事哪些错误需要重试哪些错误需要直接给用户一个可理解的失败信息哪些错误可以回退到另一个技能。我习惯在技能返回值里带一个error_code分为RECOVERABLE、PERMANENT、NEED_HUMAN三类。RECOVERABLE指临时性错误网络超时、资源锁Agent 可以等待后重试最多重两次PERMANENT指逻辑性错误文件不存在、Schema 不匹配Agent 应当向用户报告失败而非反复尝试NEED_HUMAN指需要人工介入的异常情况权限不足、数据疑似被篡改此时不仅要报告失败还要把推理链和技能调用上下文打包保留供人工查看。这个分类想清楚后Agent 的“失败表现”就不会那么蠢了——不会在一个注定失败的任务上重复循环也不会在用户面前抛出一堆技术栈栈帧。好的错误处理不是让 Agent 永远成功而是让它在失败时也能给出有意义的行为。4.4 防幻觉的评测 trick加入“反向用例”正向用例测的是“该调用时能调用”反向用例测的是“不该调用时不能调用”。我把这两类都放进评测集。比如一个用例是“帮我把这份数据画个饼图保存下来”如果技能库里没有画图技能评测期望是“清晰告知不具备此能力”而不是硬编一个方案或者报错崩溃。反向用例通过百分比直接反映技能体系的“克制力”如果一个 Agent 对什么请求都敢接说明它的技能路由缺少了拒绝机制。这个反向用例集的价值在长尾场景会体现得特别明显。越大的技能库“像但其实不是”的边界情况就越多没有反向用例约束等到线上才发现误调用代价就大了。5. 常见问题、排查技巧与维护策略实录5.1 调试技能调用时的三板斧技能调用出问题时第一反应不要去看模型 Prompt而是先确认技能本身的输入输出是否符合预期。我调试的顺序是先手动执行一次技能用命令行的方式带着固定参数跑一遍手动通过后再让模型选择技能、填充参数最后再看编排层是不是把技能顺序搞错了。这个“自底向上”的顺序能快速切分问题出在技能实现、技能路由、还是编排逻辑。如果发现模型总是选错技能我通常做三件事第一件把候选技能列表打印出来看看模型到底看到哪些描述是不是描述有歧义第二件在评测集里手动标记一次“错误选择”的样本然后调整描述措辞或增加约束第三件限制模型可见的技能范围比如明确只传入domaindata的技能让候选集合缩小到五六个。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 任务执行飘忽不定的困扰不妨从整理一份技能清单开始把模型要做的事一件一件落到可执行的组件上你可能会发现绝大多数“模型能力不足”的问题其实都是工程基础设施不足的问题。