尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AI终端技能系统设计:从书架模型到调度器与上下文隔离实战

AI终端技能系统设计:从书架模型到调度器与上下文隔离实战 “技能系统”这个词在我前几篇的AI Code终端系统里其实一直是个模糊概念。最初我以为它就是把常用的提示词分个类、存几个模板直到做到第三个版本我才意识到AI终端真正缺的不是更多的提示词而是一套能把经验结构化沉淀、按需取用的机制。换句话说把经验装进书架。这个系列做到第六篇前面我们已经把终端、命令调度、上下文管理都铺完了但这套东西拿来干真正的活——比如批量重构、跨模块排错——还是会露怯AI要么不知道该调用哪个能力要么执行到一半就迷失方向。核心原因就是经验没有制度化。这一篇要把技能系统彻底讲透从“书架—书—章节”三层模型到注册、发现、执行三大流程再到技能包的目录结构、解析器、调度器和一堆踩坑实录。适合正在做个人AI生产力工具、或者想把终端型AI助手从“演示级”推到“干活级”的开发同学。1. AI Code终端为什么必须搭一套技能系统1.1 从“什么都会一点”到“什么都做不深”的困境我第一次做出终端原型的时候效果其实还挺唬人的让它读项目、跑测试、改一个函数都能顺利完成。可一旦任务变成“把这套支付模块从单线程改成异步批处理顺便把相关的单元测试补齐”它就开始力不从心。不是模型能力不行是组织方式出了问题。传统开发里人是靠经验干活的——遇到过类似问题、知道标准路径、知道哪里容易翻车。而早期的AI终端所有“经验”都堆在系统提示词里靠一段又一段的文字描述撑场面。提示词一长模型注意力迅速分散任务一复杂每一步都重新想根本没有积累。我在上下文管理那一篇提过这个问题当时用缓存和摘要硬扛但治标不治本。真正让“经验”产生复利效应的是把它从“跟对话绑定的临时文本”变成“独立于对话、可按需加载的结构化技能”。这就像老程序员不会把代码规范写进每封邮件而是放到团队文档里谁需要谁去查。1.2 “书架”隐喻让经验有安放的位置为什么用“书架”来比喻技能系统因为书架解决了三个物理问题分类、容量和可达性。分类很好理解现实中的书架按领域摆书代码的书不会扔到烹饪区。技能系统也一样如果不分类技能库膨胀到几十上百个以后匹配和检索都会变成灾难。容量指的是任何书架都有限度放不下的书得被淘汰这逼迫技能系统做版本管理、做取舍而不是无限堆砌。可达性最容易被忽略——书摆在书架上关键不是“有这本书”而是“你需要时能快速找到那一本”。技能系统最核心的指标就是查得准、调得快。所以我把技能系统的数据模型设计成三层书架技能领域书技能包章节技能包内的执行步骤。这三层分别对应技能系统的组织、存储和执行每一层都有自己的职责边界不能混在一层里做。1.3 技能系统要解决的核心问题一句话技能系统解决的是经验的存储、检索、执行、演进四件事。存储解决的是“经验写成什么样AI读完才会用”的问题。我的答案是“元信息步骤校验器”三段式详细结构在第三章给出。检索解决的是“任务来了怎么从几十个技能包里找到最合适的那一个”的问题。我最后采用的方案是“双层匹配”先是规则层的粗匹配再用模型对候选集做精排。纯靠关键词匹配太脆纯靠模型匹配又太慢两层结合效果好很多。执行解决的是“选对了技能怎么保证它不跑偏”的问题。答案是分章节执行每章有明确产出调度器做检查点错了只重跑当前章节而不是整个技能推倒重来。演进解决的是“技能会过时、会冲突、会被更好的方案替代”的问题。技能系统必须支持版本升级、灰度加载和回滚否则早晚有一天技能库会成为新的“烂代码”。这四个问题就是整个技能系统设计的地基后面所有方案都围绕它们展开。2. 书架—书—章节技能系统的三层数据模型2.1 书架按任务流而非技术栈划分领域书架是最高一层本质是一个领域标识。我的终端系统目前有四个书架代码开发、工程协作、运维部署、日常命令。注意我没有按技术栈分比如“Python书架”“Java书架”。为什么不按技术栈分按技术栈分书架直观但短命。一个技能包往往横跨多个技术栈比如“接口联调”既涉及前端调用代码又涉及后端接口定义归到哪边都不对。相比之下按任务流分书架更稳定“代码开发”里装的就是跟写码、改码、查码有关的一切“运维部署”里装的就是环境、构建、发布、监控相关的一切。任务流是人的工作习惯不容易变技术栈是工具换起来快。书架本身不需要实现什么逻辑它就是技能元数据里的一个字段比如 domain: code。但在目录结构上我建议把书架做成一级文件夹方便人工浏览和权限控制后面会给出目录结构示例。2.2 书籍技能包的四个组成部分书架下面是“书”也就是技能包。技能包是技能的完整定义我的基线是四个组成部分元数据、触发条件、执行步骤、校验规则。元数据name技能名最好是一个“动词短语”比如 safe_refactor安全重构、bisect_build构建二分定位。动词短语让技能职责清楚检索和对话触发都更自然。description一句话描述技能做什么、在什么场景用。这段文字会进索引也用于模型精排要写得像README一样能自解释。version语义化版本号技能包每次内容变更都该升版本。domain所属书架。触发条件这是技能能不能“被想起”的关键。我把它拆成两种显式触发用户在对话里直接说“用安全重构技能处理这个模块”或者命令里写了 safe_refactor。隐式匹配系统根据任务描述去技能库做匹配命中后进入候选列表。执行步骤就是书里的章节。每个章节包含标题、输入需要的参数、动作给模型的指令模板、预期产出、校验点。注意章节的执行不一定是“问一句模型”——它可以执行终端命令、读文件、改代码、跑测试模型只是其中一种执行后端。校验规则执行完怎么判断“这一步真的做对了”依赖校验器。比如安全重构技能的最后一步会跑一遍原有测试集校验器就是“测试是否全部通过”这一条布尔规则。2.3 章节小到可重试的执行单元为什么技能内部还要分章节思想跟项目分模块一样把大任务拆成小步骤每一步都可观察、可暂停、可回退。比如“安全重构”技能被我编排成四个章节analyze读取目标模块和依赖图输出现状分析plan基于分析结果生成重构方案列出涉及文件、改动范围、风险评估execute按方案执行代码改动每改完一个文件都做一次语法检查verify跑回归测试和静态检查确认无破坏章节编排有两个原则。第一章节之间要有“契约”前一章的产出比如 plan 章节输出的改动清单就是后一章的输入调度器只传递契约字段不传递聊天背景。第二章节要“小到可重试”任何一章失败不需要全部重来只需回到当前章节的检查点重新执行。我见过很多团队把技能步骤写成一整段提示词让模型自由发挥。短期省事长期吃亏——不可控、不可观察出了问题也没法定位到底在哪一步。章节化之后整个执行过程像一个流水线每一环都有记录。2.4 三大核心流程注册、发现、执行技能系统有三个核心动作贯穿始终。注册技能包写好后通过注册器解析并写入技能索引。注册器要做的事包括YAML语法校验、字段必填校验、版本冲突检查、技能名唯一性检查。注册成功后这个技能相当于“摆上了书架”。发现拿到一个任务请求先做匹配。我的匹配流程是对任务描述抽取关键词这块用规则加小型词典实现在索引里按 description 和 name 做关键词粗筛粗筛结果少于15个时交给模型对“任务描述 vs 技能description”做精排打分取分数最高的那个进入确认环节——如果用户设置了自动执行模式跳过确认执行调度器拿到选中的技能和执行参数按章节顺序执行。调度器同时维护一个执行上下文对象里面记录当前章节状态、已产生的契约字段、日志路径。执行完一章就把状态落盘这样如果中途重启能从最近的检查点恢复。这套流程写下来很简单但细节决定成败第三章我用具体代码演示一遍。3. 实操从零搭建技能包的目录、解析器与调度器3.1 技能包目录与文件结构设计技能包我按“一技能一文件夹”组织文件夹名就是技能名。整个技能库长这样skills/ code/ safe_refactor/ skill.yaml steps/ 01-analyze.md 02-plan.md 03-execute.md 04-verify.md project_scan/ skill.yaml steps/ 01-scan.md 02-report.md dependency_audit/ skill.yaml steps/ 01-list.md 02-graph.md 03-report.md ops/ deploy_check/ skill.yaml steps/ 01-check-env.md 02-run-migrations.md 03-rollback.md log_analyze/ skill.yaml steps/ 01-tail.md 02-pattern.md 03-report.md我把 skill.yaml 和 steps 分开放核心原因YAML负责元信息和逻辑编排Markdown负责给模型的指令文本。前者是机器读的后者是给模型读的混在一个文件里会让注册器的解析逻辑非常拧巴。skill.yaml 的样子name: safe_refactor version: 1.2.0 domain: code description: 对指定模块进行安全重构生成方案、执行改动并通过回归验证。适用于大型函数拆分、多处重复代码整理、跨文件接口调整等场景。 trigger: explicit: [safe_refactor, 安全重构, 重构] keywords: [重构, refactor, 提取函数, 拆分模块] steps: - id: analyze title: 现状分析 input: [module_path] output: analysis_report verify: 读取目标模块成功且生成依赖清单 - id: plan title: 生成重构方案 input: [analysis_report] output: refactor_plan verify: 方案中包含文件列表、改动类型、风险评估 - id: execute title: 执行改动 input: [refactor_plan] output: changed_files verify: 全部改动文件通过语法检查 - id: verify title: 回归验证 input: [changed_files] output: verify_result verify: 原有测试集通过率 100%注意我给每个章节都定义了几个字段。input 是这一章节启动前需要的实例化参数名output 是这个章节结束时要写回执行上下文的契约产物名verify 是人类可读的校验描述调度器会把它翻译成具体的检查指令或规则。3.2 技能清单解析Loader实现细节有了文件结构第一步要做的是把 skill.yaml 解析成内存里的技能对象。我用 Python 实现核心函数不复杂但有一些边界处理必须做。# skill_loader.py import os import yaml from dataclasses import dataclass from typing import Dict, List, Optional dataclass class SkillStep: id: str title: str input: List[str] output: str verify: str prompt_path: str # 对应steps下的markdown文件 dataclass class Skill: name: str version: str domain: str description: str trigger_keywords: List[str] steps: List[SkillStep] path: str class SkillLoader: def __init__(self, skills_root: str): self.skills_root skills_root self.skills: Dict[str, Skill] {} def load_all(self) - None: for domain_dir in os.listdir(self.skills_root): domain_path os.path.join(self.skills_root, domain_dir) if not os.path.isdir(domain_path): continue for skill_name in os.listdir(domain_path): skill_dir os.path.join(domain_path, skill_name) yaml_path os.path.join(skill_dir, skill.yaml) if not os.path.isfile(yaml_path): continue skill self._load_one(skill_dir, yaml_path) if skill: self.skills[skill.name] skill def _load_one(self, skill_dir: str, yaml_path: str) - Optional[Skill]: with open(yaml_path, r, encodingutf-8) as f: data yaml.safe_load(f) required {name, version, domain, description, steps} missing required - set(data.keys()) if missing: print(f[skill_loader] {yaml_path} 缺少字段: {missing}) return None steps [] for idx, step in enumerate(data[steps], start1): step_id step.get(id, fstep{idx}) md_filename f{idx:02d}-{step_id}.md prompt_path os.path.join(skill_dir, steps, md_filename) steps.append(SkillStep( idstep_id, titlestep.get(title, ), inputstep.get(input, []), outputstep.get(output, ), verifystep.get(verify, ), prompt_pathprompt_path, )) return Skill( namedata[name], versiondata[version], domaindata[domain], descriptiondata[description], trigger_keywordsdata.get(trigger, {}).get(keywords, []), stepssteps, pathskill_dir, )这段代码有两个细节我想强调。其一步骤文件的命名是 01-analyze.md 这种带序号的形式。不要小看这个序号它保证了章节在磁盘上的自然排序让人在文件夹里肉眼扫一遍就知道执行顺序也方便后续做断点恢复时快速定位。其二解析时做了必填字段校验。技能加载最怕的就是“静默失败”一个技能YAML少了字段注册器没报错整个系统运行到一半才发现这个技能是残废的。所以我在 _load_one 里明确打印了缺失字段宁可让注册器失败率高一点也不放过有问题的技能包。3.3 调度器与执行上下文把步骤跑起来技能解析完还不算完真正的核心是调度器。调度器负责“拿着技能定义一步一步把它跑起来”。# skill_scheduler.py import os import json import time from dataclasses import dataclass, field from typing import Any, Dict dataclass class ExecutionContext: skill_name: str params: Dict[str, Any] outputs: Dict[str, Any] field(default_factorydict) run_id: str field(default_factorylambda: frun_{int(time.time())}) log_path: str current_step: str class SkillScheduler: def __init__(self, executor, max_retries2, step_timeout120): self.executor executor # 注入通用的执行器模型代理或终端命令Runner self.max_retries max_retries self.step_timeout step_timeout def run(self, skill: Skill, params: Dict[str, Any]) - Dict[str, Any]: ctx ExecutionContext(skill_nameskill.name, paramsparams) ctx.log_path flogs/{ctx.run_id}.jsonl os.makedirs(logs, exist_okTrue) for step in skill.steps: ctx.current_step step.id self._log(ctx, step_start, step.id) result self._execute_step_with_retry(ctx, step) if result[status] failed: self._log(ctx, step_failed, step.id, errorresult[error]) return {status: failed, failed_step: step.id, error: result[error]} # 章节契约只把output指定的字段写回上下文 ctx.outputs[step.output] result[payload] self._log(ctx, step_done, step.id, outputstep.output) return {status: success, outputs: ctx.outputs} def _execute_step_with_retry(self, ctx, step): last_error None for attempt in range(self.max_retries 1): try: payload self.executor.execute( skill_namectx.skill_name, step_idstep.id, prompt_pathstep.prompt_path, inputs{k: ctx.outputs.get(k) if k in ctx.outputs else ctx.params.get(k) for k in step.input}, timeoutself.step_timeout, ) return {status: ok, payload: payload} except Exception as e: last_error str(e) self._log(ctx, step_retry, step.id, attemptattempt, errorlast_error) return {status: failed, error: last_error} def _log(self, ctx, event, step_idNone, **extra): rec {run_id: ctx.run_id, ts: time.time(), event: event, step: step_id} rec.update(extra) with open(ctx.log_path, a, encodingutf-8) as f: f.write(json.dumps(rec, ensure_asciiFalse) \n)调度器设计里我坚持了一个原则执行器是注入的不是内置的。这个 executor 可以是“调LLM接口的代理”也可以是“执行终端命令的Runner”甚至可以是“读文件、跑git命令的工具集”。技能系统不关心执行器是谁只关心 execute 方法返回什么结构。这让技能既能指挥模型干活又能指挥Shell干活两者还能混在同一个技能的不同章节里。关于执行上下文我采用了“只写契约字段”的策略。step.input 里声明了需要哪些参数调度器就去 ctx.params用户传入的初始参数和 ctx.outputs前序章节的产物里取不会把整个对话历史或上一个技能的内部中间状态塞给当前章节。这一步是严防上下文污染的关键实话说踩了不少坑后面专门讲。3.4 沉淀经验的标准流程复盘到发布架构和代码都有了接下来是我自己沉淀经验的标准套路。每次给系统加新技能我都走五步。第一步复盘。做完一个任务先在执行日志里翻“哪里绕了路、哪里返工了、最终哪条路径是好的”。比如有一次手动排查一个构建失败问题最后发现是环境变量冲突但排查过程绕了一大圈。复盘结论就是下次这类问题应该先查环境差异再查代码改动。第二步提炼步骤。把复盘结论变成可执行的步骤序列。这一步的关键是把“经验”翻译成“动词对象检查点”的格式。比如“检查环境差异”太模糊拆成“对比目标环境与本地环境的变量清单产出差异表”才算一个合格的章节。第三步固化参数。这个技能启动时需要调用方提供什么以“构建失败排查”技能为例参数就是项目路径、失败日志路径、最近改动文件列表。第四步写校验器。每个章节都要回答一个问题怎么知道这一步做对了校验器不一定是自动化脚本也可以是一条“模型自检指令”——比如让模型核对“差异表中的项目是否与失败日志中的报错模块一一对应”。有自动化判断就优先自动化没有就先占个坑后面补。第五步注册发布。把技能包丢进 skills 目录运行 load_all看有没有报错再用一个测试任务跑一遍最后在终端里模拟触发一次确认匹配和确认流程都正常。这五步看着简单但真正做到位很花功夫。尤其是第一步复盘很多人跳过直接把网上抄来的“最佳实践”塞成技能结果技能库变成了大杂烩匹配率一塌糊涂。经验是给自己系统用的不是给别人看的必须来自真实踩坑。4. 踩坑实录技能冲突、超时、上下文污染与回滚策略4.1 技能冲突与优先级之争技能库一多最头疼的就是“多个技能抢一个任务”。举个例子我先后写了 deploy_check部署前检查和 env_diff环境差异分析。结果有一次任务描述是“帮我看一下测试环境为什么连不上数据库”两个技能都跳出来了一个从部署角度查一个从环境变量角度查用户根本不知道该选哪个。我的解决方案分三层。第一层规则层提高门槛。每个技能包里的 trigger.keywords 不能只写宽泛的词要写成“限定语境短语”。env_diff 的关键词从“环境”改成“环境变量差异、env 对比、配置差异”deploy_check 改成“部署检查、发布前检查、构建产物验证”。关键词越具体粗筛命中率越准。第二层模型精排。这一步我会构造一个评选提示把任务描述和候选技能的 description 一起给模型要求它按“解决任务的可能性”排序。精排结果通常能拉开差距。第三层兜底确认。只要候选技能中前两名的精排得分差距不大调度器就停下来问用户“是想做A还是B”别怕打断模棱两可的时候硬猜才是最大的风险。4.2 执行超时与死循环的治理技能执行到一半卡住的情况我遇到得比预料中多。最典型的是让模型跑 plan 章节的时候它反复改写方案越写越长直到逼近上下文窗口极限。这个问题的根源是模型天然喜欢“多干活”来显得认真但技能系统需要的是“干对活”。我的对策是三条。步骤超时每个章节都有 time_limit默认我用120秒超过就终止记日志。后来发现处理超大仓库的分析类章节不够单独把这个技能的超时调到300秒。最大重试次数目前设2次超过直接失败。2次的意义是既允许模型改正一次明显错误又不会让失败的环节无限消耗资源。输出截断守护在executor里对模型返回做长度限制plan章节的输出超过20条改动项就强制让模型合并、压缩逼它做结论而不是罗列细节。死循环的另一个隐蔽来源是“模型自问自答型”章节提示词里写得太开放“寻找更多的潜在风险”这种话会让模型在分析章节里无限挖细节。这类问题要从源头治提示词必须限定产出物的大小和格式。4.3 上下文污染“串味”的根因与隔离方案上下文污染是我踩得最深的一个坑具体表现是执行第二个技能时模型忽然引用第一个技能的中间结果或者把一个项目的路径当成另一个项目的路径甚至把上一次重构里的“临时文件清理”直接带进这次部署的指令里。原因很明显早期实现里为了让对话连续我把所有技能执行的完整记录都塞给了模型。模型分不清哪些是“当前任务的合法上下文”哪些是“上一次任务的残留信息”。我现在的处理策略是“上下文按需注入”每个技能章节执行前只注入三样东西章节提示词、契约字段里声明的输入、技能启动时的用户参数。对话历史只保留“用户意图级别的摘要”不保留技能执行的技术细节。执行日志独立记到 run_id 对应的 jsonl 文件里供排查用但不进模型上下文。这套策略下来“串味”问题基本绝迹。代价是模型失去了部分“跨技能联想”的能力但我觉得这个代价值得——在终端系统里确定性和隔离性比发散性重要得多。4.4 版本化加载、灰度配置与一键回滚技能是活的必然要更新。但更新带来的最大风险是“一个技能改坏了拖累了整个系统”。我用的方法版本化加载。skill.yaml 里有 version 字段注册器在加载时对同名技能的版本做比较旧版本不覆盖新版本时保留二者索引里使用最高版本。这样发布新版本后如果发现不对随时切回旧版本。灰度加载。把技能库拆成两个配置stable 库和 test 库。新技能先进 test 库加载时通过环境变量指定 SKILL_PROFILEtest确认跑稳了再切到 stable。我在跑批任务时遇到过bug真是靠这个配置救了命。一键回滚。每次 load_all 之前注册器会自动备份当前技能索引到 backups 目录。一旦发现加载后系统异常直接把索引恢复到上一次快照十秒钟完成。这套回滚机制不复杂但价值极高。技能系统的本质是“经验沉淀”经验沉淀的前提是“敢于实验”。如果没有回滚保底每次更新都战战兢兢很快就会停止新增技能书架就僵死了。4.5 常见问题速查表问题典型表现排查思路预防与对策技能匹配冲突多个技能响应同一任务查看匹配日志中的候选技能与评分关键词限定语境 模型精排 人工确认兜底执行超时章节长时间无产出查看run日志的 step_retry 记录设置步骤超时 最大重试次数 输出截断守护上下文污染模型引用先前技能残留信息对比当前章节输入是否混入无关字段契约字段隔离 按需注入 历史摘要化更新后异常加载技能后系统崩溃查看注册器报错与技能索引差异版本化加载 灰度配置 一键回滚其实做技能系统这套东西我个人最大的感受是它不是一个“加功能”的工程而是一个“改习惯”的工程。以前我写AI终端想的都是“再给我加一个能力”现在我第一个念头是“这个经验能不能沉淀成技能下次自动用上”。书架装的不只是代码还有我自己对任务的判断方式。如果你也在做自己的AI工具建议从小处开始先挑一个你每周都会重复做、但每次都靠手工翻日志去解决的任务把它按“复盘—提炼—固化—校验—发布”走一遍做成第一个技能。不要贪多先让一个技能真正好用地跑两个星期再慢慢加书架。技能系统的价值不是一蹴而就的而是随着书架里的书一本本变厚系统才越来越像你的老搭档。
返回列表