☰
Agent技能系统从零搭建:定义、注册、编排与实战避坑指南
2026/10/14 8:52:57 网站建设 项目流程

最近在做 Agent 相关的东西,被问得最多的一个点就是:Agent 到底怎么“学会”一个新能力的?看了一圈开源项目和团队分享,答案其实都落在同一个关键词上——agent-skills。这名字直白得有点不起眼,但它是整个智能体能不能脱离“聊天机器人”定位、真正去干活的命门。这篇东西我把从零搭建一个技能系统的完整路径拆开讲,包括技能怎么定义、怎么注册、怎么编排调用,以及我在实践里踩过的那些文档里不会写的坑,希望对正在做 Agent 落地的小伙伴有帮助。

1. 内容整体设计与思路拆解

1.1 从“工具调用”到“技能系统”,差的不是词汇

很多人一上来就跟我讨论“Agent 调用工具”,但实际做项目你会发现,工具(tool)和技能(skill)是两个层次的东西。举一个最直白的例子:一个函数 get_weather(city) 是工具,它只是把输入映射到输出;“根据用户模糊的描述判断城市、决定是否需要联网、把天气数据转成口语化回答、并在用户追问时能平滑补充穿衣建议”这整个过程,才是技能。

这个区别决定了系统架构的走向。如果只做工具调用,你在代码里写死一堆 if-else、或者让模型去选一个函数就完事了。但技能系统不一样,它要解决的是“模型在什么场景下、按什么顺序、组合哪些原子操作、通过什么标准判断任务完成”的问题。这本质上是在把“人完成一个任务的经验”结构化、可复用化,说得夸张一点,它是一个微观的专家系统,只不过大脑换成了大模型。

我在设计技能系统时,第一件事就是建了一个分类认知框架:

  • 原子技能:对应一个 API 调用或一次确定性计算,比如发请求、查数据库、算哈希值;
  • 复合技能:编排多个原子技能,比如“查天气并生成穿衣建议” = 定位城市 + 调天气 API + 读取知识库规则 + LLM 生成;
  • 元技能:决定何时用哪些技能,甚至动态生成新技能,比如任务规划器、技能编排器。

一句话,工具是手,技能是手的用法,元技能是决定怎么用手的大脑。这个分层想清楚之前,先别急着写代码,不然最后必然是一团乱麻。

1.2 为什么不能直接靠提示词搞定一切

刚开始我也试过“偷懒方案”:把所有技能说明写进 system prompt,让模型自己决定何时调用。实测下来在小 demo 里确实能跑通,但一旦技能数量超过十几个、技能之间有依赖关系或者需要顺序执行时,问题就一串一串地冒出来。

第一个问题是指令冲突。模型是概率生成的,你在 prompt 里写“先查库存再下单”,它可能在简单场景下遵守了,但用户换个说法、或者上下文稍微一长,它就开始“自由发挥”,跳过校验步骤直接调下单接口。这种不确定性在生产环境是不可接受的。

第二个问题是上下文爆炸。每个技能都附带使用说明、参数模式、示例,十几个技能的全量描述轻松吃掉几千 token。模型还得在这么多文本里找当前该用的那一段,注意力一分散,选错工具的概率直线上升。实测 5 个技能时准确率 95%,加到 20 个技能直接掉到 80% 以下。

第三个问题是无法沉淀经验。提示词写得再好,它只是一堆文本。技能执行过程中的失败记录、修正策略、参数校准经验,完全没办法结构化保存下来。今天调好明天又漂移,永远在打补丁。

所以我的结论很明确:Agent 的技能必须代码化、结构化,把它当成一等公民来设计,而不是靠提示词碰运气。这也是 agent-skills 这类项目存在的根本原因。

2. 核心细节解析与实操要点

2.1 技能定义的四个核心要素

一个能被 Agent 稳定使用的技能,光有“功能实现”是不够的。我在反复迭代后,把技能定义收敛为四个核心要素:

