☰
AI Agent技能系统实战:从Prompt解耦到调度器与工程落地
2026/10/7 6:09:18 网站建设 项目流程

1. 项目概述:Agent技能系统到底解决了什么问题

最近有不少朋友在讨论agent-skills这个方向,其实它说的就是给 AI Agent(智能体)定义一套可复用的"能力单元"。说白了,就是把你希望 Agent 能做的事——比如查天气、写周报、调API、分析数据——整理成一个个结构化的"技能包",然后让 Agent 在合适的场景下自动调用。这不是一个具体的开源库名字,而是当前构建 Agent 应用时绕不开的一套设计方法论。

先说几个常见的痛点,你应该也遇到过。第一,Agent 对话一长就容易"跑偏",明明让它做 A 任务,聊着聊着就飘到 B 方向去了。第二,每次想给 Agent 加一个新功能,就得改一大段提示词(Prompt),改来改去还可能把原有能力弄崩。第三,团队里好几个人都在写 Agent,但每个人写的"技能逻辑"完全不统一,代码复用基本靠复制粘贴。

agent-skills的思路就是把这些能力从 Prompt 里剥离出来,做成标准化、模块化的"技能单元"。每个技能单元就像手机上的 App,有明确的入口、输入输出、执行逻辑和错误处理。Agent 本身只负责理解用户意图、决定调用哪个技能,具体怎么执行就交给技能模块去完成。

这样做至少带来三个直接收益:

  • 稳定性提升:Prompt 里不再塞一大堆"如果用户问天气就调用天气接口,如果问新闻就..."这种混合指令,Agent 的意图识别负担大大减轻,跑偏概率显著下降。
  • 扩展性变好:新增一个技能,只需写一个独立模块,注册进去就行,完全不需要动已有逻辑。我见过一个团队,加了十来个技能之后,Agent 的响应速度反而快了,因为每次判断的路径更清晰了。
  • 可测试性增强:每个技能可以单独测试、单独出问题单独修,不用再对着整个 Agent 来回试。

这篇文章我会从技能系统的设计思路、核心格式定义、具体实现流程、常见坑点这几个维度展开,全程结合实际代码示例和我在项目中踩过的坑。不管你是刚接触 Agent 开发的新手,还是已经写了几个 Demo、正愁着怎么把系统做规范的老手,这篇都能给你一些可以直接用的参考。

2. 技能系统的整体设计思路:先想清楚再动手

2.1 为什么要把能力拆成"技能"而不是继续堆 Prompt

早期大家做 Agent 应用,都喜欢把功能写进 System Prompt 里。比如把天气查询的接口说明、参数格式、返回样例一股脑塞给模型,让模型"自己看着办"。在小实验阶段这招确实好使,但一旦功能多起来就乱套了。

我做过的项目里,有个真实例子。一开始只做了 3 个功能,Prompt 控制在 1200 字左右,模型表现挺稳。后来加到 8 个功能,Prompt 涨到 4000 字,模型就开始"精神分裂"了——有时用户问天气,它反而调用新闻接口,有时用户要写周报,它却去查日历。问题根源在于:Agent 的每一步决策都要在超长的上下文中找"现在该干什么"的线索,信息越杂,决策准确率就越差。

agent-skills的思路恰好反过来。它把"做什么"和"怎么做"分开:

  • "做什么"——由 Agent 的核心 Prompt 负责。这部分只保留角色设定、最基本的对话风格,以及"遇到什么场景该调用哪个技能"的映射关系。
  • "怎么做"——由技能模块负责。每个技能封装了自己的入参校验、接口调用、结果解析、异常处理,Agent 只需要传入必要参数,等技能返回结果就行。

用生活里的话说:你雇了个助理,你只需要告诉助理"帮我订饭店",至于助理是打电话还是用 App、是选川菜还是粤菜、怎么跟店家沟通,那是助理的事,你不需要全程盯着。技能系统就是把这个助理从"事事问你怎么做"训练成"你说意图,我来执行"。

