☰
Agent技能库治理:自动归档与LLM整合的双相位设计
2026/10/14 4:49:53 网站建设 项目流程

如果你维护过一个超过 50 个技能定义的 Agent 平台,大概会懂我经历过的互联网:技能目录越来越长,旧技能没人用但没人敢删,新技能和旧技能功能重叠,还有不少描述写得模棱两可,大模型在意图路由时根本不知道该触发哪一个。Hermes Curator 就是我为了解决这个问题写的一个后台维护服务,核心思路是“自动归档 + LLM 整合”双相位设计:先用确定性规则筛掉不健康的技能,再用大模型对活下来的技能做语义层加工。这篇文章会把这套机制的细节展开,包括数据模型、归档阈值、调度编排、提示词设计,以及我踩过的坑,适合正在做 Agent 技能库、自动化运维体系或者类似“元数据治理”系统的开发者参考。

1. 项目概述与双相位设计思路

1.1 我遇到的是什么样的混乱

技能库一开始只有十几个技能的时候,整个流程其实很轻:每个技能就是一个 YAML 文件,里面写清楚描述、参数 Schema 和触发示例,路由层按关键词去匹配就行。但是当技能数量增长到上百个,问题就冒出来了。

首先是“僵尸技能”。有些技能是某个同学在某个实验项目里顺手写的,只被测试环境调用过几次,上线之后再也没有人用。但它们依然躺在技能索引里,每次意图路由都要参与打分,既浪费上下文额度,又可能在错误场景里被选中,产生很诡异的输出。

其次是“双胞胎技能”。查天气这个能力被拆成了三个技能:一个叫“天气查询”,一个叫“今日天气助手”,还有一个叫“weather_tool”。三个技能的描述、参数 Schema 都非常相似,模型靠命中选择,用户得到的回答质量完全随缘。

第三是“描述失真”。技能的真实能力其实已经迭代了,但描述字段还停留在第一版。比如某技能已经支持批量文件夹重命名了,描述里却仍写着“单个文件改名”,模型只能按过期描述做路由。这类问题靠人工排查效率很低,因为没人会把所有技能重新读一遍。

这些问题的共同点是什么?它们都不是“代码逻辑坏掉”的问题,而是“技能资产没有被持续维护”的问题。代码还有人 review,技能描述却几乎没人管。所以我想做的不是一个一次性的清理工具,而是一个能长期跑的 Curator 服务——也就是后台的“策展人”,不停地理清技能库。

1.2 为什么是“自动归档 + LLM 整合”而不是一个通用清理工具

在最开始,我尝试过用一个简单的规则脚本去清扫:90 天没调用就删掉,遇到关键词重复就报警。跑了一阵子之后发现问题很严重:直接删,风险太高;只报警,又没人处理。

后来我把需求拆成了两个完全不同的工作:

归档,是“减法”,需要高确定性。它处理的是“这个技能没有价值”的判断,一旦判断错,会造成线上能力不可用,所以必须靠可解释的规则去驱动,绝对不能靠大模型“猜测”认为某技能没用了。

整合,是“加法”,需要高语义理解。它处理的是“这个技能应该被更好地描述、归类、补全”的任务,比如改写描述、找同义词触发器、识别相似技能。这些工作用规则做很费劲,但大模型很擅长。

把这两类工作硬凑在一个流程里会很别扭。如果让大模型先判断“要不要删”,就得承担幻觉风险;如果让规则去做“描述好不好”的评判,又只能做关键词层面的粗浅判断。所以我的设计是两个相位分开跑:相位一用规则做高置信度归档,相位二用大模型做低风险元数据加工,中间用一张技能数据模型把它们串起来。

这个“双相位”不是纯粹为了结构好看,而是为了把风险等级不同的操作隔离。归档动作会在运行索引上生效,影响面大;元数据增强只影响静态信息,影响面小。隔离之后,两个相位可以用完全不同的频率、节奏和审核方式去发布。

2. 技能数据模型与归档判定逻辑

2.1 技能包长什么样

要让归档规则和 LLM 加工都能复用,技能必须有一个统一定义的模型。我这里采用的是“技能包”的概念,每个技能包对应一个独立可加载的能力单元,存储成一个 YAML 文件,同时把关键字段冗余到数据库表里供检索。