触发条件(trigger):什么场景下这个技能应该被激活。它可以是显式的关键词、意图分类器的输出,也可以是另一个技能的执行结果。这块一定要做到机读,而不是让模型“感觉应该用”。我的做法是给每个技能配一段语义化描述,同时配一组示例场景,注册进一个轻量级的意图匹配器里做预筛,预筛过了才轮到 LLM 做精细确认。

输入输出协议(IO contract):参数怎么传、结果怎么回,必须像 REST API 一样定义清楚,包括类型、取值范围、必填可选项、错误码。这块我用 JSON Schema,因为它天然可以验证、可以转成自然语言描述、还能直接生成 TypeScript/Python 类型定义,一套东西全链路复用。

执行逻辑(execution):技能内部到底是查库、调 API、还是调另一个 Agent 来处理,这部分就是普通的函数实现。但有一个铁律:执行逻辑内不允许出现自由生成的 LLM 调用(除非这个技能本身就是生成类技能,比如写文案)。否则排查问题时你会面对“到底是代码错了还是模型抽风了”的无底洞。

验证与回退(validation & fallback):执行完了怎么确认结果是对的?结果不对怎么办?这一步最容易被忽略,但生产事故往往就出在这里。由于 LLM 参数幻觉的存在,任何结果尤其在边界情况下必须做后置校验,校验不过要能自动触发回退逻辑。

2.2 技能注册中心:让 Agent 知道自己“会什么”

技能定义好了之后,得有个地方让 Agent “查到”自己有哪些技能。这个组件的正式名字叫技能注册中心(Skill Registry),它维护的是一整套技能元数据。

我在实现里用的是装饰器加自动扫描的方式,大概长这样:

# skills/registry.py class SkillRegistry: def __init__(self): self._skills = {} self._overrides = {} def register(self, skill_cls): """通过装饰器注册技能类""" name = skill_cls.name if name in self._skills: raise SkillConflictError(f"技能重名: {name}") self._skills[name] = { "name": name, "description": skill_cls.description, "schema": skill_cls.input_schema(), "tags": skill_cls.tags, "handler": skill_cls, } return skill_cls def get_skill(self, name): return self._skills.get(name) def list_skills(self, tag=None): if tag: return {k: v for k, v in self._skills.items() if tag in v["tags"]} return self._skills registry = SkillRegistry() def skill(name, description, tags=None, input_schema=None): """技能装饰器""" def decorator(cls): cls.name = name cls.description = description cls.tags = tags or [] if input_schema: cls.input_schema = staticmethod(input_schema) registry.register(cls) return cls return decorator

使用侧就可以写得很干净:

@skill( name="fetch_product_detail", description="根据商品ID查询商品详情", tags=["ecommerce", "read"], ) class FetchProductDetailSkill: @staticmethod def input_schema(): return { "type": "object", "properties": { "product_id": {"type": "string", "description": "商品ID"}, }, "required": ["product_id"], } def execute(self, args): pid = args["product_id"] data = get_product_from_db(pid) if data is None: return {"status": "not_found", "data": None} return {"status": "ok", "data": data}

注册中心最大的好处是它把“技能清单”变成一个可实时查询的 API,Agent 的核心调度器只需要调用 registry.list_skills() 拿清单、按语义相似度或意图匹配选技能、再校验参数执行,整个闭环就打通了。后续加新能力只需要在代码里多放一个文件,不用改任何调度逻辑。

2.3 技能的编排与状态管理

单个技能只是积木,真正干活靠的是编排。这里我用了一个很朴素但极其可靠的模型,就是链式编排器(Chain Executor)。核心思想是:一个任务由多个技能按 DAG 顺序执行,前一个技能的输出作为后一个技能的输入,每个节点都有明确的入参和出参校验。做任务之前,任务规划器先把用户请求拆成一张执行图,然后由执行器照着图跑。为了绕开 LLM 在复杂编排上的不可控,我把图定义成 DSL 表现的静态结构,再运行时逐节点执行。比如写一个“查天气 + 穿衣建议”的技能链,DSL 长这样:

