从工具函数到技能编排:Agent技能栈的完整设计实践
项目地址agent-project/agent-skills先交代一下背景。我今年主要在做一个偏垂直场景的智能代理项目目标不是做通用大模型而是让模型能在特定业务里真正干成事。一开始我的思路很简单给模型写一堆 tool function让它按需调用。但做到中后期一个很扎心的问题浮现出来——工具越堆越多模型根本不记得怎么用也不清楚什么场景该不该调某个能力。与其叫工具不如把它们重新组织成技能skills。这个转向解决了我一多半的困扰。今天这篇就把我搭建 agent-skills 的完整过程、踩坑记录和一些设计取舍讲清楚给正在做类似项目的朋友一个参考。1. 为什么 Agent 需要一套技能栈而不是一堆函数先把概念对齐一下。很多人做 Agent 就是在 system prompt 里列一堆 function description然后让模型输出 function call 的 JSON代码里去路由执行。说实话这套方案在工具数量少于 10 个、场景单一的时候完全够用。但一旦工具上了二三十个问题就来了。第一个问题模型的上下文窗口不是无底洞。你不可能把 30 个函数的详细 description、参数 schema、调用范例全部塞进 system prompt。塞进去既浪费 token又干扰模型对核心任务的理解。我当时统计过把每个函数的 JSON Schema 完整展开平均一个就要 1K 左右的 token30 个就是 30K再加上对话历史和业务上下文直接触顶。所以必须做按需加载而不是全量注入。第二个问题工具之间天然有依赖关系。比如你要让 Agent 完成一份经营分析查数据库只是第一步后面还要跑聚合算法生成图表写报告。如果这些都是扁平的工具调用模型每次都要自己从头拼装整套流程不但容易漏步骤而且每次的表现都很不稳定。而技能的概念是把一系列原子操作、经验规则、判定条件打包成一个整体像乐高块一样可以被复用和组合。第三个问题评测和回归变得更难。工具越来越多之后你会发现修了 A 工具的 bug可能让 B 工具在某些场景下表现变差。没有一个技能层做隔离和收敛整个系统就越长越乱最后谁都不敢动。所以我做的第一件事就是把这个项目拆成两个层次底层是原子工具只做一件事比如读文件、查数据库、执行代码上层是技能面向任务的多步骤编排判定逻辑预设知识。模型只面向技能做推理而不是直接操作原子工具。个人体会工具是功能技能是能力。功能的粒度是能做什么能力的粒度是做成一件事。Agent 要稳定地做成事停留在功能层是不够的。2. 技能描述层与注册中心模型靠什么发现技能刚开始我把技能清单写在系统提示词里白底黑字列出来技能名称、适用场景、输入参数。后来发现模型非常容易混淆两个相关技能尤其是描述写得不够严格的时候。比如我写了一个数据清洗技能又写了一个格式校正技能模型经常不知道该选哪个。问题就出在描述层设计上。2.1 技能描述的侧面拆分法一个技能我建议至少从五个侧面来描述描述维度解决什么问题示例技能 ID机器识别用名字skill_report_generation_zh触发条件什么情况下必须使用当用户需要完整的经营分析报告时前置条件什么情况下禁止使用未连接数据源时不可用输入参数需要哪些信息时间范围、业务线、报告格式使用流程摘要大致执行步骤查询数据、聚合计算、生成图表、成文我踩过的坑是只写技能 ID输入参数结果模型不会用。后来加了触发条件和使用流程摘要命中率明显提升。原因也简单模型在做技能选择的推理时其实是在做一种阅读理解它需要知道这个技能在什么场景下应该出现、执行后大概会产生什么结果而不仅仅是一个函数签名。一个非常值得注意的细节是技能的描述不要用形容词要用行为动词和可验证的状态。比如高效地处理数据就是废话将原始 CSV 文件中的缺失值按列均值填充并输出一份清洗报告就是有效描述。因为模型无法理解高效但能理解按列均值填充这个确定动作。2.2 注册表是技能层的元数据中心我在项目里维护了一个技能注册表registry每个技能对应一个描述结构体集中放在一起管理。核心结构长这样dataclass class SkillSpec: skill_id: str name: str version: str description: str trigger_hint: str # 触发条件 block_hint: str # 禁用条件 parameters_schema: dict # JSON Schema 格式的参数定义 flow: Callable # 执行入口接收规范化的参数 required_context: list[str] # 需要从上下文读取的数据 output_schema: dict # 输出的格式定义注册中心维护了一张字典按 skill_id 存储 SkillSpec同时提供一份精简版清单用于注入一份完整版档案用于调用。模型推理时先用精简版清单做技能选择最多花 300 token定位到具体技能之后再按需拉取完整参数 Schema 和流程信息。这个分层加载的方式比一次性全量注入省了大量 context又保证了按需精确匹配。2.3 技能清单的缓存与更新机制技能清单不能每次请求都重新构建要在项目启动时加载运行中支持热更新。我做了一个简单的缓存方案注册表打包成一个带版本号的技能包只有当技能包版本变化时才重建清单。另外建议加一个技能可用性标记有些技能依赖外部服务比如短信发送接口如果服务掉线了清单里要动态隐藏这个技能否则模型会选一个执行必失败的技能浪费整个轮次。避坑提示技能描述是面向模型的不是给人看的。我见过不少项目把技能描述写成了产品文档风格结果模型的技能命中率很低。描述应该像给一个聪明但没做过这个业务的新同事写工作说明场景、步骤、边界一个都不能少。3. 技能执行的调度链路选择、编排、校验技能层搭好了框架真正的难度在调度链路的实现。这一部分我分三段讲模型如何选中技能、选中之后如何编排步骤、参数校验怎么做到严格又不僵硬。3.1 模型如何选中技能避免想当然式命中我在早期版本里让模型直接输出一个技能 ID然后代码无脑执行。效果很糟糕因为模型经常在用户只是问了句相关话题的情况下就把技能调起来了。比如用户问我们的数据库支持哪些字段它居然触发了一个数据迁移技能完全跑偏。纠正方法是用一个选择器selector中间模块。选择器模块做的事情解析用户意图判断是否存在明确的技能触发信号如果存在选出一个候选技能top-1如果存在多个可匹配的技能输出候选列表并附带置信度最终交给代码判定置信度过低时不执行任何技能而是转成普通对话。这个中间模块我给模型定义了一个结构化的输出类似这样{ candidate_skills: [ {skill_id: skill_report_generation_zh, confidence: 0.92, reason: 用户明确要求生成报告} ], final_action: execute }final_action有三种取值execute执行技能、ask_clarification追问澄清、reply_only不经过技能直接回复。这个机制的收益非常明显把技能的触发权从模型手里收回了一部分放到规则层来做安全兜底。这在稳定性优先的生产级 Agent 中几乎不可或缺。3.2 编排层技能执行时的上下文传递选中技能只是开始。一个技能内部要跑多步每一步之间还要共享中间数据。我在最初实现时把整个技能流程写到一个超大的 Python 函数里局部变量传来传去结果代码难看、难测试出了 bug 也难定位。后来我转向上下文管理器context manager步骤函数的模式。每个技能实现为一个流程一个技能由若干步骤组成每一步是一个独立的函数函数接收一个共享的TaskContext对象从中读取输入往里写入中间结果。举个例子报告生成技能的执行流大概是def step_collect_data(ctx): start_time ctx.get(start_time) end_time ctx.get(end_time) raw database.query(...) ctx.set(raw_data, raw) def step_aggregate(ctx): raw ctx.get(raw_data) agg compute_metrics(raw) ctx.set(metrics, agg) def step_visualize(ctx): agg ctx.get(metrics) chart make_chart(agg) ctx.set(chart_path, chart)这种设计的优势在于每一步都可以独立复用和测试步骤之间的依赖通过 ctx 的 key 契约来管理而不是纠缠不清的函数参数可以为整个技能记录执行轨迹每一步拿到什么、产出什么都有迹可查。我在执行轨迹的记录上吃亏不少。早期没有记录模型在技能执行中出错时只能看到最终报错完全不知道是哪一步引发的连锁反应。加了对 ctx 变更的异步日志后定位问题的时间缩短了 70% 以上。3.3 参数抽取与校验宁可多问一次不要带错参数执行模型从用户输入里抽取技能参数这是一道天然容易出错的环节。我遇到过一个例子用户说帮我把 3 月的数据导出来模型把时间范围抽成 3 月 1 号到 3 月 31 号看起来没问题但这个业务里的3 月数据其实指财务周期起始日是 2 月 26 日。错误的参数一旦进了技能后面全错。所以我定了三条规则参数缺失直接触发澄清对话不擅自填充默认值。特别是时间类参数、筛选条件类参数宁可让模型问清楚。参数类型严格校验用 JSON Schema。不符合就报参数校验错误不尝试智能修复。早期我试过让模型自己把错误参数改对结果模型会胡猜反而引入更多问题。参数语义校验以技能内部实现为准。比如数值范围必须在 0~100 之间这个逻辑写在技能内部执行入口做一次统一校验。这能避免技能 A 对参数宽松、技能 B 对参数严格的标准不统一。参数校验失败和技能执行失败的反馈信息都应该传回给模型做修正。我的做法是构造统一的ActionFeedback{ status: param_error, field: time_range, expected: ISO date or datetime range, received: 3月, message: 无法识别时间范围请明确起始时间和结束时间 }模型看到这个反馈就知道自己漏了什么信息而不是毫无头绪地连续报错。关于编排层一句话总结让流程可控让状态可见。技能里的每一步都要有名字、有输入输出契约、有日志记录别写那种一坨执行函数后面有你哭的。4. 技能仓库与经验沉淀从一次性脚本到知识资产刚开始我写技能都是需求来了直接写一个函数注册进去功能完成就再也不管了。直到某天我发现一个写了没用的技能占了整个注册表三分之一才意识到技能仓库必须当成一个长期资产来经营而不是当一次性脚本写。4.1 技能的版本化与演进记录我给每个技能加了版本号version和updated_at每次变更必须写变更日志。变更日志不需要复杂一行说明即可v1.1.0 - 增加按业务线参数过滤调整输出格式为 markdown 表格 v1.1.1 - 修复大时间范围下内存溢出问题 v1.2.0 - 新增数据源连接池复用为什么这么抠版本因为技能变更可能引发连锁回归报告生成技能改了数据源连接方式导致图表生成技能的数据格式对不上。有了版本日志回溯定位就很容易。我用的工具是最普通的 git技能代码和 skill spec 放在同一个仓库走标准的 Pull Request 流程review 通过后合并然后触发技能包重新构建。4.2 技能仓库的组织结构我目前的agent-skills仓库目录大致是这样组织的agent-skills/ ├── registry/ │ ├── registry.json │ └── manifest.yaml ├── skills/ │ ├── report_generation/ │ │ ├── skill.yaml # 技能元数据与描述 │ │ ├── flow.py # 执行流程 │ │ ├── steps/ │ │ └── tests/ │ └── data_migration/ │ ├── skill.yaml │ ├── flow.py │ └── tests/ └── shared/ ├── context.py # 共享上下文类 └── validators.py # 通用校验逻辑每个子技能文件夹都自带测试这个非常重要。我要求每个技能的测试覆盖率不低于 80%重点覆盖参数正确时的正常执行参数边界条件空值、超长、非法类型外部依赖不可用时的降级行为。4.3 技能去重的会议机制技能一旦多了就逃不掉重复发明轮子的问题。我目前每个周五会做一个技能评审把新提交的技能和已有技能做一次矩阵比对。方法很简单跑一个脚本把技能的描述文本做向量化然后算两两相似度高于阈值的一对列出来人工确认。这样既能发现重复技能也能发现看似不同但可以合并的技能。举个例子我之前有数据去重和重复行过滤两个技能功能高度重合只是名字不同。评审脚本把它们列出来后我合并成一个重复记录清理技能对外只暴露一个入口内部通过参数决定具体策略。合并之后模型选择技能的准确率显著提升——因为少了一个干扰项。经验技能仓库的整洁度直接影响模型的上限。每多一个模棱两可的技能都是对模型技能选择能力的一次干扰测试。定期做技能去重、归档低价值技能比堆一堆看起来有用的技能更能提升整体效果。5. 实测中的性能瓶颈与稳定性调优技能架构跑通之后真正让人头疼的是性能和稳定性问题。这一节讲三个我真实遇到的性能陷阱和优化手段。5.1 技能选择阶段的延迟陷阱技能选择阶段模型需要先读一遍技能清单再做结构化输出。技能数量超过 20 个后我发现单次技能选择就耗时 2~3 秒而且这个模块是串行阻塞的直接影响用户体验。我做的优化分两层第一层清单瘦身。给技能加了一个display_in_selector字段。有些技能明显只能由特定场景触发比如发票 OCR 识别就不出现在通用清单里而是通过一个粗粒度路由规则在代码层先做分流。通用清单只保留 10~12 个高频技能。第二层并行打探。把选择器的任务改成异步并发一边跑意图分类一边生成候选技能等结果回来后统一做决策。实测下来技能选择的延迟从 2.9 秒降到 0.9 秒左右。5.2 技能内部多步骤的长耗时不阻塞有些技能内部有多步骤网络请求比如报告生成技能里要查数据库、调图表服务、调文件生成服务。早期我串行执行一个技能跑完要 40 多秒用户早就等跑了。后来我把没有依赖关系的步骤改成并发执行比如查数据和检查数据源健康状态能同时跑聚合计算依赖数据只能等前面完成。优化后的流程是输入 → 并发[查询原始数据, 检查数据源状态] → 聚合 → 并发[生成图表, 生成表格] → 成文总耗时从约 40 秒降到约 15 秒效果非常明显。这里的关键是写技能流程时第一步就要把依赖关系图画出来哪些步骤能并发哪些必须串行不要写完再优化。5.3 技能运行的超时熔断与重试外部依赖不稳定这是 Agent 项目里最让人头疼的事。我给技能执行加了两道防护超时设置每个技能步骤都设了超时时间。查询型步骤 5 秒生成型步骤 10 秒。超时后抛出异常进入重试逻辑。重试策略网络类错误最多重试 2 次间隔 500ms参数错误和业务逻辑错误不重试直接失败返回描述性错误。还加了一个熔断开关。如果某个技能在 5 分钟内连续失败 5 次就自动把它的状态置为degraded技能清单里不再展示。这样模型不会反复选到一个必定失败的技能浪费用户的等待时间。运营人员可以在管理后台手动恢复。5.4 从跑通到稳定的回归测试策略技能执行结果的验证不能靠感觉。我的做法是准备一份评估集包含 200 条真实用户语料的变体覆盖每个技能的正常、边界、异常场景。每次技能代码更新都要跑一遍评估集对比新旧版本的技能命中率和执行成功率。只要命中率掉了 2 个百分点以上合并请求就打回重新调。这个评估集不需要很复杂只要能反映真实用户分布就行关键是持续维护。实测数据技能架构从 v1 迭代到 v2 之后技能命中率从 78% 提升到 92%参数错误比例降低了一半而整体上下文 token 消耗反而下降了 30%。收益不是单点的而是整个链路的结构性改善。6. 技能编排的高级形态多技能协作与自动批处理单个技能稳定以后我开始琢磨更复杂的场景一个任务可能需要顺序执行多个技能而且中间要传递数据。比如用户说统计上季度的新增用户数并给出对比分析报告这就至少需要数据统计和报告生成两个技能协作。6.1 技能编排器工作流级别的控制我不打算让模型一次性规划好全部步骤那样不可控。反而采用分步决策的方式编排器每次只选下一个要执行的技能执行完看结果再决定下一步。这样每一步都可以人工介入调整不至于一错到底。编排器维护一个执行栈class Orchestrator: def __init__(self, registry): self.registry registry self.stack [] self.context TaskContext() def plan_next(self, user_intent, last_result): # 根据用户意图和上一步结果选择下一个技能 skill_id select_next_skill(user_intent, last_result) return skill_id这个走一步看一步的模式比让模型一次性输出完整的技能序列要稳得多。原因在于模型对大局的把握能力尚可但对多步骤执行中每一步的精确衔接还远不完美。分步决策给了系统在每步间纠错的机会。6.2 技能结果的标准化与适配两个技能协作最大的坑是数据格式不一致。技能 A 输出的是{status: ok, data: {...}}技能 B 期待的是{code: 0, result: {...}}模型看不懂代码也对不上。我的解决办法是约定技能输出层的统一协议。所有技能输出顶层字段固定为三种status、data、error。其中data内部格式由技能自己定义但如果是给另一个技能用的数据必须附带一个schema_ref标明格式版本。编排器可以在技能之间的边界做一次轻量转换。这样既保证了技能的独立性又让它们之间有共同语言。6.3 自动批处理高吞吐场景下的技能管道在后台批处理场景比如每天定时跑一批数据的清洗、统计、归档单个请求实时执行就不太合适。我基于技能架构做了一条批处理管道pipeline每个技能在管道里作为一个阶段stage。管道有一个简单的 DAG 描述执行引擎按拓扑顺序调度支持每个技能并发处理多条独立数据分片。这块的核心收益是复用平时实时对话里用的技能到了批处理流程里直接注册成管道阶段几乎不用额外写代码。真正需要的设计只是技能要支持幂等——同一个输入执行两次和一次结果一致。批处理里的重试就依靠这个性质保证最终结果一致不产生重复副作用。关于高级编排想提醒一点不要一上来就追求全自动多技能协同。先把单技能链路做扎实再逐步开放编排能力。否则你会发现所有问题都变成了模型规划错了调试难度无限大。7. 技能选型与演进方向我的后续规划做完了上面这些这个项目目前的技能体系已经能支撑中小规模业务的日常交互。接下来有几个方向我准备继续投入。一个是技能效果的数据回采。现在技能的命中率、执行成功率都有统计但执行成功了用户是否真的满意还缺数据。我准备在技能输出后接入用户反馈通道好评差评、追加提问内容都归集到技能的 dashboard 上形成技能质量分。有了这个分数做技能淘汰和优化就有了硬依据。另一个方向是技能的自学习/自更新。有些技能的执行策略依赖业务规则业务规则是一直变的。未来考虑做成策略与代码分离把业务判定逻辑外置为可配置规则业务人员可以直接改配置而不动代码。这能释放一部分开发压力也降低技能更新的发布风险。最后技能描述的质量评估我还在找更好的办法。现在主要靠模型命中率来间接衡量不够直接。我计划建一个小工具定期用测试语料探测每个技能对比模型看到了描述后是否会正确选择它用这个来定期发现描述质量下降的技能。8. 给刚起步做 Agent 技能模块的朋友几条建议技能体系的设计其实是在回答一个问题如何让模型在尽量小的上下文里稳定地选择并执行正确的能力。如果你刚准备入坑我的建议按优先级排序如下第一别从零造轮子站在已有的中间层上起步。函数调用、结构化输出这类基础能力直接使用模型服务商提供的标准接口把精力花在技能描述和编排设计上这是真正能拉开差距的地方。第二技能命名和描述多花一倍时间都不过分。这一层是模型和业务之间最关键的接口。命名要规范描述要写触发条件、禁用条件、行为动词、输出说明逐字打磨。我见过太多项目重执行轻描述最后模型乱选技能问题百出。第三用数据说话别用感觉调优。把技能命中率、参数错误率、执行成功率、耗时、成本这些指标全部采集出来。每个迭代都对比前后数据。没有数据支撑的优化大概率是自我感动。第四给技能执行加充足的安全阀。超时、熔断、重试、降级这些治理手段不要等出事故了再补一开始就要在架构上预留位置。最后一点也是我最深的一点体会Agent 项目里模型的推理能力只是起点真正决定上限的是你如何组织能做之事——技能层就是这套组织方式。tools 解决的是模型能摸到哪些按钮skills 解决的是模型知道怎么按按钮能把一件事办成。后者才是一个 Agent 从玩票走向可用的关键一步。