☰
从提示词到技能库:Agent稳定落地的工程实践
2026/10/13 7:53:41 网站建设 项目流程

做 Agent 的朋友大概都会有同一种焦虑:Demo 跑得挺顺,一接真实任务就露馅。我上半年把好几个 Agent 项目拆开重做,最后都收敛到同一个设计——把 Agent 的“技能”从对话逻辑里剥出来,单独维护。agent-skills 这个名字,就是我给这套技能库起的内部代号。

它能做的事其实很简单:把 Agent 每一步可执行的操作,定义成结构化的技能;运行时不靠人肉的 if-else 分发,而是让模型根据任务描述去挑选技能、填入参数,再由统一的执行器去跑。它解决的核心问题是复用和失控,不再为每个新场景重写一套 Prompt,也能在 Agent 做错事的时候知道它错在哪一步。

这篇文章适合两类人:一类是自己搭 Agent 应用的开发者,一类是想给现有框架加技能体系的同学。下面的内容不依赖任何特定厂商的模型,只聊一套你能直接抄走的设计。

1. 为什么 Agent 需要一套技能库而不只是提示词

先把问题说透。很多人一开始做 Agent,都是在 system prompt 里写“你可以使用以下工具”,然后把函数名、参数说明、注意事项全部灌给模型。这种做法的优点是快,半小时就能跑通一个 Demo。但它有个很隐蔽的代价:所有能力都被塞进了上下文的同一层,模型要自己从一大段文字里找“现在该用什么”,一旦工具多了,它就开始乱。

我试过三十多个工具全部列在提示词里的情况。模型没崩溃,但每次调用前都要“回忆”一遍工具列表,响应变慢,选错工具的概率也明显上升。更麻烦的是,提示词里只要有一个格式错误,整个会话都会受影响;改一个工具的参数,其他工具的说明也要跟着重新检查。

1.1 提示词的复用上限

提示词的复用上限,比很多人想象中低。同一个功能,如果 A 场景和 B 场景对参数的要求不一样,你往往只能复制一份说明再改,于是维护成本开始成倍增加。到后来你甚至分不清某段描述是给哪个版本用的,线上出了问题也只能靠搜日志去猜。

生活里有个类比:把技能写进提示词,相当于把菜谱贴在便签上交给厨师;菜谱多了以后,厨师每次做菜都要翻一整叠便签,还容易把糖和盐看混。而技能库的做法,是给每道菜一个独立卡片,卡片上写明原料、步骤和验收标准,厨师按菜名查卡片就行。

换成工程语言,就是把“知道有什么能力”和“具体怎么做”拆开。模型只需要看到技能名称、简短描述、参数结构,真正执行时的业务规则放在外部代码里。这个拆法带来的直接好处是:技能可以单独测试、单独改版本、单独控制权限,不会互相污染。

1.2 技能模块解决的三类问题

我整理下来,agent-skills 主要是在解决三类问题。

第一类是复用。把高频操作收成技能后,不同 Agent 项目可以共用同一套技能库。比如搜索文档、读取数据库、导出报表,这些能力在不同项目里明明是一样的,但以前每换一个项目就要重新写一遍调用逻辑。现在只需要注册一次。

第二类是可控。技能运行之前,执行器可以统一做参数校验、权限校验、超时控制、日志记录。模型给了非法参数,不会直接触发一个危险的系统调用,而是会被拦下来,返回一条可读的错误信息。这一点在真实业务里几乎是刚需。

第三类是可测。技能是独立的纯函数单元,可以写单元测试、做假数据模拟、跑回归。对 Agent 做测试一直很头疼,因为对话流程不稳定;但把单个技能拎出来测就简单多了,只要输入输出符合约定,技能本身的质量就有了保障。

1.3 与常见框架的定位差异

现在很多 Agent 框架也自带工具调用能力,但它们的重心通常放在“如何让模型调工具”上,对“工具本身怎么组织”讲得比较少。agent-skills 的定位不太一样,它更像是一个偏后端的技能管理模块,用来定义技能的格式、注册方式、执行规范,至于最终是接对话系统还是接自动化脚本,都是上层的事情。

我自己的体会是:框架自带的工具机制适合快速踩点,但项目一旦进入维护期,还是需要一套自己的技能注册和治理方案。因为业务技能往往带权限、带审计、带重试策略,这些是框架默认配置给不了你的。与其事后打补丁,不如一开始就把技能层独立出来。

2. 技能库的核心设计:接口约定与数据模型