chain: - id: locate_city skill: extract_city_from_query input: {query: "{{input.user_query}}"} - id: fetch_weather skill: fetch_weather_info input: {city: "{{steps.locate_city.output.city}}", date: "{{steps.locate_city.output.date}}"} - id: gen_advice skill: generate_outfit_advice input: {weather: "{{steps.fetch_weather.output}}"} fallback: use_template_advice

执行器读取这个 YAML,依次执行三个节点。第二步如果输出异常走回退;第三步优先让 LLM 生成,但为了可控,把温度调低,同时提供模板兜底。这套机制让我同时拿到了两个好处:第一是出问题时能定位到具体是哪个节点失败了;第二是非核心节点(比如城市提取)可以直接换成规则实现,跑得快还不花钱。

但请特别注意一点:编排不是越灵活越好,过度抽象只会带来维护地狱。我一度想做一个完全自动的动态规划器,让 Agent 自己生成 DAG,但一到线上就发现,模型生成的图经常包含不存在的技能名、缺少必要的后置校验节点,甚至死循环——这个方案最后还是降级成“预设模板 + 少量动态扩展”。在稳定性优先的场景里,预设链条永远比全动态更可靠。

3. 实操过程与核心环节实现

3.1 首个里程碑:让 Agent 真正“学会”一个技能

下面给一个最小可跑的完整例子,让你对标自己的环境快速把第一个技能跑通。我这里用的是 Python 3.10 + FastAPI,模型为演示方便走本地推理接口,但生产上你有网关就接网关,能跑就行。

大概结构如下几个文件:

agent-skills/ ├── registry.py # 注册中心 ├── executor.py # 链式执行器 ├── skills/ │ ├── __init__.py # 自动扫描并导入技能 │ ├── calculator.py # 复杂计算工具 │ └── reminder.py # 创建提醒 └── server.py # FastAPI 入口,对外暴露 Agent 调用接口

executor 的核心调度逻辑如下,围绕“拿任务 -> 匹配技能 -> 验证参数 -> 执行 -> 校验输出”的闭环:

# executor.py class ChainExecutor: def __init__(self, registry): self.registry = registry def execute(self, chain_plan, initial_input): context = {"input": initial_input} step_outputs = {} for node in chain_plan.nodes: skill_entry = self.registry.get_skill(node.skill_name) if not skill_entry: raise SkillNotFoundError(node.skill_name) rendered_input = self._resolve_template(node.input_template, context) # 参数校验 validator = Validator(skill_entry["schema"]) errors = validator.validate(rendered_input) if errors: raise SkillInputValidationError(node.skill_name, errors) # 执行 handler = skill_entry["handler"]() result = handler.execute(rendered_input) # 简单的输出校验:必须有 status 字段 if "status" not in result: raise SkillOutputValidationError(node.skill_name) step_outputs[node.id] = result context["steps"] = step_outputs if node.fallback and result.get("status") == "error": fallback_entry = self.registry.get_skill(node.fallback) result = fallback_entry["handler"]().execute(rendered_input) step_outputs[node.id] = result context["steps"] = step_outputs return step_outputs

这里最有价值的经验就是那个 fallback 字段。模板兜底让我在 LLM 生成类的技能上直接省掉了一大半的重试成本。你可以把 fallback 理解成“安全网”,专门接住不可靠环节的意外。

3.2 技能选择的策略与实时路由

执行器负责跑,但“这一次到底用哪个技能”谁说了算?我一开始把选择权完全交给 LLM,让它从技能清单里挑。后来发现当技能数量增多、描述之间近似时,模型会频繁挑错,尤其两个技能输入长得像、输出格式完全不同的时候,代价很高。

后来我改成两级策略:

第一级,基于嵌入向量的召回。给每个技能的描述跑一遍 embedding,同样把用户当前意图也转成 embedding,做向量相似度召回,先取 Top-N(通常 N=5 就够)。这一步非常快、成本也低,能先把明显不相关的技能滤掉。

