“agent-skills”这个词,最近一阵在Agent工程圈子里出现的频率越来越高了。我第一次看到它的第一反应是:这不就是把提示词拆出来挂在某个目录下,让大模型当插件调用吗?真动手做过一轮之后才发现,根本不是这么回事。技能模块化这件事,表面上是文件组织问题,骨子里是Agent架构的职责边界问题、调用链生命周期问题,以及上下文预算分配的工程取舍。
这篇文章我不想讲那些大而全的Agent框架,只想从我在一个本地知识库问答项目里落地agent-skills的真实过程讲起:怎么设计一份技能协议,怎么把复杂任务包装成可执行的“子智能体”,怎么做技能注册和参数校验,以及最后怎么把整个调度压到本地模型可接受的时延范围内。如果你是团队里负责Agent编排、工具调用层或模型落地的工程师,又或者你正在被“工具太多了、提示词乱成一锅粥、上下文动不动就爆”这些问题折磨,那这篇文章应该对你有用。
1. 先想清楚一个前提:技能为什么不能全塞进提示词里
动手设计agent-skills之前,我先做的不是写代码,而是把“为什么非要这层抽象”这个问题想透。只有想明白了瓶颈在哪,后面做出来的东西才不会变成另一堆花架子配置。
1.1 纯文本提示词方案的三个瓶颈
我最早的第一版Agent,指令全部写在系统提示词里,工具描述也全塞在里面,代码大概长这样:
SYSTEM_PROMPT = """ 你是智能助手。你可以执行以下操作: 1. 搜索本地文件,工具名: search_files,参数: query(str), path(str) 2. 读取文件内容,工具名: read_file,参数: path(str), line_start(int), line_end(int) 3. 执行Python代码,工具名: run_python,参数: code(str) 4. 搜索网页,工具名: web_search,参数: q(str) ... """刚开始只有五六个工具的时候,这套方案没问题。但当工具数量上到二十几个,每个工具描述还带example的时候,三个问题会同时爆发。
第一个是上下文浪费。系统提示词本身就要占token,工具越多,提示词越长,留给真正对话和检索内容的预算就越少。我实际量过一组数据:工具描述加示例大概占了每轮请求context的18%到23%,而我最终真正用到的工具通常只有两三个。花了大价钱把几千个token塞给模型,它一次只看其中一小部分。
第二个是冲突和漂移。工具的调用参数如果设计得不够统一,模型在长指令里经常搞混参数名,比如把search_files的query传到read_file里去。这类错误不是模型笨,而是提示词里的信息密度太高,模型在做工具选择时等同在考场上做一道超长阅读理解。
第三个是维护性问题。任何一个工具描述改了,整个系统提示词都要跟着改,涉及到多个Agent时还要同步修改多处副本。版本回滚更是难受,经常出现“明明改的是A机器人,结果B机器人的响应风格也变了”这种诡异情况。后来我才意识到,问题出在共享了同一份System Prompt。
1.2 技能模块化真正要解决的任务边界
把技能拆成独立模块,真正要回答的问题不是“把字符串放到哪里”,而是:模型在什么情况下应该加载哪段指令、那段指令里的工具参数边界是什么、执行逻辑由谁来实现。
所以agent-skills在我的项目里被定义为一组“自带说明书、自带执行器、自带参数校验规则”的功能单元,每个技能都遵循同一个协议。大模型的核心提示词只保留基础角色设定和对话规则,所有具体能力以技能方式挂载,用的时候才加载,用完就从上下文里卸载。这样设计后,提示词长度从原来的几千token降到几百token,而且各个技能可以独立开发、独立测试、独立更新,互不污染。
还有一个很容易被忽略的好处:技能模块化之后,我可以在不同项目间直接复制技能目录。比如文件搜索技能写好了,在知识库项目里能用,换到代码审查项目里也能用,只需要改一下允许搜索的根路径。这种复用价值才是技能体系真正的意义所在,而不是单纯给提示词减减肥。
2. 设计技能执行接口:少一些“万能函数”,多一些窄接口
把“技能”从概念变成代码,第一步是定协议。我见过很多团队这一步就翻车了,最常见的做法是把每个技能实现成一个“万能函数”,接收一个巨大的kwargs字典,然后在函数内部用if-else去判断要做什么。这种设计一开始写起来痛快,后面让你哭的地方可太多了。
2.1 一份可落地的技能协议:get_instructions与execute
我最终采用的协议非常简单,每个技能是一个目录,目录下至少有3个文件:
skills/ ├── __init__.py ├── calculator/ │ ├── instructions.md │ ├── executor.py │ └── schema.py └── file_search/ ├── instructions.md ├── executor.py └── schema.py一个技能的核心是三个函数:get_instructions()、get_schema()、execute(arguments, context)。
# skills/file_search/executor.py from typing import Any, Dict SKILL_NAME = "file_search" def get_instructions() -> str: """返回给大模型看的技能使用说明,这块内容只在需要调用本技能时才注入上下文""" return """当用户需要在本机查找文件时使用。 1. 调用参数:query为文件名关键词,path为搜索根目录。 2. 如果用户没有指定目录,默认path为当前项目根目录。 3. 返回结果包含文件路径和修改时间,按修改时间倒序排列。""" def get_schema() -> Dict[str, Any]: """返回技能参数JSON Schema,用于大模型生成结构化参数和运行时校验""" return { "type": "object", "properties": { "query": {"type": "string", "description": "文件名关键词"}, "path": {"type": "string", "description": "搜索根目录,默认为项目目录"}, "max_results": {"type": "integer", "description": "最多返回几条结果", "default": 10} }, "required": ["query"] } def execute(arguments: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: import os, time, glob root = arguments.get("path") or context.get("workspace_root", ".") pattern = f"**/*{arguments['query']}*" results = [] for p in glob.glob(os.path.join(root, pattern), recursive=True): if os.path.isfile(p): results.append({"path": p, "mtime": os.path.getmtime(p)}) results.sort(key=lambda x: -x["mtime"]) results = results[:arguments.get("max_results", 10)] return {"results": results}这里有个关键点:get_instructions返回的指令只在需要调用该技能时才被放进提示词。我管这个叫“指令按需注入”,也是后面能压住上下文长度的核心原因。刚开始做agent-skills的人最容易忽略的是context参数,执行器往往只依赖调用参数,导致很多任务必须把所有信息都塞进参数里传给模型,又变回上下文爆炸的老路。我的做法是把工作区路径、用户身份、会话历史摘要之类的基础上下文统一放到context里,参数里只放任务本身的名词性描述,大模型生成的参数就干净很多。
2.2 “窄接口”为什么比“万能接口”更适合大模型调用
窄接口的意思是:一个技能只做一件清晰的事,参数尽量少,语义尽量明确,让模型一眼就知道该传什么。例如文件搜索这个技能就到路径拼接和glob匹配为止,不做文件内容解析,不返回摘要,更不做提炼关键词的联动。后面这些功能交给别的技能处理,如果大模型判断确实需要,它会依次调用多个技能来完成一个复杂任务。
如果你把文件搜索做成“搜索+摘要+情感分析”的万能接口,参数数量会迅速膨胀到十个以上,模型漏参、错参的概率会成倍增加。这不是模型能力不行,而是任务边界模糊导致参数空间过于复杂。窄接口本质上是在帮模型缩小决策空间,降低每一步工具选择的难度。这一条我在后面多轮调用数据里也得到了验证:窄接口技能的参数解析成功率,比宽接口技能高不少。
实际操作中,我给自己定了一条规矩:一个技能描述里只允许出现一个核心动词和最多三个核心名词参数。动词超过一个,就说明要拆技能;参数超过三个,就要想想哪些能放进context、哪些能通过默认值解决。
3. 用子智能体包装复杂技能:当工具节点本身也需要推理时
技能协议能解决“简单工具的标准化”,但真实项目里还有一类更麻烦的技能:表面上看是一个动作,实际上需要内部走完一整套决策流程。比如“对一份新文档生成摘要并归档到指定知识分类”,单靠一个函数很难优雅完成,因为你要模型自己判断分类,又要模型写摘要,摘要风格还可能随文档类型不同而不同。
3.1 什么情况下必须把技能升级成子智能体
我判断该不该把技能升级成子智能体的标准很简单:看这个技能在执行过程中需不需要“自我决策”。如果技能内部只是循环处理固定规则,用函数就够了;如果内部要根据输入内容动态决定下一步动作,比如读文件、判断类型、换不同的摘要模板,那就该换子智能体方案。
子智能体的本质,是把一个复杂技能包装成带独立上下文的Agent节点。它有自己的系统提示词、自己的工具集和自己的对话循环,对外只暴露统一的execute(arguments, context)接口。主智能体只需要知道“我有一个技能可以完成文档归档”,至于技能内部读了几次文件、调用了几轮模型,主智能体完全不用关心。这种分工方式和团队里的“专家外派”很像:需要专业判断时把整件事外包出去,拿到结果就好。
在agent-skills架构里引入子智能体还有一个额外的收益:技能内部的推理过程不会污染主智能体的上下文。文档分类和摘要生成的推理走的是子智能体自己的上下文窗口,结束后只把最终结果以小段文本返回给主智能体。从主智能体的角度看,这次技能调用只是一个智能函数,输入是文档路径,输出是归档位置和摘要。
3.2 一个子智能体执行器的实现示例
我基于上面的思路实现了一个轻量子智能体执行器,没有引入重型框架,核心就是拿到参数后自行构建临时的system prompt和消息历史,走同样的模型接口完成多轮推理。
# skills/doc_archiver/executor.py import asyncio from typing import Any, Dict SKILL_NAME = "doc_archiver" def get_instructions() -> str: return """当用户需要将文档归类并生成摘要时使用。 1. 调用参数必须包含target_path,指向完整文件路径。 2. 归档目录固定为archive_root下的{类别}/{年}/{月}。 3. 类别只能从: project_docs, meeting_notes, research, misc 中选择。 4. 摘要输出不超过200字,必须包含核心结论。""" def get_schema() -> Dict[str, Any]: return { "type": "object", "properties": { "target_path": {"type": "string", "description": "需要归档的文档完整路径"}, "archive_root": {"type": "string", "description": "归档根目录,默认取context中的archive_root"} }, "required": ["target_path"] } async def execute(arguments: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: import os, shutil, datetime from pathlib import Path from model_api import chat_completion # 统一模型接口 file_path = Path(arguments["target_path"]) archive_root = Path(arguments.get("archive_root") or context["archive_root"]) # 第一轮:读取文件,让子智能体判断分类 content = file_path.read_text(encoding="utf-8")[:3000] sub_messages = [ {"role": "system", "content": "你是文档分类助手,只输出一个类别词。"}, {"role": "user", "content": f"文档内容:\n{content}\n\n请从project_docs/meeting_notes/research/misc中选一个分类"} ] category_resp = await chat_completion(messages=sub_messages, temperature=0.2) category = category_resp.strip() today = datetime.date.today() dest_dir = archive_root / category / str(today.year) / f"{today.month:02d}" dest_dir.mkdir(parents=True, exist_ok=True) dest_file = dest_dir / file_path.name shutil.move(str(file_path), str(dest_file)) # 第二轮:让子智能体生成摘要 sub_messages.append({"role": "assistant", "content": category_resp}) sub_messages.append({"role": "user", "content": "请生成不超过200字的摘要,必须包含核心结论。"}) summary_resp = await chat_completion(messages=sub_messages, temperature=0.4) return {"category": category, "dest_path": str(dest_file), "summary": summary_resp.strip()}注意这里有个细节:子智能体执行器里的chat调用,我复用了和主智能体相同的模型接口,但temperature设得不一样。分类任务用低温度追求确定性,摘要生成用略高一点的温度保持表达自然。同一套技能接口可以给我足够的自由度去给内部决策配置不同参数,这才是子智能体方案比普通函数优雅的地方。
子智能体方案唯一让我犹豫过的是成本和时延。多轮调用必然比单次调用慢,但在归档这种对时延不敏感的场景里,多花两三秒换回的结果质量是值的。如果你在做一个必须实时响应的技能,就不要执迷于子智能体,老老实实把内部决策固化成规则,用普通函数实现。
4. 技能注册与参数校验:把“能用”变成“稳定用”
技能协议和子智能体方案定下来后,项目迎来一个幸福的烦恼:技能目录越来越多,文件搜索、文档归档、代码格式化、数据库查询、邮件草稿生成……几十个技能堆在同一个目录里,调度器该怎么知道自己有哪些技能可用?大模型该从哪里知道技能的最新参数定义?一个团队多人协作时,怎么避免技能写法和参数风格五花八门?
4.1 技能注册表结构与加载时机
我的解决办法是给所有技能加一个统一的注册机制。每个技能目录里增加一个skill.yaml,里面标注技能名称、版本、作者、描述、入口模块和默认超时时间。调度器启动时扫描所有技能目录,把skill.yaml解析成一张注册表。
# skills/file_search/skill.yaml name: file_search version: 1.2.0 description: 按文件名关键词搜索指定目录,返回路径和修改时间 entry: executor.py timeout: 15 tags: [local, search] allowed_roots: ["/workspace", "/home/user/docs"]加载时机也很讲究。如果所有技能都提前加载到内存里,几十个技能的instructions加起来照样是几万token,前面省的上下文又回来了。所以我在注册表里只保留技能的“元信息”,包括名称、一句话描述和必要的tag。大模型在做工具选择时,看到的是一份精简的技能列表,只包含技能名和一句话说明。真正要把某个技能的instructions注入上下文,是在模型表现出意图之后。
这一步的感受是,agent-skills设计里最容易被忽略的效率点其实不在执行阶段,而在“发现阶段”。如果一个技能描述写得过长,模型在扫描整个技能列表时就会花更多的注意力,也更容易被不相关信息干扰。注册表里一句话描述必须说清楚“这个技能什么时候用”,其他细节全部放到instructions里延迟注入。
4.2 参数校验、依赖声明与安全边界
技能参数严格说需要两道校验。第一道是模型生成参数阶段的schema约束,我让大模型按JSON Schema格式输出参数,生成后再做一次程序化校验,防止模型幻觉出不存在的关键字。第二道是执行前的运行时校验,包括路径是否在允许范围内、文件是否存在、代码是否有执行权限等。
def validate_arguments(schema: Dict[str, Any], args: Dict[str, Any]) -> List[str]: errors = [] required = schema.get("required", []) for field in required: if field not in args or args[field] in (None, ""): errors.append(f"缺少必填参数: {field}") for key, meta in schema.get("properties", {}).items(): if key in args: expected = meta.get("type") if expected == "string" and not isinstance(args[key], str): errors.append(f"参数 {key} 应为字符串") if expected == "integer" and not isinstance(args[key], int): errors.append(f"参数 {key} 应为整数") return errors这个函数看起来不复杂,但它是把技能从“在特定模型上碰巧能用”变成“在模型迭代后依然稳定”的关键。我遇到过模型升级后某次调用把max_results传成了字符串,如果没有运行时校验,这个错误会一路透传到glob的slice逻辑里,返回结果异常,排查半天还以为是技能代码本身的问题。现在所有技能入口统一走校验函数,任何参数问题在入口直接卡住并返回给模型修正。
安全边界也要在注册阶段声明,而不是在执行时碰运气。比如file_search的allowed_roots字段,限制了搜索路径只能落在工作区目录内;exec_code类技能必须声明allow_network: false。调度器在执行前会检查传入参数是否越界,越界就直接拒绝,不给模型一个“绕过边界”的机会。
依赖管理方面,我给每个技能准备了一个requirements.txt,安装器会在激活技能时检查并安装缺失依赖。这里有个值得分享的坑:不要用“全局安装所有技能依赖”的方式,因为不同技能的依赖可能冲突。我实际遇到过A技能要求numpy版本小于2.0,B技能要求大于等于2.0,放到同一个环境里互相打架。后来改成每个技能一个轻量虚拟环境,代价是第一次调用会慢一点,但环境隔离带来的稳定性远大于这点性能损失。
5. 把时延压下去:流式任务队列与按需加载
agent-skills这块拼图到了这里,功能上已经完整了,但我真正在项目里花掉最多时间的地方,是性能和稳定性调优。本地部署的Agent模型跟云端商业模型不一样,显存有限、推理速度慢、并发能力弱,如果技能调度器不做控制,分分钟把服务打满。
5.1 本地模型的并发瓶颈与worker调度
本地模型服务最常见的问题是并发把显存挤爆。多个技能被先后触发时,如果调度器不管不顾地同时发起推理,模型服务端会进来大量并发请求,导致显存溢出或者推理互相排队超时。我的方案是给技能执行套一个流式任务队列,每个模型推理请求按顺序进入worker池,而worker池的数量根据显卡显存动态调整。
import asyncio from collections import deque class SkillExecutor: def __init__(self, max_concurrency=2): self.max_concurrency = max_concurrency self._queue = deque() self._workers = [] async def submit(self, skill_name, arguments, context): future = asyncio.get_event_loop().create_future() self._queue.append((skill_name, arguments, context, future)) return await future async def run(self): for _ in range(self.max_concurrency): worker = asyncio.create_task(self._worker_loop()) self._workers.append(worker) async def _worker_loop(self): while True: if not self._queue: await asyncio.sleep(0.05) continue skill_name, arguments, context, future = self._queue.popleft() try: result = await self._dispatch(skill_name, arguments, context) future.set_result(result) except Exception as e: future.set_exception(e)这个队列的价值,不只是限制并发。它还是一个天然的背压机制:当任务太多时,新的请求会排队而不是把系统打崩。我把最大并发数设成2,实测在单卡部署的模型上,推理吞吐反而比并发5的时候更高,因为显存不撞了,每个请求的batch也更稳定。如果你在云端用商业模型API,这个队列可以考虑不放那么紧,但要加一个基于时间的超时重试,很多模型API偶尔会慢,直接在超时后重试比一直等更现实。
调度器还有一个优先级策略,主智能体的第一轮响应永远是最高的优先级,技能子任务其次,后台预加载类任务最低。这样可以保证用户的第一字响应时间尽量短,技能调用产生的等待不阻塞主对话。
5.2 用eval scope按需加载压缩上下文
性能优化里另一个大头是上下文压缩。技能注册表虽然避免了把所有技能描述都塞进提示词,但多技能联动时,主上下文里还是可能同时出现好几个技能的instructions。一个文档归档任务运行完两轮之后,主上下文里残留了大量技能指令和子智能体返回的中间结论,再接新问题时成本非常高。
我的办法是引入一个eval_scope的概念。每个技能调用有自己的临时上下文栈,技能运行结束后,会把“对后续对话仍然有用的结论”显式写到主上下文摘要里,而把完整的执行过程细节留在日志中,不再污染主上下文。过去的完整对话历史会交给一个压缩模块,定期把不重要的工具调用轨迹折叠成一行摘要。这个做法让一个长会话在连续使用二十多项技能后,上下文占用仍然能控制在一个比较健康的水平。
举一个具体数字感受一下:优化前,一个“查找文件→读取内容→生成摘要→归档”的四步流程,累计消耗的context token大约在16000到22000之间。优化后,主上下文只保留归档结论和摘要,中间的查找路径、文件内容、分类推理全部被折叠,一轮完整的同类任务消耗只有7000到9000 token,节省超过一半。这个数字在云端API项目里可能只是省点钱,但在本地模型固定显存的场景里,可能就是“能跑”和“跑不动”的区别。
6. 可观测性:技能轨迹、性能画像与回放调试
技能多了以后,你迟早会遇到一个场景:用户在对话里说“帮我找一下上个月开会提到的预算表”,系统先调用了文件搜索,又调用了文档解析,最后调用了归档技能,结果用户反馈说“这不是我要的东西”。这时候如果没有记录技能调用轨迹,你几乎没法定位是哪一步出了问题。
6.1 技能调用轨迹的关键字段与回放
我给技能调度器加了一个极简的轨迹日志,每次技能调用都会记录下面几个字段:技能名称、版本、输入参数、输出摘要、调用耗时、模型token用量、校验是否通过、错误信息。日志会写进结构化文件中,使用时按会话ID聚合成一条调用链,回放时能看到整个任务流。
def log_skill_call(record: dict): import json, time with open("logs/skill_trace.jsonl", "a", encoding="utf-8") as f: record["ts"] = time.time() f.write(json.dumps(record, ensure_ascii=False) + "\n")这条日志文件看似简单,但排查问题的效率提升是立竿见影的。有一次,用户反馈文档摘要风格突然变了。回放轨迹后发现是某个技能版本号从1.1升到了1.2,而1.2版本里把摘要的要求从“不超过200字”改成了“不超过300字”,但get_instructions文档没有同步更新,导致模型描述和执行逻辑产生偏差。如果没有轨迹日志里记录版本号,这个问题几乎不可能查到。
还有一点是“回放”不仅仅是看日志,我还在本地实现了一个简单的replay工具,可以把某次会话的技能调用参数重新执行一遍。无论模型服务升级还是技能代码改动,回放工具都能快速验证“同一个输入是否仍然得到同样的输出”,这让技能迭代时回归测试的成本大大降低。
6.2 技能时延与token消耗的量化分析
性能优化的前提是量化和可衡量。我把技能调用数据按周聚合,生成一幅技能调用画像,内容包括调用频次、平均时延、P95时延、token消耗占比和失败率。
某次统计后我发现,调用频次最高的前三名是文件搜索、代码格式化、数据库查询,但token消耗最大的却是文档摘要、子智能体归档这类复杂技能。也就是说,高频技能并不等于高消耗技能,优化时要分开处理。高频技能要优化注册表发现效率,让模型更快选中;高消耗技能要优化内部推理轮数和指令长度,减少子智能体内部的无效对话。
另一个量化指标是“技能选择的精确率”。我统计了每次模型选定技能后,该技能是否在后续执行中被证明是正确选择,把误解模型意图、调错技能的情况记录下来。这个指标比单看模型准确率更有工程价值,因为工具选择的错误往往可以通过更好的技能描述修正,而不用动模型本身。
7. 落地过程中的踩坑实录:六个高频问题与排查方法
前面讲的都是设计顺畅时的理想情况。真实项目里,agent-skills从初版到稳定,我踩了不少坑,有些坑到现在想起来还觉得疼。整理成一份速查表,给后续做类似架构的同学一些参考。
7.1 症状、原因与解决对照表
| 症状 | 可能原因 | 排查方法 | 解决思路 |
|---|---|---|---|
| 模型总是选错技能 | 技能描述太泛,多个技能边界重叠 | 查看技能选择日志,确认模型是被哪个描述误导 | 重写描述,突出“什么时候用”而不是“能做什么” |
| 参数解析频繁失败 | schema太复杂,必填字段过多 | 直接打印模型生成的原始JSON,看错在哪 | 减少必填字段,能填默认值的都填默认值 |
| 技能内部多轮推理后上下文溢出 | 子智能体没有自己的摘要压缩 | 观察sub_messages长度变化 | 在子智能体内部加入旧的摘要折叠逻辑,每轮处理固定块 |
| 技能调用后主对话风格突变 | 技能返回结果包含了大量指令性文本 | 检查execute返回内容,确认没有混入prompt文本 | 对技能返回结果做后置清洗,只保留结构化字段 |
| 依赖冲突导致技能时好时坏 | 所有技能共用同一个Python环境 | 查看安装时间和技能激活顺序 | 改成技能级虚拟环境或依赖隔离机制 |
| 重试时重复执行副作用操作 | 执行器缺少幂等控制 | 查看日志,确认重试是否来自超时 | 给技能调用生成唯一request_id,执行前检查是否已执行过 |
我印象最深的是最后一个幂等性问题。一个“发送邮件”类技能如果执行超时但邮件实际已经发出,重试就会造成重复发送。给每个技能调用加唯一request_id,并在技能内部按id记录执行状态,是治本的方案。
7.2 让技能体系活下来的几条心得
如果只看文档,每个技能都写得规规矩矩,但它能不能在真实场景里活下来,往往取决于很多“软性”因素。
第一,技能描述要请真实用户来写,至少也要从真实问题数据里提炼。我早期技能描述都是自己拍脑袋写的,后来把用户实际对话记录翻出来,才发现用户问“预算表”的时候就是要文件搜索,问“上个月的账”的时候其实是要数据库查询。贴近真实表达的技能描述,模型选择准确率会显著提升。
第二,技能要不要合、要不要拆,永远以调用数据为准,别靠感觉。我有个技能“搜索并总结文档内容”的调用失败率特别高,一查发现是尝试把异步搜索和摘要生成打包进了一步,丢了中间状态。把它拆成一个搜索技能加一个摘要技能后,同一个任务的失败率立刻下来。拆技能有时候确实会多一轮模型调度,但换来的是每步的确定性,对于工具调用来说这是值的。
第三,设计agent-skills时不要追求一次性搞一套“完美协议”,更实际的路径是从两三个最常用技能开始,把协议跑顺了,再逐步扩展。协议稳定后尽量别频繁改动,因为我统计过每次协议变化后模型都需要一段时间适应,期间工具选择准确率会有一个明显回落。如果必须改,尽量保持函数签名和schema字段的向后兼容。
最后一点,当整个技能系统运行稳定后,我会有意识地去审查每一条技能是否真的有被调用。有些技能写出来之后半年都没被模型选中哪怕一次,这不一定是技能没用,更可能是描述和真实需求脱节。要么重写描述,要么果断下线,别让它留在注册表里继续消耗模型每次扫描时的注意力。
我在这个项目里的一个深刻体会是:Agent能不能理解用户,很大程度上取决于工程上给模型铺了一条多顺的路。agent-skills做得好的时候,模型不是变聪明了,而是它每一步的判断都更容易做对了。