最近手头在做一套智能体技能库相关的方案项目代号就叫 agent-skills。简单说就是给大模型驱动的智能体挂上一批可复用、可组合、可独立调用的技能模块让它不再只靠对话里那点上下文硬撑而是真正能做点具体的事。这篇文章我会从设计思路到落地实现完整复盘一遍把中间踩过的坑、想明白的取舍、实测下来有用的方法都写出来适合正在做 Agent 应用、想给自家智能体增加工具能力的开发者以及刚接触大模型应用层开发的同学参考。1. 先把概念说清楚Agent 为什么需要一堆“技能”1.1 没有技能库的 Agent就像只会聊天不会干活的实习生最开始我调试智能体的时候发现一个很尴尬的问题模型本身能力确实强能写文案、能总结文档、能回答复杂问题但只要你让它“帮我把这份数据整理成表格再发到群里”它就卡住了。不是不会而是缺少一个关键的中间层——它没有手没有工具唯一的输出就是文本生成。这时候你可能会说那直接调 API 不就行了把外部系统接进来不就有“手”了。理论上确实是这样但实际做起来会发现项目里各种外部能力会越来越多查库存的、发邮件的、生成报表的、调数据库的、调用内部HTTP服务的……如果每个都散落着放在不同的代码分支里智能体的调用逻辑会很快失控。Agent 需要一个统一的、可管理的“技能库”把外围能力包装成模型能理解的“技能”让智能体像人一样按需“调用工具”。skills 这个概念本质上就是对大模型应用层能力做了一层正式化的封装。它把一个外部动作比如“调用某个API”“执行一段计算”“读取某个文件”和一个语义描述绑定起来然后用模型能读懂的自然语言暴露给 Agent。它解决的不只是“能调”的问题更是“怎么调、什么时候调、调完结果怎么用”的完整链路。1.2 Agent Skills 是什么、不是什么我先说清楚边界。这里的 skills 不是让模型学会一种新的推理方法论也不是专门去微调模型参数它是一个应用层机制。技能是“外部能力 可读描述”的组合体技能需要通过函数签名、参数描述、返回值说明来暴露自己技能的调度权在智能体手里的“决策循环”里模型根据对技能描述的理解决定是否调用技能的执行结果会被回填到上下文里成为后续推理的一部分。我用一个生活化的类比你雇了一个能力很强的助手但新人入职第一件事不是背公司战略而是先学会用公司的 OA、报销系统、会议室预订工具。你给他一本操作手册告诉他每个系统是干什么的、什么情况下用哪个、参数怎么填。这本手册就是技能库系统中的每个操作入口就是一个个 skills。Agent 自己不会天然知道这些系统怎么用只有你把操作手册喂给它它才能按规范执行。项目里我管理 skills 的角度通常有三个维度基础技能原子操作、领域技能行业专用能力、编排技能组合多个技能来完成复杂流程。后面我会逐个展开说。2. 技能库的整体设计先搭骨架再填细节2.1 基础技能、领域技能、编排技能三级拆解在设计 agent-skills 的目录结构之前我先把技能按使用频率和复用度做了分级。这套分级方法最后被证明是项目里最划算的决策之一因为不同层级的技能有完全不同的生命周期和管理策略。基础技能是不依赖任何业务系统的通用能力。比如文本处理截取、转换格式、时间计算今天是星期几、两周后的日期、数据校验手机号格式、邮箱格式、简单的数学计算等。这类技能的特点是稳定、无副作用、几乎每个 Agent 项目都能复用。我通常会用一个独立的 basics 目录去维护改动频率很低写一次就长期受益。领域技能是某个业务场景下的专业能力。比如我做一个“智能客服数据分析助手”的时候就会用到技能查询订单状态、计算售后率、生成周报摘要。这类技能必须对接具体的数据源或业务 API带有明显的领域语义。它们的描述要写得非常谨慎因为同一个函数在不同业务下可以暴露成完全不同的“技能”——背后逻辑可能一样但语义描述决定了模型会不会在正确时机调用它。编排技能是组合多个技能来完成一个完整流程的能力。比如“处理客户投诉工单”这个技能内部可能是先调“查询工单详情”再调“获取客户历史记录”然后调“生成处理建议”最后调“更新工单状态”。这种技能我建议用一段代码去编排底层调用而不是让模型在长长的多轮对话中自己一步步去碰运气。把常见流程固化成编排技能既能提高稳定性也能显著减少 token 消耗。2.2 技能描述怎么写才不被模型拿错技能描述是整个 skills 机制里最容易忽视、但影响最大的部分。很多开发者的习惯是“功能写清楚就行了”但实际测试下来模型对技能的选择非常依赖描述里的“触发场景提示词”。同一个工具你说“查询订单状态”和“获取订单物流信息”模型在遇到“我的快递到哪了”这个问题时的选择可能完全不同。我的经验是写技能描述时一定要覆盖三个东西当什么时候用、主要做什么、不要做什么。举个例子“查询订单状态”这个技能When to use当用户询问订单当前的处理进度、商品是否已发货、物流是否已签收时使用What it does根据订单编号查询内部订单系统的状态字段返回最新节点信息Do not use不要用于查询退款进度退款进度请使用“查询退款进度”技能。你会发现模型实际调用准确率会提升不少。原因是描述里给了模型足够的“触发锚点”模型在选择技能时更像是在做一道选择题而不是靠猜。我在项目里把这一条写成了硬性规范每个新增技能都必须带“场景锚点”字段否则不让合入技能库。另外还要注意技能参数的设计。参数名要有语义像 query、keyword、data 这种太模糊的命名很容易让模型填错值。更好的做法是明确参数含义比如 order_id、customer_name、start_date、end_date。如果参数是枚举值一定要在描述里列出所有可选值和各自含义。模型不懂你的系统它只能依据你写的 schema 来判断参数怎么填。3. 实操落地从零实现一个带技能调用的智能体3.1 技术选型和整体架构这一节我直接给一套实际跑通过的技术方案。语言用 Python框架层面没有选重量级的 Agent 框架而是直接用 Function Calling 的方式和模型交互。这样做的好处是透明、可控、出问题容易排查而且对模型的要求也不高市面上主流的大模型接口基本都支持 function calling 或工具调用。整体分成四个模块技能仓库、技能注册中心、调度循环、执行器。技能仓库负责存储和索引所有技能定义包含函数的 JSON Schema 和对应的执行函数技能注册中心把技能仓库里的定义加载成模型接口期待的 tools 格式调度循环是智能体的核心它会持续与模型对话直到模型不再请求调用工具为止执行器负责真正去执行函数执行结果重新作为 message 返回给模型。目录结构大致是这样的agent_skills/ ├── basics/ │ ├── text_tools.py │ ├── time_tools.py │ └── validator.py ├── domain/ │ ├── order_skills.py │ ├── customer_skills.py │ └── report_skills.py ├── orchestration/ │ ├── complaint_flow.py │ └── daily_summary_flow.py ├── registry.py ├── scheduler.py └── executor.py3.2 技能注册与调用的完整流程技能注册最简单的做法是为每个技能写一个装饰器把函数信息自动提取成 JSON Schema。我用 pydantic 做参数校验解决了手写 schema 容易出错的问题。from pydantic import BaseModel from typing import Literal from agent_skills.registry import register_skill class OrderStatusParams(BaseModel): order_id: str 订单编号必填 source: Literal[app, web, shop] 订单来源渠道可选默认app register_skill( name查询订单状态, description当用户询问订单当前处理进度、是否已发货、物流是否签收时使用。不要用于查询退款进度。, ) def query_order_status(params: OrderStatusParams): # 实际代码里这里会去调用内部订单系统的HTTP接口 result internal_api.get_order_status(params.order_id, params.source) return {order_id: params.order_id, status: result.status, message: result.message}有了注册装饰器之后技能仓库可以自动扫描目录拉出所有被 register_skill 标记的函数生成模型接口需要的 tools 列表。调度循环我写成了一个通用函数它接收用户消息和技能列表循环调用模型接口如果有工具调用请求就执行并回传结果直到模型给出最终答案。def run_agent(user_message, tools, executor, modelgpt-4o, max_iterations8): messages [{role: user, content: user_message}] for _ in range(max_iterations): response llm.chat_completion(modelmodel, messagesmessages, toolstools) msg response.choices[0].message if msg.tool_calls: messages.append(msg) for tc in msg.tool_calls: tool_result executor.execute(tc.function.name, tc.function.arguments) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(tool_result, ensure_asciiFalse) }) continue return msg.content return 达到最大迭代次数任务结束。这里的特殊之处在于工具调用的结果不一定只是“成功/失败”还应该包含模型下一步决策需要的信息。比如查询订单状态后把订单状态的下一步操作提示也一起放进返回值里可以减少模型几轮无效推断。3.3 编排技能把复杂流程固化成一段代码编排技能是最考验设计功底的部分。我最初偷懒让模型自由调用多个基础技能逐步完成复杂任务结果发现多步任务中间一旦某一步因为参数问题失败模型很容易卡在一个死循环里反复试错。后来我把高频流程整理成编排技能从源头杜绝了这个问题。以“投诉工单处理”为例编排技能内部是代码控制流程模型只需要一次性调用它不需要反复发起多次工具请求。register_skill( name处理投诉工单, description当用户提交投诉工单并要求给出处理方案时使用。包含查询工单、读取客户历史、生成建议、更新状态的完整流程。, ) def handle_complaint_ticket(params: ComplaintParams): ticket order_api.fetch_ticket(params.ticket_id) logs customer_api.fetch_history(ticket.customer_id) suggestion suggestion_model.generate(ticket, logs) order_api.update_status(ticket.ticket_id, processing, suggestion) return {ticket_id: params.ticket_id, suggestion: suggestion, status: processing}这样做的好处是双重的。第一稳定性提升模型不需要自己设计流程少了很多低级路径错误第二token 成本下降因为调用编排技能只需要一次工具调用而此前自由调用可能需要 4 到 5 次每次都会产生完整的工具请求和响应。实测下来一个投诉工单处理流程的 token 消耗下降了 40% 左右。3.4 用模拟项目验证技能库的效果为了验证这套机制不是只在理想环境里能跑我做了一个模拟项目一个跨平台的数据查询助手。它需要支持查天气、查汇率、查节假日、生成周报摘要等能力对应的技能库里有 20 个左右的技能。测试集第一批是 100 条用户问题覆盖不同表达习惯比如“今天上海冷不冷”“美元兑人民币现在什么价位”“帮我把这三天的销售数据生成一个周报”。没加技能描述优化之前准确率只有 62% 左右把技能描述的触发锚点、参数枚举写完以后一轮测试准确率直接到了 84%。后续又调整了部分技能的“不使用”提示语把易混淆的技能区分开准确率最终稳定在 91% 左右。这个数据说明技能库的收益主要不在于代码本身而在于模型对技能的理解是否到位。4. 真机实测Agent 多轮对话中的技能调度体验4.1 多轮对话里的技能记忆与上下文实测中最容易翻车的是多轮对话中的技能调度。用户在第一轮问“查一下订单 12345 的状态”模型调用技能拿到了结果然后用户接着说“顺便把物流信息也发我”。如果技能库没有设计好模型在第二轮可能根本想不起来第一轮的订单号是多少必须重新问用户。这个问题可以通过在上下文里保留足够的工具结果来解决。我在调度循环里会把每次工具调用的参数和结果都保留在 messages 中这样模型在后续轮次里依然能引用之前的执行结果。另外一个技巧是在工具返回值里直接预置好状态的延续信息比如查询订单状态后结果中直接附带“如果需要物流信息请调用查询物流技能订单号为当前订单”引导模型按预设路径走。4.2 参数校验与异常处理不能省技能执行器的参数校验是另一个容易马虎的地方。模型生成参数偶尔会犯错比如枚举字段传了一个无效值、时间字段的格式不对。如果技能函数不做校验异常会直接抛到调度循环里导致整个会话崩溃。我引入 pydantic 做参数层校验后所有入参都会先过一遍类型检查和值检查无效参数会返回一个带有“参数错误提示”的结果回传给模型模型看到后可以自动修正参数重新调用。def execute(self, name, args_json): skill self.get_skill(name) params skill.param_model(**json.loads(args_json)) try: result skill.fn(params) return {status: success, data: result} except ApiError as e: return {status: error, message: f系统异常{e}可尝试重试或联系管理员}我遇到过一个很典型的案例模型把一个日期参数填成了2024-13-45当时还没接参数校验内部实现直接抛了异常而且异常信息非常不优雅。加校验之后返回值变成“日期格式错误请重新提供有效日期”模型在下一轮自己就把日期修正了。这看似是一个小细节但实际体验差异非常大。4.3 技能执行的并发控制和限额真实业务里还有一个容易忽略的问题技能执行不是无限资源的。如果一个 Agent 会话同时被触发 20 次技能调用背后可能对应着 20 个外部 API 请求。不加控制的话某个内部系统可能在几秒内被打满。我在执行器里加了简单的信号量控制限制并发技能调用数不超过 5同时对每个技能加了超时时间超过一定时间就直接返回超时错误避免整个会话被一个慢调用卡死。5. 常见问题与排查技巧实录5.1 模型根本不调技能只凭记忆回答这个问题在我第一次接入技能库时出现过。后来排查发现原因是技能列表没有传进消息的 tools 参数里模型根本不知道有这些技能存在。另外一个小坑是部分模型的工具调用参数格式要求严格要求 tools 里的每个函数必须有严格的 JSON Schema否则该工具会被静默忽略。排查方法很简单先打日志确认 tools 参数是否正确传递再检查模型接口返回里有没有 tool_calls 字段。5.2 技能描述相似模型选错了技能技能库里如果有两个功能相近的技能模型很容易选错。解决办法就是我在前文提到的“Do not use”描述法。我测试过一个场景有“查询订单状态”和“查询退款进度”两个技能如果不写用途区分模型在处理“我的退款到哪一步了”时偶尔会去查订单状态。后来我在两个技能描述里都加入了明确边界准确率从 72% 提升到 93%效果立竿见影。5.3 工具返回内容太长把上下文撑爆技能执行结果如果是个很大的 JSON全部塞回上下文里既浪费 token又可能把上下文窗口撑爆。我的做法是在执行器返回前做一次“结果摘要”只提取模型后续决策真正需要的字段比如把订单详情中冗长的物流轨迹压缩成“当前节点已签收共5条轨迹最新一条由快递员于今日14时录入”。这样模型能拿到关键信息但不需要阅读一大段无用数据。5.4 多技能组合时执行顺序乱套早期让模型自由调用多个技能时它偶尔会把“先查询再更新”的顺序搞反造成脏数据。排查后发现模型并不是不知道顺序而是它偶尔会高估自己的能力去并行执行两个前后依赖的操作。代码上我没有完全依赖模型自觉而是把有依赖关系的操作封装进编排技能从根本上消除了执行顺序混乱的问题。6. 几个关于技能库的进一步思考和心得如果你已经把技能库的核心机制跑通了下一步可以考虑引入技能评估体系。我目前的做法是给每一个技能记录调用次数、成功率、平均执行时间、导致重试的次数然后定期审视哪些技能在被高频调用、哪些技能几乎从未被选中。这个数据分析能帮你持续优化技能的描述和参数设计。比如某个技能调用量极低大概率不是它没用而是描述没能让模型理解它的适用场景。另外一个思路是把技能库从“静态注册”升级为“动态加载”。有些技能只在特定对话上下文下才有意义比如只有在用户明确询问某个业务时才需要加载对应的域技能。我做过一次实验把所有技能都塞进 tools 列表时模型在上述测试里的工具选择准确率是 84%而根据对话意图动态注入部分技能后准确率能提升到 90%同时请求体大小明显下降延迟也低了。代价是需要额外做一层意图识别权衡起来还是值得的。最后关于技能模块的边界我个人的体会是尽量保持技能函数的“单一职责”。一个技能只做一件具体的事比一个技能内部塞进多个功能要可靠得多。因为描述、参数、返回值的可控性都会更好模型也更容易在正确时机调用它。以后扩展的时候你也会发现新需求的接入成本非常低复制一个技能模板改一改描述和内部实现就又是一个新的技能了。