☰
从提示词到技能库:AI Agent 标准化能力的设计与实践
2026/10/14 9:17:04 网站建设 项目流程

大语言模型应用做多了以后,你会发现一个特别矛盾的现场:模型能力很强,但落地到具体任务上,光靠一个提示词很难搞定复杂的多步操作。我最近在重构自己的一个自动化项目时,把之前零零散散封装的功能统一整理成了一套技能库,项目代号就叫agent-skills。这个思路本质上不是新造轮子,而是把“给模型写提示词”升级成“给代理装备标准化能力”,让大模型代理(agent)能够像积木一样组合、复用、编排这些技能模块。

这篇博文我会从为什么需要技能体系开始,讲到技能定义结构、注册调度机制、与大模型交互的完整链路,最后分享一下调试过程中踩过的问题。如果你正在做自动化任务、智能客服、个人助理类项目,或者已经尝试过写各种复杂提示词但觉得维护起来痛苦,这篇内容应该能帮上忙。

1. 整体设计与思路拆解

1.1 为什么不用巨型提示词

前一段时间我接了一个内部知识库问答的辅助工具,需求不复杂:根据用户问题检索文档、总结要点、按指定格式输出。最开始我确实就是写了一段很长的系统提示词,把检索逻辑、总结逻辑、格式要求全塞进去。运行一段时间后发现一个致命问题:一旦用户问题稍微偏离预设场景,模型就开始“自由发挥”,要么跳过检索直接把总结编出来,要么格式跑偏。而且每次改一个细节,整段提示词都要跟着动,连带测试也要重跑。

后来我换了一个思路:不要试图让模型在一个回合里把事全做完,而是把整个任务拆成多个独立能力模块,每个模块只负责一件事,比如“检索文档”“提取关键信息”“按模板生成报告”。这些模块要有统一的输入输出接口,还要能被模型按需调用。这就是agent-skills的雏形。

拆解之后的好处非常明显。首先是提示词长度大幅下降,每个技能内部只需要聚焦各自的指令,模型理解起来更准确;其次是可测试性提升,每个技能可以单独用样本验证,不需要等整体链路跑通;最后是复用性,之前调研了十几个项目的技能需求,发现很多低频功能可以抽象成公共技能,新项目直接注册进来就能用。

1.2 技能体系的核心设计目标

在设计这套技能库时,我给自己定了几个硬性目标,后续所有实现都围绕这些目标展开:

  • 标准化:所有技能必须有统一的元数据结构、输入输出协议,方便注册、检索和组合。
  • 可插拔:技能之间不互相依赖,单个技能可以独立启用、停用或升级。
  • 可观测:每次技能调用都要有日志和中间结果,定位问题时不能靠猜。
  • 模型无关:技能本身不绑定具体的大模型服务,上层切换模型时不影响已注册技能。

这里特别说一下模型无关这件事。实际开发中,很多人会直接把技能定义写成某个模型专用的 JSON 格式,或者依赖某个厂商的特殊参数。一旦换了模型,整个技能库就废了。我的做法是把技能逻辑拆成两层:一层是模型交互层,负责调用大模型服务并解析流式输出;另一层是纯业务技能层,只接收和返回标准结构。两层的耦合度降到最低,换模型时只用改交互层的适配器。

1.3 与工具调用、工作流引擎的区别

市面上其实已经有不少工具调用(function calling)方案,也有一些工作流引擎产品,那agent-skills和他们到底有什么区别?我的理解是:工具调用解决的是“模型如何触发外部函数”的协议问题,工作流引擎解决的是“多步骤如何编排”的执行问题,而agent-skills更像是一个介于两者之间的能力管理框架。

工具调用通常只给模型一个函数清单,模型选择函数和参数,然后系统执行。这很好,但如果你有上百个工具,函数清单本身就会让模型混淆,而且缺少版本管理和依赖关系。工作流引擎则往往是图形化编排,适合固定流程,灵活性不够。agent-skills的设计重点放在技能描述的组织、调用权限的控制、以及技能之间如何协作,算是把工具调用升级成了一个结构化的技能市场。

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

2.1 技能定义的标准结构

