☰
智能体技能体系设计与实战:从Prompt到可插拔能力层
2026/10/28 5:15:50 网站建设 项目流程

"agent-skills"这个标题,字面直译是"智能体技能",但在实际工程语境里,它指的是给大模型智能体(Agent)搭一套"可插拔能力层"的方法论。说白了,就是回答一个问题:当LLM只会聊天时,你凭什么让它去调API、写代码、查数据库、操作浏览器?这篇文章我基于自己把多个Agent项目落地的经验,把技能体系的设计思路、接口规范、编排方式和排坑过程完整拆一遍,给正在折腾Agent开发的人一个可直接参考的底稿。

1. 为什么智能体技能体系如此关键

1.1 技能层是Agent从"能说"到"能做"的分水岭

现在市面上的Agent框架很多,LangChain、AutoGPT、MetaGPT各有拥趸,但不管哪个框架,底层逻辑都绕不开一个东西:模型本身只是在做"文本进,文本出"的概率预测,它并不知道怎么真正操作外部世界。要让模型完成"帮我查一下这个月云账单里哪一项最贵"这种任务,就必须给它配一个"查云账单"的技能,而这个技能本质上是一段可被模型调用的程序。

技能体系做得好的项目,模型只需要输出一个函数名和几个参数,剩下的脏活累活由技能代码完成。技能体系做得烂的项目,所有逻辑都塞在Prompt里,模型每走一步都要靠角色扮演来"猜"下一步该干什么,结果就是不可控、不可复现、不可调试。

我接手的第一个Agent项目就吃过这个亏。当时想做一个自动运维脚本,把SSH连接、命令执行、日志采集全部写在System Prompt里,让模型自由发挥。前两次演示效果惊艳,第三次直接连错服务器执行了rm命令,差点酿成大祸。从那之后我彻底转向"Prompt只做决策,技能层做执行"的架构,把每一个外部操作封装成独立的函数,模型只负责在函数之间做选择和传参。

这个转变可以用一个简单的代码对比来说明:

# 反面示例:让模型直接输出shell命令 decision = llm.chat("请连接服务器并查看nginx日志") os.system(decision) # 灾难现场 # 正面示例:模型输出技能调用意图 decision = llm.chat("请连接服务器并查看nginx日志") # 输出: {"skill": "ssh_execute", "params": {"host": "web01", "cmd": "tail -100 /var/log/nginx/error.log"}} execute_skill(decision["skill"], decision["params"]) # 安全可控

区别在哪儿?第一种方案里模型掌握全部权限,第二种方案里模型只能在一个白名单技能集合里做选择。技能边界就是安全边界,这句话我在每一个项目里都当成铁律来执行。

1.2 技能栈的层次划分:原语、场景与编排

把技能体系做深之后,你会发现技能不能是"一个大函数打天下"的乱炖。我习惯把技能拆成三个层级,每个层级有各自的生命周期和管理方式。

底层是原语技能,也就是原子操作,比如"发起HTTP请求""读写文件""执行SQL查询""发送邮件"。这一层的特点是单一职责、不可再分、无状态。原语技能通常几十行代码就能写完,是整套体系的砖块。

中间层是场景技能,面向具体业务场景做封装,比如"查云账单""生成周报""拉取竞品价格"。场景技能会组合多个原语技能,内部可以带状态、带缓存、带重试逻辑,对外暴露的接口却很简单。

顶层是编排层,这层不写业务代码,只负责根据用户的目标动态决定调用哪些场景技能、按什么顺序调用、参数如何传递。编排层是LLM真正参与决策的地方,也是Prompt工程的主战场。

层次划分带来的最大好处是可测试性和可替换性。底层原语技能出问题,写好单元测试直接修;场景技能的业务逻辑变了,不影响原语API;编排层的策略想换,只要技能接口稳定,模型随便换都行。

我在设计时给每个技能都挂了一个简单的元信息标签:技能名、描述、参数Schema、幂等性、超时时间、调用权限。这些信息既是给模型看的(用于决定何时调用),也是给运维看的(用于审计和排障)。

2. 设计技能接口时的三个关键决策

2.1 技能颗粒度:太粗则失控,太细则让模型抓狂

技能颗粒度是最难把握的设计决策,没有之一。颗粒度太粗,比如"执行任何命令""处理任何文件",模型确实省事了,但安全性和精确性全面失控;颗粒度太细,比如"读取/etc/hostname""读取/etc/resolv.conf"各写一个技能,模型选择的空间特别小,每次任务都要生成一大串调用序列,上下文消耗翻倍,出错的概率也直线上升。

