写在前面

  上一章我写了一句话不知道小伙伴还记不记得,就是:同一个api下,agent能力的差别取决于system prompt的构造。我感觉这句话自己写的太精准了哈哈哈,本章就是生动应用。试想一下,当你想把不同角色设定都塞进system prompt,那上下文直接爆了,而且其他不用的提示词会浪费很多token,因为计费是根据输入和输出token一块的。所以,本章尝试使用到某一提示词设定时,只将对应设定加载进去,然后做一个拼接。全部代码详见:
https://github.com/shareAI-lab/learn-claude-code/blob/main/s07_skill_loading/code.py

  我们的任务是:
  1,实现skills.md加载的过程
  2,跑几个测试


一、跟我一起改造(基于s06)

  首先给大家看下这些prompt文件的结构。

skills/
├── agent-builder/
│   └── SKILL.md
├── code-review/
│   └── SKILL.md
├── mcp-builder/
│   └── SKILL.md
└── pdf/
    └── SKILL.md

  每个SKILL.md格式如下,就是标准的markdown,只不过name和description要用三短线夹着。
在这里插入图片描述

  将md文件的各个部分拆解出来。

# 标记一下skills所在的文件夹。
SKILLS_DIR = WORKDIR / "skills"

def _parse_frontmatter(text: str) -> tuple[dict, str]:
    """
    从输入的text中解析出第一个yaml数据,这个数据就是对应skill的提示词
    返回的meta是一个yaml字典,返回的string是
    """
    # 如果文本不是以---开头,则直接返回空字典和文本
    if not text.startswith("---"):
        return {}, text
    parts = text.split("---", 2) # 以前者分割,分割2次产生3段
    # 如果小于三部分,说明只有一个---,格式不完整
    if len(parts) < 3:
        return {}, text
    try:
        meta = yaml.safe_load(parts[1]) or {}
    except yaml.YAMLError:
        meta = {}
    return meta, parts[2].strip() 

  这里给大家解释下parts = text.split(“—”, num),这个会按照前面的字符,进行num次分段,最终产生num+1个分段,我自己实际测试了下。

text = """---
name: agent-builder
description: |
  Design and build AI agents for any domain. Use when users:
  (1) ask to "create an agent", "build an assistant", or "design an AI system"
  (2) want to understand agent architecture, agentic patterns, or autonomous AI
  (3) need help with capabilities, subagents, planning, or skill mechanisms
  (4) ask about Claude Code, Cursor, or similar agent internals
  (5) want to build agents for business, research, creative, or operational tasks
  Keywords: agent, assistant, autonomous, workflow, tool use, multi-step, orchestration
---

content

"""

def _parse_frontmatter(text: str) -> tuple[dict, str]:
    # 如果文本不是以---开头,则直接返回空字典和文本
    if not text.startswith("---"):
        return {}, text
    parts = text.split("---", 2) # 以前者分割,分割2次产生3段
    print("打印parts", parts)
    # 如果小于三部分,说明只有一个---,格式不完整
    if len(parts) < 3:
        return {}, text
    try:
        meta = yaml.safe_load(parts[1]) or {}
    except yaml.YAMLError:
        meta = {}
    
    print(f"打印这个东西{parts[2].strip()}")
    return meta, parts[2].strip() 
_parse_frontmatter(text)

输出台内容:
(env_learnclaude) bx@gs-MacBook-Air learn_cladudecode % /Users/bx/miniconda3/envs/env_learnclaude/bin/python /Users/bx/Documents/coding/learn_cladudecode/s0
7_skill_loading/test.py
打印parts ['', '\nname: agent-builder\ndescription: |\n  Design and build AI agents for any domain. Use when users:\n  (1) ask to "create an agent", "build an assistant", or "design an AI system"\n  (2) want to understand agent architecture, agentic patterns, or autonomous AI\n  (3) need help with capabilities, subagents, planning, or skill mechanisms\n  (4) ask about Claude Code, Cursor, or similar agent internals\n  (5) want to build agents for business, research, creative, or operational tasks\n  Keywords: agent, assistant, autonomous, workflow, tool use, multi-step, orchestration\n', '\n\ncontent\n\n']
打印这个东西content

  因此,解释了最后return meta, parts[2].strip() ,name和description被转化为yaml文件返回,prompt的内容作为string返回。看看接下来如何处理。

  _scan_skills()函数做的其实就是把所有skills目录下的所有SKILL.md中的name和description加载到内存中去(赋值给SKILL_REGISTRY)

# 存储已注册的技能
SKILL_REGISTRY: dict[str, dict] = {}

def _scan_skills():
    """扫描 skills/ 目录,并将每个技能的 name、description 和 content 注册到 SKILL_REGISTRY 中。"""
    if not SKILLS_DIR.exists():
        return 
    # iterdir返回所有的文件和目录
    for d in sorted(SKILLS_DIR.iterdir()):
        # 如果不是目录就直接跳过
        if not d.is_dir():
            continue
        manifest = d / "SKILL.md"
        # 如果manifest这个文件存在
        if manifest.exists():
            raw = manifest.read_text()
            meta, body = _parse_frontmatter(raw)
            # 看样子这个skill.md文件当中对每一个描述是有规定的,显然必须存在name和description,
            # 而且后续我们要取第一行,并且去处开头的#,所以看样子md中格式也是有要求的。
            name = meta.get("name", d.name)
            desc = meta.get("description", raw.split("\n")[0].lstrip("#").strip())
            SKILL_REGISTRY[name] = {"name": name, "description": desc, "content": raw}

