☰
AI Agent技能层设计指南:从提示词工程到可维护的技能系统
2026/10/28 15:39:47 网站建设 项目流程

做了大半年AI Agent,我发现自己掉进过一个特别隐蔽的坑:表面上看是模型能力不够,实际上是技能太散。同样是“给长文本打标签”这件事,内容组写了一套提示词,运营组又写了一套,产品Demo里还藏着一份更老的版本。每个Agent都像一座孤岛,各自为战,规则互相冲突,结果没人说得清线上到底跑的是哪套逻辑。后来我把这些东西收敛成一个叫 agent-skills 的公共技能层,才算是把Agent从“一段很长的提示词”变成了“一组可以注册、调用、迭代的技能单元”。

这篇文章就把我对 agent-skills 的设计思路、落地步骤和踩坑记录完整分享一下。主要讲三层东西:为什么Agent需要技能层、技能层的核心结构怎么设计、以及如何用最小成本把手上的Agent改造成技能驱动的方式。适合正在搭Agent应用、被提示词维护逼疯的工程师和产品同学,如果你只是刚开始接触Agent,也能从里面抄到一份可以直接用的技能定义模板。

1. 为什么AI Agent需要一层“技能系统”

1.1 你可能也遇到过这种尴尬

先聊一个很常见的场景。假设你要做一个电商评论分析Agent,需求是把用户评论拆成情感倾向、核心主题、是否包含质量反馈几个维度。最朴素的做法是写一段超长System Prompt,把每个维度的分析要求、输出格式、例子全部塞进去,然后祈祷大模型每次都严格按照要求输出。

刚开始确实能跑通,但两周后问题就来了。产品经理说要加一个“是否包含价格敏感词”的判断,营销部门说要区分“推荐意愿强弱”,法务说评论里如果有“医疗功效”表述必须特殊标记。每一次需求变更都是在同一段Prompt上做加法,几千字下来,模型开始出现选择困难:今天它把情感倾向和推荐意愿混在一起,明天又把“医疗功效”当成普通关键词。更难受的是,你根本不知道模型在哪一步开始跑偏,因为整条链路是一个黑盒,问题定位只能靠猜。

我自己把这种状态叫“提示词肥胖症”。它有两个直接的副作用:第一,每次修改都可能引入隐性回归,今天改完一个规则,明天另一个场景的输出格式就崩了;第二,Agent完全不具备结构性,无法针对某个环节单独测试、单独回滚、单独优化。

1.2 技能层到底在解决什么问题

agent-skills 的核心思路其实很简单:不要把Agent的所有能力塞进一段提示词里,而是把Agent完成某类子任务的能力,拆成一个个相对独立、可复用、可度量、可独立升级的单元,这个单元就叫“技能”。

一个技能通常包含四样东西:它负责什么任务、它需要什么输入、它怎么执行、它保证输出什么格式。技能之间通过一个注册表集中管理,Agent在每次执行任务时,会根据当前用户的诉求动态匹配技能,组合调用,而不是靠模型在上下文里漫无目的地“猜”。

举个例子,还是电商评论分析。我不再写一段“请分析情感、主题、质量反馈等”的万能提示词,而是拆成五个技能:

  • sentiment_analyzer:负责判断评论情感倾向,输出positive/neutral/negative;
  • topic_extractor:抽取评论讨论的核心主题,输出预定义标签;
  • quality_flag:判断是否包含产品质量反馈,输出是/否及证据句;
  • price_mention:识别价格敏感表述,输出相关片段;
  • urgency_alert:在情感强烈或涉及维权诉求时触发告警。

每个技能都有自己的输入输出Schema和执行的模型/工具配置,可以单独测试,也可以自由组合。需要新增一个维度时,写一个新技能注册进去就行,完全不用动其他技能的逻辑。

这背后的关键认知转变是:Agent 不应该是一个巨大的概率函数,而应该是一个可分解的系统。你不需要让模型同时理解所有规则,只需要让它每次只负责一件事,把这件事做扎实。技能层存在的意义,就是把“系统复杂度”从模型上下文里挪出来,放到代码和数据结构里。

2. 核心设计拆解:技能的四层结构与运行机制

2.1 如何优雅地把技能定义写成JSON