我花了不少精力设计技能定义的字段,最后稳定下来的版本大概长这样:

skill: id: doc_search name: 文档检索 version: 2.1.0 description: > 用于在已接入的知识库中执行关键词或语义检索, 返回最相关的片段列表及对应文档来源。 keywords: [文档, 搜索, 知识库, 检索] author: internal tags: [rag, retrieval] input_schema: query: { type: string, description: "检索关键词或自然语言问题" } top_k: { type: integer, default: 5, description: "返回片段数量" } output_schema: results: { type: array, description: "命中片段列表" } dependencies: - embedding_provider permissions: - read_knowledge_base

字段不多,但每个都有讲究。description是给模型看的“技能说明书”,我实验过很多写法,摘要式描述的效果远好于长篇大论。keywords用于本地检索技能的候选匹配;input_schema和output_schema不光做校验,还会在构造模型输入时参与生成函数签名;dependencies声明技能运行需要哪些基础设施;permissions用来做调用前鉴权,避免模型误操作敏感功能。

2.2 技能描述如何写模型才更愿意用

这一条是最容易被忽略但痛感最强的。同样一个技能,描述写得好不好,直接影响模型能不能正确调用。我发现有几个实用的口径:

  • 明确说清楚“什么时候用”,例如“当用户询问合同中的违约责任条款时,使用该技能”。
  • 避免过于宽泛的词,像“提供信息”这种等于没说。
  • 把输出约束写进描述里,例如“仅返回文档中明确存在的内容,不要补充外部知识”。
  • 如果技能是组合型,可以在描述里列出推荐的串联顺序。

我自己踩过的坑是:某次给一个技能写了很详细的接口参数示例,结果模型每次调用都照着示例传参,哪怕遇到明显不同的场景也不变通。后来我把示例改成最小化形式,并在描述里增加了一句话“参数请基于当前对话的具体信息生成”,问题立刻缓解。这个现象说明模型在技能调用时存在很强的模式匹配倾向,描述字面内容会在潜意识里限制它的自由度。

2.3 输入输出 schema 的校验与容错

技能接口的 schema 校验我一开始做得非常严格,参数类型、必填项、枚举值一个不满足就报错。实际跑起来发现问题很大:模型生成的参数偶尔会出现 JSON 格式错误、字段拼写错误、类型轻微不匹配。如果直接报错,整体任务就中断了,体验非常差。

后来我调整了策略,把校验分为两层:第一层是软校验,遇到小问题自动修复,比如把字符串"3"转成整数 3、去掉多余的空白字符、补上缺失的可选字段;第二层是硬校验,只有当必填参数缺失或者语义完全不符时才拒绝调用。为了尽量减少模型参数错误的概率,构造函数签名时要让参数名自解释,比如用search_query而不是param1。

输出端的校验同样重要。模型输出的结果经过结构化解析后,需要验证output_schema中的字段是否存在、类型是否正确。如果输出不符合结构,可以让模型重新生成一次,但要注意重试次数限制。我在实践中发现,允许一次自动重试通常就够了,多次重试不仅浪费 token,还有可能把正确的回答改成错的结构。

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

3.1 技能注册中心的设计

技能注册中心是整套体系的骨架,一个最简单的实现可以用纯 Python 写一个注册表,但是为了后续扩展,我直接用了一个 SQLite 存储注册信息,包括技能 ID、版本、启停状态、更新时间等。注册中心的主要功能有四个:

  • 注册新技能:把技能定义写入存储,并做格式校检。
  • 更新技能版本:保留历史版本记录,方便回滚。
  • 查询技能列表:支持按标签、关键词、ID 精确查询。
  • 动态加载技能:技能业务逻辑以模块路径形式登记,调用时动态导入。

我特意选择了“定义与实现分离”的方式:YAML 文件描述技能元信息,而实际执行的函数是普通的 Python 方法。注册中心只保存 YAML 内容和实现模块的引用路径,运行时才真正加载。这样技能库可以共享给不同项目,而每个项目可以有自己的实现适配版本。

3.2 大模型如何调用技能

