大模型Agent技能体系:从Prompt堆工具到可注册可编排的插件化架构

发布时间:2026/10/22 22:02:00
大模型Agent技能体系:从Prompt堆工具到可注册可编排的插件化架构
先聊聊我为什么会对“agent-skills”这个方向投入这么多精力。过去半年我一直在做基于大模型的智能体项目最开始的版本非常朴素把工具函数一股脑塞进System Prompt再配上几段工具说明让模型自己挑着用。能跑通但维护起来极其痛苦——每个场景都要重写大段提示词新增一个能力要全局改动技能之间还会互相干扰。后来我发现问题的根源在于我把“能力”和“调用”混在了一起没有抽象出“技能”这一层。这才有了“agent-skills”这套体系把Agent能做的事封装成独立、可注册、可编排的技能单元让大模型通过统一的接口去发现和使用它们。这篇文章我从设计思路、核心架构、实操落地到排查技巧完整拆解一遍。适合两类读者一是正在做大模型Agent应用、但还在用“Prompt堆工具”方案的开发者二是想把现有Agent改造成“技能插件化”架构的团队。内容不会堆概念全部来自我实际跑通、反复迭代过的代码和踩坑记录。1. 项目整体设计与思路拆解1.1 先从“技能”这个词说起在Agent语境里“技能”Skill不是一个花哨的概念它本质上是一次能力封装。人的技能是“知道怎么做某件事”的本领Agent的技能则是“能执行某个操作”的代码单元。它把输入参数、执行逻辑、输出格式、依赖条件全部封装在一个标准接口后面大模型不需要理解内部实现只需要知道“这个技能做什么、需要什么参数、能返回什么结果”。这个抽象的价值在于它把Agent的“大脑”推理决策和“手脚”执行能力解耦了。大脑负责理解用户意图、制定计划、调用合适的技能手脚负责具体执行、返回结果。没有这一层抽象你只能把工具描述写进Prompt让模型做“一次性的工具选择”而有了技能层模型做的是“基于技能清单的策略决策”二者完全不在一个量级。我见过很多团队在Agent雏形期就踩进一个误区把工具函数直接作为函数注册给模型确实单轮调用体验很好但一旦任务复杂化——需要异步执行、需要技能组合编排、需要依赖前一个结果决定下一步——这种“裸函数式”设计立刻崩盘。技能层天然适合做这些事因为技能之间可以组合、可以依赖、可以共享上下文。1.2 为什么不能继续用“Prompt堆工具”最直接的痛点有三个。其一Prompt长度失控。每个工具说明平均200到400字挂5个工具就要多出近2000字。一旦技能数量超过10个模型就开始“注意力涣散”优先调用描述靠前或措辞生动的工具而不是真正合适的那个。技能体系里模型看到的是技能清单列表——名称、一句话简介、参数摘要——整体开销大幅压缩。其二工具逻辑分散。没有技能层时工具的鉴权、参数校验、错误重试、上下文管理都写在调用点附近四处复制粘贴。技能层强制把所有相关的横切逻辑收拢进一个执行体内调用方只管“发起请求、拿结果”。其三无状态。很多Agent初版是无状态的一次性调用但真实业务几乎都需要多轮、多步的上下文累积。技能框架天然能携带状态——比如一个“文件上传技能”需要在多个步骤间记住会话上传了哪些文件技能状态对象可以在请求间存活。从设计决策上说我选择用“技能注册表 统一执行接口 上下文状态对象”这样的三层结构而不是搞一个重型的Agent框架原因只有一个轻量、可嵌入。很多Agent项目其实只差一层技能抽象不需要引入一整个规则引擎或编排平台。我的目标是让任何已有Agent代码库通过引入一个skills目录和一两个核心类就能获得技能化的能力。1.3 方案选型为什么是“注册制”而非“枚举制”初期我试过“枚举制”——把所有技能写在一个大的if-else或switch里。性能倒是没问题但每次加技能都要改主分支代码违反开闭原则。后来切换到“注册制”每个技能是一个独立模块启动时扫描并注册进技能表。注册制还有一个隐藏好处技能可以按需加载。比如图像处理类技能只在用户对话中出现“图片”相关意图时才导入对应模块内存占用和启动时间都更可控。技能模块可以独立测试甚至可以做成热插拔——我现在的项目里新技能发布不需要重启主服务注册中心会自动发现新模块。从运维角度看注册制让技能的“可见性”变得可观测。我可以随时列出当前系统注册了多少技能、哪些被频繁调用、哪些从未被触发这些数据对后续优化模型决策、精简技能列表非常有用。枚举制下技能是否存在只有代码review时才知道。2. 核心架构技能系统的组成模块2.1 技能定义Schema让模型“看懂”技能技能要让大模型准确调用它的描述格式必须严格结构化。我最终确定的技能Schema包含这几个核心字段name全局唯一标识采用类似file.upload、db.query的命名风格前缀区分领域名称就是技能的身份。description一句话说明技能能力重点写“能做什么、何时应该用”避免写实现细节。模型靠这句话做语义匹配描述越精准误用率越低。parametersJSON Schema格式的参数定义模型严格按照这个结构生成调用参数。参数描述必须注明类型、是否必填、取值范围。executor实际执行的入口可以是函数引用、HTTP端点或一个子Agent。runtime_hints运行期提示比如超时时间、并发限制、需要哪些环境变量。其中“description”是最容易被低估的字段。我做过对比实验把“计算两个日期的差值”改成“当用户询问XX事件到XX事件间隔多少天、或需要剩余天数倒计时时使用”模型调用准确率从71%提升到了92%。描述里带触发场景比泛泛描述效果强得多。参数Schema方面我强烈建议直接用JSON Schema而非自创格式。原因很实际大模型在众多训练数据里见过大量JSON Schema它理解这种结构的成本最低同时校验库成熟Python里jsonschema库可以直接复用。参数命名也有讲究用file_path而不是path用start_date而不是date_start模型生成参数时的直觉准确性会更高。2.2 技能注册与发现机制技能注册的核心是一个注册表类内部用一个字典维护技能名到技能对象的映射。启动流程不长但顺序和细节决定了整个体系是否可靠。第一步是技能扫描。我按目录约定扫描skills/下的所有模块每个模块暴露一个register()函数返回技能类实例。用pkgutil.iter_modules遍历目录避免手写导入列表这样新增技能文件后无需改动主入口。第二步是依赖检查。很多技能依赖外部服务或配置项注册时就要检查环境变量是否存在缺依赖就标注为inactive而不是直接报错。这样系统不会因为某个技能缺配置就整个崩溃问题技能会被隔离。第三步是注册确认。所有技能注册完成后生成一份技能清单摘要记录数量、活跃数、名称列表。这份摘要就是后续注入给大模型“可调用技能列表”的依据。2.3 技能编排与组合从单技能到工作流单技能能力有限真正复杂的业务需要技能组合。我实现了一套简单的编排机制技能的执行结果统一放入上下文状态对象下一个技能可以通过上下文访问上一个技能的结果。比如“用户上传了一份财报要求提取所有数字并做同比分析”那么流程是file.parse解析文档finance.extract提取关键指标analysis.compare做对比分析。编排时最关键的技巧是“结果摘要”。大模型上下文窗口有限不能把前一个技能的完整输出直接灌给下一个技能。我的做法是定义了一个summarize()方法每个技能负责把自己的结果压缩成一段摘要文本放进上下文。这样编排链条上传递的是高密度信息而不是原始数据。技能之间还可以声明依赖关系比如finance.extract声明依赖file.parse的输出格式。编排器会检查依赖图自动调整执行顺序避免重复解析。依赖关系也防止了环形调用——我在注册时用拓扑排序检测环一旦发现循环依赖直接标记配置错误。3. 实操过程从零实现一个技能框架3.1 目录结构与初始化我用一个最小的工程结构演示这套结构能直接迁移到大多数项目中。agent-skills/ ├── agent/ │ ├── __init__.py │ ├── core/ │ │ ├── __init__.py │ │ ├── registry.py # 技能注册表 │ │ ├── context.py # 上下文状态对象 │ │ ├── executor.py # 技能执行器 │ │ └── errors.py # 异常定义 │ ├── skills/ │ │ ├── __init__.py │ │ ├── file_parse.py │ │ ├── db_query.py │ │ └── calc_diff.py │ └── main.py # 入口示例 └── requirements.txt注册表的实现非常简洁核心逻辑是维护注册字典并提供查找、列表能力# agent/core/registry.py class SkillRegistry: def __init__(self): self._skills {} # name - Skill self._dependencies {} # name - set(names) def register(self, skill): name skill.name if name in self._skills: raise DuplicateSkillError(fskill {name} already registered) self._skills[name] skill for dep in skill.dependencies: self._dependencies.setdefault(name, set()).add(dep) def resolve(self, name): if name not in self._skills: raise SkillNotFoundError(fskill {name} not found) return self._skills[name] def active_skills(self): return [s for s in self._skills.values() if s.status active] def to_catalog_prompt(self): return \n.join( f- {s.name}: {s.description} (params: {s.parameters}) for s in self.active_skills() )to_catalog_prompt()就是把技能清单转成给模型看的文本这一步让注册表和Prompt生成之间建立了天然联动。3.2 上下文状态对象让技能之间有“记忆”技能执行过程中输入参数、中间结果、最终输出都需要一个统一载体。我定义了一个SkillContext它在一次Agent任务的生命周期中贯穿始终# agent/core/context.py dataclass class StepResult: step_id: str skill_name: str status: str summary: str payload: Any class SkillContext: def __init__(self, initial_input: str): self.initial_input initial_input self.steps: list[StepResult] [] self._memory {} def put(self, key: str, value: Any): self._memory[key] value def get(self, key: str, defaultNone): return self._memory.get(key, default) def add_step(self, result: StepResult): self.steps.append(result) # 自动把摘要保存进memory方便后续技能读取 self._memory[result.skill_name] result.summary def full_history_text(self): return \n.join( fStep {r.step_id} [{r.skill_name}] {r.status}: {r.summary} for r in self.steps )注意add_step()里做了两个设计选择一是保留完整payload但同时总是生成summary二是每一步的摘要自动写入内存技能名就是key。这样后续技能既能通过ctx.get(file.parse)拿到摘要也能在需要原始数据时访问ctx.steps[-1].payload。3.3 技能执行器统一调度入口技能的调用不能是裸函数调用必须经过执行器统一处理这样横切逻辑才能集中。执行器负责参数注入把模型生成的JSON参数传给技能。前置校验检查技能是否active检查必填参数。超时控制用asyncio.wait_for包裹执行防止技能卡死拖垮Agent。结果规范化把任意返回值统一包装成StepResult。异常隔离技能抛出的异常被捕获后转为statusfailed并附错误摘要而不是直接中断整个任务。# agent/core/executor.py async def execute_skill(registry, context, skill_name, params: dict, timeout30): skill registry.resolve(skill_name) if skill.status ! active: raise SkillInactiveError(fskill {skill_name} is inactive: {skill.inactive_reason}) # 参数校验 errors validate_against_schema(params, skill.parameters) if errors: return StepResult( step_idcontext.next_step_id(), skill_nameskill_name, statusparam_error, summaryf参数校验失败: {errors}, payloadNone ) try: result await asyncio.wait_for( skill.execute(params, context), timeouttimeout ) except asyncio.TimeoutError: return StepResult(... statustimeout, summaryfskill {skill_name} timed out) except Exception as e: return StepResult(... statusfailed, summaryf{type(e).__name__}: {str(e)}) return StepResult( step_idcontext.next_step_id(), skill_nameskill_name, statussuccess, summaryresult.summary(), payloadresult )有了统一执行器技能内部就可以“只做正事”不用自己处理超时、校验、错误格式化。从我的经验看这一步是最值得投入的它把所有技能的鲁棒性问题收敛在一处解决。3.4 一个标准技能长什么样以“日期差计算”技能为例完整的技能模块结构如下# agent/skills/calc_diff.py from datetime import datetime class CalcDiffSkill: name calc_diff description 计算两个日期之间的间隔天数适用于用户询问XX日到XX日有多少天、剩余天数等场景 parameters { type: object, properties: { start_date: {type: string, description: 起始日期格式YYYY-MM-DD}, end_date: {type: string, description: 结束日期格式YYYY-MM-DD} }, required: [start_date, end_date] } dependencies [] status active async def execute(self, params, context): start datetime.strptime(params[start_date], %Y-%m-%d) end datetime.strptime(params[end_date], %Y-%m-%d) diff (end - start).days class Result: def __init__(self, days): self.days days def summary(self): return f两个日期相差 {self.days} 天 return Result(diff) def register(): return CalcDiffSkill()这是一个最小可执行技能。register()函数就是之前扫描机制依赖的入口。这里没有复杂的业务但体现了一个技能该有的基本要素标准接口、参数Schema、结构化结果。3.5 模型决策循环技能框架如何与LLM联动技能框架本身不产生智能智能来自大模型对技能清单的决策。我推荐的执行循环是“计划-执行-反馈-循环”在一次任务中重复进行直到满足终止条件。执行顺序如下把技能清单registry.to_catalog_prompt()和用户输入一起发给模型。让模型输出一个结构化的决策格式为{thought: ..., skill: skill_name, params: {...}}或者输出{answer: 最终答复用户}表示任务完成。如果是技能调用执行器运行对应技能把StepResult追加进上下文。把“上一步执行结果”反馈给模型允许它评估结果、决定继续调用还是给出最终答案。设置最大循环次数比如10轮防止模型陷入反复调用同一技能的死循环。这一循环的关键是“反馈必须包含状态”。如果技能失败模型需要看到失败原因才能决定是换参数重试、换技能还是直接结束。我会在Prompt里明确提示模型“技能执行可能失败请根据失败原因调整策略”这一步能极大降低模型在异常路径上乱撞的概率。3.6 参数计算与错误重试策略实操中经常出现模型生成的参数格式不对比如把时间写成“2024-1-1”而不是“2024-01-01”。我总结的经验是与其依赖模型永远生成标准格式不如在技能内部做一层容错解析。具体做法是技能入口处先做“宽松解析”尝试多种时间格式和字符串清理解析失败才返回参数错误。重试策略也要谨慎设计。盲目让模型重试同一技能通常无效更好的做法是第一次失败后把“可选的修正建议”注入下一轮Prompt。例如参数错误时把校验错误信息原样返回并加一句“请严格按参数Schema重新生成参数特别是类型和格式”。另一种情况是技能返回statustimeout此时应该建议模型尝试更小粒度的子任务或者直接放弃该步并转向其他路径。4. 常见问题与排查技巧实录4.1 模型就是不肯调用技能这是最常见、也最容易排查错方向的问题。模型明明看到了技能清单却总是自己“脑补”答案或返回一段泛泛而谈的文字而不是结构化调用。我排查这个问题的经验有三步。第一步检查技能描述里的触发信号是否明确。如果技能描述只写“计算两个日期的差”模型可能把它理解为“可选的辅助信息”而不是“必须调用的工具”。把描述改成“当用户询问日期差、剩余天数、倒计时时必须调用此技能”触发率立刻上升。重点在于“必须”和具体场景不是泛泛的能力声明。第二步检查Prompt的决策指令是否用中文提示模型“输出JSON”。很多模型在收到开放式任务时更倾向直接回复而非调用工具需要在指令里明确说明如果你判断需要执行操作才能回答请输出{skill: ..., params: {...}}的JSON若能直接回答才输出普通文本。第三步确认技能描述是否侵入上下文。我见过一个项目因为技能描述写得太长模型上下文被大量描述占满导致模型“忘了”要调用工具。把描述压到一句话以内长内容放runtime_hints而不是description字段。4.2 技能调用对但参数总出错参数错误大部分集中在三种情况类型不匹配、日期格式不规范、枚举值超出范围。类型不匹配我通过两层校验来解决第一层是前端Schema校验错误立即返回第二层是技能内部“宽松模式”尽量从字符串中智能转换。前端校验避免脏数据进入执行逻辑宽松模式则尽量给模型一个机会。我的建议是在参数Schema里把枚举型字段描述的非常具体。例如字段是mode取值范围包括fast和accurate描述里一定要写明“可选值仅限fast、accurate”而不是笼统地写“模式参数”。模型在生成参数时会倾向从描述中提取合法值如果描述不够明确它就可能生成quick或efficient这类自定义值。另外我强烈建议在开发阶段开启“参数错误日志记录”。每次模型生成的参数校验失败都把原始参数、错误信息存下来。积累两三百条以后会看到明显的模式——哪个技能最容易被某类参数坑到。针对模式修改Schema描述参数错误率能稳定下降50%以上。4.3 多个技能互相“打架”调用当Agent同时拥有两个能力相近的技能时模型可能会随机选择或者反复尝试不同的技能直到一个成功。例如同时有search_db和search_web两个技能用户问“帮我查一下产品库存”模型有时调用数据库有时调用网页搜索结果天差地别。这个问题要从根上解决技能边界要清晰描述要互斥。search_db的描述应注明“仅查询内部数据库中的结构化记录适合库存、订单、用户资料”search_web的描述应注明“仅查询互联网公开信息适合实时资讯、公开资料”。模型看到两个技能的边界后能做出正确的职责判断。如果两个技能本质上是同一个领域的不同实现更好的做法是合并成一个技能用参数区分执行路径而不是让模型在两个技能之间周旋。我踩过几次坑后总结了一个原则同名不同实现的技能不要拆开注册宁可内部用策略模式。4.4 编排链路太长导致上下文爆炸一个复杂任务可能产生15到20条步骤记录每步的摘要和反馈都累积在上下文中几轮之后模型就“记不清”前面的结果了。这个问题很隐蔽任务本身没有跑错输出质量却急剧下降。我的解决方案是“滚动摘要窗口”。保留最近N步的完整记录更早的步骤统一压缩成一条“任务进度摘要”。具体做法是每隔几步就对之前的步骤做一次汇总把关键结果提炼成一句长文本放进上下文。这个压缩动作本身可以用一个独立的context.summarize技能实现让它用LLM对步骤历史做摘要。虽然增加了模型调用成本但长任务稳定性提升非常明显。另外关于Agent技能项目的搭建我再分享一个很多人忽略的技巧给技能加“健康自检”能力。每个技能注册后定期跑一次最小用例确认其返回结果正常。大模型时代依赖接口的工具经常因上游变化而悄悄失效自检能在用户发现问题之前就暴露故障。我给框架写了一个health_check方法每天早上自动跑一遍配合告警推送效果显著。这个机制在技能数量超过20个之后尤其有价值。5. 从技能框架到技能生态后续扩展路径这套Agent技能框架跑通以后我体会最深的一点是技能化的核心价值不在于单个技能的效率而在于能力的积累与复用。有了统一框架每解决一个新需求就沉淀成一个新技能技能库越来越大系统能应对的任务范围也随之扩张。扩展方向上我建议优先考虑三件事。一是技能版本管理技能的参数和逻辑总会演进给技能加版本号在注册表里记录兼容版本可以让系统在技能升级时保持稳定。二是技能观测面板把技能调用频率、成功率、平均耗时做实时的可视化统计这不仅是运维工具更是优化技能描述、调整编排策略的数据基础。三是技能的跨项目复用把技能库设计成独立的Python包或服务多个Agent项目共享同一套技能资产避免重复开发。我个人在实际迭代中最深的一个体会是技能框架不是“做完就结束”的东西它需要持续校准。模型的决策能力在变技能的输入输出在变使用场景也在变。每隔一段时间回顾技能注册表和调用日志删掉长期闲置的技能、合并重叠的技能、优化高频技能的描述这个维护节奏比写新代码更重要。最后再分享一个我最近正在做的小扩展把技能的选择从“单次决策”升级为“并行探索”。当任务目标不太明确时允许Agent先并行调用两到三个相关技能获取信息再综合分析后做出最终回答。这种方式在多源信息聚合场景下效果特别好代价是增加了资源消耗需要配合好并发控制和结果去重。技能的抽象让你可以低成本地试验这类新交互范式这正是它最大的魅力所在。