id: skill_weather_query name: 天气查询 owner: team_public status: active # active / pending_archive / archived description: 根据城市或经纬度查询当前天气、未来24小时预报 version: 3.2.0 tags: [weather, location] parameters: city: string lat: float lon: float unit: enum[celsius, fahrenheit] entrypoint: function://weather_service/query examples: - "北京今天会下雨吗" - "帮我查一下东京的天气" usage_metrics: last_used_at: 2025-06-01T08:00:00Z invocations_30d: 312 error_rate_30d: 0.021 created_at: 2024-03-10T00:00:00Z updated_at: 2025-05-20T00:00:00Z

这个模型里最容易被忽略的是usage_metrics这组字段。很多人做技能管理时只存技能定义,不存调用统计,结果就是归档规则完全没有数据可用。我是在数量增长到超大之后才开始埋点的,补数据的过程非常痛苦。如果你的技能库还小,建议从第一天就把这三个字段收集起来:最后使用时间、30 天调用次数、30 天错误率。

另外,status字段是归档机制的状态基础。我没有直接把“归档”设计成一个布尔值,而是用了一个三态状态机:active表示正常生效,pending_archive表示已进入待归档观察期,archived表示正式归档。这个中间态非常重要,后面会细讲。

2.2 打分模型:哪些指标可以进公式

归档判断不能只看单一指标。比如一个技能 30 天调用次数是 0,可能只是因为它只服务一个低频但关键的业务场景;错误率高也可能是因为下游接口临时故障,而不是技能本身废了。

我最终采用的是“一票否决 + 加权累计”的双层判断方式。

一票否决条件包括:

  • 连续 90 天last_used_at为空或从未上线;
  • 技能引用的上游接口在过去 30 天有超过 10 次 5xx 且没有任何成功调用;
  • 技能包 schema 格式非法,导致加载失败超过 3 次。

只要命中其中一个,就直接进入待归档候选池。这不代表立即归档,只是给后续流程一个起点。

对于那些没有命中一票否决,但表现也很差的技能,我用一个加权分来排序:

archive_score = 0.35 * usage_penalty + 0.25 * recency_penalty + 0.20 * error_penalty + 0.20 * overlap_penalty

四个子分的计算方式如下:

  • usage_penalty:30 天调用次数少于 10 次记 1.0,10 到 50 次按比例递减,超过 50 次记 0;
  • recency_penalty:距今 7 天内使用过记 0,7 到 30 天记 0.3,30 到 60 天记 0.7,超过 60 天记 1.0;
  • error_penalty:错误率低于 1% 记 0,1% 到 5% 记 0.5,高于 5% 记 1.0;
  • overlap_penalty:通过技能描述和参数 Schema 的相似度计算,相似度高于 0.85 时记 1.0;相似度越低,分数越低。

当archive_score >= 0.70时,技能进入待归档候选池。这个阈值不是拍脑袋定的,而是用历史一个季度的人工清理结果做了一次回溯:把所有被人工判定为“该删”的技能跑一遍公式,找区分度最好的阈值。最后发现 0.70 这一档能覆盖人工判断的 92% 左右,同时误杀率控制在 5% 以内。

这里要特别提醒:阈值和权重不要迷信统一值。每个平台对“低频技能”的容忍度不同。如果你是做企业内部助手,一个季度用一次的流程自动化技能也可能很关键;如果你是做 C 端助理,低使用率基本就等于没人需要。所以我建议把打分权重做成配置项,而不是硬编码。

2.3 状态机与半删除

确定了候选技能后,不对其立即删除,而是先进pending_archive状态。这个中间态是我认为整个归档机制里最值得保留的设计。

pending_archive状态下的技能不会从索引里移除,但会在管理界面里以“即将归档”的标签展示,同时系统会给技能负责人和技能使用者发送通知。观察期我设定为 7 天。在这 7 天里,如果有人手动“保活”这个技能,它就回到active;没有人反对,观察期结束后自动转为archived。