模型调用技能的核心是函数描述生成。我以某通用的函数调用协议为例,系统先把所有可用技能转换成 JSON Schema 形式:

{ "type": "function", "function": { "name": "doc_search", "description": "用于在知识库中执行检索,返回相关片段。当用户问题需要依据具体文档内容时使用。", "parameters": { "type": "object", "properties": { "search_query": { "type": "string", "description": "检索词或自然语言问题" }, "top_k": { "type": "integer", "description": "返回数量" } }, "required": ["search_query"] } } }

实际操作中,我并不是把所有技能都一股脑传给模型,而是在对话开始前先用本地检索做一轮候选筛选,找出最相关的 5 到 10 个技能,再作为函数清单传给模型。如果技能数量很少,自然不需要这个环节;但技能库一旦超过 30 个,模型在大量函数中做选择时准确率会明显下降,候选筛选是必须的。

当模型返回一个函数调用请求时,系统会走一个统一的执行入口:先校验调用是否在权限范围内,然后加载实现模块,再执行函数并捕获返回值。执行结束后,返回值会被格式化成工具消息重新传给模型,让模型基于结果生成最终回复。如果执行过程中抛异常,也需要把错误信息结构化传给模型,让它知道刚刚的尝试失败了,可以换一种方式再试。

3.3 多技能协作的编排方式

技能编排是agent-skills里最有意思的部分。我试着支持三种模式:

  • 顺序执行模式:上一个技能的输出直接作为下一个技能的输入。适合“检索然后总结”这种固定流程。
  • 条件路由模式:根据特定字段的值决定走哪个分支,例如如果检索结果为空,就切换到“通用问答”技能。
  • 自主规划模式:只给模型一个总目标和一个可技能列表,让模型自己生成调用序列。

让我用一个实际场景说明。之前给某公司做的周报自动生成工具,一共注册了四个技能:get_events、analyze_trend、summary_report、format_markdown。整体是一个顺序执行流程,但analyze_trend内部条件判断了事件数量过少时跳到备用策略。这个流程我用 YAML 描述如下:

workflow: id: weekly_report steps: - skill: get_events output_var: events - skill: analyze_trend input: events: ${events} output_var: analysis on_empty_events: skill: fallback_policy - skill: summary_report input: analysis: ${analysis} - skill: format_markdown

你会发现这个描述里没有大模型参与编排,是纯规则化的。原因是我在实验中发现,对于业务上已经明确的步骤,用规则编排更稳、更快、更便宜。自主规划模式更适合探索阶段的任务,一旦探索清楚流程,就应该固化成规则,而不是继续让模型每次重新规划。这个思想贯穿了我后来所有 agent 项目的设计。

3.4 与外部系统对接的适配层

没有哪个 agent 是孤立存在的,技能一定会访问数据库、文件系统、第三方接口等。为了不把外部系统的复杂性泄漏到技能里,我在所有实现模块外面包了一层适配层,主要做了三件事:

  • 统一身份传递:技能调用方传入上下文对象,里面包含请求 ID、用户 ID、会话 ID,适配层据此做数据隔离和审计。
  • 统一错误分类:外部异常全部转成三类型之一:临时故障(可重试)、客户端错误(参数问题)、权限错误(无访问权限)。不同类型对应不同的处理策略。
  • 统一超时与重试:对外部接口调用设置默认超时和幂等重试机制,避免一次网络抖动导致整个任务失败。

这个适配层虽然不是技能库本身的一部分,但是没有它,技能库很难用在生产环境。我见过不少项目,技能定义写得漂亮,一接真实数据就各种超时、权限报错,都是因为没有做这层保护。

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

4.1 模型在多个相似技能之间选错的处理

这是我在技能库接近 20 个时开始遇到的问题。比如同时有search_doc、search_web、search_code三个技能,描述里都写了“搜索获取信息”,模型经常选错。我实验过几种方案,最终有效的是:

  • 在每个技能描述的结尾都加上一段“不适用”条件,例如“仅当需要检索代码仓库内容时使用,日常百科类问题请勿使用”。
  • 把高频场景的动作动词统一,避免一个技能用 query,另一个用 search,另一个用 get。
  • 候选项裁剪时保留描述语义上确实有差异的技能,让模型的选择面缩小。

