1. 从"写死"到"生长":技能生成的底层逻辑
1.1 为什么静态技能包注定短命
做过 Agent 项目的人大概都有过这种体验:花了两周时间精心打磨一套技能包,把各种边界条件、异常处理、参数校验都写得明明白白,上线第一周效果拔群,第二周开始陆续出现"这个场景没覆盖到""那个输入格式变了"的反馈,一个月后整个技能库变成了一座没人敢动的屎山。这不是个别现象,而是静态技能设计的结构性缺陷。
静态技能的本质是"把某个时间点的知识固化成代码或配置"。问题在于,Agent 面对的真实环境是持续变化的——用户的表达方式在变、上游数据格式在变、业务规则在变、甚至连模型本身的能力边界都在变。你写死的那套逻辑,本质上是在用昨天的地图找今天的路。
我见过太多团队在这个坑里反复挣扎。他们的第一反应通常是"再加一层 if-else",第二反应是"再写一个技能包",第三反应是"算了,让模型自己发挥吧"。这三条路都走不通:加 if-else 会让维护成本指数级上升,堆技能包会导致技能之间互相冲突,完全放手则意味着不可控。
真正可行的路径只有一条:让技能具备从知识和经验中自动生成、并持续演化的能力。这就是 SkillGen、Anything2Skill、Trace2Skill 这几个概念要解决的核心问题。
1.2 SkillGen、Anything2Skill、Trace2Skill 到底在解决什么
先把这三个概念掰开揉碎说清楚,不然后面没法聊。
SkillGen的核心思路是"从知识源生成技能"。你给它一份文档、一段教程、一个 API 说明,它能把里面的操作流程抽取成结构化的技能定义。比如你丢给它一份"如何用 Pandas 做数据清洗"的教程,它应该能生成一个包含"读取数据→处理缺失值→类型转换→去重→输出"这样步骤序列的技能。
Anything2Skill更进一步,它强调的是"任何东西都能变成技能"。不只是文档,一段对话记录、一个操作录屏、甚至一个别人写好的脚本,都能被转化成可复用的技能。这个思路的野心在于:把人类积累的隐性知识显性化、结构化。
Trace2Skill则聚焦在"从执行轨迹中学习"。Agent 在执行任务时会产生大量的 trace(调用记录、中间状态、成功/失败标记),这些 trace 本身就是最宝贵的经验来源。Trace2Skill 要做的是从这些轨迹中识别出"什么样的操作序列能成功""什么情况下会失败""失败后怎么恢复",然后把这些模式固化成技能。
这三个概念合在一起,构成了一个完整的技能生命周期:从知识中生成 → 从经验中演化 → 持续迭代优化。
1.3 技能演化的三层架构
我在实际项目中总结出一套三层架构,比较好用:
| 层级 | 职责 | 输入 | 输出 |
|---|---|---|---|
| 生成层 | 从知识源抽取技能骨架 | 文档、教程、对话、脚本 | 技能定义(步骤+参数+前置条件) |
| 执行层 | 运行技能并记录轨迹 | 技能定义+运行时上下文 | 执行 trace(成功/失败+中间状态) |
| 演化层 | 从 trace 中学习并优化技能 | 历史 trace+反馈信号 | 技能更新(新增分支/调整参数/废弃步骤) |
这三层不是串行的,而是循环的。生成层产出的技能进入执行层,执行层产生的 trace 反馈给演化层,演化层更新后的技能又回到执行层。整个系统像一个活的有机体,而不是一堆死代码。
注意:三层架构的关键在于"反馈信号"的设计。如果反馈信号只是简单的成功/失败,演化层能学到的东西非常有限。理想情况下,反馈信号应该包含:任务是否完成、完成质量如何、耗时多少、消耗了多少 token、用户是否满意等多维度信息。
2. 技能生成的核心技术点拆解
2.1 知识抽取:从非结构化文本到结构化技能
知识抽取是技能生成的第一步,也是最容易翻车的一步。很多人以为把文档丢给大模型让它"总结成步骤"就行了,实际做下来会发现:模型总结出来的东西要么太粗("处理数据"这种废话),要么太细(把每个标点符号都当成一步),要么漏掉关键的前置条件。
我在实践中总结出一套"三段式抽取法",效果比较稳定:
第一段:识别操作意图。先让模型判断这段知识描述的是什么类型的操作——是数据转换、是 API 调用、是决策判断、还是流程编排。不同类型的操作,后续的抽取策略完全不同。
第二段:抽取步骤序列。针对识别出的操作类型,用对应的模板去抽取。比如数据转换类,重点抽"输入格式→转换规则→输出格式";API 调用类,重点抽"端点→参数→认证方式→返回处理"。
第三段:补全前置条件和异常处理。这一步最容易被忽略,但恰恰是技能能否真正跑通的关键。前置条件包括"需要什么权限""需要什么环境""依赖哪些其他技能";异常处理包括"如果输入为空怎么办""如果超时怎么办""如果返回格式不符预期怎么办"。
# 三段式抽取的伪代码示意 def extract_skill(knowledge_text): # 第一段:识别操作意图 intent = llm.classify(knowledge_text, categories=[ "data_transform", "api_call", "decision", "orchestration" ]) # 第二段:按类型抽取步骤 steps = llm.extract_steps(knowledge_text, template=intent) # 第三段:补全前置条件和异常处理 preconditions = llm.infer_preconditions(knowledge_text, steps) error_handlers = llm.infer_error_handling(knowledge_text, steps) return Skill( intent=intent, steps=steps, preconditions=preconditions, error_handlers=error_handlers )这套方法的关键在于"分而治之"。一次性让模型做所有事,它会在各个维度上都做得马马虎虎;拆成三步之后,每一步的准确率都能显著提升。
2.2 技能编码:把步骤变成可执行的指令
抽取出来的步骤还是自然语言,需要进一步编码成 Agent 能执行的指令。这里有个常见的误区:很多人直接把自然语言步骤塞进 prompt 里让模型照着做。这样做在简单场景下能跑通,但一旦步骤多了、分支复杂了,模型就会开始"自由发挥"。
我的做法是把技能编码成一种半结构化的格式,我称之为"技能脚本"。它长这样:
skill_name: clean_csv_data version: 1.2 intent: data_transform preconditions: - file_exists: "{{input_path}}" - format_is: "csv" steps: - id: step_1 action: read_file params: path: "{{input_path}}" encoding: "utf-8" on_error: - retry_with_encoding: "gbk" - fail: "无法读取文件" - id: step_2 action: handle_missing params: strategy: "{{missing_strategy | default: 'drop'}}" branches: - condition: "missing_ratio > 0.5" action: warn_and_confirm - id: step_3 action: type_convert params: target_types: "{{type_mapping}}" on_error: - log_and_skip: true这种格式的好处是:Agent 执行时有明确的边界,不会随意发挥;同时保留了足够的灵活性(通过{{}}占位符支持参数化);而且可以被程序化地分析和修改,为后续的演化层提供了操作接口。
2.3 从 Trace 中学习:演化层的核心机制
Trace2Skill 是整个体系里最有意思的部分。Agent 每次执行技能都会产生一条 trace,这条 trace 记录了:执行了哪些步骤、每步的输入输出、耗时、是否成功、失败时的错误信息。
从这些 trace 中,我们可以挖掘出几类有价值的模式:
模式一:高频失败点。如果某个步骤在 30% 的执行中都失败了,那这个步骤大概率有问题——要么是前置条件没检查到位,要么是参数默认值不合理,要么是异常处理缺失。
模式二:成功路径的共性。对比成功和失败的 trace,找出成功路径中共同出现的操作。比如发现"成功的执行都在 step_2 之前做了数据校验",那就可以把数据校验固化成技能的一部分。
模式三:参数的最优区间。如果某个参数在不同取值下成功率差异很大,就可以从 trace 中统计出最优区间,作为默认值。
# 从 trace 中挖掘失败模式的简化逻辑 def mine_failure_patterns(traces): failure_by_step = defaultdict(list) for trace in traces: if not trace.success: failure_by_step[trace.failed_step].append(trace) patterns = [] for step, failures in failure_by_step.items(): failure_rate = len(failures) / total_executions(step) if failure_rate > 0.2: common_errors = extract_common_errors(failures) patterns.append({ "step": step, "failure_rate": failure_rate, "common_errors": common_errors, "suggested_fix": suggest_fix(common_errors) }) return patterns这套机制跑起来之后,技能库会自己"长"出新的分支和异常处理。我有个项目跑了三个月,技能库从最初的 12 个技能演化到了 47 个技能,其中大部分新增技能都是从 trace 中自动识别出来的高频操作模式。
3. 实操:搭建一个能自我演化的技能系统
3.1 环境准备与依赖选型
先说环境。这套系统对基础设施的要求不算高,但有几个关键组件必须到位:
向量数据库:用来存储技能定义和知识片段,支持语义检索。我常用的是 Chroma 或 Qdrant,前者轻量适合快速验证,后者性能更好适合生产环境。
Trace 存储:可以用 PostgreSQL 或 ClickHouse。数据量小的时候 PostgreSQL 完全够用,数据量大了(每天百万级 trace)就得上 ClickHouse。
LLM 接口:生成层和演化层都需要调用 LLM。建议至少准备两个模型——一个能力强的用于技能生成(对准确性要求高),一个速度快的用于 trace 分析(对吞吐量要求高)。
任务队列:技能执行和 trace 分析都是异步任务,需要一个队列来调度。Celery 或 Redis Queue 都可以。
# 基础依赖安装 pip install chromadb qdrant-client psycopg2-binary celery redis openai提示:如果你的 Agent 运行在容器环境里,注意把 trace 存储挂载到持久化卷上。我踩过一次坑,容器重启后三个月的 trace 数据全没了,演化层直接失忆。
3.2 技能生成流水线的搭建
技能生成流水线分四个阶段:知识摄入、技能抽取、技能编码、技能入库。
知识摄入阶段,需要处理各种格式的输入。文档类(PDF、Markdown)直接解析文本;对话类需要先做角色分离和话题切分;脚本类需要做 AST 分析提取关键操作。
技能抽取阶段,用前面说的三段式抽取法。这里有个实操技巧:不要一次性把整篇文档丢给模型,而是先做段落级切分,每个段落独立抽取,最后再合并。这样做的好处是准确率高,坏处是可能丢失跨段落的依赖关系。我的做法是:段落级抽取 + 一次全局合并 pass,兼顾准确率和完整性。
技能编码阶段,把抽取出的步骤转成前面说的 YAML 格式。这一步建议加一个"人工审核"环节,尤其是前几十个技能,人工过一遍能发现很多模型注意不到的问题。
技能入库阶段,把技能定义存入向量数据库,同时建立索引(按 intent、按前置条件、按依赖关系)。
# 技能生成流水线的核心逻辑 class SkillGenerationPipeline: def __init__(self, llm, vector_store): self.llm = llm self.vector_store = vector_store def ingest(self, source): if source.type == "document": return self.parse_document(source) elif source.type == "conversation": return self.parse_conversation(source) elif source.type == "script": return self.parse_script(source) def extract(self, chunks): skills = [] for chunk in chunks: intent = self.llm.classify(chunk) steps = self.llm.extract_steps(chunk, intent) skills.append({"intent": intent, "steps": steps, "source": chunk}) return self.merge_skills(skills) def encode(self, skills): encoded = [] for skill in skills: yaml_skill = self.llm.to_yaml(skill) if self.validate(yaml_skill): encoded.append(yaml_skill) return encoded def store(self, skills): for skill in skills: self.vector_store.add( embedding=self.embed(skill), metadata=skill )3.3 Trace 采集与演化触发机制
Trace 采集的关键是"采集什么"和"怎么触发演化"。
采集什么?我建议至少采集这些字段:技能 ID、执行时间戳、输入参数、每步的输出、每步的耗时、最终状态(成功/失败/部分成功)、错误信息、用户反馈(如果有)。
怎么触发演化?有两种策略:定时触发和阈值触发。定时触发就是每天/每周跑一次演化分析;阈值触发是当某个技能的失败率超过阈值(比如 20%)时立即触发。我通常两个都用:定时触发做全局优化,阈值触发做紧急修复。
# Trace 采集的装饰器 def trace_skill(skill_id): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): trace = { "skill_id": skill_id, "timestamp": time.time(), "input": kwargs, "steps": [], "status": "running" } try: result = func(*args, **kwargs, trace=trace) trace["status"] = "success" return result except Exception as e: trace["status"] = "failed" trace["error"] = str(e) raise finally: save_trace(trace) return wrapper return decorator3.4 演化策略:什么时候该改技能,什么时候该新建
演化层最难的决策不是"怎么改",而是"该不该改"。我总结了一个决策矩阵:
| 情况 | 策略 | 理由 |
|---|---|---|
| 单一步骤失败率高 | 修改该步骤的异常处理 | 局部问题局部解决 |
| 多个步骤失败率都高 | 检查前置条件是否缺失 | 可能是环境问题 |
| 成功路径中出现新步骤 | 考虑新增该步骤 | 可能是新的最佳实践 |
| 失败 trace 中出现重复模式 | 新增分支处理 | 覆盖新的边界情况 |
| 技能整体使用率低 | 考虑废弃或合并 | 避免技能库膨胀 |
这个矩阵不是绝对的,但能覆盖 80% 的决策场景。剩下的 20% 需要人工判断,尤其是涉及业务逻辑变更的时候。
注意:演化层一定要有"回滚"机制。我见过一次演化把某个技能的核心步骤删了,导致依赖它的下游技能全部崩溃。后来加了版本控制和灰度发布,新版本技能先在小流量上跑,确认没问题再全量。
4. 常见问题与排查技巧实录
4.1 技能生成质量不稳定的排查思路
技能生成质量不稳定是最常见的问题。表现是:同样的输入,有时候生成得很好,有时候生成得一塌糊涂。
排查思路分三步:
第一步,检查输入质量。如果输入文档本身结构混乱、术语不统一,模型很难抽出好的技能。解决办法是加一个预处理步骤,做术语归一化和结构标准化。
第二步,检查 prompt 稳定性。如果 prompt 里有模糊的指令(比如"提取关键步骤"),模型每次的理解可能不同。解决办法是把指令具体化(比如"提取所有涉及数据读写的操作,按执行顺序排列")。
第三步,检查模型温度参数。生成技能时温度应该设低(0.1-0.3),保证输出稳定。如果温度设高了,模型会"创造性发挥",生成一些看似合理但实际跑不通的步骤。
4.2 Trace 数据稀疏时的冷启动方案
新系统刚上线时,trace 数据很少,演化层基本学不到东西。这时候需要冷启动方案。
我的做法是"人工注入 trace":手动构造一批典型场景的执行记录,包括成功案例和失败案例,作为演化层的初始训练数据。这批数据不需要很多,每个技能 10-20 条就够,但覆盖面要广——正常情况、边界情况、异常情况都要有。
另一个技巧是"跨技能迁移":如果技能 A 和技能 B 的 intent 相同,可以把 A 的 trace 经验迁移到 B 上。比如 A 是"清洗 CSV 数据",B 是"清洗 JSON 数据",两者在"处理缺失值"这个步骤上的经验是可以共享的。
4.3 技能冲突与版本管理
技能库大了之后,冲突是必然的。两个技能可能对同一个输入有不同的处理方式,或者一个技能的输出格式和另一个技能的输入格式不匹配。
解决冲突的核心是"显式声明依赖关系"。每个技能定义里都要写清楚:我依赖哪些技能、我产出什么格式、我期望什么格式的输入。这样在编排的时候,系统可以自动检测冲突。
版本管理方面,我建议用语义化版本(major.minor.patch)。major 版本变更表示不兼容的修改,minor 表示新增功能,patch 表示 bug 修复。演化层自动生成的修改默认是 patch 版本,需要人工确认才能升 minor 或 major。
4.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 技能生成步骤过粗 | prompt 指令不够具体 | 检查 prompt 中的动词 | 用具体动词替换模糊动词 |
| 技能执行时卡住 | 前置条件未满足 | 检查 preconditions | 补充前置条件检查 |
| 演化后技能变差 | 反馈信号有噪声 | 检查 trace 质量 | 过滤低质量 trace |
| 技能之间互相干扰 | 依赖关系未声明 | 检查技能定义 | 补充依赖声明 |
| 生成速度慢 | 模型调用次数过多 | 统计 LLM 调用次数 | 合并批量请求 |
| 技能库膨胀 | 缺少废弃机制 | 统计技能使用率 | 定期清理低使用率技能 |
4.5 几个踩过的坑
坑一:过度依赖 LLM 生成。一开始我让 LLM 全权负责技能生成,结果生成了一堆"看起来很美但跑不通"的技能。后来改成"LLM 生成 + 规则校验 + 人工抽检",质量才稳定下来。
坑二:忽略 trace 的时效性。早期的 trace 反映的是早期的情况,如果业务已经变了,用老 trace 训练出来的技能可能不适用。后来加了时间衰减因子,近期 trace 的权重更高。
坑三:演化频率过高。有段时间我设置了每小时演化一次,结果技能库天天变,下游系统根本跟不上。后来改成每天一次,紧急情况才手动触发。
坑四:没有区分"技能问题"和"模型问题"。有时候技能执行失败不是技能本身的问题,而是模型能力不够。这时候改技能没用,得换模型或者加 few-shot 示例。判断方法是:如果同一个技能在不同模型上表现差异很大,那就是模型问题。
5. 技能演化的边界与未来方向
5.1 什么该演化,什么不该演化
不是所有技能都适合自动演化。我的经验是:操作类技能适合演化,决策类技能要谨慎,合规类技能禁止演化。
操作类技能(数据转换、格式处理、API 调用)的演化风险低,因为它们的正确性有客观标准——跑通了就是跑通了,跑不通就是跑不通。
决策类技能(判断优先级、选择策略)的演化风险中等,因为"好"的标准比较主观。这类技能的演化需要更谨慎的反馈信号,最好有人工审核环节。
合规类技能(权限校验、数据脱敏、审计日志)绝对不能自动演化。这类技能的修改必须经过严格的人工审核,因为一旦出错后果严重。
5.2 演化系统的可观测性建设
演化系统本身也需要被观测。我建议至少监控这几个指标:
- 技能生成成功率:生成层产出的技能中,能通过校验的比例
- 技能执行成功率:执行层中技能成功完成的比例
- 演化采纳率:演化层提出的修改中,被实际采纳的比例
- 技能库增长率:每周新增技能数量
- 技能复用率:一个技能被多个任务调用的比例
这些指标能帮你判断系统是否健康。比如演化采纳率突然下降,可能是演化层的建议质量变差了;技能复用率低,可能是技能粒度太细了。
5.3 从单 Agent 到多 Agent 的技能共享
单个 Agent 的技能演化已经够复杂了,多 Agent 场景下更麻烦——不同 Agent 的技能库怎么共享?一个 Agent 学到的经验怎么迁移给另一个?
我目前探索的方案是"技能市场"模式:每个 Agent 把自己的技能发布到中心化的技能市场,其他 Agent 可以订阅。订阅之后,技能会在本地执行并产生本地 trace,这些 trace 反馈给技能市场,用于全局演化。
这个模式的好处是:技能可以跨 Agent 复用,演化可以汇聚多个 Agent 的经验。坏处是:需要处理技能版本兼容性问题,以及不同 Agent 环境差异导致的执行结果不一致问题。
这块我还在摸索阶段,等跑通了再单独写一篇。
我个人在实际操作中的体会是:技能演化这件事,技术难度其实不是最大的,最大的挑战是"克制"。看到系统能自动生成技能、自动优化技能,很容易产生一种"让它自己跑就行了"的冲动。但实际跑下来会发现,没有人工干预的演化系统,用不了多久就会跑偏。真正好用的系统,一定是"自动演化 + 人工把关"的组合。自动演化负责处理 80% 的常规情况,人工把关负责处理 20% 的关键决策。这个比例不是固定的,随着系统成熟度提高可以逐步调整,但永远不要让它变成 100% 自动。