资讯动态

大模型Agent技能库工程实战:从Prompt硬扛到可复用执行层

发布时间:2026/10/14 8:16:51 来源:尧图企业网站定制
如果最近你也在折腾大模型应用多半和我一样绕不开“agent-skills”这个词。我最早看到“agent-skills”时还以为是某个炫酷的模型能力包后来动手做了一整套基于技能库skills的AI助手才发现它真正解决的是大模型“只会说话、不会干活”的尴尬模型负责判断和拆解任务真正操作文件、请求接口、读写数据库的都是挂载在Agent上的一个个技能模块。没有技能库Agent只是一个加强版聊天框有了技能库它才像一支真正有“手脚”的执行团队。这组内容想做的是把一个实用的Agent技能库工程从思路到代码完整拆开包括技能的定义方式、注册机制、调度策略、权限控制还有我在实际落地过程中踩过的坑。不管你是刚开始接触Agent开发还是已经在做自己的自动化工具这份拆解都能帮你少走不少弯路。1. Agent技能库的定位为什么不能只靠Prompt硬扛1.1 从“生成文本”到“生成行动”Agent缺的是执行层先想一个问题你让模型“帮我把当前目录下的日志文件按时间排序整理成报告”它该怎么做到模型本身不接触文件系统它只能输出文字。过去大家用Prompt硬扛在系统提示词里写“你可以调用这些接口1. 查询文件列表 2. 读取文件内容 3. 写文件”然后期望模型自己编一段类似函数调用的文本出来。这个玩法在Demo阶段没问题但真到复杂任务就开始失控。模型生成文本的天性决定了它很难稳定遵循一套“手写协议”。你可能遇到过模型把参数名写错、把接口路径拼接错、把“调用”和“假装调用”混在一起。本质上模型是一个“决策器”不是“执行器”。技能库就是夹在决策器和真实系统之间的执行层把能力注册成标准模块让模型以“选技能、填参数”的方式发起动作并由代码负责真正干活。用生活里的例子说模型像大脑它想“我要喝水”但倒水这个动作必须由手来完成。技能库就是连接大脑和手的神经接口。你给大脑的不是“你怎么倒水”的废话指令而是一张清单“你的手可以执行的动作有拿起杯子、拧开瓶盖、倒水”每个动作标注适用场景、参数要求。大脑负责选动作手负责执行接口负责把结果传回给大脑。1.2 纯Prompt方案撑不起Agent的三个原因我踩过最深的坑就是过度依赖Prompt“万能话术”最终项目复杂度大到不可收拾。具体说纯Prompt方案有三个硬伤第一行为不稳定。模型对工具的描述理解是概率性的今天它可能老老实实把参数写成JSON明天同样的Prompt下它就开始自由发挥参数类型全错。尤其当技能数量超过10个之后模型选错工具的概率直线上升。第二能力不可复用。你在这个Agent里写的“发送邮件”Prompt换个Agent又要重新写一遍。技能逻辑比如SMTP连接、附件处理已经写好但描述、约束、错误处理全杂揉在Prompt里根本没法拆分打包。第三无法观测和管控。模型在对话里“模拟调用”接口时调用是否成功、参数是什么、耗时多久、用户有没有权限这些全都不可控。真正生产环境需要的审计日志、白名单、并发隔离纯Prompt方案完全给不了。技能库的解法是把“技能的描述文件”和“技能的执行代码”完全分离。模型只看一份结构化说明技能名、用途、参数Schema、返回说明而代码负责校验、执行、异常处理、日志记录。这样模型的行为边界被刻意收缩“你只能从清单里选技能参数必须符合Schema”实现了对Agent的工程化约束。2. 技能库的整体设计与核心机制2.1 一个技能的本质三段式结构我在实际工程里每个技能都由三部分组成元信息描述让模型知道何时该用、参数Schema让模型知道怎么填参、执行函数真正干活的地方。三者缺一不可。元信息描述最重要的是“触发条件”。只写“查询天气”远远不够要写清楚“当用户需要获取某城市当前或未来几天的天气情况时使用该技能。该技能返回温度、风力、降水概率。”模型的技能选择很大程度依赖描述文本和用户问题的语义匹配度描述写得越精确选错率越低。参数Schema建议直接用JSON Schema标准。它是一份机器可读的“参数说明书”既能约束模型填参还能在代码里做校验。有个额外的好处很多模型厂商的工具调用接口原生支持JSON Schema你定义一份Schema就可以直接透传过去不用重复维护。执行函数不用什么花哨设计保持“入参清晰、出参结构化、异常可控”即可。出参最好也固定为JSON格式方便模型解析后继续推理。如果是图片、文件之类的大块数据别直接塞回上下文传一个可访问的路径或者预生成的摘要就够。2.2 技能注册表为什么不用一大串if-else技能多了以后最忌讳的就是在业务代码里写一长串if-else“如果是weather就调weather函数如果是email就调email函数”。这种写法改一个技能要动主流程加一个技能要改核心代码排查问题更是灾难。我用的方案是把技能注册成一个字典映射。启动时扫描技能目录每个技能文件用装饰器注册自己的名字和执行函数最后形成一个统一的路由表。模型说“我要调用skill_email_send”路由表直接定位到对应执行器把参数传进去就行。整个过程只有“注册”和“查询”主流程完全不关心某个技能内部如何实现。这样做有三个直接收益。扩技能时不动主流程新人加一个技能文件、写一个装饰器、写一个处理函数就能接入隔离性变好某个技能出现问题只影响它自己不至于拖垮整个Agent权限控制更容易可以在路由表这一层统一做用户级校验而不是在每个技能里重复判断。2.3 调度策略模型每轮都能看到所有技能吗这是技能库设计里最容易被忽视的问题。技能数量少的时候把全部技能描述塞给模型没问题但技能库一旦扩展到三五十个描述本身就会占去大量上下文模型的选择准确率也会显著下降。我的做法是加一个“技能路由层”先把技能按领域分组比如“信息查询类”“文件操作类”“通信通知类”“数据处理类”。当用户请求进来先由一个大模型或者轻量规则模块判断请求属于哪个领域再只把该领域的技能描述注入给主Agent。这相当于先给模型划定了“你在这组场景下可以从这些技能里选”减小选择面提升稳定性。另外调度时要注意“技能返回结果”的二次加工。很多技能执行完返回的是原始数据结构模型需要基于它继续回答用户。所以我在调度循环里做了约定技能执行完成后结果会作为一条“工具消息”重新送回模型让模型用自然语言总结给用户。这里的顺序是“用户提问 - 模型决策选技能 - 执行技能 - 返回结果给模型 - 模型总结回复”整个链路由一个循环控制而不是一次调用搞定。3. 实操从零搭建一个可直接用的Skills工程3.1 项目目录结构与基础基类我建议按下面的目录结构来组织技能库工程这套结构我自己用了很久扩展性很好agent-skills/ ├── core/ │ ├── skill_base.py # 技能基类与装饰器 │ ├── registry.py # 技能注册表 │ ├── schema_validator.py # 参数校验器 │ └── executor.py # 技能执行器与调度循环 ├── skills/ │ ├── __init__.py # 技能包入口自动注册 │ ├── file_search.py # 文件搜索技能 │ ├── web_fetch.py # 网页抓取技能 │ └── database_query.py # 数据库查询技能 ├── routers/ │ └── skill_router.py # 领域路由选择候选技能集合 ├── config/ │ └── settings.py # 技能白名单、超时、模型配置 └── logs/ └── agent_trace.log # 技能调用流水核心基类skill_base.py里我定义了一个轻量的技能描述数据结构from dataclasses import dataclass, field dataclass class SkillDefinition: name: str description: str parameters_schema: dict # 可选的示例参数帮助模型理解 examples: list field(default_factorylist)然后是装饰器reigster_skill它做的事是把当前技能加入全局注册表。注册表本质就是一个普通字典key是技能名value是SkillDefinition加执行函数。这个设计简单直接不用引入重量级框架也方便后续加中间件日志、权限、限流。3.2 手写一个“文件搜索”技能拿最常用的“文件搜索”技能演示完整流程。先定义描述和Schemafile_search_definition SkillDefinition( namefile_search, description在指定目录下按文件名或扩展名搜索文件。 当用户需要查找某个目录里的文件、确认文件是否存在、 或者按类型枚举文件时使用。返回文件路径列表。, parameters_schema{ type: object, properties: { root_dir: { type: string, description: 搜索的根目录绝对路径 }, keyword: { type: string, description: 文件名包含的关键词可为空字符串 }, extension: { type: string, description: 文件扩展名例如 .md可为空字符串 }, max_results: { type: integer, description: 最多返回多少条结果默认20, default: 20 } }, required: [root_dir] }, examples[ {root_dir: /workspace, extension: .md, max_results: 10} ] )接着实现执行函数import os def file_search_executor(params, contextNone): root_dir params[root_dir] keyword params.get(keyword, ) extension params.get(extension, ) max_results params.get(max_results, 20) # 注意不要用os.walk直接遍历过大目录设置深度限制 results [] for dirpath, _, filenames in os.walk(root_dir): # 跳过隐藏目录和虚拟环境目录 if /. in dirpath or /node_modules in dirpath: continue for fname in filenames: if extension and not fname.endswith(extension): continue if keyword and keyword not in fname: continue results.append(os.path.join(dirpath, fname)) if len(results) max_results: return {file_list: results, truncated: True} return {file_list: results, truncated: False}这个执行函数有几个细节值得注意遍历目录时跳过虚拟环境和隐藏目录可以省掉大量无效IO返回结果里带上truncated字段模型看到这个字段就知道结果可能被截断可以引导用户精准搜索参数是有默认值的这样模型偶尔漏填不致命。注册入口也非常简单from core.registry import registry from core.skill_base import SkillDefinition registry.register(SkillDefinition(...)) def file_search(params, contextNone): return file_search_executor(params, context)整个文件写完后任何技能文件只要被导入过一次就会自动出现在全局注册表里。主流程完全不知道file_search内部怎么实现只用注册表查询入口。3.3 与模型的调用链路打通技能库落到Agent最核心的是和模型交互的循环。我现在用的方案是标准的工具调用接口第一轮把用户问题加上可用的技能描述一起发给模型模型如果决定需要调用某个技能会在返回里带有结构化工具调用请求。代码解析这个请求从注册表里取出对应执行函数传入参数拿到结果再把结果拼进消息列表发给模型继续生成回复。一个简化版的调度循环如下from core.registry import registry def run_agent(user_input, available_skills, model_client): messages [{role: user, content: user_input}] # 将可用技能描述注入模型请求 tool_defs [ registry.to_tool_schema(skill_name) for skill_name in available_skills ] for turn in range(5): # 最多允许5轮工具调用 response model_client.chat(messages, toolstool_defs) if not response.tool_calls: return response.content for tool_call in response.tool_calls: result registry.execute( tool_call.function.name, tool_call.function.arguments ) messages.append({ role: tool, tool_call_id: tool_call.id, content: json.dumps(result, ensure_asciiFalse) }) return 执行步骤过多已自动停止这里的循环上限5轮是为了防止Agent陷入无限调用实际使用中建议3到8轮之间具体看任务复杂度。每一轮工具调用都是模型的一次新决策轮数太高既费token又容易跑偏。调用链路中一个很重要的细节工具结果必须以结构化字符串回传给模型且最好带上“当前是第几步”或者“这个结果正常/异常”的标记。我在很多案子里看到结果是返回了但模型没意识到自己已经执行过某个工具于是重复发起同一个调用浪费大量时间。3.4 权限与资源控制技能库能跑起来的底线技能库只要一暴露给真实用户权限问题和资源控制就绕不开。我总结下来至少要做三件事。第一技能白名单。用户角色不同可用技能集合不同。普通用户只能调“文件搜索”“信息查询”管理员才允许调“删除文件”“执行脚本”。这个控制不是在技能内部做而是在路由表层面做根据当前会话的用户级别过滤一遍可注入的技能描述。第二执行超时与资源限制。某些技能可能因为网络问题或数据量过大卡住。我在执行器里统一加了超时控制单个技能执行上限比如10秒超过就返回“执行超时”给模型让模型告知用户或换一种方式。同时记录每次调用的token成本跑完一个任务可以汇总成本方便分析Agent是不是过度调用工具了。第三不可逆操作二次确认。删除、写入、发送类技能在定义描述时明确要求“执行前必须向用户确认参数”并在执行函数里先返回一个“待确认状态”等用户确认后真正执行。最初我没有这个设计结果模型根据用户一句“把那些文件都删了吧”真的把所有文件删掉后来花了一整天才恢复数据。从那以后所有破坏性技能一律强制二次确认模型侧也增加相应的Prompt约束。4. 技能库落地中的高频问题排查实录4.1 模型总选错技能或者虚构技能名这是技能库上线后遇到最多的一个问题请求明明是查天气模型却调用了一个“获取地理位置”的技能甚至有时会调用一个注册表里根本不存在的技能名。这个问题的根源通常有三个。第一是技能描述太笼统和其他技能的描述边界模糊。比如“get_weather”和“get_location”都提到了“城市”这个词模型就容易混淆。解决方法是把每个技能的“适用场景”和“不适用场景”都写清楚。给get_weather加一句“只负责查询天气不要用于查询城市名称或坐标”。第二是技能数量太多模型选择空间过大。此时需要加重技能路由层先把任务分到领域再让模型在几个候选技能里选。我实测过同一批30个技能不分组准确率只有70%分组后准确率能提到95%左右。第三是模型幻觉直接编了一个“log_read”这种不存在的技能。这种情况不能只靠Prompt约束应该在调度循环里做一层“工具名校验”请求的工具名不在注册表内时直接返回“未知工具”给模型并附上当前可用工具名单让模型重新决策。有了这层保护幻觉工具名不会产生任何实际动作。4.2 参数解析失败模型填了Schema不认的数据另一类高频问题是参数格式对不上。比如Schema要求max_results是integer模型填了字符串“二十”或者漏掉必填字段导致执行函数直接崩溃。纯靠模型自我纠正很费劲更可靠的是参数校验加自动重试。我通常在执行器外层套一个校验函数def validate_args(args_json, schema): try: params json.loads(args_json) except json.JSONDecodeError: return None, 参数不是合法JSON # 这里用jsonschema库校验 errors list(jsonschema.Draft7Validator(schema).iter_errors(params)) if errors: return None, 参数不符合要求: str(errors[0].message) return params, None校验失败时把具体错误信息作为工具结果返回给模型让模型根据错误信息修正参数。我在Prompt里加了一句“认真参考参数Schema字段名和类型必须以Schema为准”。配合这套做法参数类错误能减少八成以上。还有一个实战技巧给每个技能加上示例参数。模型对示例的依赖比对纯Schema的依赖强得多示例里出现过的填法模型更容易模仿。尤其是枚举值字段比如状态只能是“open/closed”给一个示例会让模型少犯很多错。4.3 工具返回结果太大上下文被撑爆技能执行结果回传给模型后模型要把它和用户问题一起处理。如果工具返回的是一份几十万字符的日志上下文窗口迟早被撑爆。这个问题我在做“代码库分析Agent”时体会特别深。解决思路是“结果瘦身”回传给模型的不是原始结果而是摘要或元数据。文件列表太长就只传前10条加总数数据库查询结果太大就分页每页只传必要的字段网页抓取则用提取算法先去掉导航、广告等噪音只传正文内容。更彻底的方案是引入一个“结果摘要技能”。工具执行完原始结果不出来先进入一个专门的摘要模型生成一段紧凑的总结再传给主Agent。这相当于给所有技能加了一层信息压缩。需要保留细节的场景比如代码修复则改成把结果写入临时文件回传文件路径模型需要详细内容时再调一个“读取文件片段”的技能。4.4 多用户并发下的状态污染如果Agent跑在服务端同一个技能可能同时被多个用户的会话调用到。最容易踩的坑是在技能实例里保存“当前用户”或者“当前任务”的状态。比如一个技能里定义了self.current_dirA用户设置了路径B用户再调用时读到的却是A的路径。这个问题要从两个层面根治。一是技能本身做成无状态所有输入都靠params传入执行完不留任何内部状态。二是引入一个上下文对象session里面存放user_id、当前工作目录、权限级别等信息执行函数需要情境时从context参数里取而不是从自身属性里取。我改造后的执行函数签名统一为executor(params, contextNone)context由调度循环每次调用时注入天然隔离不同会话。另外并发场景要注意日志追踪。我给每次任务生成一个trace_id所有技能调用日志都挂上这个id。排查线上问题时只需按trace_id拉出完整的调用链就能看清哪一步参数填错、哪一个调用超时、模型为什么走偏。没有trace_id之前排查一个问题要在各个日志文件里来回翻效率极低。4.5 技能内部报错怎么反馈给模型技能执行函数难免抛异常比如文件不存在、接口返回500、数据库连接失败。最忌讳的是把异常堆栈直接抛给上层导致整个Agent中断。我在执行器里统一把异常捕获转换成结构化的错误信息回传模型{ error: FILE_NOT_FOUND, message: 目录 /tmp/xxx 不存在, suggestion: 请检查路径拼写或先调用 dir_list 查看当前目录内容 }错误信息里附带“suggestion”字段相当于给模型一个纠偏指引。模型看到之后能直接根据建议调整计划而不是一脸茫然地让用户“尝试换一个说法”。这个设计极大提升了多步任务的成功率工具出错不再是终点而是一个可以被模型消化并修正的中间状态。5. 技能库维护的经验沉淀与后续方向技能库用久了我发现最需要花心思的不是实现而是治理。技能数量膨胀得很快一不当心就会积累出一堆描述含糊、功能重叠、连作者自己都不确定该怎么用的技能。我在自己的技能库里沉淀了一个原则每个技能必须有一个“负责人场景”宁可让两个场景共用一套底层函数也不要拆成两个描述相似但边界模糊的技能。加新技能前先跑一遍现有技能列表能复用旧能力就复用不能复用再新建。另一个心得是迭代周期里一定要看“技能调用失败日志”而不只是看用户反馈。模型经常在悄悄尝试某个技能但调不到日志会清楚记录这类情况。我定期统计各个技能的调用次数、失败率、平均参数错误数把这些指标作为技能优化的依据。很多“用户说Agent不太好用”的问题根源其实是某个技能描述陈旧功能已经更新了描述还停留在旧版。后续我打算在技能库基础上做两层扩展。一层是技能编排能力让一个技能内部可以调度其他技能比如“生成周报”技能内部去调用“读取本周任务”和“汇总统计”两个技能而不是在主循环里硬写流程。另一层是基于用户反馈的自动技能修正通过收集用户对某一类回答的满意度反向调整对应技能的描述权重或者参数约束。这条路还很早但技能库的注册表和观测机制会给它留好接口。技能库从“能跑”到“好用”差距往往就在这里一堆技能的堆积和一套被精心维护的能力体系本质上是两种工程。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