1. 从agent-skills这个命名说起它到底在解决什么问题第一次看到agent-skills这个命名我的直觉是这大概率不是一个具体的业务系统而是一套围绕智能体能力做抽象、封装、复用和编排的工程化方案。事实也确实如此。在当下的技术语境里agent这个词已经被用得很泛——从对话机器人到自动化工作流从代码助手到多步骤任务执行器几乎只要带点自主决策味道的东西都被叫成 agent。但真正落到工程实践里大家很快会发现一个尴尬的现实单个 agent 能跑通 demo 很容易但要让它的能力可复用、可组合、可维护难得多。agent-skills这个标题的核心价值恰恰在于它试图回答一个工程问题如何把智能体的能力拆成一个个独立、可插拔、可测试的技能单元再通过统一的协议把它们组装起来。这就像乐高积木——单块积木本身没什么了不起但当你有一套标准化的凸点和凹槽就能拼出任意形状。这里的凸点和凹槽就是技能之间的输入输出契约。这篇文章适合三类人看第一类是想从零搭建一个多技能智能体框架的开发者你需要知道技能该怎么切、怎么注册、怎么调度第二类是已经在用某个现成 agent 框架、但被能力越加越乱困扰的工程师你需要一套重构思路第三类是对智能体工程化感兴趣、想理解背后设计取舍的技术爱好者。我会尽量把每个设计决策背后的为什么讲清楚而不是只丢一堆代码让你抄。需要先说明的是由于原始项目正文和关键词都是空的下面的内容是我基于agent-skills这个命名在常见工程实践中的合理推演——也就是说我会按照一个合格从业者在设计这类系统时最可能采用的方案来展开并明确标注哪些是通用实践、哪些是我的个人经验补充。这样你读的时候能分清哪些是行业共识哪些是我的私货。2. 技能抽象的第一道坎粒度到底切多细2.1 粒度太粗和太细都会翻车设计 agent-skills 这类系统最先撞上的问题不是技术而是哲学问题一个技能到底应该多大我见过两种极端。一种是巨型技能——把帮我订一张明天去某地的机票整个打包成一个技能。这种切法的好处是调用方简单一句话就能触发坏处是内部逻辑黑盒化一旦中间某步失败比如支付环节你根本没法复用前面的查询航班能力只能整个重来。另一种是原子技能——把打开浏览器输入文字点击按钮都拆成独立技能。这种切法复用性拉满但编排成本爆炸一个简单任务要串几十个技能调试起来像在追踪一条几百米长的多米诺骨牌。我的经验是技能的粒度应该对齐业务语义上的完整动作而不是技术实现上的原子操作。判断标准很简单——如果一个技能的名字需要用并且来连接两个动作那它可能太粗了如果它的名字里出现了然后那它大概率太细了。比如查询天气是一个好技能查询天气并发送邮件就太粗调用天气 API 的 HTTP 请求又太细。2.2 用输入输出契约倒推粒度更工程化的做法是先定义技能的输入输出契约再倒推粒度。一个设计良好的技能它的输入应该是一组语义清晰的参数输出应该是一个结构化的结果对象而不是一段自由文本。举个例子{ skill_name: search_flights, input: { origin: 北京, destination: 上海, date: 2025-06-01, max_results: 5 }, output: { flights: [ {flight_no: XX123, depart: 08:00, price: 1200} ], status: success } }当你强迫自己把输入输出写成这样的结构时粒度问题会自动浮现如果某个字段你根本填不出来比如用户偏好这种模糊参数说明这个技能承担了它不该承担的职责如果输出里塞了三种完全不同类型的数据说明它该拆了。这个契约倒推法是我在实际项目里用得最多的技巧比拍脑袋决定粒度靠谱得多。2.3 一个反直觉的结论允许技能重叠很多人设计技能库时会追求正交性——每个技能职责唯一互不重叠。理论上很美实践中很痛。因为真实世界的任务往往有多种路径强行正交会导致调用方在编排时要做大量选路判断。我的建议是允许适度的技能重叠用优先级和适用条件来管理。比如快速查询天气和详细查询天气可以是两个技能前者只返回当前温度后者返回未来七天预报。调用方根据场景选择而不是让一个技能内部用 if-else 处理所有情况。这样每个技能的实现都保持简单组合的灵活性反而更高。代价是技能库会变大但配合良好的检索和分类机制这个代价是值得的。3. 技能注册与发现让 agent 知道自己会什么3.1 注册表不是简单的字典技能写好了下一步是让 agent 能发现它们。最朴素的做法是维护一个字典key 是技能名value 是函数引用。但这样做很快就会遇到问题技能多了之后agent 怎么知道在什么场景下该调用哪个技能所以一个合格的技能注册表至少要包含这几类信息技能标识唯一名字、功能描述自然语言供语义检索用、输入输出 schema供参数校验和自动填充用、适用条件什么情况下该用、优先级多个技能都匹配时选谁。这五样东西缺一不可。功能描述尤其重要——它是连接自然语言任务和结构化技能的桥梁。class SkillRegistry: def __init__(self): self.skills {} def register(self, name, func, description, schema, conditionNone, priority0): self.skills[name] { func: func, description: description, schema: schema, condition: condition, priority: priority } def find_candidates(self, task_description): # 基于描述做语义匹配返回候选技能列表 candidates [] for name, meta in self.skills.items(): score semantic_similarity(task_description, meta[description]) if score THRESHOLD: candidates.append((name, score, meta[priority])) return sorted(candidates, keylambda x: (x[1], x[2]), reverseTrue)3.2 语义检索是发现机制的核心上面代码里的semantic_similarity是整个发现机制的心脏。早期我试过用关键词匹配结果非常糟糕——用户说我想看看明天要不要带伞关键词匹配根本找不到查询天气技能因为字面上没有天气两个字。后来换成基于向量嵌入的语义检索命中率立刻上了一个台阶。具体做法是在注册技能时把技能的 description 通过嵌入模型转成向量存起来调用时把任务描述也转成向量算余弦相似度取 top-k 作为候选。这里有个实操细节——description 的写法直接决定检索质量。我踩过的坑是一开始 description 写得太简短比如查询天气结果和查询股票的向量距离很近经常误召回。后来改成根据城市和日期查询该地区的天气状况包括温度、降水和风力信息区分度立刻提升。所以写 description 时要遵循一个原则把技能能处理的输入维度、能产出的输出维度都写进去越具体越好。3.3 条件过滤语义匹配之后的第二道闸语义检索只能保证相关不能保证可用。比如一个发送邮件技能语义上可能和通知用户这个任务很匹配但如果当前上下文里根本没有用户的邮箱地址这个技能就是不可用的。所以注册表里的condition字段就是干这个的——它是一个返回布尔值的函数用来判断当前上下文是否满足技能的前置条件。def email_condition(context): return context.get(user_email) is not None registry.register( namesend_email, funcsend_email_impl, description向指定用户发送邮件通知需要用户邮箱地址, schemaemail_schema, conditionemail_condition, priority5 )这个设计的好处是把能不能用和该不该用分开了。语义检索负责该不该用条件过滤负责能不能用。两者解耦之后每个模块的逻辑都变得简单调试时也能快速定位问题出在哪一环。4. 技能编排从会很多技能到能完成复杂任务4.1 编排的本质是状态机有了技能库和发现机制agent 理论上会很多技能了。但会技能和能完成任务之间还差一个编排层。编排的本质说白了就是一个带状态的决策循环观察当前状态 → 选择下一个技能 → 执行 → 更新状态 → 判断是否完成。这个循环听起来简单但魔鬼在细节里。最大的挑战是如何决定下一步该干什么。有两种主流思路。一种是规划式——先让模型生成一个完整的技能调用序列然后按序执行。这种方式的优点是全局最优缺点是计划赶不上变化一旦中间某步失败整个计划就废了。另一种是反应式——每一步都根据当前状态重新决策。优点是灵活缺点是容易短视可能陷入局部循环。我的实践结论是混合使用用规划做骨架用反应做修正。具体来说先让模型生成一个粗粒度的计划比如先查天气再决定是否提醒带伞然后每一步执行时根据实际返回结果动态调整后续步骤。这样既有全局视野又有局部灵活性。4.2 状态管理别让上下文无限膨胀编排过程中每一步的输入输出都会累积到上下文里。如果不加控制上下文会迅速膨胀既拖慢推理速度又增加成本还可能超出模型的上下文窗口。我见过一个项目跑十几个技能之后上下文塞了几万 token模型开始胡言乱语因为它被无关信息淹没了。解决办法是分层状态管理。把状态分成三层全局状态整个任务共享的关键信息如用户 ID、任务目标、步骤状态当前步骤的输入输出、历史摘要已完成步骤的压缩摘要。每次调用模型时只把全局状态 当前步骤状态 历史摘要喂进去而不是把每一步的原始输出都塞进去。class TaskState: def __init__(self): self.global_ctx {} # 全局关键信息 self.step_ctx {} # 当前步骤上下文 self.history_summary # 历史压缩摘要 def update_after_step(self, step_result): # 把步骤结果压缩进摘要而不是原样保留 self.history_summary compress( self.history_summary summarize(step_result) ) self.step_ctx {}这个compress和summarize可以用一个小模型来做成本很低但效果立竿见影。实测下来同样的任务上下文体积能压到原来的三分之一甚至更少而任务成功率基本不受影响。4.3 失败重试与降级编排的健壮性设计真实环境里技能失败是常态——网络超时、API 限流、参数不合法什么都有可能。如果编排层没有失败处理机制整个任务就会卡死。我的做法是给每个技能调用包一层重试 降级逻辑。重试策略上指数退避是标配第一次失败等 1 秒第二次等 2 秒第三次等 4 秒最多重试三次。但要注意不是所有失败都值得重试——参数错误这种重试一万次也一样的失败应该直接跳过重试进入降级逻辑。降级逻辑就是如果这个技能实在用不了有没有替代技能比如详细天气查询失败了能不能降级到快速天气查询如果连替代都没有就把这个失败信息记录到状态里让后续的决策步骤知道这条路走不通换一条路。提示重试和降级一定要设置总时长上限。我踩过的坑是某个技能一直超时重试整个任务跑了五分钟还没结束用户体验极差。后来加了全局超时比如 60 秒超时就直接返回当前已完成的部分结果并说明哪一步没完成。5. 技能的可测试性别让 agent 变成玄学系统5.1 每个技能都要能独立测试智能体系统最让人头疼的一点是不可复现——同样的输入这次成功下次失败你根本不知道问题出在哪。要解决这个问题必须让每个技能都能脱离 agent 独立测试。这也是技能化相对于单体 agent最大的工程优势单体 agent 是一个大黑盒技能化之后变成了一堆小白盒加一个调度器。具体做法是每个技能都要有明确的输入输出 schema然后针对 schema 写单元测试。正常输入、边界输入、非法输入三类都要覆盖。比如查询天气技能正常输入是合法城市名边界输入是超长城市名或特殊字符非法输入是空值或错误类型。这些测试跑通了你才能确信技能本身没问题出问题时可以把排查范围缩小到编排层。def test_search_flights_normal(): result search_flights(origin北京, destination上海, date2025-06-01) assert result[status] success assert len(result[flights]) 0 def test_search_flights_invalid_date(): result search_flights(origin北京, destination上海, datenot-a-date) assert result[status] error assert date in result[message]5.2 编排层的测试用回放代替实时技能测完了编排层怎么测总不能每次都真的去调外部 API 吧又慢又不稳定。我的做法是录制回放第一次真实执行时把每个技能的输入输出都录下来存成 fixture之后的测试都用回放模式技能调用直接返回录好的结果。这样测试速度快、结果可复现还能模拟各种异常场景比如手动改 fixture 让某个技能返回错误测试编排层的降级逻辑。这个思路借鉴了 HTTP 测试里的录制回放模式搬到 agent 编排上同样好用。唯一要注意的是fixture 要定期更新因为外部 API 的返回格式可能会变用太旧的 fixture 测试会给你虚假的安全感。5.3 端到端评估别只看成功率最后是端到端评估。很多人只盯着任务成功率这一个指标但这是不够的。我通常会看四个维度成功率任务是否完成、效率用了多少步、多少 token、成本调用了多少次外部 API、稳定性同样输入的多次运行结果方差有多大。这四个指标要一起看才能全面反映系统健康度。举个例子某个版本成功率 90%看起来不错但平均步数是上一个版本的两倍token 消耗翻了三倍——这说明系统虽然能完成任务但走了很多弯路实际体验和成本都很差。只看成功率的话你根本发现不了这个问题。评估维度关注点常见问题成功率任务是否达成目标虚高把部分完成也算成功效率步数、token 消耗绕路、重复调用成本外部 API 调用次数无缓存、重复请求稳定性多次运行的方差依赖随机性、不可复现6. 我在实际搭建中踩过的几个坑6.1 技能描述写得太聪明反而坏事前面提过 description 要具体但这里有个度。我一度把 description 写得很聪明比如智能分析用户意图并给出最佳建议——听起来很厉害但语义检索时这种描述和几乎所有任务都沾点边导致误召回严重。后来我改回朴素的根据 X 和 Y 查询 Z检索准确率反而上去了。description 是给检索算法看的不是给老板看的越朴素越精确越好。6.2 别让技能内部再调 agent这是个架构层面的坑。我一开始图省事让某个复杂技能内部又去调用了一个子 agent。结果调试时彻底懵了——外层 agent 的日志和内层 agent 的日志混在一起根本分不清哪步是哪步。而且内层 agent 的失败会以各种奇怪的方式冒泡到外层错误处理逻辑变得极其复杂。技能就应该是纯函数式的——给定输入产出输出不搞嵌套 agent。需要多步协作的场景交给编排层去串而不是塞进技能内部。6.3 上下文里的隐形污染这个坑很隐蔽。编排过程中某些技能的返回结果里会带一些元信息比如调试用的 trace_id、时间戳、内部状态码。这些信息对决策没用但会占用上下文还可能干扰模型的判断。我有一次发现 agent 总是莫名其妙地重复调用同一个技能排查半天才发现是上一个技能的返回里带了个retry_hint字段模型误以为需要重试。解决办法是在技能返回结果进入上下文之前做一层清洗只保留决策必需的字段。这个清洗逻辑最好做成统一的中间件而不是每个技能自己处理。6.4 优先级冲突当两个技能都想抢活技能多了之后经常出现两个技能语义得分接近、优先级也一样的情况。这时候如果随机选一个系统行为就不可预测了。我的做法是加一个决胜规则优先选输入参数更容易满足的那个。比如快速天气查询只需要城市名详细天气查询还需要日期那在日期未知的情况下优先选前者。这个规则实现起来简单但能显著提升系统的确定性。7. 技能库的演进从能用 to 好用7.1 技能版本管理不能省技能是会迭代的。今天查询天气返回温度明天可能要加个体感温度字段。如果直接改依赖旧格式的编排逻辑就会崩。所以技能必须带版本号注册表里同时保留多个版本调用方显式指定用哪个版本。新版本上线后观察一段时间确认没有调用方还在用旧版本再下线。这套机制听起来重但比起线上事故这点成本完全值得。7.2 用技能使用日志反哺设计系统跑起来之后会产生大量技能调用日志。这些日志是金矿——哪些技能被调用最多、哪些技能经常失败、哪些技能从来没被调用过。调用最多的技能值得投入优化经常失败的技能要么是 description 写得不好导致误调用要么是本身实现有问题从来没被调用的技能考虑下线减少检索时的干扰项。我每隔一段时间就会拉一次这些统计效果比拍脑袋优化好得多。7.3 技能市场的可能性当技能库积累到一定规模一个自然的演进方向是技能市场——不同团队贡献技能通过统一的注册协议接入形成共享生态。这时候前面强调的 schema、description、condition 这些契约就体现出价值了只要契约统一技能来自哪个团队、用什么语言实现都不影响接入。当然这也带来了新的挑战比如技能质量参差不齐、安全边界如何界定。我的建议是先从团队内部的小市场做起跑通了再考虑扩大范围。8. 关于这套方案我最后想说的几句实在话agent-skills 这类系统的核心思想其实就一句话把智能和能力分开。智能体现在编排层的决策上能力体现在一个个独立的技能里。这个分离带来的好处是全方位的——技能可以独立开发、独立测试、独立迭代编排逻辑可以独立优化不影响技能整个系统的行为变得可观测、可复现、可调试。但我也要泼盆冷水这套方案不是银弹。如果你的任务非常简单就一两个固定步骤那直接写死流程比搭一套技能框架划算得多。技能化的价值在于复用和组合如果你的场景根本不需要复用那这套复杂度就是纯负担。我见过不少团队为了架构先进硬上技能框架结果维护成本比原来高了好几倍。判断要不要上这套方案我的标准是当你发现同样的能力在多个任务里被重复实现或者任务的步骤组合经常变化时才值得做技能化。否则老老实实写几个函数可能才是最优解。技术选型这件事合适永远比先进重要。