我用一个"决策熵"的概念来衡量颗粒度是否合适。决策熵指的是模型为了完成某个任务需要做选择的次数。理想状态下,一次任务调用3到5个技能就能搞定,每个技能的参数不超过5个。如果模型平均要调用10个以上技能才能完成一个简单任务,基本说明技能粒度太细了,应该考虑合并;如果只需要调1个技能就能完成所有事情,说明这个技能封装了太多逻辑,模型无法对中间过程进行干预。

举个例子,我之前写文章时想做一个"自动配图"的Agent。技能拆法如果按原语去做,需要"搜索图片API""筛选图片参数""下载图片""压缩图片""上传图床""拼接Markdown"六个技能,模型调用链条又长又容易断。后来我合并成两个技能:一个是"根据关键词推荐3张候选图片"(内部封装搜索、筛选、下载),另一个是"按指定样式插入图片"(内部封装压缩、上传、拼接)。模型的任务就变成先推荐后插入,决策链短了一半,成功率直接提升。

2.2 技能参数协议:用JSON Schema锁定边界

接口设计上我强烈建议不要用自由文本描述参数,而是用JSON Schema做严格校验。原因很简单:模型输出的参数经常是不规范的,你有Schema就能在入口处把非法参数挡掉,而不是让技能代码在运行时去猜。

我在实际项目里维护了一份类似这样的Schema定义:

