ARTICLE DETAIL

资讯详情

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

Agent技能系统设计:从工具调用到标准化工作流的工程实践

Agent技能系统设计:从工具调用到标准化工作流的工程实践 做AI应用的兄弟应该都有过这种经历模型换了一代又一代Prompt技巧卷了一篇又一篇结果落到真实业务里产品还是像个嘴强王者——问什么答什么很溜一让它正经干活就抓瞎。问题不出在模型智商出在能力供给Agent没有可复用的手。这也是我去年开始维护agent-skills的初心把智能体的做事能力标准化、模块化让模型想得到且用得来真正能落地的技能。这篇不写概念PPT直接讲设计选型、代码骨架、以及我踩过的坑希望给正在做Agent工程化的同行一点参考。1. 项目概述Agent为什么需要技能而不是工具先说结论工具Tool是点状能力技能Skill是线状工作流。如果你的Agent只需要调用一两个API工具调用Function Calling完全够用但一旦业务涉及到目标拆解、多步操作、条件分支、异常恢复工具就会显得非常碎片化。agent-skills项目解决的正是从能调接口到会做事之间的距离。1.1 从Function Calling到技能系统到底进化了什么早期做Agent常规做法是给模型塞几个Function让它在对话中决定调哪个。这个方案最大的问题有三个上下文失控每个工具的完整描述都在系统提示词里工具一多上下文很快被撑爆模型的选择准确率直线下降。能力不可编排工具只解决单次调用但真实业务流程往往是A结果作为B参数、失败时走C方案这种逻辑靠Prompt硬写每次新增需求都要改提示词维护成本极高。复用性差同一个抓取网页正文并提取结构化数据的能力在A场景写一遍到B场景又要重写代码重复率极高。技能系统的核心思路是把工具定义、调用策略、输入输出约束、边界条件打包成一个独立单元。模型只需要知道我有这些技能可用每个技能的完整细节在需要时才动态加载。用生活类比来说工具是工具箱里一把把单独的螺丝刀技能则是换一个设备屏幕总成的完整工序——它知道要用几号螺丝刀、先拆哪颗螺丝、遇到卡扣怎么处理、装回去怎么测试。1.2 agent-skills解决的四个核心问题我在设计项目时给自己定了四条硬指标对应Agent落地时最常见的四类痛点标准化所有技能遵循同一套定义规范无论是内部开发还是社区贡献接入成本都接近零。可发现Agent能在上百个技能里快速检索出与当前任务最匹配的3-5个候选而不是把全部技能描述塞进上下文。可编排技能之间能组合调用一个复杂任务可以拆成多个技能按顺序执行且执行顺序和条件分支由模型动态决策。可治理每个技能有独立的权限声明、超时控制、调用审计和生产/灰度开关避免Agent乱调东西。这四个问题不解决Agent规模一上来就是灾难。我见过有团队把60多个工具全部放进System Prompt结果模型回答质量肉眼可见地下降每次API调用光系统提示词就吃掉两万多token。技能系统把这种全量注入改成按需加载之后上下文清爽了很多模型的选择准确率也明显回升。2. 核心细节解析技能系统的骨架设计这一节是agent-skills最核心的部分一套技能系统到底由哪几块组成每一块在设计时有哪些不能妥协的细节。我把整个架构拆成三层定义层、注册层、调度执行层下面逐个讲透。2.1 技能的标准定义格式一个技能的本质是一个被LLM理解和触发的可执行单元。为了让模型和代码都能高效处理我定义了一套元数据结构核心字段如下字段类型说明namestring技能唯一标识小写下划线风格如web_fetch_contentdescriptionstring给LLM看的使用说明书决定它会不会在正确时机选中该技能parametersJSON Schema定义入参结构既让LLM知道怎么填参也让运行时能做校验executecallable真正的执行函数接收**kwargs参数并返回结构化结果metadataobject标签、权限级别、超时时间、启用状态、作者、版本等管理信息选JSON Schema作为参数规范我试过之后觉得它有四个不可替代的优势第一OpenAI/Claude/本地模型的Function Calling协议原生支持几乎零转换成本第二可以用现成的校验库在调用前拦截非法参数避免脏数据进入执行层第三能自动生成API文档和类型提示前后端协作省很多事第四Schema本身可嵌入LLM上下文模型理解成本最低。下面是一个技能定义的最小示例我直接用Python装饰器实现了声明式注册# skills/web.py from agent_skills import skill skill( nameweb_fetch_content, description抓取指定URL的网页正文内容并去除导航、广告等噪声。适合用于信息检索、文章总结、内容分析等场景。当用户提供链接并希望了解页面内容时使用。, parameters{ type: object, properties: { url: { type: string, format: uri, description: 需要抓取的完整网页地址必须包含协议头如 https://example.com/article/123 }, max_length: { type: integer, minimum: 500, maximum: 20000, default: 8000, description: 返回正文的最大字符数默认8000字 } }, required: [url] }, timeout30, permissionread_web ) def web_fetch_content(url: str, max_length: int 8000) - dict: # 实际抓取与正文提取逻辑 ... return {title: title, content: content, source: url}这段代码是项目的骨架肌肉。注意我故意把max_length限制在500到20000之间这个细节很重要范围太小导致每次都要让模型重新决策范围太大则模型可能填出极端值造成资源浪费给参数设一个合理的业务边界是降低模型决策压力的关键技巧。2.2 技能描述Agent的使用说明书比代码更影响效果很多人在做技能定义时把90%精力花在写执行逻辑上对description草草写一句完事。这是大错特错。在agent-skills的架构里description是影响技能被正确触发的最重要因素没有之一。因为LLM不会看你的代码它只能通过描述来理解这个技能是干嘛的、什么场景用、什么场景不能用。我总结了写description的三个准则触发条件前置在前半句直接写清楚当用户需要X时使用让模型能快速匹配。边界条件显式声明写明什么时候不要用能大幅降低误调用率。参数与行为描述具体说明关键参数的含义和默认行为让模型填参时有据可依。对比一下坏描述和好描述的区别坏描述抓取网页内容。好描述抓取指定URL的网页正文内容并去除导航、广告等噪声。适合用于信息检索、文章总结、内容分析等场景。当用户提供链接并希望了解页面内容时使用。如果URL无法访问或返回非HTML内容请勿使用本技能。坏描述的问题是模型不知道这个技能和另一个web_search技能有什么区别遇到帮我看一下这篇文章讲了什么时它可能随手选搜索也可能选抓取行为完全不可控。好描述把适用场景和边界都框清楚了模型的选择稳定性会高非常多。2.3 技能注册、发现与隔离技能定义好了要能被Agent看见需要过三道关注册、发现、隔离。注册agent-skills在启动时会扫描技能目录读取所有技能模块校验元数据和参数Schema然后写入一个技能注册表Skill Registry。注册失败的技能不会进入注册表但会记录详细的错误日志方便开发时排查。这样做的好处是不会因为某一个技能写坏了导致整个Agent无法启动。发现Agent拿到用户请求后先在技能注册表里做检索。我目前实现了两种检索策略一种是关键词匹配适合技能名称和标签定义良好的场景另一种是Embedding向量检索把用户请求和技能描述都转成向量取相似度Top K。实际生产中我愿意用混合检索先用关键词粗筛再让模型从候选列表里做最终选择。这一步直接决定了上下文里塞哪些技能描述检索质量比模型能力更影响最终效果。隔离这里要强调的是权限和运行环境隔离。每个技能声明自己的permission级别比如read_web只允许读公开网页write_file需要额外授权。运行时敏感技能在独立子进程或容器里执行避免Agent因为一次恶意输入把整个宿主环境搞崩。哪怕是自己内部用隔离层也一定要做因为LLM的输入是不可完全信任的——你永远不知道用户会往参数里塞什么。3. 实操过程从零搭一套Agent技能系统聊完设计直接上实操过程。我用一个具体案例演示给Agent添加一个网页抓取与结构化总结技能并从提问到产出走一遍完整调用链路。这套流程我基于Python 3.11实现核心依赖只有两个一个HTTP客户端和一个OpenAI兼容的SDK整体在普通开发机上就能跑。3.1 最小工程骨架技能目录与加载器先建项目结构我习惯把技能按领域分目录管理每个目录下可以有多个技能文件和一个README说明本领域的技能约定agent-skills/ ├── agent/ │ ├── core.py # Agent主循环规划、调用、回填 │ ├── registry.py # 技能注册表 │ ├── dispatcher.py # 调度执行器 │ └── memory.py # 上下文记忆管理 ├── skills/ │ ├── __init__.py │ ├── web/ │ │ ├── web_fetch.py # 网页抓取技能 │ │ ├── web_search.py # 网页搜索技能 │ │ └── __init__.py │ ├── data/ │ │ ├── csv_parse.py # CSV解析技能 │ │ └── __init__.py │ └── code/ │ ├── run_python.py # 代码执行技能 │ └── __init__.py ├── config.yaml └── requirements.txt加载器的核心逻辑很简单扫描目录、动态导入、批量注册。我在registry.py里写了一个扫描函数它会递归查找所有skill装饰器标记过的函数并注册进内部字典# agent/registry.py import importlib.util import inspect from pathlib import Path from agent_skills import SKILLS_REGISTRY def load_skills_from_directory(skills_dir: str) - int: loaded 0 for path in Path(skills_dir).rglob(*.py): if path.name.startswith(_): continue # 动态导入模块 spec importlib.util.spec_from_file_location(path.stem, path) module importlib.util.module_from_spec(spec) spec.loader.exec_module(module) for _, obj in inspect.getmembers(module): if callable(obj) and hasattr(obj, _skill_meta): skill_name obj._skill_meta[name] SKILLS_REGISTRY[skill_name] obj loaded 1 return loadedinspect.getmembers会把模块里所有callable对象扫一遍通过是否有_skill_meta标记来判断是不是技能。这个方案的好处是灵活新增技能只需要放一个文件重启服务自动加载不需要改任何注册代码。我之前试过用集中式YAML配置文件来维护技能清单后来发现技能一多那个YAML就成了没人敢改的危险文件因为每加一个技能YAML和代码的同步全靠自觉。用装饰器自动扫描的方式定义和注册天然在一起永远不会脱节。3.2 编写一个经得起实测的技能网页抓取与总结光有骨架还不够我挑一个真实技能来演示从定义到可用的完整过程。这个技能叫web_fetch_content目标很单纯输入一个URL输出干净的正文内容。执行逻辑我直接借助readability和requests两个库代码很短但有几个关键细节值得说# skills/web/web_fetch.py import requests import readability from bs4 import BeautifulSoup from agent_skills import skill skill( nameweb_fetch_content, description抓取指定URL的网页正文内容并去除导航、广告等噪声。适合用于信息检索、文章总结、内容分析等场景。当用户提供链接并希望了解页面内容时使用。, parameters{ type: object, properties: { url: {type: string, format: uri}, max_length: {type: integer, minimum: 500, maximum: 20000, default: 8000} }, required: [url] }, timeout30, permissionread_web ) def web_fetch_content(url: str, max_length: int 8000) - dict: # 1. 请求网页时带上用户代理避免被部分网站直接拒绝 headers {User-Agent: Mozilla/5.0 (compatible; AgentSkillBot/1.0)} resp requests.get(url, headersheaders, timeout20) resp.raise_for_status() # 2. 用 readability 提取正文同时处理编码问题 doc readability.Document(resp.text) content doc.summary(html_partialTrue) soup BeautifulSoup(content, html.parser) text soup.get_text(separator\n).strip() # 3. 按上限截断并保留标题 if len(text) max_length: text text[:max_length] \n...[内容已截断] return {url: url, title: doc.short_title(), content: text, length: len(text)}这里每一个步骤都踩过坑。第一步加User-Agent是因为很多站点对空UA的爬虫会直接返回403不加这个头技能到生产环境有一半概率是废的。第二步用readability而不是直接用BeautifulSoup提取是因为后者只能拿HTML结构没法判断哪块是正文哪块是导航而 readability 会用算法计算正文密度准确率高一个量级。第三步把raise_for_status()接着错误处理是后面排查问题时的生命线——我遇到过太多技能静默失败、Agent自己脑补返回结果的情况返回异常而不抛异常LLM就会一本正经地编答案。3.3 完整调用链路从用户提问到结果回填技能定义好接下来是最关键的一环Agent规划、技能发现、参数填充、执行、结果回填这五步构成一次完整的技能调用闭环。我用一个实例场景演示用户提问帮我抓取 https://example.com/blog/agent-skills-summary 这篇文章然后用三个要点总结它的核心观点。一个配置了web_fetch_content技能的Agent会这样工作步骤1意图识别。模型判断任务需要获取网页内容和总结摘要两步操作后者可以直接在对话里完成不需要额外技能。步骤2技能发现。检索器在技能注册表里匹配web_fetch_content把技能描述和参数Schema注入当前上下文。步骤3参数填充。模型从用户输入里抽取URL和默认参数生成一次符合Schema的调用请求。步骤4执行。调度器调用技能函数将返回结果暂存在上下文记忆里这一步要记录耗时、状态码、返回内容和异常信息。步骤5结果回填。模型读取技能返回的正文内容执行三要点总结的用户指令形成最终回复。整个链路看起来只有五步但每一步都可能出问题参数抽取可能漏掉URL技能可能因为网络问题抛异常返回内容可能超出上下文窗口。所以我在调度器里给每一次技能调用都加了完整的调用审计日志后面排查问题全靠它。这个调用的时序在工程实现上大约是这样# agent/core.py伪代码 async def run_request(user_input): # 1. 生成Agent规划 plan await planner.create_plan(user_input) final_result [] for step in plan.steps: if step.type skill: # 2. 发现候选技能 candidates await registry.search(step.task) # 3. 让模型选择技能并填充参数 skill_choice await llm.select_skill(candidates, step.task) # 4. 执行并校验结果 result await dispatcher.execute(skill_choice.name, skill_choice.arguments) log_audit(skill_choice.name, skill_choice.arguments, result) final_result.append(result.to_text()) else: final_result.append(await llm.respond(step.task)) # 5. 汇总结果 return await llm.summarize(final_result)run_request这里的核心思想是不要把技能执行结果直接扔回LLM而要先转成紧凑文本。比如把网页正文压缩成8000字以内的简洁摘要再把摘要喂给模型做总结。这能显著降低上下文占用和费用让Agent在长流程任务里跑得更稳是我实际用了很久之后才发现大有裨益的设计。4. 常见问题与排查技巧实录技能系统跑起来之后接下来才是真正考验耐心的阶段。我把过去几个月积累的高频问题和排查思路整理成速查表每一条都是真实踩坑换来的经验。4.1 描述模糊导致Agent选错技能现象Agent明明有A技能却在类似场景里频繁选择B技能或者干脆绕开技能自己编答案。原因描述里没有写清楚触发条件和边界模型在技能选择时陷入困惑只能靠猜。排查方法打开调用日志找出模型实际看到的候选技能描述站在模型视角评估如果我只看这些描述会不会选错答案是会那就改描述。经验描述里要有什么场景用它和什么场景别用它这两类信息前者提升召回后者降低误报。改完描述后记得做回归测试——把前几周翻过车的真实对话场景重新跑一遍看错误率有没有下降。4.2 参数Schema过严或过松都会导致失败现象技能调用成功率低于预期日志里大量validation_error或者模型频繁要求用户补充信息。原因Schema设计不合理。太严——比如把url格式限定为必须以.html结尾很多合法页面就被拒了太松——比如所有参数都设为可选且没有范围限制模型就会填出乱七八糟的值。处理心得给参数设合理边界但不设无意义的格式限制。像URL就只校验是否以http/https开头不要管域名后缀max_length设一个业务合理区间500-20000而不是让模型随便传。凡是模型经常填错的地方要么是描述不够细要么是Schema缺少default值。4.3 技能冲突与命名空间隔离现象加了新技能之后原有技能开始间歇性失灵Agent偶尔调用到错误的技能。原因技能命名全局冲突。比如有两个技能都想叫fetch_page后注册的覆盖了先注册的或者两个技能描述高度相似模型无法区分。解决方案我后来把每个领域目录都加了前缀命名空间比如web_fetch_content、data_csv_parse让技能名自带领域信息。同时在注册阶段做重复名校验发现冲突直接拒绝启动并报错而不是静默覆盖。这个宁可启动失败不要运行时诡异行为的原则帮我挡掉了不少潜在线上事故。4.4 上下文窗口溢出与结果回填超限现象技能返回了超长内容比如一个10万字的网页Agent在下一次请求时上下文爆掉或者API直接报context_length_exceeded。原因直接把技能原始返回结果整个丢给LLM没有做截断和压缩。我的做法给每个技能的输出加一个return_summary策略能在执行层就完成文本压缩的尽量在返回前完成不能压缩的设置max_length强制截断同时在调度器里维护上下文预算当累计token超过阈值时先把历史摘要替换掉再做后续步骤。这套预算制上下文管理基本杜绝了因为返回结果超大导致的整条链路瘫痪。4.5 技能热更新与缓存问题现象新版本技能部署后Agent还在用旧逻辑跑而且不报错只有在对比日志时才发现行为不一致。原因技能模块在启动时被解释器缓存后续虽然覆盖了文件但运行中的进程不会重新加载。处理方案在注册表里维护一份技能版本号和文件hash每次调用前做一次轻量比对版本变化则触发重新加载。如果技能不频繁变动也可以用消息队列下发技能变更通知让Agent优雅重启。千万不要蠢到每次请求都重新import一遍那会造成模块重复初始化带来更大的混乱。这里有个小技巧在开发环境用自动重载在预发/生产环境用显式发布按钮两条通道彻底分离从机制上杜绝误操作。最后再分享一个实用经验技能系统的设计说到底是在模型自主性和工程可控性之间找平衡。我在实际运行agent-skills的过程中最深的体会是技能的粒度直接决定系统的成败。粒度太粗——一个大技能里塞了抓取、清洗、摘要、翻译四件事看起来什么都能干实际上一出问题你根本不知道是哪个环节挂了而且模型很难灵活组合其他技能来应对变化粒度太细——一个读取环境变量都做成技能注册表几百项检索成本和模型选择负担反而成为新的瓶颈。我的建议是从业务上一个可独立验证、可独立测试的最小环节起步把技能看作微服务而不是函数。比如网页抓取是一个技能正文清洗是另一个技能两个技能可以独立上线、独立回滚也可以组合成一个网页内容采集流水线。这样的设计让系统在面对需求变化时可以从容组合而不是频繁重写。另外日志和审计一定不要省。技能调用日志不仅要记录参数和返回结果还要记录模型选中每个候选技能的推理路径如果提供方API支持的话以及每次技能调用的延迟、token消耗和异常堆栈。很多Agent系统在demo阶段什么问题都没有一上真实流量就崩绝大多数原因不是模型不行而是你根本不知道技能在执行时发生了什么。好的日志等于给Agent装了一个黑匣子。最后说一句个人观点Agent的竞争力未来不在模型本身而在它拥有的技能生态。谁先建好一套标准、开放、可插拔的技能体系谁就能在业务落地上快人一步。希望这篇agent-skills的拆解能给你带去一点参考也欢迎在评论区聊聊你在技能设计上踩过的坑。
返回列表