ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Agent技能库设计与实战:定义、路由、参数校验到可观测性全解析

2026/10/7 4:20:29 拓冰建站 浏览量
Agent技能库设计与实战:定义、路由、参数校验到可观测性全解析 1. 项目概述agent-skills到底是什么这两年做 AI Agent 的团队几乎都会遇到同一个坎大模型本身的对话能力已经很强了但一旦要把 Agent 丢进真实业务里干活你会发现它像个高智商但是没手没脚的新员工——让他查个数据库他得先搞清楚连接串让他发一封邮件他得知道你们的邮件模板长什么样让他处理一个工单他根本不知道你们公司内部那把工单流转的钥匙放在哪儿。agent-skills 就是为了解决这个手和脚的问题。它的核心思路是把 Agent 能执行的每一个原子能力——查库存、写 SQL、调接口、发通知、做数据清洗、跑测试用例——封装成一个带元信息的标准技能模块。每个技能模块都包含技能怎么被调用、参数长什么样、触发条件是什么、内部执行逻辑是什么、以及最重要的什么时候应该用它、什么时候坚决不能用它。Agent 本身不再需要背诵每个业务系统的细节它只需要在运行时做一件事——理解用户意图然后从技能库里选一个最合适的技能把参数填好交给技能去执行。这套东西解决的是三类人的痛点正在做 Agent 应用开发的工程师想给现有系统接上智能层的架构师以及做 Agent 平台化产品的人。如果你的 Agent 还在靠写死一堆 if-else 或者让大模型拿着 token 乱调 API那这一篇就是给你看的。从我实际做完一整套 agent-skills 落地的经验来看这个方向最有价值的并不是技能注册那套表面功夫而是技能的定义粒度、路由判定的准确性、以及安全和可观测性的平衡。这三点做不好技能库建得再漂亮Agent 跑起来照样满嘴跑火车。2. 整体设计与思路拆解2.1 为什么 Agent 需要技能抽象而不是一堆函数一开始很多人会问我已经把业务接口封装成 Python 函数了Agent 调用函数不就行了为什么要多一层 skills 抽象我实测下来的感受是直接让大模型调函数短期内 demo 能跑通长期做产品一定会翻车。原因有三个。第一函数是给程序员看的不是给模型看的。你的函数名叫create_order(params)参数里有个status字段取值是 0 和 1这对人来说是常识但模型根本不知道 0 代表什么、1 代表什么更不知道什么时候应该传 0 什么时候传 1。技能模块需要在描述里把这些常识写清楚让模型能在信息不充分的条件下做出正确选择。第二函数是单点能力技能是业务闭环。发一封邮件是一个函数但一个给所有逾期客户发提醒邮件的技能可能需要先查逾期名单、再过滤已联系过的客户、然后渲染模板、最后调用邮件 API 发送还得处理失败重试。这一串逻辑不应该由大模型每次现编应该由技能内部封装好。第三函数调用没有决策边界。模型调函数是被动的技能要做的是主动声明边界——什么时候轮到我上场什么时候不该我管。Agent 收到一个帮我看看今天有没有异常订单的需求如果技能库里同时有订单查询和异常检测两个技能光靠函数名和描述模型很容易选错或者两个都试一遍。技能模块里的trigger条件和constraints就是在给模型划红线什么情况下激活、什么情况下拒绝。2.2 技能粒度怎么定拆太细是灾难拆太粗是摆设这是我在实际项目里反复迭代最多的地方。刚开始做技能库的时候研发同学习惯性地把技能拆得很细恨不得一个取当前时间都做成一个技能。结果跑起来就发现Agent 在做一个稍微复杂点的任务时需要连续调用七八个技能中间任何一次路由出错整个任务就挂了而且排错特别难——到底是模型选错了技能还是选对了但参数填错还是技能本身报错三层问题叠在一起日志看得人头皮发麻。后来我总结出一个判断粒度是否合适的标准一个技能应该对应一个用户可以感知的完整动作而不是一个机器的操作步骤。用户说帮我查一下订单量是一个动作这个动作里包含了连数据库、执行 SQL、格式化结果的多个步骤但用户感知到的就是一个查询。所以查询订单统计应该是一个技能而建立数据库连接执行 SQL这些属于内部实现不应该暴露给 Agent。另一个判断标准是技能的可复用性。如果一个技能只有一个业务场景用得上那它大概率不是一个技能而是一个流程的一部分。比如获取当前登录用户信息这个技能几乎每个业务场景都要用它就是值得做成独立技能的而把订单导出成 Excel 并邮件发送这种只属于运营一个团队的需求适合做成一个技能如果拆成生成 Excel发送邮件两个技能反而会让模型在选择时犹豫。2.3 技能编排 vs 单技能调用想清楚 Agent 的工作模式做技能库之前还得先想清楚一个问题你的 Agent 是单步工具调用模式还是任务编排模式单步模式适合对话式助手用户问一句Agent 调一个技能返回结果。这种模式下技能的设计重点是意图识别准确性和参数抽取能力技能数量宜少而精每个技能的描述要写得让模型一眼看懂。任务编排模式适合自动化执行场景比如每天凌晨自动跑一遍数据巡检并生成报告。这种模式下 Agent 需要把一个复杂任务拆成多个步骤每个步骤调用一个技能技能之间还有数据依赖。这时技能设计要重点考虑输入输出兼容性——上一个技能的输出能不能直接作为下一个技能的输入如果 A 技能返回的是 JSON 对象B 技能需要的是 ID 列表中间就还得有一个转换技能或者让 Agent 做一轮额外的信息整理。我做了一次对比实验同样一个分析客户流失原因任务在单步模式下 Agent 只调用了一个流失分析技能就搞定了在编排模式下Agent 调用了客户数据查询流失定义筛选趋势计算归因分析四个技能结果因为第四个技能需要的数据格式和前三个不匹配绕了两轮才跑通。最后我得出一个结论编排模式下技能设计必须带输出契约思维每个技能的返回值不能只追求人能看懂必须追求机器能直接吃。3. 核心细节解析与实操要点3.1 Skill 定义格式一份写给人看、读给模型听的标准模板整个 agent-skills 体系里最重要的文件就是一个技能的定义清单。我的做法是给每个技能写一份 YAML 配置包含五个核心区块基本元信息、参数 Schema、触发条件、执行逻辑说明、约束与兜底。基本元信息包括技能名称、一句话描述、详细使用说明、所属领域标签。听起来简单但这里藏着第一个坑描述怎么写直接决定路由准确率。我一开始让研发同学自己写技能描述结果清一色是该技能用于查询订单信息这种干巴巴的句子。实测效果极差模型经常把查询订单信息和查询订单统计搞混因为描述里没有说明它们的区别。后来我们把描述改成带使用场景和边界说明的版本比如当用户想查看订单列表、搜索特定订单、查看订单详情时使用本技能。注意订单统计汇总请使用订单统计技能路由准确率从 78% 提升到了 93%。这是投入产出比最高的一个优化强烈建议大家优先做。参数 Schema 我直接用 JSON Schema 格式每个参数要写清楚类型、是否必填、枚举值含义、示例值。很多人忽略示例值觉得没用实际上大模型在抽取参数时非常依赖示例。比如时间范围这个参数如果你只写字符串类型必填模型可能填最近一周这种自然语言如果你在示例里写了2024-06-01~2024-06-30模型就知道应该填标准化格式了。触发条件和约束是最容易被忽略的部分。触发条件我写成自然语言描述给模型做语义匹配用约束则明确写出什么情况下绝对不要用这个技能。比如订单改价技能约束里就要写只有订单状态为待支付时允许改价已支付订单请使用退款流程。没有约束模型就敢拿着改价技能去处理已支付的订单这是线上事故的常见来源。3.2 技能注册与发现让 Agent 知道你有哪些手牌技能定义好了之后得让 Agent 在运行时能看到这些技能。这里有两个主流方案一是把全部技能定义一次性塞进系统提示词里二是把技能索引给模型、需要时按需加载。方案一实现最简单但技能数量一多就废了。我测试过塞 30 个技能描述进上下文大概占用 3000~4000 token还能接受超过 50 个模型的选择准确率明显下降因为候选太多模型开始挑花眼。而且每个技能如果描述详细token 占用会直接爆炸API 成本至少翻倍。方案二是我最终采用的方案——两级技能发现机制。第一级给模型看的是一个技能目录每个技能只有名称和一句话简介总共几百 token。模型先根据用户意图挑出 3~5 个候选技能然后第二级再把这几个候选技能的完整描述加载进上下文让模型做最终选择。这个机制实测下来有几个好处一是 token 消耗少了 60% 多二是模型先做粗筛、再做细选选型准确率比一次性全量看更高三是技能库可以无限扩容不用担心上下文塞不下。代价是要多写一个检索模块但其实就是做一次简单的文本匹配或者向量检索我用的 BM25 加内置关键词跑下来效果足够用。技能注册还有一个细节每个技能要有版本号。模型调技能有时候很倔它会记住某个技能的返回格式就算你更新了技能逻辑如果它之前调过一次后续还是会用旧逻辑的预期去处理新结果。有了版本号至少你在排查问题的时候能快速定位到底跑的是哪一版逻辑。我们甚至统计过模型对技能描述的记忆黏性同一个技能描述如果中间改过后面几天模型的行为会有明显的不稳定期——这是很多人不会告诉你的坑。3.3 参数填充与校验模型的自由发挥是事故温床技能模块执行之前参数校验是绝对不能省的环节。我见过太多事故都是模型把参数传错了还硬着头皮执行。比如一个发送营销短信技能参数里有phone字段模型有时候会传一个带空格的手机号有时候会把两个手机号拼接在一起还有一次直接把phone和user_id搞混了。我的处理办法是做一个三层参数校验管道。第一层 Schema 类型校验检查参数类型对不对、必填项有没有缺失第二层值域校验检查枚举值是否合法、正则是否匹配比如手机号格式、日期格式、金额范围第三层语义校验这两层过完之后还得做一次理性检查比如start_time不能晚于end_time、数量字段不能为负数、查询条件里不能同时出现两个互斥的筛选条件。第三层用规则引擎实现不依赖大模型因为大模型连自己填的参数都未必能自洽更别说让它检查别人填的了。校验不通过的时候不能直接报错拉倒要让技能模块返回一个参数修复建议。比如检测到手机号格式不对返回提示手机号应为 11 位数字当前值包含非法字符 abc请移除后重试模型读到这个反馈后会自动修正参数重新调用。这个闭环设计能把参数错误导致的失败率降低一半以上。注意修复建议也必须是结构化的不然模型又会自由发挥。3.4 执行沙箱与异常捕获技能内部出错了怎么兜底技能执行时的沙箱策略分三层。第一层是网络隔离所有技能的网络请求统一走代理网关禁止任意 URL 直连防止模型被注入恶意指令时把请求打到不该打的地方。第二层是权限最小化每个技能声明自己的权限范围执行时校验 token 的权限标签比如只读类技能拿到的是只读数据库账号写操作一律走需要审计的写接口。第三层是资源限额CPU、内存、执行时长、重试次数都要有上限防止个别技能死循环或者跑超时把整个 Agent 拖垮。异常捕获这块我的模板是统一的SkillExecutionResult结构success布尔值、data结果数据、error_code业务错误码、error_message给模型看的错误描述、retryable是否可重试、suggestion给模型的后续操作建议。这个结构的作用是让技能的执行结果不仅能给人看更重要的是能让模型看懂后继续干活。如果技能返回的只是一个Exception: connection refused模型大概率会懵不知道接下来该干嘛如果你告诉他数据库连接失败请稍后重试或者尝试使用查询缓存技能模型就能自己接下去。这其实是很多人忽略的 Agent 稳定性关键错误信息本质上也是给模型的上下文的一部分写作风格必须考虑模型的阅读体验。4. 实操过程与核心环节实现4.1 技能库目录结构一个可以直接抄作业的工程模板我把自己打磨过几轮之后的技能库工程结构分享出来基本上照着搭就能用agent-skills/ ├── skills/ │ ├── order/ │ │ ├── query_order.yaml │ │ ├── query_order_stats.yaml │ │ ├── modify_order_price.yaml │ │ └── export_order_report.yaml │ ├── customer/ │ │ ├── get_customer_info.yaml │ │ ├── query_customer_risk.yaml │ │ └── create_customer_tag.yaml │ └── common/ │ ├── get_current_time.yaml │ └── send_email_notification.yaml ├── core/ │ ├── registry.py # 技能注册中心 │ ├── router.py # 候选技能检索与选择 │ ├── executor.py # 技能执行器含沙箱与超时控制 │ ├── validator.py # 参数三层校验管道 │ └── observer.py # 全链路日志与指标采集 ├── schemas/ │ └── base_skill_schema.json ├── config.yaml # 技能库全局配置 └── tests/ ├── test_router.py ├── test_validator.py └── test_skills.py每个技能对应一个 YAML 文件这是整个体系的核心资产。测试目录里我建议至少写三类测试路由准确率测试——给一批模拟用户意图验证模型能否选中正确技能参数校验测试——故意构造脏参数验证校验管道能否拦截技能执行冒烟测试——真实调用一次技能验证执行链路是通的。4.2 核心代码实现技能注册中心与执行器技能注册中心的核心逻辑不复杂就是启动时扫描目录、加载 YAML、解析成 SkillSpec 对象然后注册到内存索引里。这一步很多人会想用数据库存技能定义但我的经验是技能定义变化频率不高用 YAML 文件加 Git 管理版本就够了数据库反而多一层维护成本。# core/registry.py import yaml from pathlib import Path from dataclasses import dataclass, field from typing import Any, Dict, List, Optional dataclass class SkillSpec: name: str description: str tags: List[str] version: str parameters_schema: Dict[str, Any] trigger: str constraints: Optional[str] None executor_module: str timeout_seconds: int 30 retry_times: int 2 permission_tag: str read enabled: bool True class SkillRegistry: def __init__(self, skill_dir: str skills): self.skill_dir Path(skill_dir) self._skills: Dict[str, SkillSpec] {} self._tag_index: Dict[str, List[str]] {} def load_all(self) - None: 扫描技能目录加载所有 YAML 定义的技能。 for yaml_file in self.skill_dir.rglob(*.yaml): with open(yaml_file, r, encodingutf-8) as f: raw yaml.safe_load(f) spec SkillSpec( nameraw[name], descriptionraw[description], tagsraw.get(tags, []), versionraw.get(version, 1.0.0), parameters_schemaraw.get(parameters_schema, {}), triggerraw.get(trigger, ), constraintsraw.get(constraints, ), executor_moduleraw.get(executor_module, ), timeout_secondsraw.get(timeout_seconds, 30), retry_timesraw.get(retry_times, 2), permission_tagraw.get(permission_tag, read), ) self._skills[spec.name] spec for tag in spec.tags: self._tag_index.setdefault(tag, []).append(spec.name) def list_catalog(self) - List[Dict[str, str]]: 返回技能目录索引只含名称和一句话描述供模型粗筛。 catalog [] for spec in self._skills.values(): if spec.enabled: catalog.append({name: spec.name, brief: spec.description.split(\n)[0]}) return catalog def get_full_spec(self, name: str) - Optional[SkillSpec]: 按名称获取技能的完整定义供模型细选阶段使用。 return self._skills.get(name) def search_by_keyword(self, keyword: str) - List[SkillSpec]: 基于 BM25 简单的关键词粗筛返回候选技能。 query_terms set(keyword.lower().split()) scores {} for name, spec in self._skills.items(): haystack f{spec.name} {spec.description} {spec.trigger} { .join(spec.tags)} score sum(1 for term in query_terms if term in haystack.lower()) if score 0: scores[name] score ranked sorted(scores, keyscores.get, reverseTrue)[:5] return [self._skills[name] for name in ranked]代码里search_by_keyword用的是纯关键词匹配适合技能库在 100 个以内的情况。如果技能数量更大或者用户意图更复杂可以把这一层换成向量检索但要注意向量检索有召回过低和召回噪声的问题我实际用下来 BM25 加关键词在中文场景下反而更稳。执行器的实现核心是模板方法模式——把校验、超时、重试、日志这些横切逻辑统一处理具体业务逻辑在执行器模块里实现。下面这段代码处理的是技能调用的完整生命周期。# core/executor.py import time import traceback from typing import Any, Dict, Optional from dataclasses import dataclass from core.registry import SkillSpec from core.validator import validate_params dataclass class ExecutionResult: success: bool data: Optional[Any] None error_code: Optional[str] None error_message: Optional[str] None retryable: bool False suggestion: Optional[str] None latency_ms: int 0 class SkillExecutor: def __init__(self, registry): self.registry registry self._implementations {} def register_impl(self, skill_name: str, impl_callable): 注册技能对应的实际执行函数。 self._implementations[skill_name] impl_callable def execute( self, skill_name: str, params: Dict[str, Any], user_context: Dict[str, Any] ) - ExecutionResult: spec self.registry.get_full_spec(skill_name) if not spec or not spec.enabled: return ExecutionResult( successFalse, error_codeSKILL_NOT_FOUND, error_messagef技能 {skill_name} 不存在或已被禁用。, suggestion重新选择一个可用技能或者让用户更清晰地描述需求。, ) # 第一层参数校验 validation validate_params(spec.parameters_schema, params) if not validation.ok: return ExecutionResult( successFalse, error_codePARAMS_INVALID, error_messagevalidation.error_details, retryableTrue, suggestionvalidation.fix_suggestion, ) # 第二层权限校验伪代码实际应从用户上下文取 token if spec.permission_tag not in user_context.get(permissions, [read]): return ExecutionResult( successFalse, error_codePERMISSION_DENIED, error_messagef当前用户没有执行 {skill_name} 的权限。, suggestion告知用户权限不足并提供权限申请入口。, ) # 第三层执行技能带超时和重试 impl self._implementations.get(skill_name) if impl is None: return ExecutionResult( successFalse, error_codeIMPL_NOT_FOUND, error_messagef技能 {skill_name} 尚未注册执行实现请联系管理员。, ) last_error None for attempt in range(spec.retry_times 1): start_ts time.monotonic() try: result impl(**params, contextuser_context) latency_ms int((time.monotonic() - start_ts) * 1000) return ExecutionResult( successTrue, dataresult, latency_mslatency_ms, ) except Exception as e: last_error e latency_ms int((time.monotonic() - start_ts) * 1000) time.sleep(0.5 * (attempt 1)) error_message f技能 {skill_name} 执行失败{traceback.format_exc()} return ExecutionResult( successFalse, error_codeEXECUTION_FAILED, error_messageerror_message, retryableFalse, suggestion尝试使用替代技能或者向用户说明当前服务暂时不可用。, )代码省略了实际业务函数的接入细节但核心流程都在注册中心管有哪些技能执行器管怎么安全地跑技能校验器管参数是否合法。这套代码跑了几周之后我最大的感受是异常处理不能只抛错误就完事还是要带着suggestion走模型下一步的行动全靠这个。你要是一句话说技能不存在模型就像那颗撞墙的乒乓球只能原地打转问用户。4.3 YAML 技能定义实例三个不同粒度的真实案例先看一个比较简单的获取当前时间技能。很多人觉得这种技能没必要做成技能但它在多个 Agent 任务里都能用上而且很容易验证路由逻辑。name: get_current_time description: | 当用户询问当前时间、今天的日期、现在是几点等需要绝对时间信息时使用。 注意本技能只返回当前时间点信息不包含时区转换、日程安排等高级能力。 如果需要时区转换请使用时间处理技能。 version: 1.1.0 tags: [common, time, basic] trigger: | 用户明确询问时间或日期或者任务上下文需要当前时间戳。 parameters_schema: {} executor_module: skills.common.time_executor timeout_seconds: 5 retry_times: 1 permission_tag: read再看一个业务型的复杂技能订单统计查询。这个技能有一个硬性的参数要求统计维度和时间范围。name: query_order_stats description: | 查询订单维度的统计数据支持按订单状态、时间范围、商品类目 等维度进行分组统计返回订单数量、金额、客单价等指标。 使用场景包括查看销量、查看订单总额、对比不同时间段的订单量。 注意本技能不返回订单明细如需明细请使用“订单列表查询”技能。 version: 2.3.1 tags: [order, stats, dashboard] trigger: | 用户需要了解订单整体情况、销售趋势、汇总数字而非具体订单时触发。 parameters_schema: type: object required: [start_time, end_time] properties: start_time: type: string format: date description: 开始日期格式 YYYY-MM-DD examples: [2024-06-01, 2024-01-01] end_time: type: string format: date description: 结束日期格式 YYYY-MM-DD必须晚于 start_time examples: [2024-06-30, 2024-12-31] dimension: type: string enum: [day, week, month, status, category] default: day description: 统计维度按天/周/月/订单状态/商品类目 order_status: type: array items: { type: string } description: 订单状态过滤条件不传则统计全部 examples: [[paid, pending]] constraints: | 仅支持查询 90 天以内的数据超过 90 天请通过数据仓库离线分析技能处理。 仅支持按订单状态筛选不支持按客户等级筛选。 executor_module: skills.order.stats_executor timeout_seconds: 15 retry_times: 2 permission_tag: read这两个例子放在一起你能很明显看到简单技能和复杂技能在描述策略上的差异简单技能要把什么时候该用说清楚复杂技能除了说清楚什么时候该用还要说清楚什么时候不要用以及有什么参数限制。写 description 的时候别怕啰嗦模型不像人那样嫌你话多只要信息密度高、边界清楚描述越长路由越准。4.4 路由调度管线从用户请求到技能执行的完整链路写一下完整的调度伪代码。整个 Agent 内部的运行链路大概是用户输入进来先做意图提取和参数预填充将粗筛选目录给到模型让它返回候选技能名和初步参数将候选技能的完整定义拼接进上下文让模型做最终技能选择和完整参数抽取把参数送进校验管道校验不通过返回修复建议给模型让模型重填一次最多重填两次校验通过后执行器带超时和重试机制执行技能结果格式化后返回给模型模型结合用户原始问题生成最终回复。其中第 2 步和第 3 步是一套 Prompt 模板我把它贴在下面做参考你是技能路由选择器。 以下是可用的技能目录 {catalog} 请根据用户的请求从目录中选出你认为最相关的一个或多个技能输出 JSON 格式 {selected_skills: [skill_name_1, skill_name_2]} 判断原则 1. 优先选择描述中能覆盖用户核心意图的技能 2. 如果多个技能都能完成选择粒度最合适的那个 3. 如果没有合适的技能输出空数组不要强行选择。 用户请求{user_query}第二阶段 Prompt 模板以下是候选技能的完整定义请结合用户请求从候选技能中选择最终要执行的一个 并抽取所有必填参数。 候选技能定义 {full_skill_specs} 输出格式JSON {skill: skill_name, params: {...}} 注意 1. 严格根据技能定义的参数 Schema 抽取不要添加 Schema 中不存在的字段 2. 枚举值只能取自 enum 列表 3. 如果用户没有给出某个必填参数所需的信息且技能描述中也没有合理默认值 请将缺失参数放在 missing_params 字段中不要自行编造。这套两阶段路由跑下来我观察到一个很有意思的行为模型在第二阶段看到完整定义的时候经常会把第一阶段的候选判断推翻。比如第一阶段它选了订单列表查询看了完整定义后发现用户其实要的是订单统计它会自己改过来。这说明粗筛环节可以稍微粗一点、宁可多召回不能漏召给第二阶段留纠错空间。4.5 全链路可观测性技能版本、调用链与结果回溯Agent 排障的难点在于你很难复现它当时的思考过程所以全链路日志必须在运行时留足。我会给每个会话分配一个 trace_id用户每轮请求、路由选择、参数校验、技能执行、最终回复这五步都往同一个日志上下文里打点并把关键信息存成结构化事件。技能执行的日志至少要包含技能名称和版本、入参原始值和校验后值、执行耗时和状态码、返回结果摘要敏感字段脱敏、错误时的堆栈关键行、suggestion内容。有了这些前面说的到底是模型选错还是参数错还是执行错的三层问题基本看一遍日志就能定位。还有一个容易被忽略的点给技能调用加上一致性哈希的缓存层。同一个技能同样的入参短时间内结果基本不变比如查询订单统计如果 Agent 在两分钟内的两次任务里都调用了同一个技能直接用缓存结果就行。这样做既省 token 又省查询压力。缓存有效期按技能类型区分查询类技能 5 分钟统计类 30 分钟写操作一律不缓存。5. 常见问题与排查技巧实录5.1 模型选了技能却把参数填得乱七八糟这是遇到最多的一个问题。模型选对了技能但参数值从哪里来经常搞错典型的是把用户跟其他客户的聊天内容里的金额或者日期填进去。我的排查路径是先看校验管道的拦截日志——如果拦截率高就去看模型抽取参数时用的上下文是不是被注入了一些无关对话。解决办法有两个层面。一是调整 Prompt把仅从用户本次请求中抽取参数禁止从对话历史中推测参数值写进第二阶段 Prompt实测有效。二是在校验管道里加一个参数来源标记让模型在返回参数的时候附带参数是从哪里来的——from_user_query还是from_deduction如果来源不明确就在日志里标红。这个标记不能强制但至少你看日志的时候心里有底。还有一个技巧是给必填参数提供默认值减少模型自由发挥的机会。比如统计维度你默认给day模型抽不到用户意图时就按天统计总比它瞎填一个weekly强。5.2 技能描述像小作文一样越长越不会选这跟 5.1 是相反的另一个极端。有些团队为了让路由准确把技能 description 写成千字小作文结果模型读的时候注意力分配不均匀反而抓不住重点。我自己测试下来的一个经验性结论技能的完整描述最好控制在 300~500 字以内把最重要的信息塞进第一句话里。第一句话是这个技能的电梯陈述必须直接点明白什么情况下用我。后面的描述可以展开细节但展开的部分要有结构不要整段一气呵成——模型读半结构化文本的能力远强于读大段散文。这也是为什么 YAML 模板里我把 description 和多行文本分开trigger 单独放因为模型在执行路由判断时对带明确标签的字段敏感度远高于自由文本。5.3 技能执行成功了但 Agent 给出的回答还是错的这种问题最让人抓狂。你查日志技能调用层一切正常数据也返回了但模型最后给用户的回复里出现了错误数据。我排查过几轮之后发现根因多半是技能返回的数据结构过于复杂模型在整理结果时记混了。比如说技能返回了一个带嵌套结构的 JSON里面有total_amount和total_orders两个字段但模型在生成回复时只记住了数值附近的上下文最后把订单数和金额数说反了。要根治这个问题不能靠模型自觉而是要在技能返回结果时自带答案提炼字段——在data基础上加一个human_answer_summary把用户最关心的核心结论用一句话写清楚。然后 Prompt 里指示模型优先使用summary字段回复只在需要展开细节时才去读data明细。这个改动我们做了之后同类错误几乎绝迹。5.4 技能越加越多但新增技能抢了老技能的活技能库扩张到 30 个以上之后会出现一个新的问题新增技能描述写得不够好时会干扰模型对既有技能的判断。我举个例子。原来我们有一个发送邮件技能后来加了一个发送营销邮件技能两个技能在描述上高度重叠模型选的时候就开始摇摆不定。解决的办法有两个方向一是在旧技能的 constraints 里补一条营销类邮件请使用发送营销邮件技能本技能仅用于事务型通知邮件二是给技能增加一个conflict_with字段在做路由时如果两个技能被同时选中自动根据冲突声明剔除掉一个。我觉得前一种办法更可持续因为让模型在取舍时有一个明确依据比你在代码里硬编码优先级要优雅得多。5.5 慢技能的连锁反应有些技能执行很慢比如导出 Excel 报告动辄十秒以上。在 Agent 场景里这种长耗时技能会占着上下文窗口和用户的耐心如果不做异步化设计整个对话流程直接被卡死。我用的方案是长耗时技能降级策略执行时间超过 3 秒的技能不再同步等结果而是返回一个任务 ID同时启动后台异步执行执行完成后把结果推送到一个任务完成通知技能里。Agent 可以继续处理别的用户问题做完之后主动告知用户您的报告已经生成好了。这个方案一开始我以为会增加 Agent 的复杂度实际跑下来用户满意度反而更高了因为他们不用干等一个大报表。5.6 故障速查表故障现象排查切入点首选修复动作技能选择结果不稳定先看两阶段路由的候选集精简粗筛目录突出核心技能的差异性描述参数抽取总是错误看校验日志与回填建议增加示例值、收紧 Schema 类型、增设参数来源标记执行成功但回答错误看summary是否被覆盖统一在返回结果中提供human_answer_summary新增技能后整体准确率下降对比新增前后各技能命中率调整旧技能 constraints隔离冲突面长耗时技能导致对话卡死看技能耗时分布长耗时技能改为异步执行加结果回传技能内部报错但模型无法恢复看suggestion字段是否为空为每个错误分支补齐可操作建议6. 从技能库到技能生态规模化落地经验6.1 技能的质量评估别只看调用成功率很多团队做技能评估只盯着一个指标——调用成功率就是技能执行时有没有报错。但这个指标有一个盲区技能执行成功不代表任务完成正确。我之前见过一个客户标签查询技能调用成功率 99%但用它生成的分析报告经常把高风险客户和沉默客户混在一起因为技能底层的数据口径本身就有问题。我建议至少用三个维度评估技能质量。第一是路由准确率用户意图和技能选择是否正确匹配没选错也不能漏选。第二是参数抽取 F1参数填得完整且正确不能靠默认值蒙混过关。第三是业务结果正确性这个只能靠抽样人工评测或者结果对比去验证核心业务技能每次都去人工复核成本太高那就至少做定期抽检。这三个维度分开记录比一个综合分更能指导你优化技能。6.2 多 Agent 共享技能库时的权限边界当你把技能库从一个 Agent 扩展到多个 Agent 时权限边界就成了第一优先级的硬约束。我踩过一次很深的坑一个数据分析 Agent 和一个运营助手 Agent 共用一个技能库数据分析 Agent 有导出订单明细的权限运营助手在某个场景里把同一个技能调用了结果导出了超出其权限范围的客户明细数据——虽然运营助手本来就有查看订单权限但那条数据里包含了一些不该给运营看的内部成本价字段。教训是技能权限必须基于调用主体校验而不是基于技能本身。同一个技能被不同 Agent 调用时要携带不同的权限标签数据字段的可见性也要跟着权限走。后来我们的实现是每个技能执行时会注入一个viewer_role参数底层查询层根据这个角色动态裁剪返回字段。这个改动发布之后没有再出现过越权数据泄漏代价是技能内部实现多了一点点复杂度和几条 SQL 判断。6.3 技能的可进化性让用户反馈回写技能库最后一个经验是技能库不能在发布之后静止不动它必须像一个活文档一样持续被用户反馈喂养。我的做法是在 Agent 的最终回复下方埋一个反馈按钮用户觉得回答不对就点不满意系统会自动把当时的请求、路由快照、技能调用输出、模型回复四件套打包存成一个训练样本。每天人工抽样看几十个低质量样本你会发现三类高价值问题路由描述应该改但是一直没注意到的、参数 Schema 太窄漏掉了真实业务场景的、以及技能内部逻辑过期了该修的。我每个月会出一版技能库更新更新的依据主要就是这些反馈样本。整套体系跑下来半年多从最初的 12 个技能发展到 70 多个技能路由准确率保持稳定核心技能的业务结果正确率提升了近 20 个百分点。这个成绩不是靠某个聪明的调包或者一段漂亮的 Prompt 做出来的靠的是把技能当成产品来运营——定义要清晰、边界要分明、反馈要闭环。老实说我个人在实操中得到的最关键体会就一句话不要迷信大模型的聪明要把每一个技能当成一个需要被设计、被测试、被持续打磨的小产品。Agent 的能力上限不是由它的模型决定的而是由技能库的质量天花板决定的。