这一节是整篇文章的骨架。agent-skills 能不能用好,取决于你愿不愿意给每个技能定义一个足够清晰的接口。接口定得越稳,模型越不容易出错,调试也越省心。

2.1 一个技能到底包含什么

我给每个技能定义了七个字段:技能名、描述、参数结构、执行函数、权限、标签、示例。这七个字段不是拍脑袋定的,每一个都在实际调试中踩过坑。

字段作用对应问题
name全局唯一的技能标识模型靠它来发起调用
description告诉模型“什么时候用”描述不清晰会导致选错技能
parameters参数结构和类型约束缺少约束会导致运行时解析失败
execute实际执行逻辑承担具体的业务行为
permissions执行所需权限范围防止技能越权访问资源
tags分类标签技能多了之后方便检索和分组
examples参考调用示例帮助模型理解参数怎么填

字段看起来不多,但每个都值得单独推敲。Name 必须稳定,一旦技能被外部引用后改名,所有依赖它的编排逻辑都要改。Description 不能太长,但也绝不能糊弄。Parameters 是整个设计里最容易出错的地方,我后面专门讲。Permissions 在纯 Demo 阶段可以忽略,只要技能开始碰文件、数据库、外部接口,就必须补上。

2.2 参数描述用 JSON Schema 而不是自由文本

这一点是我调整最大的一次。早期版本里,我用自然语言写参数说明,比如“query 是搜索关键词,top_k 是返回条数”。结果模型填参数时,偶尔会给 query 传一个对象,或者把 top_k 传成字符串,执行器一调用就崩。

后来我把参数结构改成 JSON Schema,用标准格式描述字段名、类型、是否必填、取值范围。例如一个搜索技能的参数可能是这样:

{ "type": "object", "properties": { "query": { "type": "string", "description": "搜索关键词,尽量简洁" }, "top_k": { "type": "integer", "minimum": 1, "maximum": 20, "default": 5, "description": "返回结果数量" } }, "required": ["query"] }

改用 JSON Schema 之后,有两件事自动变好了。第一,模型在生成调用请求时更规范,因为它能直接看到结构化约束。第二,执行器可以在执行前做一次严格的参数校验,不合法就直接给模型返回“参数错误”的提示,而不是让底层函数抛出一个莫名其妙的异常。

在定义参数时我有几条自己的规则:能不用嵌套对象就不用嵌套对象,扁平结构对模型更友好;所有数字字段尽量给最小值和最大值;所有字段都要写 description,哪怕是布尔值;required 里只放真正必填的字段,不要为了省事全都设为必填。

2.3 技能注册表:运行时发现的目录

技能库的核心是一个注册表,它本质上是“技能名到技能对象”的映射表。所有技能在模块加载时把自身注册进去,执行器收到模型调用请求后,在这个表里查名字,查到了就执行,查不到就返回错误。

注册表设计得好不好,直接影响扩展体验。我采用的方式是装饰器注册,在 Python 里看起来是这样的:

@skills.register( name="search_docs", description="在内部文档库里搜索相关资料,适合回答文档相关问题时使用", parameters={...}, permissions=["docs:read"], tags=["search"] ) def search_docs(query: str, top_k: int = 5) -> str: # 实际搜索逻辑 return result_text

这样做的好处是,新增一个技能只需要写一个普通函数,加一行装饰器,不需要去改执行器,也不需要动注册表中心。注册表自动把函数元信息收集起来,统一转成模型能理解的工具描述。技能删除也很简单,移掉装饰器就行。

注册表还需要防止命名冲突。如果两个技能同名,加载时应该直接抛错,而不是让后加载的覆盖前者。这个坑我踩过:有一次两个模块注册了同一个技能名,后加载的把前面的覆盖了,导致线上行为异常,日志里却看不出任何报错。

2.4 组合技能与原子技能

技能库里不一定要全都是最底层的操作。我习惯把它分成两层:原子技能和组合技能。

原子技能对应一个不可再拆的操作,比如“查询订单”“读取文件”“发送邮件”。组合技能则是把多个原子技能串起来,比如“处理每日销售报表”可以拆成查询订单、汇总金额、写文件、发送通知。组合技能的好处是,模型不需要为了一个固定流程发起多次调用,只需要调一次,内部逻辑就帮你把后续步骤都编排好。

实现组合技能时,我建议不要在 execute 函数里直接调用另一个技能的 execute,而是调用同一个注册表。因为注册表里保存了权限和日志信息,通过注册表调用可以完整记录调用链,排查问题时能看到“组合技能 A 在执行过程中,依次调用了原子技能 B 和 C”。如果直接调用底层函数,日志链就断了。

