1. 这不是“调用API”——而是让Agent真正理解“该用哪个技能、怎么搭起来用”
你有没有遇到过这种情况:写好了一堆Skill——查天气、搜文档、算日期、发邮件、调数据库……可一到真实任务里,Agent要么死活找不到该用哪个,要么硬凑两个不搭界的技能一起跑,结果返回一堆乱码或直接报错?我去年带三个团队落地Agent项目,80%的交付延期都卡在这一步:技能库不是插件仓库,是能力语义网络;检索和组合不是算法题,是意图解构工程。标题里说的“怎样找到并组合正确的Skill”,核心根本不在代码怎么写,而在于——你有没有把Skill当成“有上下文、有边界、有依赖关系的活体能力单元”,而不是冷冰冰的函数列表。热搜词里反复出现的“agent开发”“rag检索”“组合算法”“意图识别到检索语义”,其实都在指向同一个底层矛盾:LLM能生成逻辑,但不能天然建立技能间的因果链。比如用户说“帮我对比上周和上月的销售数据,生成PPT发给王总”,这个请求里隐含了至少5层能力调度:时间解析(“上周/上月”)→ 数据库查询(销售表+时间过滤)→ 表格计算(同比/环比)→ PPT生成(模板+图表嵌入)→ 邮件发送(收件人+附件+正文)。任何一个环节的Skill选错,整条链就断。这不是靠“相似度打分最高”就能解决的,而是要构建一套可验证、可追溯、可干预的能力调度图谱。本文不讲抽象理论,只分享我在电商、金融、政务三类Agent项目中沉淀下来的实操框架:从Skill定义规范、语义索引结构、动态组合策略,到线上灰度验证方法——所有内容都经过日均30万次调用的真实压力检验。适合正在写第一个Skill、或正被“Agent执行终止”错误折磨的开发者,也适合技术负责人评估团队Agent能力基建是否扎实。
2. Skill不是函数,是带“身份证”和“关系网”的能力实体
2.1 为什么90%的Skill定义从一开始就埋了雷?
很多团队第一步就错了:把Skill当成一个Python函数封装。比如写个get_weather(city: str) -> dict,然后扔进技能库。问题在哪?当Agent看到“北京今天热不热”时,它需要判断:
- 这是不是天气相关意图?→ 需要语义理解
get_weather能不能处理“热不热”这种主观描述?→ 需要知道该Skill的输出是否含体感温度字段- 如果用户接着问“那上海呢”,要不要复用同一个Skill实例?→ 需要状态管理标识
- 如果天气API超时,有没有降级方案(比如查缓存)?→ 需要容错声明
这些信息,光靠函数签名根本无法承载。我们团队强制推行Skill三证制:
- 能力身份证(Capability ID):不是随便起名,而是结构化命名
weather.forecast.v2,其中weather是领域域,forecast是动作类型,v2是版本号。这样在检索时可按前缀快速过滤,避免getWeather/fetchWeather/queryWeather这类同义词爆炸。 - 语义说明书(Semantic Spec):用YAML明确定义输入约束、输出Schema、支持的实体类型、典型触发句式、失败兜底策略。例如:
input_constraints: - field: city type: string required: true entity_types: [GPE, LOCATION] # 支持地理实体识别 - field: date type: string required: false default: "today" format: "YYYY-MM-DD or 'today/tomorrow'" output_schema: temperature_feel: string # 明确包含"热/冷/舒适"等体感描述 humidity: number warning_level: enum [none, yellow, orange, red] trigger_phrases: - "今天XX热不热" - "XX现在温度怎么样" - "体感温度" fallback_strategy: cache_last_24h提示:这个YAML不是文档,是运行时可解析的元数据。我们用Pydantic Model自动校验,任何字段缺失或类型错误,在注册Skill时就抛异常,绝不让“半残废”Skill入库。
- 关系联络图(Dependency Graph):明确标注该Skill依赖哪些其他Skill或外部服务。比如
weather.forecast.v2依赖geo.resolve.v1(把“朝阳区”转成经纬度),而geo.resolve.v1又依赖geocoding.api服务。这个图不是画在纸上,而是以JSON-LD格式存入图数据库,供组合引擎实时查询依赖路径。
2.2 技能库不是“文件夹”,是支持多维检索的语义索引系统
把Skill按文件夹分类(如/skills/weather/,/skills/email/)是初级做法。真实场景中,用户说“把会议纪要发给张三和李四”,你需要同时匹配:
- 文档处理Skill(提取纪要文本)
- 邮件发送Skill(但必须支持多收件人)
- 联系人查询Skill(把“张三”转成邮箱)
- 权限校验Skill(确认当前用户能否发给李四)
这要求技能库具备四维检索能力:
- 意图维度:基于用户Query做语义向量检索(我们用Sentence-BERT微调版,专门针对中文办公语料训练,比通用模型准确率高27%);
- 能力维度:按
capability_id前缀精确匹配(如email.send.*); - 约束维度:按
input_constraints字段动态过滤(如required: true且entity_types含PERSON); - 上下文维度:结合对话历史中的已执行Skill,排除重复调用(如刚执行过
doc.extract.v1,则不再检索同类Skill)。
我们用Elasticsearch + Neo4j混合架构实现:
- Elasticsearch 存储Skill的文本描述、触发短语、YAML元数据全文,支撑意图和能力检索;
- Neo4j 存储Skill节点及其依赖关系、调用频次、成功率、平均耗时,支撑上下文感知和动态排序。
关键设计点:不依赖单一向量相似度打分。比如用户说“订明天下午三点的会议室”,向量检索可能召回calendar.create.v1(准确)和weather.forecast.v2(因“下午”“三点”有弱语义关联)。这时约束维度立刻生效:calendar.create.v1的input_constraints明确要求time字段为datetime类型且required:true,而weather.forecast.v2的date字段required:false,直接过滤掉后者。实测下来,这种多维过滤使首条命中率从61%提升到93%。
2.3 “组合”不是拼积木,是构建可执行的调度拓扑
找到单个Skill只是开始。真正的难点在于:如何把多个Skill连成一条能跑通的流水线?很多人用简单规则:“如果A输出含X字段,则调用B”。但现实更复杂:
- A输出是结构化JSON,B输入要XML格式 → 需要转换Skill;
- B执行失败,要回退到C而非重试B → 需要定义fallback路径;
- D和E可并行执行(如同时查库存和查物流)→ 需要DAG调度器;
- F依赖G和H的输出合并 → 需要聚合Skill。
我们的解决方案是组合模板(Composition Template),不是代码,而是声明式DSL:
name: "order_status_check" steps: - id: "get_order" skill: "order.query.v3" input_mapping: order_id: "$.user_input.order_id" output_alias: "order_data" - id: "check_stock" skill: "inventory.check.v2" input_mapping: sku: "$.order_data.items[0].sku" parallel_with: ["check_logistics"] # 并行执行 - id: "check_logistics" skill: "logistics.track.v1" input_mapping: tracking_no: "$.order_data.shipping.tracking_no" - id: "aggregate_result" skill: "util.aggregate.v1" input_mapping: stock_status: "$.check_stock.status" logistics_status: "$.check_logistics.status" order_info: "$.get_order" output_alias: "final_report"这个模板被编译成DAG图,由轻量级调度器执行。关键创新点:
- 输入映射支持JMESPath语法:
$.order_data.items[0].sku直接从上游输出取值,避免手写转换代码; - 并行标记自动插入Barrier节点:确保
aggregate_result等待check_stock和check_logistics都完成才启动; - 每个Step绑定超时和重试策略:
check_stock设3秒超时、最多重试1次,aggregate_result设1秒超时、不重试(纯内存操作); - 输出别名形成局部变量空间:后续Step可直接引用
$.check_stock.status,无需关心上游Skill的原始字段名。
这套DSL不是凭空设计,而是从我们处理过的237个真实业务流程中抽象出来的。比如电商“售后退款”流程平均含8.3个Skill,其中3.2个需并行,1.7个需条件分支(如“金额>500需财务审批”),全部用模板声明,开发效率提升4倍,且运维人员可直接修改模板调整流程,无需动代码。
3. 实操:从零搭建可检索、可组合的技能库(附完整代码片段)
3.1 Step 1:定义Skill注册协议(Python SDK)
我们不手写YAML,而是用Python Class声明Skill,SDK自动生成元数据:
from agent_skills import Skill, InputField, OutputField, TriggerPhrase class WeatherForecastSkill(Skill): capability_id = "weather.forecast.v2" inputs = [ InputField( name="city", type=str, required=True, entity_types=["GPE", "LOCATION"], description="城市名称,支持模糊匹配如'朝阳区'" ), InputField( name="date", type=str, required=False, default="today", description="日期,支持'today/tomorrow'或'2024-05-20'" ) ] outputs = [ OutputField( name="temperature_feel", type=str, description="体感温度描述:'炎热'/'凉爽'/'舒适'等" ), OutputField( name="humidity", type=float, description="相对湿度百分比" ) ] trigger_phrases = [ TriggerPhrase("今天{city}热不热"), TriggerPhrase("{city}现在温度怎么样"), TriggerPhrase("体感温度") ] def execute(self, city: str, date: str = "today") -> dict: # 真实调用天气API return { "temperature_feel": "炎热", "humidity": 65.0 } # 注册时自动校验并生成YAML元数据 WeatherForecastSkill.register()SDKregister()方法会:
- 校验
inputs中所有entity_types是否在预定义白名单内(防止乱填PERSON); - 检查
trigger_phrases是否含未定义占位符(如{country}但inputs无country字段); - 生成标准YAML存入ES,并同步写入Neo4j的Skill节点;
- 将Class方法
execute包装成统一接口,屏蔽底层实现差异(可对接HTTP、gRPC、本地函数)。
实操心得:我们强制要求每个Skill必须有
trigger_phrases,且至少3个。没写触发短语的Skill,注册时直接拒绝。因为这是连接用户语言和机器能力的唯一桥梁——没有它,检索就失去锚点。
3.2 Step 2:构建多维检索引擎(Elasticsearch配置)
ES索引skill_catalog的关键Mapping:
{ "mappings": { "properties": { "capability_id": {"type": "keyword"}, "semantic_vector": {"type": "dense_vector", "dims": 768}, "input_constraints": { "properties": { "field": {"type": "keyword"}, "type": {"type": "keyword"}, "required": {"type": "boolean"}, "entity_types": {"type": "keyword"} } }, "trigger_phrases": {"type": "text", "analyzer": "ik_max_word"}, "success_rate_7d": {"type": "float"}, "avg_latency_ms": {"type": "float"} } } }检索Query示例(用户Query:“查张三的邮箱”):
{ "query": { "bool": { "must": [ { "knn": { "field": "semantic_vector", "query_vector": [0.1,0.9,...], "k": 10 } }, { "term": { "input_constraints.entity_types": "PERSON" } } ], "filter": [ { "term": { "capability_id": "contact.find.v1" } }, { "range": { "success_rate_7d": { "gte": 0.8 } } } ] } }, "sort": [ { "_score": { "order": "desc" } }, { "success_rate_7d": { "order": "desc" } } ] }这里的关键是filter优先于knn:先用entity_types和capability_id前缀快速缩小候选集(从10万Skill降到200个),再对这200个做向量检索。实测响应时间从1200ms降到87ms,且首条准确率更高——因为向量检索在小集合上更稳定。
3.3 Step 3:实现组合模板编译器(核心算法)
模板编译的核心是DAG构建与依赖解析:
def compile_template(template_yaml: dict) -> nx.DiGraph: graph = nx.DiGraph() # Step 1: 添加所有节点 for step in template_yaml["steps"]: graph.add_node( step["id"], skill=step["skill"], input_mapping=step.get("input_mapping", {}), timeout=step.get("timeout", 5), retries=step.get("retries", 0) ) # Step 2: 解析依赖关系(基于input_mapping中的$.引用) for step in template_yaml["steps"]: for key, value in step.get("input_mapping", {}).items(): if value.startswith("$."): # 提取上游Step ID,如"$.get_order.items[0].sku" → "get_order" upstream_id = value.split(".")[1] if upstream_id in graph.nodes: graph.add_edge(upstream_id, step["id"]) # Step 3: 处理并行标记 for step in template_yaml["steps"]: if "parallel_with" in step: for peer_id in step["parallel_with"]: # 添加虚拟Barrier节点,确保所有并行Step完成后才执行当前Step barrier_id = f"barrier_{step['id']}" graph.add_node(barrier_id, type="barrier") graph.add_edge(step["id"], barrier_id) graph.add_edge(peer_id, barrier_id) return graph调度器执行时:
- 同时启动所有入度为0的节点(如
get_order); - 每个节点执行完,检查其下游节点入度是否降为0,是则启动;
- 遇到
barrier节点,等待所有上游节点完成才继续; - 任一节点失败,按预设
fallback路径跳转(如check_stock失败则跳check_stock_fallback)。
注意:我们禁止在模板中写
if/else逻辑。所有分支都通过独立Skill实现,比如approval.need_finance.v1(判断是否需财务审批)和approval.direct.v1(直接审批),由LLM根据用户角色选择调用哪个。这样保证模板纯粹是调度指令,不掺杂业务逻辑。
3.4 Step 4:上线灰度与效果验证(真实数据看板)
技能库上线不是“一次发布”,而是三级灰度:
- 沙箱验证:新Skill注册后,先在测试环境用100条历史Query跑回归测试,检查:
- 是否被正确检索(命中率≥95%);
- 执行是否超时(P95<2s);
- 输出是否符合Schema(字段名、类型、必填项);
- 小流量AB测试:对1%真实用户,将新Skill加入候选集,对比旧流程的:
- 任务完成率(用户是否得到最终答案);
- 平均步骤数(越少越好,说明组合更精准);
- 用户主动中断率(用户中途说“算了”);
- 全量监控看板:核心指标实时展示:
指标 计算方式 健康阈值 Skill复用率 (被调用≥2次的Skill数 / 总Skill数) ≥65% 组合链成功率 (成功执行完所有Step的流程数 / 总流程数) ≥88% 首步命中延迟 从Query到首个Skill启动的毫秒数 P95 ≤ 150ms Fallback触发率 (走fallback路径的流程数 / 总流程数) ≤5%
我们曾发现email.send.v2的Fallback触发率突然升到12%,排查发现是SMTP服务器证书过期。但更重要的是,看板显示email.send.v2的复用率仅31%——说明大部分用户需求没被覆盖,于是我们紧急上线了email.send_batch.v1(支持群发),复用率一周内升至79%。
4. 常见问题与避坑指南(来自237个真实故障现场)
4.1 “Agent execution terminated due to error”——90%源于Skill间的数据契约断裂
这个报错看似是代码异常,实则是上下游Skill对数据格式的理解错位。典型案例:
doc.extract.v1输出{ "text": "会议纪要..." };summary.generate.v2输入要求{ "content": "..." };- 组合模板没写
input_mapping,导致summary.generate.v2收到{"text": ...},解析失败。
避坑方案:
- 强制所有Skill输出用统一Schema(我们定义
StandardOutput基类,含data、metadata、error字段); - 在调度器中插入契约校验中间件:执行前检查上游输出是否含下游所需字段,缺失则自动注入默认值或报清晰错误(如“
summary.generate.v2requires fieldcontent, but gottext”); - 开发者工具链集成:VS Code插件实时高亮模板中
input_mapping的字段名,若上游无此字段则标红。
我踩过的最深的坑:某次升级
weather.forecast.v2,新增了uv_index字段,但没更新weather.display.v1的输入Schema。结果所有天气查询页面崩溃。后来我们加了CI检查:任何Skill变更,必须运行所有依赖它的组合模板的单元测试。
4.2 “检索不到Skill”——不是向量不准,是语义锚点缺失
用户说“把这份合同发给法务部”,检索却召回email.send.v2(正确)和file.upload.v1(错误)。问题出在:file.upload.v1的trigger_phrases只有“上传文件”,没覆盖“发给法务部”这种业务场景表述。
根治方法:
- 触发短语必须来自真实对话日志:我们每天从客服系统抓取1000条含“发给/转给/抄送”等动词的句子,用NER标注出实体(如“法务部”→
DEPARTMENT),批量生成TriggerPhrase; - 为Skill添加业务标签:在YAML中加
business_tags: ["contract", "legal_review", "approval"],检索时用terms查询补充语义; - 人工审核机制:新Skill上线前,由业务专家用50个典型Query测试,命中率低于90%则打回重写。
4.3 “组合结果不对”——不是算法问题,是上下文丢失
用户连续问:
- “查北京天气” → 返回“炎热”;
- “那上海呢” → 却返回“北京天气”(复用上一步结果)。
这是因为组合引擎没维护对话状态上下文。解决方案:
- 在每个Skill执行时,自动注入
context对象,含last_user_query、last_skill_output、conversation_history(最近3轮); weather.forecast.v2的execute方法签名改为:def execute(self, city: str, date: str = "today", context: dict = None) -> dict: if city == "那" and context and context.get("last_skill_output"): # 从上一轮输出中提取城市 city = self._extract_city_from_context(context) return {...}- 上下文对象本身也作为Skill的输入约束字段,强制开发者考虑状态依赖。
4.4 “技能库越来越臃肿”——不是加得太多,是没做能力归并
团队常犯的错误:为每个微小差异新建Skill,如:
email.send_to_one.v1email.send_to_two.v1email.send_to_dept.v1
这导致检索候选集爆炸,组合模板复杂度飙升。
归并原则:
- 参数化替代多版本:
email.send.v2的to字段支持string(单人)、list[string](多人)、dict(部门+角色),用同一Skill处理; - 能力分级:基础Skill(
email.send.v2)只负责发信,高级Skill(email.approval_flow.v1)封装审批流程,后者调用前者; - 定期审计:每月运行脚本,统计所有Skill的调用频次,调用率<0.1%且30天无更新的Skill,自动进入“归档队列”,由负责人确认是否删除。
我们曾清理掉47个低频Skill,技能库体积减少35%,但任务完成率反升2%,因为LLM在更精简的候选集中更容易做出正确选择。
5. 技能库的终极形态:从工具箱到能力操作系统
5.1 不是“有多少Skill”,而是“能多快构建新能力”
很多团队把技能库当成果展示墙,堆了200个Skill就沾沾自喜。但真正的价值在于:当业务方提需求“下周要支持电子签章”,你能在2小时内上线可用Skill,而不是排期两周。这要求技能库具备:
- 能力原子化:签章功能拆解为
document.hash.v1(生成文档哈希)、signature.create.v1(调用CA服务)、document.merge.v1(PDF合并),每个都是独立Skill; - 组装可视化:提供低代码界面,拖拽Skill节点,连线定义数据流,自动生成组合模板;
- 一键发布:点击发布,自动完成:注册到ES/Neo4j、生成API文档、部署到K8s集群、加入灰度流量池。
我们内部叫它“能力乐高台”,产品同学自己就能搭出新流程。上周法务部要“合同自动归档”,他们用3个现有Skill(OCR识别、关键词提取、NAS存储)搭了个模板,从提需求到上线只用了1小时17分钟。
5.2 技能库的护城河:不是代码,是持续积累的语义知识
最后说个容易被忽视的点:技能库的价值70%不在代码,而在语义元数据。
weather.forecast.v2的trigger_phrases里,“热不热”“温度怎么样”“体感如何”这些短语,是三年来从12万条用户Query中提炼的;input_constraints.entity_types里的GPE/LOCATION/DEPARTMENT,是和NLP团队联合标注的百万级实体词典;fallback_strategy的“缓存24小时”决策,是基于天气数据变化频率的统计分析。
这些知识无法复制,只能积累。所以别急着开源你的Skill代码,先保护好你的语义知识库——这才是Agent项目真正的护城河。
我在成都带团队做政务Agent时,有个老同事退休前交给我一个U盘,里面是17年整理的《政府公文术语-技能映射表》,比如“拟办意见”对应document.review.v1,“呈报领导”对应approval.route.v2。现在这个表还在驱动着每天20万次的公文处理。真正的技能库,从来不是代码仓库,而是组织能力的活体字典。