这个方法分享出去后,有几个同行反馈说有效。思路其实很简单:模型不是不会选,而是信息不足时很难做消歧。与其让它猜,不如在描述里直接告诉它排除项。

4.2 技能调用参数幻觉问题

另一种常见问题是模型捏造参数值。比如检索技能需要的参数是search_query,但模型传了一个文档里根本没有的术语,而且编得有模有样。这个问题的根源往往在于技能描述里举例太具体,或者系统提示词里用了一些让模型“填空式创作”的文字。

我的排查步骤是:先看调用日志里的对话上下文,确认参数来源;如果模型是根据用户原话生成的,那就没问题;如果是凭空构造,需要在技能描述里明确加上“参数必须基于用户消息中的原始信息,不得自行补充”。如果模型还是会犯,就在输入 schema 的字段描述里追加“如果用户未提及该字段,不要尝试推断”。

这些手段不能 100% 杜绝问题,但能显著降低概率。生产中我还会对参数做一些后置校验,例如检索词不能包含特殊符号,文本长度不能超过某阈值。

4.3 调用返回结果格式化失败的应对

大模型生成的 JSON 结果经常出现小毛病,比如多了一个尾逗号、字符串里有未转义引号、或者整个输出被 markdown 代码块包住。我在解析层做了一个容错解析函数,先用 JSON 库正常解析,如果失败就尝试剥离代码块标记再解析,再不行就用正则抽取 JSON 片段最后兜底。

兜底策略虽然是野路子,但实测保存了很多次任务。这里说一下边界:如果多次解析失败,就不要继续死磕了,直接给模型返回“解析错误”的反馈,让它重新生成结构正确的输出。重试的时候我会刻意保留原始对话历史,只修正格式要求,这样模型能基于之前的思路重新组织输出,成功率更高。

4.4 性能优化与 token 成本控制

技能体系用多了以后,最大的隐藏成本在于系统提示词、函数定义、历史消息的重复拼接。每次对话都要传完整技能列表吗?显然不是。我做了个缓存方案:按会话维度缓存技能定义和系统提示词,只有技能版本更新时才刷新。另外,候选技能筛选之后,函数定义总量被控制在大概 3000 token 以内,太多反而影响模型表现。

另一个成本控制技巧是:技能执行返回的长文本不直接塞回历史消息,而是先做摘要,再把摘要传给模型。比如检索返回了 10 个片段共 4000 token,我会先让一个轻量模型生成 200 token 的摘要,同时保留原文的存储地址,需要时再拉取。这个方法在节省 token 上立竿见影。

5. 技能库迭代的经验总结与后续扩展建议

5.1 从实际项目中发现技能需求

最后聊一点方向性的经验。不要一开始就追求技能库的完备,而是从实际项目里逐渐沉淀增量。我最初的 3 个技能都是从某个自动化脚本里拆出来的,后来随着项目数量增加,技能才慢慢扩展到 20 多个。每新增一个技能,都尝试先以参数化配置方式运行,观察是否稳定高效,再决定是否固化成独立技能。

这个过程很像搭积木:先做最小可用版本,再通过实际使用中的反馈补齐能力。不要一开始就在工程文档里设计几十个技能,那样大概率有一半根本不会被调用,还会白白增加模型每次调用的干扰项。

5.2 几个值得深入的方向

现在这个技能体系已经足够支撑我自己的自动化项目,后续我想尝试几个方向:一是给技能库加一个基于真实调用日志的自动评估模块,定期跑一遍测试集,观察各技能在不同模型版本下的调用准确率变化;二是支持更复杂的依赖注入,让技能可以动态获取上下文,而不是所有参数都靠模型传值;三是把技能版本管理与提示词版本管理统一起来,模型更新后能自动选择兼容的技能版本。

这些方向都是实践中自然生长出来的需求,我也在不断调整。如果你也正在做类似的事情,建议先从一个小范围的技能库跑通闭环,再逐步扩展。技能不是越多越好,而是每一个都得经得起真实任务考验。

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

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

立即咨询