ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent技能体系实战:从提示词驱动到可插拔可调试的工程化落地

Agent技能体系实战:从提示词驱动到可插拔可调试的工程化落地 前阵子不是流行一句话嘛大模型是大脑工具是手脚。真到自己动手给 Agent 搭能力的时候才发现光有大脑可不够手脚得一根根手指头接上去而且每根手指头怎么动、什么时候动、力气用多大全是细节中的细节。这个项目代号叫agent-skills核心就做一件事为 Agent 建立一套可插拔、可调试、可观察的技能体系让模型不再只是会聊天而是真正能干活。这篇文章我把从设计到实现再到踩坑的完整过程捋一遍给正在做 Agent 落地的朋友一份真实可参考的实战记录。1. 为什么需要一套技能而不是一组提示词很多人一开始的想法是既然大模型这么聪明我把需求写清楚不就行了让他自己生成代码、自己调函数、自己看日志听起来很美。但真跑起来就会发现纯靠提示词驱动 Agent会遇到三个绕不过去的坎。第一个坎是决策不稳定。同一个需求Ask Agent 十次他可能五次选择写脚本、三次选择调 API、两次干脆问你请问您想怎么做。不是模型不行而是没有边界。技能体系本质上是给 Agent 划定了你能干什么、不能干什么的明确边界把决策从开放式命题变成有限选项的匹配。第二个坎是行为不可复核。提示词驱动的 Agent 在执行过程中经常自由发挥用了哪个函数、传了什么参数、为什么这样处理回头看全是一团黑盒。出问题别说回滚连复现都难。而技能体系要求每个技能都有明确的输入输出、执行逻辑和错误处理每一次调用都留下可追踪的日志这才符合工程化的底线。第三个坎是能力无法沉淀。你调了三天三夜、好不容易让 Agent 学会了一种业务处理流程结果换个场景、换个项目这些经验就全丢了每次都要从头开始调。技能就不一样它把怎么做好一件事固化成了代码和配置一次封装、处处复用这才是真正的资产积累。所以我的判断很明确Agent 的底层是模型但 Agent 的上限是技能。提示词解决说什么技能解决做什么。agent-skills项目就是围绕这个判断做的整套实践目标只有一个——让 Agent 稳定、可靠、可维护地把活干完。1.1 技能、工具、插件、工作流先厘清概念我见过不少团队四个人聊 Agent 架构聊到一半才发现每个人说的工具根本不是一回事。这里先把我自己的定义放出来后面的内容都基于这套划分。概念我的定义典型例子工具Tool最细粒度的原子操作单个动作无状态发一个 HTTP 请求、读一个文件、执行一条命令技能Skill面向一个完整任务目标的操作编排可以组合多个工具有输入输出约定可复用代码仓库巡检、自动化测试执行、发布前联检插件Plugin一组相关技能/工具的打包分发单元包含配置和元数据某个云厂商的整套操作能力打包成一个插件工作流Workflow确定的、固定的流程编排分支条件预先定义不允许模型自由发挥每天凌晨的数据清洗定时任务这里我特别要强调的是技能恰好站在工具和工作流之间的黄金位置。它比工具高一层能编排多个原子操作又比工作流灵活把什么时候调哪个工具、怎么组合这部分决策留给模型。换句话说技能是模型与工具之间最好的缓冲层上面让模型做决策下面隔离掉工具的复杂性双方都不用去适应彼此的撕裂。2. 技能设计的底层拆解一个技能应该由什么构成agent-skills走了不少弯路之后最终沉淀出的技能结构包含四个核心部分元信息、触发条件、执行逻辑、结果标准化。缺一个技能的可用性和可维护性都会明显掉档。元信息是技能的外衣包括技能名称、描述、版本、作者。名称要短描述要长而精准因为对大模型来说描述就是技能的门面门面不吸引人、不明确Agent 就不会在正确的时机选它。这一块我用一个词概括给自己看的是代码给模型看的是描述。描述写不好技能再厉害也是深巷里的酒。触发条件这件事特别值得念叨一下。大部分 Agent 框架默认是模型自主决定是否调用技能但实际场景里完全依赖模型自觉是不行的。我最后采用的做法是给每个技能标注两种触发模式一种是建议触发场景写在描述里让模型自己判断另一种是强制前置依赖由框架层在流程启动时检查比如部署技能必须前置执行构建技能这是流程刚性要求不能交给模型临场决定。执行逻辑相对直白就是技能真正要干的活。但这里有个血泪教训技能内部不要追求大而全的通用处理要按场景预期收窄逻辑。比如一个获取服务器信息的技能别想着适配所有操作系统先明确支持 Linux遇到 Windows 直接返回不支持并说明原因比勉强跑一遍然后输出乱码强得多。最后是结果标准化。技能跑完返回给模型、返回给上层调度的数据格式必须稳定统一。我的做法是所有技能统一返回一个结构体包含 status成功/失败/部分成功、data业务数据、error错误信息、meta耗时、日志链接等元信息。这是agent-skills里最不起眼但价值最高的设计——它让上层编排不用为每种技能写特殊的解析逻辑。2.1 技能粒度切粗了是摆设切细了是灾难技能粒度设计是agent-skills里最反复纠结的一个问题。一开始我走了两个极端先是一股脑把功能切得特别细读文件一个技能、写文件一个技能、列出目录一个技能——结果模型选技能选到崩溃一个简单的任务要串五六个技能接着又走了另一个极端一个自动化测试大技能把所有事全包了——结果技能内部成了浆糊改一个判断逻辑都要翻上百行代码。试错之后我的结论是技能的粒度要看任务的连贯性。如果一个任务的多个步骤放在一起有明确的先后顺序和共同目标就应该是一个技能如果步骤之间可能被单独复用或者可能由不同的技能替代就应该切分。举两个具体例子。代码仓库巡检我做成一个完整技能内部封装了拉取最新代码、运行静态检查、执行单元测试、收集覆盖率四个步骤。因为这几个步骤的目标只有一个——判断仓库健康状况拆成四个独立技能不仅增加调度开销还会引入中间状态管理的问题。反过来获取数据库 Schema和生成 CRUD 代码我切成了两个独立技能。因为获取 Schema 这个动作在写文档、做数据迁移、做查询优化等多个场景都会用到它本身有独立价值生成代码则是一个更大粒度的任务可以在内部调用获取 Schema 作为子操作。2.2 技能描述这是写给模型看的说明书技能描述怎么写是agent-skills里提升效果最立竿见影的部分。我刚开始写描述跟写文档似的根据用户需求执行适当操作完成目标结果模型在多个技能之间犹豫不决。后来我把描述改成了这种风格repo_inspect: 对指定代码仓库执行全面的健康巡检。 适合在以下场景调用 - 用户要求评估代码质量 / 准备发布前检查 / 想了解仓库状态 - 作为发布工作流的前置步骤 - 需要获取测试覆盖率、静态检查结果时 不适合的场景 - 用户只是问某个文件的内容应该直接读文件 - 用户需要修改代码逻辑应该走代码修改流程 执行时会依次运行拉代码 - 静态检查 - 单测 - 覆盖率收集 总耗时通常在 10 分钟内请提醒用户耐心等待这一改动让模型选对技能的比例显著提升。原理也简单模型在匹配技能和用户意图时本质上是在做语义匹配描述里明确写清楚适合什么场景、不适合什么场景、会做什么事、要多长时间相当于直接给了模型一个填空题的答案选项命中率自然高。我总结出一个公式好描述 动词开头 目标说明 适用场景 不适用场景 预期行为 预期耗时。3. 手把手实现一个技能仓库巡检技能从零到上线纸上谈兵够了直接看代码。我用代码仓库巡检这个技能做完整拆解把这个技能从定义到注册的全部过程走一遍。这个技能也是agent-skills项目里第一个真正落地的技能后续很多技能都是照着它的骨架长出来的。3.1 第一步定义技能 Schema所有技能的第一件事是定义 Schema它同时是给模型看的接口说明、给框架看的注册信息、给调用方看的数据契约。仓库巡检技能的 Schema 长这样{ name: repo_inspect, version: 1.2.0, description: 对指定代码仓库执行全面的健康巡检涵盖静态检查、测试执行与覆盖率收集, tags: [code-quality, pre-release, testing], parameters: { type: object, properties: { repo_path: { type: string, description: 本地仓库的绝对路径 }, branch: { type: string, description: 要巡检的分支默认当前分支, default: }, strict_mode: { type: boolean, description: 严格模式开启后静态检查告警也视为不通过, default: false } }, required: [repo_path] }, timeout_seconds: 900, allowed_actions: [git_pull, run_lint, run_tests, collect_coverage] }注意几个细节。timeout_seconds我设成 900 秒因为一次完整巡检包括拉代码、跑测试太小的超时直接导致任务半途而废allowed_actions是权限声明框架层会依据这个字段拦截技能内部尝试执行的未授权命令这是安全兜底不然后面模型被提示词注入技能就成了攻击通道。parameters里每个字段都有 description这也是写给模型看的。模型要根据用户的话去填这些参数如果 description 写分支名模型不知道分支名可以从哪来但写成要巡检的分支默认当前分支模型就能在用户没说分支时直接留空。3.2 第二步实现技能核心逻辑技能的实现我放在独立目录里一个技能一个目录做到技能内部高内聚、技能之间零依赖。核心代码挺直白# skills/repo_inspect/skill.py import subprocess import time from dataclasses import dataclass, field from typing import Optional dataclass class SkillResult: status: str # success | failed | partial data: dict field(default_factorydict) error: Optional[str] None meta: dict field(default_factorydict) def execute(repo_path: str, branch: str , strict_mode: bool False) - SkillResult: started time.time() steps [ (pull_code, lambda: pull_code(repo_path, branch)), (run_lint, lambda: run_lint(repo_path, strict_mode)), (run_tests, lambda: run_tests(repo_path)), (collect_coverage, lambda: collect_coverage(repo_path)), ] # 支持断点续跑记录已完成的步骤失败时返回部分结果 finished_steps [] for step_name, step_func in steps: try: step_func() finished_steps.append(step_name) except Exception as e: return SkillResult( statuspartial if finished_steps else failed, data{finished_steps: finished_steps, failed_step: step_name}, errorstr(e), meta{duration_ms: int((time.time() - started) * 1000)} ) return SkillResult( statussuccess, data{ lint_summary: read_lint_summary(repo_path), test_summary: read_test_summary(repo_path), coverage: read_coverage(repo_path), }, meta{duration_ms: int((time.time() - started) * 1000)} )这个实现有几个值得留意的设计决策。一是返回SkillResult而不是让函数内部自己抛异常或者直接打印日志这样上层拿到的是一个结构化的结果对象格式化展示、存日志、喂给模型都顺手。二是断点续跑思路技能跑一半挂掉时返回已完成步骤列表和失败步骤模型可以基于这个信息判断是否需要重试整个技能还是只重试失败的步骤这个信息后续在编排里特别有用。三是四个内部步骤各自独立成函数每个函数内部都有 try/except 包裹并抛出带上下文的异常。比如run_lint失败异常信息不是简单的lint failed而是静态检查执行失败退出码 1输出见 /tmp/repo_inspect/lint_20250315.log。这样 Agent 看到错误信息就知道下一步该去看日志。3.3 第三步注册与接入 Agent 运行时技能实现的再完整不注册进 Agent 的运行时也白搭。agent-skills的注册机制是在一个统一清单文件里声明技能路径运行时动态加载# skills/registry.yaml skills: - name: repo_inspect entry: skills/repo_inspect/skill.py enabled: true - name: gen_crud_code entry: skills/gen_crud_code/skill.py enabled: true # ... 其他技能运行时启动时读取这个清单动态导入每个技能的execute函数并把技能的 Schema 注入到模型上下文里。这里我踩过一个坑一开始把全部技能都一股脑注入技能多了之后模型上下文暴涨选择准确率反而下降。后面改成技能分组加载基础技能常驻领域技能按需加载。比如模型收到一个跟发布相关的任务提示词时框架层先通过关键词匹配把发布相关的技能集注入其他技能暂时不可见。用这种方式控制上下文规模效果立竿见影。接入之后整个链路就是用户说一句帮我看看这个仓库能不能发版 - 模型看到可用的repo_inspect技能识别出意图填入参数repo_path/data/myproject- 框架层解析模型输出调用技能执行函数 - 技能跑完返回SkillResult- 框架把结果格式化后交回模型 - 模型基于结果生成最终回复。整个链路中模型负责决策技能负责执行框架负责连接三者各司其职可替换性也最好。4. 技能编排从单技能到多技能协作单个技能解决了单点任务但真实业务场景里Agent 通常要连续完成多个任务。agent-skills在技能协作这部分的设计核心就一句话把技能当函数把 Agent 当调度器。模型的角色不是执行者而是调度者负责拆解任务、决定调哪个技能、按什么顺序调、怎么处理技能返回的结果。4.1 串行编排发布前联检组合串行是最常用也最简单的编排模式。我实际使用频率最高的一个组合是发布前联检一共串了四个技能repo_inspect跑代码健康检查确认测试和静态检查都通过config_check检查配置文件确认环境变量、依赖版本、数据库连接串都正确db_migration_plan生成数据库迁移脚本的预演执行计划build_artifact构建发布产物并生成校验和模型在收到准备发布的指令后会按照技能描述中建议的顺序逐个调用。但要注意这里不能完全靠模型自觉我在框架层加了一个依赖检查config_check依赖repo_inspect的检查结果如果前者失败后面的技能直接跳过不给模型发挥我觉得还能继续的空间。这是流程刚性不是建议。# 伪代码示例串行编排示意 pipeline [ SkillCall(repo_inspect, {}), SkillCall(config_check, {}), SkillCall(db_migration_plan, {}), SkillCall(build_artifact, {}), ] result SkillResult(statussuccess, data{}) for call in pipeline: result orchestrator.execute(call.name, call.params) if result.status failed: result SkillResult( statusfailed, data{**result.data, blocked_by: call.name}, ) break这里的核心心得是串行编排必须显式定义失败短路逻辑。没有短路的话模型可能会在第一步失败后继续往下走最后提交了一个带着已知缺陷的发布这在真实场景里是绝对不可接受的。虽然这增加了编排代码的复杂度但换来的是整体行为的确定性值得。4.2 并行编排与任务分解有些技能的耗时大头在网络 IO 或外部系统等待技能之间互不依赖这时候并行执行能把整体耗时要缩短好几倍。举一个真实例子。用户问这几个服务最近一次发布后有没有异常我拆成三个技能并行跑log_anomaly_scan扫日志找异常模式、metric_review拉取监控指标找基线偏移、incident_record_query查历史故障记录。三个技能各自独立没有依赖关系并行跑最后模型汇总三份结果形成一个综合判断。并行编排实现上要注意的是资源限制和结果汇总顺序。资源限制是指系统同时只能跑 N 个技能不能让用户一个问题触发 20 个技能并发把机器打爆我设置的默认上限是 5。结果汇总顺序则是说并行跑完的结果给到模型之前要按任务相关性排序不能无序堆在一起否则模型总结时的逻辑会很乱。# 伪代码示例并行编排示意 skills [ SkillCall(log_anomaly_scan, {service: service_name}), SkillCall(metric_review, {service: service_name}), SkillCall(incident_record_query, {service: service_name}), ] results orchestrator.run_parallel(skills, max_concurrency5) summary model.analyze_results(results)4.3 状态管理技能之间的数据怎么传技能编排最难的不是调度是状态管理。技能 A 的输出怎么变成技能 B 的输入如果技能 B 执行失败退出技能 A 的结果要不要回滚agent-skills的答案很朴素但在我这儿很管用技能之间不直接传对象统一通过一个轻量的工作区来交换数据。每个技能执行前会从工作区读取它需要的前置产物执行后把产物写回工作区。工作区本质是一个带版本管理的目录每个文件都记录了是哪个技能的哪个实例在什么时间写入的。这个设计的最大好处是可检查、可恢复。技能 B 启动时发现需要的中间产物不存在或者版本不匹配直接抛出前置条件不满足的错误而不是用空数据硬跑然后在业务逻辑里崩溃。排查问题的时候打开工作区目录每个文件的进出记录一目了然谁污染了数据一查便知省去了一大半吵架时间。5. 真实运行中踩过的坑和排查链路任何框架看文档都光鲜跑起来全是坑。这一章把agent-skills上线后遇到的高频问题、排查思路和修复方案完整记录一下这些问题到现在还会偶尔冒出来按下面的链路排查基本能解决九成。5.1 技能超时模型还等着回复技能却悄悄挂了第一个遇到的坑是技能超时。最开始把超时设得比较保守比如仓库巡检设 120 秒想着不会超才对。结果遇到一个大型 monorepo光拉取代码就跑了三分钟技能在拉代码阶段就被框架掐断了返回一个执行超时的错误。模型拿到错误信息后又尝试重新跑了一遍结果还是超时直接陷入死循环用户看到的是 Agent 反复报错。排查链路倒是不复杂第一看技能日志发现记录显示超时发生在git_pull阶段第二看 git 操作日志发现拉取的是一个巨大的历史仓库而且工作区没有做浅克隆第三确认根因是技能对仓库大小没有预估直接用默认的全量克隆策略。修复方案做了两层。第一层在技能内部把 git 拉取改成--depth 1浅克隆只取最新提交几十倍的体积差异时间立刻从三分钟降到十秒第二层在框架层把超时做成可配置的技能 Schema 里声明预估超时同时框架设置一个硬上限防止恶意长任务。通过这个问题的排查我学到的原则是框架层的超时是最后一道保险不能当常规路径用每个技能都要对自己的耗时负责主动优化自己的执行策略。5.2 上下文污染上一次会话的脏结果混进了本次决策第二个坑更隐蔽。Agent 在连续会话中会把历史对话内容一起塞给模型做上下文这本是为了让对话连续但如果上一次技能执行返回了一个报错信息这个报错可能会被模型误以为是对当前问题的回答。我遇到过的最典型的场景长这样用户先问这个服务怎么还没恢复Agent 查了监控技能返回服务状态异常错误率 32%。然后用户又问刚才那个问题查到原因了吗本来应该触发新的日志分析技能但模型看到上下文里已经有错误率 32%这个信息直接从历史返回提取状态没有调用任何技能给了个回復错误率 32% 说明服务负载过高建议扩容。听起来像那么回事但实际上根本没有查日志确认根本原因。排查链路第一步是看 Agent 的调用记录看模型有没有触发技能调用。结果发现这次回答的调用记录是空纯粹靠上下文回答。第二步检查上下文中注入的历史结果发现上次技能执行的原始结果确实还在上下文窗口内。第三步确认 root cause 是框架层没有对技能的历史输出做时效性标记和清理策略。修复方案是双管齐下。一是框架层在把历史技能结果注入上下文时加上时间戳并标记该结果来自 X 分钟前的技能调用可能已过期二是模型指令里明确要求当问题涉及最新状态时必须重新触发技能获取新数据不能直接引用历史结果回答。做了这两处修改后这类拿旧数据回答新问题的错误明显减少。5.3 模型幻觉参数给技能传了不存在的参数技能定义得再清楚模型有时候依然会自由发挥。有一次用户说帮我把测试跑一下重点看下 API 测试的覆盖情况模型调用repo_inspect时自动填了一个api_coverage_only: true的参数但技能的 Schema 里根本没有这个参数。框架层的处理是直接抛参数校验错误然后模型收到错误后又换了个参数名重试反复多次把用户晾在那里等。这个问题的根子在于框架层没有为模型提供参数校验失败后的修正引导。修复后的流程是参数校验失败时返回的错误信息不再只是一句参数 api_coverage_only 非法而是加上合规提示repo_inspect 支持的参数有 repo_path、branch、strict_mode。你是想限制 API 测试的覆盖范围吗请使用 strict_modefalse 并额外调用 coverage_report 技能。这样一来模型拿到错误信息后就知道自己自作聪明了应该用合规的参数重新表达用户意图重试的成功率大大提高。这里我总结的心得是错误信息不只是给人类看的更是给模型看的。错误信息里包含的修正引导越具体Agent 的自恢复能力就越强。5.4 技能执行的权限边界Agent 能跑的危险命令比想象中多这是一个安全相关的坑必须单独说。一开始我的技能模板里允许执行任意 shell 命令想着反正跑在容器里问题不大。后来有一次技能在解析外部输入时被塞入了一段恶意命令因为技能内部直接拼接字符串执行结果触发了容器里的一个敏感操作。那次之后我彻底重构了权限管理。现在的权限模型是三层第一层是框架层明确规定 Agent 只能执行已注册技能内的操作未注册的命令直接拒绝第二层是技能层每个技能的 Schema 里声明allowed_actions框架启动时校验技能内部只能调用声明的动作想执行 Shell 命令必须通过一个受限的run_command帮助函数这个函数会校验命令白名单和参数合法性第三层是沙箱层技能运行在独立的容器里没有宿主机挂载目录访问权限也没有生产环境密钥。这套三层权限模型上线后再也没有出现过 Agent 跑飞 的情况。6. 技能体系上线之后维护、复盘与持续演进技能开发出来不是终点能持续稳定地跑下去才是目标。agent-skills跑了一段时间后我把精力分成了三块可观测性建设、技能健康度评估、技能版本演进。6.1 可观测性与日志体系之前排查问题的经验反复告诉我一件事没有日志排查就是猜。所以后面我给整个技能体系架设了一个统一的日志规范。每个技能执行时都会输出一份结构化日志包含执行时间、调用参数、每步耗时、结果状态、错误堆栈。日志收集到统一的展示面板上可以直接看到每一个技能调用的完整链路。这套日志体系最大的受益场景是模型表现很蠢但我不知道蠢在哪的时刻。以前遇到这种情况只能盲猜现在打开追踪面板一眼就能看到模型在这次任务里选了哪些技能、顺序对不对、哪个技能耗时长、哪个技能报错了、模型看到报错后又怎么处理。链路清晰了问题定位就快多了。6.2 技能健康度不是所有技能都值得留上线时间久了技能会越加越多有些技能可能一个月都没被调用一次有些则天天被调用但成功率不到五成。我建立了一套简单的健康度评估机制每个技能记录调用频率、成功率、平均耗时三个指标每周汇总一次。调用频率低说明技能没有解决真实痛点要么描述不吸引模型导致不被选中要么场景本身就不需要技能化成功率低说明技能的实现有 bug 或者和现实环境的兼容性差平均耗时长则说明瓶颈可能在技能内部的串行步骤上。根据这个评估我会做三种处理优化、下线、拆分。调用次数高但成功率低的技能优先优化连续一个月零调用的技能直接下线避免干扰模型的选择空间耗时长的技能检查内部步骤是不是可以并行化。这套评估机制让技能库始终保持着健康状态避免了尸体技能堆积导致的选择混乱。6.3 技能版本与经验固化技能也是有版本的。一个技能从开发到稳定通常会经历三个版本阶段V0 是先跑通功能不管代码好看不好看目标是让任务能完成V1 是加固错误处理把所有能想到的异常路径都覆盖到把失败时的错误信息写得更细V2 是性能优化和参数收敛把明显不合理的耗时点修掉去掉那些从来没被用过的参数把接口收窄到够用但不过度设计。我自己个人体会最深的一点是技能的演进不能靠想起来再改要靠线上日志驱动。每次看到某个技能出错日志多就去修一版然后看看下一周的出错率降了多少。演进方向永远来自真实运行数据的反馈而不是自己的凭空想象。这套数据驱动迭代的开发方式让我后面维护技能库的精力投入少了很多效果却比拍脑袋改要好太多。agent-skills做到现在真正让我觉得有成就感的不光是把 Agent 调得更会干活了而是我批量复制这种会干活的能力越来越熟练了。每沉淀一个新技能Agent 的能力边界就向外扩展一点而扩展边界这件事本身已经变成了一套有章法的工程流程不是靠天吃饭的玄学。这份记录如果能给正在做同类事情的朋友一些参考或者少踩几个我踩过的坑那就挺值了。
返回列表