第二级,LLM 精排。把召回结果里 N 个技能的 name、description、参数示例丢给模型,让它输出最合适的一个或一组,同时给出理由。由于候选集小,模型注意力能集中很多,准确率比全量选择高出一截。我试过 24 个技能的场景,全量选择准确率约 82%,向量召回+精排后稳定在 96% 左右。

为了让你能直接上手,这个路由函数的核心结构我贴一下(向量用 sentence-transformers 的轻量模型):

# router.py def route_to_skills(user_query, registry, top_n=5): query_vec = embed(user_query) skill_entries = list(registry.list_skills().values()) scored = [] for entry in skill_entries: desc_vec = embed(entry["description"]) score = cosine_similarity(query_vec, desc_vec) scored.append((score, entry)) scored.sort(key=lambda x: x[0], reverse=True) candidates = [entry for _, entry in scored[:top_n]] # 第二步,LLM 精排 prompt = build_ranking_prompt(user_query, candidates) chosen = llm_choose(prompt) # 返回技能名 return chosen

提醒一句:向量模型别用通用的,最好用针对你们领域文本微调过的。我一开始用通用模型,电商领域里“价格保护”和“退差价”两个技能描述语义高度相似,向量召回直接乱套,后来换了领域微调模型,召回才回归正常。

3.3 技能内部的容错与超时控制

技能执行在真实环境里面对的脏数据比想象中多得多。接口超时、返回空值、字段类型不符,几乎每天都在发生。所以凡是会发起网络请求或调用外部服务的技能,必须统一套一层容错壳,模板如下:

# skills/_base.py import time from functools import wraps DEFAULT_TIMEOUT = 5.0 def with_timeout_and_retry(max_retries=2, timeout=DEFAULT_TIMEOUT): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): last_exc = None for attempt in range(max_retries + 1): try: return func(*args, **kwargs) except TimeoutError as exc: last_exc = exc except RemoteServiceError as exc: last_exc = exc time.sleep(0.5 * (attempt + 1)) return {"status": "error", "error_type": type(last_exc).__name__} return wrapper return decorator

这里想说一个细节:外部服务调用失败后不要立即重试,加一点退避;重试次数上限不要超过 2 次,因为失败率高的时候,重试只是给下游系统加压力。最重要的是,无论重试多少次都要返回一个结构化错误,而不是抛裸异常,这样上层编排才能接到错误并触发兜底逻辑。

3.4 技能链的测试策略

测试是技能系统里最容易欠债的环节。一个技能链好不好,不能只靠一两个 happy path 例子来验证。我的做法是给每条链配上两套用例:

-D 正常流测试,验证“输入 A + 技能 B -> 输出 C”是否符合预期;

  • 异常流测试,故意构造脏输入、模拟下游 500、删掉某个中间节点,验证系统是否能按预设退避或回退策略兜住。

这两套用例的产物就是一个简单的 JSON 文件,跑 CI 的时候自动拉起。异常流用例几乎每次都能让我发现一个没想到的边缘情况,比任何 review 都管用。我甚至把这些异常流测试直接导出成上线前的冒烟用例,效果出奇的好。

4. 常见问题与排查技巧实录

4.1 技能没有按预期触发

这是接入初期最高频的问题:用户表达很明确,技能却始终不触发。排查分两步走。

第一步看召回到精排这一层是不是就断了。打印出向量相似度的分数分布,如果目标技能的 top-5 分数都低于某个阈值(比如 0.4),说明描述与真实用户表达在语义上距离过大,我就直接把真实用户案例补充进技能 description 或 tags 里。我在实践里发现,描述里用“动词+对象+场景”的句式,比名词罗列的效果好得多,比如“查询商品详情”优于“商品详情信息”。

第二步是看 LLM 精排层的 prompt 是不是给了错误信息。有时候召回没断,但精排型号自作主张选错了。这个问题的根源基本是你的 prompt 里候选技能的描述太粗糙,模型分辨不出两者的边界。解决方法是给每个技能加一句“在以下情况不要使用我”的负例描述,模型在这样的约束下会明显更谨慎。

4.2 参数校验总是失败