转为archived之后要做的事是“软删除”,而不是把 YAML 文件从库里抹掉。我这里的做法:

  1. 在数据库里把status更新为archived,写入archived_at时间戳;
  2. 把技能包文件移动到一个冷存储目录,目录名加archived/前缀;
  3. 从运行时技能索引中移除该技能,让它不再参与意图路由和 LLM 上下文拼装;
  4. 在审计日志里记录归档原因,包括命中哪些规则、当时的打分、操作人是谁。

这点非常重要:永远不要把归档做成不可逆删除。因为你根本不知道三个月后是否会有一个新的营销活动需要复用某个老技能。保留归档文件不会占用多少存储空间,但它能给你一条后悔药。我见过不少平台,一上来就“清理无用技能”,把文件删得干干净净,过两周业务方找过来说“那个技能我们还要用”,最后只能从备份里翻——非常狼狈。

3. 自动归档相位:先让规则把路走稳

3.1 扫描任务怎么落地

自动归档相位是一个典型的定时批处理任务。我用一个独立调度服务在每个凌晨跑一次全量扫描,避开业务高峰。

扫描逻辑大致如下:

def scan_skills_for_archive(batch_size=200): with db.transaction() as tx: lock = tx.select_for_update("job_lock", id="archive_phase") if lock.held_by_other_job(): return # 防止并发重复运行 candidates = [] cursor = tx.execute( "SELECT * FROM skills WHERE status = 'active' LIMIT ?", batch_size ) for skill in cursor: if hit_veto_rule(skill): candidates.append(skill) elif compute_archive_score(skill) >= 0.70: candidates.append(skill) for skill in candidates: update_skill_status( skill.id, "pending_archive", reason="auto_archive_candidate" ) send_notification(skill.owner)

之所以要加select_for_update锁,是因为这个任务如果部署了多个副本,很容易出现两个实例同时扫描、同时操作同一批技能的情况。第一次上线时我没锁,结果凌晨的归档任务跑了两份,待归档列表里出现了一堆重复通知,把同事吓了一大跳。

批大小设置为 200,是因为扫描过程里需要读技能描述、参数 Schema,还要计算相似度,这批数据量不宜太大。如果技能库已经有上千个技能,可以把任务拆成多个 batch 串行处理,每个 batch 结束后记录一个 checkpoint,下次从上次断点继续。

3.2 归档流程的六个步骤

我把一次完整的归档流程拆成了六个步骤,每一步都有独立的日志和状态记录:

  1. 快照:复制当前技能包到归档工作区,确保后续操作不影响原始文件;
  2. 标记:把技能状态从active改为pending_archive,同时记录候选原因;
  3. 通知:给技能负责人、依赖该技能的下游系统发待归档提醒;
  4. 观察:等待 7 天,期间任何人可执行“保活”操作,中断归档流程;
  5. 归档:观察期过后,把技能移入冷存储,从运行索引摘除;
  6. 验证:检查运行时索引中已经不存在该技能,确认冷存储文件完整,然后在审计表里打上完成标记。

第 1 步“快照”我单独拎出来说。你可能会觉得,软删除都做了,为什么还要快照?因为冷存储目录本身也需要版本管理。如果归档操作中途依赖的一个服务挂了,可能在移动文件时只移了一半。快照能保证任何失败场景下,都有一份完整副本可以恢复。

第 4 步的“保活”机制也很关键。实现上就是一个极简的接口:任何人看到“即将归档”的通知,点一下保活按钮,系统就把状态改回active,并记录保活人的信息和保活理由。实际上,每次发布预告之后都会有一两个被误伤的技能被捞回来,这比“自动化到底”要稳妥得多。

3.3 为什么用软删除而不是直接删文件

这里多讲一点“软删除”的考量,因为我在不同项目里见过很多清理机制,最不推荐的 Hard Delete 方案至少有三个风险:

  • 恢复成本高。你需要翻历史快照、找离线备份,恢复流程少则几十分钟,多则半天;
  • 引用不可控。有些业务流程里的代码仍会按 ID 调用老技能,删除之后直接报错;
  • 审计不完整。当你想回溯“这个技能为什么下线”时,如果没有归档记录,所有决策都会失真。

软删除则把“不可用”和“不存在”明确分开。归档后技能对路由层不可见,但在审计、BI、回滚通道里依然存在。即便真的确定某个技能再也不需要,也应该再保留至少 90 天的冷数据,然后走一个“永久删除”流程。