3. 从零实现一个最小可用技能库

光讲概念不够,我直接给你看一个可以跑起来的最小实现。这个版本我只保留核心逻辑,方便看懂数据是怎么流转的。

3.1 项目目录与模块划分

我喜欢把技能库拆成四个模块:注册表、执行器、技能定义、模型调用循环。目录结构大致是这样:

agent_skills/ registry.py # 注册表与装饰器 executor.py # 参数校验与执行入口 skills/ search.py # 具体技能 report.py # 具体技能 loop.py # 模型调用循环

模块划分的目标是让职责单向依赖:loop 依赖 executor,executor 依赖 registry,skills 里的技能函数只和业务逻辑打交道,不关心模型怎么调。这样以后想换模型、换调用方式,技能不用动。

3.2 技能注册与加载

先看注册表和技能定义的简化写法:

# registry.py from dataclasses import dataclass, field from typing import Callable @dataclass class Skill: name: str description: str parameters: dict execute: Callable permissions: list = field(default_factory=list) tags: list = field(default_factory=list) examples: list = field(default_factory=list) class Registry: def __init__(self): self._skills = {} def register(self, name, description, parameters, **kwargs): def decorator(func): if name in self._skills: raise ValueError(f"duplicated skill name: {name}") self._skills[name] = Skill( name=name, description=description, parameters=parameters, execute=func, **kwargs ) return func return decorator def get(self, name): return self._skills.get(name) def all_skills(self): return list(self._skills.values())

注册表本身不复杂,复杂的是注册顺序和命名冲突。装饰器里检查同名技能,就是为了保证每个技能名在全局唯一。我还会在注册时顺便检查 execute 是否是可调用对象、parameters 是否有 required 字段,提前暴露配置错误。

3.3 模型调用循环

执行循环是 Agent 和技能之间的桥梁。这里用一个极简版本演示:模型决定要不要调用技能,执行器执行完把结果回传给模型,模型再继续生成答案。

# loop.py def run_agent(task, registry, call_model, max_steps=5): messages = [{"role": "user", "content": task}] for _ in range(max_steps): response = call_model(messages, skills=registry.all_skills()) if response.get("finish"): return response.get("answer") skill_name = response["skill_name"] raw_args = response["arguments"] skill = registry.get(skill_name) if skill is None: messages.append({ "role": "tool", "content": f"技能 {skill_name} 不存在" }) continue try: result = executor.execute(skill, raw_args) except Exception as exc: result = f"执行失败: {exc}" messages.append({ "role": "tool", "content": result }) return "超过最大步数,请简化任务"

真实项目里,模型接口返回的结构会和这里有差异,比如有些模型会一次性返回多个技能调用。但核心思想是一致的:不断把执行结果追加到对话里,直到模型给出最终答案。每个循环都有步数上限,防止模型陷入死循环。

3.4 一次完整的执行流程

举一个实际例子。假设终端用户提问:“帮我查一下上周五的订单数量,并按金额倒序输出前五条。”

技能库里有三个技能:search_orders、sort_orders、format_report。模型看了任务描述后,第一步大概率会调用 search_orders,参数可能是{"date": "2025-06-13", "limit": 20}。执行器校验参数后,执行函数把订单数据返回成文本:共找到 18 条订单,明细: ...。

模型拿到这一步结果,发现还需要排序,于是再调用 sort_orders,参数{"sort_key": "amount", "order": "desc"}。执行器返回排序后的订单列表。最后模型调用 format_report,生成用户能直接阅读的表格文本。

整个过程中,模型只负责决策“用什么技能、填什么参数”,具体的数据查询、排序、格式转换都在技能内部完成。这也意味着,即使模型换了一个供应商,技能执行结果依然稳定,因为业务逻辑没变。

4. 几处容易翻车的实现细节

概念和 Demo 都看完了,下面这部分是真正让我在项目上花掉最多时间的几个细节。它们不会第一眼就出问题,但会在你上线后慢慢折磨你。

4.1 不要把 description 写成说明书

我见过很多新手写 description,会写得很长,恨不得把实现细节都写进去。比如“本技能用于搜索文档,会连接数据库,查询条件是 query 字段,支持模糊匹配,返回结果包含标题、摘要、链接,注意网络超时场景下会重试两次”。这种描述对模型来说信息量太大,反而干扰决策。

