如果你也调过智能体,大概率见过这种割裂感:大模型嘴上功夫一流,真让它去读写文件、查数据、发请求,它就当场“礼貌无能”,只会说“我可以帮你,但需要你手动操作”。去年我自己搭了一套叫 agent-skills 的技能库项目,专门治这个毛病。它不靠某个特定框架,而是一套把大模型的意图识别能力翻译成“可执行技能调用”的标准做法。这篇文章我会把当初做这个技能库的设计思路、目录结构、核心代码骨架、多Agent复用的方案全都拆开讲,包括我踩过的坑。如果你正在做Agent、准备接工具调用、或者纠结怎么让自己的智能体能真正“干活”,这篇文章应该比看一堆概念教程有用。
1. 先说清楚:agent-skills 到底解决什么问题
1.1 一个常见的翻车现场:Agent 死活不干活
我接手过一个内部需求,让大模型读某个数据文件,按固定模板生成汇总报告,然后把报告写到指定目录。最开始同事直接打开带对话界面的Agent,让它“看一下文件,然后生成一份周报”。结果模型每次回答都是“我可以帮你总结”,可文件它始终没读对,生成的内容也是对话窗口里的一段文字,而不是实际落盘的文件。
这就暴露了当前大模型的本质:它擅长理解意图、生成文本,但天生不适合执行精确的、有副作用的动作。模型不是不知道“写文件”是什么,而是它没有稳定的“手”去调文件系统。你让它连续做两次文件操作,哪怕参数一模一样,也可能一次成功、一次漏掉。这种不稳定性靠“prompt写得更好”很难彻底解决,真正可靠的办法是给模型配一套确定性的技能调用通道。
agent-skills 出场的时机就在这里:把每个具体动作,比如“读取文件”“写文件”“发HTTP请求”“查数据库”“发邮件”,封装成独立的小模块,并在模块顶部写清楚“什么时候用、参数是什么、返回什么样”。大模型不需要亲手执行代码,它只需要从技能清单里挑一个、把参数填对,剩下的事情由本地函数完成。
1.2 技能库的本质:把“会”变成“能做”
很多人区分不了“会”和“能做”。模型读完你的技能描述,能复述功能,这不难;难的是在真实任务里稳定触发对应动作、传对参数、处理好返回结果。技能库本质上就是在这两层中间加一道“标准化接缝”。
每个技能完成一个原子动作。什么叫原子?就是一次调用只做一件事,别把“读取文件+解析内容+写入数据库”写成一个技能。这样做的好处有三个:第一,模型容易理解,触发条件清晰;第二,方便复用,多个Agent都能调用同一个技能;第三,便于排查问题,哪个技能出错,直接把范围缩到那一个模块。
我用 agent-skills 这个名称,也是想强调“技能”而不是“插件”。插件往往绑定宿主系统,技能则更像一个能力单元:有名字、有描述、有输入输出契约、有版本,可以独立发布和更新。换一个模型、换一个Agent框架,技能定义本身不用大改。
1.3 为什么叫 agent-skills,而不是 plugins
不是故意造词。plugin 的隐含假设是“被宿主程序加载”,而 skill 的隐含假设是“被Agent按需发现”。一个Agent系统可能同时对接文档处理、API调用、数据分析等能力,如果只做插件,能力之间容易耦合成一锅粥。但技能库把能力拆成彼此独立的“可检索条目”,模型选技能时更像是“搜索”而不是“调用加载器”。
这个区别直接决定了设计方式。插件通常要考虑框架生命周期、生命周期钩子、界面集成;技能则只需要考虑三点:怎么描述、怎么执行、怎么返回。描述决定了模型会不会选中它,执行决定了能不能跑通,返回决定了模型下一步怎么走。后面的设计基本都是围绕这三件事展开的。
2. 技能库的整体设计:目录、描述、参数与返回值
2.1 目录结构与命名规范
我的技能库在文件层面就是一个文件夹,里面每个子目录对应一个技能。目录名就是技能ID,全局唯一。整体长这样:
skills/ ├── send_email/ │ ├── SKILL.md │ ├── skill.py │ └── requirements.txt ├── query_order/ │ ├── SKILL.md │ ├── skill.py │ └── requirements.txt ├── upload_file/ │ ├── SKILL.md │ ├── skill.py │ └── requirements.txt └── calculate_date_diff/ ├── SKILL.md └── skill.py命名规范是我踩过很多次坑之后定下来的:技能ID用动词开头,全部小写、下划线分隔。比如send_emailquery_order,不要用emailService或者file-handling这种既有大小写又有连字符的写法。
原因是模型在一个候选列表里做选择时,对“动词开头”的短名字有天然的语义偏向,一眼能看出“这技能是干什么的”。如果技能ID本身就模棱两可,模型就容易挑错。我试过把技能叫data_processing,结果用户说“帮我整理一下表格”时,模型好几次选了这个技能,实际它连入参是什么都说不清,最后还得靠描述补。
2.2 SKILL.md 描述文件长什么样
每个技能目录里必须有一个SKILL.md。这个文件主要不是给人看的,是给模型看的。模型靠读它来决定“当前需求要不要调用这个技能”,所以里面的字字句句都很关键。
我的做法是使用 YAML front-matter 加简短说明。下面是一个例子:
--- name: send_email version: 1.2.0 description: 发送一封纯文本邮件。适合通知、告警、报告发送等场景。 input: 收件人地址、邮件主题、正文。 output: 返回邮件唯一ID;发送失败时返回错误信息。 when_to_use: 用户明确要求发送邮件、提醒、告警,或者需要把生成结果发给某人时。 when_not_to_use: 用户只是讨论邮件内容,或者要求发送附件的场景需要看附件处理技能。 permission: ["network", "smtp"] author: internal --- ## Usage 发送一封文本邮件到指定地址。 ## Examples 用户说:给 xxx@example.com 发一封邮件,主题是“周报”,正文是“已完成本周工作”。 调用参数:{"to": "xxx@example.com", "subject": "周报", "body": "已完成本周工作"}你可能会觉得描述带when_not_to_use太啰嗦,但这一步很有必要。模型在没有限制时很容易“强行调用”技能,比如用户明明只是问“邮件服务现在通不通”,模型却可能真的去发一封测试邮件。把“什么时候不该用”写清楚,能明显降低误调用率。
description原则上不要超过200字。太长了既占上下文,又会让模型抓不住重点。我实测下来,一段100到150字、含触发场景和输入输出的描述,效果最好。
2.3 参数契约:让模型像填表一样调用技能
技能描述是给模型做“要不要选我”的决策,那参数定义就是给模型做“怎么填表”的决策。这块必须用结构化Schema,不能靠自然语言描述来约束。
我习惯在每个技能目录放一个schema.json,里面写清楚参数类型、是否必填、取值范围,甚至示例值。例如send_email的参数定义:
{ "type": "object", "properties": { "to": { "type": "string", "format": "email", "description": "收件人邮箱地址,多个地址用逗号分隔" }, "subject": { "type": "string", "maxLength": 100, "description": "邮件主题,尽量简短" }, "body": { "type": "string", "description": "邮件正文" } }, "required": ["to", "subject"] }真正执行前,我会先用这个Schema校验模型生成的JSON。比如模型可能把subject写成 null,或者漏掉to,校验阶段直接拦截,不会进入执行函数。这么做不仅安全,也能让你快速发现问题次数多的字段,反过来优化描述。
还有一个容易忽略的点:字段名尽量用通用的小写单词,不要用带特殊意义的缩写。模型如果看不懂rcpt_addr这种内部叫法,它就可能瞎猜。我后来全部改成tosubjectbody这种一看就懂的字段,调用成功率明显上升。
2.4 返回值格式的约束:让结果可被后续Agent处理
技能执行完,Model 还要读返回值决定下一步。如果每个技能的返回格式都不一样,模型就乱了。所以我在 agent-skills 里统一了返回值格式,所有技能都必须返回下面这个结构:
{ "ok": true, "data": { "mail_id": "a1b2c3", "sent_at": "2025-01-01 10:00:00" }, "error": null }失败时返回:
{ "ok": false, "data": null, "error": { "code": "SMTP_CONN_FAILED", "message": "无法连接到邮件服务器,请检查网络配置" } }为什么这么设计?因为模型在决定“是否再调用下一个技能”“是否要把错误反馈给用户”时,需要快速识别状态。如果每个技能成功时返回一个字符串,失败时抛个异常,模型处理异常非常困难。统一返回值之后,Agent的后续策略写起来也简单:ok=true继续下一个动作,ok=false读取error.message决定是否需要重新调用或告知用户。
经验是error.message一定要写成“人能看懂、模型也能直接引用”的话,不要写internal error code 503。因为模型很可能原封不动地把这条信息转述给用户,用户看到“503”多半一脸懵。
3. 核心实现:技能注册、加载与动态路由
3.1 注册机制:声明文件怎么进库
技能库的注册方式有两种,一种是“显式注册”,在代码里写register(send_email);另一种是“隐式扫描”,启动时扫目录自动发现。我最终选了隐式扫描,主进程代码不用改,新增技能就是“丢一个文件夹进去”这么简单。
但纯扫描目录还不够,因为技能类型五花八门,有Python函数,有HTTP请求,也有本地脚本。所以我实现了一个极简注册表,用装饰器把执行函数绑定到技能名上:
# registry.py _SKILLS = {} def register(name, version): """注册一个技能到全局技能表。""" def wrapper(func): _SKILLS[name] = {"func": func, "version": version} return func return wrapper然后技能模块内部可以这样写:
# skill.py import registry @registry.register("send_email", "1.2.0") def send_email(params: dict) -> dict: """执行发送邮件的真正逻辑。""" # ... 邮件发送代码 return {"ok": True, "data": {"mail_id": "..."}, "error": None}注册表本身不负责技能描述,描述还是放在SKILL.md和schema.json里。装饰器只需要记录哪个函数对应哪个技能ID,真正加载时再去读描述文件。
3.2 加载器:扫描目录、解析描述、绑定执行函数
加载器我写成一个SkillManager类,它做四件事:
- 扫描
skills/下所有子目录; - 读取每个目录里的
SKILL.md、schema.json; - 导入目录里的
skill.py,拿到注册表里对应的执行函数; - 组装成完整的技能对象,放进内存列表。
示例逻辑:
import os import json class SkillManager: def __init__(self, skills_root="skills"): self.skills_root = skills_root self.skills = [] def load_all(self): for skill_dir in os.listdir(self.skills_root): path = os.path.join(self.skills_root, skill_dir) if not os.path.isdir(path): continue self._load_skill(path) return self.skills def _load_skill(self, path): name = os.path.basename(path) skill_md_path = os.path.join(path, "SKILL.md") schema_path = os.path.join(path, "schema.json") if not os.path.exists(skill_md_path): return # 解析 SKILL.md 头部信息(简单起见用字符串处理或 PyYAML) # 读取 schema schema = {} if os.path.exists(schema_path): with open(schema_path, "r", encoding="utf-8") as f: schema = json.load(f) # 这里执行目录里的 skill.py,并捕获注册表项 # 此处略,实际会 importlib 动态导入 self.skills.append({ "name": name, "description": parsed_description, "schema": schema, "exec_func": func_ref, })这里有个我一开始没注意的坑:动态导入时,技能里的skill.py应该用importlib导入,并且要给每个技能一个独立命名空间,避免两个技能都叫skill.py时互相覆盖。用importlib.import_module(f"{path}.skill")会让模块缓存混乱,我最后是直接用文件路径的哈希生成模块名,虽然丑,但稳定。
3.3 动态路由:模型怎么从小技能列表里选对技能
技能加载完后,核心问题就是:用户说了一句自然语言,系统怎么知道该调哪个技能?这里我不建议一上来就让整个技能列表直接在模型调用接口里生成工具列表。技能少可以,一旦超过二三十个,上下文占用大,模型的选择准确率也会下降。
我的方案分两层:
第一层是“粗筛”。用文本向量的方式把用户输入和每个技能的 description 做相似度匹配,取 topK 候选,K一般在5到10之间。这一步不需要大模型参与,用本地向量库或者简单的词频计算都能实现。
第二层是“精排”。把 topK 候选技能的完整描述(包括参数schema)发给大模型,让它从候选里挑出真正匹配的技能,并输出调用参数。精排这一步才用到大模型的语义理解能力。
这个“先粗筛后精排”的好处非常明显:上下文从几百个技能的描述,压缩到几个候选技能,调用速度更快、模型不容易犯选择困难症。我早期图省事把所有技能直接塞进prompt,结果技能到100个左右时,模型经常在几个相似技能里“举棋不定”,偶尔还会选一个明显不相关的。
3.4 权限与隔离:技能越权怎么办
技能库最大的风险是执行危险动作。比如某个技能可以删文件、发外部请求、执行 shell 命令,如果只靠模型判断“这个该不该调”,等于把钥匙交给一个容易犯迷糊的人,迟早出事。
我在 agent-skills 里给每个技能打上权限标签,并在执行前做检查。比如send_email标注permission: network, smtp,delete_file标注permission: filesystem,然后在运行时设定一套策略:如果当前Agent没有被授予某个权限,就算模型选择了技能,系统也会拒绝执行并返回PERMISSION_DENIED。
对更底层的隔离,我给高危技能加了沙箱机制。简单做法是让技能跑在一个受限的临时目录里,用完就清理;再进一步可以用subprocess隔离进程,限制可执行文件路径。这些措施不用一开始全做,但至少要保留一个“默认拒绝高危操作”的开关,否则别人拿到你的技能库,再配一个自主权限过大的Agent,风险会很大。
4. 多Agent共享:从单个Agent到技能市场
4.1 多Agent场景下为什么需要集中式技能库
如果你只是跑一个Demo,技能目录放在本地就够。但如果系统里有多个Agent,分别处理客服、数据分析、自动化运维,你再回头看就会发现问题:每个Agent都在重复造轮子。A Agent 写了一个query_order,B Agent 又开始抄一份改改,然后两边行为不一致,修一个 bug 还要各处同步。
我在项目中期做了一次重构,把技能库从“本地文件夹”升级成“集中管理”。所有技能统一放在一个服务里,各Agent通过接口获取技能描述、提交调用请求。这样每个技能只有一份实现,更新一次,所有Agent立即生效。
集中式还有个额外好处:可以统一记录调用日志。谁调了哪个技能、传了什么参数、返回是否成功,都集中到一个日志系统里。后面排查“是不是技能本身坏了,还是模型选错了技能”,会省很多力气。
4.2 技能兼容层:同一个技能服务多个模型
不同大模型对工具调用的格式千奇百怪。有的模型支持原生 function calling,有的只支持在 prompt 里给一段 JSON 示例,还有的需要用 XML 描述工具。如果每个模型都重写一遍技能库的适配层,那会累死人。
我在技能库和模型之间加了一个兼容层。兼容层内部维护一份统一的技能描述格式,然后根据不同模型的目标格式做转译。比如对支持 function calling 的模型,把技能名、描述、schema 转成该模型的 function 列表;对不支持 function calling 的模型,把技能列表转成一个Available skills:的文本块,让模型在回复里夹带一段{"skill": "...", "params": {...}}再解析出来。
这个兼容层让同一个技能库可以同时服务多个Agent,也降低了以后换模型的成本。好处是真实存在的:我有一次把某Agent的基础模型从A换到B,整个过程只改了兼容层里的一个适配器,技能库本身完全没动。
4.3 技能版本号与灰度发布的实战取舍
技能集中之后,版本管理就成了不可回避的问题。假设query_order之前返回的订单金额字段叫amount,新版改成total_amount,所有依赖它的Agent都会受影响。如果没做版本控制,改一个字段可能会炸掉整个流程。
我会在每个技能对象里带version,并在Agent初始化时声明自己需要的版本范围。集中服务更新技能时,可以保留旧版本一段时间,新调用走新版本,旧调用继续用旧版本,直到确认没有异常再下掉旧版本。这一套思路本质上就是灰度发布,没必要一开始做得很重,只需要在技能服务里加一个路由策略:按Agent ID或流量比例选择不同版本。
我踩过的最痛的一个坑是:技能返回结构小改,但没有同步改所有下游脚本,结果某个Agent后续流程里读旧字段,得到 null 以后又去调了另一个技能,最后生成了完全错误的报告。自那之后我养成习惯,技能返回结构变更时,必须发布为新的minor版本,不允许原地改字段名。
5. 实操落地:我给 agent-skills 写的骨架代码
5.1 最小化技能定义示例
下面是一份能直接跑通最小闭环的骨架。假设我们要做一个read_file技能,功能是读取指定文本文件的原始内容。目录结构:
skills/ └── read_file/ ├── SKILL.md ├── schema.json └── skill.pySKILL.md:
--- name: read_file version: 1.0.0 description: 读取本地文本文件内容。适合查看日志、读取配置、加载文档等场景。 input: 文件路径。 output: 文件内容;如果文件不存在或无法读取,返回明确错误。 when_to_use: 用户要求查看某文件内容,或者后续操作需要读取文件数据时。 when_not_to_use: 用户只是提到文件名但未要求读取时,不要擅自调用。 permission: ["filesystem"] --- 读取一个本地文件的纯文本内容。schema.json:
{ "type": "object", "properties": { "path": { "type": "string", "description": "要读取的文件的绝对路径" } }, "required": ["path"] }skill.py:
import registry @registry.register("read_file", "1.0.0") def read_file(params: dict) -> dict: path = params.get("path") if not path: return {"ok": False, "data": None, "error": {"code": "MISSING_PATH", "message": "缺少参数path"}} try: with open(path, "r", encoding="utf-8") as f: content = f.read() except FileNotFoundError: return {"ok": False, "data": None, "error": {"code": "FILE_NOT_FOUND", "message": f"文件不存在: {path}"}} except PermissionError: return {"ok": False, "data": None, "error": {"code": "PERMISSION_DENIED", "message": "没有权限读取该文件"}} return {"ok": True, "data": {"content": content[:20000]}, "error": None}注意我在返回值里把内容截断到20000字。这是为了避免技能返回超长文本撑爆模型上下文。真正需要完整内容时,可以追加一个max_length参数,超出部分让模型分页读取。
5.2 加载与执行完整流程
技能库管理器需要提供两个核心方法:list_skills和execute。前者让上层Agent获取技能描述列表,后者按技能名执行。
# skill_manager.py class SkillManager: def __init__(self): self.skills = {} # ... 加载过程省略 def list_skills(self): return [ { "name": name, "description": skill["description"], "schema": skill["schema"], "version": skill["version"], } for name, skill in self.skills.items() ] def execute(self, name, params, timeout=10): skill = self.skills.get(name) if not skill: return {"ok": False, "data": None, "error": {"code": "SKILL_NOT_FOUND", "message": f"未找到技能: {name}"}} # 参数校验 # 调用执行函数,并做超时处理 return skill["func"](params)执行时用一个超时装饰器,避免技能内部发生死循环或网络阻塞。最简单的方式是threading.Thread加result_queue,或者用concurrent.futures.ThreadPoolExecutor。我实际写的是一个run_with_timeout工具,代码不长,但能在技能挂死时让Agent继续响应。
5.3 让它跑起来:一个简单的调用示例
把所有东西串起来以后,使用流程大致是这样:
manager = SkillManager() manager.load_all() # 假设模型经过粗筛和精排后决定调用 read_file result = manager.execute("read_file", {"path": "/tmp/demo.txt"}) print(result)输出:
{'ok': True, 'data': {'content': 'hello, agent'}, 'error': None}这个流程虽然朴素,但已经覆盖了技能库最核心的闭环:描述驱动模型选技能、参数校验、执行、统一返回。后续要接任何Agent框架,只需要在框架侧把“模型输出中的技能名+参数”交给manager.execute就行。
我建议先跑通这个最小版本,再慢慢加向量检索、多Agent共享和权限控制。不要一上来就把架构搞得特别复杂,否则还没体会到技能库的便利,先被各种抽象层绕晕了。
6. 常见问题与排查技巧实录
6.1 “模型就是调用不到某个技能”怎么排查
这是我在群里见到最多的问题。模型明明能看到技能列表,但就是不选某个技能。我排查的顺序一般是这样:
先看技能描述里有没有写到明确的触发场景。比如用户说“帮我看看昨晚的日志”,技能描述如果只写“读取日志文件”并且没有提到“昨天”“查看”这类词,模型就很容易漏掉。解决办法是用真实对话语料去调描述,把用户最常说的几种说法直接写进去。
再看粗筛层有没有把这个技能送进候选。如果用的是向量相似度,技能描述和用户输入语义差别太大,topK可能就不包含它。这种情况可以在粗筛时把“技能名”和“技能关键词”也加入索引,不仅仅是描述文本。
最后检查是不是模型在精排阶段被其它候选干扰了。如果两个技能描述高度相似,模型会难以抉择。我一般把相似技能拆开,要么合并,要么在when_not_to_use里显式区分。比如query_order和query_refund都涉及订单,就分别写上“查询退款信息时不要调用订单查询技能”。
6.2 技能返回结果太啰嗦,影响后续判断
另一个常见问题是技能返回了一个巨大的JSON,模型读取时占用大量上下文,还可能导致后续调用参数填错。典型场景是查数据库技能,把整表几万行都带回来。
解决办法是“大结果落盘、小结果回传”。我把查询类技能的返回控制在一个极小的摘要里,比如返回总行数、前几条样例、以及一个可以继续读取的临时文件路径。模型要更多数据时,再调用另一个read_partial_result技能去取指定行。
返回值设计原则是:只返回模型“做下一步决策”所需的信息,不要返回执行者觉得“完整”的信息。模型不是给人看的,它不需要欣赏完整数据,它只需要知道“查询成功、有12条结果、前3条是xxx”。
6.3 技能稳定性:超时、并发与重试
技能库跑久了,稳定性问题会越来越多。我遇到过几种情况:
技能内部请求外部API超时,Agent被卡住几十秒。我后来给所有技能执行都套了超时,默认10秒,超过就返回TIMEOUT。对重任务,我会拆成“提交任务”和“查询任务状态”两个技能,避免长任务长时间占住Agent。
并发调用时,某些技能不是线程安全的。比如写入同一个配置文件,多个Agent同时调用就可能互相覆盖。解决办法是给技能加一把锁,同一时间只允许一个执行;或者把状态类技能改成“写临时文件+原子重命名”的方式,最终一致地落盘。
重试也要谨慎。如果技能不是幂等的,比如发邮件、创建订单,重试会造成重复操作。我只对明显幂等的技能开放自动重试,其他技能一律让用户确认后重试。
6.4 技能列表太长导致模型卡顿,怎么办
技能库最终会越攒越多,我目前维护的库已经超过80个技能。如果每次调用都把80个技能的完整描述发给模型,prompt直接膨胀,而且模型决策质量会下降。
我采用的分层策略是:
第一层,粗筛。先把技能描述做摘要索引,只保留约50字的关键描述,用轻量关键词匹配或向量匹配筛出top10。
第二层,精排。把top10的完整描述和schema发给模型。
第三层,动态加载。真正确定调用某个技能时,才把该技能的skill.py代码加载进进程。大部分技能不需要常驻内存。
这一套下来,我实测技能搜索和调用的整体延迟被压缩到1秒以内,模型决策也更准。如果你的技能数量超过200,我建议再按领域分桶,比如support/、data/、ops/,Agent默认只看到自己所在桶的技能,需要跨桶再显式检索。
7. 一些经验总结
7.1 技能不要一开始就设计得过于抽象
新手做技能库时很容易想造一个“万能技能”,比如execute_code或run_script,觉得这样什么都能干。我一开始也这么想过,结果是模型经常把不该执行的代码也往这里塞,既危险又不可控。
后来我改成把能力拆得非常具体:read_file就只读文件,write_file就只写文件,run_shell明确限制在白名单命令。虽然看起来笨,但每个技能的职责清晰,模型的选择成功率一下子就上去了。技能库的核心不是“少写代码”,而是“让大模型有可靠的手脚”。抽象层级太高,相当于给模型发了一把万能工具,它反而不知道怎么用。
7.2 技能描述要写给人看,也要写给模型看
写描述别只站在开发者角度,要站在模型的角度。我每次新增技能,都会先不看代码,只把description和几个示例扔给模型,问它“如果用户说这句话,你会调用这个技能吗?”。如果模型说“不确定”,说明描述还要改。
另外,我强烈建议在描述里放一到两个“用户说原话”到“参数填值”的映射示例。大模型在 few-shot 场景下填参准确率会明显提升。比如calculate_date_diff的描述里写:
示例:用户说“从今天到下周五还有几天”,参数填: {"start_date": "今天", "end_date": "下周五"}注意,示例里的参数值可以保留自然语言,让兼容层去解析,模型不需要自己把“下周五”转换成年月日。这样能降低模型的计算负担。
7.3 保留一条手工执行通道,方便回滚
最后分享一个非常重要的习惯:技能库绝对不能只有“模型自动调用”这一条路。我在实际运维中经常遇到模型选错了技能,导致数据被误改的情况。这时候如果只能跟着模型走,排查会非常痛苦。
我在技能库里加了一个命令行工具,可以直接手动执行某个技能并传参:
python cli.py execute read_file --param '{"path": "/tmp/demo.txt"}'调试时还可以用--dry-run只打印参数和技能信息,不真正执行。有了这条手工通道,定位问题就快很多:先手动跑一次技能确认执行函数没问题,再去查模型为什么没选对。很多问题从一开始就能被隔离掉。
如果你也在做Agent相关的项目,我特别建议从我有一个最小技能库开始,哪怕只有两三个技能,把“描述、参数、返回、路由”这个闭环跑通,再去考虑更多能力。你会发现,大模型真正从“会聊天”变成“能干活”,关键不在于模型本身,而在于你给了它一套多可靠的“手脚”。