另一个高频问题出现在参数传递环节。比如技能 A 输出了一个 product_id,技能 B 期望的是 sku_id,如果不做映射直接接上,校验必然失败。

这个问题的根源是技能之间的“接口契约”没对齐,其实跟团队的 DDD 设计不到位一个道理。我把它当成一个基础工程问题解决:给所有技能编目时,额外维护一份“字段统一定义表”,把 product_id、sku_id、item_no 这类相同含义不同叫法的字段统一成 canonical 字段。所有技能的输入输出都先做一层字段映射,再传给执行器。这个表不大,但每次参数校验失败都能在这里找到答案。

4.3 技能执行结果的可靠性无法度量

到最后你会发现很多争执都指向一个问题:这个技能到底靠不靠谱?模型生成类的技能尤其难回答。

我的做法是为每个生成类技能加一个轻量级的输出质量评分器。它接收技能输出和原始输入,用一个更便宜的模型做二分类打分,低于阈值就自动走兜底链路或切换策略。举个例子,“生成商品卖点”这个技能,评分器会校验卖点里是否包含原素材里的核心关键词、是否重复、长度是否可控。如果分数低,就不把这段文本直接展示给用户,而是退回模板方案。一开始这么做纯粹是为了保线上稳定,后来发现它还积累了一个副作用:评分数据反过来可以指导我们对技能的输入输出做迭代优化,哪里薄弱一目了然。

4.4 多技能并发时的资源争抢

上线一段时间后,单技能调用已经没问题了,但一旦多个请求并发触发技能链,就会出现数据库连接被打满、外部 API 限流、甚至 OOM。这个问题非常实诚:技能的并发控制不等于 Agent 的并发控制,底层还得按基础设施来做治理。

我做了三层:外层做并发限流,把技能执行包装成带信号量的队列,最大并发数根据下游承载能力配置;中间层做熔断,下游接口错误率超过阈值时直接返回降级结果;内层做请求合并,比如多个用户查同一个商品的详情,合并成一个上游请求,结果广播给所有等待者。这三套下来,高峰期 P99 从 3800ms 降到了 900ms 左右,效果立竿见影。

4.5 常见问题速查表

问题现象可能原因排查步骤解决方案
技能不触发描述和真实意图偏离打印向量召回的分数分布重写 description 为动词+对象+场景
召回到精排都正常,但选错技能候选技能描述过于相似对比相似技能的 description 差异为每个技能补充“不要使用我”负例
参数校验失败字段命名不统一查看技能链节点间的输出与输入样例维护字段统一定义表做字段映射
生成类技能不稳定输出质量不受控对生成结果做批量抽样人工评估加输出质量评分器,低于阈值走模板
并发一高就超时下游承载能力不足观察 DB 连接数与上游限流日志限流 + 熔断 + 请求合并
错误信息在上层丢失技能抛的是裸异常检查技能的执行日志是否捕获到堆栈统一封装结构化错误对象

5. 最后再分享两个小技巧

第一点,技能系统上线不要一上来就追求“大而全”。我见过很多团队开局就规划 60 个技能,真正做到能稳定运行的也就十来个,其余全是半吊子。我的建议是每个迭代只打磨 3-5 个核心技能,把它们跑得像磐石一样稳,再慢慢加新技能,这样的节奏比一口气堆数量快得多,还容易沉淀出可复用的模式。

第二点,多去看看真实用户到底是怎么表达需求的。很多人把技能设计建立在自己想象的需求上,结果发现用户根本不那么说话。我后来养成了一个小习惯:每周把客服聊天记录、用户反馈里带有“应该可以直接...”这类句式的内容捞出来,对照现有的技能清单看缺口在哪。这个东西比任何需求评审都精准,也是我不断扩充技能库的最大灵感来源。

技能系统的路一旦铺开,后面能玩的花样很多,比如把技能作为可插拔插件对外开放、基于日志自动生成新技能等。但现阶段先把上面这层地基打牢,比什么都实在。希望这篇基于实践总结出来的东西,能让你少走几步弯路。

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

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

立即咨询