ARTICLE DETAIL

资讯详情

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

Agent技能体系搭建指南:从提示词到可复用、可观测的技能资产

Agent技能体系搭建指南:从提示词到可复用、可观测的技能资产 1. 为什么Agent需要一套“技能体系”从能聊天到能干活我做AI应用开发这几年最明显的感受是大模型本身的价值很大程度上取决于它能调用什么。单纯把GPT扔给用户它只会一本正经地胡说八道或者老老实实写小作文但一旦给模型接上搜索引擎、数据库、代码解释器、文件系统这些“手和脚”它就能真的帮你订机票、查库存、写报表。这就是我为什么一直关注并实践“agent-skills”这套思路的原因——它的核心不是让模型变聪明而是把模型的聪明转化为可复用、可管理、可审计的“技能资产”。很多刚接触Agent开发的朋友会把“Agent技能”误解为“提示词技巧”其实差别很大。提示词是在教模型“怎么说”技能是在给模型配“能做什么的接口”。比如你给模型一段精心设计的提示词它依然无法真正执行一条SQL查询最多只能编一段SQL文本给你但如果你注册了一个execute_sql技能告诉模型“有一个工具可以连数据库执行只读查询参数是SQL语句结果是记录集”模型就能在你给出查询意图后自动生成SQL、调用工具、拿回结果并基于结果继续推理。agent-skills就是这套“给模型配工具让模型知道怎么用工具”的工程化方案。这篇文章想写给两类人一类是正在做ChatBot但发现仅靠提示词无法完成业务闭环的开发者另一类是已经在用LangChain、AutoGPT等框架但觉得黑盒封装太多、出了问题无从下手的实践者。我会从设计思路、定义规范、实操代码到排查技巧完整拆解一遍我自己在项目里沉淀下来的技能体系搭建方法。你可以直接拿来当作一份参考手册也可以照着里面任何一个技能案例改造接入自己的项目。我先说明一个关键立场不要迷信现成框架里的“工具列表”功能。很多Agent框架把技能/工具做成了“注册一下就完事”但这恰恰是问题最多的地方。技能的描述写得不好模型会抓瞎参数Schema不严谨运行时会频繁校验失败技能之间互相覆盖、调用链路混乱调试起来想撞墙。agent-skills的真正价值是把这些隐患在设计阶段就解决掉。2. 技能体系的核心设计可复用、可观测、可控2.1 技能的定义边界什么该做技能什么不该做技能先给技能下一个可操作的定义技能是一个由LLM自动触发的、完成特定任务的原子能力单元。它必须具备三个特征有明确的输入输出契约通常是JSON Schema有可被模型理解的语义描述执行结果可以被反馈给模型继续参与推理。并不是所有能力都适合做成技能。我在项目中总结了一条判断标准**“高频、稳定、有明确边界”**的任务适合做技能比如查天气、查订单、执行SQL、发送HTTP请求、读写本地文件。而那些需要多轮人机协作、判断标准模糊、或者和模型自身强相关的任务比如“写一首诗”“总结这段文本”反而不应该做成技能——前者模型本身就能做后者做成技能只会消耗额外Token且效果反而变差。这个边界想清楚很重要。我曾经见过一个团队把“情感分析”做成了技能调用外部API去分析用户情绪结果那个外部API的效果还不如模型直接理解白白增加了一次网络延迟和一截Token开销还多了一个故障点。技能的初衷是“让模型做到它本身做不到的事”而不是给模型本来就能做到的事加上一层中间商。2.2 技能描述Skill Description怎么写模型眼中的“使用说明书”技能描述是整套体系里最容易忽视、却最影响命中率的一环。模型不会阅读你的源码它只会通过一段纯文本来判断该不该调用某个技能。描述写得差模型要么在需要的时候想不到调用要么在不该调的时候强行调用。我总结了几个经验描述里要写清楚“这个技能解决什么问题”而不是“这个技能是什么”。举个例子错误写法是“数据库执行工具”正确写法是“当用户询问订单数量、销售汇总、库存明细等需要查询MySQL数据库的问题时使用该工具执行只读SQL查询禁止执行写操作”。要以用户意图为触发条件而不是以技术组件为标题。每个技能描述控制在80~150字。太短了信息不足太长了模型容易迷失重点还会挤占上下文窗口。描述里最好包含适用场景、触发条件、禁止事项。同类技能之间要做“差异化描述”。如果你的技能库里有search_product和search_order两个技能描述里必须明确写出区别。比如前者是“按商品名称、SKU、品类查询商品主数据”后者是“按用户ID、订单号查询交易订单信息”否则模型很容易选错。我在实际项目中会专门留一个descriptions/目录为每一个技能写独立的描述文档就像写产品文档一样评审。别嫌麻烦这一步的影响权重远超后面任何一行业务代码。2.3 输入输出Schema给技能装上一道严格的门禁技能的输入输出Schema本质上定义了模型和世界之间的接口契约。这里我有一个从多次生产事故中换来的原则Schema一定要“偏严格”不能“随和”。原因有两个第一模型生成的参数天然存在格式猜测倾向如果字段允许任意类型它会倾向于塞一个对象进去第二后期你很难对技能做断言和评估。我推荐用JSON Schema或Pydantic来定义。每个技能的输入字段必须包含type、properties、required同时尽可能给description。模型对字段描述非常敏感我见过一个技能把参数timezone写成“时区”两字模型经常传成GMT8后来我改成“IANA时区ID例如Asia/Shanghai”准确率立刻提上来了。模型天然是“文本敏感型”的Schema里的每个字段说明都是在给模型铺路。可能有人会问我输入校验失败怎么办我的方案是校验失败不要硬报错而是把失败信息格式化后返回给模型让它自己修正。比如“参数timezone格式非法GMT8应为IANA时区ID参考Asia/Shanghai”模型看到后通常会自己纠正再试一次。这比直接抛异常终止整个Agent循环体验好太多。这个容错机制是agent-skills里最不起眼却最影响成功率的细节。3. 实操过程从零搭建一个agent-skills技能库3.1 项目结构与基础选型我通常用Python来搭建技能库不是因为别的语言不行而是Python在数据类定义、JSON Schema生成、模型SDK支持上都最省事。基础依赖就三个pydantic做参数校验、openai或其他兼容SDK做Function Calling、fastapi可选做本地技能服务。下面是一个我常用的项目骨架agent-skills/ ├── skills/ │ ├── __init__.py │ ├── registry.py # 技能注册中心 │ ├── weather.py # 示例技能天气查询 │ ├── sql_query.py # 示例技能只读SQL查询 │ └── file_search.py # 示例技能本地文件搜索 ├── descriptions/ │ ├── weather.md │ ├── sql_query.md │ └── file_search.md ├── schemas/ │ ├── weather.json │ ├── sql_query.json │ └── file_search.json ├── agent/ │ ├── loop.py # Agent执行循环 │ └── llm_client.py # 模型封装 ├── tests/ │ └── test_skills.py └── pyproject.toml这种结构最大的好处是每个技能的代码、描述、Schema三者分离后续无论是迭代描述还是更新Schema都能单点修改不互相污染。很多人喜欢把描述字符串直接写在Python函数装饰器里开发期图省事可以但一旦技能数量超过20个维护就是噩梦。我强烈建议从第一天就用目录化、文件化管理。3.2 注册中心让技能“可被发现、可被路由”注册中心是整个技能库的入口它负责收集所有技能的定义并统一暴露给模型。我的实现里每个技能是一个Skill数据类包含name、description、input_schema、handler四个字段。注册中心有两个职责一是生成供模型调用的函数列表tools二是根据技能名路由到对应的handler。from pydantic import BaseModel, Field from typing import Dict, Callable, Any class Skill(BaseModel): name: str description: str input_schema: Dict[str, Any] handler: Callable[..., Any] class SkillRegistry: def __init__(self): self._skills: Dict[str, Skill] {} def register(self, skill: Skill): if skill.name in self._skills: raise ValueError(fSkill {skill.name} already registered) self._skills[skill.name] skill def to_openai_tools(self) - list: tools [] for skill in self._skills.values(): tools.append({ type: function, function: { name: skill.name, description: skill.description, parameters: skill.input_schema, } }) return tools def execute(self, name: str, arguments: dict) - str: skill self._skills.get(name) if not skill: return fError: unknown skill {name} try: result skill.handler(**arguments) return format_result(result) except Exception as e: return fSkill {name} execution failed: {str(e)}注意execute方法里做了两层防护技能不存在时返回友好错误执行异常时也返回可读信息。这两层都会把错误信息回传给模型让模型有机会修正自己。千万不要让异常直接冒泡到Agent循环外那样整个对话就断了。还有一个细节技能注册时要检查重名。agent-skills里最常见的线上问题不是技能写得不好而是两个技能用了同一个name后注册的覆盖先注册的模型以为在调用A工具实际跑的是B逻辑数据错得无声无息。这份防御性代码能帮你省去好几个通宵排查的夜晚。3.3 一个真实技能案例只读SQL查询我挑一个最有代表性的技能展开讲sql_query。这个技能在很多业务Agent里都是核心它最能体现“技能设计优劣会直接影响产出质量”这件事。先看Schema{ type: object, properties: { query: { type: string, description: Read-only SQL SELECT statement. Must not contain INSERT, UPDATE, DELETE, DDL or any write operations. }, limit: { type: integer, description: Maximum number of rows to return, default 100, max 1000., default: 100 } }, required: [query] }这段Schema的核心是query字段的description。我故意把“只读”、“禁止写操作”这类约束写进字段描述里而不是只在总描述里提一句。原因还是那句话模型对就近的信息敏感。总描述里写了禁止写操作模型生成具体SQL时注意力已经聚焦在字段上了容易“忘”在字段描述里强调命中率显著提升。然后是handler实现import sqlite3 import pandas as pd def sql_query_handler(query: str, limit: int 100): # 连接只读数据库使用uri模式强制只读 conn sqlite3.connect(file:app_data.db?modero, uriTrue) try: # 防御拒绝非SELECT开头的语句 stripped query.strip().lstrip(().lower() if not stripped.startswith(select): return Error: only SELECT statements are allowed. # 强制追加limit防止模型查询全表 safe_query f{query.rstrip(;)} LIMIT {limit} df pd.read_sql_query(safe_query, conn) return df.to_json(orientrecords) finally: conn.close()这里我有几个刻意为之的设计用SQLite的modero打开数据库从数据库层面硬性禁止写操作而不是只靠字符串判断。因为模型可能会构造SELECT ... ; DROP TABLE ...这种语句字符串前缀检查拦不住。强制追加LIMIT防止模型在全表上跑SELECT *把数据库拖死。用finally保证连接关闭避免技能对象长期运行导致连接泄漏。你可能注意到了这段代码里我还在handler内部做了两层防御。技能的设计原则是“不要信任模型的输出”即使Schema和描述写得再严谨也要在运行时加防护。这套思路尤其适用于那些有副作用的操作。3.4 Agent循环理解模型为什么调用技能技能定义好之后还需要一个Agent执行循环把它们串起来。很多人以为model.call(tools...)然后拿到tool_calls就结束了其实完整的最小闭环至少包含四步构造消息、请求模型、执行技能、回传结果。我贴一个干净版本def run_agent(registry: SkillRegistry, user_message: str, max_steps: int 5): messages [{role: user, content: user_message}] tools registry.to_openai_tools() for _ in range(max_steps): response llm_call(messages, tools) message response.choices[0].message if not message.tool_calls: # 模型判断不再需要调用技能直接返回最终回答 return message.content # 追加模型的工具调用消息 messages.append({ role: assistant, tool_calls: [ { id: tc.id, type: function, function: { name: tc.function.name, arguments: tc.function.arguments } } for tc in message.tool_calls ] }) # 逐个执行技能并追加结果 for tc in message.tool_calls: arguments json.loads(tc.function.arguments) result registry.execute(tc.function.name, arguments) messages.append({ role: tool, tool_call_id: tc.id, content: result }) return Agent did not converge within max_steps这个循环的核心逻辑是让模型看到技能执行结果再决定下一步说什么、做什么。技能执行结果要尽量简洁我通常让handler返回JSON字符串而不是自然语言因为自然语言会占用大量上下文且信息密度低。还有一个很多人踩过的坑工具调用的arguments解析失败。tc.function.arguments返回的是字符串形式的JSON必须json.loads但模型偶尔会生成带注释或尾逗号的非法JSON。我建议在解析外层加容错解析失败时把错误信息原样回传给模型让它重新生成参数。这一步能让成功率提升好几个百分点。3.5 技能编排技能与技能之间的组合单技能Agent只能完成原子操作真正的业务场景往往是组合技能。比如“帮我统计周销量最高的商品”Agent可能需要先调用sql_query查出销量数据再调用chart_generator生成图表甚至还要调用email_sender把图表发出去。技能编排有两种形态线性编排上一技能的输出作为下一技能的输入。这种方式最简单Agent循环天然支持——模型会基于前一次工具结果生成下一次工具调用参数。条件编排根据技能执行结果决定是否继续。比如执行sql_query后如果发现查询结果为空就不再调用chart_generator而是直接生成“暂无数据”的总结。我的建议是先把线性编排跑通再根据实际需求加条件分支。很多刚入门的开发者一上来就想搭DAG式的复杂流程结果调试难度成指数上升。Agent最宝贵的特性是灵活性用线性编排模型自身的判断力已经能覆盖绝大多数场景。4. 常见问题与排查技巧实录4.1 模型死活不调用技能描述与上下文脱节这是我最常被问到的抱怨“我的技能明明写好了但模型就是不调用。”多数情况不是技能本身的问题而是技能描述与用户当前上下文之间缺乏语义关联。比如用户说“今天上海适合出门吗”你的技能描述是“获取天气预报信息”模型会觉得“天气预报”和“适合出门”有点距离犹豫要不要调用但如果描述改成“当用户询问天气、温度、降水和是否适宜出行时获取指定城市的实时天气预报”模型几乎不会犹豫。排查思路很简单把你当前的用户消息和技能描述放在一起读一遍看模型作为第三方能不能自然对接。如果觉得生硬那就改描述而不是怪模型笨。4.2 参数类型校验失败字段描述太模糊模型生成参数时对字段描述极其敏感。前面提到过timezone的例子我再补一个更普遍的枚举字段。比如一个send_notification技能channel字段定义成string模型就会随意填短信、sms、SMS而你的代码只认sms结果就是反复校验失败。解法有二在enum里写死合法值在字段描述里给明确示例“取值为sms或email分别表示短信和邮件”。我用得最多的是“描述enum双保险”channel: { type: string, enum: [sms, email, push], description: 通知渠道可选值sms/email/push }模型在90%以上情况下会直接选中枚举里的合法值。剩下10%靠运行时容错兜底。4.3 技能执行超时与死循环给循环加“安全绳”Agent循环最常见的生产事故是“模型调用同一个技能几十次把API账单打爆”。比如用户连续追问某个问题模型每次都先执行一次search_document结果每次都没搜到然后它仍然不死心继续搜。这不是模型“固执”而是你的Agent循环缺少状态控制。我的解决方案有三个设置max_steps上限比如5轮超过后强制结束返回“请换一种问法或联系人工”的回应对同技能连续失败做次数累计同一技能失败超过2次就尝试切换技能或直接让模型给最终回答把技能执行耗时纳入监控单个技能超过5秒就记录告警因为正常技能不该卡这么久。这些措施看似简单但在高并发场景下能帮你避免“一个用户拖垮整个服务”的极端情况。我在生产环境中见过典型的雪崩某个技能外呼第三方API超时Agent循环不知道超时反复重试直接把第三方服务打到限流最后连自己的服务也一起挂了。所以技能handler里一定要给网络调用设置显式超时比如3秒并把超时信息作为错误回传。4.4 技能效果如何评估命中率、成功率、成本技能体系上线后不能一放了之你需要一套尽可能自动化的评估机制。我在项目里主要看四个指标指标定义常见问题与设置建议技能命中率模型在应当调用技能的场景里实际调用了技能的比例偏低说明描述不准确或触发条件写得太苛刻技能选择准确率在多个技能可选的场景里选对了正确技能的比例偏低说明技能间描述区分度不够需做差异化技能执行成功率技能被调起后成功返回有效结果的比例偏低说明Schema设计或handler代码存在缺陷平均调用耗时与Token成本每次技能调用的时间开销和Token开销偏高说明描述过长、返回结果过于冗长这里有一个很务实的建议每次Agent执行完后把“用户消息、模型选择的技能、模型传入的参数、技能返回结果、最终回答”完整落盘存成结构化日志。这样积累一周后你就能拿真实数据做评估和优化而不是靠感觉。我项目里每天跑一个脚本筛出所有“模型选了A技能但结果是错误”的样本人工标注后回填到描述里。这个循环——记录、标注、改写描述——才是技能体系持续进化的真正引擎。4.5 避坑清单速查表最后整理一张速查表都是我在真实项目里实打实踩过的坑坑后果预防策略技能名重复后注册覆盖先注册模型调用A实际执行B注册中心做重名校验技能名全局唯一参数Schema只用typeobject不做字段级描述模型参数格式五花八门校验失败率高每个字段写清楚类型、枚举值、示例描述里只说技术功能不提触发场景模型不知道何时该用技能描述开头写“当用户需要……时使用”技能handler返回超长结果Token成本暴涨上下文被挤爆强制限制返回行数/长度或先聚合再返回网络调用不设超时单技能卡死拖垮整个Agent线程所有外部调用显式设置超时失败快速返回异常直接抛出对话直接中断体验崩溃捕获异常并格式化为可回传给模型的错误信息忽略技能执行顺序多技能组合时参数耦合混乱用线性编排跑通后再加条件分支不要一步到位Schema的required列表过于宽松关键字段缺失时模型自己补猜required只放真正必须字段其余给default工具列表一次性塞太多技能模型选择困难命中率下降按场景分组动态过滤候选技能列表不做日志落盘问题复盘没数据只能靠猜每个Agent循环全程记录结构化日志5. 一点个人经验与后续方向技能体系的搭建不是一个一次性工程它更像是在做产品先定义边界再实现原子能力然后不断通过日志反馈迭代描述与Schema。我在实际使用中最受益的一个习惯是每次模型选错技能我不去责怪模型而是把那次对话存档改造技能描述让它下次更容易选对。模型其实是极其“诚实”的你把描述写得多清楚它就回报你多高的命中率。如果这个项目继续往下扩展我会优先尝试的方向有两个。第一个是把技能库接入一个可视化的配置后台让业务人员不用写代码也能定义新技能的触发条件和参数Schema第二个是给技能调用加一层“成本预算”比如每次会话最多允许调用3次高成本技能超出后模型必须改用更低成本的路径。这两个方向的核心目标一致让技能体系更可控、更经济。另外如果你想在团队里推广这套方案建议不要追求一口气把几十个技能全部定义完而是从3~5个核心业务技能开始跑通评估闭环再逐步扩充。技能库和代码库一样多而烂不如少而精。一个只有5个技能但每个都很可靠的系统价值远高于一个有50个技能但经常选错的系统。先把这套“描述—Schema—执行—反馈”的循环跑顺了后面再加技能就是水到渠成的事。
返回列表