2.2 技能系统需要具备哪些核心组件

在动手写代码之前,建议先把整个系统的组件结构画出来。根据我实际项目的经验,一个基本的技能系统至少需要以下几块:

组件作用类比
技能注册表记录有哪些技能可用,以及每个技能的名称、描述、版本手机的 App 列表
技能定义文件描述技能的功能、参数格式、调用方式、返回结构App 的功能说明书
调度器Agent 根据用户意图,匹配并调用正确的技能手机桌面上的图标,点击就能启动 App
技能执行器实际运行技能的代码,执行具体逻辑App 底层的运行引擎
上下文管理器在技能与技能之间传递共享状态,比如用户信息、会话历史手机的系统级数据共享

这五个组件里,最容易被人忽略的是"上下文管理器"。很多人第一次实现技能系统,都是每个技能独立干活、互不通信,结果发现:用户先让 Agent 查了今天的日程,接着让 Agent"把日程发给项目群",第二个技能却获取不到第一个技能的查询结果。这就是典型的上下文断裂。

正确的做法是设计一个上下文对象,在技能运行时把关键信息写进去。比如用户授权过的身份信息、当前会话的全局参数、上一个技能的输出摘要等。这样技能之间可以形成简单的数据链条,Agent 也才能在处理复杂任务时保持连贯性。

2.3 设计时就要避开的三个大坑

第一,不要把技能定义得太大。比如"帮助用户完成一切办公事务"这种技能定义,听上去很全能,实际上调度器根本不知道该什么时候调用它,因为它的边界太模糊了。技能定义应该遵循"单一职责"原则:一个技能只做一件明确的事,比如"查询指定日期的待办事项"。

第二,技能之间尽量避免隐式依赖。比如"发送周报"这个技能内部默默依赖"读取日历"技能,一旦日历技能挂了,周报技能也会莫名报错。依赖关系应该在文档里写得清清楚楚,或者在调度层面显式处理,不能让技能 A 直接在代码里调用技能 B。

第三,不要把所有逻辑都塞进自然语言描述里。技能定义文件里的描述文字,是为了让 Agent 理解"什么时候该用这个技能",不是为了教 Agent 怎么执行。执行逻辑应该写在代码里,描述写得再详细,Agent 也不可能靠看文字就学会调用 API。这个边界一定要把握好,否则技能定义文件会膨胀成又臭又长没人愿意维护的文档。

3. 技能定义与实现的核心细节

3.1 一份结构合理的技能定义文件长什么样

技能定义是整个系统的"语法核心",一份标准的技能定义文件,最好采用独立的 JSON 或 YAML 结构。我推荐 JSON,因为它的兼容性最好,几乎所有主流编程语言都能原生解析。下面是我在项目里常用的格式模板:

{ "skill_name": "query_todo", "version": "1.0.0", "description": "查询指定日期或日期范围的待办事项。当用户要求查看日程、任务、待办时使用。", "parameters": { "type": "object", "properties": { "date_from": { "type": "string", "description": "开始日期,格式 YYYY-MM-DD", "default": "今天" }, "date_to": { "type": "string", "description": "结束日期,格式 YYYY-MM-DD", "default": "date_from" } }, "required": [] }, "output_schema": { "type": "array", "items": { "task_id": "string", "title": "string", "due_date": "string", "status": "string" } }, "execution": { "language": "python", "entry": "skills/query_todo/run.py", "timeout_seconds": 10 } }

这其中的关键点在于description字段。它决定了 Agent 在什么情况下会调用这个技能。我建议把这个字段当成"触发条件"来写,而不是"功能说明书"。同样是查待办事项,下面两种写法效果完全不同:

  • 模糊写法:"处理待办事项相关的查询。"——Agent 看完不知道是查今天还是查这周,也不知道"处理"是调 API 还是直接让用户自己说。
  • 精准写法:"当用户要求查看某个日期的待办事项、日程安排、任务清单时,使用此技能。参数中可以指定日期范围,如果不指定,默认返回今天的待办。"——Agent 一听就明白"哦,这就是用来查待办的,参数还能选日期"。

