ARTICLE DETAIL

资讯详情

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

从提示词到技能:构建稳定可靠的LLM Agent执行模块

从提示词到技能:构建稳定可靠的LLM Agent执行模块 这两年做LLM Agent相关的工作我最大的一个感受是模型能力已经不太是瓶颈真正拉开差距的反而是那些看起来不起眼的工程细节——比如 Agent 会不会用工具、怎么用工具、用错了能不能自己纠正。好模型可以快速学会套路但如果任务执行本身就是一团乱麻再聪明的脑袋也没用。 agent-skills 这个话题本质就是在解决这件事把任务执行能力标准化、模块化让 Agent 不只是会接话而是真的能把一件事从头到尾干完并且干得可以预期。这篇文章我会从技能的定义开始拆解一个技能应该包含哪些部分、怎么落地实现、怎么接进主循环最后把我在实践里踩过的坑也一并交代清楚。无论你是在搭第一个 Agent还是在优化已经跑起来的系统这套思路都可以直接拿来做参照。1. 把概念先对齐Agent Skill 到底是什么我们每天说“给 Agent 写一段提示词”但提示词是一种很原始的表达。它把所有逻辑都摊在会话窗口里模型每次都要重新理解一遍不同轮次还可能理解得不一样。技能 Skill 做的是另一件事它把“面对某类任务应该按什么步骤、调什么资源、在什么条件下给什么结果”这些经验包装成一个可命名、可加载、可验证的模块。简单来说提示词是零散的一句话技能则是组织好的一个生产线描述、输入、执行、校验、兜底全部齐备。Agent 技能不是新东西。往早了说它类似于早期专家系统里的“规则库”或者是编程里的“函数”。只是过去函数的调用者是人现在调用者是一个会自己规划的大模型。由于大模型是概率模型技能需要比普通函数更严格的“使用说明”才能约束它不乱来。所以技能的核心不是代码本身多复杂而是代码旁边那套描述和约束有多清楚。1.1 从“放飞的提示词”到“封闭的技能”我先说说自己早期的做法。最初我让 Agent 处理代码评审就是写了一长串提示词你是一个资深工程师请从正确性、性能、可读性三个方面评审下面的 diff并给出修改建议。听起来没问题实际用起来你会发现同一个模型今天可能认真找问题明天只回一句“改动整体看起来不错”。它对“评审”的理解完全依赖当下这段上下文没有稳定的行为基准。后来我把提示词换成一个技能固定找到 diff 来源、固定逐文件分析、固定输出字段和格式、固定质量门槛。改动看起来不大但效果差别很明显。模型的行为从“自由发挥”变成“按流程执行”该走的分支一个不少该输出的关键信息也不会漏。这背后的逻辑是大模型本身很适合做语义理解和生成但天生不擅长保证每次都走完同一套步骤。把“步骤”外置到技能里用结构去约束它正好弥补了这个短板。1.2 技能、工具函数和子代理的边界这里要把几个容易混的概念分清。工具函数是一段可以被模型调用的代码比如 search_web、read_file、git_diff特点是单一动作不关心任务目标。子代理则更像一个独立小员工有自己的模型、提示词和上下文负责一个完整子任务返回总结给主代理。技能夹在两者之间它不只是一个动作它关心“做对一件事”但它也不像子代理那样需要完整上下文它更轻、更标准通常在一轮思考内完成。我个人的判断标准是这样如果一件事只需要“调一次 API、拿一个结果”它是工具如果一件事要“多步骤、有判断、有质量要求”但范围清晰、短时间内能定义清楚它适合做成技能如果一件事复杂到需要拆出好几个技能、且需要持续对话和独立上下文那就上子代理。很多人的 Agent 越写越乱就是因为该用技能的地方堆了十个子代理该用工具的地方却塞进了一大段提示词。1.3 拆出技能模块的三个直接好处把技能拆出来最直接的好处是可控。代码评审技能只会做代码评审不会因为前面聊了几轮别的就把目标带偏。第二个好处是可验证。每个技能有明确的输入和输出我可以准备十几条测试用例定期回归不再靠“感觉这次回答不错”来判断质量。第三个好处是可组合。我今天用这套技能跑 GitHub 流程明天换到 GitLab 或者本地仓库只需要替换内部一个获取 diff 的接口就行不需要重写整个链路。这套得失其实很像写程序时“把逻辑抽成函数”刚开始多花一点时间设计边界后面省下的时间是指数级的。多技能共存的 Agent 尤其如此不然技能之间就是互相干扰的一团长提示词。想清楚这一层后面的一切都好办了。2. 一个技能包到底该装什么从描述到验证技能包怎么设计直接决定模型用得好不好。我见过很多“技能”其实只是文件名起得很厉害、内容是一段 Markdown里面没有任何可执行的约束。这类东西跑一两次还凑合放到系统里很快就会出问题。下面我把一个合格技能包应该包含的东西拆开讲。2.1 技能描述是第一个硬门槛描述是模型选择用什么技能的入口也是整个系统里性价比最高的文本。写技能的时候大多数人把时间花在实现逻辑上描述随便两句话了事。这是完全搞反了。一个合格的技能描述要解决四个问题这个技能在什么场景下触发它到底做什么、不做什么它需要什么输入它会输出什么。举个反例“帮助用户进行代码评审。”这句话基本没用因为模型不知道什么时候该用它也不知道范围。更好的写法是当用户请求涉及一个或多个代码变更文件的分析希望通过审查发现潜在问题、改进建议时使用本技能。技能只做静态分析和建议输出不直接修改代码不执行测试如果用户只需要某一行函数的解释或整体架构的高层总结请优先使用通用对话而不是本技能。这段描述把触发条件、能力边界、不做的事和替代路径都写清楚了。模型看到后调用准确率能提高一大截。我把这种写法叫“技能描述四要素”。你自己设计技能的时候可以照着这个结构先写一版再找几个真实请求测一测看它选得对不对。2.2 技能清单文件把声明和实现分开我建议每个技能用一个独立目录承载里面至少包含一个清单文件和实现文件。清单文件里写的是元信息实现文件里写的是代码或者提示模板。这样做的目的很明确模型读清单做决策系统加载实现去执行两边不会混在一起。技能清单简单可以是一个 YAML 文件我平时用的骨架长这样name: code-reviewer version: 1.2.0 description: | 当用户请求涉及一个或多个代码变更文件的审查 希望系统发现潜在问题并提出改进建议时使用本技能。 技能只做静态分析和建议输出不直接修改代码不执行测试。 input_schema: type: object properties: diff_text: type: string description: 变更文件的差异内容 repo_path: type: string description: 仓库本地路径用于读取上下文文件 required: - diff_text implementation: type: python entry: skill.py runtime_env: python3 validation: enabled: true require_fields: - issues - conclusion字段看起来不多但每个都是有用的。input_schema 是输入契约implementation 告诉系统怎么跑起来validation 是自检开关。一个技能如果描述不清楚输入是什么加载器就无法提前检查参数如果没有验证规则模型输出再烂系统也会当成功处理。这两条是技能能不能被信任的关键。2.3 输出契约和错误处理边界比功能更重要一个技能的输出最好也是结构化的。自然语言不是不行但结构化数据可以让下一步流程更好地判断结果是否达标。以代码评审技能为例我通常要求输出这样一组字段字段类型说明summarystring总体结论和建议issuesarray问题列表每项含 file、line、severity、message、reasonconclusionstring通过、需修改、不建议合并metaobject耗时代币、检查文件数等统计把输出固定住后面可以接两个动作一个自动校验器检查字段完整性和类型一个自动报告格式化器把结果转交给下游。错误处理同样要提前定义当输入缺失、diff 内容为空、或者模型输出校验不过时技能应返回什么样的失败信息。我见过不少技能在异常时让模型自由发挥结果就是它编造出根本不存在的文件路径。技能里应当有一个统一的“失败出口”一旦触发就明确告诉上层本技能在此输入下无法完成任务并说明缺什么。3. 实操做一遍搭一个代码评审技能并接入主循环理论说得再多不如完整过一遍。下面我以“代码评审”这个技能为例从零到一做一个能跑的版本再把它接到一个最简单的 Agent 主循环里。之所以选代码评审是因为它边界清楚、输入好拿、结果可验证非常适合当第一个练手技能。3.1 先拆解流程再写技能做技能的第一步不是写代码而是把人的工作流拆出来。一个工程师做代码评审大概是拿到 diff逐个文件浏览对比上下文找问题评估严重程度给结论。Agent 技能也要复刻这套流程但要让步骤更明确。我先把它拆成四步获取 diff 文本。根据 diff 中涉及的路径读取相关源码。逐个变更点分析记录问题、严重度和建议。汇总输出。拆完之后技能的骨架就出来了。我把它写成一个 Python 文件入口接收结构化参数内部顺序执行这些步骤。为了让结果稳定我在提示模板里把“什么算问题”也固定下来只关注正确性风险、性能影响、明显的可读性问题不针对代码风格争论。3.2 写实现把判定规则也交给模型但有兜底一份典型技能实现看起来是这样import json import os import re def parse_diff_files(diff_text: str) - list[str]: # 返回被修改的文件路径 return re.findall(r^\\\ b/(.)$, diff_text, flagsre.MULTILINE) def read_file_safely(path: str) - str: try: with open(path, r, encodingutf-8) as f: return f.read(20000) except OSError: return def call_llm(prompt: str, structured_output: bool True) - dict: # 接入你的模型调用 return json.loads({}) def normalize(raw, fallback_rules) - list: issues raw.get(issues, []) for rule in fallback_rules: issues rule(issues) return issues def derive_conclusion(issues: list) - str: critical [i for i in issues if i.get(severity) in (critical, major)] return 不建议合并 if len(critical) 1 else 需修改 def run_code_review(diff_text: str, repo_path: str | None None) - dict: touched_files parse_diff_files(diff_text) contexts {} if repo_path: for f in touched_files: contexts[f] read_file_safely(os.path.join(repo_path, f)) prompt build_review_prompt(diff_text, contexts) raw call_llm(prompt, structured_outputTrue) issues normalize(raw, fallback_rulesbasic_pattern_checks) meta {files_scanned: len(touched_files), issues_found: len(issues)} return ensure_schema({ summary: raw.get(summary, ), issues: issues, conclusion: derive_conclusion(issues), meta: meta, })注意几个细节。parse_diff_files 是纯函数输入输出确定可以单测。build_review_prompt 不直接拼用户输入而是把 diff 和文件内容作为数据块嵌入避免提示注入。normalize 会先把模型的输出整理成 issues 数组如果模型漏了字段fallback_rules 里的一组正则和规则会补做基础检查。也就是说模型负责理解和生成规则负责兜底。这样即使模型今天状态不好技能输出的结构也不会崩。3.3 把技能接到 Agent 运行循环里技能再好也得让 Agent 调度器认识它。我的加载流程是启动时扫描技能目录读取每个技能的清单注册到 registry运行中根据用户请求选择一个或多个技能执行后把结果的 meta 信息记录到运行日志。最简的主循环可以这样写registry load_skills(skills/) def agent(request): candidates registry.match(request) if not candidates: return general_response(request) for skill in candidates[:1]: # 示例只取最匹配的一个 if not skill.validate_inputs(request): return skill.missing_input_message() result skill.execute(request) if not skill.validate_result(result): return skill.failed_output_message() return format_result(result)这段伪代码值得盯着看的地方是两个 if。前者保证参数先校验后者保证输出后校验。很多 Agent 的问题是只调用、不回检结果模型输出了错误数据系统照单全收。在我的实践里回检这一步能让最终成功率提高三成以上。还有一个容易忽略的点registry.match 依赖的就是前面 2.1 写的描述。如果你的技能描述写成一团浆糊这里 match 出来的东西自然也不对。4. 构建技能时踩过的坑和可复用的验证套路技能搭起来不难难的是让它稳定可靠。下面这部分是我最近大半年里反复踩完又填平的坑以及我在项目里沉淀下来的一套验证方法。如果你正在往生产环境里放技能这部分应该最有用。4.1 三个最容易把技能做废的坑第一个坑是描述写得太泛。我早期有个技能叫 general_assist想让它啥都能干结果模型在需要调用具体技能时经常选它然后输出就没法控制了。后来我把这个技能删掉强制每个请求都落到具体技能或通用对话里准确率马上回升。技能描述一定要具体到能被直接检索泛化描述等于没有描述。第二个坑是在技能里写死环境依赖。比如在提示词里写了“仓库在 /home/user/project”或者“使用本地 MySQL 实例”一旦换机器就全面失效。正确做法是环境信息全部走参数技能内部只处理逻辑。我后来在技能清单里加了一层环境变量映射任何路径类信息都从参数接收兼容性好了很多。第三个坑是只建技能不做验证。我有一段时间写完技能就往生产里丢靠线上对话反馈来调结果改一次提示词历史问题解决的同时又带出两个新问题。后来我把所有技能都配了验证器和回归测试集改动必须过测试才能发布返工率立刻降下来。4.2 给技能建一套回归测试清单每个技能都值得一个属于自己的回归集。回归集不用一开始就做得很大十来条典型 case 就够了关键是要覆盖三类正常输入、边界输入、故意误导输入。以代码评审为例正常 case 是有 bug 的 diff边界是空 diff 或超大 diff误导是让技能“顺便把代码改了”之类的指令。跑回归的时候我会统计四件事指标我的目标值说明技能选择正确率不低于95%模型有没有在正确场景调用技能输入校验通过率100%非法输入必须在参数层被拦住输出结构正确率100%输出字段完整且类型正确内容质量抽查不低于90%人工抽查 issues 是否真实有效注意前两项是硬性的不达标不能上线第三项是格式层面第四项才是真正的质量评估需要人工或者规则抽样做。回归测试本质上是在给模型行为“上保险”。模型是概率系统改动一个技能实现很可能连带影响其他技能的选择所以技能一多回归更要勤快。4.3 多技能共存时我的一些管理习惯技能数量超过十几个之后管理成本会陡增。我现在给自己立了几条规则。一是一个技能只做一类边界清晰的任务宁可多拆一个技能也不要让技能内部长出多个无关分支。二是技能描述要定期清理把调用频率很低的技能重新评估该合并合并、该删除删除而不是留着让模型多一次错误选择的机会。三是每个技能要有明确版本号改动后记录变更原因便于回滚。四是软技能和硬技能分开管理软技能比如“总结讨论”“整理笔记”这类不需要额外工具、主要靠模型硬技能比如“执行部署”“修改文件”这类有真实副作用验证逻辑必须更严格。在日志方面我还会记录每个技能的调用次数、失败次数、平均耗时和 token 消耗。时间一长数据会告诉你哪个技能该优化哪个技能其实根本没人用。这些经验不复杂但都是我从代码里一个 bug 一个 bug 攒出来的。如果你正准备开始折腾技能我个人的建议是先挑一件你自己每天都做、能明确看到好坏结果的事情把它拆成一个技能不用追求功能多先把边界画清楚把验证规则写上。我最初做这个的时候也犯过贪多的毛病一次接了很多个技能结果维护不过来当我真正把一两个技能做深做实之后才感受到这玩意的价值。技能这玩意儿很像我学骑行时教练讲的那句话先学会把车骑直再学怎么爬坡。技能做直了Agent 才会有真正靠谱的成长底盘。
返回列表