"agent-skills"这个项目名我第一次看到时,第一反应是"这不就是给AI加一堆工具调用吗"。但真正在业务里蹚过一遍之后,我发现自己错得挺离谱。它解决的问题不是"能不能调工具",而是"怎么把调工具这件事变得有边界、可积累、能复用"。
如果你也遇到过这种情况:Agent看起来啥都能聊,可一落到具体任务上就抓瞎——让它读代码库它只给表面结论,让它整理线上文档它能给你编出几个不存在的接口——那你大概率缺的不是模型能力,而是一套把"会聊"变成"会干"的技能层。
这篇文章不聊论文,也不做理论推演。我会拿我实际维护了大半年的若干套技能定义仓库做底子,讲清楚agent-skills里每一条技能该怎么拆、怎么描述、怎么编排、怎么调优,以及我在落地过程中踩过的最典型的几个坑。适合两类人看:一类是刚上手Agent开发、被Function Calling和工具调用绕晕的读者;另一类是已经跑了Demo、正卡在"让Agent稳定完成多步骤任务"这个坎上的开发同学。
1. 一个跑不通的Agent,缺的从来不是模型能力
先讲个真实场景。某开发同学给了Agent一堆工具:代码搜索、文件读写、执行测试、调内部接口文档。表面上权限齐全,工具列表漂亮得很,可实际一跑,Agent要么不知道该先调哪个工具,要么调完一个就忘了下一步该干嘛,要么给出的结论和工具返回的内容对不上。问题出在哪?出在"工具"这个概念太薄了。
1.1 工具是一张卡片,技能是一条流水线
单个工具本质上只是一张能力卡片。你告诉模型"有个工具能搜代码",模型知道可以调用它,但不知道在什么条件下调用、调用完怎么处理、结果和另一个工具的结果怎么拼起来。这些决策全部压在模型临场发挥上。
而技能不一样。我在agent-skills里对技能的定位是:针对某一类可重复任务,把工具调用、参数拼接、结果处理、容错分支打包成一个可复用的完整动作单元。比如"搜索代码库中某个函数的定义"不是技能,而是工具调用。但"回答一个关于存量代码库具体实现的问题"就是技能,因为它包含了:先判断问题是否需要看源码(分支)、用搜索API抓候选文件、对候选做相似度过滤、把片段拼进上下文、最后生成带文件路径和关键代码行的结论。
这套流程如果靠每次让模型当场设计,效果极不稳定。你可能用同一个模型跑十次,五次表现不错,五次跑偏。原因很简单:开放式规划的空间越大,出错的概率越高。技能的思路就是把这个空间压缩到最小——模型不再自由决定流程,而是在我预设的轨道里做选择和填空。
1.2 我在技能里封装的四层结构
我习惯把每条技能拆成四个部分,这个结构已经在多个任务场景里验证过:
| 层 | 内容 | 作用 |
|---|---|---|
| 触发条件 | 什么情况下该选这条技能 | 防止模型在错误场景硬套 |
| 执行步骤 | 按序完成的工具调用链 | 把多步骤流程固化 |
| 输出规范 | 返回内容的字段格式 | 保证下游能力稳定消费 |
| 纠错分支 | 工具失败或结果为空时的处理方式 | 让Agent不会卡死或强行编造 |
这四层看着简单,但每条技能的实际编写里,最耗时间的往往是最后那层"纠错分支"。绝大多数工具几乎不会永远稳定返回正确结果,检索可能为空、接口可能超时、文件可能不存在。模型在遇到这些异常时,如果不让它沿预设分支走,它就极大概率开始编造一个"看起来合理"的答案。这是为什么很多Agent看起来能说会道,一深问就露馅的原因。
2. 技能拆解的颗粒度:太粗了失控,太细了废掉
做agent-skills时我最早犯的错是把技能定义得过大。比如"数据分析"一条技能,里面塞了数据库查询、图表生成、指标计算、报告输出。结果模型每次调用这条技能,光是判断"当前这一步到底执行到哪个子环节"就消耗了大量上下文,还经常走错分支。后来我把技能拆到"数据质量初步探测""时序指标异常扫描""图表数据格式组装"这种粒度,稳定性立刻上来了。
2.1 判断颗粒度是否合适的三个简单标准
模型在调用前能不能一眼看清边界。技能描述里如果出现过长的步骤清单,说明它内部的状态分支太多,会加重模型的选择负担。
技能之间底层工具是否大量重叠。如果两条技能都会去调用同一个搜索工具,且后续处理逻辑完全不同,说明你的"决策差异"应该显式拆成两条技能,而不是靠模型在执行中自行分流。
单条技能能否在两次工具往返内完成主流程。注意我说的是主流程,不是全部流程。以"回答代码库某模块的实现细节"为例,主流程是:搜索候选代码(第一次工具调用)、拼接上下文并生成结论(第二次工具调用)。如果主流程需要三次以上往返,就要检查是不是把"检索方案设计"和"回答问题"两件事揉在一起了。
2.2 一个技能的完整定义示例
我用一个偏文本形式的配置文件来描述技能,你也可以用JSON或YAML。关键在于字段的设计,而不在于格式本身:
skill_name: repo_qa_search description: | 当用户提出关于某个仓库内具体实现的问题时使用。 典型问题:xxx模块怎么处理并发?yyy函数在哪个文件里? 不适合:问设计思路、问架构演进、问系统全貌。 trigger_condition: 问题中包含明确的代码符号、文件名或模块名 steps: - step: 1 action: code_search params: query: "{user_question}" repo: "{repo_name}" expect: 返回候选文件路径列表和匹配片段 - step: 2 action: code_read params: paths: "从step1结果中取评分最高的3个文件" need_range: true expect: 返回文件关键段落 - step: 3 action: infer_answer params: context: "step1的片段 + step2的段落" format: "带文件路径和行号的结论" output_format: | - 结论(直接回答问题) - 依据代码位置(文件 + 函数名) - 引用的关键片段(不超过200字) fallback: - case: code_search返回空 action: 用仓库目录树分析替换,获取模块结构后二次搜索 - case: step2文件读取失败 action: 重试一次,仍失败则返回目录树结论,不编造具体行号这个示例中最关键的一点是fallback行为。我明确要求模型在检索不到结果时降级到目录树分析,而不是让它自由发挥。实际效果差异很大:不加fallback时,约有近三成问题模型会给出"类似功能的通用实现方式"这种语义正确、但和实际仓库无关的答案;加上fallback后,答案基本都会落在仓库内部真实存在的文件结构上。
2.3 描述怎么写,模型才真的会选它
技能配置里,description往往是开发人员最不重视、但影响最大的字段。很多人写的是"用于回答仓库相关问题",这种描述几乎等于没说。模型在技能选择阶段看到的是一段摘要,它需要在百毫秒级决定"这条技能对该问题的适配度有多高"。
我自己的写法包括三部分:适配场景的正例、明确排除的反例、以及留下印象的关键词。比如上面的skills示例里我写"典型问题"和"不适合"两个部分,看似多余,实际上让选路的准确率高了很多。你可以把它理解为招人时的职位描述——光写"招技术员"没有用,得写清楚"负责后端接口开发、不出差、不接受转岗"。
3. 三个我实际维护最久、收益最大的技能模块
前面聊的是原则,这里拿三个真正跑了好几个月的技能模块开刀,看看它们在真实业务里怎么发挥价值,也看看哪些地方是刚开始设计时完全没想到的。
3.1 代码库焦点问答:从"随便聊聊"到"指哪打哪"
第一个模块是关于存量代码库问答的。这个模块解决的场景特别具体:协作者接手一个遗留系统,文档几乎没有,只有代码。他想知道"某个接口的请求参数是怎么校验的"、"某个定时任务的执行入口在哪",但这些信息散布在多文件、多模块里,人工翻很痛苦,直接问通用大模型又拿不到内网代码细节。
我把它设计成三层结构。第一层是搜索入口,负责把用户问题改写成几组搜索词,分别去检索函数定义、调用链和配置引用;第二层是内容精读,把命中的文件切成带行号的片段,按和被检索目标的关联度排序;第三层是答案合成,只把这些片段拼给模型,并强制要求输出里必须包含"文件路径+函数名"。这套链路跑下来,最大的感受是:模型不需要你给它讲架构,你只需要给它设计好"应该看哪几个文件"的路径。
这里有个常见的误区:很多人试图把整个代码库塞进上下文,指望模型自己综合。这个做法在几万行以内勉强可行,到了几十万行甚至百万行级别,要么成本爆炸,要么回复质量急剧下降。正确的做法是让搜索能力承担"缩小范围"的工作,上下文只承载"阅读和推理"——这其实就是聚焦问答技能的核心设计逻辑。
3.2 自动代码评审的边界:能扫低级错,别碰主观判断
第二个模块是自动代码评审。做之前我以为让模型看Pull Request差异就够了,后来发现"看diff"和"评审一个变更"之间隔着不止一步。PR里经常有几十个文件,直接全量塞进上下文会被大量无关片段稀释注意力。我最终把评审技能拆成三个独立的子技能:
- 变更概览:统计变更文件清单、增删行数、涉及模块,输出结构化清单
- 高风险文件扫描:按变更影响面、循环复杂度、是否触及核心链路排序,挑出前几个需要细看的文件
- 逐文件评审:对筛选出的文件分别做问题挖掘,输出问题级别、位置、原因和修改建议
把三个子技能串起来后,我发现评审质量稳定多了。但必须承认,它的边界也很清楚:这类自动化评审擅长发现逻辑漏洞、重复代码、明显错误,但对"这个设计是否应该改成另一种方案"这类主观判断基本无能为力。所以我给这个模块加了一个很关键的用户提示:"建议由开发者人工判断的设计类意见,不要直接告警"。这一行提示词的设计,比调十个模型参数都有效。
3.3 文档站点批量采集与分析:让RAG真的能落地
第三个模块是关于文档数据抓取和检索增强问答的。它解决的是"产品文档在官网或内部文档站上更新频繁,手册版本永远滞后"的问题。以前的做法是定期全量抓一遍文档页面,转成向量后丢进知识库。听起来挺美好,但实际用起来经常遇到两个尴尬:一是文档页面的导航栏和广告占了大量篇幅,切出来的向量全是噪音;二是不同页面间的链接关系没人管,模型只能回答单页问题,一遇到"跨页对比"就哑火。
我在技能模块里加了两个环节解决上述问题:页面主体提取和链接关系建图。提取阶段会先对HTML结构做密度分析,把正文区块和导航区块区分开;建图阶段会解析页面的相对链接,构建页面之间的引用关系。这样模型在回答"xx功能和yy功能有什么区别"时,能沿着链接图找到相关页面组,而不是只能碰巧命中某一页。
这个模块的思路其实可以复制到任意垂直领域的知识库搭建上——关键是先解决"文档结构和引用关系"这两个元问题,再谈向量化和问答质量。
4. 让Agent选到对的路:技能编排和选择策略
技能有了,技能库也有了,接下来最现实的问题是:给到Agent一堆技能之后,它每次到底该选哪一条?这个问题没人可以绕开。我在agent-skills里同时设计了几种选路策略,它们的适用场景差别非常大。
4.1 方案一:把决策交给语义检索
第一种方案,是所有技能的描述向量化后存进索引,当新用户请求过来时,先匹配最相近的几条技能,把它们作为候选给模型选。它的优点是完全不挑场景,适合开放性极强的任务,比如办公知识问答、跨模块信息整合。缺点是选路质量直接依赖技能描述写得好不好,描述里模棱两可的词越多,选错路的概率越大。
我在这个方案里得到的实践经验是:与其把大量技能都放在候选区,不如设置一个截断阈值。如果用户请求和所有技能描述的相关度都低于某个值,就直接走兜底逻辑,让模型直接回答或转人工。这个兜底分支非常重要,它避免了模型在低相关度的技能里硬挑一个,然后生硬执行造成错误结果。
4.2 方案二:让状态驱动技能选择
第二种更适合有明确流程路径的场景。比如"故障处置"这个场景,技能分成问题信息确认、相关模块映射、日志检索、处置建议生成四步,每一步的下一步怎么走,部分由上一步的输出状态决定。这时候把决策交给语义检索就不合适了,因为流程是强约束的,上一步都没做完,模型不可能选出合理的下一步。
我在这类场景里做的是给每一条技能打上输入依赖标记:需要哪些结构化的前置输出,产生哪些状态的变更。Agent在执行完一条技能后,会先更新当前状态,再根据状态匹配"下一步可用技能集合"。这种方式下选错的概率很低,因为可选空间本来就限定了,模型的不确定性只发生在限定的等值选项里。
4.3 什么时候让Agent自己编排?我的答案是尽量别
很多从业者热衷于让Agent"自由规划",这个理想状态我也追过,效果不太行。在真实业务里,流程越是自由规划,越容易出现两个问题:一个是上下文爆炸,Agent为了做规划,把历史状态全部塞进对话里,成本指数级上升;另一个是不可复现,同样的输入,今天跑和明天跑结果不一样,后续排查问题都不知道该从哪个环节入手。
所以在我维护的仓库里,自由规划只存在于探索初期。一旦探索出了一条稳定路径,我会马上把它固化成技能,让模型不再自由发挥。这相当于把"训练时探索、推理时固化"的思路应用到了技能层面。时间久了,你就会发现真正让Agent稳定可用的,不是某个强大的模型,而是一堆固化得足够好的技能。
5. 调技能密度和踩坑记录:给同样在填坑的你
工具是人用的,技能也是人调的。这条路上有几个坑属于"你不踩一次就长不了记性"的类型,写出来给大家做参考。
5.1 技能描述里的"模糊词"就是一把双刃剑
我第一次给"文档浏览"技能写描述时,用了"当用户需要查看或了解文档时使用"。结果很惨,用户问"帮我查一下某某配置的默认值",这条技能总是跑出来,因为它认为"查配置"是"了解文档"。后来我把描述改成"当用户需要浏览、搜索或阅读文档页面正文内容时使用;配置值的查询应优先选择配置检索技能"。加上"应优先"三个字之后,选路准确率明显提升。
这里有个本质规律:模型做技能选择时最信任的其实是排他性信息。你告诉它"什么时候不该选",比告诉它"什么时候该选"效果更明显。每条技能描述的反例部分,是和模型之间的隐性契约。
5.2 工具返回结果为空,绝不等于"没找到"
这句教训我吃了不少亏。之前"日志检索"技能返回空的时候,Agent会给出"系统中未发现相关日志"的结论。但真实情况往往是:检索时间范围不对、日志索引未建立、查询语句写错。空结果对应的是"没匹配到",而不是"系统里没有"。
修正方式是在技能fallback分支里加一条很明确的规则:当工具返回结果为空时,先检查检索条件和覆盖范围,而不是直接把空结果当作结论返回给用户。这条规则在很多语义搜索类技能里都适用。目前每次结果为空时,Agent至少会给出一个"可能原因"而不是斩钉截铁的"不存在",用户感知和信任度都提高了。
5.3 参数模板是给模型的"填空题",不是"自由度"
技能与工具的交互离不开参数。早期我把参数设计得页面太宽松,例如只写"query: 搜索内容",模型在生成搜索词时经常天花乱坠,一下把问题原封不动丢进去,一下又过度精简,结果都是检索质量低下。
改进之后,我为每一个参数都设计了明确的生成模板和示例值。比如:
def build_search_params(user_question: str): # 问题改写:提取关键实体 + 限定条件 keywords = extract_keywords(user_question, max_num=5) filters = extract_filters(user_question) return { "query": " ".join(keywords), "filter": filters, "limit": 10 }别小看这个简单的封装,它背后其实是在为每次工具调用提供"固定的搭参数方式"。模型只负责从用户问题里抽内容,而参数的组织规则由代码固定下来。这套组合下来,检索的稳定性提升比想象中明显。
5.4 评测集:没有评测,就没有优化方向
最后也是最重要的一个建议:任何技能模块都要配一个固定评测集。我不需要它覆盖所有场景,只需要它覆盖最核心的若干类问题。每次改动技能配置或描述,就重跑一遍评测集,比较输出质量。坚持一段时间后,你会积累下一批"改了反而变差"的反例,这些反例是比正向案例更有价值的资产。
我自己维护的众多技能里,这半年来真正收益可观的那些,没有一个不是经过了至少三轮评测-修改-回测循环。那些写完就丢线上跑的技能,几乎全都在某个角落埋了雷。
6. 技能库的自然演进方向:从一个仓库到一个生态
做到这一步,agent-skills就不再只是单纯工具列表,而变成了一套内部能力资产。但随着模块数量超过十几个,新的问题又冒出来:技能的发现、版本、复用效率,以及对新场景的覆盖速度。
6.1 版本化:改技能配置必须能回滚
技能配置本质上是代码,而且是对模型行为影响很大的代码。早期我改完描述后直接生效,跑一周发现效果不好再回退时,只能靠git log翻历史,非常痛苦。后来所有技能配置都严格纳入版本管理,每次修改必须写变更说明,并且至少在评测集上跑一轮回归。这条规则从源头杜绝了那种"今天调了下效果不错,过段时间又变差但不知道哪个版本引入"的问题。
6.2 共享与复用:技能是团队的语言
当技能库成熟以后,它的价值就不止于单次业务的优化,更会成为团队内部的语言共识。比如说"这条链路需要加一条输入校验技能",大家都知道指的是什么、在哪改、怎么测。这种效率提升是无形的,但比任何一个单点性能优化都实在。
我目前的做法是把技能库按领域拆包:代码相关一组、文档相关一组、数据相关一组。新项目接入时,先看现有包里有哪些能直接复用、有哪些需要微调,而不是从零开始定义一套全新的。这个"先复用后新增"的顺序,长期看能省下大量重复开发成本。
6.3 技能 DSL 化的可能路径
最后提一个我自己在踩的发展方向:把技能配置从通用格式转向更紧凑的结构化定义。未来一个技能可能更接近一个"小流程定义",包含状态机或者分支条件。这样设计和调整的体验会更接近写业务代码,而不是写Prompt。当然目前提前思考这个方向还为时尚早,但至少有一个雏形确实可以提高复杂技能的维护效率。
回到文章开头的问题:Agent不稳定,缺的往往不是模型有多聪明,而是周围有没有一堆能把任务扛下来的技能。在选定一个开放场景时,先别急着把最贵的模型请出来,试着先定义好十几条高性价比的技能,效果也许比你预想的好很多。至于路线怎么选、优先级怎么排,以项目目标和手里已有的工具为起点,边跑边调,就是最稳妥的路径。