在我现在的实现里,archived状态的技能还有一个默认的保留策略:30 天内可以一键恢复,随后自动转纯冷存储,再过 60 天进入可清除序列。这个时间窗不算长,但足以覆盖绝大多数“误杀补救”的场景。

4. LLM 整合相位:语义层加工由模型接手

4.1 LLM 在这里到底解决什么问题

自动归档相位把技能库里的“量”降下来了,但这只是第一步。剩下的技能依然可能描述不清、命名混乱、相似度高。这些问题的共性是需要理解语义,而不是计算指标。所以我设计了第二个相位:LLM 整合。

它主要做四件事:

  • 描述规范化:把模糊、冗余的技能描述改写成适合意图路由的短文本;
  • 别名挖掘:从历史用户请求中提取技能的隐性触发词,补充到examples或aliases字段;
  • 相似技能识别:输出“技能 A 和技能 B 的语义重叠度”以及合并建议;
  • 新技能草稿生成:从意图路由的兜底日志中挖掘未被覆盖的高频请求,生成新技能草稿。

前两件事相对简单,属于“锦上添花”;后两件事才是真正的难点。比如相似技能识别,如果只看描述和参数,容易被表面措辞误导。“天气查询”和“气温查询”乍看是同一个技能,但它们可能一个接外部气象接口,一个接本地传感器数据,合并了反而出问题。所以 LLM 的输出只能作为“建议”,不能直接落地。

4.2 提示词与输出协议

为了让 LLM 整合相位可复用,我把它做成了一个批量增强任务。输入是一个技能包的元数据,输出是一个严格格式化的 JSON 结构。示例提示词骨架如下:

你是一个技能库策展助手。下面是一个技能包的元数据,包括描述、参数 Schema 和已有示例。 技能名称:{skill.name} 现有描述:{skill.description} 参数 Schema:{skill.parameters} 示例触发句:{skill.examples} 请完成以下任务: 1. 将描述改写成 50 字以内的意图路由友好版本,保留全部关键信息; 2. 额外生成 3 个用户可能使用的触发句变体,包含口语化说法; 3. 评估该技能是否有潜在歧义,如果有,输出一个明确的“不要在什么场景下使用”的说明。 只输出 JSON,不要解释过程。

模型的输出会严格按照下面的 JSON Schema 做校验,校验失败则重试一次,仍失败就跳过该技能,不会覆盖原数据:

{ "rewritten_description": "string", "trigger_variants": ["string", "string", "string"], "ambiguity_warning": "string" }

为什么要强制 JSON 输出并做 Schema 校验?因为 LLM 整合相位是无人值守跑的,我没办法每一条输出都人工看。如果模型偶尔输出一段散文,解析逻辑就会崩,而且崩在任务中间很难排查。用 Schema 校验之后,不符合格式的结果会被当成“本次增强失败”,不影响技能原有字段,非常干净。

4.3 防幻觉与成本控制

大模型做语义加工最大的风险不是“没干活”,而是“干活但编错了”。我在第一个版本就遇到过:模型把某个技能描述改写得很通顺,但把“支持城市列表”写成了“支持全球任意城市”,后续所有依赖城市白名单的业务全乱了套。

所以我在这个相位里加了三条硬规则:

  1. 只允许增补,不允许改写核心逻辑。rewritten_description只能基于原始描述压缩和润色,不能新增原描述里不存在的能力点;所有新增信息必须放在独立的enhancements字段,而不是混进原始字段;
  2. 生成内容进人工审核队列。自动生成的新描述不会直接写入active技能,而是先进入一个 review 表,由技能负责人在管理后台一键确认或驳回。只有确认后的变更才会合并;
  3. 同版本去重。给每个技能包加一个metadata_versions字段,调用 LLM 前先对比该技能的updated_at与上次增强任务记录。如果没有任何变化,直接跳过,不再调用模型。

成本控制上,关键是不把整个技能包塞给模型。技能包里的 few-shot 示例可能很长,每个示例有用户输入、助手输出、工具调用链,全塞进上下文既贵又容易让模型分心。我只传描述、参数 Schema 和 2 条代表性示例,其余内容一概不传。这样单次调用的 token 消耗会从几千降到几百,跑完全量技能的耗时会明显缩短。