我在设计 agent-skills 时,参考了插件系统、工具协议和函数调用的思路,最终确定用一份 JSON 描述一个技能的全部信息。每份技能定义包含元信息、输入规约、输出规约、执行语义以及策略约束五块内容。下面是一个精简但可直接套用的模板:

{ "name": "sentiment_analyzer", "version": "1.2.0", "description": "分析评论的情感倾向,返回二分类或三分类结果", "category": "text_analysis", "input": { "type": "object", "properties": { "text": { "type": "string", "maxLength": 2000 }, "language": { "type": "string", "enum": ["zh", "en"] } }, "required": ["text"] }, "output": { "type": "object", "properties": { "sentiment": { "type": "string", "enum": ["positive", "neutral", "negative"] }, "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, "evidence": { "type": "string" } }, "required": ["sentiment", "confidence"] }, "execution": { "type": "llm", "model": "gpt-4o-mini", "temperature": 0.1, "max_retries": 2, "prompt_template": "你是一个情感分析引擎。只允许输出JSON,不要输出任何解释。\\n文本:{text}\\n语言:{language}" }, "constraints": { "allowed_tools": [], "max_input_tokens": 2048, "timeout_seconds": 15 } }

这段JSON里值得注意的有几个地方。

input和output我都用了 JSON Schema 风格来描述,它的价值在于让技能变成一个可校验的接口,而不是一段口头约定。Agent在调用技能之前可以先用Schema校验输入,在拿到输出之后可以再校验一次,不合法就直接报错或重试,这能大幅提升稳定性。

execution.type是技能的执行方式,不只有“调用LLM”这一种。我可以把type设为function,指向一段本地Python函数;也可以设为api_call,指向某个外部服务的Http接口;甚至可以是human_review,表示这个技能需要人工介入。这种设计让技能层不绑定于某一种实现,而是变成了一套统一抽象。

constraints这块我强烈建议不要省略。它定义了技能的权限边界和资源上限,比如允许调用哪些外部工具、最大输入长度、超时时间。安全性不是后期加的功能,而是在技能定义阶段就要写清楚的东西。

2.2 注册表与路由:为什么不能靠模型自由发挥

有了技能定义之后,下一步是注册表。注册表的作用是维护一份当前环境里所有可用技能的清单,并提供能力检索。我把这套逻辑类比成餐厅点餐:用户不会直接跟厨师说“我要吃那个”,而是跟服务员描述需求,服务员翻菜单,帮你匹配应该找哪个厨师。

在 Agent 场景里也是这样。用户不会说“我要调用sentiment_analyzer”,用户只会说“帮我看下这几条评论口碑怎么样”。所以我需要一个路由模块,把用户的自然语言表达映射到一个或多个技能上。

路由策略我按优先级排了三级:

  • 第一级是关键词与规则命中。比如用户输入里出现“批评、太差、失望”等词,优先偏向情感分析技能;
  • 第二级是语义相似度匹配。我把每个技能的描述向量化,跟用户query做相似度计算,取Top-K;
  • 第三级是上下文与历史兜底。如果前面两级的置信度都不高,就根据当前会话的意图类型来选默认技能。

注册表的核心代码逻辑其实不复杂,核心是维护一个字典,外加一个评分函数:

type SkillRegistry = Map<string, SkillDefinition>; function matchSkills(query: string, threshold: number): SkillDefinition[] { const scored: Array<{ skill: SkillDefinition; score: number }> = []; for (const skill of registry.values()) { score = ruleScore(query, skill) + semanticScore(query, skill); if (score >= threshold) scored.push({ skill, score }); } return scored.sort((a, b) => b.score - a.score); }

这里有两个常见误区想提醒一下。

误区一:路由逻辑交给大模型自己判断。听起来很智能,但实际等于放弃控制权,模型可能这次选了技能A,下次选了技能B,系统行为不稳定。我的建议是,路由尽量用确定性的代码逻辑来做,只有在多技能高度模糊的场景里才引入模型判断。

误区二:总是同时调用所有匹配到的技能。技能不是越多越好,每次额外调用都意味着更多延迟和成本。我一般会设置一个max_skills_per_task,默认是1到2个,超过上限时让路由模块做一次优先级仲裁,避免“高射炮打蚊子”。

2.3 技能组合与依赖:让技能像乐高一样可拼装

单个技能解决了“一件事怎么做”的问题,但真实的Agent任务往往是复合的。用户说“分析这条评论,然后帮我写一条回复”,至少涉及两个技能的组合:先分析情感,再基于情感结果写回复,第二个技能依赖第一个技能的输出。

我需要给技能层加入组合能力。agent-skills 里我用三种方式处理组合。

  • 顺序组合:技能A的输出,经过格式转换后作为技能B的输入。类似于管道,每个技能只关心上游给它的结构化数据。
  • 并行组合:多个技能同时处理同一份输入,最后汇总。比如同时做情感分析、主题抽取、质量标记,三个结果合并进同一个输出结构里。
  • 条件分支:根据某个技能的结果决定是否调用后续技能。比如情感分析为“negative”时,才触发维权风险告警技能。

负责组合的模块是编排器(Orchestrator)。它读的是一份非常简单的编排配置,用文本描述技能间的调用关系:

workflow: comment_analysis_pipeline steps: - skill: sentiment_analyzer output_key: sentiment_result - skill: topic_extractor output_key: topic_result - skill: quality_flag output_key: quality_result - if: condition: $sentiment_result.sentiment == "negative" then: - skill: urgency_alert input: $sentiment_result

把编排配置独立出来,而不是写死在业务代码里,最大的好处是让非工程角色也能参与Agent行为的调整。运营同学想调整“什么情况下触发告警”,只需要改配置,不用动代码。这在一个小团队里能释放出巨大的协作效率。

3. 从零落地:agent-skills 最小实现

3.1 定义技能接口与实现类

理论讲再多,不如直接看一套能跑的代码。我用 TypeScript 写了一套最小实现,去掉了框架依赖,核心就三门功课:定义技能、管理注册表、编排调用。

先定义技能接口。为了让不同类型的执行体(LLM调用、本地函数、外部API)都能被统一处理,我把技能抽象成一个接口,只约束输入输出,不约束内部实现:

export interface Skill { id: string; version: string; inputSchema: JSONSchemaType; outputSchema: JSONSchemaType; constraints?: SkillConstraints; execute(params: Record<string, unknown>): Promise<SkillOutput>; validateInput(input: Record<string, unknown>): boolean; validateOutput(output: unknown): boolean; }

execute是技能的执行入口,validateInput和validateOutput负责边界校验。我见过很多Agent项目把校验这一步省略掉,直接把模型输出塞给下一步,结果下游一拿到脏数据就崩溃。校验不是可选项,而是技能执行的必经流程。

然后写一个基于LLM的技能实现类,把“调用大模型”这件事收敛到同一个地方,避免每个技能各自调用SDK、各自处理错误:

export class LLMSkill implements Skill { constructor( public id: string, public version: string, private inputSchema: JSONSchemaType, private outputSchema: JSONSchemaType, private execution: LLMExecutionConfig, public constraints?: SkillConstraints ) {} async execute(params: Record<string, unknown>): Promise<SkillOutput> { if (!this.validateInput(params)) { throw new SkillValidationError(`${this.id}: 输入参数不符合Schema定义`); } const prompt = applyTemplate(this.execution.prompt_template, params); const maxRetries = this.execution.max_retries ?? 2; let lastError: Error | null = null; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const rawOutput = await callLLM(this.execution.model, prompt, { temperature: this.execution.temperature }); const parsed = JSON.parse(rawOutput); if (!this.validateOutput(parsed)) { throw new SkillOutputError(`${this.id}: 模型输出不满足Schema要求,第${attempt + 1}次尝试`); } return { skillId: this.id, data: parsed, rawOutput, latencyMs: 0 }; } catch (err) { lastError = err; // 重试前追加修复指令,让模型知道上次哪里错了 prompt += retryFixInstruction(String(err)); } } throw lastError; } }

这段代码是整套系统的地基。你注意到我每轮重试都会把上一轮的报错信息拼回Prompt,这个技巧在实际运行中非常管用,经常能把模型从“固执己见”的边缘拉回正轨。我自己在实验里统计过,第二次重试的成功率能达到九成以上。

3.2 注册表怎么避免技能冲突

有了技能实现类,接下来要解决的是注册问题。我把注册表设计成一个单例,启动时加载所有技能定义,运行时只允许查询,不允许动态修改,这样可以避免线上环境出现“技能竞态”问题。

注册表里有一个容易被忽略的细节:同名技能的不同版本处理。我的策略是“新版本优先,旧版本兜底”。假设sentiment_analyzer同时存在1.1.0和1.2.0两个版本,路由时我会优先选高版本,但如果高版本在运行时连续失败两次,就自动降级到低版本。

这个降级机制看起来简单,实际救过我很多次。新版本技能上线时,不一定在所有场景下都比旧版本强,让它灰度跑一段时间,有问题自动回退,团队就没有心理负担,敢于频繁迭代技能。

注册与路由的最小实现如下:

class SkillRegistry { private skills: Map<string, Skill[]> = new Map(); private skillsByCategory: Map<string, Skill[]> = new Map(); register(skill: Skill): void { const skillList = this.skills.get(skill.id) || []; skillList.push(skill); skillList.sort((a, b) => compareVersion(b.version, a.version)); this.skills.set(skill.id, skillList); } getBestSkill(id: string): Skill | undefined { return this.skills.get(id)?.[0]; } getCompatibleSkills(query: string, category?: string): Skill[] { // 先按分类过滤,再做评分排序 const candidates = category ? this.skillsByCategory.get(category) || [] : [...this.skills.values()].flat(); return candidates .map(s => ({ skill: s, score: scoreSkill(query, s) })) .filter(x => x.score > 0.5) .sort((a, b) => b.score - a.score) .map(x => x.skill); } }

要将一次技能调用快速复现,上面这段代码已经够用。真正要花心思的是scoreSkill的权重设计。我不会把语义相似度作为唯一标准,而是按“意图标签加权 + 描述相似度 + 关键词覆盖”三项综合评分。意图标签是从历史日志里学出来的,比纯文本相似度更贴合业务;描述相似度用的是本地Embedding模型,速度快,不用依赖外部服务;关键词覆盖是最朴素的兜底,保证极端情况下至少能命中技能。

3.3 三步把技能层接入Agent主循环

技能体系搭起来之后,接入现有Agent只需要改主循环的调用方式。我以最常见的“接收消息→回复消息”循环为例,改造前是直接把整个对话历史丢给模型,改造后是拆成四步:

async function agentRun(userMessage: string): Promise<string> { // 第一步:识别意图,确定需要哪些技能 const intent = await classifyIntent(userMessage); const matchedSkills = registry.getCompatibleSkills(userMessage, intent.category); // 第二步:执行技能,收集结构化结果 const skillResults: SkillOutput[] = []; for (const skill of matchedSkills.slice(0, 2)) { const result = await skill.execute({ text: userMessage, language: "zh" }); skillResults.push(result); } // 第三步:把技能结果和对话历史拼进最终Prompt const finalPrompt = buildFinalPrompt(userMessage, history, skillResults); // 第四步:交给模型生成最终回答,并做一次输出校验 const answer = await callLLM("gpt-4o", finalPrompt, { temperature: 0.4 }); return answer; }

这四步看起来平淡,但它把原来“一个黑盒Prompt干所有事”变成了“每个环节可观测”。我可以在技能执行那一层加日志、加耗时统计、加成功率监控;我可以在构建最终Prompt时精细控制模型看到哪些信息;我还可以在输出校验那一层拦截异常结果。

有朋友问,为什么不让模型自己决定调用哪个技能,而是主循环里先帮它选好?我的回答是:模型负责“说什么”,代码负责“连接什么”。技能匹配是确定性系统问题,让位于概率模型的LLM来决策,等于把最需要稳定性的环节交给不确定性。Agent的下限由代码保证,上限才由模型发挥。

3.4 用一条真实数据流检验整个链路

看代码只看单个函数很容易误判,我自己评估一个技能系统是否合格,习惯跑一遍完整数据流。下面用电商评论分析的场景完整走一遍。

用户输入:“这耳机降噪效果挺惊艳的,但戴久了耳朵有点胀,续航倒还不错。”

第一步,意图分类模块把这句话归类为product_comment_analysis,同时给出一个辅助标签content_review。

第二步,注册表匹配技能。关键词“降噪”“续航”命中了topic_extractor的规则,语义相似度命中sentiment_analyzer。路由模块按综合评分选了这两个技能,分别执行。

第三步,sentiment_analyzer返回{ sentiment: "positive", confidence: 0.62, evidence: "惊艳、不错" }。它识别出整体偏正向,但置信度不高,原因是用户同时提到了“耳朵有点胀”这个负面信息。topic_extractor返回{"topics": ["降噪效果", "佩戴舒适度", "续航"], "labels": ["音质", "舒适度", "续航"]}。

第四步,编排器把两个技能的结构化结果交给最终输出模块。大模型根据这些结构化信息生成了完整回复:“这款耳机在降噪和续航上得到了不少用户认可,不过佩戴舒适度存在一定争议,有用户反馈长时间佩戴会有胀感。”

整条链路里,模型只做了两件事:第一步判断意图,最后一步组织语言。中间的抽取、归类、情感判断,全部由技能完成。这就意味着,如果“佩戴舒适度”这个标签识别不准确,我只需要修topic_extractor这一个技能,其他环节完全不用动。

4. 落地过程中踩过的坑与排查实录

4.1 模型输出不遵循Schema,直接写废了下游

第一次跑通技能层之后,我天真的以为有了输出Schema就万事大吉。结果上线第二天,quality_flag技能开始间歇性输出乱码,有时多出一个"summary"字段,有时"evidence"是数组不是字符串。下游消息队列一拿到这种脏数据就反序列化失败,整个评论分析链路直接瘫痪。

排查之后定位到两个原因:第一,我的提示词虽然写了“只允许输出JSON”,但没说“不要输出JSON以外的任何字符”,模型偶尔会在JSON外面包一层解释性文本;第二,小模型在长输入场景下偶尔会自行扩展字段,绕过了我的Schema约束。

解决办法是在技能执行的双重校验上做了强化。第一重是JSON解析成功性校验,只要解析失败就重试;第二重是Schema合规校验,字段类型、必填项、枚举值全部对齐,不合规就带着报错信息重试。同时我还加了一个“强制JSON净化”的预处理函数,把模型输出中包围JSON的废话先用正则剥离掉。这套组合拳下来,脏数据比例从百分之几降到了千分之几。

4.2 技能之间共享上下文导致的“记忆污染”

技能层上线后的第二周,运营反馈了一个诡异的问题:明明是分析耳机评论,生成的回复里却出现了咖啡口味的内容。我排查了一下午,最后发现是编排器把上一个用户会话的处理结果放到了共享上下文里。由于两个用户任务都经过了topic_extractor,而我的缓存键只用了技能ID,没有带会话ID,导致技能A在处理新请求时读到了旧缓存。

这是典型的“记忆污染”问题。技能之间可以共享规则,但不能共享状态。我给每个技能执行环境加了一个context_id字段,所有上下文读取都必须在指定的context_id范围内进行。同时把技能运行改成轻量级沙箱模式,技能内部产生的临时变量一律不回写全局,只有通过输出Schema定义的字段才能向外传递。

4.3 技能无限循环和死锁

并行组合不是银弹,它有个隐藏雷区:技能循环依赖。我的第一个版本里,urgent_reply_generator需要先调用urgency_alert来确认是否需要生成紧急回复,而urgency_alert又想复用urgent_reply_generator来判断回复紧急程度。两个技能互相等待,虽然框架层面没有报错,但任务队列越积越长,最终把执行线程池耗尽了。

我的修复措施有两层。第一层是静态检查,在注册技能时就用拓扑排序检测技能依赖图里有没有环,有环就直接拒绝注册;第二层是运行时保护,给每个编排任务设置最大调用深度(默认4层)和总超时(默认60秒),超过就强制终止并返回错误。依赖图检查是仪器层面的防线,运行时保护是人命关天时的熔断器,两者缺一不可。

4.4 权限边界没守住,技能拿到不该拿的API

有一次我给hot_search_analyzer技能添加了“抓取搜索结果”能力,在allowed_tools里填上了网页抓取工具。结果这个技能在处理用户上传的文档时,居然尝试抓取文档内附带的URL。虽然最后因为目标URL不在白名单里被拦截了,但这给我敲了警钟:技能的权限边界如果太宽,就相当于把一把万能钥匙交给了模型,风险很大。

我现在给每个技能配置一份工具ACL,白名单机制,默认全部拒绝。技能的constraints.allowed_tools里明确写出它能访问的工具列表,同时工具层再做一次调用方校验,双重保险。即便将来有大模型越权调用工具的Bug出现,底层的ACL也能挡住。

4.5 常见问题速查表

我把真实运行中碰到的典型问题整理成了一张速查表,可以直接收藏备用。

症状根因解决办法
技能输出偶尔解析失败模型输出JSON之外多了解释性文字增加JSON净化预处理、双重校验、带错重试
两个技能同时改同一个变量缺省上下文隔离,状态串了按会话ID隔离上下文,技能状态不落全局
任务队列越来越多,最终线程耗尽技能循环依赖或死锁注册时拓扑排序检测依赖环、运行时限制深度与超时
生成了不该有权限的功能技能的工具白名单过宽默认拒绝,按最小权限原则显式放行
多次重试仍然失败重试时Prompt没带修复指令,模型原地打转重试前把上次报错拼回Prompt,引导模型修正
新版本技能上线后效果不如旧版缺少灰度链路同ID多版本共存、低版本兜底自动降级

这张表里的每一条,都是我付出实际代价换来的。技能层本身不是复杂的架构,但任何一环没守住,后面运维起来都会很难受。

5. 关于技能库演进的几点经验

5.1 动手编码前,先把“技能清单”写出来

如果你现在正打算在自己的Agent项目里引入技能层,我建议你抵抗住“先写代码”的冲动,先做一次技能盘点。找一个周末,把当前Agent承担的每个任务点写下来,问自己三个问题:

  • 这个任务是否是相对独立的子任务,还是依赖其他任务的结果?
  • 这个任务的输入输出是否可以用结构化的Schema描述?
  • 这个任务是否会在多个地方被复用?

三个问题的答案如果都是肯定的,那它就应该被设计成一个技能;如果第一个问题是否定的,说明它需要先拆解;如果第二个问题是否定的,说明这个任务还没想清楚,硬做成技能只会把模糊放大。

我自己的经验是,一个业务复杂度适中的Agent,初始技能清单在8到12个比较合理。少于5个说明拆分粒度太粗,多余20个说明拆分得太碎,管理和路由成本都会上升。

5.2 技能评估与迭代的三个指标

技能上线后不能只靠“感觉好用”来迭代,我做了一套非常朴素但有效的评估体系,用三个指标来量化每个技能的表现。

第一是成功率,指技能在执行过程中通过输入输出校验的比例。成功率低于九成的技能,要么是Prompt有问题,要么是Schema设计不合理,还有可能是模型选型错了,需要尽快处理。

第二是调用成本,指每次技能执行平均消耗的Token数和延迟。LLM类技能的成本大头在Prompt长度上,我把“输入约束尽量精简”作为技能定义的一项硬性要求,每个技能只接收自己真正需要的字段。

第三是端到端贡献度,指在组合流程中,这个技能的结果有没有被最终输出模块真正采纳。如果某个技能的返回值经常被最终Prompt忽略,那它就是在空转,消耗了成本和延迟却不创造价值,应该考虑摘除或调整定位。

5.3 什么时候值得从“提示词工程”升级为“技能系统”

最后聊聊适用范围。有些朋友看完这篇文章可能会问,我只有一个简单的客服问答Agent,也要搞技能层吗?我的建议是:不用急着上技能层。

触发我搭建技能系统的分水岭有三个信号:第一,提示词超过800字后仍频繁调整;第二,同一条业务逻辑在多个Agent里各写了一份,开始出现行为漂移;第三,业务方提出的新需求无法通过简单加规则满足,而是需要对处理链路做结构调整。这三个信号出现任意一个,才值得投入做技能层。早期项目还是以快速验证为主,不要过度设计。

我自己的体会是,技能层最大的价值不是让模型更聪明,而是让团队能以“维护代码库”的方式维护Agent能力。模型永远存在不确定性,但你可以用系统设计把不确定性控制在一个一个小盒子里,让它出错时错得明白,修起来也修得轻松。希望这篇关于 agent-skills 的拆解,能帮你少踩几个坑。

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

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

立即咨询