正确的做法,是把 description 的中心放在“什么场景下应该选我”,而不是“我怎么实现”。一个比较理想的模式是:先一句说明功能,再说适合什么场景,最后补一句不适合什么场景。例如:

  • 推荐:在回答文档相关问题、需要引用内部资料时使用。
  • 不推荐:当用户只是闲聊,不需要查资料时,不要调用本技能。

模型选择技能的机制很像搜索引擎:query 是当前对话意图,技能描述是索引条目。索引写得越准确,匹配越容易命中。索引写得再华丽,如果和真实意图错位,照样白搭。

4.2 参数校验要交给实际执行器,而不是模型

模型再聪明,也偶尔会传错参数。你不能指望模型每次都能按照 JSON Schema 填出完全合法的参数。所以在执行前,执行器必须用 jsonschema 库或等价逻辑做一次硬校验。

在校验失败时,返回给模型的错误信息也要设计。不要直接返回KeyError: 'date'这种底层信息,模型读不懂,也没法修正。比较好的方式是返回结构化提示,例如:

参数校验失败:缺少必填字段 date。可选字段包括 date, status, limit。请修正后重试。

这里有个小技巧:尽量把 JSON Schema 里的 required 字段和错误提示联动起来。工具函数可以自动化生成“缺少什么字段”的提示,不用每个技能手写。我封装过一个简易校验器,内部捕获校验错误,把错误原因格式化成上面这种模型友好文本,再返回给 Agent。

4.3 超时、并发与幂等

技能执行是真实代码,不是纸面定义。只要技能涉及外部调用,就必须考虑超时。我给每次 execute 都加了统一超时机制,默认 10 秒;超过时间就中断,并把“执行超时”作为结果返回给模型。否则模型会一直等一个永远不会返回的技能,整个 Agent 流程卡死。

并发问题容易被忽略。多个 Agent 同时执行同一个技能时,如果技能内部操作了共享文件或数据库,会互相干扰。我的处理方式是在技能函数上增加一个可选参数request_id,由执行器自动注入。每次执行都把 request_id 写到日志,对写操作尽量使用带业务唯一键的幂等写法,重复执行不会产生重复数据。

还有一个经验:不要在技能执行函数里做无限等待的循环。如果你发现某个技能经常卡住,排查时优先看它是不是在等锁、等网络、等用户输入。把超时控制做在外层,比在技能内部到处加判断要省事得多。

4.4 安全边界:权限与操作白名单

技能一旦可以执行外部行为,权限问题就不是“以后再说”,而是“现在必须想”。我给每个技能都加了一个 permissions 字段,执行器在调用前会检查当前运行环境是否拥有对应权限。

比如搜索技能需要docs:read,发邮件技能需要mail:send,写文件技能需要fs:write。在测试环境里,我会用一个宽松的权限文件;在线上环境,只放最小权限子集。这个设计和操作系统的权限模型很像,区别在于执行者是 Agent,是一个由模型驱动的进程,你没法预测它下一步会调用什么,所以只能在权限层把边界卡死。

更严格的场景,还应该把能执行外部命令的技能单独隔离到沙箱里。我的原则是:凡是技能内部出现os.system、subprocess.run、路径拼接、数据库写操作,都要经过人工审批后才允许注册。宁可少一个功能,也不要让模型拿到一把没有保险栓的枪。

5. 常见问题与排查实战

这部分是我从大量调试记录里整理出来的问题清单,每个都真实发生过。为了方便阅读,先放一个速查表,再讲几个典型的案例。

现象可能原因排查思路
模型老是选错技能技能描述不准确;候选技能过多;缺少示例精简 description;减少每次暴露的技能数;补充 examples
技能执行成功但 Agent 不认返回格式不规范;结果被截断;状态信息缺失统一返回 status/data/error 三件套
参数校验报错反复出现JSON Schema 约束错误;模型不会按格式生成检查参数结构;在 examples 中给合法调用示例
技能越来越多,加载变慢注册时扫描太重;技能列表太长按需加载;按标签分组暴露
一个 Agent 卡住影响其他进程技能执行时间过长;共享资源锁统一超时;引入 request_id 隔离日志

5.1 模型老是选错技能怎么办

选错技能是最常见的问题,也是最让人血压升高的。有一次用户问“订单退款到账了吗”,模型没有选“查询退款进度”,而是选了“查询订单列表”。原因是我给这两个技能写的 description 高度相似,模型根本分不出来。