有一个小技巧:把调用结果按技能内容的 hash 做缓存。如果某个技能的内容没变,第二次跑 LLM 整合时直接读缓存,成本能再降一个量级。尤其在你调整提示词、重新跑全量的时候,这个缓存能让成本从“吓人”变成“可控”。

5. 双相位编排细节:谁先谁后、锁和幂等

5.1 为什么必须先归档后增强

我把这两个相位串成了一个固定节奏的流水线:每天凌晨先跑自动归档,等归档相位确认结束后,再跑 LLM 整合相位。

之所以坚持这个顺序,是因为 LLM 整合阶段的输出质量会被输入质量严重干扰。如果技能库里还堆着大量僵尸技能,LLM 在做相似技能识别时,很容易把某个活跃技能和一个废弃技能标记为“重复”,并生成合并建议。这个建议一旦被执行,就等于把一个有用的技能带下了水。

先归档后增强,等于是先做一轮“数据清洗”,再做一轮“数据加工”。清洗后的集合更小、更健康,LLM 在有限上下文里也能把注意力集中在真正值得关注的技能上。从实际效果看,先归档后增强跑出来的相似技能识别结果,和乱序跑出来的结果相比,有效建议率从 60% 左右提高到了 83%。

另外,LLM 整合相位还能反过来辅助归档:它会在生成新技能草稿时,先检索已归档技能,如果发现高频请求其实已经被某个旧技能覆盖,就会生成一个“复活建议”,而不是“新技能草稿”。这样两个相位就形成了一个闭环:归档负责降噪,LLM 负责发现误杀。

5.2 并发控制与幂等

双相位流水线最怕的是两个任务同时修改同一个技能的状态。自动归档要把技能改成archived,LLM 整合可能正好在读这个技能并尝试生成新描述,如果两个操作交叉,轻则丢更新,重则把已归档技能重新激活。

我的做法是给每类任务加一个全局互斥锁:

  • archive_phase_lock:自动归档相位持有,直到观察期标记全部完成;
  • llm_enrichment_lock:LLM 整合相位持有,直到本轮所有技能提交审核。

两个锁互斥,确保同一时间只有一个相位在消费技能表。锁的粒度是“相位级”而不是“技能级”,虽然粗了一点,但实现简单,而且这两个任务本来就不应该并行跑。如果你的技能量足够大,需要分片区跑,可以把锁改成“分区键”,比如按team_id分片,但核心原则不变:同一技能同时只能被一个维护任务处理。

幂等上,我依赖的是技能状态字段。所有任务在执行前都会重新读取技能状态,再决定是否可以操作。比如归档任务发现技能已经是pending_archive,那就跳过;LLM 整合发现技能上次内容 hash 没变,那就直接复用旧结果。用状态字段做幂等,比用 UUID 任务 ID 更直观,查问题也方便。

5.3 可观测性:怎么知道它在干活

双相位都是后台任务,如果不可观测,出了事都不知道是哪一步出的。我在 Hermes Curator 里加了一组维护专用的 Metrics 和审计表:

  • 归档相位:候选技能数、进入观察期数、保活数、归档成功数、归档失败数;
  • LLM 整合相位:处理技能数、调用成功数、 schema 校验失败数、生成建议数;
  • 流水线整体:相位开始时间、结束时间、耗时、是否有任务被锁阻塞。

审计表里每一条操作都记录了操作类型、操作前状态、操作后状态、触发规则或提示词版本、执行人和执行时间。一旦有人问“这个技能为什么归档了”,我可以直接查出一条完整链路,而不是从日志文件里 grep。

另外,我把“保活率”单独做成了可视化指标。保活率的含义是:进入pending_archive后,被人工保活的技能占候选技能的比例。如果保活率突然升高,说明归档阈值过严了;如果保活率长期为 0,又可能是阈值过松。这是一个反馈信号,比“删了多少技能”更能反映归档质量。

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

6.1 问题速查表