{ "name": "search_web", "description": "搜索互联网信息,返回网页标题和摘要列表", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词", "minLength": 1, "maxLength": 100}, "top_k": {"type": "integer", "description": "返回结果条数", "minimum": 1, "maximum": 10, "default": 5} }, "required": ["query"] } }

Schema一旦定下来,就可以做三件事:第一,用代码自动生成调用的JSON示例,拼进Prompt给模型看;第二,在技能执行前做参数校验,不合法直接返回错误信息,让模型自己改;第三,把参数结构作为工具的"外部记忆"存起来,模型就不用靠猜。

这里有一个容易被忽略的细节:description字段一定要写清楚技能是干什么的、什么场景下才需要调用、参数代表什么业务含义。模型最终是读这个描述来做决策的,描述写得越精准,决策就越靠谱。我不会在描述里写"这个技能非常有用,推荐使用"这种废话,而是写"当用户表达想了解新闻资讯时,必须调用此技能获取实时信息,否则无法回答任何关于当前事件的问题"。

2.3 注册与发现:技能要有"说明书"也要有"菜谱"

技能数量一旦超过十个,靠人力去记住每个技能怎么用就不现实了。所以技能注册表(Skill Registry)非常重要。注册表里不仅存技能的元信息,还要存"使用场景示例"和"常用组合方式"。前者帮助模型理解什么情况下调这个技能,后者则负责告诉模型怎么把多个技能串起来解决问题。

我管这套做法叫"技能说明书+菜谱"。说明书是给模型看的单技能说明,菜谱是给模型参考的组合套路。举个例子,技能"generate_chart"的说明书会说"这个技能用于根据数据表格生成折线图、柱状图或饼图,数据格式为CSV或JSON数组",而菜谱部分会写"生成销售趋势图时,建议先调用query_database获取月度汇总,再调用generate_chart,最后调用save_markdown将图片插入文档"。

模型在思考时,注册表会按照相似度检索最相关的若干技能,连同菜谱一起拼入Prompt。这个设计解决了一个实际问题:当注册表中的技能有20个以上时,全量塞进Prompt会占据大量上下文,而且无关技能还会干扰模型判断,只放Top N相关技能就既省token又提升准确率。

3. 实操:一个可落地的技能插件方案

3.1 技能配置业结构定义

前面说的都是方法论,这一节我直接给一套可以照抄的结构。我在生产环境中用的技能目录长这样:

agent-skills/ ├── registry.yaml ├── skills/ │ ├── web_search/ │ │ ├── skill.yaml │ │ ├── handler.py │ │ └── schemas.py │ ├── db_query/ │ │ ├── skill.yaml │ │ ├── handler.py │ │ └── schemas.py │ └── report_generator/ │ ├── skill.yaml │ ├── handler.py │ └── templates/ ├── executor/ │ ├── router.py │ ├── validators.py │ └── sandbox.py └── registry_loader.py

每个技能目录里必配一个skill.yaml,它描述技能的所有关键信息:

name: web_search description: 用于搜索互联网公开信息,输入查询关键词,返回网页标题、链接与摘要列表 version: 1.2.0 author: ops_team parameters: query: type: string required: true description: 用户输入的搜索关键词 top_k: type: integer required: false default: 5 description: 返回结果条数,默认5条,最大10条 execution: timeout_seconds: 15 allow_parallel: false requires_auth: true permissions: network_access: true local_file_write: false shell_exec: false

这个YAML文件同时承担四个职责:注册信息的载体、参数Schema的载体、执行权限的声明、模型描述语料的来源。每加一个新技能,开发人员只需要照着模板写一个目录,注册表自动加载,Agent当天就能用上新能力。

3.2 执行器路由与沙箱控制

核心执行器是router.py,它负责接收LLM的输出,解析成标准的技能调用意图,然后做路由分发。这里我放一个简化但完整的示例:

# executor/router.py import json import importlib from typing import Any, Dict from .validators import validate_parameters from .sandbox import SandboxExecutor class SkillRouter: def __init__(self, registry: Dict[str, Any]): self.registry = registry def parse_intent(self, llm_output: str) -> Dict[str, Any]: """将LLM输出解析为技能调用意图。 兼容纯JSON和带标记语言两种情况。 """ raw = llm_output.strip() # 剥掉Markdown代码块包裹 if raw.startswith("```"): raw = raw.strip("`") if raw.startswith("json"): raw = raw[4:] try: return json.loads(raw) except json.JSONDecodeError as e: raise ValueError(f"无法解析LLM输出为JSON: {e}") def dispatch(self, intent: Dict[str, Any]) -> Any: skill_name = intent["skill"] params = intent.get("params", {}) if skill_name not in self.registry: return {"error": f"未知技能: {skill_name}"} entry = self.registry[skill_name] # 参数校验 errors = validate_parameters(entry["schema"], params) if errors: return {"error": "参数校验失败", "details": errors} # 加载handler handler = importlib.import_module(entry["handler_path"]) # 在沙箱中执行 result = SandboxExecutor(entry).run(handler.execute, params) return result

沙箱这块根据不同场景有不同实现。最简单的是依赖进程权限隔离,技能代码和Agent主进程同权限跑,适合可信脚本;严格一点的是放进Docker容器,只挂载必要目录,限制网络出口和文件系统;再严格一点是直接用宿主机上跑一个特制环境,用Linux capability机制控制网络和文件权限。

我在自用项目中采用的是受限subprocess + 超时机制,因为中间态最灵活。每个技能handler跑在单独的subprocess里,超过timeout_seconds直接杀掉,防止模型一次失误导致资源被占死。比同进程执行安全得多,又比开Docker轻量不少。

3.3 如何让模型"学会"调用技能

配置体系和执行器都就绪之后,还有一个关键环节:把注册表中的技能信息转化成Prompt的一部分。我用一个简单的拼接函数实现:

# registry_loader.py def build_skill_prompt(registry: Dict[str, Any], relevant_skills: list) -> str: lines = ["可用技能如下(只有声明过的技能才可以被调用):"] for skill_name in relevant_skills: entry = registry[skill_name] lines.append(f"- {entry['name']}: {entry['description']}") lines.append(f" 参数要求: {json.dumps(entry['schema'], ensure_ascii=False)}") if entry.get("examples"): lines.append(f" 示例调用: {json.dumps(entry['examples'][0], ensure_ascii=False)}") lines.append("请严格按照JSON格式返回调用意图,不要输出其他解释。格式: {\"skill\": \"技能名\", \"params\": {...}}") return "\n".join(lines)

不要小看这个拼接逻辑,"只有声明过的技能才可以被调用"这句约束非常重要,它直接框定了模型的可行动空间。同时我在Prompt里明确要求"不要输出其他解释",这句话能杜绝模型滔滔不绝地分析然后不给出JSON的常见毛病。如果某个技能返回参数校验失败,我会把具体错误原样喂回给LLM,让它自行修正后重试。这样等于把编译器报错机制引入Agent工作流,形成了自然的纠错循环。

4. 实测踩坑与排查手册

4.1 上下文爆炸:技能描述太长,模型反而"失焦"

我踩过的第一个大坑是技能描述嵌得太长。当时想着把每个技能的细节都写清楚,结果系统Prompt加上技能描述后体量超过5K token,模型处理复杂任务时表现惨不忍睹——经常还没开始干活,上下文就消耗过半。

后来我做了两个优化:一是给技能描述写了摘要,最多保留150字,细节塞进外部文档,需要时再检索;二是做技能召回而不是全量注入,每次只把最相关的5到8个技能拼进Prompt。实测下来,输出质量和token消耗都大幅改善。这个经验可以用一句话概括:Prompt里的技能不是越多越好,够用就好。

4.2 幻觉技能名:模型"一本正经"调用不存在的技能

这个问题出现过不止一次。模型在编造技能名,而且编出来的名字还挺像那么回事,比如我明明注册的是"query_database",它偏要给"read_mysql_data"。遇到这种情况,排查方向有两个:一个是技能描述不够清晰,模型没理解哪个技能才对口;另一个是示例太少,模型不知道该用哪个名字。

我解决的办法是双保险:一是在Discovery阶段限制候选技能范围,模型只能从可见的技能中挑;二是dispatch阶段对技能名做模糊匹配和别名映射。比如"query_database""read_mysql_data""执行SQL"都会归一化到同一个实际技能。每次误调用都会被记录成一条测试case,进测试集,回归时候防止复发,这个习惯帮我越做越稳。

4.3 并发与幂等:重试不是简单"再来一次"

Agent在生产环境里经常需要重试。但如果技能不是幂等的,重试就会造成重复执行、双倍扣费、重复发邮件之类的灾难。关于幂等,我有几条硬性建议:

所有非查询类技能,都必须带上request_id或idempotency_key参数,服务端按这个key做去重处理。比如技能"send_email",同一个request_id重复调用只发一次邮件;所有写操作技能,要明确在skill.yaml里标记allow_parallel: false,防止模型同时发起两个互相冲突的操作;所有耗时技能,必须以任务模式(异步+回调)暴露,不要阻塞Agent决策循环。

这里的痛苦是真金白银的教训——我在一个自动订购资源的系统里,因为重试逻辑没做幂等,模型把同一台服务器订购了三次,月底账单直接起飞。从那以后,我给自己立了个规矩:凡是技能涉及钱、权限、外发消息,必须默认非幂等不发布。

4.4 观测与审计:Agent出了问题,别靠猜

做Agent技能体系,观察能力跟不上,后面排坑会非常抓狂。我建议每个技能执行时都打结构化日志,包括:技能名、入参、出参、执行时长、是否成功、重试次数、消耗的模型token数、用户会话ID。所有这些汇总到一张表里,用SQL随时查。

我现在用的就是一套极简陋但好使的方案:日志打到本地JSONL文件,进程内做了个内存环形缓冲,配合一个自带的诊断命令,随时导出最近500条技能调用记录。发现问题后,按会话ID查全链路,一眼就能看出模型是从哪一步开始跑偏的。

审计这块同样重要。每个技能调用都会生成一条审计记录,存到独立的审计日志中。这不仅是防止模型"乱来"的最后一道防线,还能帮你在复盘时看清楚——哪些技能被频繁调用,哪些技能几乎没被用过,哪些场景下模型总是在多个技能之间来回横跳。这些数据才是优化技能设计方向的最客观依据。

5. 技能后续的扩展方向与我的个人体会

5.1 从技能到记忆:让Agent越用越懂你

技能体系稳定之后,很自然的下一个需求是"记忆"。技能负责执行,记忆负责感受。我目前给自己的Agent加了一层轻量的用户偏好记忆:每次任务完成后,把关键信息抽成结构化的偏好标签,比如"用户偏好Python而非JavaScript""用户认为成本和时效性比精确性重要""用户在周报场景下希望用bulleted list而非table"。这些偏好会在下次任务开始前被检索并注入Prompt。

这样做的好处是Agent的行为会越来越贴合特定用户的习惯。比如同一个"发送周报"技能,面对技术负责人时会用简短的要点式摘要,面对CFO时会先做财务指标汇总,后面再接详细图表。技能不变,但这些"软配置"让输出更聪明。这一步做起来不复杂,性价比却奇高。

5.2 组合技能与多级Agent:从单个步骤到业务流程

技能再多,最终还是要回答业务流程的问题。单个技能解决的是"查一下""写一段""发一封"这种单步任务,但真实世界的任务往往是多步的:"先调研市场对手,再生成竞品分析,最后发摘要邮件给团队"。我在实践中最有效的方案是搞一个组合技能,它内部编排若干子技能,但对外暴露成一个整体。组合技能执行过程中如果发现某个子步骤失败,它自己决定是否重试、换一条路径,或者终止并向上层返回错误。

配合组合技能的,是多级Agent架构。编排Agent负责理解高层目标,拆解成子任务,分发给多个工作Agent;每个工作Agent掌握对应领域的技能集,执行完成后把结果交还组织者。这套思路和团队工作模式完全一致:有人统筹、有人执行、坏结果层层上报,好结果逐级汇总。

5.3 我的几个真实体会

带技能体系的Agent做了一年多后,我最大的体会是:技能体系的设计更像建一套约束系统,而不是建一个自由发挥的大脑。真正稳定的Agent,恰恰是在边界内"戴着镣铐跳舞"的产物。模型越自由,就越容易偏离预期;边界定义得越清晰,执行结果就越可预测。把技能边界当成产品功能来设计,而不是当成一个简单的代码模块,会让整个Agent系统的稳定性和可靠性显著上一个台阶。

另外,对做技能框架的团队,我强烈建议把"技能正确率"纳入CI/CD流水线。我自建了一个很小的评测集,几十条标准问题,每改一次Prompt或技能代码就在评测集上跑一遍,保证"接服务器查日志"这个原本没问题的技能不会被一次改配置的优化给改坏。这种能力是短期见效慢但长期极其有价值的投资。有时间、有余力,就尽早搞起来。

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

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

立即咨询