后来我做了三件事。第一,重新打磨 description,让每个技能有一个明确的“区别于其他技能的关键词”。退款进度技能里写上“包含退款申请时间、审批状态、到账时间”;订单列表技能里写上“只返回订单汇总信息,不包含退款进度”。第二,在 examples 里给出真实场景下的调用参数,模型看到示例后更容易做匹配。第三,每次调用时不要再把所有技能都暴露给模型,先根据标签筛选出候选集合,把无关技能过滤掉。

我实测下来,候选技能数量控制在五到八个效果最好。太多会分散注意力,太少又担心漏掉正确选项。筛选逻辑可以很简单:把任务内容里的关键词和技能 tag 做匹配,优先暴露高相关的。

5.2 技能执行成功了,Agent 却认为失败了

这个问题最隐蔽,因为后端明明拿到了正确结果,但模型在最终回答里说“查询失败”。我第一次遇到时以为是模型幻觉,后来才发现是我返回给模型的字符串太随意。

比如我返回的字符串是18,模型不知道这个数字代表什么,于是怀疑过程没走完。解决办法是把技能返回值改成结构化文本,至少包含状态和数据:

status: success data: 最近一笔订单金额为 199 元,支付时间为 2025-06-10 14:30。

如果有异常,则返回:

status: error error: 订单编号不存在

模型看到 status 字段后,能非常确定地判断是否成功。如果你想再细一点,可以加一个next_action_hint,比如“你可以继续引导用户查看物流信息”,帮助模型在拿到结果后知道下一步该干什么。

5.3 技能越来越多以后怎么维护

技能数量超过二十个以后,维护成本会指数级上升。如果你不做分类、不做测试,最后一定会失控。我的做法是有新技能加入时,必须同时提交两样东西:一条真实场景的测试用例,一段技能说明文档。

维护工具上,我写了一个小脚本,遍历注册表并生成一个 Markdown 索引,包含技能名、描述、tag、权限、最近修改时间。每次更新索引,我就能看到哪些技能长期没被调用、哪些技能权限过宽、哪些技能描述已经过时。这比在源码里翻注册装饰器直观得多。

技能测试也建议自动化。给每个技能准备一套模拟输入,直接调用 execute 函数,校验返回结果。不需要跑完整的 Agent 对话,只需要跑单个技能,就能在早期发现参数结构变化、依赖缺失、返回值格式不兼容等问题。

6. 沉淀下来的几条心得

项目做到后面,我对 agent-skills 的期望已经不是“跑通”,而是“稳定可维护”。这一步靠的不是某一个漂亮设计,而是每一步都愿意多花十分钟去加固。

6.1 我给新技能定下的验收标准

现在新增技能时,我都会按固定清单审查一下:参数结构是不是 JSON Schema;描述里有没有写出适用的用户意图;示例参数是否真实可用;执行函数有没有设置超时;权限字段是否最小化;返回值是否包含 status。清单看着琐碎,但它能挡住大部分线上问题。

有一次我想加一个“读取服务器日志”的技能,功能很简单,一行代码就能实现。但过清单时发现,这个技能没有做路径白名单。我补上“只允许读取日志目录下的文件,不允许任意路径”之后,才觉得它可以安全注册。这份谨慎,是踩过几次日志泄露的坑换来的。

6.2 可以继续扩展的方向

技术选型和架构上,还有几个方向可以继续做。一个是技能自动生成,让模型基于函数签名和文档生成 JSON Schema 和 description,减少手写成本。另一个是技能热更新,通过独立进程动态加载、卸载技能,不改主程序。还有一个方向是技能缓存,对相同参数的结果做短期缓存,可以减少外部接口压力。

这些方向不急着一口气做完。最优先的还是前面那些基本功:规范、校验、权限、日志。基础不稳,扩展越多,坑越深。

6.3 给刚开始做技能库的同学一点建议

如果你想在自己的项目里也搭一套技能库,我建议不要一上来就铺开几十个技能。先把最高频的三五个操作做好了,跑通注册、执行、日志这一整条链路,再慢慢加。技能少的时候改设计成本很低,技能多了以后再重构,会非常痛苦。

我个人现在的习惯是:每加一个新技能,都在真实任务里跑一遍,再回到 description 和 examples 里调整措辞。这个过程有点像给搜索引擎调索引,不是一个纯代码问题,而是一个需要观察模型行为的问题。

踩过几次坑之后,我越来越认同一个观点:Agent 项目的成败,很多时候不是模型不够聪明,而是工程的边界够不够清楚。agent-skills 这个方向我会继续往下做,因为它解决的,恰恰是我自己每天都会遇到的真实痛点。

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

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

立即咨询