☰
从提示词到技能:构建稳定可靠的LLM Agent执行模块
2026/10/7 17:12:05 网站建设 项目流程

这两年做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、reason
conclusionstring通过、需修改、不建议合并
metaobject耗时代币、检查文件数等统计

把输出固定住,后面可以接两个动作:一个自动校验器检查字段完整性和类型,一个自动报告格式化器把结果转交给下游。错误处理同样要提前定义:当输入缺失、diff 内容为空、或者模型输出校验不过时,技能应返回什么样的失败信息。我见过不少技能在异常时让模型自由发挥,结果就是它编造出根本不存在的文件路径。技能里应当有一个统一的“失败出口”,一旦触发就明确告诉上层:本技能在此输入下无法完成任务,并说明缺什么。

3. 实操做一遍:搭一个代码评审技能并接入主循环

理论说得再多,不如完整过一遍。下面我以“代码评审”这个技能为例,从零到一做一个能跑的版本,再把它接到一个最简单的 Agent 主循环里。之所以选代码评审,是因为它边界清楚、输入好拿、结果可验证,非常适合当第一个练手技能。

3.1 先拆解流程,再写技能

做技能的第一步不是写代码,而是把人的工作流拆出来。一个工程师做代码评审,大概是:拿到 diff,逐个文件浏览,对比上下文,找问题,评估严重程度,给结论。Agent 技能也要复刻这套流程,但要让步骤更明确。我先把它拆成四步:

  1. 获取 diff 文本。
  2. 根据 diff 中涉及的路径读取相关源码。
  3. 逐个变更点分析,记录问题、严重度和建议。
  4. 汇总输出。

拆完之后,技能的骨架就出来了。我把它写成一个 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, flags=re.MULTILINE) def read_file_safely(path: str) -> str: try: with open(path, "r", encoding="utf-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_output=True) issues = normalize(raw, fallback_rules=basic_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 才会有真正靠谱的成长底盘。

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

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

立即咨询