资讯动态

Agent技能化实战:用Skills架构告别不稳定Prompt,实现可编排的工程化智能体

发布时间:2026/10/13 5:29:31 来源:尧图企业网站定制
接手 agent-skills 这个方向最初是因为一个非常朴素的烦恼同一个模型同一套系统提示词昨天还能稳定完成任务的行为今天需求调了几行就变得飘忽不定。后来转向技能化skills的思路把 agent 的行为从“靠一段长文约束”变成“靠一组结构化的技能模块去调度”实测稳定度提升非常明显。这篇博客把 agent-skills 的设计思路、最小实现和踩坑记录完整展开适合正在做智能体应用、想摆脱纯 prompt 脆弱性、又不想一上来就扛重型框架的开发者参考。很多人觉得 agent 开发最难的是模型能力不够但真正做过一段时间就会发现更头疼的是不可控。你不知道模型下一步会调用什么、会输出什么格式、会不会在一个简单环节上突然发散。agent-skills 的核心就是把“模型自由发挥”这件事压缩到最小范围把常用的、稳定的、需要约束的行为固化成技能模块让模型只负责“选哪个技能”和“填哪些参数”而不是“现场想一套解决方案”。这样工程上可测、可追踪、可回滚比纯提示词方案可靠得多。1. 为什么我盯上了“agent skills”这个方向1.1 agent-skills 到底解决什么问题先说一个真实场景。我之前维护一个数据处理类的智能体项目主要让模型读取结构化文件、做简单清洗、生成统计摘要。一开始所有流程都写在系统提示词里效果也还行但遇到几个非常尴尬的问题一是稍微改一下业务规则那一段提示词就得重写牵一发动全身二是模型偶尔会跳过清洗步骤直接去生成摘要导致下游输出全是脏数据三是很难做单元测试你没法说“这段逻辑变化是模型造成的还是流程造成的”。后来我看到技能化skills的思路才意识到这其实是一个工程问题不是模型调优问题。把任务拆成一个一个技能每个技能内部有固定的步骤、参数、输出格式整个 agent 做的事情就是根据用户意图去匹配技能、执行技能、组合结果。模型在其中的角色被限制成“意图理解 参数提取”而真正干活的全是确定性的代码逻辑。这样一来第一步要解决的其实是“确定性与灵活性的边界”问题哪些环节应该固定死哪些环节应该留给模型发挥。1.2 技能化方案比单纯堆 prompt 好在哪打个比方传统 prompt 方案就像让一个厨师看着一段手写菜谱去炒菜菜谱写得再细火候和盐量还是靠厨师的临场感觉而技能化方案是把“配料称重”“热锅”“下菜”“调味”都做成标准工序厨师只需要根据客人需求决定先走哪个工序、选中哪条线路。具体优势有这么几个方向。第一是可复用性。同一个技能可以服务多个业务场景。比如“内容摘要”技能既可以用在信息流产品里也可以用在日报自动生成里。你不需要为每个场景重写一遍提示词描述只需要注册同一个技能然后引用它。第二是可测试性。每个技能是一个独立的函数输入输出都有明确的 Schema可以直接写单元测试跑回归。而传统提示词方案里隐含的行为规则没法单测只能靠端到端的人工体验去碰运气。第三是可观测性。技能执行的过程会留下清晰的调用记录模型选了哪个技能、传入什么参数、技能返回什么结果。出了问题可以直接定位到具体模块不用对着对话日志反复猜。第四是模型切换成本低。因为模型只负责技能选择和参数提取这部分行为比较容易通过描述优化来适配真正涉及业务逻辑的部分都在代码层完成与模型厂商没有耦合。这套思路让我重新审视了 agent 项目的架构与其把希望寄托在“模型变聪明”不如先把系统自身的确定性做扎实。模型永远是那个擅长举一反三但也会发挥失常的成员而技能体系则是一个把成员行为规范成流程的协作者。2. 一套技能系统的核心设计思路2.1 skill 的最小单元应该长什么样在设计 skill 结构时我参考了不少公开资料里的做法同时结合自己的项目反复迭代。一个 skill 最终被我收敛成下面这几个核心字段。from dataclasses import dataclass, field from typing import Any, Callable, Dict, List, Optional dataclass class Skill: name: str # 技能唯一标识如 web_search description: str # 给模型看的说明描述何时使用、能做什么 parameters: Dict[str, Any] # 参数 JSON Schema约束模型提取的字段 handler: Callable[..., Any] # 实际执行技能的函数 dependencies: List[str] field(default_factorylist) # 依赖的其他技能名 max_retries: int 2 # 失败重试次数 timeout: float 30.0 # 超时控制这几个字段并不是随意定的。name 是用来在程序里定位技能的稳定标识绝不能让模型自由生成description 是给模型看的写得好不好直接决定模型能不能准确调用parameters 用 JSON Schema 来定义这能直接利用模型厂商对结构化参数提取的支持大幅降低解析出错概率handler 是真正的执行函数里面可以写任何确定性逻辑dependencies 用来声明技能间的依赖关系为后续做图编排预留空间。实际项目中我会把 description 写得非常“功能导向”重点是触发条件和注意事项。比如一个搜索技能的描述我不会写“这是一个搜索函数”而是写“当用户提出查资料、找新闻、获取最新信息等需求时使用本技能输入应该是一个简短的字符串关键词不要包含多余的祈使句”。这样模型在意图匹配时命中率会高很多。2.2 技能注册与发现机制有了一堆技能定义之后关键问题是怎么让模型在运行时“看到”它们。这就需要一个注册中心也就是一个技能库把技能按照一定规则组织起来供意图匹配环节查询。我最初的做法非常简单用一个全局字典把所有技能按 name 存起来然后在生成模型上下文时把全部技能的描述信息打包塞给模型。这个方案在技能数量少于 10 个时完全可用一旦技能数量增长到几十个就会出现上下文占用过多、模型选择困难等问题。后来我做了分层热技能常驻上下文冷技能通过关键词索引按需加载只有用户意图命中索引时才把对应技能描述注入。class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} self._index: Dict[str, List[str]] {} def register(self, skill: Skill, tags: Optional[List[str]] None) - None: self._skills[skill.name] skill for tag in tags or []: self._index.setdefault(tag, []).append(skill.name) def get(self, name: str) - Optional[Skill]: return self._skills.get(name) def search(self, text: str) - List[Skill]: # 简化实现按包含关键词匹配技能名与标签 matched [] for tag, skills in self._index.items(): if tag in text: for sname in skills: if sname not in matched: matched.append(sname) return [self._skills[n] for n in matched]注册中心的一个重要细节是description 和索引词要分开维护。description 是给模型做意图匹配的索引词是给自己做程序过滤的。两者互补缺一不可。项目中期我曾经只维护 description导致每轮都要把所有技能描述塞给模型既不经济又容易干扰选择加入标签索引之后整体效果立刻改善。2.3 技能编排的两种范式单个技能只能解决单一任务真实业务往往是把多个技能串起来。从我的实践看编排主要落在两种范式上。第一种是固定流水线。技能之间的先后顺序是确定的业务方在配置阶段就写死。比如“理解用户问题—查询数据—生成摘要—发送通知”这条链路每一步对应一个技能顺序不随模型意图而改变。这种做法适合业务流程稳定的场景优点是可靠缺点是缺乏弹性。第二种是模型驱动的动态编排。模型根据用户意图选择技能并且可以把多个技能组合起来执行。比如用户说“把昨天运营日报里的核心指标整理成周报”模型需要先调用“读取数据”技能、再调用“指标分析”技能、最后调用“报告生成”技能。动态编排的灵活性高但对技能描述的质量和上下文管理的要求也高。真实项目里我通常两种混用主干流程固定分支节点让模型动态选择。有一个容易被忽略的点技能内部的步骤也可以是“小流程”。不要把技能理解成一个单一函数它完全可以是一个子 Agent内部有自己的提示词和工具。这样项目会形成“大 Agent 编排技能技能内部再编排子步骤”的嵌套模式。我建议在初期控制嵌套深度最多两层否则排查问题时会非常痛苦。3. 从零搭一个 agent-skills 最小可用系统3.1 基础结构定义前面已经把 Skill 数据类定义出来了这一节直接上可运行的最小系统。我用纯 Python 实现了一个极简版本不依赖任何特定模型框架核心逻辑都聚在三个模块里技能定义、注册中心、执行引擎。假设我们的 agent 需要处理一个很典型的场景用户给一段文本agent 要判断它属于“咨询”还是“投诉”然后分别走不同处理路径。常规做法是让模型直接输出分类标签但实际业务里分类之后往往还要接后续处理动作这就适合拆成两个技能“意图分类技能”和“意图处理技能”。import json from typing import Any, Dict, List, Union def classify_intent(text: str) - Dict[str, str]: # 实际项目中这里一般会调用模型或规则引擎 # 这里用一个示例只接受模型提取的文本后进行本地规则判断 if 退款 in text or 投诉 in text: return {intent: complaint, level: high} return {intent: consult, level: normal} def handle_by_intent(intent: str, text: str) - Dict[str, Any]: # 模拟下游处理 if intent complaint: return {action: create_ticket, priority: high} return {action: reply_template, priority: normal}这是 handler 层逻辑非常直白。真正的难点在于如何把这两个函数包装成让模型看得懂的技能并且让模型学会“先分类再处理”的流程。3.2 注册中心与加载器注册中心承担两件事把技能登记到全局库以及按需生成“技能摘要清单”。摘要清单是真正会被拼进模型上下文的文本它的格式直接决定模型对技能的理解程度。我通常按下面的模板生成def build_skill_summary(skill: Skill) - str: schema_str json.dumps(skill.parameters, ensure_asciiFalse) return ( f技能名称: {skill.name}\n f功能说明: {skill.description}\n f参数结构: {schema_str}\n --- )加载器在系统启动时读取所有技能定义并进行注册。考虑到后续技能数量变大加载器还需要支持按目录加载、按配置项启用停用这样上线的技能列表就可以由配置文件控制不用改代码。class SkillLoader: def __init__(self, registry: SkillRegistry): self.registry registry def load_from_dict(self, skills: List[Skill]) - None: for skill in skills: self.registry.register(skill, tags[skill.name]) def load_from_config(self, config: dict) - None: # config 里可以声明需要启用的技能以及自定义标签 for item in config.get(skills, []): skill Skill(**item[definition], handleritem[handler]) self.registry.register(skill, tagsitem.get(tags, []))这段代码虽然简单但体现了一个工程原则技能要能配置化启停。项目早期我把技能全部硬编码注册每次联调都要动代码非常麻烦。改成配置驱动之后产品同事自己就能调整线上技能范围开发效率明显提升。3.3 让模型根据意图调用技能技能注册好之后下一步就是把技能清单注入到模型消息里并让模型返回“要调用的技能名称和参数”。这里有一个实现细节我没有让模型直接自己去执行技能而是让它返回一个结构化的“工具调用指令”然后由程序安全地映射到 handler 上执行。这样做有两个好处一是执行过程可控可打断二是模型输出如果不符合预期可以在映射阶段做兜底。def build_agent_messages(user_query: str, skills: List[Skill]) - List[Dict[str, str]]: system_content ( 你是一个技能调度器。请根据用户需求从可用技能中选择最合适的技能 并提取调用参数。必须使用如下格式返回\n {skill: 技能名, arguments: {...}}\n ) skill_text \n.join([build_skill_summary(s) for s in skills]) return [ {role: system, content: system_content \n可用技能:\n skill_text}, {role: user, content: user_query}, ]接着是执行循环。模型返回“调用技能”的指令后由执行引擎解析参数、调用注册中心里的 handler、拿结果再回传给模型让模型可以基于结果继续决策。这是 agent 应用惯用的 ReAct 式循环只是把“action 空间”缩小到注册表里的技能集合防止模型即兴创造动作。def run_agent(query: str, registry: SkillRegistry, max_steps: int 5): messages [] skills registry.search(query) or list(registry._skills.values()) messages build_agent_messages(query, skills[:5]) # 只注入部分技能避免上下文爆炸 for step in range(max_steps): model_out call_model(messages) # 伪代码实际接入某个模型接口 parsed safe_parse(model_out) if not parsed or skill not in parsed: return {error: no_valid_skill, raw: model_out} skill registry.get(parsed[skill]) if not skill: return {error: skill_not_found, skill: parsed[skill]} result skill.handler(**parsed.get(arguments, {})) messages.append({role: assistant, content: model_out}) messages.append({ role: system, content: f技能 {skill.name} 执行结果: {json.dumps(result, ensure_asciiFalse)} }) if is_final_answer(result): # 业务逻辑判断任务是否完成 return result return {error: max_steps_reached}这里的 safe_parse 很关键。模型偶尔会输出非法 JSON或者输出混在普通文本里所以我会先尝试从文本中正则提取 JSON 片段失败再用“召唤”方式要求模型规范输出再失败就返回错误信息。这个兜底策略在实际运行中能把成功率从 80% 拉到 95% 以上。3.4 实际跑通一个多技能流程用一个具体例子说明完整流程。假设我的 agent 接到的用户消息是“我刚买的东西有质量问题想退货”。第一步系统把两个技能摘要注入模型上下文。模型判断这是一个投诉于是输出技能调用指令调用 classify_intent参数是文章中的文本。执行引擎执行后得到 intentcomplaint。这个结果回传给模型。第二步模型看到分类结果后继续调用 handle_by_intent拿到下游动作 create_ticket。整个流程只用了两个技能模型全程没有直接操作业务对象所有副作用都发生在 handler 里。这种设计最直接的好处是如果我想把“投诉处理”的规则改得更复杂比如增加一个审核步骤只需要改 handler 逻辑或者调整技能依赖关系模型部分几乎不用动。也就是说技能系统把业务知识的维护地点从提示词时代转移到了代码层工程师可以像维护普通函数一样维护 agent 的行为。4. 落地过程中的常见坑与排查实录4.1 模型死活不调用你想要的技能这个问题出现的频率最高。原因也很有意思技能的 description 写得像一份给软件开发者的 API 文档而不是给模型看的“触发指令”。比如有人会写“查询函数接受一个 query 参数”模型看了之后只会在用户明说“查询”时才调用它稍微换个说法就不触发了。解决办法是在 description 里明确触发条件、触发句式、绝对不使用的场景。比如这样改当用户想查找资料、了解新闻、获取信息、确认事实时调用本技能。 输入是查询关键词不要包含疑问词例如不用传“帮我查一下”。 如果用户只是闲聊不要调用本技能。改了之后命中率提升非常明显。另外还有一个经验参数说明要写“对用户原话的提炼”而不是写“参数定义”。参数描述可以让模型更好地从用户输入里抽取关键词。4.2 技能嵌套与依赖混乱虽然前面建议最多两层嵌套但实际项目里总有人把技能写成 A 调用 B、B 又调用 C、C 又依赖 A形成循环引用。一旦模型在某个环节选错了依赖链整个流程就会卡死或者无限重试。我的处理办法有两个第一是在执行引擎里加一个调用深度计数器超过深度直接中断并返回错误信息第二是在注册中心初始化时做依赖环检测发现环直接报错拒绝启动。def detect_cycle(registry: SkillRegistry) - List[str]: visited, stack set(), [] def dfs(name): if name in stack: return stack[stack.index(name):] [name] if name in visited: return [] visited.add(name) stack.append(name) for dep in registry.get(name).dependencies: cycle dfs(dep) if cycle: return cycle stack.pop() return [] for sname in registry._skills: cycle dfs(sname) if cycle: return cycle return []detect_cycle在服务启动时执行一次开销极小但省掉了无数个线上事故。4.3 上下文被技能说明撑爆怎么办技能数量增长到 20 个以上时把所有技能的描述全部塞进消息里就不现实了。一是 token 成本快速增长二是技能太多之后模型的选择准确率反而下降输出经常混淆不同技能的边界。我的应对策略是三级索引方案第一级是一个极简的“技能目录”只有技能名和一句话摘要第二级是标签索引按关键词匹配候选技能第三级才是完整描述只有候选技能才会把完整的 description 和参数 Schema 注入上下文。平时上下文里只有第一级目录候选技能分析时才注入完整描述。这套方案比较适合 20-50 个技能规模的项目再大的话可能需要引入向量检索但思路还是一样的。4.4 排查实录速查表现象可能原因排查方法解决办法模型不调用技能直接自己回答description 缺少触发条件打印该技能注入模型的原文重写 description加入明确的触发句式模型调用技能但参数乱传参数 Schema 描述不清晰检查模型输出的 arguments 与真实用户输入的映射在参数描述里写明“从用户原话中提取核心名词”技能执行报错但 agent 反复重试缺少失败策略看执行日志中的错误类型给每个技能配 max_retries异常直接返回固定错误信息多个技能描述互相干扰技能边界模糊把两个技能摘要放一起对比增加“不要使用本技能当……”的排除描述上下文 token 超限技能描述全量注入统计每次请求的 token 消耗启用索引加载或压缩描述模型输出解析失败JSON 格式不稳定检查模型原始输出增加 safe_parse 兜底强制模型重新输出最后说点个人体会。agent-skills 给我最大的一个改变是让我把“调模型”这件事变成了“调流程”。以前每次效果波动第一反应是改提示词改完也只能靠感觉验证现在我会先看日志模型选了哪个技能、参数是不是对的、handler 执行是不是出了问题。这个转变带来的工程化收益比单纯换一个更强的模型要大得多。如果你也在做 agent 类应用建议先别急着堆功能挑一个最小场景把技能系统搭起来跑一遍你会回来感谢自己这个决定的。

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

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

免费获取报价 →
↑