ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Agent技能系统设计实战:注册表、参数Schema与调用链路拆解

Agent技能系统设计实战:注册表、参数Schema与调用链路拆解 构建一个像样的 Agent 应用方向和实现方案往往不在模型选型上卡壳而是在“如何把能力边界切清楚”这一步反复折腾。我最初做 agent-skills 时也是从一段段散装代码起步今天回头看真正让整个系统从“能跑”变成“扛用”的并不是某个大模型有多聪明而是一套设计得当的技能Skill体系。这篇文章就围绕 agent-skills 这个主题把技能系统拆开揉碎聊清楚为什么需要它、怎么设计一套最小可用的技能机制、参数和描述里藏着哪些坑以及什么场景下千万别硬上。1. 为什么我会把“Agent 技能”当成一个独立系统来做1.1 一个让我推倒重来的真实场景起因是我在做一套内部的知识库问答 Agent。第一版很朴素用户问问题Agent 先从向量库检索文档片段然后拼进 Prompt让模型生成答案。测了几个星期简单问题表现不错但一旦用户问“上周的销售周报数据为什么比上上周少”这种需要跨系统查数据、做聚合、还要比对数值的问题对话就会变得又臭又长模型频繁地把语义理解对了却不知道应该调哪个函数、以什么参数去调。第二版我尝试把所有能力都塞进一个大 System Prompt 里把函数定义、调用规则、数据源信息全都铺进去。效果有提升但随后就出现了新的矛盾函数越加越多Prompt 越长模型的注意力被稀释反而连原本简单的问题都开始答错。更可怕的是新增一个数据源就要改 Prompt改完还可能影响别的模块。当时我意识到一个问题我需要一套机制把“能力”本身变成一种可发现、可加载、可独立维护的资源而不是一段被硬编码在链路里的逻辑。这就是 agent-skills 的雏形——把一切可复用的能力抽象成“技能”让 Agent 在需要时自行检索、理解、调用而不是一上来就把所有招式都背在身上。1.2 技能系统与工具调用的本质区别现在很多人会把 agent-skills 和 Function Calling 混在一起但它们解决的问题不在一个层面。Function Calling 解决的是“模型如何按格式输出一次调用”而 Skill 系统解决的是“在那么多能力里模型应该如何知道此刻该用哪一个、以及怎样正确使用”。这两者很像是“提供了一堆按钮”和“给按钮配了清晰的使用说明书和触发条件”的区别。没有 Skill 系统Agent 依然能调用工具但代价是系统里每多一个能力Prompt 的负担就更重模型的选择空间就越大误用率也随之上升。而一个设计合理的 Skill 系统可以让能力被封装成独立单元每一个单元都有自身的作用描述、输入参数、适用条件和返回格式当需要时再动态注入上下文。从工程角度看Skill 化还带来一个额外好处技能可以独立于主流程进行测试和版本管理。就像开发时你不会把全部业务逻辑写在一个文件里一样Agent 的能力边界也应该隔离、解耦。1.3 适合读这篇文章的人如果你正在做 Agent 应用无论你是用 LangChain、AutoGen 还是直接撸大模型 API只要遇到了下面几个迹象那这篇文章就适合你你的 System Prompt 越来越长连你自己都不想看第二遍新增一个工具接口总是担心影响已有能力模型经常“明明看到了功能列表却选择了错误的那一个”你想把不同的工具组合成更上层的业务能力但不知道怎么抽象接下来我会用一个简化但不失真实性的案例演示如何从零搭一套 Skill 系统并给出我在迭代中总结出的关键经验。2. 技能注册表、描述文件与参数 Schema最小闭环怎么搭2.1 技能目录结构设计我最早尝试的方式是把所有技能写在一个 skills.py 里用一个字典存函数名和描述。但在管理超过 20 个技能后这种做法基本崩了——搜索、版本管理、多人协作全都变得困难。现在的做法是每个技能占一个目录形成如下结构skills/ ├── data_search/ # 技能ID │ ├── SKILL.md # 技能描述面向LLM │ ├── schema.json # 参数Schema面向调用校验 │ └── executor.py # 执行逻辑 ├── report_compare/ # 另一个技能 │ ├── SKILL.md │ ├── schema.json │ └── executor.py └── registry.py # 技能注册表扫描并加载所有技能不用小看这个目录结构。它带来的第一个好处是“可发现性”Agent 或开发者能通过扫描目录知道系统里有哪些能力不用把整个项目翻一遍。第二个好处是“可独立演进”每个技能自带描述和执行代码改一个技能不影响其他技能。2.2 注册表是怎么做到动态发现技能的注册表的核心是一个扫描器它会在启动时遍历 skills 目录读取每个子目录里的 SKILL.md 和 schema.json把它变成一个 Skill 对象存进内存字典里。为了方便调试我还会在注册阶段做两层校验一是描述文件必须存在且能被解析二是参数 Schema 必须是合法 JSON Schema。# registry.py 简化版 import json from pathlib import Path from dataclasses import dataclass dataclass class Skill: id: str description: str # SKILL.md 中的完整描述 when_to_use: str # 什么场景用来自 SKILL.md parameter_schema: dict # 参数约束 executor: object # 可执行对象 version: str class SkillRegistry: def __init__(self, skills_dir: str skills): self.skills_dir Path(skills_dir) self._skills {} def scan(self): for skill_path in self.skills_dir.iterdir(): if not skill_path.is_dir(): continue skill_id skill_path.name try: desc (skill_path / SKILL.md).read_text(encodingutf-8) schema json.loads((skill_path / schema.json).read_text(encodingutf-8)) executor self._load_executor(skill_path) except Exception as e: print(f[registry] 技能 {skill_id} 加载失败: {e}) continue self._skills[skill_id] Skill( idskill_id, descriptiondesc, when_to_useself._extract_when_to_use(desc), parameter_schemaschema, executorexecutor, versionself._resolve_version(skill_path) ) return self._skills注册表这套东西看起来简单但它是 Agent 技能体系的地基。后续如果要加权限控制、统计调用、灰度发布都能很方便地挂在这层上。2.3 SKILL.md 里该有哪些内容SKILL.md 是整个技能系统里最容易被忽视、却又最决定模型表现的部分。它不像代码不能调试但模型的调用准确率有一半以上取决于这份文档写得是否清晰。我目前的模板包含四块作用概述一句话说清这技能是干嘛的适用条件When to use越具体越好直接告诉模型“看到哪些关键词或哪些意图时才用”不适用条件When NOT to use这块特别重要能大幅降低误调用比例使用示例给一个具体的输入输出示例模型模仿能力很强示例比抽象描述有用得多一个反面案例是我早期写的# 数据搜索技能 可以帮助查数据。这种描述等于没写。模型看完了依然不清楚它可以查什么库支持哪些查询语法返回什么格式我应该传哪些参数结果必然是被各种无关问题触发。2.4 参数 Schema 的约束逻辑参数用 JSON Schema 来声明这一点几乎已经成为行业共识。它的直接好处是能在执行前做一层硬校验减少非法参数对执行器的冲击。更重要的是一份靠谱的 Schema 能引导模型生成更规范的调用。比如时间范围参数如果只写“start_time”模型可能给你任意格式但如果 Schema 里明确标注了格式并写上示例值模型输出的规范率会提升很多。{ type: object, properties: { keyword: { type: string, description: 搜索关键词可以是商品名/类目 }, start_date: { type: string, format: date, description: 开始日期格式YYYY-MM-DD }, limit: { type: integer, minimum: 1, maximum: 100, default: 20 } }, required: [keyword] }这套参数校验的好处还体现在和下游系统的衔接上一旦模型生成格式错误的参数不至于直接抛异常而是能被拦截转成一条友好的重新生成指令。3. 技能描述的五层信息让模型不但“会用”还要“不误用”3.1 从一次失败调用看描述的重要性我遇到过一桩很典型的事故。系统里有一个“天气查询”技能当时我的描述是“查询天气”。结果用户问“明天适合穿什么衣服”模型居然去调了天气查询技能当成了穿衣建议。这种误用不是模型蠢而是技能的“适用条件”没写清楚。后来我在“适用条件”里加了“当用户明确要求气温、降水、风力等信息时使用”并加了“不适用条件当用户询问穿搭建议、出行建议时不调用此技能请根据常识结合天气数据推理回答”。此后这块的误用率明显下降。这件事让我彻底意识到技能描述不是写给开发者看的注释而是写给模型看的路标。描述质量的好坏直接决定调用质量。3.2 五层信息框架我后来总结了一套技能描述的五层信息框架按优先级排列能力边界做什么/不做什么。边界比能力更关键不做什么要写充分才能把误用率打下来适用触发信号用户表达中的哪些词、哪些语义线索。帮模型把“用户的这句话”和“技能的触发条件”对应起来调用前置条件需要什么输入、依赖哪些数据。避免在条件不满足时产生无效调用输出响应格式返回给用户什么是否还需加工。这个信息对后续链路非常关键比如天气返回的温度是不是要再做一层建议使用示例少而精的2~3个示例。模型天然是 few-shot learner示例比任何描述都直接事实上前四层可以用一段自然语言写进 SKILL.md而第五层可以额外放在一个 examples 字段里。两者结合比散装描述更有效。3.3 可用示例对比维度差劲描述有效描述能力边界查询天气仅支持查询指定城市当天气温和降水概率不提供穿衣建议触发信号无当用户询问“某地今天/明天天气”“气温多少度”“会不会下雨”时使用不适用信号无询问穿搭、出行建议时不得调用基于已有天气数据自行回答示例无输入北京明天天气如何输出调 weather 技能city北京date明天日期这组对比能直观看到描述里多一些反例和限定条件模型的行为就能收敛很多。这并不神秘本质是给模型减少猜测空间。4. 从调度层到执行层一次典型的技能调用链路拆解4.1 技能编排中的两层路由当系统里多了一些技能之后就必须考虑编排了。Agent 的对话过程里用户一句话可能涉及多个技能也可能是单技能的简单调用。我习惯把路由逻辑分为两层技能选择Skill Selection根据用户意图和技能描述决定选用哪个或哪几个技能参数填充Parameter Filling为选中的技能生成符合 Schema 的参数这两个步骤可以合并成一次 LLM 调用也可以拆成两次。如果技能数量少、描述短合并调用更省 token但当技能总量超过一定阈值拆开反而更稳因为一次调用里的信息量太大了模型容易在选错技能的同时还把参数弄错。4.2 一次“数据报表查询”的完整调用序列假设用户的问题是“帮我查一下上周各渠道的GMV汇总再对比前一周。”这时技能系统会发生这些事Agent 先把问题送入路由层路由层根据“上周”“GMV”“对比”这些信号从技能库里选出gmv_summary技能和trend_compare技能参数填充阶段为gmv_summary生成参数channelall, start_date2025-01-06, end_date2025-01-12执行前注册表做 Schema 校验日期格式正确、channel 值合法执行器按技能逻辑到数据仓库取数返回结果后生成最终回答这个流程看起来简单但每一步都可能崩。最常出问题的是第 1 步当用户说“上周”时模型需要理解“今天是几号”才能真正算对日期。4.3 时间语义处理最容易踩坑的隐性参数我见过太多技能调用失败是因为没处理时间。你在 Schema 里定义了start_date为“YYYY-MM-DD”但用户说的是“上周”模型要能把它换算成具体日期。这个能力不是模型天生就有的而是需要你在系统层面提供“当前日期”上下文。我的解决方案是在所有技能调用的公共上下文中注入当前日期和星期几同时要求参数填充时遵循一个“日期解析”的提示词规则帮助模型把模糊时间词转换为具体日期。这是一个小到容易被忽略、却极其影响可靠性的点。4.4 执行器返回的标准化执行器返回格式也是设计重点。最开始的版本让每个执行器自由返回结果千奇百怪。后来我统一规定了返回结构{ status: success, data: {}, message: # 给LLM看的简要说明 }如果失败返回{ status: error, error_type: PARAM_INVALID, message: start_date 格式不符应该是YYYY-MM-DD }这一步带来的改善是Agent 拿到结构化的成功或失败结果后能更清醒地决定下一步——是直接回答、还是修正参数重试还是告诉用户能力不足。5. 真实翻车现场一个“技能能发现但选错参数”的完整排查过程5.1 现象描述某次上线后我收到内部体验者的反馈用户问“帮我查一下这个月会员消费 TOP10”Agent 调用了member_query技能但把参数time_range传成了last_3_months结果返回的数据完全对不上。第一反应当然是“参数填充没做好”。但我重新测了几次发现它并不是每次都错而是时好时坏。这就有点麻烦了说明不是简单规则能解决的问题。5.2 排查的第一层是不是技能描述里引入的歧义我去看member_query的 SKILL.md其中有一句“查询会员消费数据可以按时间范围筛选”。这里的问题很明显“时间范围”含义模糊模型不确定用户说的“这个月”应该映射为current_month还是last_3_months。我还在描述里写了一行“如果需要更长期趋势可以扩展时间范围”这直接给模型提供了错误暗示。我决定先改描述明确“这个月”就对应current_month并把时间范围的可选值枚举列清楚。改完测试错误率下降了一些但没完全消除。5.3 排查的第二层是不是 Schema 缺少了枚举约束打开 schema.json发现time_range字段是纯字符串类型没写enum。模型面对一个开放式字符串时生成的偏好会很不稳定。于是我在 Schema 里加上了枚举{ type: string, enum: [current_month, last_month, last_3_months, last_7_days, custom], description: 时间范围枚举custom 表示用户自定义时间区间此时需要配合 start_date/end_date }加了枚举之后模型的选择空间被压缩了误传概率进一步下降。但仍有一个问题为什么模型会在用户明确说“这个月”的时候选中last_3_months5.4 排查的第三层是不是用户的历史对话污染了技能调用翻看日志后我发现了一个共同点出现误传的对话里用户在前几轮曾问过“最近三个月会员增长”之类的问题。上下文里的“三个月”对当前轮次的参数选择形成了干扰。这个发现让我意识到参数填充不只是看当前意图还要避免被多轮上下文误导。解决方案是在技能调用前给参数填充模块增加了一条约束指令请仅根据当前用户问题推断参数不要参考历史消息中的时间信息除非当前问题明确引用了它。并且在 Prompt 里把“当前轮次问题”与“历史上下文”分开标注让模型知道哪些信息是当前决策依据。5.5 排查总结与分析从这次排查里我提取了几条可复用的经验技能描述里的模糊词汇会被模型放大你需要把“时间范围”这类词细化为枚举JSON Schema 的 enum 约束是成本最低、效果最直接的手段多轮污染是 Agent 调用参数不稳定的隐藏元凶不要只盯着技能本身排查顺序要循着“描述 - Schema - 上下文”来而不是一上来就怀疑模型能力这个排查过程深刻地改变了我的编码方式技能系统的任何一个环节从描述到参数到上下文组织都会成为模型质量的一部分。不能把“模型能力”和“系统设计”割裂开来看。6. 什么项目真该用 Agent Skills什么项目别硬上6.1 该用 agent-skills 的信号我简单列了一种“先决条件”清单满足两条以上时认真考虑技能化Agent 需要调用的外部能力超过 5 个新增能力时需要反复修改系统 Prompt 或其他全局配置同一种能力在不同对话链路中需要复用你需要对能力的调用做独立的版本迭代团队里有多个开发者并行参与 Agent 开发在这些条件下技能化带来的结构收益远超它的学习成本。你会明显感到“新增能力”从一个流程变成了一个目录操作。6.2 不该用 agent-skills 的信号但我也要讲点反调。如果你的项目只是在校验大模型能不能解决某个问题总共就两三个函数那别为了“架构感”强行引入技能系统。过早抽象是很多技术债的来源。技能系统会引入这些额外成本每个技能都要写描述、写 Schema维护文档时不轻松动态发现和校验机制需要额外的工程代码模型的技能选择环节本身会引入新的失败模式如果你现在的项目规模还在“能用手写 if-else 或简单工具列表搞定”的阶段就先别上重型框架。技能系统为你节省的是“能力变多之后”的维护成本而不是“能力还少时”的构建成本。6.3 一个渐进式的迁移路线如果你的项目已经处于过渡状态或者你不确定该不该全量迁移可以参考我走的渐进式路径先把工具函数目录化统一挂上SKILL.md和schema.json在启动时扫描加载所有技能但调用时仍然保留原有的显式分发逻辑当显式分发开始变得臃肿或模糊时再把分发逻辑交给模型来做最后补上执行器返回结构化、参数校验、调用日志等能力这个过程不会推翻原有系统每一步都能体会到收益风险也小得多。7. 一些让我最后悔没早点想通的实践心得7.1 技能不只是“函数入口”而是你的产品边界很多开发者把技能系统当成纯工程问题来设计但我做了几轮后感觉它其实是在定义产品的边界。用户透过 Agent 说“帮我统计一下数据”背后能不能找到对应的技能这个技能又能做到什么程度——这本质上是产品能力的一种表达。所以每次新增技能除了写文档我都会问自己用户为什么要用这个技能这个技能能给他在对话里带来什么结果如果回答不了那这个技能本身可能就不该存在。7.2 每隔一段时间要做一次“技能瘦身”技能系统和业务功能一样也会随着时间膨胀。有些技能可能上线后被模型调用过几次就不再使用了有些技能描述已经和实际行为偏离。我一开始没有做这类清理结果很多技能躺在注册表里成为噪音干扰模型的选择。现在的做法是季度性统计查调用日志看看哪些技能在 N 天内零调用再结合当前产品需求决定是下线、合并还是改造。这个动作对控制技能总数、保持系统精简非常有帮助。7.3 日志和数据反馈是技能优化的唯一依据技能描述写得好不好、Schema 定得合理不合理光靠主观评价很难说清。后来我搭了一套简单的调用留存系统每次技能调用都记录用户问题、被选中的技能、影响因子技能 ID、实际参数、执行结果、模型最终回答。有了这批数据很多“凭感觉”的问题就变成了“看数据”的问题。比如某技能的点击率高但用户后续追问多你大概能推断出技能的结果没满足用户期望再比如某技能经常在参数填充环节被重试你就能判断出 Schema 设计还有不足。7.4 别神话 Agent 的“自适应”能力最后想怼一个观点很多人以为 Agent 有了技能系统以后就能自己学会用什么、不用什么。实际不是这样。技能系统只是把决策信息组织得更清晰最终的决策者还是模型本身。模型并不会“偷偷进化”能把调用准确率打上去的依然是你对技能描述的打磨和 Schema 的严格约束。认识到这一点就不会再沉迷于“搭建一个完事”的幻想而是把功夫花在真正有效的地方。
返回列表