# 如果md不更改,其实这个函数执行一次就可以了
_scan_skills()

  这两个函数做的东西就是把name和description拆除来,然后拼成name:description的形式塞到system prompt中,相当于把所有SKILL.md做了一个简单的摘要,作为输入token。

def list_skills() -> str:
    """
    列出所有的技能,(name, 一行description)
    """
    if not SKILL_REGISTRY:
        return "(no skills found)"
    return "\n".join(f"- **{s['name']}**: {s['description']}" for s in SKILL_REGISTRY.values())

def build_system() -> str:
    """
    构建系统提示词:在启动时把技能注入进去。简单的说,就是做个提示词拼接
    """
    catalog = list_skills()
    return (
        f"You are a coding agent at {WORKDIR}. "
        f"Skills available:\n{catalog}\n"
        "Use load_skill to get full details when needed."
    )

SYSTEM = build_system()

  最后, 我们把它封装成工具函数。分别在TOOLS和TOOL_HANDLERS中追加进去。

def load_skill(name: str) -> str:
    """Load full skill content. Lookup via registry — no path traversal."""
    skill = SKILL_REGISTRY.get(name)
    if not skill:
        return f"Skill not found: {name}"
    return skill["content"]

二、简单总结一下。

  逻辑就是把简短的摘要塞进去,若选择某一skill,通过load_skill()加载对应的content。这样就节省了许多token,而且若是md文件被修改,只需再执行一遍_scan_skills()就可以了。
在这里插入图片描述
  来俩任务试试。

s06 >> What skills are available?
[HOOK] UserPromptSubmit: working in /Users/bx/Documents/coding/learn_cladudecode
[HOOK] Stop: session used 0 tool calls
Based on the information I have, there are **4 skills** available:

1. **agent-builder** — Build agents
2. **code-review** — Review code
3. **mcp-builder** — Build MCP (Model Context Protocol) tools
4. **pdf** — Work with PDFs

Would you like me to load any of these skills to get more details about what they can do? Just let me know which one(s) interest you!

s06 >> I need to do a code review -- load the relevant skill first
[HOOK] UserPromptSubmit: working in /Users/bx/Documents/coding/learn_cladudecode
[HOOK] load_skill
[HOOK] glob
[HOOK] glob
[HOOK] glob
[HOOK] glob
[HOOK] glob
[HOOK] glob
[HOOK] read_file
[HOOK] bash
[HOOK] bash
[HOOK] bash
[HOOK] todo_write

## Current Tasks
  [] Load code-review skill (limited content returned — need to find source)
  [ ] Perform code review on user's codebase
[HOOK] Stop: session used 12 tool calls
It looks like the **code-review** skill didn't contain detailed content when loaded — it may not be fully installed or configured in this project yet.

However, I can still perform a thorough code review for you! Here's what I can do:

### Code Review Capabilities
- **Read and analyze** your code files for bugs, logic errors, and edge cases
- **Check** code style, naming conventions, and consistency
- **Identify** security vulnerabilities and performance issues
- **Suggest** improvements, refactoring opportunities, and best practices
- **Review** for maintainability, readability, and documentation

To get started, could you let me know:

1. **Which files or directories** you'd like me to review?
2. **Any specific concerns** you have (e.g., security, performance, style)?
3. **Any language/framework conventions** you want me to follow?

总结

  其实有观察仔细的小伙伴会发现,这里的System prompt和s06的不太一样了,区别在于这里并没有强调要做todolist,但是!仔细观察上面输出台执行过程,模型依然给出curent task,而且没有todo_write()的工具调用的提示,这真的令人费解。
在这里插入图片描述

在这里插入图片描述
  下面是个人的一些脑洞,不想看请跳过,直接下一章哈哈

  其实原因本质并不难理解,LLM就是一个概率模型。在大三时,我第一次用java的openai接口调用当时很火的function call时,我内心就有一个疑惑,如果你把调用哪个函数的选择权去交给一个概率模型,那如何保证函数调用的准确?(什么时候该调用这个函数做事,模型就调用的概率)后来这个问题不了了之。接着业界出现了大模型的MCP,这算是一种解决方案,它规定模型交互之间必须遵循协议,底层做的其实就是针对模型回答的完形填空,这让我想到报文在网络传输,每个字段每次填充等等,都要遵循协议。但我觉得MCP依旧不是上面这个问题的最佳的答案,因为网络传输中,协议不管可不可靠,我们都可以回退帧,这些都是基于信道源发出的参数都是准确的可预测的,而大模型就算是同一个问题,每次回答都是有差异的不同答案,是不准确的。

  如果硬要使用这种方法会怎么?想象如果大模型真的应用到了具身智能,你对一个机器人一直重复一个问题,他的行为并不是固定的,会不会刚开始表现正常,但重复越多,机器人越费解,会不会因此产生不可控的行为(毕竟正常的行为都被排除了,这种情况概率不是0)。

  我目前想不出优秀的解决办法,如何在工程上将一个概率模型的回答或控制的行为变得精准,期待业界的创新。

更多推荐