output_schema也很重要。它定义了技能返回给 Agent 的数据格式,Agent 会依照这个结构来组织最终的回复。说得夸张点,output_schema就是"技能给 Agent 的承诺"——我保证返回这样的结构,你可以放心基于它回答用户。没有这个承诺,Agent 只能猜技能返回了什么,猜错了自然答非所问。

3.2 技能执行器的实现套路:读定义、做校验、取数据、返回结果

技能定义文件解决的是"怎么描述技能",而技能执行器解决的是"怎么跑起来"。我用 Python 写过一个非常轻量的执行器,核心逻辑其实就四步:

import json import importlib.util from datetime import datetime def load_skill(skill_manifest_path): """加载技能定义文件""" with open(skill_manifest_path, "r", encoding="utf-8") as f: manifest = json.load(f) return manifest def load_execution_entry(entry_path): """动态加载技能入口函数""" spec = importlib.util.spec_from_file_location("skill_entry", entry_path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def execute_skill(manifest, params): """ 执行技能主流程: 1. 参数校验 2. 调用入口函数 3. 校验输出 4. 返回结果 """ # 参数校验:把用户传来的参数与定义文件里的 properties 做比对 # 缺失的字段用 default 补上,多余的字段直接忽略或报错 clean_params = validate_and_fill_params(manifest["parameters"], params) # 加载入口模块,调用 run 函数 module = load_execution_entry(manifest["execution"]["entry"]) result = module.run(clean_params) # 这里可以做输出结构校验,确认跟 output_schema 一致 validate_output(result, manifest.get("output_schema")) return { "skill_name": manifest["skill_name"], "result": result, "executed_at": datetime.now().isoformat() }

这段代码最值得留意的,是validate_and_fill_params这一步。很多初学 Agent 开发的朋友,喜欢把参数校验完全交给模型"自觉"。但模型对于参数的填写其实经常出问题,比如日期格式传成"2025/1/1"而不是"2025-01-01",用户说"下周二"模型也算不明白具体日期。所以技能执行器里必须有一道程序化的校验和兜底逻辑,把格式不规范、缺字段的参数尽量修复成标准格式,修复不了的再明确抛错,让 Agent 拿到错误信息后可以反问用户。

我在实际项目中踩过一个大坑:技能返回的数据结构跟预期不一致,有时多字段有时少字段,导致 Agent 输出质量忽高忽低。后来就是靠validate_output解决了问题——返回前强制做一次结构校验,不合格就直接报错,把问题暴露在调试阶段,而不是让错误数据流向用户。

3.3 上下文怎么传:别让技能变成"信息孤岛"

前文提到上下文管理器,这里是具体实现。不同技能之间,有些数据是有共性的,比如用户 ID、当前时间、偏好设置(时区、语言、日期格式等)。如果每个技能都去请求一次用户信息,既慢又容易不一致。

我建议在系统里维护一个全局上下文对象,典型结构如下:

class AgentContext: def __init__(self, user_id, user_timezone="Asia/Shanghai"): self.user_id = user_id self.user_timezone = user_timezone self.shared_storage = {} # 跨技能共享的数据,比如上一个技能的输出摘要 ctx = AgentContext(user_id="user_123", user_timezone="Asia/Shanghai") def execute_skill_with_context(skill_manifest, params, ctx): context = { "user_id": ctx.user_id, "timezone": ctx.user_timezone, "now": datetime.now().astimezone() # 统一使用当前时间,避免技能里各自调用 time() 导致时区混乱 } # 将 context 合并进参数中 final_params = {**params, "_context": context} result = execute_skill(skill_manifest, final_params) # 把结果摘要写入共享存储,供后续技能使用 ctx.shared_storage["last_skill_output"] = result return result

这样做的好处是:技能不需要自己猜测"现在几点""用户在哪个时区",直接从_context里取就行。比如查待办的小技能,如果用户没说具体日期,技能可以从context["now"]推导出今天,而不是拿服务器时间硬算——不然用户在美国,查到的"今天"可能跟他那边的日期对不上。

3.4 技能注册表:让 Agent 知道"我有什么牌可以打"

技能注册表是 Agent 的"能力列表"来源。它的实现非常简单,无非是一个数组或字典,存储所有技能的定义摘要。但真正讲究的地方在于:你给注册表里放什么,决定了 Agent 的性能。

我做过的性能对比实验(同一个 Agent 模型、同一批测试问题)如下:

注册表内技能数量Agent 正确调用技能的比例平均响应耗时(秒)
5 个技能96%0.9
15 个技能87%1.4
30 个技能72%2.3

随着注册表里技能数量增加,模型需要在更大的空间里做"匹配选择",正确率和速度都会下降。所以不是技能越丰富越好,而是要保证注册表里的技能"真正有用、真正会被调用"。

另一个经验是:同一个类别的技能,合并胜过拆分。比如"查天气"和"查空气质量",功能高度相似,完全可以合并成一个"查询气象信息"技能,通过参数data_type区分。这样注册表里少了一个条目,Agent 决策压力更小,准确率更高。

另外每次 Agent 进行技能调用决策时,如果能把当前的用户意图、会话摘要、候选技能列表一起喂给模型,决策效果会好不少。这个"候选技能列表"可以预先做一次粗过滤,而不是把所有技能一股脑全传给模型。粗过滤规则不复杂,用关键词匹配即可,比如用户提到了"待办""日程",就优先把query_todo和add_todo放前面。

4. 从零搭建一个技能系统的完整实操过程

4.1 第一步:定义技能清单,先列需求再写代码

搭建系统前,先静下心来梳理你自己的需求。我建议用一张表格来列:

用户高频需求对应技能名称数据来源预计返回内容
查待办事项query_todo本地数据库 / 日历 API待办清单列表
新增待办add_todo本地数据库新增成功的记录
查天气query_weather气象 API天气状况、温度、风力
写周报generate_report结合待办数据+模板Markdown 格式周报

我自己犯过的错误是:一开始看别人项目里有"技能"就很兴奋,一下子定了 20 个技能目标,结果真正写完能用的只有 6 个,还有 8 个是"自我感动型"功能,用户压根没那么频繁地用。所以技能清单一定要从真实需求出发,优先做高频、确定性强的功能。那些需求模糊、逻辑复杂的,先放着,等系统跑起来再加。

4.2 第二步:准备好你的开发环境和目录结构

推荐用 Python 搭技能系统,因为 AI 生态对 Python 最友好,写技能时调各种第三方库都方便。目录结构建议如下:

project/ ├── manifests/ # 技能定义 JSON │ ├── query_todo.json │ ├── add_todo.json │ └── query_weather.json ├── skills/ # 技能执行代码 │ ├── query_todo/ │ │ └── run.py │ ├── add_todo/ │ │ └── run.py │ └── query_weather/ │ └── run.py ├── core/ │ ├── registry.py # 技能注册表 │ ├── scheduler.py # 调度器:让 Agent 选择技能 │ ├── executor.py # 执行器:运行技能 │ └── context.py # 上下文管理 └── tests/ ├── test_query_todo.py └── test_scheduler.py

开发环境这块,几个关键依赖建议提前装好:

pip install openai # 调用大模型 API pip install pydantic # 参数校验 pip install pytest # 跑测试用例

我通常还会加一个python-dotenv用来管理 API Key 之类的环境变量,放在.env文件里,千万别把密钥硬编码进代码。这不是技术上的难点,但属于必须养成的习惯,一旦密钥泄露到公开仓库,麻烦是很大的。

4.3 第三步:手写一个最小可用的调度器

调度器是 Agent 决定"该调用哪个技能"的核心。我用的是最常见也最稳定的方式:让大模型根据用户请求和技能定义,输出一个 JSON 格式的调用决策。

核心逻辑如下:

import json from openai import OpenAI client = OpenAI() def decide_skill(user_input, skill_manifest_list, context): """ 让大模型决定调用哪个技能,返回技能名称和参数。 """ system_prompt = ( "你是一个技能调度器。根据用户的请求,从候选技能中选择一个," "并按照技能参数定义提取所需参数。如果无法确定该调用哪个技能," "或用户请求超出所有技能能力范围,返回 action=no_skill。\n" f"候选技能信息:\n{json.dumps(skill_manifest_list, ensure_ascii=False, indent=2)}\n" f"当前上下文:{json.dumps(context, ensure_ascii=False)}\n" "请严格按以下 JSON 格式返回,不要输出任何额外内容:\n" '{"action": "skill_name" | "no_skill", "reason": "判断理由", "params": {...}}' ) response = client.chat.completions.create( model="gpt-4o-mini", temperature=0, # 调度决策场景一定要把 temperature 设为 0,保证稳定性 messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input} ] ) # 提取并解析 JSON raw = response.choices[0].message.content try: decision = json.loads(raw.replace("```json", "").replace("```", "").strip()) except json.JSONDecodeError: return {"action": "no_skill", "params": {}} return decision

这里有几个细节我吃了不少亏,值得多说两句:

  • temperature=0很重要。调度不是创作,不需要让模型"发挥想象力"。把 temperature 调成 0,它每次都会选择最保守、最确定的技能,输出格式也更稳定。
  • 解析 JSON 时要容错。大模型偶尔会在 JSON 外面套一层 Markdown 代码块标记,或者多了几个换行。上面代码里我对内容做了replace处理,能滤掉这些干扰。实际项目中你可能还需要更健壮的解析方案,比如用正则抽取第一个{到最后一个}之间的内容。
  • reason字段别小看它。它让模型说明为什么选择这个技能,虽然不直接影响用户,但你在调试系统时能清楚看到每一步决策依据,排查问题效率会高很多。

4.4 第四步:把调度器、执行器、上下文串起来

调度器返回了"调用哪个技能",接下来就是把整个流程串起来。最简流程可以封装成下面的函数:

def run_agent(user_input): # 1. 加载所有技能定义 skills = load_all_skills() # 从 manifests/ 读取所有 JSON # 2. 调度器做技能选择 decision = decide_skill(user_input, skills, ctx.shared_storage) # 3. 如果命中了技能,执行它 if decision["action"] != "no_skill": manifest = get_manifest(skills, decision["action"]) # 执行技能,并把上下文一并传入 result = execute_skill_with_context(manifest, decision["params"], ctx) return result # 4. 如果没有命中,交给普通聊天回复 return chat_with_llm(user_input)

这个主流程看着简单,但实际运行中我建议再叠加一个"技能结果后处理"环节。技能返回的是结构化数据,不一定适合直接给用户看,比如查天气技能返回了温度、湿度、风速一堆字段,Agent 需要把它们组织成一句自然流畅的话。这一步也要交给大模型来完成,提示词大致是:

你是一个回答助手。用户提出了请求,技能系统返回了如下结果。 请基于这些结果,用自然语言回复用户。结果如下: <result>...</result>

这样一来,Agent 的回复质量不再依赖技能返回格式的"可读性",而是交给语言模型去润色和组织,效果会好一大截。

4.5 第五步:写测试用例,让系统敢改

没有测试的技能系统,越到后面越不敢动。每次加一个技能,都可能影响已有技能的调度效果,这是必然的。所以我强烈建议在搭建初期就建立测试基线。

最基本的测试至少覆盖三类:

  1. 调度正确性测试:给出一批用户输入,断言系统调用的是预期技能。
  2. 参数提取测试:用户说"查一下周五的待办",断言调度器返回的日期参数是"2025-01-10"(当然这个要看当天日期来算)。
  3. 技能执行测试:直接调用技能入口,喂入标准参数,断言返回的数据结构符合output_schema。

下面是一个简单的pytest示例:

import pytest from core.scheduler import decide_skill from core.executor import execute_skill class TestQueryTodoSkill: def test_query_todo_today_without_params(self): user_input = "我今天有什么待办?" skills = load_all_skills() decision = decide_skill(user_input, skills, {}) assert decision["action"] == "query_todo" # 断言参数不算严谨,但至少能防住"调度到别的技能"这种问题 def test_query_todo_execution(self): manifest = get_manifest(skills, "query_todo") result = execute_skill(manifest, {"date_from": "2025-01-10"}) assert "result" in result assert isinstance(result["result"], list) def test_no_skill_for_unrelated_question(self): user_input = "讲个冷笑话" decision = decide_skill(user_input, skills, {}) assert decision["action"] == "no_skill"

有了测试基线,后续每加一个新技能、每调一次 Prompt,都能快速确认有没有把旧功能搞坏。我个人的经验是:调度正确率低于 85% 时,系统基本没法用,得先停下来调技能定义和 Prompt,而不是继续堆功能。

4.6 第六步:接入大模型,处理流式输出的场景

很多 Agent 应用需要流式输出,也就是让用户看到"字一个一个蹦出来"的效果。技能系统加进去后,流式场景会变复杂——你不能在调用技能期间让用户干等,也不能把整个响应切成没头没尾的片段。

我常用的处理套路是:

  • 用户发送请求后,先给一个"正在准备..."的占位回复,让前端有动态反馈。
  • 后台执行技能调用,这部分通常是毫秒到秒级,用户能明显感受到"等了一下"。
  • 技能返回结果后,再由大模型生成最终回复,这段可以走流式输出。

前端实现上,用 Server-Sent Events(SSE)比较合适。不用上 WebSocket 那么重的方案,SSE 天然支持单工推送,正好匹配"服务端一边生成一边推给客户端"的需求。

5. 常见问题与排查技巧实录

5.1 技能调度不准,动辄选错技能

这是所有做技能系统的人最先遇到、也最头疼的问题。我排查过十几次,总结下来根因往往出在两个地方:

一是技能描述不够具体。比如description写得太泛,模型就搞不清它到底是干嘛的。解决办法前面已经说过——描述要写清"什么场景触发"。

二是注册表里技能太多,但触达范围高度重叠。比如"查询天气"和"查询气温"两个技能,用户说"今天冷吗"调度器在两者之间来回横跳。解决办法是合并同类项,或者明确技能之间的边界,比如规定"query_weather"只负责"描述整体天气状况",而"query_temperature"负责"返回具体温度数值",并且描述里把边界写死。

还有一个容易被忽略的原因:Prompt 的顺序会影响判断。在decide_skill的 system prompt 里,把最新、最高频使用的技能排在前面,模型命中率会高一些。这个结论我在被测过的项目里反复验证过,虽然谈不上严谨的心理学依据,但实际效果确实存在。

5.2 技能执行了,但返回结果格式不对,Agent 不会用

output_schema不明确,或者执行代码里没做严格校验,都会导致这种情况。我之前写过一个技能,返回的日期字段是"2025-01-01 00:00:00",而 Agent 期望的是"2025-01-01",结果回答用户时直接把时间戳原样甩出去了,特别难看。

解决办法:第一,技能执行器返回前做严格校验,格式不对就转成规范格式再返回。第二,在output_schema里清晰地标注每个字段的类型和格式要求,给大模型一个明确的"数据结构蓝图"。最好在描述里加一个example字段,直接给一个真实的返回样例,模型照着样例组织输出,效果会立竿见影。

5.3 技能调用耗时太长,用户已经等得不耐烦了

有时候技能本身不慢,慢的是调度过程中的模型计算和上下文组装。我也遇到过单纯的技能代码慢——比如某个技能内部做了多层 HTTP 请求,每次请求 500ms,三次下来就 1.5 秒了。这还没算调度器的耗时。

排查步骤建议如下:

  1. 先区分瓶颈在哪一环。给整条链路的所有关键节点打上耗时日志:
    • decide_skill用了多久?
    • execute_skill用了多久?
    • 大模型生成最终回复用了多久?
  2. 如果是execute_skill慢,优先检查技能内部有没有串行请求,能不能改成并发调用。
  3. 如果是decide_skill慢,可以考虑给调度器加缓存。比如用户同样问"今天有什么待办",在缓存命中的情况下直接返回调度决策,无需再次调用模型。

另外,技能定义里可以加timeout_seconds字段,执行器读到这个值后强制限制技能运行时间,超时就返回一个"技能执行超时"的错误结果给大模型,让它妥善回复用户,而不是让用户干等。

5.4 上下文串味:技能 A 的脏数据污染了技能 B

上下文共享是双刃剑。共享数据用好了,能让技能之间协同;用不好,一个技能产生的脏数据会污染后续所有技能。

最常见的脏数据类型是"残留的错误信息"。比如技能 A 执行失败了,它往共享存储里写了一堆错误堆栈,之后技能 B 被调用时把错误信息夹杂在上下文里,大模型看到后就开始胡言乱语,甚至把错误堆栈当成用户提问来回答。

解决思路很简单:共享上下文里只存"成功收敛后的结果摘要",不存原始错误日志。具体可以在execute_skill_with_context里加一道开关——技能执行成功才写上下文,失败就不写。代码类似:

try: result = execute_skill(manifest, final_params) ctx.shared_storage["last_skill_output"] = { "skill": manifest["skill_name"], "status": "ok", "summary": summarize(result) # 只存摘要,不存原始数据 } except Exception as e: # 错误信息不写入共享存储 pass

这个小改动基本上能解决 90% 的上下文污染问题。

5.5 测试用例自己都测不准:断言太严或太松

写测试的时候,很多人在调度测试上过度用功,对 AI 生成的 JSON 做非常严格的断言——比如参数值精确到字符串完全匹配。这在 non-deterministic 的系统里是徒劳的,因为同一句话,模型两次生成的参数可能有细微差别。

我建议的测试策略是:

  • 调度行为断言宽松:只管action是否是预期技能,不苛求参数的精确性。
  • 参数格式断言严格:如果断言了参数,重点检查类型和格式(是不是字符串、是不是 YYYY-MM-DD 格式),而不是具体值。
  • 技能执行断言全面:对执行器返回的数据做深度结构校验,字段有无、类型对不对、空值处理得如何,这些必须严格。

按这个思路,测试的稳定性大幅提升,不再三天两头因为模型"发挥不稳"而红一大片。测试的目的不是证明模型是完美的,而是保障系统整体行为是可靠的。

6. 关于技能系统后续演进的一点个人体会

做agent-skills这个方向的这几个月,我最大的感受是:这类系统的维护成本主要不在写代码,而在持续迭代技能定义和测试基线。你每加一个技能,都要重新审视注册表的结构、描述的写法、边界划分,稍一偷懒后面就会加倍还债。

有一个小技巧我愿意推荐给所有人:每次调整技能定义后,把 Agent 之前容易犯错的测试用例重新跑一遍,并把新发现的边界情况补充进测试集里。我就是靠这个办法,让系统的调度准确率从最初的 70% 出头,慢慢稳到现在的 90% 以上。

另外,如果以后你的技能数量真的发展到了几十个,可以考虑引入简单的语义检索再做一层粗过滤,把候选技能范围从一开始就缩小。我试过用向量化关键词组合来实现,效果不错,但那是另一个故事了。现阶段,先把技能定义、调度器、执行器、上下文管理这套基本功打扎实,你的 Agent 能力边界会清晰得多,维护起来心里也有底。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询