这一路下来遇到了不少问题,很多问题看起来各不相同,但根因往往就集中在几个点上。我整理了一个速查表,方便在出问题时能快速定位:

现象可能原因快速排查方式
归档任务重复执行缺少互斥锁看任务日志里是否有两个实例同时拿到锁
已归档技能被异常唤起运行时索引没摘除干净查技能索引构建任务是否依赖了全量缓存
LLM 生成的新描述出现幻觉输入上下文包含太多无关示例减少上下文,只传描述和参数 Schema
保活操作后技能仍被归档状态更新被后续扫描覆盖检查保活接口是否更新了updated_at字段
技能相似度误判只比较了描述,没比较参数把参数 Schema 加进相似度计算特征

第三行的幻觉问题我再展开一下。我第一次跑全量增强时,贪心地把每个技能的完整 few-shot 示例都塞进了提示词,结果模型“学到”了很多工具调用的中间过程,生成的新描述反而偏离了技能真实入口。后来我把示例从 5 条减到 2 条,并且告诉模型“只描述能力,不要描述过程”,情况立刻好了很多。给 LLM 的上下文不是越多越好,如果里面混有大量过程性信息,它会把过程当能力,这是特别容易踩的坑。

6.2 让我印象深刻的三个坑

第一个坑是“测试环境污染调用统计”。自动归档的规则依赖invocations_30d,但我当时没有区分环境维度,测试环境和生产环境的调用全算在一起。结果某个技能被回归测试脚本高频调用,看起来非常活跃,永远不会进入归档候选。后来我在埋点数据里加了env字段,归档计算只取env = production的数据,问题才解决。

第二个坑是“低频但不可替代”的技能。有一个季度报表示例技能,每个季度只被财务部门调用一次,手动触发,但没有任何其他技能能替代它。按照我最初的 90 天无调用自动归档规则,它会在每个季度末被误杀一次。后来我在规则里增加了“受保护标签”机制:个别技能可以打上protected标签,归档流程会直接跳过。现在我会在归档规则上线前,先拿历史调用数据跑一遍全量回测,找出所有“低频但受保护”的技能,提前打标签。

第三个坑是 LLM 输出校验失败时的静默降级。最初校验失败时,我选择直接跳过该技能,不做任何标记。结果跑了两个月之后,有十几个技能从来没被增强过,但没有任何人发现问题。后来我把“连续跳过 3 次”作为一个告警条件,一旦连续失败就会在管理后台显示“该技能增强异常”,同时发通知给负责人。现在这个告警已经帮我提前发现过两次提示词 bug。

说起告警,还有一个细节:告警消息一定要带上技能 ID 和失败的原始输出,最好再附带一个“重跑”按钮。不然运维的人看到告警还得自己去翻日志,效率会低很多。这个需求是我在实际使用中被同事吐槽后才补上的。

写在最后:如果重新做,我会怎么改

这套 Hermes Curator 从最初一个几百行的 Python 脚本,逐渐长成了带状态机、带审核流、带双相位编排的小系统。回看整个过程,我认为最值得保留的设计不是某一个具体的归档公式,也不是 LLM 提示词,而是那个中间状态pending_archive。它给了系统一个低成本纠错窗口,让自动化不至于变成一场豪赌。

如果我现在重新写一遍,会在第一天就把“可恢复性”做进去,而不是等到误杀事故发生后再补。具体来说,就是归档记录不只记一个archived_at,还得记清楚归档前的完整技能包、当时的打分详情、是谁在什么环节确认的。这些数据越早设计,后面的回溯分析和误杀恢复就越省力。

另外我会把 LLM 整合相位从“增强技能元数据”扩展成“持续审查技能入口”。比如定期把所有归档技能和活跃技能放在一起做一次语义索引,用新的用户请求去检索最相关的技能。这样,每当出现一个新需求,系统能先回答“我们是不是已经有一个旧技能可以覆盖”,而不是直接让用户从零开始写新技能。这个扩展方向我还在慢慢完善,但双相位的底座已经够用了。如果你也在维护一个日益膨胀的技能库,不妨先从一个简单的扫描脚本开始,把状态机和审计记录立起来,再一步步引入 LLM 的语义加工,效果会比一开始就上大而全的平台好得多。

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

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

立即咨询