智能体技能体系设计:从提示词膨胀到可复用技能拆解与调度
1. 从“agent-skills”这个标题说起它到底在解决什么问题第一次看到“agent-skills”这个标题我脑子里蹦出来的第一个念头是这大概率不是一个单纯的工具库而是一套围绕“智能体能力”做拆解、封装、复用的方法论或者工程框架。为什么这么判断因为“agent”这个词在当下的技术语境里早就从学术论文里的抽象概念变成了工程落地时绕不开的一个实体——它要感知环境、要做决策、要调用工具、要维护记忆、还要在多轮交互里保持目标不跑偏。而“skills”这个词恰恰点出了智能体最核心的痛点一个智能体强不强不取决于它底层挂的是哪个大模型而取决于它手里攥着多少可复用、可组合、可验证的“技能”。我做过几个智能体相关的项目踩过最深的坑就是一开始把所有逻辑都塞进一个巨大的提示词里结果稍微改一个分支整个行为就崩了。后来才慢慢意识到真正靠谱的做法是把智能体的能力拆成一个个独立的“技能单元”每个单元有明确的输入输出、有边界条件、有失败回退策略。而“agent-skills”这个标题恰好就是在讲这件事——它要解决的是智能体从“能聊天”到“能干活”之间的那道鸿沟。这篇文章适合谁看如果你正在做智能体应用开发不管是任务型对话、自动化流程编排还是多智能体协作只要你遇到过“提示词越写越长、行为越来越不可控”的问题那这篇内容就是写给你的。如果你刚接触智能体概念还没动手写过一行相关代码也没关系我会从最基础的设计思路讲起用生活化的类比把核心原理说清楚再一步步拆到实操层面。整篇内容会围绕“技能拆解、技能封装、技能调度、技能验证”这条主线展开中间穿插我实际踩过的坑和总结出来的参数配置经验。需要提前说明的是下面涉及的具体实现细节有一部分是基于常见工程实践做的合理补全因为原始标题只给了一个方向没有附带完整代码。但所有设计思路和排查方法都是我在真实项目里验证过的你可以直接拿去参考。2. 智能体技能体系的核心设计思路拆解2.1 为什么不能把智能体做成“一个万能提示词”很多人刚上手智能体开发时最容易犯的错误就是“提示词膨胀”。我见过一个项目系统提示词写了三千多字里面塞了角色设定、任务描述、工具说明、输出格式、异常处理、语气要求甚至还有几条“不要做什么”的负面约束。结果呢模型在简单任务上表现还行一旦遇到需要多步推理的场景就开始丢三落四——要么忘了调用工具要么调用了工具但没解析返回值要么解析了返回值但忘了更新内部状态。这个问题的根源在于大模型在处理长上下文时注意力是会被稀释的。你塞进去的约束越多每条约束分到的“注意力权重”就越低。更麻烦的是不同技能之间可能存在逻辑冲突比如一个技能要求“先确认再执行”另一个技能要求“直接执行提高效率”模型在长提示词里很难判断当前该听谁的。所以“agent-skills”这套思路的核心就是把一个复杂的智能体行为拆解成多个独立的技能模块。每个模块只关心一件事有自己的触发条件、执行逻辑和输出规范。这样做的好处非常明显第一每个技能的提示词可以写得很短很聚焦模型执行准确率大幅提升第二技能可以独立测试和迭代改一个不会影响其他第三技能可以组合复用比如“查询天气”这个技能既可以用于出行规划智能体也可以用于日程提醒智能体。打个比方这就像一家餐厅的厨房。如果只有一个厨师他既要切菜又要炒菜还要摆盘忙起来肯定出错。但如果把流程拆成配菜、炒制、装盘三个岗位每个岗位只做自己最擅长的事整体效率和稳定性都会高很多。智能体的技能体系也是同样的道理。2.2 技能粒度的划分原则多大算一个“技能”拆技能听起来简单但实际操作时最头疼的问题就是粒度怎么定拆得太粗等于没拆拆得太细调度开销比执行开销还大。我试过几种不同的划分方式最后总结出一个比较实用的判断标准一个技能应该对应一个“可独立验证的原子操作”。什么叫可独立验证就是这个技能执行完之后你能明确判断它是成功了还是失败了。比如“从文本中提取日期”是一个可验证的技能因为你可以检查输出里有没有合法的日期格式。“帮用户规划一次旅行”就不是一个原子技能因为它包含了查询目的地、比较价格、安排行程、生成建议等多个步骤每一步都可能失败你没法用一个简单的断言来判断整体是否成功。按照这个标准我通常会把技能分成三个层级原子技能层最细粒度的操作比如文本分类、实体抽取、格式转换、数值计算、单次API调用。这一层的特点是输入输出明确执行时间短失败原因单一。组合技能层由多个原子技能编排而成比如“从用户消息中提取出行意图并查询对应城市的天气”。这一层需要处理原子技能之间的数据传递和异常回退。策略技能层决定在什么场景下调用哪些组合技能比如“当用户表达不满时优先调用安抚话术技能再调用问题解决技能”。这一层更接近传统意义上的“对话策略”。在实际项目里我建议先把原子技能层做扎实再往上搭组合技能。很多团队一上来就想做策略层结果底层原子技能不稳定策略再聪明也白搭。2.3 技能注册与发现机制的设计考量当技能数量多起来之后下一个问题就是智能体怎么知道有哪些技能可用怎么在需要的时候找到正确的技能这就涉及到技能注册与发现机制。最朴素的做法是把所有技能写在一个列表里每次让模型从列表里选。但技能一多这个列表就会变得很长模型选择准确率会下降。我实测下来当技能数量超过十五个之后模型选错技能的概率明显上升。更靠谱的做法是给每个技能打上标签和描述然后做两阶段检索先根据当前上下文召回一批候选技能再让模型从候选里做最终选择。这个思路借鉴了信息检索里的“召回排序”架构效果很稳。具体实现上我会给每个技能定义几个关键属性属性名作用示例skill_id唯一标识extract_datedescription自然语言描述用于语义召回从文本中提取日期信息tags分类标签用于规则过滤文本处理、信息抽取input_schema输入参数结构{“text”: “string”}output_schema输出结构{“date”: “string”, “confidence”: “float”}trigger_hint触发场景提示当用户消息包含时间相关词汇时有了这些元数据就可以先用关键词或向量相似度做召回把候选技能控制在五到八个再交给模型做精细选择。这个流程我跑过很多次选择准确率比直接让模型从全量列表里选要高出一大截。注意技能描述一定要写得具体不要写“处理文本”这种模糊描述要写“从用户消息中提取日期、时间或时间段”。描述越具体召回和选择的准确率越高。3. 核心细节解析与实操要点3.1 技能提示词的结构化写法每个技能本质上是一段提示词加一段执行逻辑。提示词写得好不好直接决定技能的执行成功率。我经过多次迭代总结出一个比较通用的技能提示词模板包含四个部分角色声明一句话说明这个技能是干什么的比如“你是一个日期提取器”。输入说明明确告诉模型输入是什么格式有哪些字段。输出规范用JSON Schema或者示例的方式规定输出必须长什么样。边界条件说明什么情况下应该返回失败或空值避免模型强行编造。举个例子一个“提取日期”技能的提示词可以这样写你是一个日期提取器。你的任务是从用户提供的文本中提取所有日期、时间或时间段信息。 输入格式 {“text”: “用户原始文本”} 输出要求 返回一个JSON对象包含一个字段 “dates”值为数组。 数组每个元素包含 “raw”原文片段和 “normalized”标准化后的日期格式YYYY-MM-DD。 如果文本中没有日期信息返回 {“dates”: []}。 边界条件 - 不要推测文本中没有明确出现的日期。 - 如果日期表述模糊如“下个月”normalized字段填nullraw字段保留原文。 - 不要输出任何解释性文字只输出JSON。这个模板的好处是模型不需要“理解”太多背景知识只需要按照明确的规则做映射。我对比过结构化提示词和自由格式提示词的效果前者在重复任务上的稳定性明显更好。3.2 技能执行中的参数校验与容错技能被调用时传入的参数不一定总是合法的。比如一个“查询天气”技能需要城市名和日期但模型可能只传了城市名或者传了一个不存在的城市。如果不做校验技能内部就会报错整个智能体流程就断了。我的做法是在每个技能入口处加一层参数校验分三步走必填检查检查所有必填参数是否存在缺失则返回明确的错误码和提示信息。类型检查检查参数类型是否符合schema定义比如日期字段是不是字符串数值字段是不是数字。范围检查检查参数值是否在合理范围内比如城市名是否在支持列表里日期是否在可查询范围内。校验失败时不要直接抛异常而是返回一个结构化的错误对象让上层调度器决定怎么处理。比如可以返回{“success”: false, “error_code”: “MISSING_PARAM”, “message”: “缺少城市名参数”}调度器看到这个错误后可以选择让模型重新生成参数或者直接向用户追问。实操心得参数校验的错误信息要写得足够具体最好能告诉调用方“缺了什么”和“应该怎么补”。我见过一些项目只返回“参数错误”四个字结果模型完全不知道该怎么修正只能反复重试浪费大量token。3.3 技能之间的数据传递与状态管理多个技能组合执行时技能A的输出往往要作为技能B的输入。这个数据传递过程如果设计不好很容易出现字段对不上、类型不匹配的问题。我一般采用“共享上下文”的方式来做数据传递。整个智能体维护一个上下文对象每个技能执行完后把输出写入上下文的一个命名空间里。后续技能需要数据时从上下文里按路径读取。比如{ “context”: { “user_input”: “帮我查一下明天北京的天气”, “extract_date”: { “dates”: [{“raw”: “明天”, “normalized”: “2025-06-11”}] }, “extract_location”: { “city”: “北京” }, “query_weather”: { “temperature”: “28°C”, “condition”: “晴” } } }这样做的好处是每个技能的输入输出都是显式的调试时一眼就能看出数据在哪一步出了问题。同时上下文可以持久化支持多轮对话里的状态延续。需要注意的是上下文不能无限膨胀。我一般会设置一个清理策略比如只保留最近五轮对话的上下文或者只保留被后续技能引用过的数据。否则上下文越来越长不仅浪费token还会干扰模型的注意力。3.4 技能失败时的回退与重试策略技能执行失败是常态关键是怎么优雅地处理失败。我通常会把失败分成三类分别对应不同的处理策略可重试失败比如网络超时、临时限流。这类失败直接重试即可但要注意设置最大重试次数和退避间隔。我一般设置最多重试两次间隔分别为1秒和3秒。可修正失败比如参数缺失、格式错误。这类失败不重试原技能而是把错误信息返回给调度器让模型重新生成参数后再调用。不可恢复失败比如技能不存在、依赖服务永久下线。这类失败直接向上报错触发兜底话术告诉用户当前功能不可用。这里有个容易忽略的点重试的时候最好把上一次失败的原因也带上。比如第二次调用时在参数里加一个retry_reason字段说明上次为什么失败。这样模型在重新生成参数时会有针对性地修正而不是盲目重试。4. 实操过程与核心环节实现4.1 环境准备与基础依赖在动手写代码之前先把基础环境搭好。我用的技术栈比较常规Python作为主语言因为生态成熟、调试方便用Pydantic做数据模型定义和校验省去大量手写校验逻辑的功夫用FastAPI暴露技能接口方便本地测试和后续集成。依赖清单大致如下pip install pydantic fastapi uvicorn httpx如果你打算用向量检索来做技能召回还需要装一个嵌入模型相关的库。不过初期技能数量少的时候用关键词匹配就够了不必过早引入向量检索否则会增加不必要的复杂度。目录结构我习惯这样组织agent_skills/ skills/ __init__.py base.py # 技能基类 extract_date.py # 日期提取技能 extract_location.py query_weather.py registry.py # 技能注册中心 dispatcher.py # 技能调度器 context.py # 上下文管理 main.py # 入口这个结构的好处是每个技能独立一个文件改哪个技能就动哪个文件不会互相干扰。4.2 技能基类的定义与实现所有技能都继承同一个基类基类里定义好统一的接口和通用逻辑。这样做的好处是调度器可以用同样的方式调用所有技能不需要为每个技能写特殊逻辑。基类大概长这样from pydantic import BaseModel from typing import Any, Dict class SkillInput(BaseModel): pass class SkillOutput(BaseModel): success: bool data: Dict[str, Any] {} error_code: str message: str class BaseSkill: skill_id: str description: str tags: list [] input_schema: type SkillInput output_schema: type SkillOutput def validate_input(self, raw_input: dict) - SkillInput: return self.input_schema(**raw_input) def execute(self, validated_input: SkillInput) - SkillOutput: raise NotImplementedError def run(self, raw_input: dict) - SkillOutput: try: validated self.validate_input(raw_input) except Exception as e: return SkillOutput( successFalse, error_codeINVALID_INPUT, messagestr(e) ) try: return self.execute(validated) except Exception as e: return SkillOutput( successFalse, error_codeEXECUTION_ERROR, messagestr(e) )这个基类做了两件事一是统一了输入校验二是统一了异常捕获。子类只需要实现execute方法专注于业务逻辑本身。我实测下来这种设计能减少大量重复代码而且新增技能时不容易漏掉校验和异常处理。4.3 一个完整技能的实现示例拿“提取日期”这个技能来说完整实现如下from pydantic import BaseModel from typing import List import re from .base import BaseSkill, SkillInput, SkillOutput class ExtractDateInput(SkillInput): text: str class DateItem(BaseModel): raw: str normalized: str None class ExtractDateOutput(SkillOutput): data: dict {dates: []} class ExtractDateSkill(BaseSkill): skill_id extract_date description 从用户文本中提取日期、时间或时间段信息 tags [文本处理, 信息抽取] input_schema ExtractDateInput def execute(self, validated_input: ExtractDateInput) - SkillOutput: text validated_input.text # 这里用正则做基础匹配实际项目可以接更复杂的解析逻辑 pattern r(今天|明天|后天|\d{4}-\d{2}-\d{2}|\d{1,2}月\d{1,2}日) matches re.findall(pattern, text) dates [{raw: m, normalized: None} for m in matches] return SkillOutput( successTrue, data{dates: dates} )这个实现很朴素但结构是完整的。你可以把正则替换成更强大的日期解析库或者接一个专门的日期理解模型但接口和错误处理逻辑不用变。4.4 技能注册中心的搭建注册中心负责管理所有技能的元数据并提供检索接口。我一般用一个字典来存技能实例key是skill_idvalue是技能对象。class SkillRegistry: def __init__(self): self._skills {} def register(self, skill: BaseSkill): self._skills[skill.skill_id] skill def get(self, skill_id: str) - BaseSkill: return self._skills.get(skill_id) def search(self, query: str, top_k: int 5) - list: # 简单关键词匹配实际可以换成向量检索 results [] for skill in self._skills.values(): score 0 if query.lower() in skill.description.lower(): score 1 for tag in skill.tags: if tag in query: score 1 if score 0: results.append((score, skill)) results.sort(keylambda x: x[0], reverseTrue) return [skill for _, skill in results[:top_k]]这个检索逻辑很粗糙但足够支撑初期开发。等技能数量超过二十个之后再考虑引入嵌入模型做语义召回。4.5 调度器的核心逻辑调度器是整个技能体系的“大脑”它负责根据当前上下文决定调用哪个技能、传什么参数、怎么处理结果。我实现的调度器分四步召回根据用户输入从注册中心召回候选技能。选择把候选技能列表和用户输入一起交给模型让模型输出要调用的技能ID和参数。执行调用对应技能拿到执行结果。决策根据执行结果决定是继续调用下一个技能还是返回结果给用户。第三步和第四步之间有一个循环因为一个用户请求可能需要多个技能串联完成。我一般设置最大循环次数为五防止无限循环。def dispatch(user_input: str, context: dict, max_steps: int 5): for step in range(max_steps): candidates registry.search(user_input) if not candidates: return {reply: 抱歉我没有找到合适的处理方式。} # 这里省略了调用模型做选择的代码 selected_skill_id, params model_select(candidates, user_input, context) skill registry.get(selected_skill_id) if not skill: return {reply: 技能不存在请检查配置。} result skill.run(params) if not result.success: # 把错误信息写回上下文让下一轮选择参考 context[last_error] result.message continue context[selected_skill_id] result.data if is_final_result(result): return {reply: format_result(result)} return {reply: 处理步骤过多已终止。}这个调度逻辑不算复杂但覆盖了大部分常见场景。实际项目里我会在“选择”这一步加更多约束比如限制模型只能从候选列表里选不能自己编造技能ID。4.6 本地测试与调试方法技能体系搭好之后不要急着接真实用户输入先用构造好的测试用例跑一遍。我一般会准备三类测试正常路径测试输入明确、参数完整验证技能能正确执行。边界测试输入为空、参数缺失、格式错误验证错误处理是否合理。组合测试多个技能串联验证数据传递是否正确。调试时我会把每一步的上下文打印出来方便定位问题。比如import json print(json.dumps(context, ensure_asciiFalse, indent2))这样一眼就能看出哪个技能的输出不符合预期或者哪个字段在传递过程中丢了。实操心得调试技能体系时最耗时的往往不是技能本身的逻辑而是技能之间的数据对接。我建议在开发初期每新增一个技能就立刻写一个组合测试确保它能和已有技能正确串联。不要等所有技能都写完再联调那样排查问题的成本会高很多。5. 常见问题与排查技巧实录5.1 技能选择错误模型选了不该选的技能这是最常见的问题之一。表现是用户问了一个问题模型却调用了一个完全不相关的技能。排查思路分三步第一检查召回结果。把候选技能列表打印出来看看正确技能有没有被召回。如果没被召回说明检索逻辑有问题需要调整关键词或描述。第二检查选择提示词。如果正确技能在候选列表里但没被选中说明选择提示词写得不够清晰。我一般会在提示词里加一句“优先选择与用户意图最匹配的技能不要选择仅部分相关的技能”。第三检查技能描述。有时候两个技能描述太相似模型分不清。这时候需要把描述改得更具体突出差异点。5.2 参数传递错误技能收到了错误的参数表现是技能执行时报参数校验失败或者执行结果明显不对。常见原因有三个上一个技能输出的字段名和下一个技能期望的字段名不一致。比如一个输出city_name另一个期望city。参数类型不匹配。比如一个输出字符串“28”另一个期望数字28。参数嵌套层级不对。比如一个输出{“data”: {“city”: “北京”}}另一个期望{“city”: “北京”}。解决方法是统一字段命名规范并在技能基类里加一层类型转换逻辑。我一般会在validate_input里做自动类型转换比如把字符串数字转成数字把单层嵌套展平。5.3 上下文膨胀token消耗过快多轮对话跑久了上下文会越来越大token消耗直线上升。我遇到过跑十几轮之后单次请求的token量翻了五倍的情况。解决办法有两个一是设置上下文窗口大小只保留最近N轮的数据二是对历史上下文做摘要压缩把不重要的细节丢掉只保留关键状态。我一般用组合策略最近三轮保留完整上下文更早的轮次只保留技能执行结果的摘要。这样既能维持对话连贯性又能控制token量。5.4 技能执行超时响应时间过长有些技能依赖外部服务响应时间不稳定。如果某个技能卡住了整个智能体流程都会阻塞。我的做法是给每个技能设置超时时间默认五秒。超时后直接返回失败让调度器决定是重试还是走兜底逻辑。超时时间可以根据技能类型调整比如查询类技能可以设短一点生成类技能可以设长一点。5.5 常见问题速查表问题现象可能原因排查方向解决建议模型选错技能召回不准或描述模糊打印候选列表检查描述优化检索逻辑细化技能描述参数校验失败字段名或类型不匹配对比上下游schema统一命名规范加类型转换上下文过长历史数据未清理统计token增长趋势设置窗口大小做摘要压缩技能执行超时外部依赖慢加日志记录耗时设置超时加降级逻辑多技能串联断掉中间步骤失败未处理逐步打印上下文加错误回传让模型修正5.6 几个容易忽略的避坑技巧第一个技巧技能描述里不要用“等”“之类”这种模糊词汇。比如“处理文本、日期等信息”这种描述模型很难判断边界。要写成“从文本中提取日期信息不处理其他类型的信息”。第二个技巧技能输出尽量用扁平结构不要嵌套太深。嵌套层级超过三层之后模型解析出错的概率明显上升。第三个技巧给每个技能加一个“示例输入输出”放在描述里。模型看到具体示例后选择和执行准确率都会提升。我实测下来加示例能让选择准确率提升两成左右。第四个技巧定期回顾技能使用日志看看哪些技能从来没被调用过哪些技能经常失败。没被调用的技能可能是描述有问题经常失败的技能需要优化实现或加容错。6. 技能体系的扩展与维护经验6.1 新增技能的标准化流程当你想给智能体加一个新能力时不要直接改调度器代码而是按照标准流程新增一个技能文件。流程如下在skills/目录下新建一个Python文件命名用下划线风格比如query_stock_price.py。定义输入输出的Pydantic模型字段名要符合项目统一规范。继承BaseSkill实现execute方法。在注册中心里注册这个技能。写一个单元测试验证正常路径和边界情况。写一个组合测试验证它能和已有技能串联。这个流程看起来繁琐但能避免很多低级错误。我见过不少项目因为跳过测试步骤上线后才发现技能之间有冲突。6.2 技能版本管理与灰度发布技能迭代时不要直接覆盖旧版本而是保留版本号。比如extract_date_v1和extract_date_v2同时存在调度器根据配置决定用哪个版本。这样做的好处是新版本出问题时可以快速回滚不会影响线上服务。灰度发布时可以先让一小部分请求走新版本观察一段时间后再全量切换。6.3 技能性能监控与优化每个技能都应该记录执行耗时、成功率、失败原因分布。这些数据能帮你发现性能瓶颈和稳定性问题。我一般会在技能基类的run方法里加埋点记录开始时间、结束时间、执行结果。然后定期汇总看看哪些技能拖慢了整体响应速度。如果某个技能耗时特别长可以考虑做缓存。比如查询类技能同样的参数在短时间内重复调用可以直接返回缓存结果不必每次都走完整流程。6.4 从单智能体到多智能体协作的扩展思路当技能体系成熟之后可以进一步扩展到多智能体协作。思路是把技能按领域分组每组技能交给一个专门的子智能体管理。比如一个子智能体负责信息查询类技能另一个负责内容生成类技能还有一个负责流程控制。子智能体之间通过消息传递来协作每个子智能体只关心自己领域的技能选择范围更小准确率更高。这种架构在复杂任务场景下优势明显但实现复杂度也更高建议先把单智能体技能体系跑稳之后再考虑。我在实际项目里发现技能体系的真正价值不在于技能本身有多复杂而在于它提供了一种可管理、可测试、可迭代的方式来构建智能体行为。以前改一个行为要动整个提示词现在只需要改一个技能文件风险可控迭代速度也快了很多。如果你正在做智能体相关的东西不妨从拆出第一个原子技能开始试试跑通之后再逐步扩展这条路我走过确实比一上来就搞大而全的方案要稳。