ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Agent技能体系设计:从堆工具到搭体系,解决多工具调度难题

Agent技能体系设计:从堆工具到搭体系,解决多工具调度难题 去年我做了一个内部使用的 Agent 项目一开始功能很简单就几个工具挂上去效果也还行。等场景一多、工具一多问题马上来了Agent 经常调错工具或者干脆不调明明该用搜索的时候它跟你硬编该算数的时候给你一本正经胡说八道。后来我把重心从“拼命堆工具”转到了“搭一套技能体系”上也就是项目名里写的 agent-skills才算把这堆问题理顺。这篇文章就把我这套体系的设计思路、目录规范、注册机制、调用决策和踩坑记录完整拆开讲。不是放一个 Demo 出来糊弄人而是聚焦在“如果你的 Agent 要上生产、接十几个技能这套东西该怎么组织和维护”。适合正在做 Agent 应用、被多工具调度搞得头疼的开发者也适合准备从 Demo 往工程化方向走的朋友。1. 为什么需要一套 Agent 技能体系而不只是堆工具1.1 从 Demo 到生产Agent 项目最大的坑在哪里先回忆一下大多数人的 Agent 起步路径调一个 LLM 的 API写两个函数用 function calling 把函数注册进去跑通一个用例就觉得很爽。我自己也是这么过来的。但一旦真做起产品工具的粒度、描述、参数、边界都开始出问题。核心矛盾其实是一个LLM 的调度能力不是无限的它对工具的“理解”完全依赖于你给它的描述和参数定义。工具越细碎描述越含糊它就越容易做出错误选择。比如你有两个技能一个是“查天气”一个是“查空气质量”底层调的是同一个第三方接口只是返回字段不同。你如果描述写得含糊Agent 就可能拿“查天气”的技能去回答空气质量问题然后理直气壮地给出一堆温度湿度。磨刀不误砍柴工。你需要的不是更多工具而是一套有结构、有规范、有边界的“技能库”让 Agent 在每次决策时面对的是经过精心设计的选项而不是一团乱麻。1.2 Agent Skill 到底是什么和“工具”“插件”有什么区别现在业界对 Skill、Tool、Plugin 这几个词用得非常混。按我这边的理解把它们落到工程上可以这么区分Tool工具最细粒度的可执行单元一般是一个函数、一个 API 调用比如“获取当前时间”“调用某某搜索接口”。Skill技能面向“任务目标”封装的能力单元内部可能组合多个工具、提示词模板、执行策略和后处理逻辑。比如“市场调研”是一个技能它可能要调用搜索工具、网页抓取工具、信息整理模板最后输出结构化报告。Plugin插件更重的集成单元通常带 UI 或者外部系统的深度绑定比如一个飞书插件、一个 Photoshop 插件。所以 Agent Skill 更像是“给 Agent 配置的一项可复用能力”它不只是把一个函数暴露给模型而是把完成某类任务所需的全部要素打包。整体结构可以参考下方代码目录。skills/ ├── web_research/ │ ├── SKILL.md │ ├── input_schema.json │ └── execute.py ├── calculator/ │ ├── SKILL.md │ ├── input_schema.json │ └── execute.py └── report_generator/ ├── SKILL.md ├── input_schema.json └── execute.py1.3 技能体系要解决的三个核心问题我设计这套东西时目标非常明确就三个问题。第一是决策准确性问题。Agent 面对十几个技能时必须能根据用户请求快速选对技能并给出合法参数。这依赖技能描述是否清晰、参数定义是否完备、技能之间是否有明显边界。第二是代码复用与工程维护问题。技能不能只为一两个场景写死它应该能以配置化、模块化的方式被多个 Agent 复用。你给客服机器人写的“订单查询”技能完全可以给内部运营助手再挂一份。第三是可观测与可回滚问题。生产环境里的 Agent技能调用链路必须能追踪。用户说了一句话Agent 为什么选了这个技能参数是什么执行结果是什么这些日志你得能完整拉出来。后面所有设计都是围绕这三件事展开的。技术选型反而不是最重要的你用什么框架都行核心是把“技能”当一等公民来设计而不是随手在代码里加个 if-else。2. 技能结构拆解一个能稳定工作的 Skill 长什么样2.1 描述文件是灵魂比代码重要得多很多人做 Agent 项目喜欢先把执行函数写出来描述随便填两句。我见过最离谱的是描述只写了四个字“处理订单”然后抱怨 Agent 老调错。模型不是人它没法从“处理订单”四个字里知道你处理的是电商订单还是工单是查询还是退货。在我这套体系里每个技能必须有一个标准的描述文件 SKILL.md它是 Agent 决策时的主要依据。描述文件里至少要包含几个部分技能名称英文 ID、显示名称、一句话概述、详细说明、适用场景、不适用场景、输出说明、使用限制。下面给你看一个写法和一个反例你就明白差距了。--- name: order_query display_name: 电商订单查询 description: 根据用户提供的订单号查询电商平台的订单状态、物流信息和商品明细。 applicable_scenarios: - 用户想查“我的订单到哪了” - 用户提供订单号并询问订单状态 - 用户询问某笔订单包含哪些商品 not_applicable_scenarios: - 用户想退货或换货请引导至售后流程 - 用户想查询线下门店订单数据源不同 output_format: JSON包含 order_status、items、logistics 字段 usage_limits: 每分钟最多调用 30 次超过返回限流错误这是反例写法--- name: order display_name: 订单 description: 处理订单相关需求。 ---只要你的技能描述长成反例那样Agent 不调错才是怪事。模型做工具选择时本质上是在做一次文本匹配加语义推理你的描述越是有区分度、越明确边界它的决策就越准。2.2 参数定义里藏着 80% 的调用错误技能描述负责让 Agent 选对技能参数定义则负责让 Agent 把参数传对。很多人卡在这一步Agent 明明选对了技能执行时报 Missing required parameter 或者参数类型不对跑去追问模型模型又绕回一句“请检查您的输入格式”。问题通常出在参数定义写得像摆设。我给一个订单查询技能定义过这样的参数格式{ type: object, properties: { order_id: { type: string, description: 订单号纯数字字符串例如 202406120001, minLength: 12, maxLength: 20 }, customer_phone: { type: string, description: 下单手机号11 位用于身份校验可省略, pattern: ^1\\d{10}$ } }, required: [order_id], additionalProperties: false }单个参数定义做到三件事类型明确、格式示例、校验规则。模型其实很擅长按样例填参数你给它一个正经示例它就能举一反三。你再对比那种只写“订单号”三个字的参数定义效果完全两码事。2.3 执行函数内部也要有门道前置校验、重试与后处理技能描述和参数定义是给模型看的执行函数是给自己写的。我建议执行函数至少分成三段逻辑。第一段是入参校验。模型生成的参数有可能不合法比如订单号长度不对、日期格式错了。你不能直接拿出去查数据库要在执行函数入口做一次严格校验不合法就直接返回错误码而不是让异常冒泡。第二段是核心业务逻辑。这里可以调外部 API、查数据库、做计算。这一层要尽量做到无副作用、可重试。第三段是后处理与格式化。模型直接拿到原始 API 返回的 JSON 时经常会被一堆无用字段干扰。你要在技能内部把关键信息提取出来重新组织成简洁、结构清晰的文本或 JSON再交还给 Agent 做最终回答。def execute(order_id: str, customer_phone: str None): # 第一段前置校验 if not order_id or len(order_id) 12: return {success: False, error_code: INVALID_ORDER_ID, message: 订单号格式不正确} # 第二段核心业务逻辑调查询 API raw_result query_order_api(order_id, customer_phone) # 第三段后处理与格式化 simplified { order_status: raw_result[status][text], items: [{name: i[title], qty: i[quantity]} for i in raw_result[items]], logistics: raw_result.get(logistics, {}).get(latest_trace, 暂无物流信息) } return {success: True, data: simplified}2.4 技能互斥与优先级别让两个技能打架工具一多很容易出现“两个技能好像都能干这件事”的情况。比如“查天气”和“查穿衣建议”都能接收城市名前者返回天气后者返回推荐穿搭。如果不做边界切割Agent 大概率选偏。我常用的做法是画一个技能边界矩阵把容易混淆的技能两两拎出来明确各自的适用场景和不适用场景。还不行的话就在描述文件里加一条“优先选择规则”明说“如果用户同时提到天气和穿衣建议优先使用穿衣建议技能并在回复中附带天气信息”。另外在注册技能的时候我会给每个技能加一个“触发强度”标签。像“定时提醒”这种触发意图非常明确的就高像“通用文本处理”这种啥都能沾边的就给低优先级。调度层排序时会参考这个标签减少误命中。3. 实操我把“技能”做成了可插拔的模块化组件3.1 目录规范先走起来所有花里胡哨的设计落到代码里第一件事就是目录。我最终定下来的目录结构长这样agent-skills/ ├── core/ │ ├── registry.py # 技能注册中心 │ ├── dispatcher.py # 调度器根据模型决策执行技能 │ └── schema.py # 技能数据模型定义 ├── skills/ │ ├── __init__.py │ ├── web_research/ │ │ ├── SKILL.md │ │ ├── input_schema.json │ │ ├── execute.py │ │ └── requirements.txt │ ├── calculator/ │ │ ├── SKILL.md │ │ ├── input_schema.json │ │ └── execute.py │ └── ... ├── prompts/ │ └── selector.md # 给 LLM 用的技能选择提示词 └── logs/ └── skill_trace.log # 技能调用日志每个技能目录独立自带说明、参数定义、执行代码和依赖声明。好处是团队协作时互不干扰你新增一个技能只需要新建一个目录注册中心能自动扫描加载。坏处是如果你完全不重视规范目录会变成垃圾场所以“技能命名唯一、描述必填、参数必校验”这些规矩必须靠 review 守死。3.2 注册中心与自动加载机制我先定义了一个统一的技能类核心字段包括 name、description、input_schema、execute_fn。所有技能都实现同一个接口。# core/schema.py from dataclasses import dataclass from typing import Callable, Any dataclass class Skill: name: str description: str input_schema: dict execute_fn: Callable[..., Any] priority: int 5注册中心做的事情很简单启动时扫描 skills 目录下所有子目录读取 SKILL.md 和 input_schema.json动态加载 execute.py 中的 execute 函数生成一个 Skill 实例并注册到内存字典里。这里的核心逻辑是“约定大于配置”每个技能目录都必须有那三个文件否则跳过并打警告。# core/registry.py import importlib.util import json from pathlib import Path from core.schema import Skill SKILL_REGISTRY {} def load_skills(skills_root: str skills): root Path(skills_root) for skill_dir in root.iterdir(): if not skill_dir.is_dir(): continue skill_md skill_dir / SKILL.md schema_json skill_dir / input_schema.json execute_file skill_dir / execute.py if not (skill_md.exists() and schema_json.exists() and execute_file.exists()): print(f[warn] 技能目录 {skill_dir.name} 文件不完整已跳过) continue # 解析 SKILL.md 的 YAML front-matter meta parse_front_matter(skill_md.read_text(encodingutf-8)) schema json.loads(schema_json.read_text(encodingutf-8)) # 动态导入 execute.py 并找到 execute 函数 spec importlib.util.spec_from_file_location(fskills.{skill_dir.name}.execute, execute_file) mod importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) skill Skill( namemeta[name], descriptionmeta[description], input_schemaschema, execute_fnmod.execute, prioritymeta.get(priority, 5) ) SKILL_REGISTRY[skill.name] skill print(f[info] 已加载 {len(SKILL_REGISTRY)} 个技能)实际部署时我通常还会把技能元信息存一份到数据库或 Redis方便可视化后台查看哪些技能在线、哪些技能报错率过高。不过起步阶段用文件目录就足够了。3.3 调度器把“选择权”交给模型但也别全交给模型技能加载完之后核心是调度器。流程大概是拿到用户输入把技能列表和用户意图一起发给模型让模型返回要调用的技能名和参数然后调度器执行并返回结果。这里有几个细节值得单独说。一是技能列表怎么发给模型。如果你有 30 个技能全部塞进 system prompt 里既费 token 又容易干扰模型注意力。我的做法是给每个技能维护一些“触发关键词”先在本地做一轮粗筛选再只把候选技能的描述发给模型。比如用户输入里有“订单”“快递”“物流”先把这三个词和技能索引里的关键词做匹配筛掉明显无关的再进入 LLM 决策阶段。二是模型返回的解析要健壮。有些模型会规规矩矩返回 JSON有些会在 JSON 外面包一层 markdown 代码块。解析时一定要做容错提取第一组花括号出来而不是直接 json.loads。# core/dispatcher.py import json import re def parse_model_skill_choice(raw_content: str): # 去掉可能的 markdown 代码块标记 content raw_content.strip() content re.sub(r^(?:json)?|$, , content, flagsre.MULTILINE).strip() try: return json.loads(content) except json.JSONDecodeError: # 兜底尝试提取第一组花括号内容 match re.search(r\{.*\}, content, re.DOTALL) if match: return json.loads(match.group()) raise ValueError(f无法解析模型输出: {raw_content})三是执行结果的反馈回路。技能执行成功或失败要把结构化结果返回给模型由模型决定是直接回答用户还是需要再调一次别的技能。这个“模型—技能—模型”的循环就是 Agent 的核心工作方式。def run_agent_task(user_input: str): candidates retrieve_candidate_skills(user_input) prompt build_selector_prompt(user_input, candidates) model_output call_llm(prompt) decision parse_model_skill_choice(model_output) skill SKILL_REGISTRY.get(decision[skill_name]) if skill is None: return 抱歉我没有找到可用的技能处理这个请求。 result skill.execute_fn(**decision.get(arguments, {})) answer call_llm(f用户的问题是{user_input}\n\n技能执行结果如下\n{json.dumps(result, ensure_asciiFalse)}\n\n请基于以上信息回答用户。) return answer这段代码只体现主干流程。真实项目里你还需要处理多轮调用、技能执行超时、模型单次决策是否要限定只调一个技能等策略。3.4 给 Prompt 里加上“技能选择说明”技能调度能不能做对一大半取决于你给模型的“选择说明”写得是否清楚。我系统里有一份 selector prompt固定以“你是任务调度助手负责将用户请求分配给合适的技能”开头。然后依次列出候选技能的信息最后给几个规则。最重要的一条规则是如果没有任何技能能处理用户请求直接返回 no_skill_match不要硬编。没有这条规则的时候模型遇到没见过的问题会强行选一个最像的技能然后执行结果往往是错的或者莫名其妙。我再贴一个简化版选择说明的骨架你是任务调度助手。以下是从技能库中筛选出的候选技能。 skills {{skills}} /skills 请根据用户请求选择一个技能并提取参数输出 JSON 格式 {skill_name: 技能名, arguments: {...}} 规则 1. 只从候选技能中选择不能自行臆造技能名。 2. 如果没有任何技能能合理处理请求输出 {skill_name: no_skill_match, arguments: {}}。 3. 参数值必须从用户输入中提取不要编造缺失信息。 4. 如果用户输入缺少必要参数arguments 里只填已有的字段并在回复中说明缺什么。3.5 可观测性没有日志的技能库等于盲飞前面说了生产环境里技能调用链路必须能追踪。我在 dispatcher 里每一步都埋了日志用户输入原文、筛出的候选技能、模型返回的决策 JSON、技能执行耗时、执行结果摘要、最终回答。每一条日志都带 request_id方便把一次会话的完整链路串起来。这是我在项目里实际打印的字段样例2025-06-15 14:23:01 [INFO] request_idreq_001 用户输入: 帮我查一下订单202406120001到哪了 2025-06-15 14:23:02 [INFO] request_idreq_001 候选技能: order_query(0.92), order_return(0.31) 2025-06-15 14:23:04 [INFO] request_idreq_001 模型决策: {skill_name:order_query,arguments:{order_id:202406120001}} 2025-06-15 14:23:05 [INFO] request_idreq_001 执行耗时: 0.38s 2025-06-15 14:23:05 [INFO] request_idreq_001 执行结果: {success:true,order_status:已签收,items:[...]}有了这份日志你才能回答“Agent 今天为什么抽风了”这种终极问题。我见过太多项目跑得好好的一上线就崩连样例都没有然后只能靠肉眼猜模型哪一步选错了。日志不是给老板看的 KPI是给你自己排障的底气。4. 常见问题与排查技巧技能多了以后什么妖魔鬼怪都有4.1 Agent 死活不调用技能而是自己胡编答案很多人遇到的第一座大山就是技能就在注册表里模型偏不用直接凭训练记忆回答。比如你接了一个实时天气技能模型却告诉你今天气温 25 度实际上是三天前的旧数据。这个问题十有八九出在技能描述与实际能力的匹配度上。模型觉得你的技能描述不够“强”或者它压根没意识到这个请求需要外部数据。我会做两件事一是把技能描述里加上“实时数据”“必须调用此技能获取最新信息”这类强引导词二是在系统 Prompt 里写死一句话比如“涉及实时数据、用户私有数据、具体订单信息等内容一律不得凭空编造必须调用对应技能获取”。还有一种情况是候选技能根本没被送进模型上下文。我遇到过过滤逻辑把关键词匹配写得过严用户换了种表达就筛不到了。这时候查日志看候选技能列表是最快的。4.2 两个技能高度相似Agent 频繁选错技能边界没切干净是调度准确率的大敌。我之前同时挂了“周报生成”和“日报生成”两个技能描述都类似。模型经常用户说“总结一下这周的工作”时调成日报生成输出格式全对不上。我的解法是三种手段叠加第一把两个技能的描述差异拉到最大周报着重写“一周汇总、本周完成、下周计划”日报着重写“当日记录、工作内容、明日安排”第二在各自 not_applicable_scenarios 里明确写不能覆盖对方的场景第三给“周报生成”加一个高优先级标签让调度器在语义模糊时优先选它。这些手段不一定每个都能 100% 生效但叠加在一起能把准确率从 70% 拉到 90% 以上。4.3 模型决策时把参数传错或传漏参数问题也极其常见。用户输入里同时有订单号和手机号模型只传了订单号导致身份校验没过。这里别急着怪模型看下 input_schema 里有没有把参数的可选性、缺省时的兜底逻辑说清楚。另一个经验是不要设计太多必填参数。能通过上下文推断的就不要设为必填能拆成多步询问的就别让模型一次性提取所有字段。真遇到过八个必填参数模型每次最多填对六个后来我干脆把技能拆分第一个技能负责识别实体第二个技能负责查询准确率反而上来了。4.4 技能执行超时把整个会话拖垮外部 API 动不动就慢你技能执行花 15 秒模型上下文已经等超时了。我在执行层面给每个技能加了超时控制默认 5 秒超时就返回一个固定的 TIME_OUT 错误码调度器拿到之后可以转给模型让它回复“该服务暂时繁忙请稍后再试”而不是整个请求卡死。重试策略也要分场景。写操作、非幂等接口绝对不能盲目重试否则可能造成重复下单、重复扣款。读操作可以重试一到两次间隔建议用指数退避。4.5 常见问题排查速查表现象可能原因排查方法解决方案模型从不调用某技能描述不够清晰、关键词过滤太严查看候选技能列表日志确认技能是否进入模型上下文强化描述、放宽过滤条件模型频繁选错相似技能技能边界模糊描述区分度低对比两个技能的描述和适用场景增加 not_applicable_scenarios设置优先级参数频繁报错或缺失input_schema 设计不合理必填参数过多查看模型决策 JSON 和参数校验日志补充示例、减少必填项、增加兜底逻辑技能执行超时外部 API 慢、无超时控制查看调用耗时日志加超时控制、异步化、重试策略上下文被技能返回内容撑爆执行函数返回了完整、未精简的数据查看返回结果摘要后处理层压缩内容只保留关键字段最后再分享几个小技巧技能体系这东西做的时候会觉得“不就是封装几个函数嘛”等真正跑起来才发现它是一个需要持续运营的模块。我每次上线新技能都会用一组固定的测试语句去跑回归看看会不会误伤旧技能也会随机抽一些真实用户日志统计技能命中率、参数错误率、平均执行耗时这些指标。另外如果你刚开始搭这套技能体系不要一上来就做几十个技能。先精挑细选三五个高频场景把描述和参数打磨到极致跑通之后再慢慢扩展。我自己的经验是5 个精雕细琢的技能远比 30 个随手挂上去的技能好用。还有一个小细节技能描述文件里我习惯在最后加一句“使用此技能后请用中文自然地向用户说明查询结果并标注数据来源时间”。就这么一句话能让最终回答的体验提升很多模型不会再干巴巴地吐 JSON 给用户。做 Agent 最后拼的其实不是模型本身而是你给它打造的“工具箱”够不够顺手。希望这套技能体系能帮你少走点弯路。
返回列表