
多智能体项目一旦跑起来最先让人头疼的不是 Agent 怎么对话而是怎么让 Agent 真正“动手做事”。我用 AgentScope 做内部工具这段时间把它的 skills 机制从注册用到团队共享踩过的坑比文档里写的翻了好几倍。这套框架的 skills 机制简单说就是把一段业务逻辑封装成函数挂到 Agent 身上让模型在对话中按需调用。它能解决什么问题、怎么一步步接进去、哪些地方容易翻车这篇文章一次性讲清楚。适合刚接触 AgentScope、想给多智能体加外部能力的开发者做参考。1. AgentScope 和 skills先解决两个问题1.1 多智能体开发里最让人难受的几件事先说说没有这套机制之前我经历过什么。第一个版本里我为了让 Agent 能查库存把所有查询逻辑都写进了 Prompt结果每次规则变动都要重写一大段描述还会和别的指令打架模型经常答非所问。第二个问题是调用外部接口这件事完全没法验证Agent 说“查到了”但你不知道它到底调了哪个函数、传了什么参数、返回了什么。第三是职责混乱多个 Agent 各写各的逻辑同样的查询能力在两处各维护一份改一处忘另一处。这三个痛点才是 AgentScope 这类框架要处理的核心。skills 机制正好把“决策”和“执行”分开决策由模型负责执行由技能函数负责。业务逻辑从文本描述里搬出来变成可以运行、测试、复用的函数块。整个项目也从“全靠 Prompt 撑着”变成“代码为核心、模型做编排”的结构Debug 的时候至少有一个明确的函数入口可以下断点。我见过不少团队在 AgentScope 上直接裸写模型调用Agent 只会聊天不会执行任何操作那等于没用到框架的真正价值。等你在项目里维护一段时间就会发现给模型一个可靠的外部执行环境远比给它一堆华丽的 Prompt 重要。skills 就是这个执行环境的载体。1.2 skills 在这套框架里的真实定位技能在 AgentScope 里不是一个虚构概念它实际上是 Tool 体系的上层封装。我习惯把底层对象叫 Tool把对外暴露的能力叫 Skill两者经常混着用但意图一致给模型一个可以调用的函数入口。在 AgentScope 的对话循环里模型收到用户消息后不是直接找答案而是根据上下文决定是否调用一个 Skill。它可以调用查库存的技能、查天气的技能也可以调用一段固定的代码片段。这种设计对 LLM 来说暴露的是一组标准化按钮模型只需要理解按钮的用途并按下它而不用关心按钮背后的实现细节。从工程角度看Skill 让能力和对话解耦能力可以单独测试对话逻辑可以单独调 Prompt两者不互相污染。对比维度不用 skills 的写法用 skills 的写法改动成本改 Prompt整体重写改函数范围可控可测试性难靠人工看回复可单测可断言返回值复用性复制粘贴注册一次多处挂载权限控制无法控制模型自由发挥按 Agent 注册最小技能集合这套机制真正解决的问题是把系统设计的问题提前到了模型交互之前。你写得越克制、边界越清楚Agent 的表现就越稳定。下面进入具体用法。2. skills 的底层设计与注册细节2.1 把一个函数变成技能本质上是给模型塞了一份说明书把函数变成技能对框架来说很简单难的是让模型会用它。模型站在函数外面它只能看到函数名、参数签名、docstring。所以这个函数从命名到注释本质上是一份给模型的说明书。命名要动词开头比如get_stock_quantity、query_order_status、create_after_sale一眼能看出这个技能是做什么的。不要用do_stuff、handle_data这种含糊名字模型会忽略它。docstring 要写清楚三件事什么时候用、参数是什么意思、返回值是什么结构。还有边界条件比如查不到数据时返回什么格式这直接影响模型在异常情况下的表现。我当时犯过一个典型错误函数名写得很规范但 docstring 里没有说明返回字段含义模型拿到了{status: 0}却不知道 0 代表有货还是没货最后就会胡编。技能的描述不是写给人看的注释是写给模型看的接口文档这个观念转过来之后很多问题迎刃而解。2.2 注册一个 Skill 的标准流程注册过程在 AgentScope 里非常直白。典型做法是把一个普通函数用装饰器转成工具对象再传给 Agent 的初始化参数。我实际项目里常写成这样from agentscope.func_to_tool import func_to_tool func_to_tool def query_stock_quantity(product_id: str) - dict: 查询某商品当前可用库存。 Args: product_id: 商品唯一编码例如 SKU-1001。 Returns: dict: 包含字段 quantity、status、warehouse。 # 这里可以是数据库查询也可以是外部 API 调用 return { quantity: 100, status: available, warehouse: east-01, }装饰器把函数包装成框架可识别的 Tool 对象Agent 初始化时传入 tools 列表。注册过程的核心是让函数签名被框架读取所以类型注解不要省略。product_id: str和- dict这种基础标注既方便模型理解参数类型也方便框架做参数校验。不同 AgentScope 版本里这个装饰器和类名可能有差异我见过有些版本是用Tool类手动构造。重点不是 API 长什么样而是“把纯函数转成框架能识别的技能对象”这一步。先在本地环境里跑通一个最小例子再复制到项目里比一次写完更稳。2.3 参数描述、命名和加载顺序的小讲究Skill 的参数描述直接影响模型能不能正确填参。我给自己定了几条规则单个技能的入参尽量控制在 3 个以内超出就拆分参数名要贴近业务语言比如order_id比oid好能用基础类型就用基础类型复杂结构用 TypedDict 或者直接返回 dict。参数多了之后模型很容易传错值。比如传商品编码的时候把订单号传进来这种问题靠模型自身很难避免最好的办法是从源头减少参数数量。一个查询技能如果同时需要商品 ID、仓库 ID、日期范围考虑拆成两个技能让模型按需分步调用。技能加载顺序也要注意。Agent 的 tools 列表里前面放被调用频率高、最关键的基础技能后面放低频技能。模型在选择时会受到顺序影响虽然不明显但顺序靠前的技能被选中的概率更高。高频技能放前面至少能少一些无谓的反复试错。3. 实战给 Agent 挂上一组可用的 Skills3.1 一个很典型的业务场景和工程结构用一个模拟项目 X 来说明某智能售后助手 Demo需要让用户通过自然语言查询商品库存、查询订单状态、创建售后单。这三个能力正好对应三个技能。工程结构我当时是按领域拆分的这样每个文件只维护一类能力project_x/ skills/ __init__.py stock.py order.py aftersale.py agent_flow.py config.pystock.py里面只放库存相关函数order.py放订单查询aftersale.py放售后创建。文件拆分的意义不是为了让代码好看而是为了后续维护方便。技能多了以后一个文件里塞几十个函数找起来很痛苦而且容易在改动时误伤其他技能。按领域拆分之后每个文件可以被单独 import、单独测试。团队协作的时候一个同事负责库存模块一个同事负责订单模块互相不会产生代码冲突。这就是把 agent 开发变成了普通的后端开发每个人只需要关注自己那部分函数逻辑。3.2 从定义 Skill 到初始化 Agent 的完整过程完整流程分四步定义技能、初始化框架、创建 Agent、发送消息。技能定义在上一节已经写了这里从初始化开始。import agentscope from agentscope.agent import ReActAgent from agentscope.message import Msg from agentscope.func_to_tool import func_to_tool # 1. 初始化框架配置模型 agentscope.init( model_configs{ config_name: my_model, model_type: chat, model_name: your_model_id, api_key: xxx, } ) # 2. 注册技能实际项目里从 skills 包导入 func_to_tool def query_stock_quantity(product_id: str) - dict: 查询某商品当前可用库存。 return {quantity: 100, status: available} func_to_tool def query_order_status(order_id: str) - dict: 查询订单当前状态。 return {order_id: order_id, status: shipped} func_to_tool def create_after_sale(order_id: str, reason: str) - dict: 为订单创建售后单。 return {after_sale_id: AS-2001, status: created} # 3. 创建 Agent把技能挂进去 agent ReActAgent( nameassistant, modelmy_model, tools[ query_stock_quantity, query_order_status, create_after_sale, ], max_iters5, ) # 4. 发送消息 response agent( Msg(nameuser, contentSKU-1001 库存还剩多少有货的话直接下单) ) print(response.content)这里我用的是 ReActAgent它的核心是“观察-思考-行动”循环模型先看用户消息和相关技能说明决定要不要调用某个 Skill框架执行函数拿到结果模型再根据结果生成回复。max_iters5表示最多循环 5 轮防止模型陷入无限调用。第一次跑通这个流程的时候有个很明显的感受模型真的会自己去选函数。用户说“查库存”它就调query_stock_quantity用户说“查订单”它就调query_order_status。这也是 AgentScope 的 skills 机制和普通函数调用最大的区别——模型在做决策代码在执行。3.3 调用链路与最小权限原则技能被调用的完整链路是这样的用户消息进入 AgentReAct 循环开始模型分析后输出工具调用意图框架解析意图并执行对应的 Skill 函数函数返回值作为观察结果拼回对话上下文模型基于结果生成最终回答。这条链路里任何一环出问题Agent 的表现都会异常。权限控制在多 Agent 场景里尤其重要。不是每个 Agent 都需要全部技能给每个 Agent 分配最小技能集合能减少很多误操作风险。我见过一个只负责问答的客服 Agent被误挂了删除订单的技能模型在用户抱怨时直接调用了它这是非常危险的事。敏感操作必须单独加确认步骤。比如创建售后单、修改库存这类写操作不要直接放在默认技能列表里可以考虑拆成“申请单”和“确认单”两步或者由另一个专门 Agent 来审批。宁可流程上多一步也不要让模型随随便便就执行破坏性操作。4. 我把这些坑都踩了一遍帮你整理好了4.1 Agent 就是不肯调用 Skill怎么排查技能不生效是最常见的问题表现是模型回答得很流畅但完全没有调用函数或者干脆胡编乱造。我的排查顺序基本固定先看 docstring再看技能数量然后看参数类型最后看函数内部异常。docstring 写得太含糊是头号原因。模型看不懂“这个技能是干什么的”自然不会调用。检查一下你的 docstring 有没有写清楚使用场景、参数含义、返回结构如果还是不行直接在描述里加上“当用户询问XX时必须调用此工具”这种强制指令。技能数量太多也会让模型选择困难。一次挂 10 个以上技能模型很容易漏掉最合适的那个。我的做法是只挂当前对话可能用到的技能其他技能放在后台服务里由 Agent 动态调用。参数类型不匹配的问题通常表现为模型传了字符串、函数要求整数这种可以通过在函数内部做类型转换来兜底。最后函数内部报错会让框架把异常信息当作返回值传回给模型模型一看有结果就以为任务完成了。所以每个技能函数里要加 try/except返回结构化错误信息比如{error: timeout, retry: false}让模型知道这次调用失败了。调试的时候把原始 tool call 日志打出来一眼就能看出模型到底选了哪个函数、传了什么参数。4.2 多个 Agent 共用 Skill 时的并发问题多 Agent 同时调用一个 Skill 时首个容易出现的问题是线程安全。Skill 函数如果是无状态的也就是所有数据都来自参数那并发没有问题。但如果你在函数里用了全局变量、共享连接对象、或者模块级缓存多个 Agent 同时调用时数据会互相覆盖。我踩过的典型坑是一个技能里用了模块级的requests.Session()多个 Agent 并发调用时偶现连接串号返回的数据张冠李戴。解决方法是把 Session 做成局部变量或者用连接池管理。数据库连接也是同理不要在技能函数里写死一个全局 connection要用连接池或者每次调用取新连接。问题现象可能原因处理方案返回值串号模块级共享变量技能函数改为无状态超时阻塞外部 HTTP 无超时设置加 timeout 与重试机制连接耗尽每次新建连接不释放改用连接池并发场景下技能函数还要设外部接口超时。我习惯在 HTTP 调用里显式加上timeout5避免一个外部服务卡住导致整个 Agent 卡到超时。重试逻辑也要做好但重试次数不要太多两到三次就够重试间隔递增。4.3 技能顺序错乱流程完全走偏在一个 Agent 里塞多个 Skill模型会选择执行哪个技能但不一定能按业务预期顺序来。比如用户说“查一下库存有货就下单”模型可能直接调用下单技能跳过了查库存这一步。业务流程明确时不能指望模型自己排序。我的解决方案是能力与流程分离技能负责“能做什么”流程在代码层控制“先做什么、后做什么”。具体做法有三种。第一种是拆成多个专用 Agent比如“库存查询 Agent”和“下单 Agent”用工作流串联第二种是在系统提示词里强调流程顺序效果有限但简单第三种是把顺序判断逻辑写在代码里由一个编排函数来判断应该先调用哪个技能。最可靠的还是搭个工作流层。Agent 只管单步操作流程编排放在代码里这样顺序可控、可测试、可回滚。模型擅长的是理解意图和生成自然语言严谨的先后顺序交给代码更稳。5. 让 Skills 在团队里持续沉淀下去5.1 把技能做成团队共享的能力中心项目做久了技能会越来越多。如果不做统一管理每个 Agent 自己维护一份技能列表早晚会失控。我后来把技能收敛成一个共享注册表所有技能集中在skills/目录通过一个注册函数统一导出。一个简单的做法是写一个SKILL_REGISTRY字典key 是技能名字value 是对应函数对象Agent 初始化时按需从注册表取。这样既控制了 Agent 的可见范围又方便统一审计有哪些技能可被调用。新成员加入项目时只需要看注册表就能了解全部能力。团队共享还有一个好处是标准和规范能落地。要求每个技能必须写 docstring、必须返回 dict、必须有异常处理代码评审时按这些规则检查。技能的质量上去了Agent 的稳定性自然也会上去。5.2 技能的版本维护与灰度上线技能也是代码也会有改动。我吃过一次亏改动了一个查询技能的内部实现没影响函数签名但返回字段结构变了Agent 的最终回答开始出错。从那以后技能函数如果改了返回结构我会同步更新 docstring并在 changelog 里记录。更稳妥的办法是给技能加版本号。每个技能函数在 docstring 里标注版本或者返回结构里带一个version字段。内部接口改动时先让一部分 Agent 用新版其他 Agent 用旧版跑一段时间没问题再整体切换。这就是灰度上线思路。回滚也很重要。技能出问题时最怕的是找不到上一个稳定版本。代码仓库的 commit 历史可以帮到这一点每次技能改动单独提交不要把若干无关修改混在一起这样出了问题能快速定位和回滚。5.3 从调用技能到动态加载技能技能很多之后全部挂到 Agent 上会让模型选择困难。更好的做法是动态加载给每个 Agent 只暴露当前任务需要的技能子集。比如售后 Agent 只看到query_order_status和create_after_sale库存查询 Agent 只看到query_stock_quantity。动态加载的关键是有一个技能清单下发机制。先让一个入口 Agent 理解用户意图再把对应的技能清单注入给具体执行 Agent。这样技能的规模不会影响模型的表现还能顺带做权限控制——哪个角色能看到哪些技能完全由上层决定。我自己用下来最大的体会是skills 机制的本质是把系统设计问题提前到了模型交互之前。你把它当成普通的函数库去维护它表现得非常稳定你把它当成“只要挂上去就能自动工作”的黑盒它迟早会在某个关键时刻给你惊喜。先把一个技能做好再谈多技能协作这是最值得花时间的路径。